peclet-dem
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
mpi_halo.hpp
Go to the documentation of this file.
1
16#ifndef PECLET_DEM_MPI_HALO_HPP
17#define PECLET_DEM_MPI_HALO_HPP
18#ifdef PECLET_DEM_MPI
19
20#include <mpi.h>
21
22#include <array>
23#include <cstring>
24#include <Kokkos_Core.hpp>
25#include <stdexcept>
26#include <string>
27#include <vector>
28
29#include "dem_portable.hpp" // F3, F4
30#include "particles.hpp" // Particles, V3/V4/Vf/Vi, CpExec/CpMem
31#include "peclet/core/common/types.hpp"
32#include "peclet/core/common/view.hpp"
33#include "peclet/core/decomp/block_decomposer.hpp"
34#include "peclet/core/halo/particle_halo.hpp"
35#include "peclet/core/halo/particle_halo_topology.hpp"
36#include "peclet/core/halo/particle_migrator.hpp"
37#include "peclet/core/halo/particle_rebalance.hpp"
38
39namespace peclet::dem {
40
41// All non-position per-particle state in one record, so the substep gather is a single MPI exchange
42// (latency dominates the host-staged path). Mirrors MpiParticleHalo::GatherPack; positions are
43// forwarded separately (they need the periodic image shift). invMass is its own field here (the
44// CUDA SoA carries it in pos.w; the Kokkos SoA keeps it in a separate array). POD =>
45// MPI_BYTE-copyable.
46struct MpiGatherPack {
47 F3 vel, velPred, angVel, angVelPred, invInertia;
48 F4 quat, quatPred;
49 float scale, invMass;
50 int shape;
51};
52
53// The committed per-particle state that defines a particle across steps — everything except its
54// position (which drives ownership and travels as the migrator's coordinate) and the predicted /
55// delta / ghost scratch the step rebuilds. This is the payload moved when a particle changes owner
56// during a load re-balance. POD => MPI_BYTE-copyable.
57struct MigratePack {
58 F4 quat;
59 F3 vel, angVel, invInertia;
60 float invMass, scale, targetScale;
61 int shapeId;
62 float planeFric0, planeFric1;
63};
64
65// --- free-function pack/unpack kernels (namespace scope: nvcc forbids KOKKOS_LAMBDA in member fns)
66// ---
67
68inline void haloPackF3(V3 field, peclet::core::View<F3> owned, int n) {
69 Kokkos::parallel_for(
70 "peclet::dem::halo::packF3", Kokkos::RangePolicy<CpExec>(0, n),
71 KOKKOS_LAMBDA(int i) { owned(i) = F3{field(i, 0), field(i, 1), field(i, 2)}; });
72}
73inline void haloPackF4(V4 field, peclet::core::View<F4> owned, int n) {
74 Kokkos::parallel_for(
75 "peclet::dem::halo::packF4", Kokkos::RangePolicy<CpExec>(0, n),
76 KOKKOS_LAMBDA(int i) { owned(i) = F4{field(i, 0), field(i, 1), field(i, 2), field(i, 3)}; });
77}
78// ghost[g] -> field(no+g,:), optionally adding the per-ghost periodic image shift (positions only).
79inline void haloUnpackF3(V3 field, peclet::core::View<F3> ghost, peclet::core::View<F3> shift,
80 int no, int ng, bool doShift) {
81 Kokkos::parallel_for(
82 "peclet::dem::halo::unpackF3", Kokkos::RangePolicy<CpExec>(0, ng), KOKKOS_LAMBDA(int g) {
83 F3 v = ghost(g);
84 if (doShift) {
85 v.x += shift(g).x;
86 v.y += shift(g).y;
87 v.z += shift(g).z;
88 }
89 field(no + g, 0) = v.x;
90 field(no + g, 1) = v.y;
91 field(no + g, 2) = v.z;
92 });
93}
94inline void haloUnpackF4(V4 field, peclet::core::View<F4> ghost, int no, int ng) {
95 Kokkos::parallel_for(
96 "peclet::dem::halo::unpackF4", Kokkos::RangePolicy<CpExec>(0, ng), KOKKOS_LAMBDA(int g) {
97 field(no + g, 0) = ghost(g).x;
98 field(no + g, 1) = ghost(g).y;
99 field(no + g, 2) = ghost(g).z;
100 field(no + g, 3) = ghost(g).w;
101 });
102}
103
104inline void haloPackGather(V3 vel, V3 velPred, V3 angVel, V3 angVelPred, V3 invInertia, V4 quat,
105 V4 quatPred, Vf scale, Vf invMass, Vi shapeId,
106 peclet::core::View<MpiGatherPack> owned, int n) {
107 Kokkos::parallel_for(
108 "peclet::dem::halo::packGather", Kokkos::RangePolicy<CpExec>(0, n), KOKKOS_LAMBDA(int i) {
109 MpiGatherPack g;
110 g.vel = F3{vel(i, 0), vel(i, 1), vel(i, 2)};
111 g.velPred = F3{velPred(i, 0), velPred(i, 1), velPred(i, 2)};
112 g.angVel = F3{angVel(i, 0), angVel(i, 1), angVel(i, 2)};
113 g.angVelPred = F3{angVelPred(i, 0), angVelPred(i, 1), angVelPred(i, 2)};
114 g.invInertia = F3{invInertia(i, 0), invInertia(i, 1), invInertia(i, 2)};
115 g.quat = F4{quat(i, 0), quat(i, 1), quat(i, 2), quat(i, 3)};
116 g.quatPred = F4{quatPred(i, 0), quatPred(i, 1), quatPred(i, 2), quatPred(i, 3)};
117 g.scale = scale(i);
118 g.invMass = invMass(i);
119 g.shape = shapeId(i);
120 owned(i) = g;
121 });
122}
123// Unpack the gathered owner state into the ghost slots [no, no+ng) and self-map realIndices (the
124// owner is remote, so velocity/position deltas landing on the ghost slot are discarded next
125// forward).
126inline void haloUnpackGather(V3 vel, V3 velPred, V3 angVel, V3 angVelPred, V3 invInertia, V4 quat,
127 V4 quatPred, Vf scale, Vf invMass, Vi shapeId, Vi realIndices,
128 peclet::core::View<MpiGatherPack> ghost, int no, int ng) {
129 Kokkos::parallel_for(
130 "peclet::dem::halo::unpackGather", Kokkos::RangePolicy<CpExec>(0, ng), KOKKOS_LAMBDA(int g) {
131 const MpiGatherPack p = ghost(g);
132 const int s = no + g;
133 vel(s, 0) = p.vel.x;
134 vel(s, 1) = p.vel.y;
135 vel(s, 2) = p.vel.z;
136 velPred(s, 0) = p.velPred.x;
137 velPred(s, 1) = p.velPred.y;
138 velPred(s, 2) = p.velPred.z;
139 angVel(s, 0) = p.angVel.x;
140 angVel(s, 1) = p.angVel.y;
141 angVel(s, 2) = p.angVel.z;
142 angVelPred(s, 0) = p.angVelPred.x;
143 angVelPred(s, 1) = p.angVelPred.y;
144 angVelPred(s, 2) = p.angVelPred.z;
145 invInertia(s, 0) = p.invInertia.x;
146 invInertia(s, 1) = p.invInertia.y;
147 invInertia(s, 2) = p.invInertia.z;
148 quat(s, 0) = p.quat.x;
149 quat(s, 1) = p.quat.y;
150 quat(s, 2) = p.quat.z;
151 quat(s, 3) = p.quat.w;
152 quatPred(s, 0) = p.quatPred.x;
153 quatPred(s, 1) = p.quatPred.y;
154 quatPred(s, 2) = p.quatPred.z;
155 quatPred(s, 3) = p.quatPred.w;
156 scale(s) = p.scale;
157 invMass(s) = p.invMass;
158 shapeId(s) = p.shape;
159 realIndices(s) = s;
160 });
161}
162
165class ParticleHalo {
166 public:
167 // Block decomposition over the GLOBAL domain (the per-block solver stays non-periodic; the halo
168 // supplies the periodic wrap). gsize is the ORB cell grid. Mirrors MpiParticleHalo::init.
169 void initMpi(std::array<double, 3> origin, std::array<double, 3> size, std::array<long, 3> gsize,
170 std::array<bool, 3> periodic, MPI_Comm comm) {
171 comm_ = comm;
172 int sz = 1;
173 MPI_Comm_rank(comm_, &rank_);
174 MPI_Comm_size(comm_, &sz);
175 dec_.init(static_cast<std::size_t>(sz), peclet::core::IVec<3>{gsize[0], gsize[1], gsize[2]});
176 peclet::core::halo::DomainMap<3> map;
177 for (int i = 0; i < 3; ++i) {
178 map.origin[i] = origin[i];
179 map.cellSize[i] = size[i] / static_cast<double>(gsize[i]);
180 map.periodic[i] = periodic[i];
181 }
182 map_ = map;
183 mig_.init(dec_, rank_, map, comm_);
184 halo_.init(mig_);
185 inited_ = true;
186 }
187 // Shared-decomposition overload: adopt an EXTERNALLY-built ORB (so dem shares one BlockDecomposer
188 // with the flow solver in a coupled run, and migrateTo() can move onto a re-decomposed partition).
189 // `size`/`origin` map the ORB cell grid (= dec.globalSize()) to the physical domain.
190 void initMpi(const peclet::core::decomp::BlockDecomposer<3>& dec, std::array<double, 3> origin,
191 std::array<double, 3> size, std::array<bool, 3> periodic, MPI_Comm comm) {
192 comm_ = comm;
193 MPI_Comm_rank(comm_, &rank_);
194 dec_ = dec;
195 const auto& gs = dec.globalSize();
196 peclet::core::halo::DomainMap<3> map;
197 for (int i = 0; i < 3; ++i) {
198 map.origin[i] = origin[i];
199 map.cellSize[i] = size[i] / static_cast<double>(gs[i]);
200 map.periodic[i] = periodic[i];
201 }
202 map_ = map;
203 mig_.init(dec_, rank_, map, comm_);
204 halo_.init(mig_);
205 inited_ = true;
206 }
207 const peclet::core::decomp::BlockDecomposer<3>& decomposer() const { return dec_; }
208
209 bool inited() const { return inited_; }
210 int rank() const { return rank_; }
211 int numGhost() const { return numGhost_; }
212
220 void setVerletSkin(float skin) { verletSkin_ = skin < 0.0f ? 0.0f : skin; }
221 float verletSkin() const { return verletSkin_; }
224 long numRebuilds() const { return nRebuild_; }
225 long numGathers() const { return nGather_; }
226
227 // Rebuild the owner<->ghost correspondence over the current owned positions and populate the
228 // ghost slots [numReal, numReal+numGhost) with the owners' full forwarded state. Sets
229 // P.numParticles. Returns numReal+numGhost. Faithful port of Simulation::mpi_gather_ghosts.
230 int gather(Particles& P, double rcut) {
231 const int no = P.numReal;
232 ++nGather_;
233
234 // Decide whether to rebuild the owner↔ghost topology or reuse the cached one (Verlet skin, D2).
235 // A rebuild is forced when reuse is off (skin==0), on the first gather, when the owned count
236 // changed (a migration happened ⇒ topology invalid), or when an owned particle has displaced ≥
237 // skin since the last build (a particle could have entered the rcut band without being in the
238 // rcut+skin list).
239 bool rebuild = (verletSkin_ <= 0.0f) || !haveTopo_ || (no != lastNumReal_);
240 if (!rebuild && maxOwnedDisplacement(P.pos, no) >= verletSkin_)
241 rebuild = true;
242 numReal_ = no;
243
244 if (rebuild) {
245 ++nRebuild_;
246 const double band = rcut + static_cast<double>(verletSkin_);
247 // (1) download owned positions, (re)build the host halo topology, capture it on device.
248 auto hpos = Kokkos::create_mirror_view(P.pos);
249 Kokkos::deep_copy(hpos, P.pos);
250 std::vector<peclet::core::Vec<3>> pv(static_cast<std::size_t>(no));
251 for (int i = 0; i < no; ++i)
252 pv[i] = peclet::core::Vec<3>{hpos(i, 0), hpos(i, 1), hpos(i, 2)};
253 // includePeriodicSelf: a rank that owns a full (undecomposed) periodic axis -- a "x1" ORB
254 // axis (e.g. z of a 2x2x1 layout) or np=1 -- is its own periodic image on that axis, so the
255 // periodic neighbours are local self-ghosts the cross-rank exchange never makes. This
256 // supplies them.
257 halo_.build(pv, band, /*includePeriodicSelf=*/true);
258 dev_.init(halo_);
259 const int ng = static_cast<int>(halo_.numGhost());
260 // The halo topology (forward / device self-gather) writes ALL ng ghost slots [no, no+ng); the
261 // Particles SoA must have room for them. Silently truncating ng here would leave the halo
262 // writing past the truncated count -> out-of-bounds SoA writes (memory corruption, not a
263 // clean drop). So require adequate capacity and fail loudly instead. Size Simulation capacity
264 // for the worst-case ghost band (a fully periodic box at rcut+skin needs a thick boundary
265 // layer of ghosts).
266 if (no + ng > P.capacity)
267 throw std::runtime_error(
268 "ParticleHalo::gather: ghost overflow -- need capacity >= " + std::to_string(no + ng) +
269 " (numReal=" + std::to_string(no) + " + numGhost=" + std::to_string(ng) + "), have " +
270 std::to_string(P.capacity) + "; increase the Simulation capacity.");
271 numGhost_ = ng;
272 allocBuffers(no, ng);
273 uploadShift();
274 // Snapshot the owned positions at build time — the reference for the displacement check.
275 if (verletSkin_ > 0.0f) {
276 if (refPos_.extent(0) < static_cast<std::size_t>(no))
277 refPos_ = V3("peclet::dem::halo::refPos", static_cast<std::size_t>(P.capacity));
278 Kokkos::deep_copy(Kokkos::subview(refPos_, std::pair<int, int>(0, no), Kokkos::ALL),
279 Kokkos::subview(P.pos, std::pair<int, int>(0, no), Kokkos::ALL));
280 }
281 haveTopo_ = true;
282 lastNumReal_ = no;
283 }
284
285 const int ng = numGhost_;
286 P.numParticles = no + ng;
287 // self-map realIndices for the reals (owner deltas land on themselves); done every step like
288 // demStep.
289 selfMapReals(P.realIndices, no);
290 if (ng == 0)
291 return no;
292
293 // (2) positions (committed + predicted) with the periodic image shift. d_pos_pred was already
294 // advanced by predict_velocity, so it is forwarded too (NOT copied from pos) -- see CUDA
295 // comment.
296 forwardPositions(P.pos);
297 forwardPositions(P.posPred);
298
299 // (3) all other state packed into one record -> single exchange -> ghost slots + self-mapped
300 // idx.
301 haloPackGather(P.vel, P.velPred, P.angVel, P.angVelPred, P.invInertia, P.quat, P.quatPred,
302 P.scale, P.invMass, P.shapeId, ownedPack_, no);
303 dev_.forward(ownedPack_, ghostPack_);
304 haloUnpackGather(P.vel, P.velPred, P.angVel, P.angVelPred, P.invInertia, P.quat, P.quatPred,
305 P.scale, P.invMass, P.shapeId, P.realIndices, ghostPack_, no, ng);
306 return no + ng;
307 }
308
309 // Dynamic load re-balance: re-decompose the ORB by per-block particle COUNT (weighted ORB) and
310 // migrate each owned particle, with its committed state, to its new owner. A pure redistribution
311 // of the same global particle set (count conserved, per-particle state preserved) — only
312 // ownership and this rank's owned slice change; the physics result is unchanged. Must be called
313 // at a step boundary (committed pos/quat/vel/angVel valid; predicted/delta/ghost scratch are
314 // rebuilt next gather()). Returns this rank's new owned count. No-op-safe at np=1.
315 // Weighted re-decompose by particle count (the equal-cell ORB imbalances as particles cluster)
316 // and migrate owners. dec_ updated in place; mig_ points to it. Returns the new owned count.
317 int rebalance(Particles& P) {
318 std::vector<peclet::core::Vec<3>> pos;
319 std::vector<char> payload;
320 packState(P, pos, payload);
321 const std::size_t newN = peclet::core::halo::rebalanceByParticleCount(
322 dec_, mig_, pos, payload, sizeof(MigratePack), comm_);
323 if ((int)newN > P.capacity)
324 throw std::runtime_error("ParticleHalo::rebalance: owned overflow -- rank received " +
325 std::to_string(newN) + " particles, capacity " +
326 std::to_string(P.capacity));
327 unpackState(P, pos, payload, newN);
328 return (int)newN;
329 }
330 // Migrate particles onto an EXTERNALLY-supplied decomposition (dynamic co-rebalancing: the same
331 // BlockDecomposer the flow solver redistributes onto). Pure ownership move — counts/state
332 // conserved. Must be called at a step boundary. No-op-safe at np=1.
333 int migrateTo(Particles& P, const peclet::core::decomp::BlockDecomposer<3>& newDec) {
334 std::vector<peclet::core::Vec<3>> pos;
335 std::vector<char> payload;
336 packState(P, pos, payload);
337 dec_ = newDec; // in place: mig_ still points at dec_; mig_.migrate() sends to dec_.ownerOf(...)
338 const std::size_t newN = mig_.migrate(pos, payload, sizeof(MigratePack));
339 if ((int)newN > P.capacity)
340 throw std::runtime_error("ParticleHalo::migrateTo: owned overflow -- rank received " +
341 std::to_string(newN) + " particles, capacity " +
342 std::to_string(P.capacity));
343 unpackState(P, pos, payload, newN);
344 return (int)newN;
345 }
346 // Migrate onto the weighted ORB of per-cell weights `w` (global x-fastest, matching the ORB grid).
347 // The Lagrangian half of the co-rebalance: dem builds the SAME deterministic partition flow does
348 // from the same weight field, so no BlockDecomposer object crosses the language boundary.
349 int migrateToWeights(Particles& P, const std::vector<peclet::core::Real>& w) {
350 int size = 1;
351 MPI_Comm_size(comm_, &size);
352 peclet::core::decomp::BlockDecomposer<3> newDec((std::size_t)size, dec_.globalSize(), w);
353 return migrateTo(P, newDec);
354 }
355
356 private:
357 // Download the committed state and pack it (position drives ownership; the rest is the payload).
358 void packState(Particles& P, std::vector<peclet::core::Vec<3>>& pos, std::vector<char>& payload) {
359 const int no = P.numReal;
360 auto h_pos = Kokkos::create_mirror_view(P.pos);
361 auto h_quat = Kokkos::create_mirror_view(P.quat);
362 auto h_vel = Kokkos::create_mirror_view(P.vel);
363 auto h_angVel = Kokkos::create_mirror_view(P.angVel);
364 auto h_invI = Kokkos::create_mirror_view(P.invInertia);
365 auto h_invM = Kokkos::create_mirror_view(P.invMass);
366 auto h_scale = Kokkos::create_mirror_view(P.scale);
367 auto h_tScale = Kokkos::create_mirror_view(P.targetScale);
368 auto h_shape = Kokkos::create_mirror_view(P.shapeId);
369 auto h_pf = Kokkos::create_mirror_view(P.planeFriction);
370 Kokkos::deep_copy(h_pos, P.pos);
371 Kokkos::deep_copy(h_quat, P.quat);
372 Kokkos::deep_copy(h_vel, P.vel);
373 Kokkos::deep_copy(h_angVel, P.angVel);
374 Kokkos::deep_copy(h_invI, P.invInertia);
375 Kokkos::deep_copy(h_invM, P.invMass);
376 Kokkos::deep_copy(h_scale, P.scale);
377 Kokkos::deep_copy(h_tScale, P.targetScale);
378 Kokkos::deep_copy(h_shape, P.shapeId);
379 Kokkos::deep_copy(h_pf, P.planeFriction);
380 pos.assign((std::size_t)no, peclet::core::Vec<3>{});
381 payload.assign((std::size_t)no * sizeof(MigratePack), 0);
382 for (int i = 0; i < no; ++i) {
383 pos[(std::size_t)i] = peclet::core::Vec<3>{h_pos(i, 0), h_pos(i, 1), h_pos(i, 2)};
384 MigratePack m;
385 m.quat = F4{h_quat(i, 0), h_quat(i, 1), h_quat(i, 2), h_quat(i, 3)};
386 m.vel = F3{h_vel(i, 0), h_vel(i, 1), h_vel(i, 2)};
387 m.angVel = F3{h_angVel(i, 0), h_angVel(i, 1), h_angVel(i, 2)};
388 m.invInertia = F3{h_invI(i, 0), h_invI(i, 1), h_invI(i, 2)};
389 m.invMass = h_invM(i);
390 m.scale = h_scale(i);
391 m.targetScale = h_tScale(i);
392 m.shapeId = h_shape(i);
393 m.planeFric0 = h_pf(i, 0);
394 m.planeFric1 = h_pf(i, 1);
395 std::memcpy(&payload[(std::size_t)i * sizeof(MigratePack)], &m, sizeof(MigratePack));
396 }
397 }
398 // Unpack the migrated particles back into the SoA [0,newN) and upload.
399 void unpackState(Particles& P, const std::vector<peclet::core::Vec<3>>& pos,
400 const std::vector<char>& payload, std::size_t newN) {
401 auto h_pos = Kokkos::create_mirror_view(P.pos);
402 auto h_quat = Kokkos::create_mirror_view(P.quat);
403 auto h_vel = Kokkos::create_mirror_view(P.vel);
404 auto h_angVel = Kokkos::create_mirror_view(P.angVel);
405 auto h_invI = Kokkos::create_mirror_view(P.invInertia);
406 auto h_invM = Kokkos::create_mirror_view(P.invMass);
407 auto h_scale = Kokkos::create_mirror_view(P.scale);
408 auto h_tScale = Kokkos::create_mirror_view(P.targetScale);
409 auto h_shape = Kokkos::create_mirror_view(P.shapeId);
410 auto h_pf = Kokkos::create_mirror_view(P.planeFriction);
411 for (std::size_t i = 0; i < newN; ++i) {
412 h_pos((int)i, 0) = pos[i][0];
413 h_pos((int)i, 1) = pos[i][1];
414 h_pos((int)i, 2) = pos[i][2];
415 MigratePack m;
416 std::memcpy(&m, &payload[i * sizeof(MigratePack)], sizeof(MigratePack));
417 h_quat((int)i, 0) = m.quat.x;
418 h_quat((int)i, 1) = m.quat.y;
419 h_quat((int)i, 2) = m.quat.z;
420 h_quat((int)i, 3) = m.quat.w;
421 h_vel((int)i, 0) = m.vel.x;
422 h_vel((int)i, 1) = m.vel.y;
423 h_vel((int)i, 2) = m.vel.z;
424 h_angVel((int)i, 0) = m.angVel.x;
425 h_angVel((int)i, 1) = m.angVel.y;
426 h_angVel((int)i, 2) = m.angVel.z;
427 h_invI((int)i, 0) = m.invInertia.x;
428 h_invI((int)i, 1) = m.invInertia.y;
429 h_invI((int)i, 2) = m.invInertia.z;
430 h_invM((int)i) = m.invMass;
431 h_scale((int)i) = m.scale;
432 h_tScale((int)i) = m.targetScale;
433 h_shape((int)i) = m.shapeId;
434 h_pf((int)i, 0) = m.planeFric0;
435 h_pf((int)i, 1) = m.planeFric1;
436 }
437 Kokkos::deep_copy(P.pos, h_pos);
438 Kokkos::deep_copy(P.quat, h_quat);
439 Kokkos::deep_copy(P.vel, h_vel);
440 Kokkos::deep_copy(P.angVel, h_angVel);
441 Kokkos::deep_copy(P.invInertia, h_invI);
442 Kokkos::deep_copy(P.invMass, h_invM);
443 Kokkos::deep_copy(P.scale, h_scale);
444 Kokkos::deep_copy(P.targetScale, h_tScale);
445 Kokkos::deep_copy(P.shapeId, h_shape);
446 Kokkos::deep_copy(P.planeFriction, h_pf);
447
448 P.numReal = (int)newN;
449 P.numParticles = (int)newN;
450 }
451
452 public:
453 // owner slice [0,numReal) -> ghost slots [numReal,..), verbatim (velocity / angular velocity).
454 void forward(V3 field) {
455 if (numGhost_ == 0)
456 return;
457 haloPackF3(field, ownedF3_, numReal_);
458 dev_.forward(ownedF3_, ghostF3_);
459 haloUnpackF3(field, ghostF3_, shiftDev_, numReal_, numGhost_, /*doShift=*/false);
460 }
461 // owner slice -> ghost slots with the periodic image shift added (positions).
462 void forwardPositions(V3 field) {
463 if (numGhost_ == 0)
464 return;
465 haloPackF3(field, ownedF3_, numReal_);
466 dev_.forward(ownedF3_, ghostF3_);
467 haloUnpackF3(field, ghostF3_, shiftDev_, numReal_, numGhost_, /*doShift=*/true);
468 }
469 // owner slice -> ghost slots, verbatim (quaternions).
470 void forward4(V4 field) {
471 if (numGhost_ == 0)
472 return;
473 haloPackF4(field, ownedF4_, numReal_);
474 dev_.forward(ownedF4_, ghostF4_);
475 haloUnpackF4(field, ghostF4_, numReal_, numGhost_);
476 }
477
478 void selfMapReals(Vi realIndices, int no) {
479 Kokkos::parallel_for(
480 "peclet::dem::halo::selfMapReals", Kokkos::RangePolicy<CpExec>(0, no),
481 KOKKOS_LAMBDA(int i) { realIndices(i) = i; });
482 }
483
484 private:
485 void allocBuffers(int no, int ng) {
486 // Exact-sized: ParticleHalo::forward host-stages a deep_copy into the ghost View, so
487 // its extent must equal numGhost; owned is indexed by sendIdx in [0,numReal).
488 ownedF3_ = peclet::core::View<F3>("peclet::dem::halo::ownedF3", no);
489 ghostF3_ = peclet::core::View<F3>("peclet::dem::halo::ghostF3", ng);
490 ownedF4_ = peclet::core::View<F4>("peclet::dem::halo::ownedF4", no);
491 ghostF4_ = peclet::core::View<F4>("peclet::dem::halo::ghostF4", ng);
492 ownedPack_ = peclet::core::View<MpiGatherPack>("peclet::dem::halo::ownedPack", no);
493 ghostPack_ = peclet::core::View<MpiGatherPack>("peclet::dem::halo::ghostPack", ng);
494 }
495 // Max Euclidean displacement of any owned particle since the last topology build (device reduce +
496 // one scalar read-back) — the Verlet-skin reuse criterion.
497 // public: nvcc forbids an extended __host__ __device__ (KOKKOS_LAMBDA) lambda inside a private
498 // method.
499 public:
500 float maxOwnedDisplacement(const V3& pos, int no) const {
501 if (no <= 0 || refPos_.extent(0) < static_cast<std::size_t>(no))
502 return 1e30f;
503 float md = 0.0f;
504 V3 p = pos, r = refPos_;
505 Kokkos::parallel_reduce(
506 "peclet::dem::halo::maxdisp", Kokkos::RangePolicy<CpExec>(0, no),
507 KOKKOS_LAMBDA(const int i, float& m) {
508 const float dx = p(i, 0) - r(i, 0), dy = p(i, 1) - r(i, 1), dz = p(i, 2) - r(i, 2);
509 const float d = Kokkos::sqrt(dx * dx + dy * dy + dz * dz);
510 if (d > m)
511 m = d;
512 },
513 Kokkos::Max<float>(md));
514 return md;
515 }
516
517 private:
518 void uploadShift() {
519 auto t = halo_.flatten();
520 std::vector<F3> hs(t.shift.size());
521 for (std::size_t i = 0; i < t.shift.size(); ++i)
522 hs[i] = F3{static_cast<float>(t.shift[i][0]), static_cast<float>(t.shift[i][1]),
523 static_cast<float>(t.shift[i][2])};
524 shiftDev_ = peclet::core::toDevice(hs, "peclet::dem::halo::shift");
525 }
526
527 bool inited_ = false;
528 int rank_ = 0, numReal_ = 0, numGhost_ = 0;
529 // Verlet-skin reuse state (D2): skin width, the build-time owned positions, and the cache-valid
530 // flags.
531 float verletSkin_ = 0.0f;
532 bool haveTopo_ = false;
533 int lastNumReal_ = -1;
534 long nRebuild_ = 0, nGather_ = 0;
535 V3 refPos_;
536 MPI_Comm comm_ = MPI_COMM_NULL;
537 peclet::core::decomp::BlockDecomposer<3> dec_;
538 peclet::core::halo::DomainMap<3> map_; // physical<->cell mapping (for migrateTo)
539 peclet::core::halo::ParticleMigrator<3> mig_;
540 peclet::core::halo::ParticleHaloTopology<3> halo_;
541 peclet::core::halo::ParticleHalo<3> dev_;
542 peclet::core::View<F3> ownedF3_, ghostF3_, shiftDev_;
543 peclet::core::View<F4> ownedF4_, ghostF4_;
544 peclet::core::View<MpiGatherPack> ownedPack_, ghostPack_;
545};
546
547} // namespace peclet::dem
548
549#endif // PECLET_DEM_MPI
550#endif // PECLET_DEM_MPI_HALO_HPP
dem — portable POD types + math + analytic SDFs shared by the Kokkos kernel ports.
Kokkos::View< float *, CpMem > Vf
Kokkos::View< int *, CpMem > Vi
Kokkos::View< float *[3], CpMem > V3
Kokkos::View< float *[4], CpMem > V4
dem — portable (Kokkos) particle SoA container: the storage the dem flip pivots on.