core
Shared MPI block decomposition + asynchronous ghost-layer exchange (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 <vector>
23
29
30namespace peclet::core::halo {
31
33template <typename T>
36 std::size_t bytesPerElem() const { return sizeof(T); }
37 void pack(Index localIdx, char* dst) const { std::memcpy(dst, &data[localIdx], sizeof(T)); }
38 void unpack(Index localIdx, const char* src) { std::memcpy(&data[localIdx], src, sizeof(T)); }
39 // Accumulate (for reverseAdd — folding ghost-layer deposits back onto the owner).
40 void addFrom(Index localIdx, const char* src) {
41 T v;
42 std::memcpy(&v, src, sizeof(T));
43 data[localIdx] += v;
44 }
45};
46
47template <int Dim>
49 public:
50 GridHaloTopology() = default;
51
54 void buildTopology(const decomp::BlockDecomposer<Dim>& dec, int rank, int ghostWidth,
55 const std::array<bool, Dim>& periodic, MPI_Comm comm = MPI_COMM_WORLD) {
56 comm_ = comm;
57 rank_ = rank;
58 indexer_.init(dec.origins()[rank], dec.sizes()[rank], ghostWidth);
59 clear();
60
61 // For each ghost cell, find the owner of its (wrapped) global cell.
62 std::map<int, std::vector<Index>> recvIdxByRank; // local ghost cell indices to fill
63 std::map<int, std::vector<Index>> recvGlobByRank; // the global cells we need from that owner
64 const IVec<Dim>& gsize = dec.globalSize();
65
66 indexer_.forEachAll([&](const IVec<Dim>& lmd) {
67 if (indexer_.isInner(lmd))
68 return;
69 IVec<Dim> g{}, gw{};
70 bool skip = false;
71 for (int i = 0; i < Dim; ++i) {
72 g[i] = lmd[i] + indexer_.originInclGhost()[i];
73 Index c = g[i];
74 if (c < 0 || c >= gsize[i]) {
75 if (periodic[i]) {
76 c = wrap(c, gsize[i]);
77 } else {
78 skip = true;
79 break;
80 }
81 }
82 gw[i] = c;
83 }
84 if (skip)
85 return; // non-periodic physical boundary: BC fills these, no comm
86 int owner = dec.ownerOf(gw);
87 Index ghostLocal = indexer_.localMdToLocal(lmd);
88 if (owner == rank_) {
89 // Periodic wrap onto our own block (e.g. single block, periodic): pure local copy.
90 selfDst_.push_back(ghostLocal);
91 selfSrc_.push_back(indexer_.globalToLocal(gw));
92 } else {
93 recvIdxByRank[owner].push_back(ghostLocal);
94 recvGlobByRank[owner].push_back(dec.linearGlobal(gw));
95 }
96 });
97
98 for (auto& [r, idx] : recvIdxByRank) {
99 recvRanks_.push_back(r);
100 recvIdx_.push_back(std::move(idx));
101 recvGlob_.push_back(std::move(recvGlobByRank[r]));
102 }
103
104 // One NBX round: tell each owner which global cells we need; learn what we must send.
105 NbxEngine nbx(comm_);
106 std::size_t k = 0;
107 auto packNext = [&](std::vector<char>& out) -> int {
108 if (k >= recvRanks_.size())
109 return -1;
110 int dest = recvRanks_[k];
111 const auto& g = recvGlob_[k];
112 ++k;
113 out.resize(g.size() * sizeof(Index));
114 std::memcpy(out.data(), g.data(), out.size());
115 return dest;
116 };
117 auto onRecv = [&](int src, std::vector<char>& msg) {
118 std::size_t n = msg.size() / sizeof(Index);
119 const Index* g = reinterpret_cast<const Index*>(msg.data());
120 std::vector<Index> sidx(n);
121 for (std::size_t i = 0; i < n; ++i) {
122 sidx[i] = indexer_.globalToLocal(dec.multiGlobal(g[i]));
123 }
124 sendRanks_.push_back(src);
125 sendIdx_.push_back(std::move(sidx));
126 };
127 nbx.exchange(packNext, onRecv, /*tag=*/7301);
128
129 buildRecvLookup();
130 persistentReady_ = false;
131 }
132
133 // --- Field-agnostic exchange (NBX engine, with overlap support)
134 // ---------------------------------
135
137 template <typename Field>
138 void start(Field& field, int tag = 0) {
139 applySelfCopy(field);
140 const std::size_t es = field.bytesPerElem();
141
142 // Post receives first.
143 recvReqs_.assign(recvRanks_.size(), MPI_REQUEST_NULL);
144 recvBufs_.resize(recvRanks_.size());
145 for (std::size_t k = 0; k < recvRanks_.size(); ++k) {
146 recvBufs_[k].resize(recvIdx_[k].size() * es);
147 MPI_Irecv(recvBufs_[k].data(), static_cast<int>(recvBufs_[k].size()), MPI_BYTE, recvRanks_[k],
148 tag, comm_, &recvReqs_[k]);
149 }
150 // Pack and post sends.
151 sendReqs_.assign(sendRanks_.size(), MPI_REQUEST_NULL);
152 sendBufs_.resize(sendRanks_.size());
153 for (std::size_t k = 0; k < sendRanks_.size(); ++k) {
154 auto& buf = sendBufs_[k];
155 buf.resize(sendIdx_[k].size() * es);
156 for (std::size_t i = 0; i < sendIdx_[k].size(); ++i) {
157 field.pack(sendIdx_[k][i], buf.data() + i * es);
158 }
159 MPI_Isend(buf.data(), static_cast<int>(buf.size()), MPI_BYTE, sendRanks_[k], tag, comm_,
160 &sendReqs_[k]);
161 }
162 pendingTag_ = tag;
163 }
164
166 template <typename Field>
167 void wait(Field& field) {
168 const std::size_t es = field.bytesPerElem();
169 // Unpack receives as they arrive.
170 for (std::size_t done = 0; done < recvReqs_.size(); ++done) {
171 int k = 0;
172 MPI_Waitany(static_cast<int>(recvReqs_.size()), recvReqs_.data(), &k, MPI_STATUS_IGNORE);
173 if (k == MPI_UNDEFINED)
174 break;
175 for (std::size_t i = 0; i < recvIdx_[k].size(); ++i) {
176 field.unpack(recvIdx_[k][i], recvBufs_[k].data() + i * es);
177 }
178 }
179 if (!sendReqs_.empty()) {
180 MPI_Waitall(static_cast<int>(sendReqs_.size()), sendReqs_.data(), MPI_STATUSES_IGNORE);
181 }
182 }
183
185 template <typename Field>
186 void exchangeNbx(Field& field, int tag = 0) {
187 start(field, tag);
188 wait(field);
189 }
190
191 // --- Field-agnostic exchange (persistent neighborhood collective)
192 // -------------------------------
193
196 template <typename Field>
198 applySelfCopy(field);
199 const std::size_t es = field.bytesPerElem();
200 ensureGraphComm(es);
201
202 // Pack the contiguous send buffer in destination order.
203 for (std::size_t k = 0; k < sendRanks_.size(); ++k) {
204 char* dst = graphSendBuf_.data() + graphSendDispl_[k];
205 for (std::size_t i = 0; i < sendIdx_[k].size(); ++i) {
206 field.pack(sendIdx_[k][i], dst + i * es);
207 }
208 }
209 MPI_Neighbor_alltoallv(graphSendBuf_.data(), graphSendCnt_.data(), graphSendDispl_.data(),
210 MPI_BYTE, graphRecvBuf_.data(), graphRecvCnt_.data(),
211 graphRecvDispl_.data(), MPI_BYTE, graphComm_);
212 // Scatter the contiguous recv buffer into ghost cells (source order == recvRanks_ order).
213 for (std::size_t k = 0; k < recvRanks_.size(); ++k) {
214 const char* src = graphRecvBuf_.data() + graphRecvDispl_[k];
215 for (std::size_t i = 0; i < recvIdx_[k].size(); ++i) {
216 field.unpack(recvIdx_[k][i], src + i * es);
217 }
218 }
219 }
220
221 // Reverse (transpose) exchange with ACCUMULATE — the adjoint of the forward exchange: each rank
222 // sends its GHOST-cell values back to the owners, which ADD them into the matching owned cells
223 // (and the periodic self-copy folds ghost -> inner with +=). This is the fold a scatter/deposit
224 // needs across rank boundaries (CFD-DEM void-fraction / momentum feedback that lands in ghost
225 // cells). The Field must provide addFrom(localIdx, src) in addition to pack. Point-to-point
226 // (Isend/Irecv) so it needs no second graph comm.
227 template <typename Field>
228 void reverseAdd(Field& field, int tag = 0) {
229 const std::size_t es = field.bytesPerElem();
230 // self: owner (selfSrc) += ghost (selfDst)
231 std::vector<char> tmp(es);
232 for (std::size_t i = 0; i < selfSrc_.size(); ++i) {
233 field.pack(selfDst_[i], tmp.data());
234 field.addFrom(selfSrc_[i], tmp.data());
235 }
236 // I receive owner-updates for my SENT cells (sendIdx_) from the ranks I forward-send to.
237 std::vector<std::vector<char>> rbuf(sendRanks_.size());
238 std::vector<MPI_Request> rreq(sendRanks_.size(), MPI_REQUEST_NULL);
239 for (std::size_t k = 0; k < sendRanks_.size(); ++k) {
240 rbuf[k].resize(sendIdx_[k].size() * es);
241 MPI_Irecv(rbuf[k].data(), static_cast<int>(rbuf[k].size()), MPI_BYTE, sendRanks_[k], tag,
242 comm_, &rreq[k]);
243 }
244 // I send my GHOST cells (recvIdx_) back to the owners they came from (recvRanks_).
245 std::vector<std::vector<char>> sbuf(recvRanks_.size());
246 std::vector<MPI_Request> sreq(recvRanks_.size(), MPI_REQUEST_NULL);
247 for (std::size_t k = 0; k < recvRanks_.size(); ++k) {
248 sbuf[k].resize(recvIdx_[k].size() * es);
249 for (std::size_t i = 0; i < recvIdx_[k].size(); ++i)
250 field.pack(recvIdx_[k][i], sbuf[k].data() + i * es);
251 MPI_Isend(sbuf[k].data(), static_cast<int>(sbuf[k].size()), MPI_BYTE, recvRanks_[k], tag,
252 comm_, &sreq[k]);
253 }
254 // As owner-updates arrive, accumulate into my owned (sendIdx_) cells.
255 for (std::size_t done = 0; done < sendRanks_.size(); ++done) {
256 int k = 0;
257 MPI_Waitany(static_cast<int>(rreq.size()), rreq.data(), &k, MPI_STATUS_IGNORE);
258 if (k == MPI_UNDEFINED)
259 break;
260 for (std::size_t i = 0; i < sendIdx_[k].size(); ++i)
261 field.addFrom(sendIdx_[k][i], rbuf[k].data() + i * es);
262 }
263 if (!sreq.empty())
264 MPI_Waitall(static_cast<int>(sreq.size()), sreq.data(), MPI_STATUSES_IGNORE);
265 }
266
267 // --- Flattened topology (contiguous, device-friendly; consumed by the CUDA exchange) ---
270 std::vector<int> sendRanks, recvRanks; // neighbour ranks
271 std::vector<int> sendCounts, recvCounts; // elements per neighbour (same order)
272 std::vector<Index> sendIdx, recvIdx; // flattened local indices, grouped by neighbour
273 std::vector<Index> selfSrc, selfDst; // local periodic self-copy (inner -> ghost)
274 };
275
278 t.comm = comm_;
279 t.sendRanks = std::vector<int>(sendRanks_.begin(), sendRanks_.end());
280 t.recvRanks = std::vector<int>(recvRanks_.begin(), recvRanks_.end());
281 for (const auto& v : sendIdx_) {
282 t.sendCounts.push_back(static_cast<int>(v.size()));
283 t.sendIdx.insert(t.sendIdx.end(), v.begin(), v.end());
284 }
285 for (const auto& v : recvIdx_) {
286 t.recvCounts.push_back(static_cast<int>(v.size()));
287 t.recvIdx.insert(t.recvIdx.end(), v.begin(), v.end());
288 }
289 t.selfSrc = selfSrc_;
290 t.selfDst = selfDst_;
291 return t;
292 }
293
294 // --- Introspection (used by tests/benchmarks) ---
295 const decomp::BlockIndexer<Dim>& indexer() const { return indexer_; }
296 std::size_t numNeighbors() const { return recvRanks_.size(); }
297 std::size_t numGhostRecv() const {
298 std::size_t n = 0;
299 for (auto& v : recvIdx_)
300 n += v.size();
301 return n;
302 }
303
304 ~GridHaloTopology() { freeGraphComm(); }
305
308
309 private:
310 // Free the cached graph communicator, but never after MPI_Finalize (e.g. if this object outlives
311 // finalize on the stack of main()). Calling MPI_Comm_free post-finalize aborts the job.
312 void freeGraphComm() {
313 if (graphComm_ == MPI_COMM_NULL)
314 return;
315 int finalized = 0;
317 if (!finalized)
318 MPI_Comm_free(&graphComm_);
319 graphComm_ = MPI_COMM_NULL;
320 }
321
322 void clear() {
323 sendRanks_.clear();
324 sendIdx_.clear();
325 recvRanks_.clear();
326 recvIdx_.clear();
327 recvGlob_.clear();
328 selfSrc_.clear();
329 selfDst_.clear();
330 freeGraphComm();
331 persistentReady_ = false;
332 }
333
334 void buildRecvLookup() {
335 recvRankPos_.clear();
336 for (std::size_t k = 0; k < recvRanks_.size(); ++k)
337 recvRankPos_[recvRanks_[k]] = k;
338 }
339
340 template <typename Field>
341 void applySelfCopy(Field& field) {
342 // Periodic copy within our own block: read inner cell, write ghost cell. We go through the
343 // field's pack/unpack so it works for any payload type.
344 if (selfSrc_.empty())
345 return;
346 std::vector<char> tmp(field.bytesPerElem());
347 for (std::size_t i = 0; i < selfSrc_.size(); ++i) {
348 field.pack(selfSrc_[i], tmp.data());
349 field.unpack(selfDst_[i], tmp.data());
350 }
351 }
352
353 void ensureGraphComm(std::size_t es) {
354 if (persistentReady_ && graphElemSize_ == es)
355 return;
356 if (graphComm_ != MPI_COMM_NULL)
357 MPI_Comm_free(&graphComm_);
358
359 // Distributed graph: sources = ranks we receive from, destinations = ranks we send to.
360 std::vector<int> sources(recvRanks_.begin(), recvRanks_.end());
361 std::vector<int> dests(sendRanks_.begin(), sendRanks_.end());
362 MPI_Dist_graph_create_adjacent(comm_, static_cast<int>(sources.size()), sources.data(),
363 MPI_UNWEIGHTED, static_cast<int>(dests.size()), dests.data(),
364 MPI_UNWEIGHTED, MPI_INFO_NULL, /*reorder=*/0, &graphComm_);
365
366 // Counts/displacements (in bytes) in the graph's neighbour ordering, which for
367 // create_adjacent matches the order we passed sources/dests.
368 graphSendCnt_.resize(dests.size());
369 graphSendDispl_.resize(dests.size());
370 int off = 0;
371 for (std::size_t k = 0; k < dests.size(); ++k) {
372 graphSendCnt_[k] = static_cast<int>(sendIdx_[k].size() * es);
373 graphSendDispl_[k] = off;
374 off += graphSendCnt_[k];
375 }
376 graphSendBuf_.resize(off);
377
378 graphRecvCnt_.resize(sources.size());
379 graphRecvDispl_.resize(sources.size());
380 off = 0;
381 for (std::size_t k = 0; k < sources.size(); ++k) {
382 graphRecvCnt_[k] = static_cast<int>(recvIdx_[k].size() * es);
383 graphRecvDispl_[k] = off;
384 off += graphRecvCnt_[k];
385 }
386 graphRecvBuf_.resize(off);
387
388 graphElemSize_ = es;
389 persistentReady_ = true;
390 }
391
392 MPI_Comm comm_ = MPI_COMM_WORLD;
393 int rank_ = 0;
394 decomp::BlockIndexer<Dim> indexer_;
395
396 // Matched topology: element i of send list k corresponds to element i of the peer's recv list.
397 std::vector<int> sendRanks_;
398 std::vector<std::vector<Index>> sendIdx_; // local inner-cell indices to send
399 std::vector<int> recvRanks_;
400 std::vector<std::vector<Index>> recvIdx_; // local ghost-cell indices to fill
401 std::vector<std::vector<Index>> recvGlob_; // global cells requested (build-time only)
402 std::map<int, std::size_t> recvRankPos_;
403 std::vector<Index> selfSrc_, selfDst_; // local periodic self-copy (inner -> ghost)
404
405 // NBX exchange scratch.
406 std::vector<MPI_Request> sendReqs_, recvReqs_;
407 std::vector<std::vector<char>> sendBufs_, recvBufs_;
408 int pendingTag_ = 0;
409
410 // Persistent neighborhood-collective state.
411 MPI_Comm graphComm_ = MPI_COMM_NULL;
412 bool persistentReady_ = false;
413 std::size_t graphElemSize_ = 0;
414 std::vector<int> graphSendCnt_, graphSendDispl_, graphRecvCnt_, graphRecvDispl_;
415 std::vector<char> graphSendBuf_, graphRecvBuf_;
416};
417
418} // namespace peclet::core::halo
419
420#endif // PECLET_CORE_HALO_GRID_HALO_TOPOLOGY_HPP
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:27
int MPI_Comm
Definition mpi_stub.hpp:16
#define MPI_COMM_WORLD
Definition mpi_stub.hpp:26
int MPI_Waitall(int, MPI_Request *, MPI_Status *)
Definition mpi_stub.hpp:126
#define MPI_REQUEST_NULL
Definition mpi_stub.hpp:28
#define MPI_UNWEIGHTED
Definition mpi_stub.hpp:34
int MPI_Comm_free(MPI_Comm *)
Definition mpi_stub.hpp:73
#define MPI_STATUS_IGNORE
Definition mpi_stub.hpp:30
#define MPI_BYTE
Definition mpi_stub.hpp:37
#define MPI_STATUSES_IGNORE
Definition mpi_stub.hpp:31
int MPI_Waitany(int, MPI_Request *, int *idx, MPI_Status *)
Definition mpi_stub.hpp:129
int MPI_Isend(const void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:98
int MPI_Finalized(int *f)
Definition mpi_stub.hpp:57
#define MPI_INFO_NULL
Definition mpi_stub.hpp:29
int MPI_Irecv(void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:101
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:141
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:136
#define MPI_UNDEFINED
Definition mpi_stub.hpp:33
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)