Coarse-level telescoping for the pressure multigrid — design + implementation plan¶
Status 2026-09-24: the machinery this plan built in flow now lives in
core(peclet::core::decompstage policy, communicators andRedistributeTopology); flow's telescope delegates to it byte-identically (flow4da7171,84aaa62) and gained a Repartition stage for a weighted level-0 decomposition (d907c57); amr uses the same machinery for its pressure stages. Design:amr/docs/amr_mg_core_boundary.md§7–§11. This note is history.
Written 2026-09-01, answering open problem 1 of
DECOMPOSITION_AND_MULTIGRID.md and issue 2 of
SCALING_ISSUES.md: the multigrid hierarchy stops coarsening when a per-rank
block hits an odd extent, which costs a third of the strong-scaling efficiency at 1536 ranks and,
on a dense bed, costs convergence outright.
Revised the same day after a second read against the code (three corrections, all in §4 — the first draft had the transfer ordering backwards, which would not have worked on the blocked axis; it under-specified the trigger in a way that could merge along the wrong axis; and it missed that a far cheaper first rung falls straight out of machinery that already exists. The staging in §6 now leads with that rung.)
Status 2026-09-02 — IMPLEMENTED (rung 2 directly), tested, measuring on Snellius.
core a156528(BlockDecomposer::agglomerated),flow db7b1ba(CutcellMGtelescoping,set_pressure_telescope,predict_hierarchy,check_decomposition.py --predict,tests/kokkos_mpi/test_telescope_mpi). Off by default; byte-identical with it off. See §9.
Conclusion up front. Do geometric telescoping on the ORB tree: when in-place coarsening is
blocked, merge sibling ORB blocks onto half (or a quarter, or an eighth) as many ranks, move the
level's data with one gather per group, and carry on halving. This is PETSc PCTELESCOPE's
structure, and peclet is unusually well set up for it — three of the four primitives it needs
already exist. The two alternatives the current doc floats (hand the stopping level to GraphAMG;
pad the grid) do not address this constraint, for reasons given in §3.
1. What the constraint actually is¶
CutcellMG::initMpi builds level L+1 as curDec.coarsened(ratio) — the same ORB tree with
origins, sizes and split values divided by ratio. That is what makes the transfer operators purely
local: coarse-local i ↔ fine-local ratio*i on the same rank, so restrictAvg and prolongAdd
are single-rank kernels with no communication. The price is the gate
can(gs[ax]) && evenBlocks(ax) // every rank's block ORIGIN and SIZE even on that axis
and a break when no axis passes — note the gate is per axis, so an axis freezes individually
while others may keep halving. The stopping criterion is a property of the smallest block, not of
the grid. At 384³ on 1536 ranks the level-0 block is 24×48×32 — even on every axis, nicely
factored, imbalance 1.000 — and §2.8 records it stopping at a block of 3×6×4. The robust consequence,
independent of exactly how far y and z limp on afterwards, is that x freezes at a global
extent of 48 the moment its block is 3 wide. Per §2.7 the figure that decides whether a V-cycle is
domain-independent is the coarsest grid's largest extent, and 48 is far too large: a 64×2×2
bottom is only 256 cells and still costs 6.0 iterations against 4.0. (The exact coarsest grid at each
rank count is what P2's tool should report; it is not worth deriving by hand, and this plan
deliberately does not.)
Two consequences worth separating, because they have different fixes:
- The hierarchy is too short. Iterations rise 16.6 → 38.7 from 24 to 1536 ranks while time per iteration improves super-linearly (156 % efficiency). The entire wall-clock loss is the iteration count. This is what telescoping fixes.
- The bottom is then solved badly, and the two FoxBerry cases fail differently. The single-phase
case has an inlet/outlet, so the operator is non-singular,
removeMean_is false, andagglomerateBottom()returns false — it runs a smoother on a grid 48 cells across. The packed-periodic case is singular, soautoengages and gathers the whole coarsest operator — tens of thousands of cells at this rank count — to every one of 1536 ranks, every V-cycle, solved by serial host code. Same depth cap, two different failure modes downstream of it.
That second point generates two cheap probes that must run before any code is written (§6, P0), because either could re-order this plan.
2. What the libraries do¶
| library | mechanism | keyed on |
|---|---|---|
| PETSc | PCTELESCOPE — redistribute the problem onto a sub-communicator, recurse (May, Sanan, Rupp, Knepley & Smith, Extreme-Scale Multigrid Components within PETSc, PASC '16) |
explicit reduction factor, or -telescope_ranks |
| Trilinos MueLu | RepartitionFactory + RebalanceTransferFactory (Zoltan2) |
minimum rows per rank |
| hypre BoomerAMG | redundant/replicated coarse solve; agglomeration on coarse levels (Baker, Falgout, Kolev & Yang, Scaling hypre's Multigrid Solvers to 100,000 Cores) | coarse-grid size threshold |
| DUNE-ISTL AMG | accumulation onto fewer processes | coarse-grid size |
| textbook | processor agglomeration, idle processors on coarse grids | Trottenberg, Oosterlee & Schüller, Multigrid, ch. 6 |
They differ in how the coarse partition is chosen. MueLu and hypre must re-partition an
algebraic graph, so they need a partitioner (Zoltan2) and a general, irregular redistribution.
PETSc's telescope is closer to what we want but is generic over DMs.
peclet's ORB makes this strictly easier than any of them, and that is the design insight:
BlockDecomposerstores the recursive-bisection tree explicitly (tree_, heap layout, children at2i+1/2i+2, leaves carrying the block index), plusflattenTree().- Every internal node of that tree is a box, and its box is exactly the union of its subtree's
leaf boxes — reconstructible by a downward walk from
(0, globalSize)splitting at eachsplitDim/splitValue. - Therefore merging sibling blocks = truncating the tree, and the merged partition tiles the domain exactly, nests with the fine one by construction, and needs no partitioner and no graph.
So where MueLu needs Zoltan2 and an irregular redistribution, peclet needs a tree truncation and a gather within disjoint groups. The agglomeration hierarchy is already in the data structure; nobody has walked up it yet.
3. Why not the alternatives¶
Hand the stopping level to GraphAMG (open problem 1's "cheapest to measure first", and open
problem 7). Retire this suggestion. GraphAMG's agglomerated path gathers the assembled operator
to every rank (MPI_Allgatherv) and solves it with serial host code — a device→host→device round
trip per V-cycle. Handing it the stopping level means doing that to an operator of tens of thousands
of cells at np=1536 instead of the few hundred a telescoped hierarchy would leave, every V-cycle, on
1536 ranks. It replaces a depth problem with a communication-and-serial-work problem. It would be the right answer if GraphAMG were distributed,
but it is not (open problem 7 says so), and making it distributed — parallel aggregation, distributed
Galerkin triple products — is a far larger project than §4. The algebraic route is more work and
less suited; the geometric route is a tree walk.
Padding to friendly sizes (open problem 8). Orthogonal, and it does not touch this. The constraint is the per-rank block; at np=1536 the global grid 384³ = 2⁷·3 is already excellent and the block 24×48×32 is already even everywhere. Padding cannot help a hierarchy that dies because 1536 blocks have become 3 cells wide. Padding fixes §2.2-style badly-factored grids, a different disease.
Odd-grid coarsening (open problem 12). Would relax the parity gate, but only defers the wall — a block of 1 cell cannot coarsen however clever the transfer operator — and it changes the numerics of every level. Telescoping removes the pressure to do it at all, which the doc already notes.
4. Design¶
4.0 Two rungs on one primitive¶
Both rungs need the same thing: move a distributed level, at its own resolution, onto fewer ranks, and continue the geometric hierarchy there. They differ only in how many fewer.
| rung 1 — redundant gather | rung 2 — sub-communicator telescoping | |
|---|---|---|
| where the level goes | to every rank (Allgatherv), each continues the hierarchy alone | to a subset (ORB tree truncation), continues on a sub-communicator |
| new decomposition code | none | BlockDecomposer::agglomerated() + group comms |
| below the telescope point | the existing single-rank CutcellMG path, unchanged |
the existing distributed path on the sub-comm |
| scatter back | none — each rank extracts its own block | one scatter per group |
| per-V-cycle traffic | one Allgatherv of the level's residual, N_L doubles to every rank | one gather + one scatter within groups |
| scales to | N_L that one rank can hold and solve in ~ms: this campaign's whole ladder | arbitrary N_L (large weak-scaling runs) |
| effort | days | weeks |
Rung 1 is exactly the design pattern the existing agglomerated bottom already uses — it gathers
the bottom rhs redundantly every V-cycle and the operator once, "so the assembled coarse
problem is bit-identical on all ranks and each solves it locally with no rank-0 bottleneck and no
result broadcast". Rung 1 applies that pattern one or more levels earlier and, instead of handing
the gathered level to GraphAMG, hands it to the single-rank geometric hierarchy — which already
exists, already goes to full depth, already carries every BC path and the exact bottom, and is
already tested. Rung 2 is rung 1's general form, for when N_L is too large to replicate.
The rest of §4 describes the shared mechanism; where the rungs differ it says so.
4.1 Where the redistribution happens — at the fine level's resolution¶
This is the correction to the first draft. Restriction cannot be done first and gathered second, because restriction on the parent partition is precisely what is blocked: a block 3 cells wide has no in-place half. The order is therefore
[all ranks at L] move lv.res (resolution L) onto the merged partition -> T.res
[merged ranks] restrictAvg(cs.rhs, T.res, ...) // legal: merged blocks are even
[merged ranks] zero cs.x; vcycle(L+1) // the hierarchy continues from cs
[merged ranks] fill/BC ghosts on cs.x; zero T.x; prolongAdd(T.x, cs.x, ...)
[all ranks at L] move T.x back and ADD into lv.x // prolongAdd is additive
smooth(lv, post)
T is the telescope stage: level L's grid on the merged partition. What moves per V-cycle is two
resolution-L fields, the residual down and the correction up. restrictAvg and prolongAdd are
untouched.
T needs no halo topology. restrictAvg reads fine inner cells only (fx = ratio*icx+dx+gf,
always inside the inner box), and prolongAdd writes fine inner cells only — the coarse ghosts it
reads are cs's own, filled by cs's existing halo and BC ghost policies. So T is two buffers
with the fine extent, nothing more. (The first draft's "staging level on dec_L.coarsened(ratio)"
is gone; that partition does not exist, which was the whole problem.)
4.2 The level model (rung 2)¶
A Level already carries per-level og, gdim, ghost width g (1 or 2 by CA eligibility) and its
own GridHaloTopology built as buildTopology(dec, rank, v.g, per, comm) — parameterized by
decomposition, rank and communicator. A telescoped level passes different values for all three and
needs no new halo machinery. Level gains dec, comm, active, and the transition into it
gains a T stage. Under rung 1 the "merged partition" is one block on every rank and cs onward is
a nested single-rank CutcellMG driven through its existing entry points
(init(nx,ny,nz,levels), setBoundaryConditions(bc), setOpenness(ox,oy,oz,…)).
4.3 The trigger — and what actually binds¶
The gate is can(gs[ax]) && evenBlocks(ax), and evenBlocks demands even origins as well as
even sizes. Origins are what bind in practice: after three halvings of a level-0 partition whose
splits sit at multiples of 24, the split values are multiples of 3 and every other block starts at
an odd cell. Merging two ORB siblings restores parity because the merged block's origin is the
parent's split value, one tree level up — a multiple of 6 in that example. This is why sibling
merging is the right primitive and not merely a convenient one.
The rule, a pure function of (tree, gs) replicated on every rank with no communication:
telescope(dec, gs):
if every axis with can(gs[ax]) already passes evenBlocks -> no telescope
rung 1: merge to ONE block (tree depth 0) -- trivially passes for every axis
rung 2: for d = depth-1 down to 1:
cand = dec.agglomerated(d)
if EVERY axis with can(gs[ax]) passes evenBlocks(cand, ax): return cand
return dec.agglomerated(0)
"Every coarsenable axis", not "any axis" — the first draft had "any", and that can merge along
z, unblock z, and leave x frozen at 48 cells, which is the extent that matters (§2.7). The
"every" rule may merge more than strictly necessary — the ORB alternates split axes, so reaching
an x-split can cost two y/z merges on the way — but it restores full-depth coarsening on all
axes, and P2's tool makes the cost visible. Prefer the largest d (fewest merges).
A second, economic trigger — telescope before the block is so small that ghosts dominate, the
analogue of MueLu's minimum-rows-per-rank — belongs behind PECLET_FLOW_TELESCOPE_MIN_EXTENT and
should not ship until measured. Under rung 1 it also has a memory meaning: every rank then holds the
whole level, so telescoping early multiplies memory by the rank count.
4.4 Communicators and idle ranks (rung 2)¶
MPI_Comm_split(parent, color = isGroupRoot ? 0 : MPI_UNDEFINED, key = groupIndex) gives the
coarse level a communicator in which sub-rank == block index. Ranks that receive
MPI_COMM_NULL are inactive on that level and below: they take part in the gather and the scatter
(which run on the parent comm, where everyone is present) and skip the recursion. The collectives
currently hard-wired to comm_ — allreduce, allreduceSum2 (used by removeMean), and the
bottom's gatherv — must become per-level. Under rung 1 nothing is inactive and no sub-comm exists.
4.5 The data movement¶
Rung 1: one Allgatherv of the resolution-L inner cells keyed by global id — the same call and the
same keying graphAmgSolveBottom already uses for the bottom rhs. Each rank then holds the full
level and extracts its own block from the correction; no scatter.
Rung 2: because sibling-merge is nested, each merged block is exactly the union of a contiguous
run of fine blocks (the ORB assigns leaf indices in order — initImpl pushes the right child first
so the left is popped first), so the movement is a gather within disjoint groups: one MPI_Gatherv
per group communicator, deterministic, no handshake. core's
decomp::grid_redistribute.hpp::redistributeGridFields (general A→B, NBX, bit-exact) is more than
is needed; keep it as the debug oracle — PECLET_FLOW_TELESCOPE_CHECK=1 performs the movement
both ways and compares. Host-staged on GPU at first (buffers are small), device-resident later, as
GridHalo did it.
4.6 Setup¶
The coarse operator is built once. Move ox, oy, oz at resolution L through the same gather, then
run the existing coarsenOpenAvg on the merged partition — a per-coarse-cell average of the same
fine cells, so bitwise identical to what the in-place path would have computed had it been legal.
applyBoundaryOpenness and touchesGlobalFace(lv, f) key off og/gdim, which a merged level
carries correctly, so the domain-BC path needs nothing special. Under rung 1, setOpenness on the
nested instance does all of this.
4.7 What stays bitwise, what does not¶
Restriction and prolongation are per-cell operations over the same cells with the same arithmetic
regardless of which rank executes them; Red-Black Gauss–Seidel updates each colour as a parallel
per-cell operation, so it is partition-independent once ghosts are correct. Both are bitwise
identical under telescoping. removeMean sums in a different order on a different partition, so
results move at the reduction-order floor — the same class as coarse-first vs aligned (3.3e-12,
§2.4). Contract: telescoping off ⇒ byte-identical; on ⇒ equal within solver tolerance.
5. What changes where¶
| where | change | rung | size |
|---|---|---|---|
flow/src/mac_cutcell_mg.hpp |
bottom mode "geometric": at the blocked level, Allgatherv res (and ox/oy/oz once) by global id, drive a nested single-rank CutcellMG on the full level, extract own block, add |
1 | ~150 lines |
flow/scripts/check_decomposition.py |
print the telescoped ladder (rank count and block per level) — rung 1 and rung 2 forms | 1 | ~40 lines |
core/decomp/block_decomposer.hpp |
agglomerated(int depth) → truncated-tree BlockDecomposer + group map; boxes from a downward walk; asserts exact tiling. Reusable by dem/voro/AMR |
2 | ~80 lines |
core (new or in grid_redistribute.hpp) |
gatherGroups / scatterGroups on a group comm |
2 | ~120 lines |
flow/src/mac_cutcell_mg.hpp |
Level gains dec/comm/active; T stage; initMpi hook at the break; V-cycle bracketing; per-level comm in the three collectives |
2 | ~250 lines |
flow/tests/kokkos_mpi/ |
new ctests (§6) | 1, 2 | ~200 lines |
restrictAvg, prolongAdd, the smoother, GridHalo and GridHaloTopology are untouched by
either rung.
6. Staging¶
P0 — two probes before any code (hours). Either could re-order everything.
- Is the single-phase loss the depth, or the anchored bottom? That case runs a smoother on a
48-across coarsest grid because auto is gated to the singular path. Force it:
set_pressure_bottom("agglomerated") at np=384 and 1536, single-phase. Record max|div|
alongside the iteration count — §2.7 already measured that the exact bottom lowers the
attainable floor on the anchored path (8e-8 → 2e-5 on the inflow/outflow channel), so a run
that "converges in fewer iterations" to a worse divergence is not a win, and the probe must be
able to tell. If iterations fall and the floor holds, part of issue 2 is the bottom, and
lifting the anchored-path gate becomes the urgent fix.
- Is the packed np=1536 cliff the redundant gather? Rerun with
PECLET_FLOW_AGGLOM_EXTENT=1000000. If the regression vanishes, the cliff is the coarsest-level
Allgatherv + serial host solve per V-cycle, not the contrast threshold, and
SCALING_ISSUES.md's compounding paragraph needs revising.
P1 — the prediction tool. Extend check_decomposition.py to print, for a grid and rank count,
the level at which in-place coarsening stops, N_L there, and the ladder each rung would build
below it. This is the artifact that says whether rung 1 suffices at the rank counts in question
(is N_L a few hundred thousand cells or a few million?) before a line of solver code exists, and
it fixes issue 6's usefulness. Gate: at 384³/np=1536 the predicted rung-1 hierarchy reaches a
coarsest extent ≤ 4, against ~48 today.
P2 — rung 1, off by default. Bottom mode "geometric". Gates: (a) off ⇒ byte-identical
on the existing tests/kokkos_mpi suite; (b) np=1 unaffected by construction; (c) a ctest that
forces it on a grid where in-place coarsening is also legal, so the two hierarchies can be
compared — converged solutions agree to solver tolerance; (d) a ctest on a deliberately starved
partition — iteration count must fall to the single-rank value for the same problem.
P3 — the decisive measurement, on a dense bed. Re-run np=384 (reference: 81.7 iterations, reproducible to 5 %) and np=1536 packed with rung 1. Iterations should hold near the low-rank value; the np=1536 cliff should be gone. Also record rung 1's cost — the Allgatherv volume and the per-rank serial work — because that number decides whether rung 2 is needed at this scale or only for larger weak-scaling runs.
P4 — rung 2, if P3 says the redundant gather costs enough to matter. agglomerated(), the
group gather with its redistributeGridFields oracle, per-level comms, the T stage. Gates as
P2 plus PECLET_FLOW_TELESCOPE_CHECK=1 agreement and a deadlock-free run at ≥ 2 telescope points.
P5 — promote and follow through. Default on where it helps; re-run the FoxBerry ladder and
republish the provisional study page. Then what it unlocks: open problem 5 (a dense-Cholesky
bottom becomes trivially attractive once the bottom really is tiny), open problem 6 (the redundant
gather's cost is now measured, not assumed), open problem 4 (the anchored-bottom pathology is
cheaper to study on a small bottom), and issue 5 (VelocityMG can reuse the same primitive).
7. Risks¶
- Deadlock on the sub-communicator. The main hazard: an inactive rank entering a collective, or
an active one skipping it. Contained by making the gather/scatter run on the parent comm (all
ranks present) and by auditing every
comm_use inCutcellMG— there are only three (allreduce,allreduceSum2, the bottomAllgatherv). The intermittent multi-node hang already on the register (issue 4) is a reason to be careful here and to add a barrier-and-timeout debug mode rather than to assume. - Results move. Only when enabled, and only at the reduction-order floor. The contract is the one CA smoothing and coarse-first already established: off ⇒ byte-identical; on ⇒ equal within solver tolerance. State it in the gate, not after the fact.
- GPU staging. Host-staged first (small buffers), device-resident later, exactly as
GridHalodid it. Measure before optimizing. - Rung 1's replication cost. Every rank holds and solves the whole level: N_L doubles of memory and N_L-proportional serial work per V-cycle, and an Allgatherv whose aggregate volume grows linearly with the rank count. Fine where N_L is ~10⁵ (this ladder); not fine at ~10⁷ (a badly-factored grid, §2.2, or an early economic trigger). P1's tool prints N_L, so this is known per configuration before running, and it is the number that decides rung 2.
- Load imbalance in the merged partition. Merging siblings preserves the ORB's own balance, and
§2.4 shows the ORB balances well on coarse grids.
agglomerated()should report imbalance so P2 can show it rather than assume it. - Interaction with CA smoothing. A telescoped block is larger, so it is more likely to clear
minBlockExtent >= 4and become CA-eligible. A bonus, but it means the g=1/g=2 choice must be recomputed per level after telescoping, andparityOg's g-dependent red-black origin is a known trap (flow's CLAUDE.md flags it explicitly).
8. What it should buy¶
The claim to test, not to assume: the pressure iteration count should become a property of the problem, not of the rank count. Concretely, single-phase 384³ should hold near its np=24 value of 16.6 iterations across the ladder instead of rising to 38.7, recovering most of the 33 % efficiency gap; and the packed bed should stop falling off a cliff above 384 ranks, which is the outcome that actually matters, because that cliff is what limits the IBM path today.
Rung 1 should deliver most of that at this ladder's scale within days, because below the telescope point it is the single-rank solver, whose iteration counts are the target. Rung 2 is what keeps it true when the gathered level no longer fits one rank.
The honest caveat: P0 may show that a share of the single-phase gap is the anchored bottom rather than the depth. That would not invalidate this plan — telescoping is still what makes the bottom small enough for any bottom solver to be cheap — but it would change which fix is urgent, and it is much better to learn that from two reruns than from a week of implementation.
9. What was built (2026-09-02)¶
Rung 2 was implemented directly — the user asked for the ORB-tree telescoping in the code — and rung 1 was not built separately, since a merge to depth 0 is the same code path.
core — BlockDecomposer::agglomerated(depth, &groupOf, &rootOf) truncates the bisection tree;
every kept node's box is reconstructed by a downward walk from (0, globalSize); dropped nodes
become leaves so coarsened()/refined()/ownerOf work on the result unchanged. treeDepth().
Unit test: for every depth, exact tiling + ownerOf agreement, exact-union group map with
contiguous members and lowest-member root, identity at full depth, one block at depth 0; plus the
multigrid scenario (8 blocks of 12³ halve to 3³ and stick; agglomerated(0).coarsened() reaches 3³).
flow — CutcellMG::Level gains its own MPI_Comm; a Telescope record on level L holds the
group communicator, the roots' sub-communicator, the merged-block geometry and stage buffers at
resolution L. The V-cycle gathers lv.res at L's own resolution onto the group root, the root
restricts / recurses / prolongs into the stage, and the correction is scattered and added.
setOpenness gathers the openness once per telescope point. Every per-level collective
(removeMean, dot, maxabs, the bottom's gatherv) takes the level's communicator. A rank
idling below a telescope point ends its level list there and takes the gather/skip/scatter branch.
restrictAvg, prolongAdd, coarsenOpenAvg, the smoother and GridHalo are untouched.
Trigger. Telescope when a still-coarsenable axis has an odd block, or when the smallest block
extent is below PECLET_FLOW_TELESCOPE_MIN_EXTENT (default 4). Merge the fewest tree levels at
which every coarsenable axis is even and the merged blocks are ≥ 2× the min extent — so they
survive the halving that follows (keyed on the candidate's own extent, the ladder oscillated
6³ → 3³ at every level).
Prediction (P1). CutcellMG::predict() / flow.predict_hierarchy() /
check_decomposition.py --predict — a pure function, no mpirun, any rank count. 384³:
| ranks | in place | telescoped |
|---|---|---|
| 24 | 7 levels, coarsest 6×12×6 on 24 ranks | 8 levels, 3³ on 1 rank (24 → 1) |
| 384 | 6 levels, coarsest 24×12×24 on 384 | 8 levels, 3³ on 1 (384 → 8 → 1) |
| 1536 | 5 levels, coarsest 24×48×24 on 1536 | 8 levels, 3³ on 1 (1536 → 64 → 1) |
Gates (test_telescope_mpi, np=1/2/4). A forced telescope on a grid that also coarsens in
place matches the control to 1e-14. On a starved partition (a plain ORB on 24³, since the solver's
own factory nests at these rank counts) the in-place hierarchy stops at 6×3×3 / 6×6×3 while the
telescoped one reaches the single-rank 3³ with the single-rank iteration count, and its solution
matches the single-rank reference to 2.5e-14 — better than the untelescoped 1.3e-10, because it
reproduces the single-rank hierarchy exactly. cutcellmg_mpi and sdflow_mpi np=1/2/4 unchanged
with it off. End to end through the Python driver at np=4 (IbmSolver, wall-confined bed, FoxBerry
inlet/outlet): packed 20.0 → 16.8 iterations, single-phase 12.2 → 10.5, <u> and max|div|
identical to seven digits.
P0 results. Single-phase np=384 with the agglomerated bottom forced on the anchored path: 24.9 → 10.9 iterations, 2.48 → 1.88 s/step, floor 2.1e-9 → 3.4e-10 — so the anchored-path gate (open problem 4) is real money at ≤384 ranks, and its risk did not materialise here. Both np=1536 probes hung in the first warmup step (issue 4) and were cancelled after 17 minutes.
10. At scale (2026-09-02, Snellius genoa, 384³, fp64 operator build for the bed)¶
The FoxBerry ladder with telescoping on, wall-confined 5000-sphere bed with FoxBerry's inlet /
outlet BCs (BED=walls, BCMODE=foxberry), 100 steps (20 / 25 / 50 on the one-node rungs):
| ranks | ladder | packed iters (max) | packed s/step | eff. vs 24 | single iters | single s/step | eff. |
|---|---|---|---|---|---|---|---|
| 24 | 24 → 1 at 12³ | 43.1 (45) | 128.7 | 100 % | 14.7 | 34.8 | 100 % |
| 48 | 48 → 1 | 42.9 (45) | 64.9 | 99 % | 14.8 | 18.3 | 95 % |
| 96 | 96 → 1 | 41.7 (45) | 31.6 | 102 % | 14.4 | 8.56 | 102 % |
| 192 | 192 → 8 → 1 | 39.8 (45) | 15.6 | 103 % | 14.0 | 4.29 | 101 % |
| 384 | 384 → 8 → 1 | 39.8 (45) | 7.28 | 110 % | 14.0 | 2.01 | 108 % |
| 768 | 768 → 8 → 1 | 39.8 (45) | 3.24 | 124 % | 14.0 | 0.768 | 142 % |
| 1536 | 1536 → 64 → 1 | 39.8 (45) | 1.33 | 151 % | 14.0 | 0.656 | 83 % |
Iteration counts are rank-independent, as the design predicted (every ladder bottoms at the
single-rank 3³), and the 100-step rungs at 192 / 384 / 768 / 1536 agree on <u> and max|div|
to seven digits. At 1536 the single-phase step (37 k cells/rank, ~200 momentum sweeps each with a
halo exchange) is latency-bound — that is the momentum solve's problem, not the pressure solve's. The A/B against the in-place hierarchy: at 384 ranks packed 49.9 (max 69) /
10.8 s → 39.8 (45) / 7.28 s, single 24.9 / 2.48 s → 14.0 / 2.01 s; at 24 ranks identical
(129.5 vs 128.7 s), so the trigger correctly stays silent where the hierarchy is already deep.
Against FoxBerry: 3.0–4.1× on the bed (widening up the ladder, where before it collapsed at
1536), 8–14× single-phase.
The 1536 rung exposed a different bug, not in the telescoping: the NBX consensus round that
builds each level's halo topology raced with the next level's round on the same communicator
(SCALING_ISSUES.md §4, core 10294e6). The telescoped sub-hierarchy made it more likely
(three back-to-back topology builds on a 64-rank sub-communicator), which is how it was caught;
the un-telescoped 1536-rank hierarchy hung on the same race. With the fix, 768 and 1536 ran
first time, and the 1536 packed rung (1.33 s/step, 39.8 iterations) is the fastest point of
the whole campaign relative to FoxBerry (4.1×). The halo timeout diagnostic that
named the missing messages (PECLET_CORE_HALO_TIMEOUT) and the topology symmetry check that now
throws at build time are the permanent yield.
Open (in priority order). (1) Default the trigger on once a GPU (device-staged gather) run
and the anchored-path agglomerated bottom (open problem 4 of DECOMPOSITION_AND_MULTIGRID.md — at
384 ranks single-phase it is worth 14.0 → 10.9 iterations on top of telescoping's 24.9 → 14.0)
have been measured together. (2) MIN_EXTENT = 4 was chosen by the 6³ → 3³ oscillation, not
tuned; 8 would telescope one level earlier and is worth a sweep at 768 ranks where the per-level
halo latency dominates. (3) The idle members could take the next V-cycle's fine-level work
(pipelining) — not worth it until the momentum solve (SCALING_ISSUES.md §5) stops dominating
the step.
Variable-density outflow across a telescope point (2026-09-02, evening). The WO-R2 outflow
coefficient path stores each level's high-side outflow coefficient on a ghost plane, which the
inner-only telescope gather left behind — the other session guarded it with a throw. Now
teleGatherPlane gathers that plane from the members touching the global +face into the merged
stage, and applyBoundaryOpennessFrom coarsens the coarse boundary coefficient from the stage
exactly as the in-place path does from the finer level. test_telescope_varrho_mpi (density
jump advected through a +z outflow, telescope forced at level 1) reproduces the in-place hierarchy
to the last digit at np 1/2/4. Telescoping therefore has no remaining exception.