core
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
Loading...
Searching...
No Matches
particle_migrator_view.hpp
Go to the documentation of this file.
1// core — device-resident Lagrangian particle migration (D1).
2//
3// ParticleMigrator::migrate (particle_migrator.hpp) is 100% host: a consumer holding its particle
4// SoA on the device must download it, migrate on the host, and re-upload — every migration. This
5// class keeps the SoA on the device: the per-particle periodic wrap + owner lookup (the ORB tree,
6// flattened to Views) run as a Kokkos kernel (device binning); departing particles are gathered
7// into compact per-rank buffers on the device (device pack) and only those compact buffers are
8// host-staged for the NBX consensus exchange (which stays host — it is sparse control logic).
9// Arrivals are unpacked back into the device SoA. The bulk payload never round-trips; only the
10// compact migrating records cross.
11//
12// Correctness contract (matching test_particle_migration): count is conserved, every surviving
13// particle is owned by this rank (ownerOf == rank), and the global id multiset is preserved. The
14// device wrap / cellOf / ownerOf reproduce the host ParticleMigrator math exactly (bit-identical on
15// OpenMP), so the per-particle destination is identical; the NBX exchange itself is
16// order-nondeterministic by design, so only the resulting set is well-defined (as for the host
17// migrator).
18//
19// Requires a Kokkos build + MPI.
20#ifndef PECLET_CORE_HALO_PARTICLE_MIGRATOR_VIEW_HPP
21#define PECLET_CORE_HALO_PARTICLE_MIGRATOR_VIEW_HPP
22
23#include <cstring>
24#include <map>
25#include <vector>
26
31#include "peclet/core/halo/particle_migrator.hpp" // DomainMap<Dim>
32
33namespace peclet::core::halo {
34
38template <int Dim>
40 public:
42 MPI_Comm comm = MPI_COMM_WORLD) {
43 rank_ = rank;
44 comm_ = comm;
45 std::vector<int> sd;
46 std::vector<Index> sv;
47 dec.flattenTree(sd, sv);
48 splitDim_ = toDevice(sd, "pmd::splitDim");
49 splitVal_ = toDevice(sv, "pmd::splitVal");
50 const IVec<Dim>& gs = dec.globalSize();
51 for (int d = 0; d < Dim; ++d) {
52 origin_[d] = map.origin[d];
53 cellSize_[d] = map.cellSize[d];
54 gsize_[d] = gs[d];
55 periodic_[d] = map.periodic[d] ? 1 : 0;
56 }
57 }
58
59 std::size_t lastSent() const { return sent_; }
60 std::size_t lastReceived() const { return received_; }
61
65 const std::size_t posBytes = Dim * sizeof(double);
66 const std::size_t recBytes = posBytes + stride;
67
68 // ---- 1. device: periodic-wrap each position in place + compute its destination rank ----
69 View<int> sd = splitDim_;
70 View<Index> sv = splitVal_;
71 double org[Dim], csz[Dim];
72 Index gsz[Dim];
73 char per[Dim];
74 for (int d = 0; d < Dim; ++d) {
75 org[d] = origin_[d];
76 csz[d] = cellSize_[d];
77 gsz[d] = gsize_[d];
78 per[d] = periodic_[d];
79 }
80 View<int> dest(Kokkos::view_alloc("pmd::dest", Kokkos::WithoutInitializing),
81 static_cast<std::size_t>(n));
82 Kokkos::parallel_for(
83 "pmd::wrap_bin", Kokkos::RangePolicy<ExecSpace>(0, n), KOKKOS_LAMBDA(const Index i) {
84 double x[Dim];
85 for (int d = 0; d < Dim; ++d)
86 x[d] = pos(i * Dim + d);
87 for (int d = 0; d < Dim; ++d)
88 if (per[d]) {
89 const double L = csz[d] * static_cast<double>(gsz[d]);
90 double rel = x[d] - org[d];
91 rel -= L * Kokkos::floor(rel / L); // into [0,L)
92 x[d] = org[d] + rel;
93 }
94 for (int d = 0; d < Dim; ++d)
95 pos(i * Dim + d) = x[d]; // store wrapped position
96 Index g[Dim];
97 for (int d = 0; d < Dim; ++d) {
98 const double rel = (x[d] - org[d]) / csz[d];
99 Index c = static_cast<Index>(Kokkos::floor(rel));
100 if (per[d])
101 c = ((c % gsz[d]) + gsz[d]) % gsz[d];
102 else
103 c = c < 0 ? 0 : (c >= gsz[d] ? gsz[d] - 1 : c);
104 g[d] = c;
105 }
106 Index node = 0;
107 while (sd(node) != -1)
108 node = (g[sd(node)] < sv(node)) ? 2 * node + 1 : 2 * node + 2;
109 dest(i) = static_cast<int>(sv(node));
110 });
111
112 // ---- 2. host consensus: split into kept indices + per-rank departing indices ----
113 auto hdest = Kokkos::create_mirror_view(dest);
114 Kokkos::deep_copy(hdest, dest);
115 std::vector<Index> keepIdx;
116 keepIdx.reserve(static_cast<std::size_t>(n));
117 std::map<int, std::vector<Index>> sendByRank;
118 for (Index i = 0; i < n; ++i) {
119 if (hdest(i) == rank_)
120 keepIdx.push_back(i);
121 else
122 sendByRank[hdest(i)].push_back(i);
123 }
124 const Index keep = static_cast<Index>(keepIdx.size());
125 std::vector<int> sendRanks, sendCounts;
126 std::vector<Index> sendIdxFlat;
127 for (auto& kv : sendByRank) {
128 sendRanks.push_back(kv.first);
129 sendCounts.push_back(static_cast<int>(kv.second.size()));
130 for (Index s : kv.second)
131 sendIdxFlat.push_back(s);
132 }
133 const Index nSend = static_cast<Index>(sendIdxFlat.size());
134 sent_ = static_cast<std::size_t>(nSend);
135
136 // ---- 3. device: gather departing particles into compact send buffers; host-stage. MUST happen
137 // BEFORE the compaction below, which overwrites pos[0,keep) (the departing indices are
138 // original). ----
139 std::vector<double> hSendPos(static_cast<std::size_t>(nSend) * Dim);
140 std::vector<char> hSendPay(static_cast<std::size_t>(nSend) * stride);
141 if (nSend) {
142 IndexView si = toDevice(sendIdxFlat, "pmd::sendIdx");
143 View<double> sp(Kokkos::view_alloc("pmd::sendPos", Kokkos::WithoutInitializing),
144 static_cast<std::size_t>(nSend) * Dim);
145 View<char> spay(Kokkos::view_alloc("pmd::sendPay", Kokkos::WithoutInitializing),
146 static_cast<std::size_t>(nSend) * stride);
147 Kokkos::parallel_for(
148 "pmd::pack", Kokkos::RangePolicy<ExecSpace>(0, nSend), KOKKOS_LAMBDA(const Index j) {
149 const Index s = si(j);
150 for (int d = 0; d < Dim; ++d)
151 sp(j * Dim + d) = pos(s * Dim + d);
152 for (std::size_t b = 0; b < stride; ++b)
153 spay(j * stride + b) = payload(s * stride + b);
154 });
155 Kokkos::fence();
156 auto hsp = Kokkos::create_mirror_view(sp);
157 Kokkos::deep_copy(hsp, sp);
158 for (std::size_t k = 0; k < hSendPos.size(); ++k)
159 hSendPos[k] = hsp(k);
160 if (stride) {
161 auto hspay = Kokkos::create_mirror_view(spay);
162 Kokkos::deep_copy(hspay, spay);
163 for (std::size_t k = 0; k < hSendPay.size(); ++k)
164 hSendPay[k] = hspay(k);
165 }
166 }
167
168 // ---- 4. device: compact kept particles to the front via a scratch gather (overwrites
169 // pos[0,keep)). ----
170 ensureScratch(pos.extent(0), payload.extent(0));
171 if (keep) {
172 IndexView ki = toDevice(keepIdx, "pmd::keepIdx");
173 View<double> tp = tmpPos_;
174 View<char> tpay = tmpPay_;
175 Kokkos::parallel_for(
176 "pmd::compact", Kokkos::RangePolicy<ExecSpace>(0, keep), KOKKOS_LAMBDA(const Index k) {
177 const Index s = ki(k);
178 for (int d = 0; d < Dim; ++d)
179 tp(k * Dim + d) = pos(s * Dim + d);
180 for (std::size_t b = 0; b < stride; ++b)
181 tpay(k * stride + b) = payload(s * stride + b);
182 });
183 Kokkos::deep_copy(Kokkos::subview(pos, std::pair<std::size_t, std::size_t>(
184 0, static_cast<std::size_t>(keep) * Dim)),
185 Kokkos::subview(tmpPos_, std::pair<std::size_t, std::size_t>(
186 0, static_cast<std::size_t>(keep) * Dim)));
187 if (stride)
188 Kokkos::deep_copy(
189 Kokkos::subview(payload, std::pair<std::size_t, std::size_t>(
190 0, static_cast<std::size_t>(keep) * stride)),
191 Kokkos::subview(tmpPay_, std::pair<std::size_t, std::size_t>(
192 0, static_cast<std::size_t>(keep) * stride)));
193 }
194
195 // ---- 5. host: NBX exchange of the compact records (pos bytes + payload bytes per particle)
196 // ----
197 std::vector<std::vector<char>> outbox(sendRanks.size());
198 {
199 Index base = 0;
200 for (std::size_t r = 0; r < sendRanks.size(); ++r) {
201 const Index cnt = sendCounts[r];
202 std::vector<char>& buf = outbox[r];
203 buf.resize(static_cast<std::size_t>(cnt) * recBytes);
204 for (Index j = 0; j < cnt; ++j) {
205 char* rec = buf.data() + static_cast<std::size_t>(j) * recBytes;
206 std::memcpy(rec, &hSendPos[(static_cast<std::size_t>(base) + j) * Dim], posBytes);
207 if (stride)
208 std::memcpy(rec + posBytes, &hSendPay[(static_cast<std::size_t>(base) + j) * stride],
209 stride);
210 }
211 base += cnt;
212 }
213 }
214 std::vector<double> hRecvPos;
215 std::vector<char> hRecvPay;
216 received_ = 0;
217 {
218 NbxEngine nbx(comm_);
219 std::size_t q = 0;
220 auto packNext = [&](std::vector<char>& out) -> int {
221 if (q >= outbox.size())
222 return -1;
223 int d = sendRanks[q];
224 out = outbox[q];
225 ++q;
226 return d;
227 };
228 auto onRecv = [&](int /*src*/, std::vector<char>& msg) {
229 const std::size_t cnt = msg.size() / recBytes;
230 for (std::size_t i = 0; i < cnt; ++i) {
231 const char* rec = msg.data() + i * recBytes;
232 std::size_t po = hRecvPos.size();
233 hRecvPos.resize(po + Dim);
234 std::memcpy(&hRecvPos[po], rec, posBytes);
235 if (stride) {
236 std::size_t yo = hRecvPay.size();
237 hRecvPay.resize(yo + stride);
238 std::memcpy(&hRecvPay[yo], rec + posBytes, stride);
239 }
240 ++received_;
241 }
242 };
243 nbx.exchange(packNext, onRecv, /*tag=*/7411);
244 }
245
246 // ---- 6. device: append arrivals to the compacted SoA at [keep, keep+recv) ----
247 const Index recv = static_cast<Index>(received_);
248 if (recv) {
249 View<double> rp = toDevice(hRecvPos, "pmd::recvPos");
250 Kokkos::parallel_for(
251 "pmd::unpack_pos", Kokkos::RangePolicy<ExecSpace>(0, recv), KOKKOS_LAMBDA(const Index j) {
252 for (int d = 0; d < Dim; ++d)
253 pos((keep + j) * Dim + d) = rp(j * Dim + d);
254 });
255 if (stride) {
256 View<char> ry = toDevice(hRecvPay, "pmd::recvPay");
257 Kokkos::parallel_for(
258 "pmd::unpack_pay", Kokkos::RangePolicy<ExecSpace>(0, recv),
259 KOKKOS_LAMBDA(const Index j) {
260 for (std::size_t b = 0; b < stride; ++b)
261 payload((keep + j) * stride + b) = ry(j * stride + b);
262 });
263 }
264 }
265 Kokkos::fence();
266 return keep + recv;
267 }
268
269 private:
270 void ensureScratch(std::size_t posLen, std::size_t payLen) {
271 if (tmpPos_.extent(0) < posLen)
272 tmpPos_ =
273 View<double>(Kokkos::view_alloc("pmd::tmpPos", Kokkos::WithoutInitializing), posLen);
274 if (tmpPay_.extent(0) < payLen)
275 tmpPay_ = View<char>(Kokkos::view_alloc("pmd::tmpPay", Kokkos::WithoutInitializing), payLen);
276 }
277
278 int rank_ = 0;
279 MPI_Comm comm_ = MPI_COMM_WORLD;
280 View<int> splitDim_;
281 View<Index> splitVal_;
282 double origin_[Dim] = {}, cellSize_[Dim] = {};
283 Index gsize_[Dim] = {};
284 char periodic_[Dim] = {};
285 View<double> tmpPos_;
286 View<char> tmpPay_;
287 std::size_t sent_ = 0, received_ = 0;
288};
289
290} // namespace peclet::core::halo
291
292#endif // PECLET_CORE_HALO_PARTICLE_MIGRATOR_VIEW_HPP
Device counterpart of ParticleMigrator.
Index migrate(View< double > pos, View< char > payload, Index n, std::size_t stride)
Reassign every particle (i ∈ [0,n)) to its owning rank, in place.
void init(const decomp::BlockDecomposer< Dim > &dec, int rank, DomainMap< Dim > map, MPI_Comm comm=MPI_COMM_WORLD)
int MPI_Comm
Definition mpi_stub.hpp:16
#define MPI_COMM_WORLD
Definition mpi_stub.hpp:26
View< T > toDevice(const std::vector< T > &h, const std::string &label)
Upload a host std::vector into a freshly-sized device View (empty vector => empty view).
Definition view.hpp:44
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26
Kokkos::View< Index *, MemSpace > IndexView
Device array of grid/particle indices (the matched send/recv/self-copy lists).
Definition view.hpp:34
std::int64_t Index
Signed index type for grids and particles (supersedes block_decomposer's long int IndxT).
Definition types.hpp:15