-
Notifications
You must be signed in to change notification settings - Fork 0
GPU Balance Notes
📄 Canonical source:
docs/design/gpu-balance-notes.md— edit there, not in the wiki.
Design notes for the CUDA balance() operation: what the literature says the
ideal algorithm is, why, and how our implementations map onto it. Citation
keys resolve in REFERENCES.md.
We store a linear (sorted) Morton octree: leaves are axis-aligned cells encoded as 64-bit Morton codes in a sorted array with a per-leaf level (p4est/t8code style, [BurstWG2011]). The expensive operation is 2:1 balance: enforce that face-neighbouring leaves differ by at most one level. Our application (DIC for fracture) produces thin, deeply refined crack features in coarse domains, i.e. deep ripple fronts over large meshes — the adversarial regime for any balancer.
There are three paradigms, in increasing sophistication:
-
Ripple ([SSB2008] §I; what
cuda/tree.cuh::balance_refdoes). Mark every leaf whose face-neighbour is >1 level coarser, split the coarse violator one level, repeat until no violations. Correct and simple, but "splitting one octant can force a non-adjacent octant to split" ([SSB2008], the ripple effect), so it is inherently iterative and each pass re-scans O(N). -
Active-front ripple (our production
cuda/tree.cuh::balance). Same output, but after pass 1 only re-check cells adjacent to the previous pass's refinements. Byte-identical tobalance_ref(testrun_test_active_parity), ~1.8× faster on thin features, ~parity on wide fronts (adaptive fallback). This is an engineering approximation of the next idea. -
Insulation-layer + preclusion ([SSB2008], [IBG2012]; the ideal target). The key theorem ([IBG2012] §II-B): two octants
o,rcan be unbalanced only ifolies inr's insulation layerI(r)— the 3^d envelope of like-sized octants aroundr. Nothing outsideI(r)can forcerto split. This bounds the ripple and lets balance be done by generating the cells each leaf demands, then completing the octree — work proportional to the active boundary, not O(N) per pass.
Crucial distinction the research flagged: "iterative in execution" ≠ "non-minimal in output". [SSB2008] produces the coarsest (minimal) balanced octree. Do not conflate "not single-pass" with "not minimal".
-
Insulation layer
I(r): "an envelope of 3^d like-sized octants" aroundr. Balance information only needs to flow within insulation layers. -
Coarse neighbourhood
N(o): the parent-level octants neighbouringparent(o). "each l-octant ... attempt[s] to add to the octree its family and the (l+1)-octants that neighbor its parent ... the coarse neighborhood N". Which neighbours (face / edge / corner) depends on the balance condition ([IBG2012] Fig. 5). Ourbalance_ref/balanceenforce face balance, soN(o)= the 2·d face-neighbours ofparent(o)at the parent's level. -
Preclusion
r ≺ o("o precludes r"): "if and only if parent(r) is ancestor (or equal) to parent(o)." Equivalence classes are families; precluded octants are redundant and removed, then recovered by completion. -
0-sibling(o): the first child ofparent(o)— the canonical family representative. -
Reduce(S)([IBG2012] Fig. 8): collapse each family in the sorted set to one representative. "R is the smallest subset of leaves that can be used to reconstruct a linear octree using a completion algorithm"; |R| ≤ |S|/2^d. -
Linearize: O(n) removal of coarse octants that overlap a finer one (keep only leaves).Complete: O(n) fill of gaps between leaves in the coarsest way.
Reduce(S) (Fig. 8) — sorted in, sorted out, ≤ |S|/2^d:
R[0] ← 0-sibling(S[0]); i ← 1
for 0 ≤ j < |S|:
s ← 0-sibling(S[j]) # canonical rep of S[j]'s family
r ← R[i-1] # last kept
if r ≺ s: R[i-1] ← s # s is finer & same lineage → replace
else if s ⊀ r: R[i] ← s; i++ # different family → append
Old subtree balance (Fig. 6) — correct, ripple-style, our reference:
S_new ← ∅
for all o ∈ S ∪ S_new: # worklist: includes newly added (the ripple)
for all s ∈ family(o) ∪ N(o):
if s ∉ S ∪ S_new: S_new ← S_new ∪ {s}
return Linearize(S ∪ S_new)
New preclusion balance (Fig. 7) — the GPU target:
R ← Reduce(S)
R_new ← ∅; R_prec ← ∅
for all o ∈ R:
for all s ∈ N(o):
s ← 0-sibling(s)
if s ∉ R ∪ R_new: R_new ← R_new ∪ {s}
if s ≺ o: R_prec ← R_prec ∪ {s} # tag precluded
else if ∃ t ∈ R with t ≺ s: R_prec ← R_prec ∪ {t} # tag precluded
R ← R\R_prec; R_new ← R_new\R_prec
return Complete(R ∪ R_new)
The win: the costly postprocessing sort runs on a set 2^d smaller; "requires roughly 3 times fewer hash queries ... and a reduction of the set that is sorted in postprocessing by the factor 2^d", and constructs no auxiliary/parent nodes.
Every step is a standard data-parallel primitive — this is why [IBG2012] is the right target for a GPU port:
| Step | Primitive |
|---|---|
Reduce(S) |
transform (→ 0-sibling) + unique/segmented compaction on sorted codes |
generate N(o) 0-siblings |
one kernel, ≤ 2·d outputs/leaf → copy_if
|
dedup vs R ∪ R_new
|
sort + unique, or a device hash set |
preclusion test ∃ t ∈ R: t ≺ s
|
single lower_bound (binary search) per candidate |
Linearize / Complete
|
segmented scan over sorted codes (drop ancestors / fill gaps) |
Per-pass work becomes O(active boundary), fixing the wide-front weakness of active-front ripple. The multi-GPU "one-pass" structure ([IBG2012] §II: local balance → query → response → rebalance) maps the Reduce/preclusion kernel onto each rank with a single insulation-layer exchange round (vs ripple's many).
-
Now:
balance(active-front) is production;balance_refis the parity oracle. Both face-balance, byte-identical, benchmarked (benchmarks/results/cuda_balance_active_front_*.csv). -
Next: implement
balance_preclusion(Fig. 7) in CUDA, cross-validated byte-identical againstbalance_refon every fixture before it can become the default. Expected to dominate on the wide crack fronts. -
Ghosts/multi-GPU: the recursive ghost construction of [IBWG2015] (works
without a 2:1 precondition) is the reference for
mpi/halo tuning.
-
No public single-GPU CUDA 2:1-balance reference exists. Every primary
source ([SSB2008] MPI/Dendro; [BurstWG2011] p4est MPI; csteuer's
ParallelBottomUpBalancedOctreeBuilderOpenMP) is CPU. The algorithm is fully specified; the GPU port is genuinely ours to make (and is itself a publishable contribution). - The GPU-balance status of Karras-2012 LBVH, AMReX-GPU, GAMER-2, Enzo-E/Cello, t8code and dendro-GR could not be verified — treat as unknown, not absent.
- All complexity bounds assume near-uniform distributions; our deep-crack load imbalance is exactly where those assumptions break, so SFC-partition quality (weighted vs equal-count) dominates multi-GPU behaviour and needs measurement.
-
Dendro-5.01 —
github.com/paralab/Dendro-5.01(Sundar's [SSB2008] impl). -
p4est —
github.com/cburstedde/p4est([BurstWG2011];p?est_balance). - csteuer/ParallelBottomUpBalancedOctreeBuilder — MIT C++/OpenMP, the most readable [SSB2008] port to translate from.