core 1.5.0
Shared MPI block decomposition, asynchronous ghost-layer exchange, SDF geometry, load balancing (header-only C++20)
Loading...
Searching...
No Matches
grid_halo_topology.hpp
Go to the documentation of this file.
1// core — asynchronous ghost-layer exchange for a block-decomposed structured grid.
2//
3// Design (see suite/docs/INTERFACES.md): TOPOLOGY is separated from EXCHANGE.
4// * buildTopology() (done once, or on re-decomposition) figures out, for every ghost cell of this
5// rank's block, which rank owns the corresponding global cell (with periodic wrap), then does a
6// single NBX round so each owner learns which of its inner cells to send. The result is matched
7// send/recv index lists plus a local self-copy list (for periodic wrap onto one's own block).
8// * exchange*(field) (done every step) just moves payload. It is FIELD-AGNOSTIC: any type with
9// bytesPerElem()/pack(localIdx,dst)/unpack(localIdx,src) works, so a scalar grid field, a
10// vector grid field, or (later) a particle attribute array all flow through one path.
11//
12// Two interchangeable engines, identical results:
13// exchangeNbx(field) — nonblocking-consensus; robust for dynamic/sparse patterns.
14// exchangePersistent(field) — MPI_Neighbor_alltoallv on a cached distributed-graph communicator;
15// fastest for the STATIC neighbour pattern of a fixed grid.
16// Both support compute/comm overlap via start()/wait() (NBX) — see exchangeNbx.
17#ifndef PECLET_CORE_HALO_GRID_HALO_TOPOLOGY_HPP
18#define PECLET_CORE_HALO_GRID_HALO_TOPOLOGY_HPP
19
20#include <cstring>
21#include <map>
22#include <stdexcept>
23#include <string>
24#include <vector>
25
31
32namespace peclet::core::halo {
33
35template <typename T>
38 std::size_t bytesPerElem() const { return sizeof(T); }
39 void pack(Index localIdx, char* dst) const { std::memcpy(dst, &data[localIdx], sizeof(T)); }
40 void unpack(Index localIdx, const char* src) { std::memcpy(&data[localIdx], src, sizeof(T)); }
41 // Accumulate (for reverseAdd — folding ghost-layer deposits back onto the owner).
42 void addFrom(Index localIdx, const char* src) {
43 T v;
44 std::memcpy(&v, src, sizeof(T));
45 data[localIdx] += v;
46 }
47};
48
49template <int Dim>
51 public:
52 GridHaloTopology() = default;
53
56 void buildTopology(const decomp::BlockDecomposer<Dim>& dec, int rank, int ghostWidth,
57 const std::array<bool, Dim>& periodic, MPI_Comm comm = MPI_COMM_WORLD) {
58 comm_ = comm;
59 rank_ = rank;
60 indexer_.init(dec.origins()[rank], dec.sizes()[rank], ghostWidth);
61 clear();
62
63 // For each ghost cell, find the owner of its (wrapped) global cell.
64 std::map<int, std::vector<Index>> recvIdxByRank; // local ghost cell indices to fill
65 std::map<int, std::vector<Index>> recvGlobByRank; // the global cells we need from that owner
66 const IVec<Dim>& gsize = dec.globalSize();
67
68 indexer_.forEachAll([&](const IVec<Dim>& lmd) {
69 if (indexer_.isInner(lmd))
70 return;
71 IVec<Dim> g{}, gw{};
72 bool skip = false;
73 for (int i = 0; i < Dim; ++i) {
74 g[i] = lmd[i] + indexer_.originInclGhost()[i];
75 Index c = g[i];
76 if (c < 0 || c >= gsize[i]) {
77 if (periodic[i]) {
78 c = wrap(c, gsize[i]);
79 } else {
80 skip = true;
81 break;
82 }
83 }
84 gw[i] = c;
85 }
86 if (skip)
87 return; // non-periodic physical boundary: BC fills these, no comm
88 int owner = dec.ownerOf(gw);
89 Index ghostLocal = indexer_.localMdToLocal(lmd);
90 if (owner == rank_) {
91 // Periodic wrap onto our own block (e.g. single block, periodic): pure local copy.
92 selfDst_.push_back(ghostLocal);
93 selfSrc_.push_back(indexer_.globalToLocal(gw));
94 } else {
95 recvIdxByRank[owner].push_back(ghostLocal);
96 recvGlobByRank[owner].push_back(dec.linearGlobal(gw));
97 }
98 });
99
100 for (auto& [r, idx] : recvIdxByRank) {
101 recvRanks_.push_back(r);
102 recvIdx_.push_back(std::move(idx));
103 recvGlob_.push_back(std::move(recvGlobByRank[r]));
104 }
105
106 // One NBX round: tell each owner which global cells we need; learn what we must send.
107 NbxEngine nbx(comm_);
108 std::size_t k = 0;
109 auto packNext = [&](std::vector<char>& out) -> int {
110 if (k >= recvRanks_.size())
111 return -1;
112 int dest = recvRanks_[k];
113 const auto& g = recvGlob_[k];
114 ++k;
115 out.resize(g.size() * sizeof(Index));
116 std::memcpy(out.data(), g.data(), out.size());
117 return dest;
118 };
119 auto onRecv = [&](int src, std::vector<char>& msg) {
120 std::size_t n = msg.size() / sizeof(Index);
121 const Index* g = reinterpret_cast<const Index*>(msg.data());
122 std::vector<Index> sidx(n);
123 for (std::size_t i = 0; i < n; ++i) {
124 sidx[i] = indexer_.globalToLocal(dec.multiGlobal(g[i]));
125 }
126 sendRanks_.push_back(src);
127 sendIdx_.push_back(std::move(sidx));
128 };
129 nbx.exchange(packNext, onRecv, /*tag=*/7301);
130
131 // The pattern is symmetric by construction (every requested cell has exactly one owner), so
132 // the global number of cells promised equals the number requested. One allreduce; a mismatch
133 // means the consensus round lost a message, which would otherwise surface as a hang in the
134 // first exchange with nothing to say which rank never learned it had to send.
135 {
136 long cnt[2] = {0, 0};
137 for (const auto& g : recvGlob_)
138 cnt[0] += static_cast<long>(g.size());
139 for (const auto& sidx : sendIdx_)
140 cnt[1] += static_cast<long>(sidx.size());
141 long tot[2] = {0, 0};
142 MPI_Allreduce(cnt, tot, 2, MPI_LONG, MPI_SUM, comm_);
143 if (tot[0] != tot[1])
144 throw std::runtime_error("peclet.core GridHaloTopology: asymmetric halo topology (" +
145 std::to_string(tot[0]) + " cells requested, " +
146 std::to_string(tot[1]) +
147 " promised) -- the NBX round lost messages");
148 }
149
150 buildRecvLookup();
151 persistentReady_ = false;
152 }
153
154 // --- Field-agnostic exchange (NBX engine, with overlap support)
155 // ---------------------------------
156
158 template <typename Field>
159 void start(Field& field, int tag = 0) {
160 applySelfCopy(field);
161 const std::size_t es = field.bytesPerElem();
162
163 // Post receives first.
164 recvReqs_.assign(recvRanks_.size(), MPI_REQUEST_NULL);
165 recvBufs_.resize(recvRanks_.size());
166 for (std::size_t k = 0; k < recvRanks_.size(); ++k) {
167 recvBufs_[k].resize(recvIdx_[k].size() * es);
168 MPI_Irecv(recvBufs_[k].data(), static_cast<int>(recvBufs_[k].size()), MPI_BYTE, recvRanks_[k],
169 tag, comm_, &recvReqs_[k]);
170 }
171 // Pack and post sends.
172 sendReqs_.assign(sendRanks_.size(), MPI_REQUEST_NULL);
173 sendBufs_.resize(sendRanks_.size());
174 for (std::size_t k = 0; k < sendRanks_.size(); ++k) {
175 auto& buf = sendBufs_[k];
176 buf.resize(sendIdx_[k].size() * es);
177 for (std::size_t i = 0; i < sendIdx_[k].size(); ++i) {
178 field.pack(sendIdx_[k][i], buf.data() + i * es);
179 }
180 MPI_Isend(buf.data(), static_cast<int>(buf.size()), MPI_BYTE, sendRanks_[k], tag, comm_,
181 &sendReqs_[k]);
182 }
183 pendingTag_ = tag;
184 }
185
187 template <typename Field>
188 void wait(Field& field) {
189 const std::size_t es = field.bytesPerElem();
190 // Unpack receives as they arrive.
191 for (std::size_t done = 0; done < recvReqs_.size(); ++done) {
192 int k = 0;
193 MPI_Waitany(static_cast<int>(recvReqs_.size()), recvReqs_.data(), &k, MPI_STATUS_IGNORE);
194 if (k == MPI_UNDEFINED)
195 break;
196 for (std::size_t i = 0; i < recvIdx_[k].size(); ++i) {
197 field.unpack(recvIdx_[k][i], recvBufs_[k].data() + i * es);
198 }
199 }
200 if (!sendReqs_.empty()) {
201 MPI_Waitall(static_cast<int>(sendReqs_.size()), sendReqs_.data(), MPI_STATUSES_IGNORE);
202 }
203 }
204
206 template <typename Field>
207 void exchangeNbx(Field& field, int tag = 0) {
208 start(field, tag);
209 wait(field);
210 }
211
212 // --- Field-agnostic exchange (persistent neighborhood collective)
213 // -------------------------------
214
217 template <typename Field>
219 applySelfCopy(field);
220 const std::size_t es = field.bytesPerElem();
221 ensureGraphComm(es);
222
223 // Pack the contiguous send buffer in destination order.
224 for (std::size_t k = 0; k < sendRanks_.size(); ++k) {
225 char* dst = graphSendBuf_.data() + graphSendDispl_[k];
226 for (std::size_t i = 0; i < sendIdx_[k].size(); ++i) {
227 field.pack(sendIdx_[k][i], dst + i * es);
228 }
229 }
230 MPI_Neighbor_alltoallv(graphSendBuf_.data(), graphSendCnt_.data(), graphSendDispl_.data(),
231 MPI_BYTE, graphRecvBuf_.data(), graphRecvCnt_.data(),
232 graphRecvDispl_.data(), MPI_BYTE, graphComm_);
233 // Scatter the contiguous recv buffer into ghost cells (source order == recvRanks_ order).
234 for (std::size_t k = 0; k < recvRanks_.size(); ++k) {
235 const char* src = graphRecvBuf_.data() + graphRecvDispl_[k];
236 for (std::size_t i = 0; i < recvIdx_[k].size(); ++i) {
237 field.unpack(recvIdx_[k][i], src + i * es);
238 }
239 }
240 }
241
242 // Reverse (transpose) exchange with ACCUMULATE — the adjoint of the forward exchange: each rank
243 // sends its GHOST-cell values back to the owners, which ADD them into the matching owned cells
244 // (and the periodic self-copy folds ghost -> inner with +=). This is the fold a scatter/deposit
245 // needs across rank boundaries (CFD-DEM void-fraction / momentum feedback that lands in ghost
246 // cells). The Field must provide addFrom(localIdx, src) in addition to pack. Point-to-point
247 // (Isend/Irecv) so it needs no second graph comm.
248 template <typename Field>
249 void reverseAdd(Field& field, int tag = 0) {
250 const std::size_t es = field.bytesPerElem();
251 // self: owner (selfSrc) += ghost (selfDst)
252 std::vector<char> tmp(es);
253 for (std::size_t i = 0; i < selfSrc_.size(); ++i) {
254 field.pack(selfDst_[i], tmp.data());
255 field.addFrom(selfSrc_[i], tmp.data());
256 }
257 // I receive owner-updates for my SENT cells (sendIdx_) from the ranks I forward-send to.
258 std::vector<std::vector<char>> rbuf(sendRanks_.size());
259 std::vector<MPI_Request> rreq(sendRanks_.size(), MPI_REQUEST_NULL);
260 for (std::size_t k = 0; k < sendRanks_.size(); ++k) {
261 rbuf[k].resize(sendIdx_[k].size() * es);
262 MPI_Irecv(rbuf[k].data(), static_cast<int>(rbuf[k].size()), MPI_BYTE, sendRanks_[k], tag,
263 comm_, &rreq[k]);
264 }
265 // I send my GHOST cells (recvIdx_) back to the owners they came from (recvRanks_).
266 std::vector<std::vector<char>> sbuf(recvRanks_.size());
267 std::vector<MPI_Request> sreq(recvRanks_.size(), MPI_REQUEST_NULL);
268 for (std::size_t k = 0; k < recvRanks_.size(); ++k) {
269 sbuf[k].resize(recvIdx_[k].size() * es);
270 for (std::size_t i = 0; i < recvIdx_[k].size(); ++i)
271 field.pack(recvIdx_[k][i], sbuf[k].data() + i * es);
272 MPI_Isend(sbuf[k].data(), static_cast<int>(sbuf[k].size()), MPI_BYTE, recvRanks_[k], tag,
273 comm_, &sreq[k]);
274 }
275 // As owner-updates arrive, accumulate into my owned (sendIdx_) cells.
276 for (std::size_t done = 0; done < sendRanks_.size(); ++done) {
277 int k = 0;
278 MPI_Waitany(static_cast<int>(rreq.size()), rreq.data(), &k, MPI_STATUS_IGNORE);
279 if (k == MPI_UNDEFINED)
280 break;
281 for (std::size_t i = 0; i < sendIdx_[k].size(); ++i)
282 field.addFrom(sendIdx_[k][i], rbuf[k].data() + i * es);
283 }
284 if (!sreq.empty())
285 MPI_Waitall(static_cast<int>(sreq.size()), sreq.data(), MPI_STATUSES_IGNORE);
286 }
287
288 // --- Flattened topology (contiguous, device-friendly; consumed by the CUDA exchange) ---
291 std::vector<int> sendRanks, recvRanks; // neighbour ranks
292 std::vector<int> sendCounts, recvCounts; // elements per neighbour (same order)
293 std::vector<Index> sendIdx, recvIdx; // flattened local indices, grouped by neighbour
294 std::vector<Index> selfSrc, selfDst; // local periodic self-copy (inner -> ghost)
295 };
296
299 t.comm = comm_;
300 t.sendRanks = std::vector<int>(sendRanks_.begin(), sendRanks_.end());
301 t.recvRanks = std::vector<int>(recvRanks_.begin(), recvRanks_.end());
302 for (const auto& v : sendIdx_) {
303 t.sendCounts.push_back(static_cast<int>(v.size()));
304 t.sendIdx.insert(t.sendIdx.end(), v.begin(), v.end());
305 }
306 for (const auto& v : recvIdx_) {
307 t.recvCounts.push_back(static_cast<int>(v.size()));
308 t.recvIdx.insert(t.recvIdx.end(), v.begin(), v.end());
309 }
310 t.selfSrc = selfSrc_;
311 t.selfDst = selfDst_;
312 return t;
313 }
314
315 // --- Introspection (used by tests/benchmarks) ---
316 const decomp::BlockIndexer<Dim>& indexer() const { return indexer_; }
317 std::size_t numNeighbors() const { return recvRanks_.size(); }
318 std::size_t numGhostRecv() const {
319 std::size_t n = 0;
320 for (auto& v : recvIdx_)
321 n += v.size();
322 return n;
323 }
324
325 ~GridHaloTopology() { freeGraphComm(); }
326
329
330 private:
331 // Free the cached graph communicator, but never after MPI_Finalize (e.g. if this object outlives
332 // finalize on the stack of main()). Calling MPI_Comm_free post-finalize aborts the job.
333 void freeGraphComm() {
334 if (graphComm_ == MPI_COMM_NULL)
335 return;
336 int finalized = 0;
338 if (!finalized)
339 MPI_Comm_free(&graphComm_);
340 graphComm_ = MPI_COMM_NULL;
341 }
342
343 void clear() {
344 sendRanks_.clear();
345 sendIdx_.clear();
346 recvRanks_.clear();
347 recvIdx_.clear();
348 recvGlob_.clear();
349 selfSrc_.clear();
350 selfDst_.clear();
351 freeGraphComm();
352 persistentReady_ = false;
353 }
354
355 void buildRecvLookup() {
356 recvRankPos_.clear();
357 for (std::size_t k = 0; k < recvRanks_.size(); ++k)
358 recvRankPos_[recvRanks_[k]] = k;
359 }
360
361 template <typename Field>
362 void applySelfCopy(Field& field) {
363 // Periodic copy within our own block: read inner cell, write ghost cell. We go through the
364 // field's pack/unpack so it works for any payload type.
365 if (selfSrc_.empty())
366 return;
367 std::vector<char> tmp(field.bytesPerElem());
368 for (std::size_t i = 0; i < selfSrc_.size(); ++i) {
369 field.pack(selfSrc_[i], tmp.data());
370 field.unpack(selfDst_[i], tmp.data());
371 }
372 }
373
374 void ensureGraphComm(std::size_t es) {
375 if (persistentReady_ && graphElemSize_ == es)
376 return;
377 if (graphComm_ != MPI_COMM_NULL)
378 MPI_Comm_free(&graphComm_);
379
380 // Distributed graph: sources = ranks we receive from, destinations = ranks we send to.
381 std::vector<int> sources(recvRanks_.begin(), recvRanks_.end());
382 std::vector<int> dests(sendRanks_.begin(), sendRanks_.end());
383 MPI_Dist_graph_create_adjacent(comm_, static_cast<int>(sources.size()), sources.data(),
384 MPI_UNWEIGHTED, static_cast<int>(dests.size()), dests.data(),
385 MPI_UNWEIGHTED, MPI_INFO_NULL, /*reorder=*/0, &graphComm_);
386
387 // Counts/displacements (in bytes) in the graph's neighbour ordering, which for
388 // create_adjacent matches the order we passed sources/dests.
389 graphSendCnt_.resize(dests.size());
390 graphSendDispl_.resize(dests.size());
391 int off = 0;
392 for (std::size_t k = 0; k < dests.size(); ++k) {
393 graphSendCnt_[k] = static_cast<int>(sendIdx_[k].size() * es);
394 graphSendDispl_[k] = off;
395 off += graphSendCnt_[k];
396 }
397 graphSendBuf_.resize(off);
398
399 graphRecvCnt_.resize(sources.size());
400 graphRecvDispl_.resize(sources.size());
401 off = 0;
402 for (std::size_t k = 0; k < sources.size(); ++k) {
403 graphRecvCnt_[k] = static_cast<int>(recvIdx_[k].size() * es);
404 graphRecvDispl_[k] = off;
405 off += graphRecvCnt_[k];
406 }
407 graphRecvBuf_.resize(off);
408
409 graphElemSize_ = es;
410 persistentReady_ = true;
411 }
412
413 MPI_Comm comm_ = MPI_COMM_WORLD;
414 int rank_ = 0;
415 decomp::BlockIndexer<Dim> indexer_;
416
417 // Matched topology: element i of send list k corresponds to element i of the peer's recv list.
418 std::vector<int> sendRanks_;
419 std::vector<std::vector<Index>> sendIdx_; // local inner-cell indices to send
420 std::vector<int> recvRanks_;
421 std::vector<std::vector<Index>> recvIdx_; // local ghost-cell indices to fill
422 std::vector<std::vector<Index>> recvGlob_; // global cells requested (build-time only)
423 std::map<int, std::size_t> recvRankPos_;
424 std::vector<Index> selfSrc_, selfDst_; // local periodic self-copy (inner -> ghost)
425
426 // NBX exchange scratch.
427 std::vector<MPI_Request> sendReqs_, recvReqs_;
428 std::vector<std::vector<char>> sendBufs_, recvBufs_;
429 int pendingTag_ = 0;
430
431 // Persistent neighborhood-collective state.
432 MPI_Comm graphComm_ = MPI_COMM_NULL;
433 bool persistentReady_ = false;
434 std::size_t graphElemSize_ = 0;
435 std::vector<int> graphSendCnt_, graphSendDispl_, graphRecvCnt_, graphRecvDispl_;
436 std::vector<char> graphSendBuf_, graphRecvBuf_;
437};
438
439} // namespace peclet::core::halo
440
441#endif // PECLET_CORE_HALO_GRID_HALO_TOPOLOGY_HPP
Orthogonal recursive bisection of a Dim-dimensional global cell grid into numBlocks axis-aligned bloc...
const std::vector< IVec< Dim > > & sizes() const
Extent in cells of every block, indexed by block.
const std::vector< IVec< Dim > > & origins() const
Inclusive lower corner of every block, in global cell coordinates, indexed by block.
const IVec< Dim > & globalSize() const
Global grid size in cells per axis.
Index linearGlobal(const IVec< Dim > &g) const
Global multi-index -> global linear index (x-fastest: I = x + y*nx + z*nx*ny).
int ownerOf(const IVec< Dim > &g) const
Owning block index of a global cell coordinate. Caller must wrap into [0, globalSize) first.
IVec< Dim > multiGlobal(Index lin) const
Global linear index -> global multi-index (inverse of linearGlobal).
void start(Field &field, int tag=0)
Post all sends/recvs; returns immediately so the caller can compute the block interior.
void reverseAdd(Field &field, int tag=0)
void buildTopology(const decomp::BlockDecomposer< Dim > &dec, int rank, int ghostWidth, const std::array< bool, Dim > &periodic, MPI_Comm comm=MPI_COMM_WORLD)
Build the halo topology for rank's block of dec, with the given ghost width and per-axis periodicity.
const decomp::BlockIndexer< Dim > & indexer() const
void exchangeNbx(Field &field, int tag=0)
Convenience: full blocking exchange via the NBX-style engine (start + wait).
void wait(Field &field)
Complete a started exchange: wait for receives and scatter them into the ghost cells.
GridHaloTopology(const GridHaloTopology &)=delete
GridHaloTopology & operator=(const GridHaloTopology &)=delete
void exchangePersistent(Field &field)
Full exchange via MPI_Neighbor_alltoallv on a cached distributed-graph communicator.
#define MPI_COMM_NULL
Definition mpi_stub.hpp:28
int MPI_Allreduce(const void *sbuf, void *rbuf, int count, MPI_Datatype dt, MPI_Op, MPI_Comm)
Definition mpi_stub.hpp:114
int MPI_Comm
Definition mpi_stub.hpp:17
#define MPI_COMM_WORLD
Definition mpi_stub.hpp:27
int MPI_Waitall(int, MPI_Request *, MPI_Status *)
Definition mpi_stub.hpp:160
#define MPI_REQUEST_NULL
Definition mpi_stub.hpp:29
#define MPI_UNWEIGHTED
Definition mpi_stub.hpp:35
int MPI_Comm_free(MPI_Comm *)
Definition mpi_stub.hpp:107
#define MPI_STATUS_IGNORE
Definition mpi_stub.hpp:31
#define MPI_BYTE
Definition mpi_stub.hpp:38
#define MPI_STATUSES_IGNORE
Definition mpi_stub.hpp:32
int MPI_Waitany(int, MPI_Request *, int *idx, MPI_Status *)
Definition mpi_stub.hpp:163
int MPI_Isend(const void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:132
int MPI_Finalized(int *f)
Definition mpi_stub.hpp:58
#define MPI_INFO_NULL
Definition mpi_stub.hpp:30
#define MPI_SUM
Definition mpi_stub.hpp:43
#define MPI_LONG
Definition mpi_stub.hpp:40
int MPI_Irecv(void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:135
int MPI_Neighbor_alltoallv(const void *, const int *, const int *, MPI_Datatype, void *, const int *, const int *, MPI_Datatype, MPI_Comm)
Definition mpi_stub.hpp:175
int MPI_Dist_graph_create_adjacent(MPI_Comm c, int, const int *, const int *, int, const int *, const int *, MPI_Info, int, MPI_Comm *out)
Definition mpi_stub.hpp:170
#define MPI_UNDEFINED
Definition mpi_stub.hpp:34
Index wrap(Index x, Index n)
Periodic wrap of a coordinate into [0, n): wrap(x, n) = (x % n + n) % n.
Definition types.hpp:37
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
A contiguous local array of T (one per cell of the extended block) viewed as a packable field.
void addFrom(Index localIdx, const char *src)
void pack(Index localIdx, char *dst) const
void unpack(Index localIdx, const char *src)