core
Shared MPI block decomposition + asynchronous ghost-layer exchange (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 <cstring>
25#include <map>
26#include <vector>
27
32
33namespace peclet::core::halo {
34
35template <int Dim>
37 public:
39 void init(const ParticleMigrator<Dim>& mig) { mig_ = &mig; }
40
52 void build(const std::vector<Vec<Dim>>& pos, double rcut, bool includePeriodicSelf = false) {
53 numOwned_ = pos.size();
54 int nranks = 0;
55 MPI_Comm_size(mig_->comm(), &nranks);
56 const int me = mig_->rank();
57
58 // Sender side: which of my particles each other rank needs, and the periodic shift to apply.
59 std::map<int, std::vector<Index>> sendMap;
60 std::map<int, std::vector<Vec<Dim>>> shiftMap;
62 for (std::size_t i = 0; i < pos.size(); ++i) {
63 for (int r = 0; r < nranks; ++r) {
64 if (r == me)
65 continue;
66 if (!mig_->withinRcutOfBlock(pos[i], r, rcut, img))
67 continue;
68 Vec<Dim> shift;
69 for (int d = 0; d < Dim; ++d)
70 shift[d] = img[d] - pos[i][d];
71 sendMap[r].push_back(static_cast<Index>(i));
72 shiftMap[r].push_back(shift);
73 }
74 }
75 sendRanks_.clear();
76 sendIdx_.clear();
77 sendRankPos_.clear();
78 for (auto& [r, idx] : sendMap) {
79 sendRankPos_[r] = sendRanks_.size();
80 sendRanks_.push_back(r);
81 sendIdx_.push_back(std::move(idx));
82 }
83
84 // Exchange the shift vectors (one NBX round) to size the ghost array + matched recv lists.
85 recvRanks_.clear();
86 recvCount_.clear();
87 recvRankPos_.clear();
88 shift_.clear();
89 NbxEngine nbx(mig_->comm());
90 std::size_t k = 0;
91 auto packNext = [&](std::vector<char>& out) -> int {
92 if (k >= sendRanks_.size())
93 return -1;
94 int dst = sendRanks_[k];
95 auto& sh = shiftMap[dst];
96 ++k;
97 out.resize(sh.size() * sizeof(Vec<Dim>));
98 std::memcpy(out.data(), sh.data(), out.size());
99 return dst;
100 };
101 auto onRecv = [&](int src, std::vector<char>& msg) {
102 int cnt = static_cast<int>(msg.size() / sizeof(Vec<Dim>));
103 recvRankPos_[src] = recvRanks_.size();
104 recvRanks_.push_back(src);
105 recvCount_.push_back(cnt);
106 const Vec<Dim>* sh = reinterpret_cast<const Vec<Dim>*>(msg.data());
107 for (int i = 0; i < cnt; ++i)
108 shift_.push_back(sh[i]);
109 };
110 nbx.exchange(packNext, onRecv, /*tag=*/7501);
111
112 recvOffset_.assign(recvRanks_.size() + 1, 0);
113 for (std::size_t i = 0; i < recvCount_.size(); ++i)
114 recvOffset_[i + 1] = recvOffset_[i] + recvCount_[i];
115 numReceived_ = shift_.size();
116
117 // Local periodic self-ghosts (no MPI): each owned particle's non-identity periodic image(s)
118 // that land within rcut of this rank's own block. Appended after the received ghosts; their
119 // shift goes in the shift_ tail so the per-ghost position forward applies the wrap uniformly.
120 selfIdx_.clear();
121 selfShift_.clear();
123 std::vector<Vec<Dim>> imgs;
124 for (std::size_t i = 0; i < pos.size(); ++i) {
125 imgs.clear();
126 mig_->imagesWithinRcutOfBlock(pos[i], me, rcut, /*allowIdentity=*/false, imgs);
127 for (const auto& sh : imgs) {
128 selfIdx_.push_back(static_cast<Index>(i));
129 selfShift_.push_back(sh);
130 }
131 }
132 }
133 for (const auto& sh : selfShift_)
134 shift_.push_back(sh);
135 numGhost_ = shift_.size();
136
137 // Populate the initial ghost positions (= owner position + shift), received + self.
138 ghostPos_.assign(numGhost_, Vec<Dim>{});
139 forwardPositions(pos.data(), ghostPos_.data());
140 }
141
142 std::size_t numOwned() const { return numOwned_; }
143 std::size_t numGhost() const { return numGhost_; }
144 const std::vector<Vec<Dim>>& ghostPositions() const { return ghostPos_; }
145
150 forwardDirect<Vec<Dim>>(owned, /*tag=*/7502, [&](std::size_t p, const Vec<Dim>* in) {
151 Index off = recvOffset_[p];
152 for (int i = 0; i < recvCount_[p]; ++i)
153 for (int d = 0; d < Dim; ++d)
154 ghost[off + i][d] = in[i][d] + shift_[off + i][d];
155 });
156 // Local periodic self-ghosts: owner position + wrap shift (no MPI).
157 for (std::size_t j = 0; j < selfIdx_.size(); ++j)
158 for (int d = 0; d < Dim; ++d)
159 ghost[numReceived_ + j][d] = owned[selfIdx_[j]][d] + shift_[numReceived_ + j][d];
160 }
161
163 template <typename T>
164 void forward(const T* owned, T* ghost) {
165 forwardDirect<T>(owned, /*tag=*/7503, [&](std::size_t p, const T* in) {
166 Index off = recvOffset_[p];
167 for (int i = 0; i < recvCount_[p]; ++i)
168 ghost[off + i] = in[i];
169 });
170 // Local periodic self-ghosts: owner value, verbatim (no MPI).
171 for (std::size_t j = 0; j < selfIdx_.size(); ++j)
172 ghost[numReceived_ + j] = owned[selfIdx_[j]];
173 }
174
177 template <typename T>
178 void reverse(const T* ghost, T* owned) {
179 // Mirror of forwardDirect: the receivers (owners) post recvs sized by sendIdx_, the
180 // ghost-holders send their contiguous ghost slices back; accumulate on arrival.
181 const int ns = static_cast<int>(sendRanks_.size());
182 const int nr = static_cast<int>(recvRanks_.size());
183 std::vector<std::vector<T>> rbuf(ns), sbuf(nr);
184 std::vector<MPI_Request> rreq(ns, MPI_REQUEST_NULL), sreq(nr, MPI_REQUEST_NULL);
185 for (int k = 0; k < ns; ++k) {
186 rbuf[k].resize(sendIdx_[k].size());
187 MPI_Irecv(rbuf[k].data(), static_cast<int>(rbuf[k].size() * sizeof(T)), MPI_BYTE,
188 sendRanks_[k], 7504, mig_->comm(), &rreq[k]);
189 }
190 for (int p = 0; p < nr; ++p) {
191 Index off = recvOffset_[p];
192 sbuf[p].assign(ghost + off, ghost + off + recvCount_[p]);
193 MPI_Isend(sbuf[p].data(), static_cast<int>(sbuf[p].size() * sizeof(T)), MPI_BYTE,
194 recvRanks_[p], 7504, mig_->comm(), &sreq[p]);
195 }
197 for (int k = 0; k < ns; ++k) {
198 auto& idx = sendIdx_[k];
199 for (std::size_t i = 0; i < idx.size(); ++i)
200 owned[idx[i]] += rbuf[k][i];
201 }
202 // Local periodic self-ghosts accumulate straight onto their (same-rank) owner.
203 for (std::size_t j = 0; j < selfIdx_.size(); ++j)
204 owned[selfIdx_[j]] += ghost[numReceived_ + j];
206 }
207
208 private:
209 // Direct (persistent-topology) forward: owners send their gathered sendIdx_ slices, ghost-holders
210 // receive into contiguous slots and apply `store(recvRankPos, buf)`. No NBX consensus -- the
211 // neighbour set + message sizes are fixed by build(), so plain Irecv/Isend/Waitall is correct.
212 template <typename T, typename Store>
213 void forwardDirect(const T* owned, int tag, Store&& store) {
214 const int ns = static_cast<int>(sendRanks_.size());
215 const int nr = static_cast<int>(recvRanks_.size());
216 std::vector<std::vector<T>> sbuf(ns), rbuf(nr);
217 std::vector<MPI_Request> sreq(ns, MPI_REQUEST_NULL), rreq(nr, MPI_REQUEST_NULL);
218 for (int p = 0; p < nr; ++p) {
219 rbuf[p].resize(recvCount_[p]);
220 MPI_Irecv(rbuf[p].data(), static_cast<int>(rbuf[p].size() * sizeof(T)), MPI_BYTE,
221 recvRanks_[p], tag, mig_->comm(), &rreq[p]);
222 }
223 for (int k = 0; k < ns; ++k) {
224 auto& idx = sendIdx_[k];
225 sbuf[k].resize(idx.size());
226 for (std::size_t i = 0; i < idx.size(); ++i)
227 sbuf[k][i] = owned[idx[i]];
228 MPI_Isend(sbuf[k].data(), static_cast<int>(sbuf[k].size() * sizeof(T)), MPI_BYTE,
229 sendRanks_[k], tag, mig_->comm(), &sreq[k]);
230 }
232 for (int p = 0; p < nr; ++p)
233 store(static_cast<std::size_t>(p), rbuf[p].data());
235 }
236
237 public:
238 MPI_Comm comm() const { return mig_->comm(); }
239
240 // The send/recv topology in a flat, device-friendly form, so a CUDA consumer can drive the
241 // exchange with on-device gather kernels + device-pointer MPI (see packing-gpu's device-resident
242 // pack). recvOffsets index the contiguous [0,numGhost) ghost array (in recvRanks order); shift is
243 // per-ghost (for position forwards). Rebuild after each build().
244 struct FlatTopo {
245 std::vector<int> sendRanks; // neighbour ranks I send owned copies to
246 std::vector<Index> sendIdx; // concatenated owned indices to send (all ranks)
247 std::vector<int> sendCounts; // per send rank
248 std::vector<int> sendOffsets; // prefix sum into sendIdx (size sendRanks+1)
249 std::vector<int> recvRanks; // neighbour ranks I receive ghosts from
250 std::vector<int> recvCounts; // per recv rank
251 std::vector<Index> recvOffsets; // per recv rank: start in the [0,numReceived) ghost array
252 std::vector<Vec<Dim>> shift; // per ghost: periodic image offset (add to forwarded position)
253 std::vector<Index> selfIdx; // owned index of each LOCAL periodic self-ghost (size numSelf)
254 Index numReceived = 0; // ghost slots [0,numReceived) are cross-rank (MPI), the rest
255 // [numReceived, numGhost) are local self-ghosts gathered from selfIdx
256 };
258 FlatTopo t;
259 t.sendRanks = sendRanks_;
260 t.sendOffsets.push_back(0);
261 for (const auto& idx : sendIdx_) {
262 t.sendCounts.push_back(static_cast<int>(idx.size()));
263 for (Index id : idx)
264 t.sendIdx.push_back(id);
265 t.sendOffsets.push_back(static_cast<int>(t.sendIdx.size()));
266 }
267 t.recvRanks = recvRanks_;
268 t.recvCounts = recvCount_;
269 t.recvOffsets.assign(recvOffset_.begin(),
270 recvOffset_.begin() + static_cast<std::ptrdiff_t>(recvRanks_.size()));
271 t.shift = shift_;
272 t.selfIdx = selfIdx_;
273 t.numReceived = static_cast<Index>(numReceived_);
274 return t;
275 }
276
277 private:
278 const ParticleMigrator<Dim>* mig_ = nullptr;
279 std::size_t numOwned_ = 0, numGhost_ = 0;
280
281 // Owner side: particles I send as ghosts to each neighbour rank.
282 std::vector<int> sendRanks_;
283 std::vector<std::vector<Index>> sendIdx_;
284 std::map<int, std::size_t> sendRankPos_;
285
286 // Ghost side: ghosts I receive from each neighbour rank (contiguous slots, in recvRanks_ order).
287 std::vector<int> recvRanks_;
288 std::vector<int> recvCount_;
289 std::vector<Index> recvOffset_;
290 std::map<int, std::size_t> recvRankPos_;
291
292 // Local periodic self-ghosts (undecomposed periodic axis / np=1): appended after the received
293 // ones.
294 std::vector<Index> selfIdx_; // owned index for each self-ghost (ghost slot numReceived_ + j)
295 std::vector<Vec<Dim>> selfShift_; // matching periodic wrap shift
296 std::size_t numReceived_ = 0; // count of cross-rank received ghosts (= start of the self tail)
297
298 std::vector<Vec<Dim>>
299 shift_; // per ghost: periodic image offset (img - owner pos); received then self
300 std::vector<Vec<Dim>> ghostPos_; // per ghost: current image position
301};
302
303} // namespace peclet::core::halo
304
305#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 build(const std::vector< Vec< Dim > > &pos, double rcut, bool includePeriodicSelf=false)
(Re)establish the owner<->ghost correspondence: every owned particle within rcut of another rank's bl...
void reverse(const T *ghost, T *owned)
ghost[G] -> owned[N], accumulated (T must have operator+=).
const std::vector< Vec< Dim > > & ghostPositions() const
int MPI_Comm_size(MPI_Comm, int *s)
Definition mpi_stub.hpp:69
int MPI_Comm
Definition mpi_stub.hpp:16
int MPI_Waitall(int, MPI_Request *, MPI_Status *)
Definition mpi_stub.hpp:126
#define MPI_REQUEST_NULL
Definition mpi_stub.hpp:28
#define MPI_BYTE
Definition mpi_stub.hpp:37
#define MPI_STATUSES_IGNORE
Definition mpi_stub.hpp:31
int MPI_Isend(const void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:98
int MPI_Irecv(void *, int, MPI_Datatype, int, int, MPI_Comm, MPI_Request *)
Definition mpi_stub.hpp:101
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