core 1.5.0
Shared MPI block decomposition, asynchronous ghost-layer exchange, SDF geometry, load balancing (header-only C++20)
Loading...
Searching...
No Matches
particle_halo_topology.hpp
Go to the documentation of this file.
1// core — persistent Lagrangian ghost halo with forward + reverse(accumulate) exchange.
2//
3// The generic communication machinery behind the standard parallel particle schemes (and the
4// Voronoi conservative-flux scheme). Same topology/exchange split as GridHaloTopology: build()
5// establishes a persistent owner<->ghost correspondence from particle proximity (rebuilt only when
6// the neighbour list rebuilds), then forward()/reverse() are cheap field-agnostic exchanges over
7// it:
8//
9// forward(owned -> ghost) : copy each owner's value into its ghost copies (refresh
10// state) reverse(ghost -> owned, +=) : accumulate ghost contributions back onto the owner
11// forwardPositions(owned -> ghost) : forward with the periodic image shift applied (positions
12// only)
13//
14// The three distributed schemes are compositions of these (see packing-gpu/mpi/README.md):
15// A (frozen/replicate): forwardPositions+forward(state); compute pairs x2; integrate owned.
16// B (Newton-on): forward(state); each pair x1; reverse(force,sum); integrate owned.
17// C (force-accumulate): each pair x1; reverse(force,sum); forward(totalForce); integrate
18// owned+ghost.
19//
20// The solver supplies the interaction kernel and the integrator; the core supplies forward/reverse.
21#ifndef PECLET_CORE_HALO_PARTICLE_HALO_TOPOLOGY_HPP
22#define PECLET_CORE_HALO_PARTICLE_HALO_TOPOLOGY_HPP
23
24#include <algorithm>
25#include <cstring>
26#include <map>
27#include <vector>
28
33
34namespace peclet::core::halo {
35
36template <int Dim>
38 public:
40 void init(const ParticleMigrator<Dim>& mig) { mig_ = &mig; }
41
65 void build(const std::vector<Vec<Dim>>& pos, double rcut, bool includePeriodicSelf = false,
66 bool allImages = false) {
67 numOwned_ = pos.size();
68 int nranks = 0;
69 MPI_Comm_size(mig_->comm(), &nranks);
70 const int me = mig_->rank();
71
72 // Sender side: which of my particles each other rank needs, and the periodic shift to apply.
73 std::map<int, std::vector<Index>> sendMap;
74 std::map<int, std::vector<Vec<Dim>>> shiftMap;
76 std::vector<Vec<Dim>> imgShifts;
77 // Candidate ranks (ascending): those whose block comes within rcut of the bounding box of my
78 // particles, images included (boxWithinRcutOfBlock). A superset of every rank any particle can
79 // qualify for, so the send lists -- entries and order -- are those of the all-ranks loop; the
80 // per-particle work drops from O(numRanks) to O(neighbours).
81 std::vector<int> cand;
82 if (!pos.empty()) {
83 Vec<Dim> bmin = pos[0], bmax = pos[0];
84 for (const auto& x : pos)
85 for (int d = 0; d < Dim; ++d) {
86 bmin[d] = std::min(bmin[d], x[d]);
87 bmax[d] = std::max(bmax[d], x[d]);
88 }
89 for (int r = 0; r < nranks; ++r)
90 if (r != me && mig_->boxWithinRcutOfBlock(bmin, bmax, r, rcut))
91 cand.push_back(r);
92 }
93 for (std::size_t i = 0; i < pos.size(); ++i) {
94 for (const int r : cand) {
95 if (allImages) {
96 // Every qualifying image, one send entry each (the identity included).
97 imgShifts.clear();
98 mig_->imagesWithinRcutOfBlock(pos[i], r, rcut, /*allowIdentity=*/true, imgShifts);
99 for (const auto& sh : imgShifts) {
100 sendMap[r].push_back(static_cast<Index>(i));
101 shiftMap[r].push_back(sh);
102 }
103 continue;
104 }
105 if (!mig_->withinRcutOfBlock(pos[i], r, rcut, img))
106 continue;
107 Vec<Dim> shift;
108 for (int d = 0; d < Dim; ++d)
109 shift[d] = img[d] - pos[i][d];
110 sendMap[r].push_back(static_cast<Index>(i));
111 shiftMap[r].push_back(shift);
112 }
113 }
114 sendRanks_.clear();
115 sendIdx_.clear();
116 sendShift_.clear();
117 sendRankPos_.clear();
118 for (auto& [r, idx] : sendMap) {
119 sendRankPos_[r] = sendRanks_.size();
120 sendRanks_.push_back(r);
121 sendIdx_.push_back(std::move(idx));
122 sendShift_.push_back(shiftMap[r]); // the image each entry is sent as (parallel to sendIdx_)
123 }
124
125 // Exchange the shift vectors (one NBX round) to size the ghost array + matched recv lists.
126 recvRanks_.clear();
127 recvCount_.clear();
128 recvRankPos_.clear();
129 shift_.clear();
130 NbxEngine nbx(mig_->comm());
131 std::size_t k = 0;
132 auto packNext = [&](std::vector<char>& out) -> int {
133 if (k >= sendRanks_.size())
134 return -1;
135 int dst = sendRanks_[k];
136 auto& sh = shiftMap[dst];
137 ++k;
138 out.resize(sh.size() * sizeof(Vec<Dim>));
139 std::memcpy(out.data(), sh.data(), out.size());
140 return dst;
141 };
142 auto onRecv = [&](int src, std::vector<char>& msg) {
143 int cnt = static_cast<int>(msg.size() / sizeof(Vec<Dim>));
144 recvRankPos_[src] = recvRanks_.size();
145 recvRanks_.push_back(src);
146 recvCount_.push_back(cnt);
147 const Vec<Dim>* sh = reinterpret_cast<const Vec<Dim>*>(msg.data());
148 for (int i = 0; i < cnt; ++i)
149 shift_.push_back(sh[i]);
150 };
151 nbx.exchange(packNext, onRecv, /*tag=*/7501);
152
153 recvOffset_.assign(recvRanks_.size() + 1, 0);
154 for (std::size_t i = 0; i < recvCount_.size(); ++i)
155 recvOffset_[i + 1] = recvOffset_[i] + recvCount_[i];
156 numReceived_ = shift_.size();
157
158 // Local periodic self-ghosts (no MPI): each owned particle's non-identity periodic image(s)
159 // that land within rcut of this rank's own block. Appended after the received ghosts; their
160 // shift goes in the shift_ tail so the per-ghost position forward applies the wrap uniformly.
161 selfIdx_.clear();
162 selfShift_.clear();
164 std::vector<Vec<Dim>> imgs;
165 for (std::size_t i = 0; i < pos.size(); ++i) {
166 imgs.clear();
167 mig_->imagesWithinRcutOfBlock(pos[i], me, rcut, /*allowIdentity=*/false, imgs);
168 for (const auto& sh : imgs) {
169 selfIdx_.push_back(static_cast<Index>(i));
170 selfShift_.push_back(sh);
171 }
172 }
173 }
174 for (const auto& sh : selfShift_)
175 shift_.push_back(sh);
176 numGhost_ = shift_.size();
177
178 // Populate the initial ghost positions (= owner position + shift), received + self.
179 ghostPos_.assign(numGhost_, Vec<Dim>{});
180 forwardPositions(pos.data(), ghostPos_.data());
181 }
182
183 std::size_t numOwned() const { return numOwned_; }
184 std::size_t numGhost() const { return numGhost_; }
185 const std::vector<Vec<Dim>>& ghostPositions() const { return ghostPos_; }
186
191 forwardDirect<Vec<Dim>>(owned, /*tag=*/7502, [&](std::size_t p, const Vec<Dim>* in) {
192 Index off = recvOffset_[p];
193 for (int i = 0; i < recvCount_[p]; ++i)
194 for (int d = 0; d < Dim; ++d)
195 ghost[off + i][d] = in[i][d] + shift_[off + i][d];
196 });
197 // Local periodic self-ghosts: owner position + wrap shift (no MPI).
198 for (std::size_t j = 0; j < selfIdx_.size(); ++j)
199 for (int d = 0; d < Dim; ++d)
200 ghost[numReceived_ + j][d] = owned[selfIdx_[j]][d] + shift_[numReceived_ + j][d];
201 }
202
204 template <typename T>
205 void forward(const T* owned, T* ghost) {
206 forwardDirect<T>(owned, /*tag=*/7503, [&](std::size_t p, const T* in) {
207 Index off = recvOffset_[p];
208 for (int i = 0; i < recvCount_[p]; ++i)
209 ghost[off + i] = in[i];
210 });
211 // Local periodic self-ghosts: owner value, verbatim (no MPI).
212 for (std::size_t j = 0; j < selfIdx_.size(); ++j)
213 ghost[numReceived_ + j] = owned[selfIdx_[j]];
214 }
215
218 template <typename T>
219 void reverse(const T* ghost, T* owned) {
220 // Mirror of forwardDirect: the receivers (owners) post recvs sized by sendIdx_, the
221 // ghost-holders send their contiguous ghost slices back; accumulate on arrival.
222 const int ns = static_cast<int>(sendRanks_.size());
223 const int nr = static_cast<int>(recvRanks_.size());
224 std::vector<std::vector<T>> rbuf(ns), sbuf(nr);
225 std::vector<MPI_Request> rreq(ns, MPI_REQUEST_NULL), sreq(nr, MPI_REQUEST_NULL);
226 for (int k = 0; k < ns; ++k) {
227 rbuf[k].resize(sendIdx_[k].size());
228 MPI_Irecv(rbuf[k].data(), static_cast<int>(rbuf[k].size() * sizeof(T)), MPI_BYTE,
229 sendRanks_[k], 7504, mig_->comm(), &rreq[k]);
230 }
231 for (int p = 0; p < nr; ++p) {
232 Index off = recvOffset_[p];
233 sbuf[p].assign(ghost + off, ghost + off + recvCount_[p]);
234 MPI_Isend(sbuf[p].data(), static_cast<int>(sbuf[p].size() * sizeof(T)), MPI_BYTE,
235 recvRanks_[p], 7504, mig_->comm(), &sreq[p]);
236 }
238 for (int k = 0; k < ns; ++k) {
239 auto& idx = sendIdx_[k];
240 for (std::size_t i = 0; i < idx.size(); ++i)
241 owned[idx[i]] += rbuf[k][i];
242 }
243 // Local periodic self-ghosts accumulate straight onto their (same-rank) owner.
244 for (std::size_t j = 0; j < selfIdx_.size(); ++j)
245 owned[selfIdx_[j]] += ghost[numReceived_ + j];
247 }
248
249 private:
250 // Direct (persistent-topology) forward: owners send their gathered sendIdx_ slices, ghost-holders
251 // receive into contiguous slots and apply `store(recvRankPos, buf)`. No NBX consensus -- the
252 // neighbour set + message sizes are fixed by build(), so plain Irecv/Isend/Waitall is correct.
253 template <typename T, typename Store>
254 void forwardDirect(const T* owned, int tag, Store&& store) {
255 const int ns = static_cast<int>(sendRanks_.size());
256 const int nr = static_cast<int>(recvRanks_.size());
257 std::vector<std::vector<T>> sbuf(ns), rbuf(nr);
258 std::vector<MPI_Request> sreq(ns, MPI_REQUEST_NULL), rreq(nr, MPI_REQUEST_NULL);
259 for (int p = 0; p < nr; ++p) {
260 rbuf[p].resize(recvCount_[p]);
261 MPI_Irecv(rbuf[p].data(), static_cast<int>(rbuf[p].size() * sizeof(T)), MPI_BYTE,
262 recvRanks_[p], tag, mig_->comm(), &rreq[p]);
263 }
264 for (int k = 0; k < ns; ++k) {
265 auto& idx = sendIdx_[k];
266 sbuf[k].resize(idx.size());
267 for (std::size_t i = 0; i < idx.size(); ++i)
268 sbuf[k][i] = owned[idx[i]];
269 MPI_Isend(sbuf[k].data(), static_cast<int>(sbuf[k].size() * sizeof(T)), MPI_BYTE,
270 sendRanks_[k], tag, mig_->comm(), &sreq[k]);
271 }
273 for (int p = 0; p < nr; ++p)
274 store(static_cast<std::size_t>(p), rbuf[p].data());
276 }
277
278 public:
279 MPI_Comm comm() const { return mig_->comm(); }
280
281 // The send/recv topology in a flat, device-friendly form, so a CUDA consumer can drive the
282 // exchange with on-device gather kernels + device-pointer MPI (see packing-gpu's device-resident
283 // pack). recvOffsets index the contiguous [0,numGhost) ghost array (in recvRanks order); shift is
284 // per-ghost (for position forwards). Rebuild after each build().
285 struct FlatTopo {
286 std::vector<int> sendRanks; // neighbour ranks I send owned copies to
287 std::vector<Index> sendIdx; // concatenated owned indices to send (all ranks); with
288 // allImages an index may repeat within one rank's slice
289 // (one entry per periodic image)
290 std::vector<Vec<Dim>> sendShift; // parallel to sendIdx: the periodic image offset the entry is
291 // sent with (the receiver's ghost shift for that entry)
292 std::vector<int> sendCounts; // per send rank
293 std::vector<int> sendOffsets; // prefix sum into sendIdx (size sendRanks+1)
294 std::vector<int> recvRanks; // neighbour ranks I receive ghosts from
295 std::vector<int> recvCounts; // per recv rank
296 std::vector<Index> recvOffsets; // per recv rank: start in the [0,numReceived) ghost array
297 std::vector<Vec<Dim>> shift; // per ghost: periodic image offset (add to forwarded position)
298 std::vector<Index> selfIdx; // owned index of each LOCAL periodic self-ghost (size numSelf)
299 Index numReceived = 0; // ghost slots [0,numReceived) are cross-rank (MPI), the rest
300 // [numReceived, numGhost) are local self-ghosts gathered from selfIdx
301 };
303 FlatTopo t;
304 t.sendRanks = sendRanks_;
305 t.sendOffsets.push_back(0);
306 for (std::size_t k = 0; k < sendIdx_.size(); ++k) {
307 const auto& idx = sendIdx_[k];
308 t.sendCounts.push_back(static_cast<int>(idx.size()));
309 for (Index id : idx)
310 t.sendIdx.push_back(id);
311 for (const auto& sh : sendShift_[k])
312 t.sendShift.push_back(sh);
313 t.sendOffsets.push_back(static_cast<int>(t.sendIdx.size()));
314 }
315 t.recvRanks = recvRanks_;
316 t.recvCounts = recvCount_;
317 t.recvOffsets.assign(recvOffset_.begin(),
318 recvOffset_.begin() + static_cast<std::ptrdiff_t>(recvRanks_.size()));
319 t.shift = shift_;
320 t.selfIdx = selfIdx_;
321 t.numReceived = static_cast<Index>(numReceived_);
322 return t;
323 }
324
325 private:
326 const ParticleMigrator<Dim>* mig_ = nullptr;
327 std::size_t numOwned_ = 0, numGhost_ = 0;
328
329 // Owner side: particles I send as ghosts to each neighbour rank.
330 std::vector<int> sendRanks_;
331 std::vector<std::vector<Index>> sendIdx_;
332 std::vector<std::vector<Vec<Dim>>> sendShift_; // parallel to sendIdx_: each entry's image offset
333 std::map<int, std::size_t> sendRankPos_;
334
335 // Ghost side: ghosts I receive from each neighbour rank (contiguous slots, in recvRanks_ order).
336 std::vector<int> recvRanks_;
337 std::vector<int> recvCount_;
338 std::vector<Index> recvOffset_;
339 std::map<int, std::size_t> recvRankPos_;
340
341 // Local periodic self-ghosts (undecomposed periodic axis / np=1): appended after the received
342 // ones.
343 std::vector<Index> selfIdx_; // owned index for each self-ghost (ghost slot numReceived_ + j)
344 std::vector<Vec<Dim>> selfShift_; // matching periodic wrap shift
345 std::size_t numReceived_ = 0; // count of cross-rank received ghosts (= start of the self tail)
346
347 std::vector<Vec<Dim>>
348 shift_; // per ghost: periodic image offset (img - owner pos); received then self
349 std::vector<Vec<Dim>> ghostPos_; // per ghost: current image position
350};
351
352} // namespace peclet::core::halo
353
354#endif // PECLET_CORE_HALO_PARTICLE_HALO_TOPOLOGY_HPP
void init(const ParticleMigrator< Dim > &mig)
Bind to a migrator (provides the decomposition, domain map, rank and comm).
void forwardPositions(const Vec< Dim > *owned, Vec< Dim > *ghost)
owned[N] -> ghost[G], with the periodic image shift added (use for positions).
void forward(const T *owned, T *ghost)
owned[N] -> ghost[G], verbatim (translation-invariant fields: velocity, id, radius,...
void reverse(const T *ghost, T *owned)
ghost[G] -> owned[N], accumulated (T must have operator+=).
void build(const std::vector< Vec< Dim > > &pos, double rcut, bool includePeriodicSelf=false, bool allImages=false)
(Re)establish the owner<->ghost correspondence: every owned particle within rcut of another rank's bl...
const std::vector< Vec< Dim > > & ghostPositions() const
int MPI_Comm_size(MPI_Comm, int *s)
Definition mpi_stub.hpp:70
int MPI_Comm
Definition mpi_stub.hpp:17
int MPI_Waitall(int, MPI_Request *, MPI_Status *)
Definition mpi_stub.hpp:160
#define MPI_REQUEST_NULL
Definition mpi_stub.hpp:29
#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_Irecv(void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:135
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