core 1.5.0
Shared MPI block decomposition, asynchronous ghost-layer exchange, SDF geometry, load balancing (header-only C++20)
Loading...
Searching...
No Matches
redistribute_topology.hpp
Go to the documentation of this file.
1// core — the planned, repeatable movement of a multigrid level's fields onto a stage target and
2// back (amr/docs/amr_mg_core_boundary.md §4; the suite decision "Coarse-level redistribution lives
3// in core").
4//
5// Same split as GridHaloTopology / ParticleHaloTopology: build() (once per hierarchy build)
6// establishes who sends which cells where; forward() / backward() (every V-cycle) only move
7// payload, with no handshake. Host-staged; the buffers are a coarse level's cells.
8//
9// forward : level fields (this rank's current block) -> target fields (the target block)
10// backward : target fields -> level fields (OVERWRITES)
11//
12// LAYOUT CONTRACT. The topology never assumes where a cell lives in the caller's arrays: it calls
13// `srcIndex(gcell)` for every global cell of this rank's current block and `dstIndex(gcell)` for
14// every global cell of the target block this rank owns, once, at build, and moves values between
15// those slots. flow's padded x-fastest boxes and amr's Morton-ordered leaves use it alike.
16//
17// The kinds (stage_target.hpp):
18// SiblingMerge — MPI_Gatherv / MPI_Scatterv on the group communicator, owner = root. Each member
19// packs its block's cells in x-fastest global order and the owner unpacks member
20// by member in group order: the same permutation as flow's Telescope
21// (teleGather / teleScatterAdd; flow ADDS the scattered values into its level,
22// which is the caller's arithmetic, not the movement's).
23// Replicated — MPI_Allgatherv on the parent communicator, rank order; backward is a local
24// pick (every rank holds the identical target), as amr's ReplicatedTailStage.
25// Repartition — planned point-to-point on the parent communicator (design §11.3). The segments
26// are the box intersections of this rank's current block with every target block
27// (send) and of this rank's target block with every current block (receive), each
28// x-fastest; every rank computes all of them from the two decompositions alone,
29// so there is no handshake. One MPI_Isend / MPI_Irecv per non-empty segment, the
30// self-intersection a direct copy, MPI_Waitall before returning. backward is the
31// mirror. Each cell has one source and one destination, so the values are
32// independent of message order — bitwise those of a one-shot
33// redistributeGridFields over the same boxes.
34// Several fields move in ONE collective (one message per segment for Repartition), field-major
35// within each segment. Pure copies: every value arrives bitwise.
36#ifndef PECLET_CORE_DECOMP_REDISTRIBUTE_TOPOLOGY_HPP
37#define PECLET_CORE_DECOMP_REDISTRIBUTE_TOPOLOGY_HPP
38
39#include <algorithm>
40#include <climits>
41#include <cstddef>
42#include <stdexcept>
43#include <string>
44#include <type_traits>
45#include <vector>
46
52
53namespace peclet::core::decomp {
54
63template <int Dim, class T>
65 static_assert(std::is_trivially_copyable_v<T>, "RedistributeTopology moves T as raw bytes");
66
67 public:
69
73 static constexpr int kTagBase = 1;
75 static constexpr int kTagSpan = 10;
76
94 template <class SrcIndex, class DstIndex>
96 SrcIndex&& srcIndex, DstIndex&& dstIndex, int id = 0) {
97 if (dst.kind == StageKind::Repartition) {
98 buildRepartition(src, dst, c, srcIndex, dstIndex, id);
99 return;
100 }
102 throw std::invalid_argument("RedistributeTopology: an InPlace target has no movement");
103 if (c.kind != dst.kind)
104 throw std::invalid_argument("RedistributeTopology: StageComm was made for another target");
105 if (src.globalSize() != dst.dec.globalSize())
106 throw std::invalid_argument(
107 "RedistributeTopology: source and target decompose different grids");
108 int size = 1;
109 MPI_Comm_rank(c.parent, &rank_);
110 MPI_Comm_size(c.parent, &size);
111 if (src.numBlocks() != static_cast<std::size_t>(size))
112 throw std::invalid_argument(
113 "RedistributeTopology: the current decomposition needs one block per parent rank");
114
115 kind_ = dst.kind;
116 active_ = c.active;
117 comm_ = (kind_ == StageKind::SiblingMerge) ? c.group : c.parent;
118 srcSlots_.clear();
119 dstSlots_.clear();
120 counts_.clear();
121 displs_.clear();
122
123 const Block<Dim> mine = src.block(static_cast<std::size_t>(rank_));
124 forEachCell(mine, [&](const IVec<Dim>& g) { srcSlots_.push_back(Index(srcIndex(g))); });
125 requireDistinct(srcSlots_, "srcIndex");
126
127 // The segment layout: one segment per sending rank, in the collective's rank order (group
128 // order for SiblingMerge, parent order for Replicated), each the sender's block x-fastest.
129 auto addSegment = [&](const Block<Dim>& b) {
130 displs_.push_back(static_cast<Index>(dstSlots_.size()));
131 counts_.push_back(cellCount(b));
132 forEachCell(b, [&](const IVec<Dim>& g) { dstSlots_.push_back(Index(dstIndex(g))); });
133 };
134 if (kind_ == StageKind::SiblingMerge) {
135 if (active_) {
136 const Block<Dim> tb = dst.dec.block(static_cast<std::size_t>(c.myTargetBlock));
137 for (int m : c.members) {
138 const Block<Dim> mb = src.block(static_cast<std::size_t>(m));
139 if (!contains(tb, mb))
140 throw std::logic_error(
141 "RedistributeTopology: a member's block lies outside its target block");
142 addSegment(mb);
143 }
144 if (static_cast<Index>(dstSlots_.size()) != cellCount(tb))
145 throw std::logic_error("RedistributeTopology: the members do not tile the target block");
146 }
147 } else {
148 for (int r = 0; r < size; ++r)
149 addSegment(src.block(static_cast<std::size_t>(r)));
150 if (static_cast<Index>(dstSlots_.size()) != cellCount(dst.dec.block(0)))
151 throw std::logic_error("RedistributeTopology: the blocks do not tile the level grid");
152 }
153 requireDistinct(dstSlots_, "dstIndex");
154 }
155
157 StageKind kind() const { return kind_; }
159 bool active() const { return active_; }
161 std::size_t numSourceCells() const { return srcSlots_.size(); }
163 std::size_t numTargetCells() const { return dstSlots_.size(); }
164
173 void forward(const std::vector<const T*>& src, const std::vector<T*>& dst) {
174 const std::size_t nF = src.size();
175 if (active_ && dst.size() != nF)
176 throw std::invalid_argument("RedistributeTopology::forward: src/dst field counts differ");
177 if (kind_ == StageKind::Repartition) {
178 // send segments carry srcSlots_ from `src`, receive segments land in dstSlots_ of `dst`
179 pointToPoint(src, srcSlots_, sendSeg_, dst, dstSlots_, recvSeg_);
180 return;
181 }
182 const std::size_t n = srcSlots_.size();
183 sbuf_.resize(n * nF);
184 for (std::size_t f = 0; f < nF; ++f)
185 for (std::size_t i = 0; i < n; ++i)
186 sbuf_[f * n + i] = src[f][srcSlots_[i]];
187 const bool unpack = active_;
188 if (unpack)
189 rbuf_.resize(dstSlots_.size() * nF);
190 byteLayout(nF);
191 if (kind_ == StageKind::SiblingMerge) {
192 MPI_Gatherv(sbuf_.data(), bytes(n * nF), MPI_BYTE, unpack ? rbuf_.data() : nullptr,
193 unpack ? bc_.data() : nullptr, unpack ? bd_.data() : nullptr, MPI_BYTE, 0, comm_);
194 } else {
195 MPI_Allgatherv(sbuf_.data(), bytes(n * nF), MPI_BYTE, rbuf_.data(), bc_.data(), bd_.data(),
196 MPI_BYTE, comm_);
197 }
198 if (!unpack)
199 return;
200 for (std::size_t m = 0; m < counts_.size(); ++m) {
201 const std::size_t cnt = static_cast<std::size_t>(counts_[m]);
202 const std::size_t d0 = static_cast<std::size_t>(displs_[m]);
203 const T* q = rbuf_.data() + d0 * nF;
204 for (std::size_t f = 0; f < nF; ++f)
205 for (std::size_t i = 0; i < cnt; ++i)
206 dst[f][dstSlots_[d0 + i]] = q[f * cnt + i];
207 }
208 }
209
215 void backward(const std::vector<const T*>& dst, const std::vector<T*>& src) {
216 const std::size_t nF = src.size();
217 if (active_ && dst.size() != nF)
218 throw std::invalid_argument("RedistributeTopology::backward: src/dst field counts differ");
219 if (kind_ == StageKind::Repartition) {
220 // the receive segments go back out of dstSlots_, the send segments come home to srcSlots_
221 pointToPoint(dst, dstSlots_, recvSeg_, src, srcSlots_, sendSeg_);
222 return;
223 }
224 const std::size_t n = srcSlots_.size();
225 if (kind_ == StageKind::Replicated) {
226 const std::size_t d0 = static_cast<std::size_t>(displs_[static_cast<std::size_t>(rank_)]);
227 for (std::size_t f = 0; f < nF; ++f)
228 for (std::size_t i = 0; i < n; ++i)
229 src[f][srcSlots_[i]] = dst[f][dstSlots_[d0 + i]];
230 return;
231 }
232 byteLayout(nF);
233 if (active_) {
234 sbuf_.resize(dstSlots_.size() * nF);
235 for (std::size_t m = 0; m < counts_.size(); ++m) {
236 const std::size_t cnt = static_cast<std::size_t>(counts_[m]);
237 const std::size_t d0 = static_cast<std::size_t>(displs_[m]);
238 T* q = sbuf_.data() + d0 * nF;
239 for (std::size_t f = 0; f < nF; ++f)
240 for (std::size_t i = 0; i < cnt; ++i)
241 q[f * cnt + i] = dst[f][dstSlots_[d0 + i]];
242 }
243 }
244 rbuf_.resize(n * nF);
245 MPI_Scatterv(active_ ? sbuf_.data() : nullptr, active_ ? bc_.data() : nullptr,
246 active_ ? bd_.data() : nullptr, MPI_BYTE, rbuf_.data(), bytes(n * nF), MPI_BYTE, 0,
247 comm_);
248 for (std::size_t f = 0; f < nF; ++f)
249 for (std::size_t i = 0; i < n; ++i)
250 src[f][srcSlots_[i]] = rbuf_[f * n + i];
251 }
252
253 private:
256 struct Segment {
257 int peer;
258 Index displ, count;
259 };
260
261 template <class SrcIndex, class DstIndex>
262 void buildRepartition(const BlockDecomposer<Dim>& src, const StageTarget<Dim>& dst,
263 const StageComm& c, SrcIndex& srcIndex, DstIndex& dstIndex, int id) {
264 if (c.kind != StageKind::Repartition)
265 throw std::invalid_argument("RedistributeTopology: StageComm was made for another target");
266 if (src.globalSize() != dst.dec.globalSize())
267 throw std::invalid_argument(
268 "RedistributeTopology: source and target decompose different grids");
270 throw std::invalid_argument("RedistributeTopology: the topology id must lie in [0, " +
271 std::to_string(kTagSpan) + ")");
272 int size = 1;
273 MPI_Comm_rank(c.parent, &rank_);
274 MPI_Comm_size(c.parent, &size);
275 if (src.numBlocks() != static_cast<std::size_t>(size))
276 throw std::invalid_argument(
277 "RedistributeTopology: the current decomposition needs one block per parent rank");
278 const std::size_t npL = dst.dec.numBlocks();
279 if (npL < 1 || npL > static_cast<std::size_t>(size) || dst.ownerOf.size() != npL)
280 throw std::invalid_argument(
281 "RedistributeTopology: a Repartition target needs 1..size blocks");
282 for (std::size_t b = 0; b < npL; ++b)
283 if (dst.ownerOf[b] != static_cast<int>(b))
284 throw std::invalid_argument(
285 "RedistributeTopology: a Repartition target's owners must be the identity");
286 if (c.active != (static_cast<std::size_t>(rank_) < npL))
287 throw std::invalid_argument("RedistributeTopology: StageComm was made for another target");
288
290 active_ = c.active;
291 comm_ = c.parent;
292 tag_ = kTagBase + id;
293 srcSlots_.clear();
294 dstSlots_.clear();
295 counts_.clear();
296 displs_.clear();
297 sendSeg_.clear();
298 recvSeg_.clear();
299
300 // srcIndex once per cell of my block (block-local x-fastest), then gathered per segment.
301 const Block<Dim> mine = src.block(static_cast<std::size_t>(rank_));
302 std::vector<Index> mySlot;
303 mySlot.reserve(static_cast<std::size_t>(cellCount(mine)));
304 forEachCell(mine, [&](const IVec<Dim>& g) { mySlot.push_back(Index(srcIndex(g))); });
305 for (std::size_t t = 0; t < npL; ++t) {
307 if (!intersect(mine, dst.dec.block(t), I))
308 continue;
309 sendSeg_.push_back({static_cast<int>(t), static_cast<Index>(srcSlots_.size()), cellCount(I)});
310 forEachCell(I, [&](const IVec<Dim>& g) {
311 srcSlots_.push_back(mySlot[static_cast<std::size_t>(localLinear(g, mine))]);
312 });
313 }
314 if (static_cast<Index>(srcSlots_.size()) != cellCount(mine))
315 throw std::logic_error(
316 "RedistributeTopology: the send segments do not tile the source block");
317 requireDistinct(srcSlots_, "srcIndex");
318
319 if (active_) {
320 const Block<Dim> tb = dst.dec.block(static_cast<std::size_t>(rank_));
321 for (int s = 0; s < size; ++s) {
323 if (!intersect(tb, src.block(static_cast<std::size_t>(s)), J))
324 continue;
325 recvSeg_.push_back({s, static_cast<Index>(dstSlots_.size()), cellCount(J)});
326 forEachCell(J, [&](const IVec<Dim>& g) { dstSlots_.push_back(Index(dstIndex(g))); });
327 }
328 if (static_cast<Index>(dstSlots_.size()) != cellCount(tb))
329 throw std::logic_error(
330 "RedistributeTopology: the receive segments do not tile the target block");
331 requireDistinct(dstSlots_, "dstIndex");
332 }
333 }
334
335 // Move `out` segments (slots `outSlots` of the fields `from`) to their peers and land `in`
336 // segments (slots `inSlots` of the fields `to`) from theirs; the self segment is a direct copy.
337 // forward: out = send, in = receive; backward: the reverse. `to` is not touched when `in` is
338 // empty (an inactive rank's forward), nor `from` read when `out` is (its backward).
339 void pointToPoint(const std::vector<const T*>& from, const std::vector<Index>& outSlots,
340 const std::vector<Segment>& out, const std::vector<T*>& to,
341 const std::vector<Index>& inSlots, const std::vector<Segment>& in) {
342 const std::size_t nF = std::max(from.size(), to.size());
343 if (!out.empty() && from.size() != nF)
344 throw std::invalid_argument("RedistributeTopology: a sending rank needs every field");
345 if (!in.empty() && to.size() != nF)
346 throw std::invalid_argument("RedistributeTopology: a receiving rank needs every field");
347 sbuf_.resize(outSlots.size() * nF);
348 rbuf_.resize(inSlots.size() * nF);
349 // pack each outgoing segment, field-major within the segment
350 for (const Segment& s : out) {
351 if (s.peer == rank_)
352 continue;
353 const std::size_t cnt = static_cast<std::size_t>(s.count),
354 d0 = static_cast<std::size_t>(s.displ);
355 T* q = sbuf_.data() + d0 * nF;
356 for (std::size_t f = 0; f < nF; ++f)
357 for (std::size_t i = 0; i < cnt; ++i)
358 q[f * cnt + i] = from[f][outSlots[d0 + i]];
359 }
360 reqs_.clear();
361 reqs_.reserve(out.size() + in.size());
362 for (const Segment& s : in) {
363 if (s.peer == rank_)
364 continue;
365 reqs_.emplace_back();
366 MPI_Irecv(rbuf_.data() + static_cast<std::size_t>(s.displ) * nF,
367 bytes(static_cast<std::size_t>(s.count) * nF), MPI_BYTE, s.peer, tag_, comm_,
368 &reqs_.back());
369 }
370 for (const Segment& s : out) {
371 if (s.peer == rank_)
372 continue;
373 reqs_.emplace_back();
374 MPI_Isend(sbuf_.data() + static_cast<std::size_t>(s.displ) * nF,
375 bytes(static_cast<std::size_t>(s.count) * nF), MPI_BYTE, s.peer, tag_, comm_,
376 &reqs_.back());
377 }
378 // self: both segments enumerate (my block ∩ my target block) x-fastest, cell for cell
379 for (const Segment& so : out) {
380 if (so.peer != rank_)
381 continue;
382 for (const Segment& si : in) {
383 if (si.peer != rank_)
384 continue;
385 const std::size_t cnt = static_cast<std::size_t>(so.count);
386 for (std::size_t f = 0; f < nF; ++f)
387 for (std::size_t i = 0; i < cnt; ++i)
388 to[f][inSlots[static_cast<std::size_t>(si.displ) + i]] =
389 from[f][outSlots[static_cast<std::size_t>(so.displ) + i]];
390 }
391 }
392 if (!reqs_.empty())
393 MPI_Waitall(static_cast<int>(reqs_.size()), reqs_.data(), MPI_STATUSES_IGNORE);
394 for (const Segment& s : in) {
395 if (s.peer == rank_)
396 continue;
397 const std::size_t cnt = static_cast<std::size_t>(s.count),
398 d0 = static_cast<std::size_t>(s.displ);
399 const T* q = rbuf_.data() + d0 * nF;
400 for (std::size_t f = 0; f < nF; ++f)
401 for (std::size_t i = 0; i < cnt; ++i)
402 to[f][inSlots[d0 + i]] = q[f * cnt + i];
403 }
404 }
405
406 static bool intersect(const Block<Dim>& a, const Block<Dim>& b, Block<Dim>& out) {
407 for (int k = 0; k < Dim; ++k) {
408 const Index lo = std::max(a.origin[k], b.origin[k]);
409 const Index hi = std::min(a.origin[k] + a.size[k], b.origin[k] + b.size[k]);
410 if (hi <= lo)
411 return false;
412 out.origin[k] = lo;
413 out.size[k] = hi - lo;
414 }
415 return true;
416 }
417 // Block-local x-fastest index of a global cell of `b`.
418 static Index localLinear(const IVec<Dim>& g, const Block<Dim>& b) {
419 Index i = 0;
420 for (int k = Dim - 1; k >= 0; --k)
421 i = i * b.size[k] + (g[k] - b.origin[k]);
422 return i;
423 }
424
425 template <class F>
426 static void forEachCell(const Block<Dim>& b, F&& f) {
427 IVec<Dim> end{};
428 for (int k = 0; k < Dim; ++k)
429 end[k] = b.origin[k] + b.size[k];
430 forEachInBox<Dim>(b.origin, end, f);
431 }
432 static Index cellCount(const Block<Dim>& b) {
433 Index n = 1;
434 for (int k = 0; k < Dim; ++k)
435 n *= b.size[k];
436 return n;
437 }
438 static bool contains(const Block<Dim>& outer, const Block<Dim>& in) {
439 for (int k = 0; k < Dim; ++k)
440 if (in.origin[k] < outer.origin[k] ||
441 in.origin[k] + in.size[k] > outer.origin[k] + outer.size[k])
442 return false;
443 return true;
444 }
445 static void requireDistinct(std::vector<Index> v, const char* what) {
446 std::sort(v.begin(), v.end());
447 if (std::adjacent_find(v.begin(), v.end()) != v.end())
448 throw std::invalid_argument(std::string("RedistributeTopology: ") + what +
449 " maps two cells to one slot");
450 }
451 static int bytes(std::size_t elems) {
452 const std::size_t b = elems * sizeof(T);
453 if (b > static_cast<std::size_t>(INT_MAX))
454 throw std::overflow_error("RedistributeTopology: a message exceeds INT_MAX bytes");
455 return static_cast<int>(b);
456 }
457 // Per-segment byte counts / displacements for nF fields (segments are contiguous, field-major).
458 void byteLayout(std::size_t nF) {
459 bc_.resize(counts_.size());
460 bd_.resize(counts_.size());
461 for (std::size_t m = 0; m < counts_.size(); ++m) {
462 bc_[m] = bytes(static_cast<std::size_t>(counts_[m]) * nF);
463 bd_[m] = bytes(static_cast<std::size_t>(displs_[m]) * nF);
464 }
465 }
466
468 bool active_ = false;
469 int rank_ = 0;
470 int tag_ = kTagBase; // Repartition only
471 MPI_Comm comm_ = MPI_COMM_NULL;
472 std::vector<Index> srcSlots_; // this rank's current-block cells, x-fastest -> caller slot
473 std::vector<Index> dstSlots_; // the target block's cells, segment by segment -> caller slot
474 // (Repartition: srcSlots_ too is segment by segment)
475 std::vector<Index> counts_, displs_; // per segment (cells), active ranks only for SiblingMerge
476 std::vector<int> bc_, bd_; // the same in bytes for the current field count
477 std::vector<Segment> sendSeg_, recvSeg_; // Repartition: runs of srcSlots_ / dstSlots_ by peer
478 std::vector<MPI_Request> reqs_;
479 std::vector<T> sbuf_, rbuf_;
480};
481
482} // namespace peclet::core::decomp
483
484#endif // PECLET_CORE_DECOMP_REDISTRIBUTE_TOPOLOGY_HPP
The planned, repeatable movement of a multigrid level's fields onto a stage target and back (the file...
StageKind kind() const
The kind of the target this topology was built for (InPlace before build()).
bool active() const
Whether this rank holds target fields (dst in forward, the input of backward).
std::size_t numSourceCells() const
Cells of this rank's current block (the extent srcIndex was called on).
static constexpr int kTagSpan
Number of distinct ids, hence the tags kTagBase .. kTagBase + kTagSpan - 1.
void backward(const std::vector< const T * > &dst, const std::vector< T * > &src)
Target -> level, overwriting src[f] on this rank's current block.
void forward(const std::vector< const T * > &src, const std::vector< T * > &dst)
Level -> target.
void build(const BlockDecomposer< Dim > &src, const StageTarget< Dim > &dst, const StageComm &c, SrcIndex &&srcIndex, DstIndex &&dstIndex, int id=0)
Establish the movement from src (the level's current decomposition; parent rank r owns block r) to ds...
std::size_t numTargetCells() const
Cells of this rank's target block (0 on an inactive rank).
static constexpr int kTagBase
Repartition's point-to-point tags: kTagBase + id for the per-topology id passed to build(),...
int MPI_Comm_size(MPI_Comm, int *s)
Definition mpi_stub.hpp:70
#define MPI_COMM_NULL
Definition mpi_stub.hpp:28
int MPI_Comm
Definition mpi_stub.hpp:17
int MPI_Waitall(int, MPI_Request *, MPI_Status *)
Definition mpi_stub.hpp:160
#define MPI_BYTE
Definition mpi_stub.hpp:38
#define MPI_STATUSES_IGNORE
Definition mpi_stub.hpp:32
int MPI_Isend(const void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:132
int MPI_Comm_rank(MPI_Comm, int *r)
Definition mpi_stub.hpp:66
int MPI_Irecv(void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:135
StageKind
How a multigrid level moves when it can no longer coarsen in place (see chooseStageTarget and the fil...
@ Repartition
a fresh proportional ORB of the level grid on the first np_L parent ranks
@ InPlace
no stage: the level coarsens on its current decomposition
@ SiblingMerge
ORB tree truncated: contiguous runs of blocks merge onto their lowest rank.
@ Replicated
the whole level as one block, held identically on every rank
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26
std::int64_t Index
Signed index type for grids and particles (supersedes block_decomposer's long int IndxT).
Definition types.hpp:15
The communicators of one coarse-level multigrid stage, made by makeStageComm (the file comment spells...
int myTargetBlock
the target block this rank owns, or -1
std::vector< int > members
Parent ranks of this rank's group, in group-comm order (owner first); empty for Repartition.
bool active
this rank owns a target block
StageKind kind
the kind of the StageTarget this was made for
MPI_Comm group
this rank's target block's members; owner = group rank 0