peclet-dem 1.1.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
step_solve_mpi.hpp
Go to the documentation of this file.
1
7#ifndef DEM_STEP_SOLVE_MPI_HPP
8#define DEM_STEP_SOLVE_MPI_HPP
9
10#ifdef PECLET_DEM_MPI
11
12#include <algorithm>
13#include <cmath>
14#include <Kokkos_Core.hpp>
15
16#include "mpi_halo.hpp" // ParticleHalo
17#include "particles.hpp"
18#include "solve_driver.hpp" // demSolveContacts, maxOwnedRadius
19#include "solve_driver_force.hpp" // demStepForce + HertzMindlinLaw + fillWorldRadiiKokkos
20#include "step_solve.hpp" // narrowPhaseGrow
21
22namespace peclet::dem {
23
37struct MpiSolveHooks {
38 static constexpr bool distributed = true;
39 ParticleHalo& halo;
40 int syncEvery;
41 bool forwardRotation;
42 int numManifoldsVisible;
43 float allMax(float v) const {
44 float g = v;
45 MPI_Allreduce(&v, &g, 1, MPI_FLOAT, MPI_MAX, halo.comm());
46 return g;
47 }
50 float allMaxAny(float v, bool& any) const {
51 const float in[2] = {v, any ? 1.0f : 0.0f};
52 float out[2] = {v, in[1]};
53 MPI_Allreduce(in, out, 2, MPI_FLOAT, MPI_MAX, halo.comm());
54 any = out[1] > 0.0f;
55 return out[0];
56 }
57 bool syncPoint(int it) const { return (it + 1) % syncEvery == 0; }
58 int visibleManifolds(int) const { return numManifoldsVisible; }
59 void beginSolve(Particles& P) const { halo.markVelocityBaseline(P); }
60 void syncVelocities(Particles& P) const { halo.syncVelocities(P, forwardRotation); }
61 void publishPositions(Particles& P) const { halo.publishPositions(P, forwardRotation); }
62 void syncPositions(Particles& P) const { halo.syncPositions(P, forwardRotation); }
63 void syncFrictionCounts(Particles& P) const { halo.syncFrictionCounts(P); }
64 void syncContactCounts(Particles& P) const { halo.syncContactCounts(P); }
65 // Rank-level M (docs/contact_solve_framework.md §13.3).
66 RankK rankK() const {
67 return RankK{halo.exchanges(), halo.rankColor(), halo.numRankColors(), syncEvery};
68 }
69 void openVelocityPhase(Particles& P, bool poisson) const {
70 halo.openVelocityPhase(P, forwardRotation, poisson);
71 }
72 void openPositionCounts(Particles& P, bool velocityMask) const {
73 halo.openPositionCounts(P, velocityMask);
74 }
75 void restoreOrphanBalance(Particles& P) const { halo.restoreOrphanBalance(P); }
76};
77
81inline constexpr float kDriftSlack = 0.25f;
82
89inline void driftVoteLocalKokkos(const Particles& P, const ParticleHalo::BlockBox box, float dt,
90 float& eMax, float& dMax, float* scaleMax = nullptr) {
91 const F3 g = P.gravity;
92 const auto pos = P.pos;
93 const auto vel = P.vel;
94 const auto ext = P.extForce;
95 const auto invM = P.invMass;
96 const auto sc = P.scale;
97 const float lo0 = box.lo.x, lo1 = box.lo.y, lo2 = box.lo.z, hi0 = box.hi.x, hi1 = box.hi.y,
98 hi2 = box.hi.z, L0 = box.period.x, L1 = box.period.y, L2 = box.period.z;
99 float e = 0.0f, d = 0.0f, smax = 0.0f;
100 Kokkos::parallel_reduce(
101 "peclet::dem::drift_vote", Kokkos::RangePolicy<CpExec>(0, P.numReal),
102 KOKKOS_LAMBDA(int i, float& em, float& dm, float& sm) {
103 const float im = invM(i);
104 const float ax = g.x + ext(i, 0) * im, ay = g.y + ext(i, 1) * im, az = g.z + ext(i, 2) * im;
105 const float v0 = vel(i, 0), v1 = vel(i, 1), v2 = vel(i, 2);
106 const float di = Kokkos::sqrt(v0 * v0 + v1 * v1 + v2 * v2) * dt +
107 Kokkos::sqrt(ax * ax + ay * ay + az * az) * dt * dt;
108 float d2 = 0.0f;
109#define PECLET_DEM_DRIFT_AXIS(X, LO, HI, L) \
110 { \
111 float gk = Kokkos::fmax(0.0f, Kokkos::fmax((LO) - (X), (X) - (HI))); \
112 if ((L) > 0.0f) { \
113 const float xm = (X) - (L), xp = (X) + (L); \
114 gk = Kokkos::fmin(gk, Kokkos::fmax(0.0f, Kokkos::fmax((LO) - xm, xm - (HI)))); \
115 gk = Kokkos::fmin(gk, Kokkos::fmax(0.0f, Kokkos::fmax((LO) - xp, xp - (HI)))); \
116 } \
117 d2 += gk * gk; \
118 }
119 PECLET_DEM_DRIFT_AXIS(pos(i, 0), lo0, hi0, L0)
120 PECLET_DEM_DRIFT_AXIS(pos(i, 1), lo1, hi1, L1)
121 PECLET_DEM_DRIFT_AXIS(pos(i, 2), lo2, hi2, L2)
122#undef PECLET_DEM_DRIFT_AXIS
123 const float ei = Kokkos::sqrt(d2) + di;
124 em = ei > em ? ei : em;
125 dm = di > dm ? di : dm;
126 sm = sc(i) > sm ? sc(i) : sm;
127 },
128 Kokkos::Max<float>(e), Kokkos::Max<float>(d), Kokkos::Max<float>(smax));
129 eMax = e;
130 dMax = d;
131 if (scaleMax)
132 *scaleMax = smax;
133}
134
138inline float globalMaxRadius(const Particles& P, MPI_Comm comm) {
139 float r = P.numReal > 0 ? maxOwnedRadius(P) : 0.0f, g = r;
140 MPI_Allreduce(&r, &g, 1, MPI_FLOAT, MPI_MAX, comm);
141 return g > 0.0f ? g : maxOwnedRadius(P);
142}
143
148inline double xpbdContactReach(float rMax) {
149 return 2.1 * static_cast<double>(rMax);
150}
151
155inline void debugCaptureOwnedContacts(Particles& P, const ParticleHalo& halo, int ncOwned) {
156 DebugContactCapture& cap = P.debugCaptured;
157 const int no = P.numReal;
158 auto hc = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), P.contacts);
159 auto hg = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), P.gid);
160 auto hp = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), P.posPred);
161 auto hr = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), P.rad);
162 const std::vector<float> shift = halo.debugGhostShiftsBySlot();
163 const double box[3] = {static_cast<double>(P.domain.max.x) - P.domain.min.x,
164 static_cast<double>(P.domain.max.y) - P.domain.min.y,
165 static_cast<double>(P.domain.max.z) - P.domain.min.z};
166 auto image = [&](int slot, std::vector<int>& out) {
167 for (int d = 0; d < 3; ++d) {
168 const float sh = slot < no ? 0.0f : shift[3 * static_cast<std::size_t>(slot - no) + d];
169 out.push_back(sh == 0.0f ? 0 : static_cast<int>(std::lround(sh / box[d])));
170 }
171 };
172 cap = DebugContactCapture{};
173 for (int i = 0; i < ncOwned; ++i) {
174 const ContactC& c = hc(i);
175 cap.gidA.push_back(hg(c.bodyA));
176 cap.gidB.push_back(c.bodyB >= 0 ? hg(c.bodyB) : -1);
177 image(c.bodyA, cap.imageA);
178 if (c.bodyB >= 0)
179 image(c.bodyB, cap.imageB);
180 else
181 cap.imageB.insert(cap.imageB.end(), {0, 0, 0});
182 cap.dist.push_back(c.dist);
183 }
184 for (int i = 0; i < no; ++i) {
185 cap.gid.push_back(hg(i));
186 for (int d = 0; d < 3; ++d)
187 cap.posPred.push_back(hp(i, d));
188 cap.rad.push_back(hr(i));
189 }
190}
191
197struct MpiDriftVote {
198 float rMax; // global max radius (pre-growth)
199 float margin; // 0.1 R_max: the broad/narrow-phase margin
200 float slack; // S = kDriftSlack R_max
201 float dMax; // global max predicted displacement of the substep
202};
203inline MpiDriftVote mpiDriftVote(Particles& P, ParticleHalo& halo) {
204 float vote[3] = {0.0f, 0.0f, 0.0f}, smax = 0.0f;
205 driftVoteLocalKokkos(P, halo.blockBox(), P.dt, vote[1], vote[2], &smax);
206 // = maxOwnedRadius(P) on a non-empty rank (same float product), 0 on an empty one.
207 vote[0] = P.numReal > 0 ? smax * P.globalScale * P.baseRadius : 0.0f;
208 float g[3];
209 MPI_Allreduce(vote, g, 3, MPI_FLOAT, MPI_MAX, halo.comm());
210 const MpiDriftVote v{g[0], 0.1f * g[0], kDriftSlack * g[0], g[2]};
211 int commSize = 1;
212 MPI_Comm_size(halo.comm(), &commSize);
213 // One rank owns everything: there is no other owner to move a particle to (a migration would be
214 // a host round trip that changes nothing).
215#if defined(PECLET_DEM_TEST_MUTANT) && PECLET_DEM_TEST_MUTANT == 5
216 if (false) { // G13 mutant 5: the drift vote never migrates
217#else
218 if (commSize > 1 && g[1] >= v.slack) {
219#endif
220 halo.migrateToBlocks(P);
221 ++P.splitStats.driftMigrations;
222 }
223 return v;
224}
228inline double mpiXpbdBand(const Particles& P, ParticleHalo& halo, double rcut,
229 const MpiDriftVote& v) {
230 return std::max(rcut, xpbdContactReach(globalMaxRadius(P, halo.comm())) +
231 static_cast<double>(v.slack) +
232 (halo.verletSkin() > 0.0f ? 0.0 : static_cast<double>(v.dMax)));
233}
234
261inline void demStepMpi(Particles& P, ParticleHalo& halo, double rcut, int syncEvery,
262 bool forwardRotation) {
263 CpExec space;
264 Kokkos::Profiling::pushRegion("dem::vote");
265 const MpiDriftVote vote = mpiDriftVote(P, halo);
266 Kokkos::Profiling::popRegion();
267 // Broadphase / narrow-phase margin: 0.1 R_max over ALL ranks, so both owners of a cross-face
268 // pair report it at the same gap (a rank-local R_max dropped the pair on the rank whose own
269 // grains are small while the other kept it).
270 const float margin = vote.margin;
271
272 if (P.growthFactor != -1.0f && P.growthRate != 0.0f) {
273 P.growthFactor *= std::exp(P.growthRate * P.dt);
274 if (P.growthFactor > 1.0f)
275 P.growthFactor = 1.0f;
276 }
277 if (P.growthFactor > 0.0f)
278 updateGrowthScalesKokkos(P.numReal, P.scale, P.targetScale, P.growthFactor);
279 const double band = mpiXpbdBand(P, halo, rcut, vote);
280
281 // 1. Predict velocity on the owned set (no ghosts yet -> numParticles == numReal).
282 P.numParticles = P.numReal;
283 predictVelocityKokkos(P.numReal, P.pos, P.invMass, P.vel, P.quat, P.angVel, P.invInertia,
284 P.posPred, P.quatPred, P.velPred, P.angVelPred, P.deltaPos, P.deltaQuat,
285 P.deltaVel, P.deltaAngVel, P.constraintCounts, P.gravity, P.dt, P.extForce,
286 P.extTorque);
287
288 // 2. Gather ghosts (real mass) from owners over the halo: full state -- including gid,
289 // materialId and the warm grounded level -- into the ghost slots; sets
290 // P.numParticles = numReal + numGhost and self-maps realIndices.
291 Kokkos::Profiling::pushRegion("dem::gather");
292 halo.gather(P, band);
293 Kokkos::Profiling::popRegion();
294 // The velocity-phase slot map (docs/contact_solve_framework.md §6.2): one velocity slot per body
295 // per rank -- a periodic self image maps to the owned slot, other images of one body to its
296 // lowest ghost slot. Here, not in gather (the Hertz engine shares gather). Positions stay per
297 // slot.
298 Kokkos::Profiling::pushRegion("dem::mapSlots");
299 halo.mapVelocitySlots(P);
300 Kokkos::Profiling::popRegion();
301
302 fillWorldRadiiKokkos(P.scale, P.rad, P.globalScale, P.baseRadius, P.numParticles);
303
304 // 3. Broad/narrow phase + manifold reduction over owned + ghosts (contactSlot map included:
305 // the PGS friction bound and the position-channel Coulomb carry read through it).
306 // findCollisionsGrow fences + reads the pair count back to host and guarantees np <= P.pairs
307 // extent (growing the buffer on overflow) so the narrowphase never reads P.pairs out of bounds.
308 Kokkos::Profiling::pushRegion("dem::broad");
309 const int np = findCollisionsGrow(P, margin);
310 Kokkos::Profiling::popRegion();
311
312 Kokkos::deep_copy(space, P.contactCount, 0);
313 Kokkos::deep_copy(space, P.maxOverlap, 0.0f);
314 Kokkos::Profiling::pushRegion("dem::narrow");
315 const int nc = narrowPhaseGrow(P, np, margin);
316 Kokkos::Profiling::popRegion();
317
318 // Contact ownership (docs/mpi_momentum_conservation.md §2.1-2.2): owned contacts first, then
319 // the manifolds reduced owned-first. The solve runs on [0, ncOwned) / [0, nmOwned); the
320 // visible counts stay in P.contactCount / P.manifoldCount for the getters.
321 Kokkos::Profiling::pushRegion("dem::ownership");
322 const int ncOwned = partitionContactsKokkos(P.contacts, nc, halo.contactOwnership(P));
323 Kokkos::Profiling::popRegion();
324 if (P.debugCapture)
325 debugCaptureOwnedContacts(P, halo, ncOwned);
326 int nmOwned = 0;
327 reduceContactsToManifoldsKokkos(P.contacts, nc, P.manifolds, P.manifoldCount, P.contactSlot,
328 ncOwned, &nmOwned);
329 const int nmVisible = readInt(P.manifoldCount);
330
331 // 4-6. The shared modern velocity + position solve, distributed: rank-local colouring over the
332 // owned + ghost body slots (nBodies = numParticles; realIndices are self-mapped, so ghost
333 // copies evolve in place between reconciliations), persistent-pair keys from the global ids.
334 Kokkos::Profiling::pushRegion("dem::solve");
335 demSolveContacts(P, ncOwned, nmOwned, P.numParticles, P.gid,
336 MpiSolveHooks{halo, syncEvery < 1 ? 1 : syncEvery, forwardRotation, nmVisible});
337 Kokkos::Profiling::popRegion();
338
339 // 7. Commit (owned results kept; the ghost slots, reconciled by the final sync, are dropped and
340 // re-gathered next substep).
341 finalCommitKokkos(P.numReal, P.pos, P.invMass, P.posPred, P.quat, P.quatPred, P.domain);
342
343 if (P.thermostatTau > 0.0f && P.dt > 0.0f)
344 applyThermostatKokkos(P.numReal, P.vel, P.invMass, P.angVel, P.invInertia, P.quat,
345 P.thermostatKB, P.thermostatTau, P.thermostatTemp, P.dt);
346
347 P.numParticles = P.numReal; // restore owned-only active count for getters
348}
349
356struct MpiForceHooks {
357 static constexpr bool distributed = true;
358 ParticleHalo& halo;
359 double band;
360 float slack; // drift slack S = kDriftSlack R_max (§5.5)
361 float allMax(float v) const {
362 float g = v;
363 MPI_Allreduce(&v, &g, 1, MPI_FLOAT, MPI_MAX, halo.comm());
364 return g;
365 }
366 float allMin(float v) const {
367 float g = v;
368 MPI_Allreduce(&v, &g, 1, MPI_FLOAT, MPI_MIN, halo.comm());
369 return g;
370 }
371 void gatherGhosts(Particles& P) const {
372 // The drift vote at every pair-list rebuild (docs/contact_solve_framework.md §5.5): if some
373 // particle sits S or more beyond its owner's block (committed positions; the list criterion
374 // covers motion until the next rebuild), move every particle to its current block first. The
375 // live Mindlin springs ride the migration pack (gid-keyed) exactly as in a rebalance.
376 float e = 0.0f, d = 0.0f;
377 driftVoteLocalKokkos(P, halo.blockBox(), 0.0f, e, d);
378 int commSize = 1;
379 MPI_Comm_size(halo.comm(), &commSize);
380 if (allMax(e) >= slack && commSize > 1) {
381 halo.migrateToBlocks(P);
382 ++P.splitStats.driftMigrations;
383 // The owned force accumulators are zeroed as they are consumed (integrate), the ghost ones
384 // only at the start of each step (clearGhostScratch) and then left holding the discarded
385 // ghost halves. A migration turns former ghost slots into owned ones: clear the owned range
386 // so no stale ghost force is integrated (measured: dP 1e-8 -> 9e-4 at the first migration).
387 zeroForceScratchKokkos(P.deltaVel, P.deltaAngVel, 0, P.numReal);
388 }
389 halo.invalidateTopology(); // fresh band + fresh positions at every pair rebuild
390 halo.gather(P, band);
391 fillWorldRadiiKokkos(P.scale, P.rad, P.globalScale, P.baseRadius, P.numParticles);
392 }
393 void refreshGhostState(Particles& P, bool needQuat) const {
394 halo.forwardPositions(P.pos);
395 halo.forward(P.vel);
396 halo.forward(P.angVel);
397 if (needQuat)
398 halo.forward4(P.quat);
399 }
400 void clearGhostScratch(Particles& P) const {
401 zeroForceScratchKokkos(P.deltaVel, P.deltaAngVel, P.numReal, P.numReal + halo.numGhost());
402 }
403};
404
408inline void demStepHertzMpi(Particles& P, ParticleHalo& halo, float dt, int nsteps,
409 float skinFrac) {
410 // Ghost band = worst-case pair cutoff (2 R_max,global) + skin + drift slack S (§5.5). The
411 // driver's skin is skinFrac * R_min,global <= skinFrac * R_max,global, so this band bounds it;
412 // S covers a partner up to S beyond its owner's block (the rebuild vote keeps it there).
413 float maxR = P.numReal > 0 ? maxOwnedRadius(P) : 0.0f, maxRg = maxR;
414 MPI_Allreduce(&maxR, &maxRg, 1, MPI_FLOAT, MPI_MAX, halo.comm());
415 const float slack = kDriftSlack * maxRg;
416 const double band = (2.0 + skinFrac) * static_cast<double>(maxRg) + static_cast<double>(slack);
417 demStepForce(P, dt, nsteps, skinFrac, HertzMindlinLaw{}, MpiForceHooks{halo, band, slack});
418 P.numParticles = P.numReal; // restore owned-only active count for getters
419}
420
421} // namespace peclet::dem
422
423#endif // PECLET_DEM_MPI
424#endif // DEM_STEP_SOLVE_MPI_HPP
dem — portable (Kokkos) owner<->ghost particle halo for the distributed XPBD step.
int narrowPhaseGrow(Particles &P, int np, float margin)
Narrow phase with an automatically-grown contact buffer.
int partitionContactsKokkos(Kokkos::View< ContactC *, CpMem > contacts, int n, const Owns &owns)
Stable partition of contacts [0, n) by owns(contact): the owned ones first, [0, ncOwned),...
float maxOwnedRadius(const Particles &P)
Largest effective particle radius over the owned set (= max scale × globalScale, growth included).
void zeroForceScratchKokkos(V3 dv, V3 dw, int lo, int hi)
Zero the force/torque accumulator rows [lo, hi) — under MPI the pair kernels atomically accumulate on...
void demSolveContacts(Particles &P, int nc, int nm, int nBodies, Kokkos::View< const int *, CpMem > keyIdx, const Hooks &hooks)
One full velocity + position contact solve over the already-built contacts/manifolds (see file commen...
int readInt(Kokkos::View< int, CpMem > v)
void demStepForce(Particles &P, float dt, int nsteps, float skinFrac, const Law &law, const Hooks &hooks)
nsteps explicit force-based DEM steps of size dt with force law Law (see file comment).
int reduceContactsToManifoldsKokkos(Kokkos::View< const ContactC *, CpMem > contacts, int n, Kokkos::View< ManifoldC *, CpMem > outManifolds, Kokkos::View< int, CpMem > outCount, Kokkos::View< int *, CpMem > contactSlot={}, int numOwnedContacts=-1, int *numOwnedManifolds=nullptr)
Reduce n contacts to manifolds (one per unique canonical pair).
int findCollisionsGrow(Particles &P, float margin)
Broad phase with an automatically-grown pair buffer.
void finalCommitKokkos(int n, V3 pos, Vf invMass, V3 posPred, V4 quat, V4 quatPred, Domain dom)
Final commit: periodic wrap of the predicted position into the domain, commit position + quat.
void fillWorldRadiiKokkos(Vf scale, Vf rad, float gs, float bR, int n)
World radii rad(i) = scale(i) * globalScale * baseRadius over [0, n): the owned span before a step,...
void updateGrowthScalesKokkos(int n, Vf scale, Vf targetScale, float factor)
Growth mode: scale = target * factor (when active).
void predictVelocityKokkos(int n, V3 pos, Vf invMass, V3 vel, V4 quat, V3 angVel, V3 invInertia, V3 posPred, V4 quatPred, V3 velPred, V3 angVelPred, V3 deltaPos, V4 deltaQuat, V3 deltaVel, V3 deltaAngVel, Vi constraintCounts, F3 gravity, float dt, V3 extForce, V3 extTorque)
Predict velocity (gravity + gyroscopic precession), speculative position, and clear all deltas.
void applyThermostatKokkos(int numReal, V3 vel, Vf invMass, V3 angVel, V3 invInertia, V4 quat, double kB, double tau, double Ttarget, float dt)
Kokkos::DefaultExecutionSpace CpExec
dem — portable (Kokkos) particle SoA container: the storage the dem flip pivots on.
int MPI_Allreduce(const void *a, void *b, int c, MPI_Datatype d, MPI_Op e, MPI_Comm f)
Definition pmpi.cpp:6
dem — the shared contact-solve driver: the full modern velocity + position solve sequence (warm-start...
dem — the force-based DEM step driver: explicit soft-contact time stepping (the engine family that co...
dem — the single-rank step drivers over the particle SoA: the XPBD substep (demStep),...