peclet-dem 0.4.0
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 <algorithm>
23#include <array>
24#include <cmath>
25#include <cstring>
26#include <Kokkos_Core.hpp>
27#include <stdexcept>
28#include <string>
29#include <unordered_map>
30#include <vector>
31
32#include "dem_portable.hpp" // F3, F4
33#include "particles.hpp" // Particles, V3/V4/Vf/Vi, CpExec/CpMem
34#include "peclet/core/common/types.hpp"
35#include "peclet/core/common/view.hpp"
36#include "peclet/core/decomp/block_decomposer.hpp"
37#include "peclet/core/halo/particle_halo.hpp"
38#include "peclet/core/halo/particle_halo_topology.hpp"
39#include "peclet/core/halo/particle_migrator.hpp"
40#include "peclet/core/halo/particle_rebalance.hpp"
41
42namespace peclet::dem {
43
44// All non-position per-particle state in one record, so the substep gather is a single MPI exchange
45// (latency dominates the host-staged path). Mirrors MpiParticleHalo::GatherPack; positions are
46// forwarded separately (they need the periodic image shift). invMass is its own field here (the
47// CUDA SoA carries it in pos.w; the Kokkos SoA keeps it in a separate array). POD =>
48// MPI_BYTE-copyable.
49struct MpiGatherPack {
50 F3 vel, velPred, angVel, angVelPred, invInertia;
51 F4 quat, quatPred;
52 float scale, invMass;
53 int shape;
54 // Modern-solver state a ghost must mirror from its owner: the global id (persistent-pair keys
55 // are gid-based — local slots are not stable identities across ranks / halo rebuilds), the
56 // material id (the narrowphase reads matId by RAW slot index), and the owner's warm grounded
57 // level (Guendelman support levels; the rank-local propagation sweeps continue from it, so a
58 // support chain crossing a rank boundary stays grounded).
59 int gid;
60 unsigned char material, grounded;
61 // Poisson-restitution orphan account (owner-authoritative; the ghost copy's rank-local
62 // drawdowns are overwritten by the next mirror — the redundant ghost-pair solve computes
63 // the identical drawdown on the owner).
64 float orphan, orphanVPeak;
65};
66
67// One persistent-contact ledger entry carried through an ownership migration: the gid-based pair
68// key plus the previous substep's converged normal / tangential impulses and position-channel
69// load (the warm start + Coulomb-bound carry). POD => MPI_BYTE-copyable.
70struct WarmPairEntry {
71 unsigned long long key;
72 float lambda;
73 float lambdaT[3];
74 float posImpulse;
75 float restBank; // event-level (Poisson) restitution: remaining owed separation impulse
76 float restVPeak; // ... and the event's peak approach speed (> 0 = event active)
77};
78// Per-particle cap on carried pairs (sphere kissing number 12 + wall; lowest-weight entries are
79// dropped beyond it — a dropped entry only costs the receiving rank a cold warm-start there).
80inline constexpr int kWarmCarryMax = 14;
81
82// One force-engine (Hertz–Mindlin) per-pair history entry carried through an ownership migration:
83// the gid-based pair key + the Mindlin shear spring xi. The lagged patch stiffness (snPair) is NOT
84// carried — it resets at every pair-list rebuild anyway, and a migration forces one.
85struct HertzPairEntry {
86 unsigned long long key;
87 float xi[3];
88};
89
90// The committed per-particle state that defines a particle across steps — everything except its
91// position (which drives ownership and travels as the migrator's coordinate) and the predicted /
92// delta / ghost scratch the step rebuilds. This is the payload moved when a particle changes owner
93// during a load re-balance. POD => MPI_BYTE-copyable. Carries the particle's slice of the
94// persistent-contact ledger (each pair rides on BOTH endpoints; the unpack dedupes by key) plus
95// its grounded level, so a rebalance does not cold-restart the statics force network.
96struct MigratePack {
97 F4 quat;
98 F3 vel, angVel, invInertia;
99 float invMass, scale, targetScale;
100 int shapeId;
101 float planeFric0, planeFric1;
102 int gid;
103 unsigned char materialId, groundedLevel, numWarm, numHertz;
104 float orphan, orphanVPeak; // Poisson orphan account rides with its body across ownership
105 WarmPairEntry warm[kWarmCarryMax];
106 // Force-engine (Hertz–Mindlin) history: the particle's slice of the cached pair list's Mindlin
107 // springs plus its per-(particle, wall) shear history + lagged wall patch stiffness.
108 HertzPairEntry hertz[kWarmCarryMax];
109 float hertzXiWall[Particles::kHertzMaxWalls][3];
110 float hertzSnWall[Particles::kHertzMaxWalls];
111};
112
113// --- free-function pack/unpack kernels (namespace scope: nvcc forbids KOKKOS_LAMBDA in member fns)
114// ---
115
116inline void haloPackF3(V3 field, peclet::core::View<F3> owned, int n) {
117 Kokkos::parallel_for(
118 "peclet::dem::halo::packF3", Kokkos::RangePolicy<CpExec>(0, n),
119 KOKKOS_LAMBDA(int i) { owned(i) = F3{field(i, 0), field(i, 1), field(i, 2)}; });
120}
121inline void haloPackF4(V4 field, peclet::core::View<F4> owned, int n) {
122 Kokkos::parallel_for(
123 "peclet::dem::halo::packF4", Kokkos::RangePolicy<CpExec>(0, n),
124 KOKKOS_LAMBDA(int i) { owned(i) = F4{field(i, 0), field(i, 1), field(i, 2), field(i, 3)}; });
125}
126// ghost[g] -> field(no+g,:), optionally adding the per-ghost periodic image shift (positions only).
127inline void haloUnpackF3(V3 field, peclet::core::View<F3> ghost, peclet::core::View<F3> shift,
128 int no, int ng, bool doShift) {
129 Kokkos::parallel_for(
130 "peclet::dem::halo::unpackF3", Kokkos::RangePolicy<CpExec>(0, ng), KOKKOS_LAMBDA(int g) {
131 F3 v = ghost(g);
132 if (doShift) {
133 v.x += shift(g).x;
134 v.y += shift(g).y;
135 v.z += shift(g).z;
136 }
137 field(no + g, 0) = v.x;
138 field(no + g, 1) = v.y;
139 field(no + g, 2) = v.z;
140 });
141}
142inline void haloUnpackF4(V4 field, peclet::core::View<F4> ghost, int no, int ng) {
143 Kokkos::parallel_for(
144 "peclet::dem::halo::unpackF4", Kokkos::RangePolicy<CpExec>(0, ng), KOKKOS_LAMBDA(int g) {
145 field(no + g, 0) = ghost(g).x;
146 field(no + g, 1) = ghost(g).y;
147 field(no + g, 2) = ghost(g).z;
148 field(no + g, 3) = ghost(g).w;
149 });
150}
151
152inline void haloPackGather(V3 vel, V3 velPred, V3 angVel, V3 angVelPred, V3 invInertia, V4 quat,
153 V4 quatPred, Vf scale, Vf invMass, Vi shapeId, Vi gid,
154 Kokkos::View<unsigned char*, CpMem> materialId,
155 Kokkos::View<unsigned char*, CpMem> grounded, Vf orphan,
156 Vf orphanVPeak, peclet::core::View<MpiGatherPack> owned, int n) {
157 Kokkos::parallel_for(
158 "peclet::dem::halo::packGather", Kokkos::RangePolicy<CpExec>(0, n), KOKKOS_LAMBDA(int i) {
159 MpiGatherPack g;
160 g.vel = F3{vel(i, 0), vel(i, 1), vel(i, 2)};
161 g.velPred = F3{velPred(i, 0), velPred(i, 1), velPred(i, 2)};
162 g.angVel = F3{angVel(i, 0), angVel(i, 1), angVel(i, 2)};
163 g.angVelPred = F3{angVelPred(i, 0), angVelPred(i, 1), angVelPred(i, 2)};
164 g.invInertia = F3{invInertia(i, 0), invInertia(i, 1), invInertia(i, 2)};
165 g.quat = F4{quat(i, 0), quat(i, 1), quat(i, 2), quat(i, 3)};
166 g.quatPred = F4{quatPred(i, 0), quatPred(i, 1), quatPred(i, 2), quatPred(i, 3)};
167 g.scale = scale(i);
168 g.invMass = invMass(i);
169 g.shape = shapeId(i);
170 g.gid = gid(i);
171 g.material = materialId(i);
172 g.grounded = grounded(i);
173 g.orphan = orphan(i);
174 g.orphanVPeak = orphanVPeak(i);
175 owned(i) = g;
176 });
177}
178// Unpack the gathered owner state into the ghost slots [no, no+ng) and self-map realIndices (the
179// owner is remote, so velocity/position deltas landing on the ghost slot are discarded next
180// forward).
181inline void haloUnpackGather(V3 vel, V3 velPred, V3 angVel, V3 angVelPred, V3 invInertia, V4 quat,
182 V4 quatPred, Vf scale, Vf invMass, Vi shapeId, Vi realIndices, Vi gid,
183 Kokkos::View<unsigned char*, CpMem> materialId,
184 Kokkos::View<unsigned char*, CpMem> grounded, Vf orphan,
185 Vf orphanVPeak, peclet::core::View<MpiGatherPack> ghost, int no,
186 int ng) {
187 Kokkos::parallel_for(
188 "peclet::dem::halo::unpackGather", Kokkos::RangePolicy<CpExec>(0, ng), KOKKOS_LAMBDA(int g) {
189 const MpiGatherPack p = ghost(g);
190 const int s = no + g;
191 vel(s, 0) = p.vel.x;
192 vel(s, 1) = p.vel.y;
193 vel(s, 2) = p.vel.z;
194 velPred(s, 0) = p.velPred.x;
195 velPred(s, 1) = p.velPred.y;
196 velPred(s, 2) = p.velPred.z;
197 angVel(s, 0) = p.angVel.x;
198 angVel(s, 1) = p.angVel.y;
199 angVel(s, 2) = p.angVel.z;
200 angVelPred(s, 0) = p.angVelPred.x;
201 angVelPred(s, 1) = p.angVelPred.y;
202 angVelPred(s, 2) = p.angVelPred.z;
203 invInertia(s, 0) = p.invInertia.x;
204 invInertia(s, 1) = p.invInertia.y;
205 invInertia(s, 2) = p.invInertia.z;
206 quat(s, 0) = p.quat.x;
207 quat(s, 1) = p.quat.y;
208 quat(s, 2) = p.quat.z;
209 quat(s, 3) = p.quat.w;
210 quatPred(s, 0) = p.quatPred.x;
211 quatPred(s, 1) = p.quatPred.y;
212 quatPred(s, 2) = p.quatPred.z;
213 quatPred(s, 3) = p.quatPred.w;
214 scale(s) = p.scale;
215 invMass(s) = p.invMass;
216 shapeId(s) = p.shape;
217 realIndices(s) = s;
218 gid(s) = p.gid;
219 materialId(s) = p.material;
220 grounded(s) = p.grounded;
221 orphan(s) = p.orphan;
222 orphanVPeak(s) = p.orphanVPeak;
223 });
224}
225
228class ParticleHalo {
229 public:
230 // Block decomposition over the GLOBAL domain (the per-block solver stays non-periodic; the halo
231 // supplies the periodic wrap). gsize is the ORB cell grid. Mirrors MpiParticleHalo::init.
232 void initMpi(std::array<double, 3> origin, std::array<double, 3> size, std::array<long, 3> gsize,
233 std::array<bool, 3> periodic, MPI_Comm comm) {
234 comm_ = comm;
235 int sz = 1;
236 MPI_Comm_rank(comm_, &rank_);
237 MPI_Comm_size(comm_, &sz);
238 dec_.init(static_cast<std::size_t>(sz), peclet::core::IVec<3>{gsize[0], gsize[1], gsize[2]});
239 peclet::core::halo::DomainMap<3> map;
240 for (int i = 0; i < 3; ++i) {
241 map.origin[i] = origin[i];
242 map.cellSize[i] = size[i] / static_cast<double>(gsize[i]);
243 map.periodic[i] = periodic[i];
244 }
245 map_ = map;
246 mig_.init(dec_, rank_, map, comm_);
247 halo_.init(mig_);
248 inited_ = true;
249 }
250 // Shared-decomposition overload: adopt an EXTERNALLY-built ORB (so dem shares one BlockDecomposer
251 // with the flow solver in a coupled run, and migrateTo() can move onto a re-decomposed partition).
252 // `size`/`origin` map the ORB cell grid (= dec.globalSize()) to the physical domain.
253 void initMpi(const peclet::core::decomp::BlockDecomposer<3>& dec, std::array<double, 3> origin,
254 std::array<double, 3> size, std::array<bool, 3> periodic, MPI_Comm comm) {
255 comm_ = comm;
256 MPI_Comm_rank(comm_, &rank_);
257 dec_ = dec;
258 const auto& gs = dec.globalSize();
259 peclet::core::halo::DomainMap<3> map;
260 for (int i = 0; i < 3; ++i) {
261 map.origin[i] = origin[i];
262 map.cellSize[i] = size[i] / static_cast<double>(gs[i]);
263 map.periodic[i] = periodic[i];
264 }
265 map_ = map;
266 mig_.init(dec_, rank_, map, comm_);
267 halo_.init(mig_);
268 inited_ = true;
269 }
270 const peclet::core::decomp::BlockDecomposer<3>& decomposer() const { return dec_; }
271
272 bool inited() const { return inited_; }
273 int rank() const { return rank_; }
274 int numGhost() const { return numGhost_; }
275
283 void setVerletSkin(float skin) { verletSkin_ = skin < 0.0f ? 0.0f : skin; }
284 float verletSkin() const { return verletSkin_; }
288 void invalidateTopology() { haveTopo_ = false; }
291 long numRebuilds() const { return nRebuild_; }
292 long numGathers() const { return nGather_; }
293
294 // Rebuild the owner<->ghost correspondence over the current owned positions and populate the
295 // ghost slots [numReal, numReal+numGhost) with the owners' full forwarded state. Sets
296 // P.numParticles. Returns numReal+numGhost. Faithful port of Simulation::mpi_gather_ghosts.
297 int gather(Particles& P, double rcut) {
298 const int no = P.numReal;
299 ++nGather_;
300
301 // Decide whether to rebuild the owner↔ghost topology or reuse the cached one (Verlet skin, D2).
302 // A rebuild is forced when reuse is off (skin==0), on the first gather, when the owned count
303 // changed (a migration happened ⇒ topology invalid), or when an owned particle has displaced ≥
304 // skin since the last build (a particle could have entered the rcut band without being in the
305 // rcut+skin list).
306 bool rebuild = (verletSkin_ <= 0.0f) || !haveTopo_ || (no != lastNumReal_);
307 if (!rebuild && maxOwnedDisplacement(P.pos, no) >= verletSkin_)
308 rebuild = true;
309 numReal_ = no;
310
311 if (rebuild) {
312 ++nRebuild_;
313 const double band = rcut + static_cast<double>(verletSkin_);
314 // (1) download owned positions, (re)build the host halo topology, capture it on device.
315 auto hpos = Kokkos::create_mirror_view(P.pos);
316 Kokkos::deep_copy(hpos, P.pos);
317 std::vector<peclet::core::Vec<3>> pv(static_cast<std::size_t>(no));
318 for (int i = 0; i < no; ++i)
319 pv[i] = peclet::core::Vec<3>{hpos(i, 0), hpos(i, 1), hpos(i, 2)};
320 // includePeriodicSelf: a rank that owns a full (undecomposed) periodic axis -- a "x1" ORB
321 // axis (e.g. z of a 2x2x1 layout) or np=1 -- is its own periodic image on that axis, so the
322 // periodic neighbours are local self-ghosts the cross-rank exchange never makes. This
323 // supplies them.
324 halo_.build(pv, band, /*includePeriodicSelf=*/true);
325 dev_.init(halo_);
326 const int ng = static_cast<int>(halo_.numGhost());
327 // The halo topology (forward / device self-gather) writes ALL ng ghost slots [no, no+ng); the
328 // Particles SoA must have room for them. Silently truncating ng here would leave the halo
329 // writing past the truncated count -> out-of-bounds SoA writes (memory corruption, not a
330 // clean drop). So require adequate capacity and fail loudly instead. Size Simulation capacity
331 // for the worst-case ghost band (a fully periodic box at rcut+skin needs a thick boundary
332 // layer of ghosts).
333 if (no + ng > P.capacity)
334 throw std::runtime_error(
335 "ParticleHalo::gather: ghost overflow -- need capacity >= " + std::to_string(no + ng) +
336 " (numReal=" + std::to_string(no) + " + numGhost=" + std::to_string(ng) + "), have " +
337 std::to_string(P.capacity) + "; increase the Simulation capacity.");
338 numGhost_ = ng;
339 allocBuffers(no, ng);
340 uploadShift();
341 // Snapshot the owned positions at build time — the reference for the displacement check.
342 if (verletSkin_ > 0.0f) {
343 if (refPos_.extent(0) < static_cast<std::size_t>(no))
344 refPos_ = V3("peclet::dem::halo::refPos", static_cast<std::size_t>(P.capacity));
345 Kokkos::deep_copy(Kokkos::subview(refPos_, std::pair<int, int>(0, no), Kokkos::ALL),
346 Kokkos::subview(P.pos, std::pair<int, int>(0, no), Kokkos::ALL));
347 }
348 haveTopo_ = true;
349 lastNumReal_ = no;
350 }
351
352 const int ng = numGhost_;
353 P.numParticles = no + ng;
354 // self-map realIndices for the reals (owner deltas land on themselves); done every step like
355 // demStep.
356 selfMapReals(P.realIndices, no);
357 if (ng == 0)
358 return no;
359
360 // (2) positions (committed + predicted) with the periodic image shift. d_pos_pred was already
361 // advanced by predict_velocity, so it is forwarded too (NOT copied from pos) -- see CUDA
362 // comment.
363 forwardPositions(P.pos);
364 forwardPositions(P.posPred);
365
366 // (3) all other state packed into one record -> single exchange -> ghost slots + self-mapped
367 // idx.
368 haloPackGather(P.vel, P.velPred, P.angVel, P.angVelPred, P.invInertia, P.quat, P.quatPred,
369 P.scale, P.invMass, P.shapeId, P.gid, P.materialId, P.groundedLevel,
370 P.bodyOrphan, P.bodyOrphanVPeak, ownedPack_, no);
371 dev_.forward(ownedPack_, ghostPack_);
372 haloUnpackGather(P.vel, P.velPred, P.angVel, P.angVelPred, P.invInertia, P.quat, P.quatPred,
373 P.scale, P.invMass, P.shapeId, P.realIndices, P.gid, P.materialId,
374 P.groundedLevel, P.bodyOrphan, P.bodyOrphanVPeak, ghostPack_, no, ng);
375 return no + ng;
376 }
377
378 MPI_Comm comm() const { return comm_; }
379
380 // Dynamic load re-balance: re-decompose the ORB by per-block particle COUNT (weighted ORB) and
381 // migrate each owned particle, with its committed state, to its new owner. A pure redistribution
382 // of the same global particle set (count conserved, per-particle state preserved) — only
383 // ownership and this rank's owned slice change; the physics result is unchanged. Must be called
384 // at a step boundary (committed pos/quat/vel/angVel valid; predicted/delta/ghost scratch are
385 // rebuilt next gather()). Returns this rank's new owned count. No-op-safe at np=1.
386 // Weighted re-decompose by particle count (the equal-cell ORB imbalances as particles cluster)
387 // and migrate owners. dec_ updated in place; mig_ points to it. Returns the new owned count.
388 int rebalance(Particles& P) {
389 std::vector<peclet::core::Vec<3>> pos;
390 std::vector<char> payload;
391 packState(P, pos, payload);
392 const std::size_t newN = peclet::core::halo::rebalanceByParticleCount(
393 dec_, mig_, pos, payload, sizeof(MigratePack), comm_);
394 if ((int)newN > P.capacity)
395 throw std::runtime_error("ParticleHalo::rebalance: owned overflow -- rank received " +
396 std::to_string(newN) + " particles, capacity " +
397 std::to_string(P.capacity));
398 unpackState(P, pos, payload, newN);
399 return (int)newN;
400 }
401 // Migrate particles onto an EXTERNALLY-supplied decomposition (dynamic co-rebalancing: the same
402 // BlockDecomposer the flow solver redistributes onto). Pure ownership move — counts/state
403 // conserved. Must be called at a step boundary. No-op-safe at np=1.
404 int migrateTo(Particles& P, const peclet::core::decomp::BlockDecomposer<3>& newDec) {
405 std::vector<peclet::core::Vec<3>> pos;
406 std::vector<char> payload;
407 packState(P, pos, payload);
408 dec_ = newDec; // in place: mig_ still points at dec_; mig_.migrate() sends to dec_.ownerOf(...)
409 const std::size_t newN = mig_.migrate(pos, payload, sizeof(MigratePack));
410 if ((int)newN > P.capacity)
411 throw std::runtime_error("ParticleHalo::migrateTo: owned overflow -- rank received " +
412 std::to_string(newN) + " particles, capacity " +
413 std::to_string(P.capacity));
414 unpackState(P, pos, payload, newN);
415 return (int)newN;
416 }
417 // Migrate onto the weighted ORB of per-cell weights `w` (global x-fastest, matching the ORB grid).
418 // The Lagrangian half of the co-rebalance: dem builds the SAME deterministic partition flow does
419 // from the same weight field, so no BlockDecomposer object crosses the language boundary.
420 int migrateToWeights(Particles& P, const std::vector<peclet::core::Real>& w) {
421 int size = 1;
422 MPI_Comm_size(comm_, &size);
423 peclet::core::decomp::BlockDecomposer<3> newDec((std::size_t)size, dec_.globalSize(), w);
424 return migrateTo(P, newDec);
425 }
426
427 private:
428 // Download the committed state and pack it (position drives ownership; the rest is the payload).
429 // Each particle also carries its slice of the persistent-contact ledger (see MigratePack).
430 void packState(Particles& P, std::vector<peclet::core::Vec<3>>& pos, std::vector<char>& payload) {
431 const int no = P.numReal;
432 auto h_pos = Kokkos::create_mirror_view(P.pos);
433 auto h_quat = Kokkos::create_mirror_view(P.quat);
434 auto h_vel = Kokkos::create_mirror_view(P.vel);
435 auto h_angVel = Kokkos::create_mirror_view(P.angVel);
436 auto h_invI = Kokkos::create_mirror_view(P.invInertia);
437 auto h_invM = Kokkos::create_mirror_view(P.invMass);
438 auto h_scale = Kokkos::create_mirror_view(P.scale);
439 auto h_tScale = Kokkos::create_mirror_view(P.targetScale);
440 auto h_shape = Kokkos::create_mirror_view(P.shapeId);
441 auto h_pf = Kokkos::create_mirror_view(P.planeFriction);
442 auto h_gid = Kokkos::create_mirror_view(P.gid);
443 auto h_mat = Kokkos::create_mirror_view(P.materialId);
444 auto h_grd = Kokkos::create_mirror_view(P.groundedLevel);
445 Kokkos::deep_copy(h_pos, P.pos);
446 Kokkos::deep_copy(h_quat, P.quat);
447 Kokkos::deep_copy(h_vel, P.vel);
448 Kokkos::deep_copy(h_angVel, P.angVel);
449 Kokkos::deep_copy(h_invI, P.invInertia);
450 Kokkos::deep_copy(h_invM, P.invMass);
451 Kokkos::deep_copy(h_scale, P.scale);
452 Kokkos::deep_copy(h_tScale, P.targetScale);
453 Kokkos::deep_copy(h_shape, P.shapeId);
454 Kokkos::deep_copy(h_pf, P.planeFriction);
455 Kokkos::deep_copy(h_gid, P.gid);
456 Kokkos::deep_copy(h_mat, P.materialId);
457 Kokkos::deep_copy(h_grd, P.groundedLevel);
458 auto h_orp = Kokkos::create_mirror_view(P.bodyOrphan);
459 auto h_ovp = Kokkos::create_mirror_view(P.bodyOrphanVPeak);
460 Kokkos::deep_copy(h_orp, P.bodyOrphan);
461 Kokkos::deep_copy(h_ovp, P.bodyOrphanVPeak);
462 pos.assign((std::size_t)no, peclet::core::Vec<3>{});
463 payload.assign((std::size_t)no * sizeof(MigratePack), 0);
464 std::vector<MigratePack> packs((std::size_t)no);
465 std::unordered_map<unsigned, int> gidToLocal;
466 gidToLocal.reserve((std::size_t)no * 2);
467 for (int i = 0; i < no; ++i) {
468 pos[(std::size_t)i] = peclet::core::Vec<3>{h_pos(i, 0), h_pos(i, 1), h_pos(i, 2)};
469 MigratePack& m = packs[(std::size_t)i];
470 m.quat = F4{h_quat(i, 0), h_quat(i, 1), h_quat(i, 2), h_quat(i, 3)};
471 m.vel = F3{h_vel(i, 0), h_vel(i, 1), h_vel(i, 2)};
472 m.angVel = F3{h_angVel(i, 0), h_angVel(i, 1), h_angVel(i, 2)};
473 m.invInertia = F3{h_invI(i, 0), h_invI(i, 1), h_invI(i, 2)};
474 m.invMass = h_invM(i);
475 m.scale = h_scale(i);
476 m.targetScale = h_tScale(i);
477 m.shapeId = h_shape(i);
478 m.planeFric0 = h_pf(i, 0);
479 m.planeFric1 = h_pf(i, 1);
480 m.gid = h_gid(i);
481 m.materialId = h_mat(i);
482 m.groundedLevel = h_grd(i);
483 m.orphan = h_orp(i);
484 m.orphanVPeak = h_ovp(i);
485 m.numWarm = 0;
486 gidToLocal.emplace(static_cast<unsigned>(h_gid(i)), i);
487 }
488 // Distribute the previous-substep converged ledger onto its endpoint particles: each pair
489 // rides on BOTH locally-owned endpoints (the redundant ghost-pair pattern means either owner
490 // may need it; the unpack dedupes by key). Beyond kWarmCarryMax the lowest-|impulse| entry is
491 // evicted — a dropped pair merely warm-starts cold on the receiving rank.
492 if (P.prevPairCount > 0) {
493 const int pc = P.prevPairCount;
494 // Full-view mirrors (not row-range subviews): a row range of a LayoutLeft float*[3] view is
495 // non-contiguous, and a device->host deep_copy of it has no copy mechanism on CUDA.
496 auto h_k = Kokkos::create_mirror_view(P.prevPairKeys);
497 auto h_l = Kokkos::create_mirror_view(P.prevLambda);
498 auto h_lt = Kokkos::create_mirror_view(P.prevLambdaT);
499 auto h_pi = Kokkos::create_mirror_view(P.prevPosImpulse);
500 auto h_rb = Kokkos::create_mirror_view(P.prevRestBank);
501 auto h_rv = Kokkos::create_mirror_view(P.prevRestVPeak);
502 Kokkos::deep_copy(h_k, P.prevPairKeys);
503 Kokkos::deep_copy(h_l, P.prevLambda);
504 Kokkos::deep_copy(h_lt, P.prevLambdaT);
505 Kokkos::deep_copy(h_pi, P.prevPosImpulse);
506 Kokkos::deep_copy(h_rb, P.prevRestBank);
507 Kokkos::deep_copy(h_rv, P.prevRestVPeak);
508 auto attach = [&](int i, const WarmPairEntry& e, float w) {
509 MigratePack& m = packs[(std::size_t)i];
510 if (m.numWarm < kWarmCarryMax) {
511 m.warm[m.numWarm++] = e;
512 return;
513 }
514 int worst = 0;
515 float worstW = 1e30f;
516 for (int s = 0; s < kWarmCarryMax; ++s) {
517 const float ws = std::fabs(m.warm[s].lambda) + std::fabs(m.warm[s].posImpulse);
518 if (ws < worstW) {
519 worstW = ws;
520 worst = s;
521 }
522 }
523 if (w > worstW)
524 m.warm[worst] = e;
525 };
526 for (int e = 0; e < pc; ++e) {
527 const unsigned long long k = h_k(e);
528 if (k == ~0ull)
529 continue;
530 WarmPairEntry we;
531 we.key = k;
532 we.lambda = h_l(e);
533 we.lambdaT[0] = h_lt(e, 0);
534 we.lambdaT[1] = h_lt(e, 1);
535 we.lambdaT[2] = h_lt(e, 2);
536 we.posImpulse = h_pi(e);
537 we.restBank = h_rb(e);
538 we.restVPeak = h_rv(e);
539 if (we.lambda == 0.0f && we.posImpulse == 0.0f && we.lambdaT[0] == 0.0f &&
540 we.lambdaT[1] == 0.0f && we.lambdaT[2] == 0.0f && we.restBank == 0.0f)
541 continue; // dead entry: carrying it only evicts live ones
542 const float w = std::fabs(we.lambda) + std::fabs(we.posImpulse) + std::fabs(we.restBank);
543 const unsigned hi = static_cast<unsigned>(k >> 32);
544 const unsigned lo = static_cast<unsigned>(k & 0xFFFFFFFFu);
545 if (auto it = gidToLocal.find(hi); it != gidToLocal.end())
546 attach(it->second, we, w);
547 if (lo != 0xFFFFFFFFu)
548 if (auto it = gidToLocal.find(lo); it != gidToLocal.end())
549 attach(it->second, we, w);
550 }
551 }
552 // Force-engine (Hertz–Mindlin) history. Pack the LIVE cached-pair springs (hertzKeys/hertzXi
553 // hold the current values; keys are gid-based) onto their locally-owned endpoints, and each
554 // particle's wall-history slots verbatim.
555 if (P.hertzNumPairs > 0) {
556 const int hn = P.hertzNumPairs;
557 auto h_hk = Kokkos::create_mirror_view(P.hertzKeys);
558 auto h_hx = Kokkos::create_mirror_view(P.hertzXi);
559 Kokkos::deep_copy(h_hk, P.hertzKeys);
560 Kokkos::deep_copy(h_hx, P.hertzXi);
561 auto attachHertz = [&](int i, const HertzPairEntry& e, float w) {
562 MigratePack& m = packs[(std::size_t)i];
563 if (m.numHertz < kWarmCarryMax) {
564 m.hertz[m.numHertz++] = e;
565 return;
566 }
567 int worst = 0;
568 float worstW = 1e30f;
569 for (int s = 0; s < kWarmCarryMax; ++s) {
570 const float ws = std::fabs(m.hertz[s].xi[0]) + std::fabs(m.hertz[s].xi[1]) +
571 std::fabs(m.hertz[s].xi[2]);
572 if (ws < worstW) {
573 worstW = ws;
574 worst = s;
575 }
576 }
577 if (w > worstW)
578 m.hertz[worst] = e;
579 };
580 for (int e = 0; e < hn; ++e) {
581 HertzPairEntry he;
582 he.key = h_hk(e);
583 he.xi[0] = h_hx(e, 0);
584 he.xi[1] = h_hx(e, 1);
585 he.xi[2] = h_hx(e, 2);
586 if (he.xi[0] == 0.0f && he.xi[1] == 0.0f && he.xi[2] == 0.0f)
587 continue; // open / historyless pair: nothing worth carrying
588 const float w = std::fabs(he.xi[0]) + std::fabs(he.xi[1]) + std::fabs(he.xi[2]);
589 const unsigned hi = static_cast<unsigned>(he.key >> 32);
590 const unsigned lo = static_cast<unsigned>(he.key & 0xFFFFFFFFu);
591 if (auto it = gidToLocal.find(hi); it != gidToLocal.end())
592 attachHertz(it->second, he, w);
593 if (auto it = gidToLocal.find(lo); it != gidToLocal.end())
594 attachHertz(it->second, he, w);
595 }
596 }
597 {
598 auto h_xw = Kokkos::create_mirror_view(P.hertzXiWall);
599 auto h_sw = Kokkos::create_mirror_view(P.hertzSnWall);
600 Kokkos::deep_copy(h_xw, P.hertzXiWall);
601 Kokkos::deep_copy(h_sw, P.hertzSnWall);
602 for (int i = 0; i < no; ++i)
603 for (int wi = 0; wi < Particles::kHertzMaxWalls; ++wi) {
604 const int slot = i * Particles::kHertzMaxWalls + wi;
605 MigratePack& m = packs[(std::size_t)i];
606 m.hertzXiWall[wi][0] = h_xw(slot, 0);
607 m.hertzXiWall[wi][1] = h_xw(slot, 1);
608 m.hertzXiWall[wi][2] = h_xw(slot, 2);
609 m.hertzSnWall[wi] = h_sw(slot);
610 }
611 }
612 for (int i = 0; i < no; ++i)
613 std::memcpy(&payload[(std::size_t)i * sizeof(MigratePack)], &packs[(std::size_t)i],
614 sizeof(MigratePack));
615 }
616 // Unpack the migrated particles back into the SoA [0,newN) and upload; rebuild the rank's
617 // persistent-contact ledger (prevPairKeys sorted + aligned impulse stores) from the union of the
618 // arriving particles' carried slices.
619 void unpackState(Particles& P, const std::vector<peclet::core::Vec<3>>& pos,
620 const std::vector<char>& payload, std::size_t newN) {
621 auto h_pos = Kokkos::create_mirror_view(P.pos);
622 auto h_quat = Kokkos::create_mirror_view(P.quat);
623 auto h_vel = Kokkos::create_mirror_view(P.vel);
624 auto h_angVel = Kokkos::create_mirror_view(P.angVel);
625 auto h_invI = Kokkos::create_mirror_view(P.invInertia);
626 auto h_invM = Kokkos::create_mirror_view(P.invMass);
627 auto h_scale = Kokkos::create_mirror_view(P.scale);
628 auto h_tScale = Kokkos::create_mirror_view(P.targetScale);
629 auto h_shape = Kokkos::create_mirror_view(P.shapeId);
630 auto h_pf = Kokkos::create_mirror_view(P.planeFriction);
631 auto h_gid = Kokkos::create_mirror_view(P.gid);
632 auto h_mat = Kokkos::create_mirror_view(P.materialId);
633 auto h_grd = Kokkos::create_mirror_view(P.groundedLevel);
634 auto h_orp = Kokkos::create_mirror_view(P.bodyOrphan);
635 auto h_ovp = Kokkos::create_mirror_view(P.bodyOrphanVPeak);
636 Kokkos::deep_copy(h_orp, P.bodyOrphan); // slots past newN keep defined values
637 Kokkos::deep_copy(h_ovp, P.bodyOrphanVPeak);
638 std::vector<WarmPairEntry> ledger;
639 ledger.reserve(newN * 4);
640 std::vector<HertzPairEntry> hertzLedger;
641 hertzLedger.reserve(newN * 4);
642 auto h_xw = Kokkos::create_mirror_view(P.hertzXiWall);
643 auto h_sw = Kokkos::create_mirror_view(P.hertzSnWall);
644 Kokkos::deep_copy(h_xw, P.hertzXiWall); // slots past newN keep defined values on CUDA mirrors
645 Kokkos::deep_copy(h_sw, P.hertzSnWall);
646 for (std::size_t i = 0; i < newN; ++i) {
647 h_pos((int)i, 0) = pos[i][0];
648 h_pos((int)i, 1) = pos[i][1];
649 h_pos((int)i, 2) = pos[i][2];
650 MigratePack m;
651 std::memcpy(&m, &payload[i * sizeof(MigratePack)], sizeof(MigratePack));
652 h_quat((int)i, 0) = m.quat.x;
653 h_quat((int)i, 1) = m.quat.y;
654 h_quat((int)i, 2) = m.quat.z;
655 h_quat((int)i, 3) = m.quat.w;
656 h_vel((int)i, 0) = m.vel.x;
657 h_vel((int)i, 1) = m.vel.y;
658 h_vel((int)i, 2) = m.vel.z;
659 h_angVel((int)i, 0) = m.angVel.x;
660 h_angVel((int)i, 1) = m.angVel.y;
661 h_angVel((int)i, 2) = m.angVel.z;
662 h_invI((int)i, 0) = m.invInertia.x;
663 h_invI((int)i, 1) = m.invInertia.y;
664 h_invI((int)i, 2) = m.invInertia.z;
665 h_invM((int)i) = m.invMass;
666 h_scale((int)i) = m.scale;
667 h_tScale((int)i) = m.targetScale;
668 h_shape((int)i) = m.shapeId;
669 h_pf((int)i, 0) = m.planeFric0;
670 h_pf((int)i, 1) = m.planeFric1;
671 h_gid((int)i) = m.gid;
672 h_mat((int)i) = m.materialId;
673 h_grd((int)i) = m.groundedLevel;
674 h_orp((int)i) = m.orphan;
675 h_ovp((int)i) = m.orphanVPeak;
676 for (int s = 0; s < (int)m.numWarm && s < kWarmCarryMax; ++s)
677 ledger.push_back(m.warm[s]);
678 for (int s = 0; s < (int)m.numHertz && s < kWarmCarryMax; ++s)
679 hertzLedger.push_back(m.hertz[s]);
680 for (int wi = 0; wi < Particles::kHertzMaxWalls; ++wi) {
681 const int slot = (int)i * Particles::kHertzMaxWalls + wi;
682 h_xw(slot, 0) = m.hertzXiWall[wi][0];
683 h_xw(slot, 1) = m.hertzXiWall[wi][1];
684 h_xw(slot, 2) = m.hertzXiWall[wi][2];
685 h_sw(slot) = m.hertzSnWall[wi];
686 }
687 }
688 Kokkos::deep_copy(P.pos, h_pos);
689 Kokkos::deep_copy(P.quat, h_quat);
690 Kokkos::deep_copy(P.vel, h_vel);
691 Kokkos::deep_copy(P.angVel, h_angVel);
692 Kokkos::deep_copy(P.invInertia, h_invI);
693 Kokkos::deep_copy(P.invMass, h_invM);
694 Kokkos::deep_copy(P.scale, h_scale);
695 Kokkos::deep_copy(P.targetScale, h_tScale);
696 Kokkos::deep_copy(P.shapeId, h_shape);
697 Kokkos::deep_copy(P.planeFriction, h_pf);
698 Kokkos::deep_copy(P.gid, h_gid);
699 Kokkos::deep_copy(P.materialId, h_mat);
700 Kokkos::deep_copy(P.groundedLevel, h_grd);
701 Kokkos::deep_copy(P.bodyOrphan, h_orp);
702 Kokkos::deep_copy(P.bodyOrphanVPeak, h_ovp);
703
704 // Ledger rebuild: sort by key, dedupe (a pair arrives once per locally-received endpoint; the
705 // duplicates carry identical values), clamp to the store capacity, upload sorted + aligned —
706 // exactly the layout the warm-start gather's binary search expects.
707 std::sort(ledger.begin(), ledger.end(),
708 [](const WarmPairEntry& a, const WarmPairEntry& b) { return a.key < b.key; });
709 ledger.erase(std::unique(ledger.begin(), ledger.end(),
710 [](const WarmPairEntry& a, const WarmPairEntry& b) {
711 return a.key == b.key;
712 }),
713 ledger.end());
714 const int nl = std::min<int>((int)ledger.size(), (int)P.prevPairKeys.extent(0));
715 {
716 auto hk = Kokkos::create_mirror_view(P.prevPairKeys);
717 auto hl = Kokkos::create_mirror_view(P.prevLambda);
718 auto hlt = Kokkos::create_mirror_view(P.prevLambdaT);
719 auto hpi = Kokkos::create_mirror_view(P.prevPosImpulse);
720 auto hrb = Kokkos::create_mirror_view(P.prevRestBank);
721 auto hrv = Kokkos::create_mirror_view(P.prevRestVPeak);
722 Kokkos::deep_copy(hk, P.prevPairKeys); // preserve tail entries beyond nl
723 Kokkos::deep_copy(hl, P.prevLambda);
724 Kokkos::deep_copy(hlt, P.prevLambdaT);
725 Kokkos::deep_copy(hpi, P.prevPosImpulse);
726 Kokkos::deep_copy(hrb, P.prevRestBank);
727 Kokkos::deep_copy(hrv, P.prevRestVPeak);
728 for (int e = 0; e < nl; ++e) {
729 hk(e) = ledger[(std::size_t)e].key;
730 hl(e) = ledger[(std::size_t)e].lambda;
731 hlt(e, 0) = ledger[(std::size_t)e].lambdaT[0];
732 hlt(e, 1) = ledger[(std::size_t)e].lambdaT[1];
733 hlt(e, 2) = ledger[(std::size_t)e].lambdaT[2];
734 hpi(e) = ledger[(std::size_t)e].posImpulse;
735 hrb(e) = ledger[(std::size_t)e].restBank;
736 hrv(e) = ledger[(std::size_t)e].restVPeak;
737 }
738 Kokkos::deep_copy(P.prevPairKeys, hk);
739 Kokkos::deep_copy(P.prevLambda, hl);
740 Kokkos::deep_copy(P.prevLambdaT, hlt);
741 Kokkos::deep_copy(P.prevPosImpulse, hpi);
742 Kokkos::deep_copy(P.prevRestBank, hrb);
743 Kokkos::deep_copy(P.prevRestVPeak, hrv);
744 }
745 P.prevPairCount = nl;
746
747 // Force-engine history rebuild: wall slots verbatim per (new local index, wall); the pair
748 // ledger sorted + deduped into the hertzPrev store (the exact layout hertzRebuildPairs'
749 // key-carry binary search expects), and the cached pair LIST invalidated — local pair slots
750 // reference pre-migration indices, so the next force step must rebuild (and re-gathers the
751 // halo then). A fresh rank may never have allocated the prev store: size it here.
752 Kokkos::deep_copy(P.hertzXiWall, h_xw);
753 Kokkos::deep_copy(P.hertzSnWall, h_sw);
754 std::sort(hertzLedger.begin(), hertzLedger.end(),
755 [](const HertzPairEntry& a, const HertzPairEntry& b) { return a.key < b.key; });
756 hertzLedger.erase(std::unique(hertzLedger.begin(), hertzLedger.end(),
757 [](const HertzPairEntry& a, const HertzPairEntry& b) {
758 return a.key == b.key;
759 }),
760 hertzLedger.end());
761 const int nh = (int)hertzLedger.size();
762 if ((int)P.hertzPrevKeys.extent(0) < nh) {
763 P.hertzPrevKeys = Kokkos::View<unsigned long long*, CpMem>("hertzPrevKeys", nh);
764 P.hertzPrevXi = Kokkos::View<float* [3], CpMem>("hertzPrevXi", nh);
765 }
766 if (nh > 0) {
767 auto hk = Kokkos::create_mirror_view(P.hertzPrevKeys);
768 auto hx = Kokkos::create_mirror_view(P.hertzPrevXi);
769 Kokkos::deep_copy(hk, P.hertzPrevKeys);
770 Kokkos::deep_copy(hx, P.hertzPrevXi);
771 for (int e = 0; e < nh; ++e) {
772 hk(e) = hertzLedger[(std::size_t)e].key;
773 hx(e, 0) = hertzLedger[(std::size_t)e].xi[0];
774 hx(e, 1) = hertzLedger[(std::size_t)e].xi[1];
775 hx(e, 2) = hertzLedger[(std::size_t)e].xi[2];
776 }
777 Kokkos::deep_copy(P.hertzPrevKeys, hk);
778 Kokkos::deep_copy(P.hertzPrevXi, hx);
779 }
780 P.hertzPrevCount = nh;
781 P.hertzNumPairs = -1;
782
783 P.numReal = (int)newN;
784 P.numParticles = (int)newN;
785 }
786
787 public:
788 // owner slice [0,numReal) -> ghost slots [numReal,..), verbatim (velocity / angular velocity).
789 void forward(V3 field) {
790 if (numGhost_ == 0)
791 return;
792 haloPackF3(field, ownedF3_, numReal_);
793 dev_.forward(ownedF3_, ghostF3_);
794 haloUnpackF3(field, ghostF3_, shiftDev_, numReal_, numGhost_, /*doShift=*/false);
795 }
796 // owner slice -> ghost slots with the periodic image shift added (positions).
797 void forwardPositions(V3 field) {
798 if (numGhost_ == 0)
799 return;
800 haloPackF3(field, ownedF3_, numReal_);
801 dev_.forward(ownedF3_, ghostF3_);
802 haloUnpackF3(field, ghostF3_, shiftDev_, numReal_, numGhost_, /*doShift=*/true);
803 }
804 // owner slice -> ghost slots, verbatim (quaternions).
805 void forward4(V4 field) {
806 if (numGhost_ == 0)
807 return;
808 haloPackF4(field, ownedF4_, numReal_);
809 dev_.forward(ownedF4_, ghostF4_);
810 haloUnpackF4(field, ghostF4_, numReal_, numGhost_);
811 }
812
813 void selfMapReals(Vi realIndices, int no) {
814 Kokkos::parallel_for(
815 "peclet::dem::halo::selfMapReals", Kokkos::RangePolicy<CpExec>(0, no),
816 KOKKOS_LAMBDA(int i) { realIndices(i) = i; });
817 }
818
819 private:
820 void allocBuffers(int no, int ng) {
821 // Exact-sized: ParticleHalo::forward host-stages a deep_copy into the ghost View, so
822 // its extent must equal numGhost; owned is indexed by sendIdx in [0,numReal).
823 ownedF3_ = peclet::core::View<F3>("peclet::dem::halo::ownedF3", no);
824 ghostF3_ = peclet::core::View<F3>("peclet::dem::halo::ghostF3", ng);
825 ownedF4_ = peclet::core::View<F4>("peclet::dem::halo::ownedF4", no);
826 ghostF4_ = peclet::core::View<F4>("peclet::dem::halo::ghostF4", ng);
827 ownedPack_ = peclet::core::View<MpiGatherPack>("peclet::dem::halo::ownedPack", no);
828 ghostPack_ = peclet::core::View<MpiGatherPack>("peclet::dem::halo::ghostPack", ng);
829 }
830 // Max Euclidean displacement of any owned particle since the last topology build (device reduce +
831 // one scalar read-back) — the Verlet-skin reuse criterion.
832 // public: nvcc forbids an extended __host__ __device__ (KOKKOS_LAMBDA) lambda inside a private
833 // method.
834 public:
835 float maxOwnedDisplacement(const V3& pos, int no) const {
836 if (no <= 0 || refPos_.extent(0) < static_cast<std::size_t>(no))
837 return 1e30f;
838 float md = 0.0f;
839 V3 p = pos, r = refPos_;
840 Kokkos::parallel_reduce(
841 "peclet::dem::halo::maxdisp", Kokkos::RangePolicy<CpExec>(0, no),
842 KOKKOS_LAMBDA(const int i, float& m) {
843 const float dx = p(i, 0) - r(i, 0), dy = p(i, 1) - r(i, 1), dz = p(i, 2) - r(i, 2);
844 const float d = Kokkos::sqrt(dx * dx + dy * dy + dz * dz);
845 if (d > m)
846 m = d;
847 },
848 Kokkos::Max<float>(md));
849 return md;
850 }
851
852 private:
853 void uploadShift() {
854 auto t = halo_.flatten();
855 std::vector<F3> hs(t.shift.size());
856 for (std::size_t i = 0; i < t.shift.size(); ++i)
857 hs[i] = F3{static_cast<float>(t.shift[i][0]), static_cast<float>(t.shift[i][1]),
858 static_cast<float>(t.shift[i][2])};
859 shiftDev_ = peclet::core::toDevice(hs, "peclet::dem::halo::shift");
860 }
861
862 bool inited_ = false;
863 int rank_ = 0, numReal_ = 0, numGhost_ = 0;
864 // Verlet-skin reuse state (D2): skin width, the build-time owned positions, and the cache-valid
865 // flags.
866 float verletSkin_ = 0.0f;
867 bool haveTopo_ = false;
868 int lastNumReal_ = -1;
869 long nRebuild_ = 0, nGather_ = 0;
870 V3 refPos_;
871 MPI_Comm comm_ = MPI_COMM_NULL;
872 peclet::core::decomp::BlockDecomposer<3> dec_;
873 peclet::core::halo::DomainMap<3> map_; // physical<->cell mapping (for migrateTo)
874 peclet::core::halo::ParticleMigrator<3> mig_;
875 peclet::core::halo::ParticleHaloTopology<3> halo_;
876 peclet::core::halo::ParticleHalo<3> dev_;
877 peclet::core::View<F3> ownedF3_, ghostF3_, shiftDev_;
878 peclet::core::View<F4> ownedF4_, ghostF4_;
879 peclet::core::View<MpiGatherPack> ownedPack_, ghostPack_;
880};
881
882} // namespace peclet::dem
883
884#endif // PECLET_DEM_MPI
885#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.