peclet-dem 0.4.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
solver_multilevel.hpp
Go to the documentation of this file.
1
28#ifndef DEM_SOLVER_MULTILEVEL_HPP
29#define DEM_SOLVER_MULTILEVEL_HPP
30
31#include <Kokkos_Core.hpp>
32#include <utility>
33#include <vector>
34
35#include "contact_preprocessing.hpp" // ManifoldC, colorKey, CpExec/CpMem
36#include "solver_fused.hpp" // demGridBarrier / FusedSweepCtx (fused coarse cycle)
37#include "solver_velocity.hpp" // PGSManifoldSweep
38
39namespace peclet::dem {
40
43inline constexpr int kMlMaxLevels = 10;
44inline constexpr int kMlSlotSkip = 63;
45
48 int numLevels = 0; // coarse levels built (0 = aggregation found nothing)
49 std::vector<int> groupOff; // per level: offset of its group arrays in the group pools
50 std::vector<int> numGroups; // per level: group count
51 std::vector<int> parentOff; // per level: offset of its parent map (from level l-1 ids)
52 std::vector<int> numColors; // per level: colours used by the crossing-manifold coloring
53};
54
56struct MlScratch {
57 Kokkos::View<long long*, CpMem> colorPacked; // per manifold: 6-bit colour slot per level
58 Kokkos::View<int*, CpMem> parent; // packed parent maps (pool, 5*cap)
59 Kokkos::View<float*, CpMem> invMassG; // packed per-group inverse mass (pool, 4*cap)
60 Kokkos::View<float* [3], CpMem> velG; // packed per-group velocity (pool, 4*cap)
61 Kokkos::View<float* [3], CpMem> velG0; // restriction snapshot (pool, 4*cap)
62 Kokkos::View<float*, CpMem> massG; // packed per-group mass accumulator (pool, 4*cap)
63 Kokkos::View<int*, CpMem> grp; // composed REAL-body -> current-level group map
64 Kokkos::View<int*, CpMem> mate; // matching scratch (per group of the finer level)
65};
66
67namespace mldetail {
70KOKKOS_INLINE_FUNCTION float effMass(float invMass) {
71 return 1.0f / Kokkos::fmax(invMass, 1e-30f);
72}
73
83inline constexpr int kGatePersistent = 1;
84inline constexpr int kGateCone = 2;
85inline constexpr int kGateSlip = 4;
86
90KOKKOS_INLINE_FUNCTION bool eligible(const ManifoldC& m, int idx,
91 Kokkos::View<const int*, CpMem> mColor,
92 Kokkos::View<const float*, CpMem> vn0,
93 Kokkos::View<const float* [3], CpMem> vt0,
94 Kokkos::View<const unsigned char*, CpMem> persistent,
95 Kokkos::View<const float* [3], CpMem> posPred, F3 gHat,
96 float qsThr, int gateMask) {
97 if (m.num_points <= 0 || mColor(idx) < 0)
98 return false;
99 if ((gateMask & kGatePersistent) && persistent(idx) == 0)
100 return false;
101 if (Kokkos::fabs(vn0(idx)) > qsThr)
102 return false; // ballistic pair: never aggregated (rebound stays fine + symmetric)
103 if (gateMask & kGateSlip) {
104 // Slip threshold = qsThr. Measured trade-off: a pour-settling bed's load-bearing contacts
105 // slip at 2-8 g dt (a tighter floor excludes them and the bed crushes), while silo bulk
106 // creep slips at <= 8 g dt (so some of it stays in and costs ~7% discharge vs the one-sided
107 // default). The two distributions overlap in g dt units -- no absolute threshold separates
108 // them; a slip-persistence (time-integrated) gate is the identified next refinement.
109 const F3 vt{vt0(idx, 0), vt0(idx, 1), vt0(idx, 2)};
110 if (dot3(vt, vt) > qsThr * qsThr)
111 return false;
112 }
113 if (m.bodyB < 0)
114 return true;
115 if (gateMask & kGateCone) {
116 const F3 dx = sub3(ldF3(posPred, m.bodyA), ldF3(posPred, m.bodyB));
117 const float up = -(dx.x * gHat.x + dx.y * gHat.y + dx.z * gHat.z);
118 return Kokkos::fabs(up) > 0.3f * Kokkos::sqrt(dot3(dx, dx));
119 }
120 return true;
121}
122} // namespace mldetail
123
129 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
130 Kokkos::View<const int*, CpMem> realIdx, Kokkos::View<const int*, CpMem> mColor,
131 Kokkos::View<const float*, CpMem> vn0, Kokkos::View<const float* [3], CpMem> vt0,
132 Kokkos::View<const unsigned char*, CpMem> persistent,
133 Kokkos::View<const float* [3], CpMem> posPred, F3 gHat,
134 Kokkos::View<const float*, CpMem> invMass, float qsThr, int gateMask, int numReal, MlScratch& S,
135 Kokkos::View<long long*, CpMem> winner, Kokkos::View<std::uint64_t*, CpMem> colorMask,
136 bool excludeImmovable = false, Kokkos::View<const unsigned char*, CpMem> asleep = {}) {
137 ContactHierarchy H;
138 CpExec space;
139 if (numManifolds <= 0 || numReal <= 0)
140 return H;
141 // A sleeper carries a small POSITIVE effective inverse mass (heavy but not rigid, so the fine PGS
142 // does not diverge against it), so the invMass==0 test alone no longer identifies it — exclude by
143 // the asleep flag too. Genuinely-pinned bodies (real invMass 0) stay caught by the ==0 branch.
144 const bool haveAsleep = asleep.extent(0) > 0;
145 { // level-0 composed map = identity; packed colours all "skip"
146 auto grp = S.grp;
147 Kokkos::parallel_for(
148 "peclet::dem::ml_grp_init", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
149 KOKKOS_LAMBDA(int i) { grp(i) = i; });
150 auto cp = S.colorPacked;
151 Kokkos::parallel_for(
152 "peclet::dem::ml_packed_init", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
153 KOKKOS_LAMBDA(int idx) { cp(idx) = ~0ll; }); // every 6-bit slot = 63 (skip)
154 }
155
156 int ngPrev = numReal; // group count of the finer level (level 0 = real bodies)
157 int parentOff = 0, groupOff = 0;
158 const int parentCap = static_cast<int>(S.parent.extent(0));
159 const int groupCap = static_cast<int>(S.invMassG.extent(0));
160
161 for (int lvl = 1; lvl <= kMlMaxLevels; ++lvl) {
162 if (parentOff + ngPrev > parentCap)
163 break; // pool exhausted (matching stalled repeatedly) -- use what we have
164 // ---- greedy random-priority matching on the current group graph ----
165 Kokkos::parallel_for(
166 "peclet::dem::ml_match_reset", Kokkos::RangePolicy<CpExec>(space, 0, ngPrev),
167 KOKKOS_LAMBDA(int g) { winner(g) = -1; });
168 {
169 auto grp = S.grp;
170 Kokkos::parallel_for(
171 "peclet::dem::ml_match_contend", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
172 KOKKOS_LAMBDA(int idx) {
173 const ManifoldC m = manifolds(idx);
174 if (m.bodyB < 0 || !mldetail::eligible(m, idx, mColor, vn0, vt0, persistent, posPred,
175 gHat, qsThr, gateMask))
176 return;
177 // Never aggregate an immovable (sleeping / pinned) body: a group carrying its huge
178 // effMass wrecks the coarse-solve conditioning and pins the awake partner. (Gated so
179 // the sleeping-off path is bit-identical.)
180 if (excludeImmovable &&
181 (invMass(realIdx(m.bodyA)) == 0.0f || invMass(realIdx(m.bodyB)) == 0.0f ||
182 (haveAsleep && (asleep(realIdx(m.bodyA)) || asleep(realIdx(m.bodyB))))))
183 return;
184 const int gA = grp(realIdx(m.bodyA)), gB = grp(realIdx(m.bodyB));
185 if (gA == gB)
186 return;
187 const long long key = colorKey(idx);
188 Kokkos::atomic_max(&winner(gA), key);
189 Kokkos::atomic_max(&winner(gB), key);
190 });
191 auto mate = S.mate;
192 Kokkos::parallel_for(
193 "peclet::dem::ml_mate_init", Kokkos::RangePolicy<CpExec>(space, 0, ngPrev),
194 KOKKOS_LAMBDA(int g) { mate(g) = g; });
195 Kokkos::parallel_for(
196 "peclet::dem::ml_match_commit", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
197 KOKKOS_LAMBDA(int idx) {
198 const ManifoldC m = manifolds(idx);
199 if (m.bodyB < 0 || !mldetail::eligible(m, idx, mColor, vn0, vt0, persistent, posPred,
200 gHat, qsThr, gateMask))
201 return;
202 if (excludeImmovable &&
203 (invMass(realIdx(m.bodyA)) == 0.0f || invMass(realIdx(m.bodyB)) == 0.0f ||
204 (haveAsleep && (asleep(realIdx(m.bodyA)) || asleep(realIdx(m.bodyB))))))
205 return;
206 const int gA = grp(realIdx(m.bodyA)), gB = grp(realIdx(m.bodyB));
207 if (gA == gB)
208 return;
209 const long long key = colorKey(idx);
210 if (winner(gA) == key && winner(gB) == key) { // sole winner of both endpoints
211 mate(gA) = gB;
212 mate(gB) = gA;
213 }
214 });
215 }
216 // ---- compact new group ids: matched pair -> one id (leader = smaller), singleton keeps ----
217 int ngNew = 0;
218 {
219 auto mate = S.mate;
220 auto parent = S.parent;
221 const int off = parentOff;
222 Kokkos::parallel_scan(
223 "peclet::dem::ml_compact", Kokkos::RangePolicy<CpExec>(space, 0, ngPrev),
224 KOKKOS_LAMBDA(int g, int& run, const bool final) {
225 const bool leader = (mate(g) >= g);
226 if (leader)
227 ++run;
228 if (final && leader)
229 parent(off + g) = run - 1;
230 },
231 ngNew);
232 Kokkos::parallel_for(
233 "peclet::dem::ml_parent_nonleader", Kokkos::RangePolicy<CpExec>(space, 0, ngPrev),
234 KOKKOS_LAMBDA(int g) {
235 if (mate(g) < g)
236 parent(off + g) = parent(off + mate(g));
237 });
238 }
239 if (ngNew >= ngPrev || ngNew > (9 * ngPrev) / 10)
240 break; // matching stalled: deeper levels would not shrink the problem
241 if (groupOff + ngNew > groupCap)
242 break;
243 // ---- compose the body -> group map and record the level ----
244 {
245 auto grp = S.grp;
246 auto parent = S.parent;
247 const int off = parentOff;
248 Kokkos::parallel_for(
249 "peclet::dem::ml_grp_compose", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
250 KOKKOS_LAMBDA(int i) { grp(i) = parent(off + grp(i)); });
251 }
252 // ---- per-group inverse mass ----
253 {
254 auto grp = S.grp;
255 auto massG = S.massG;
256 auto invMassG = S.invMassG;
257 const int off = groupOff;
258 Kokkos::parallel_for(
259 "peclet::dem::ml_mass_reset", Kokkos::RangePolicy<CpExec>(space, 0, ngNew),
260 KOKKOS_LAMBDA(int g) { massG(off + g) = 0.0f; });
261 Kokkos::parallel_for(
262 "peclet::dem::ml_mass_accum", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
263 KOKKOS_LAMBDA(int i) {
264 Kokkos::atomic_add(&massG(off + grp(i)), mldetail::effMass(invMass(i)));
265 });
266 Kokkos::parallel_for(
267 "peclet::dem::ml_mass_invert", Kokkos::RangePolicy<CpExec>(space, 0, ngNew),
268 KOKKOS_LAMBDA(int g) { invMassG(off + g) = 1.0f / massG(off + g); });
269 }
270 // ---- colour the crossing manifolds of this level on the group graph ----
271 // Same round-based random-priority arbitration as colorManifoldsKokkos, endpoints = groups.
272 // Committed colours land in this level's 6-bit slot of colorPacked (63 stays = skip).
273 {
274 auto grp = S.grp;
275 auto cp = S.colorPacked;
276 const int slotShift = 6 * (lvl - 1);
277 Kokkos::parallel_for(
278 "peclet::dem::ml_color_mask_reset", Kokkos::RangePolicy<CpExec>(space, 0, ngNew),
279 KOKKOS_LAMBDA(int g) { colorMask(g) = 0; });
280 // temp per-manifold state via mate-array-free trick: track "uncoloured" in the packed slot
281 // itself (63 = pending here; eligibility recomputed per round).
282 int remaining = 1, prevRemaining = -1, maxc = -1;
283 for (int round = 0; round < 64 && remaining > 0; ++round) {
284 Kokkos::parallel_for(
285 "peclet::dem::ml_color_reset_winner", Kokkos::RangePolicy<CpExec>(space, 0, ngNew),
286 KOKKOS_LAMBDA(int g) { winner(g) = -1; });
287 Kokkos::parallel_for(
288 "peclet::dem::ml_color_contend", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
289 KOKKOS_LAMBDA(int idx) {
290 const ManifoldC m = manifolds(idx);
291 if (!mldetail::eligible(m, idx, mColor, vn0, vt0, persistent, posPred, gHat, qsThr,
292 gateMask))
293 return;
294 if (((cp(idx) >> slotShift) & 63) != kMlSlotSkip)
295 return; // committed in an earlier round
296 const int gA = grp(realIdx(m.bodyA));
297 const int gB = (m.bodyB >= 0) ? grp(realIdx(m.bodyB)) : -1;
298 if (gB == gA)
299 return; // internal to an aggregate at this level
300 const long long key = colorKey(idx);
301 Kokkos::atomic_max(&winner(gA), key);
302 if (gB >= 0)
303 Kokkos::atomic_max(&winner(gB), key);
304 });
305 int rem = 0;
306 Kokkos::parallel_reduce(
307 "peclet::dem::ml_color_commit", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
308 KOKKOS_LAMBDA(int idx, int& acc) {
309 const ManifoldC m = manifolds(idx);
310 if (!mldetail::eligible(m, idx, mColor, vn0, vt0, persistent, posPred, gHat, qsThr,
311 gateMask))
312 return;
313 if (((cp(idx) >> slotShift) & 63) != kMlSlotSkip)
314 return;
315 const int gA = grp(realIdx(m.bodyA));
316 const int gB = (m.bodyB >= 0) ? grp(realIdx(m.bodyB)) : -1;
317 if (gB == gA)
318 return;
319 const long long key = colorKey(idx);
320 if (winner(gA) != key || (gB >= 0 && winner(gB) != key)) {
321 acc += 1;
322 return;
323 }
324 std::uint64_t forbidden = colorMask(gA);
325 if (gB >= 0)
326 forbidden |= colorMask(gB);
327 int c = 0;
328 while (c < kMlSlotSkip - 1 && (forbidden & (std::uint64_t(1) << c)))
329 ++c;
330 cp(idx) = (cp(idx) & ~(63ll << slotShift)) | (static_cast<long long>(c) << slotShift);
331 const std::uint64_t bit = std::uint64_t(1) << c;
332 colorMask(gA) |= bit;
333 if (gB >= 0)
334 colorMask(gB) |= bit;
335 },
336 rem);
337 space.fence();
338 if (rem == prevRemaining)
339 break; // mask saturation: leftovers keep slot 63 and are skipped at this level
340 prevRemaining = rem;
341 remaining = rem;
342 }
343 Kokkos::parallel_reduce(
344 "peclet::dem::ml_color_max", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
345 KOKKOS_LAMBDA(int idx, int& mx) {
346 const int c = static_cast<int>((cp(idx) >> slotShift) & 63);
347 if (c != kMlSlotSkip && c > mx)
348 mx = c;
349 },
350 Kokkos::Max<int>(maxc));
351 space.fence();
352 H.numColors.push_back(maxc + 1);
353 }
354 H.groupOff.push_back(groupOff);
355 H.numGroups.push_back(ngNew);
356 H.parentOff.push_back(parentOff);
357 H.numLevels = lvl;
358 parentOff += ngPrev;
359 groupOff += ngNew;
360 ngPrev = ngNew;
361 if (ngNew <= 32)
362 break; // coarse enough: a handful of super-bodies solves in one sweep
363 }
364 space.fence();
365 return H;
366}
367
379inline void buildCoarseBucketsKokkos(const ContactHierarchy& H, MlScratch& S, int numManifolds,
380 Kokkos::View<int*, CpMem> colorScratch,
381 Kokkos::View<int*, CpMem> perm,
382 Kokkos::View<int*, CpMem> cursor,
383 std::vector<std::vector<int>>& offs) {
384 offs.assign(static_cast<std::size_t>(H.numLevels), {});
385 if (numManifolds <= 0)
386 return;
387 CpExec space;
388 for (int lvl = 1; lvl <= H.numLevels; ++lvl) {
389 const int slotShift = 6 * (lvl - 1);
390 const int nCol = H.numColors[static_cast<std::size_t>(lvl) - 1];
391 auto cp = S.colorPacked;
392 auto cs = colorScratch;
393 Kokkos::parallel_for(
394 "peclet::dem::ml_bucket_colors", Kokkos::RangePolicy<CpExec>(space, 0, numManifolds),
395 KOKKOS_LAMBDA(int idx) {
396 const int c = static_cast<int>((cp(idx) >> slotShift) & 63);
397 cs(idx) = (c < nCol) ? c : -1;
398 });
399 auto seg =
400 Kokkos::subview(perm, Kokkos::pair<int, int>((lvl - 1) * numManifolds, lvl * numManifolds));
401 buildColorBucketsKokkos(Kokkos::View<const int*, CpMem>(colorScratch), numManifolds, nCol, seg,
402 cursor, offs[static_cast<std::size_t>(lvl) - 1]);
403 }
404}
405
411 Kokkos::View<const ManifoldC*, CpMem> manifolds;
412 Kokkos::View<const int*, CpMem> realIdx;
413 Kokkos::View<const int*, CpMem> grp;
414 Kokkos::View<float* [3], CpMem> velG;
415 Kokkos::View<const float*, CpMem> invMassG;
416 Kokkos::View<float*, CpMem> lambdaAcc;
417 Kokkos::View<float, CpMem> maxApproach;
418 Kokkos::View<const float*, CpMem> restRel;
419
420 KOKKOS_FUNCTION void solveOne(int idx, int off) const {
421 // Poisson release in flight on this pair: the coarse e = 0 solve targets vtil = 0 on
422 // the SHARED accumulator and would retract the just-injected separation velocity —
423 // the releasing contact skips the coarse transport this substep.
424 if (restRel.extent(0) > 0 && restRel(idx) > 0.0f)
425 return;
426 const ManifoldC m = manifolds(idx);
427 const int gA = grp(realIdx(m.bodyA));
428 const int gB = (m.bodyB >= 0) ? grp(realIdx(m.bodyB)) : -1;
429 const float invN = 1.0f / static_cast<float>(m.num_points);
430 const F3 Nsum{m.normal_sum.x, m.normal_sum.y, m.normal_sum.z};
431 const float lenN = Kokkos::sqrt(dot3(Nsum, Nsum));
432 if (lenN < 1e-9f)
433 return;
434 const F3 rAavg = scale3(F3{m.rA_sum.x, m.rA_sum.y, m.rA_sum.z}, invN);
435 const F3 rBavg = scale3(F3{m.rB_sum.x, m.rB_sum.y, m.rB_sum.z}, invN);
436 const F3 diffCenters = (gB < 0) ? rAavg : sub3(rAavg, rBavg);
437 const F3 vA{velG(off + gA, 0), velG(off + gA, 1), velG(off + gA, 2)};
438 F3 vB{0, 0, 0};
439 if (gB >= 0)
440 vB = F3{velG(off + gB, 0), velG(off + gB, 1), velG(off + gB, 2)};
441 else
442 vB = scale3(F3{m.wallVel_sum.x, m.wallVel_sum.y, m.wallVel_sum.z}, invN);
443 const float vn = dot3(sub3(vA, vB), Nsum);
444 const float alignment = dot3(Nsum, diffCenters);
445 const float sgn = (alignment > 0.0f) ? 1.0f : -1.0f;
446 const float invMA = invMassG(off + gA);
447 const float invMB = (gB >= 0) ? invMassG(off + gB) : 0.0f;
448 const float w = dot3(Nsum, Nsum) * (invMA + invMB);
449 if (w <= 0.0f)
450 return;
451 const float vtil = sgn * vn;
452 const float dp = vtil / w; // e = 0: target 0 (pure inelastic support)
453 const float pOld = lambdaAcc(idx);
454 float pNew = pOld + dp;
455 if (pNew < 0.0f)
456 pNew = 0.0f;
457 const float d = pNew - pOld;
458 if (d == 0.0f)
459 return;
460 lambdaAcc(idx) = pNew;
461 Kokkos::atomic_max(&maxApproach(), Kokkos::fabs(d) * w / lenN);
462 const float lambda = -sgn * d;
463 const F3 J = scale3(Nsum, lambda);
464 velG(off + gA, 0) += J.x * invMA;
465 velG(off + gA, 1) += J.y * invMA;
466 velG(off + gA, 2) += J.z * invMA;
467 if (gB >= 0) {
468 velG(off + gB, 0) -= J.x * invMB;
469 velG(off + gB, 1) -= J.y * invMB;
470 velG(off + gB, 2) -= J.z * invMB;
471 }
472 }
473};
474
478 int numLevels = 0, numReal = 0, coarseSweeps = 0;
483 int permBase[kMlMaxLevels] = {}; // level's segment base in bkPerm
484 int offsBase[kMlMaxLevels] = {}; // level's colour-offset base in the flat device offs
485};
486
490 Kokkos::View<const int*, CpMem> offsDev;
491 Kokkos::View<unsigned*, CpMem> bar;
493 int maxWork = 0;
494};
495
496#ifdef KOKKOS_ENABLE_CUDA
503__device__ inline void demMlCoarseCycleDevice(
504 const MlCoarseSweep& f, const Kokkos::View<const int*, CpMem>& parent,
505 const Kokkos::View<const float*, CpMem>& invMass,
506 const Kokkos::View<float* [3], CpMem>& velPred, const Kokkos::View<float* [3], CpMem>& velG0,
507 const Kokkos::View<const float*, CpMem>& massG, const Kokkos::View<int*, CpMem>& grpW,
508 const Kokkos::View<const int*, CpMem>& bkPerm, const Kokkos::View<const int*, CpMem>& offsDev,
509 const MlFusedMeta& meta, unsigned* bar, unsigned& k, int tid, int stride) {
510 for (int i = tid; i < meta.numReal; i += stride) // reset the composed map to identity
511 grpW(i) = i;
512 demGridBarrier(bar, k++);
513 for (int lvl = 1; lvl <= meta.numLevels; ++lvl) {
514 const int off = meta.groupOff[lvl - 1], ng = meta.numGroups[lvl - 1];
515 const int pOff = meta.parentOff[lvl - 1], nCol = meta.numColors[lvl - 1];
516 for (int i = tid; i < meta.numReal; i += stride) // compose to this level
517 grpW(i) = parent(pOff + grpW(i));
518 demGridBarrier(bar, k++);
519 for (int g = tid; g < ng; g += stride) // restrict: reset
520 f.velG(off + g, 0) = f.velG(off + g, 1) = f.velG(off + g, 2) = 0.0f;
521 demGridBarrier(bar, k++);
522 for (int i = tid; i < meta.numReal; i += stride) { // restrict: momentum accumulate
523 const float m = mldetail::effMass(invMass(i));
524 const int g = grpW(i);
525 Kokkos::atomic_add(&f.velG(off + g, 0), m * velPred(i, 0));
526 Kokkos::atomic_add(&f.velG(off + g, 1), m * velPred(i, 1));
527 Kokkos::atomic_add(&f.velG(off + g, 2), m * velPred(i, 2));
528 }
529 demGridBarrier(bar, k++);
530 for (int g = tid; g < ng; g += stride) { // restrict: normalize + snapshot V0
531 const float invM = 1.0f / massG(off + g);
532 for (int c = 0; c < 3; ++c) {
533 f.velG(off + g, c) *= invM;
534 velG0(off + g, c) = f.velG(off + g, c);
535 }
536 }
537 demGridBarrier(bar, k++);
538 for (int s = 0; s < meta.coarseSweeps; ++s)
539 for (int color = 0; color < nCol; ++color) {
540 const int b = offsDev(meta.offsBase[lvl - 1] + color);
541 const int e = offsDev(meta.offsBase[lvl - 1] + color + 1);
542 if (b == e)
543 continue; // empty class: nothing written, no barrier (uniform: same offsets read)
544 for (int i2 = b + tid; i2 < e; i2 += stride)
545 f.solveOne(bkPerm(meta.permBase[lvl - 1] + i2), off);
546 demGridBarrier(bar, k++);
547 }
548 for (int i = tid; i < meta.numReal; i += stride) { // prolongate
549 const int g = grpW(i);
550 velPred(i, 0) += f.velG(off + g, 0) - velG0(off + g, 0);
551 velPred(i, 1) += f.velG(off + g, 1) - velG0(off + g, 1);
552 velPred(i, 2) += f.velG(off + g, 2) - velG0(off + g, 2);
553 }
554 demGridBarrier(bar, k++);
555 }
556}
557
558__global__ void demFusedCoarseCycleK(
559 MlCoarseSweep f, Kokkos::View<const int*, CpMem> parent,
560 Kokkos::View<const float*, CpMem> invMass, Kokkos::View<float* [3], CpMem> velPred,
561 Kokkos::View<float* [3], CpMem> velG0, Kokkos::View<const float*, CpMem> massG,
562 Kokkos::View<int*, CpMem> grpW, Kokkos::View<const int*, CpMem> bkPerm,
563 Kokkos::View<const int*, CpMem> offsDev, MlFusedMeta meta, unsigned* bar) {
564 const int stride = gridDim.x * blockDim.x;
565 const int tid = blockIdx.x * blockDim.x + threadIdx.x;
566 unsigned k = 0;
567 demMlCoarseCycleDevice(f, parent, invMass, velPred, velG0, massG, grpW, bkPerm, offsDev, meta,
568 bar, k, tid, stride);
569}
570
576__global__ void demFusedMlLoopK(
577 PGSManifoldSweep fine, Kokkos::View<const int*, CpMem> vPerm,
578 Kokkos::View<const int*, CpMem> vOffs, int vCols, MlCoarseSweep f,
579 Kokkos::View<const int*, CpMem> parent, Kokkos::View<const float*, CpMem> invMass,
580 Kokkos::View<float* [3], CpMem> velPred, Kokkos::View<float* [3], CpMem> velG0,
581 Kokkos::View<const float*, CpMem> massG, Kokkos::View<int*, CpMem> grpW,
582 Kokkos::View<const int*, CpMem> bkPerm, Kokkos::View<const int*, CpMem> offsDev,
583 MlFusedMeta meta, int maxIters, float tol, float* resQS, unsigned* bar) {
584 const int stride = gridDim.x * blockDim.x;
585 const int tid = blockIdx.x * blockDim.x + threadIdx.x;
586 unsigned k = 0;
587 for (int it = 0; it < maxIters; ++it) {
588 if (tid == 0)
589 *resQS = 0.0f;
590 demGridBarrier(bar, k++);
591 for (int c = 0; c < vCols; ++c) { // fine colored smoothing sweep
592 const int b = vOffs(c), e = vOffs(c + 1);
593 if (b == e)
594 continue;
595 for (int i = b + tid; i < e; i += stride)
596 fine.solveOne(vPerm(i));
597 demGridBarrier(bar, k++);
598 }
599 demMlCoarseCycleDevice(f, parent, invMass, velPred, velG0, massG, grpW, bkPerm, offsDev, meta,
600 bar, k, tid, stride);
601 const float r = *reinterpret_cast<volatile float*>(resQS);
602 if (r <= tol)
603 break;
604 demGridBarrier(bar, k++); // every block has read resQS before the next zero
605 }
606}
607#endif // KOKKOS_ENABLE_CUDA
608
609#ifdef KOKKOS_ENABLE_CUDA
613inline bool demLaunchFusedMlLoop(
614 CpExec& space, const PGSManifoldSweep& fine, Kokkos::View<const int*, CpMem> vPerm,
615 const FusedSweepCtx& velCtx, int vCols, Kokkos::View<const ManifoldC*, CpMem> manifolds,
616 Kokkos::View<const int*, CpMem> realIdx, Kokkos::View<const float*, CpMem> invMass,
617 Kokkos::View<float* [3], CpMem> velPred, Kokkos::View<float*, CpMem> lambdaAcc,
618 Kokkos::View<float, CpMem> maxApproachQS, Kokkos::View<const float*, CpMem> restRel,
619 MlScratch& S, Kokkos::View<const int*, CpMem> bkPerm, const MlFusedCtx& ml, int maxIters,
620 float tol) {
621 const int maxGrid =
622 std::min(demFusedMaxGrid(demFusedMlLoopK), (static_cast<int>(ml.bar.extent(0)) - 1) / 8);
623 if (maxGrid <= 0 || vCols <= 0 || velCtx.maxBucket <= 0 || ml.maxWork <= 0)
624 return false;
625 const MlCoarseSweep f{manifolds,
626 realIdx,
627 Kokkos::View<const int*, CpMem>(S.grp),
628 S.velG,
629 Kokkos::View<const float*, CpMem>(S.invMassG),
630 lambdaAcc,
631 maxApproachQS,
632 restRel};
633 const int work = std::max(velCtx.maxBucket, ml.maxWork);
634 const int want = std::min((work + kFusedBlock - 1) / kFusedBlock, std::max(1, demFusedGridCap()));
635 const int grid = want < maxGrid ? want : maxGrid;
636 cudaStream_t str = space.cuda_stream();
637 cudaMemsetAsync(ml.bar.data(), 0, (static_cast<std::size_t>(grid) * 8 + 1) * sizeof(unsigned),
638 str);
639 demFusedMlLoopK<<<grid, kFusedBlock, 0, str>>>(
640 fine, vPerm, velCtx.offsDev, vCols, f, Kokkos::View<const int*, CpMem>(S.parent), invMass,
641 velPred, S.velG0, Kokkos::View<const float*, CpMem>(S.massG), S.grp, bkPerm, ml.offsDev,
642 ml.meta, maxIters, tol, maxApproachQS.data(), ml.bar.data());
643 return true;
644}
645#endif // KOKKOS_ENABLE_CUDA
646
652 const std::vector<std::vector<int>>& bkOffs, int numManifolds,
653 int numReal, int coarseSweeps,
654 Kokkos::View<int*, CpMem> offsDev,
655 Kokkos::View<unsigned*, CpMem> bar) {
656 MlFusedCtx ctx;
657#ifdef KOKKOS_ENABLE_CUDA
658 if (H.numLevels <= 0 || H.numLevels > kMlMaxLevels || numReal <= 0)
659 return ctx;
660 std::vector<int> flat;
661 ctx.meta.numLevels = H.numLevels;
662 ctx.meta.numReal = numReal;
663 ctx.meta.coarseSweeps = coarseSweeps;
664 int maxWork = numReal; // compose/restrict/prolongate phases (ng <= numReal always)
665 for (int lvl = 1; lvl <= H.numLevels; ++lvl) {
666 const auto& lo = bkOffs[static_cast<std::size_t>(lvl) - 1];
667 if (lo.size() != static_cast<std::size_t>(H.numColors[lvl - 1]) + 1)
668 return ctx; // bucket build skipped this level (empty) — keep the launch path
669 ctx.meta.groupOff[lvl - 1] = H.groupOff[lvl - 1];
670 ctx.meta.numGroups[lvl - 1] = H.numGroups[lvl - 1];
671 ctx.meta.parentOff[lvl - 1] = H.parentOff[lvl - 1];
672 ctx.meta.numColors[lvl - 1] = H.numColors[lvl - 1];
673 ctx.meta.permBase[lvl - 1] = (lvl - 1) * numManifolds;
674 ctx.meta.offsBase[lvl - 1] = static_cast<int>(flat.size());
675 for (std::size_t c = 0; c + 1 < lo.size(); ++c)
676 maxWork = std::max(maxWork, lo[c + 1] - lo[c]);
677 flat.insert(flat.end(), lo.begin(), lo.end());
678 }
679 if (flat.empty() || flat.size() > offsDev.extent(0))
680 return ctx;
681 const Kokkos::View<const int*, Kokkos::HostSpace, Kokkos::MemoryUnmanaged> h(flat.data(),
682 flat.size());
683 auto d = Kokkos::subview(offsDev, Kokkos::pair<std::size_t, std::size_t>(0, flat.size()));
684 Kokkos::deep_copy(space, d, h);
685 ctx.offsDev = Kokkos::View<const int*, CpMem>(offsDev);
686 ctx.bar = bar;
687 ctx.maxWork = maxWork;
688#else
689 (void)space;
690 (void)H;
691 (void)bkOffs;
692 (void)numManifolds;
693 (void)numReal;
694 (void)coarseSweeps;
695 (void)offsDev;
696 (void)bar;
697#endif
698 return ctx;
699}
700
702 Kokkos::View<const ManifoldC*, CpMem> manifolds, int numManifolds,
703 Kokkos::View<const int*, CpMem> realIdx, Kokkos::View<const float*, CpMem> invMass,
704 Kokkos::View<float* [3], CpMem> velPred, Kokkos::View<float*, CpMem> lambdaAcc,
705 Kokkos::View<float, CpMem> maxApproach, int numReal, const ContactHierarchy& H, MlScratch& S,
706 int coarseSweeps, Kokkos::View<const float*, CpMem> restRel = {},
707 const std::vector<std::vector<int>>* bkOffs = nullptr,
708 Kokkos::View<const int*, CpMem> bkPerm = {}, const MlFusedCtx* fused = nullptr) {
709 CpExec space;
710#ifdef KOKKOS_ENABLE_CUDA
711 if (fused && fused->maxWork > 0) {
712 const MlCoarseSweep f{manifolds,
713 realIdx,
714 Kokkos::View<const int*, CpMem>(S.grp),
715 S.velG,
716 Kokkos::View<const float*, CpMem>(S.invMassG),
717 lambdaAcc,
718 maxApproach,
719 restRel};
720 const int maxGrid = std::min(demFusedMaxGrid(demFusedCoarseCycleK),
721 (static_cast<int>(fused->bar.extent(0)) - 1) / 8);
722 if (maxGrid > 0) {
723 const int want = std::min((fused->maxWork + kFusedBlock - 1) / kFusedBlock,
724 std::max(1, demFusedGridCap()));
725 const int grid = want < maxGrid ? want : maxGrid;
726 cudaStream_t str = space.cuda_stream();
727 cudaMemsetAsync(fused->bar.data(), 0,
728 (static_cast<std::size_t>(grid) * 8 + 1) * sizeof(unsigned), str);
729 demFusedCoarseCycleK<<<grid, kFusedBlock, 0, str>>>(
730 f, Kokkos::View<const int*, CpMem>(S.parent), invMass, velPred, S.velG0,
731 Kokkos::View<const float*, CpMem>(S.massG), S.grp, bkPerm, fused->offsDev, fused->meta,
732 fused->bar.data());
733 return;
734 }
735 }
736#else
737 (void)fused;
738#endif
739 { // reset the composed map to identity; each level applies its parent map on top
740 auto grp = S.grp;
741 Kokkos::parallel_for(
742 "peclet::dem::ml_cycle_grp_init", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
743 KOKKOS_LAMBDA(int i) { grp(i) = i; });
744 }
745 for (int lvl = 1; lvl <= H.numLevels; ++lvl) {
746 const int off = H.groupOff[lvl - 1];
747 const int ng = H.numGroups[lvl - 1];
748 const int pOff = H.parentOff[lvl - 1];
749 const int nCol = H.numColors[lvl - 1];
750 const int slotShift = 6 * (lvl - 1);
751 auto grp = S.grp;
752 { // compose to this level
753 auto parent = S.parent;
754 Kokkos::parallel_for(
755 "peclet::dem::ml_cycle_compose", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
756 KOKKOS_LAMBDA(int i) { grp(i) = parent(pOff + grp(i)); });
757 }
758 { // restrict: V_g = sum(m v) / sum(m) (momentum-conserving), snapshot V0
759 auto velG = S.velG;
760 auto velG0 = S.velG0;
761 auto massG = S.massG;
762 Kokkos::parallel_for(
763 "peclet::dem::ml_restrict_reset", Kokkos::RangePolicy<CpExec>(space, 0, ng),
764 KOKKOS_LAMBDA(int g) { velG(off + g, 0) = velG(off + g, 1) = velG(off + g, 2) = 0.0f; });
765 Kokkos::parallel_for(
766 "peclet::dem::ml_restrict_accum", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
767 KOKKOS_LAMBDA(int i) {
768 const float m = mldetail::effMass(invMass(i));
769 const int g = grp(i);
770 Kokkos::atomic_add(&velG(off + g, 0), m * velPred(i, 0));
771 Kokkos::atomic_add(&velG(off + g, 1), m * velPred(i, 1));
772 Kokkos::atomic_add(&velG(off + g, 2), m * velPred(i, 2));
773 });
774 Kokkos::parallel_for(
775 "peclet::dem::ml_restrict_norm", Kokkos::RangePolicy<CpExec>(space, 0, ng),
776 KOKKOS_LAMBDA(int g) {
777 const float invM = 1.0f / massG(off + g);
778 for (int c = 0; c < 3; ++c) {
779 velG(off + g, c) *= invM;
780 velG0(off + g, c) = velG(off + g, c);
781 }
782 });
783 }
784 // coarse colored PGS sweeps: translation-only, e = 0, shared lambda accumulator (the
785 // per-manifold body is MlCoarseSweep::solveOne — shared verbatim with the fused kernel)
786 const MlCoarseSweep fSweep{manifolds,
787 realIdx,
788 Kokkos::View<const int*, CpMem>(S.grp),
789 S.velG,
790 Kokkos::View<const float*, CpMem>(S.invMassG),
791 lambdaAcc,
792 maxApproach,
793 restRel};
794 for (int s = 0; s < coarseSweeps; ++s) {
795 for (int color = 0; color < nCol; ++color) {
796 auto cp = S.colorPacked;
797 const bool dense = bkOffs != nullptr;
798 int rb = 0, re = numManifolds;
799 if (dense) {
800 const auto& lo = (*bkOffs)[static_cast<std::size_t>(lvl) - 1];
801 rb = (lvl - 1) * numManifolds + lo[static_cast<std::size_t>(color)];
802 re = (lvl - 1) * numManifolds + lo[static_cast<std::size_t>(color) + 1];
803 if (rb == re)
804 continue;
805 }
806 Kokkos::parallel_for(
807 "peclet::dem::ml_coarse_pgs", Kokkos::RangePolicy<CpExec>(space, rb, re),
808 KOKKOS_LAMBDA(int i2) {
809 const int idx = dense ? bkPerm(i2) : i2;
810 if (!dense && static_cast<int>((cp(idx) >> slotShift) & 63) != color)
811 return;
812 fSweep.solveOne(idx, off);
813 });
814 }
815 }
816 { // prolongate: every member takes its aggregate's velocity delta (mass-proportional impulse)
817 auto velG = S.velG;
818 auto velG0 = S.velG0;
819 Kokkos::parallel_for(
820 "peclet::dem::ml_prolongate", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
821 KOKKOS_LAMBDA(int i) {
822 const int g = grp(i);
823 velPred(i, 0) += velG(off + g, 0) - velG0(off + g, 0);
824 velPred(i, 1) += velG(off + g, 1) - velG0(off + g, 1);
825 velPred(i, 2) += velG(off + g, 2) - velG0(off + g, 2);
826 });
827 }
828 }
829}
830
831} // namespace peclet::dem
832
833#endif // DEM_SOLVER_MULTILEVEL_HPP
dem — portable (Kokkos) contact->manifold reduction, replacing the thrust-based reduce_contacts_to_ma...
static nb::ndarray< nb::numpy, float > flat(std::vector< float > &&v)
bool eligible(const ManifoldC &m, int idx, Kokkos::View< const int *, CpMem > mColor, Kokkos::View< const float *, CpMem > vn0, Kokkos::View< const float *[3], CpMem > vt0, Kokkos::View< const unsigned char *, CpMem > persistent, Kokkos::View< const float *[3], CpMem > posPred, F3 gHat, float qsThr, int gateMask)
Coarse-eligibility of a manifold: active + base-coloured (non-dup) + quasi-static approach (|vn0| <= ...
float effMass(float invMass)
Effective mass with fixed bodies (invMass == 0) mapped to a huge-but-finite mass so the momentum-weig...
constexpr int kGatePersistent
Optional eligibility gates beyond the quasi-static approach test (a SELECTION, not a sink – impulses ...
void multilevelCoarseCycleKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const float *, CpMem > invMass, Kokkos::View< float *[3], CpMem > velPred, Kokkos::View< float *, CpMem > lambdaAcc, Kokkos::View< float, CpMem > maxApproach, int numReal, const ContactHierarchy &H, MlScratch &S, int coarseSweeps, Kokkos::View< const float *, CpMem > restRel={}, const std::vector< std::vector< int > > *bkOffs=nullptr, Kokkos::View< const int *, CpMem > bkPerm={}, const MlFusedCtx *fused=nullptr)
constexpr int kMlSlotSkip
void buildColorBucketsKokkos(Kokkos::View< const int *, CpMem > colorOf, int n, int numColors, Kokkos::View< int *, CpMem > perm, Kokkos::View< int *, CpMem > cursor, std::vector< int > &offs)
Dense colour buckets (numColors <= 64): perm[offs[c] .
F3 ldF3(const V &v, int i)
ContactHierarchy buildContactHierarchyKokkos(Kokkos::View< const ManifoldC *, CpMem > manifolds, int numManifolds, Kokkos::View< const int *, CpMem > realIdx, Kokkos::View< const int *, CpMem > mColor, Kokkos::View< const float *, CpMem > vn0, Kokkos::View< const float *[3], CpMem > vt0, Kokkos::View< const unsigned char *, CpMem > persistent, Kokkos::View< const float *[3], CpMem > posPred, F3 gHat, Kokkos::View< const float *, CpMem > invMass, float qsThr, int gateMask, int numReal, MlScratch &S, Kokkos::View< long long *, CpMem > winner, Kokkos::View< std::uint64_t *, CpMem > colorMask, bool excludeImmovable=false, Kokkos::View< const unsigned char *, CpMem > asleep={})
Build the aggregation hierarchy + per-level crossing-manifold colorings.
float dot3(F3 a, F3 b)
F3 sub3(F3 a, F3 b)
CpExec::memory_space CpMem
void buildCoarseBucketsKokkos(const ContactHierarchy &H, MlScratch &S, int numManifolds, Kokkos::View< int *, CpMem > colorScratch, Kokkos::View< int *, CpMem > perm, Kokkos::View< int *, CpMem > cursor, std::vector< std::vector< int > > &offs)
One multilevel stabilization cycle over an already-built hierarchy: fine colored smoothing is the cal...
F3 scale3(F3 a, float s)
long long colorKey(int idx)
splitmix32 finalizer: a well-mixed pseudo-random priority per edge index.
MlFusedCtx demMakeMlFusedCtx(CpExec &space, const ContactHierarchy &H, const std::vector< std::vector< int > > &bkOffs, int numManifolds, int numReal, int coarseSweeps, Kokkos::View< int *, CpMem > offsDev, Kokkos::View< unsigned *, CpMem > bar)
Build the fused-coarse-cycle context from an already-built hierarchy + its dense buckets: flatten the...
constexpr int kMlMaxLevels
6-bit colour slots per level in the packed word; slot value 63 = not crossing / not eligible at that ...
Kokkos::DefaultExecutionSpace CpExec
dem — fused colour sweeps: one persistent kernel per sweep instead of one kernel launch per colour.
dem — portable (Kokkos) manifold velocity solve (normal restitution impulse).
Host-side description of one built hierarchy (offsets into the packed group pools).
Portable mirror of ManifoldConstraint.
The per-manifold coarse-PGS body (translation-only, e = 0, shared fine accumulator) lives in MlCoarse...
Kokkos::View< float *[3], CpMem > velG
Kokkos::View< float *, CpMem > lambdaAcc
Kokkos::View< const ManifoldC *, CpMem > manifolds
Kokkos::View< const int *, CpMem > grp
Kokkos::View< const int *, CpMem > realIdx
Kokkos::View< const float *, CpMem > restRel
Kokkos::View< const float *, CpMem > invMassG
Kokkos::View< float, CpMem > maxApproach
void solveOne(int idx, int off) const
Fused-coarse-cycle context: flat per-level colour offsets on device + barrier + meta.
Kokkos::View< unsigned *, CpMem > bar
Kokkos::View< const int *, CpMem > offsDev
Host-POD description of a built hierarchy for the fused coarse-cycle kernel (fixed-size arrays: kerne...
Device scratch for the multilevel pass, sized once (see Particles::allocate).
Kokkos::View< int *, CpMem > mate
Kokkos::View< float *, CpMem > invMassG
Kokkos::View< float *, CpMem > massG
Kokkos::View< float *[3], CpMem > velG0
Kokkos::View< int *, CpMem > grp
Kokkos::View< long long *, CpMem > colorPacked
Kokkos::View< int *, CpMem > parent
Kokkos::View< float *[3], CpMem > velG