Skip to content

Load-balance, active-box windowing, block-structured AMR - #1628

Open
sbryngelson wants to merge 888 commits into
MFlowCode:masterfrom
sbryngelson:up/mega
Open

Load-balance, active-box windowing, block-structured AMR#1628
sbryngelson wants to merge 888 commits into
MFlowCode:masterfrom
sbryngelson:up/mega

Conversation

@sbryngelson

@sbryngelson sbryngelson commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

An opt-in, default-off family of performance features and the measurement infrastructure they rest on. With all flags at their defaults the only touched production path is s_mpi_decompose_computational_domain, refactored through the new m_box module (byte-identical; covered by the existing suite).

  • Load-balance infrastructurem_box (partition arithmetic), m_load_weight/load_weight_wrt (per-cell load-weight field + imbalance metric), m_sfc_partition/sfc_partition_wrt (Morton-SFC predicted-imbalance diagnostic), m_load_balance/load_balance (weighted static decomposition at init; AMR-fine-work-aware), m_rank_timing/rank_time_wrt (per-rank compute-time diagnostic).
  • Active-box windowingm_active_box/active_box: restricts reconstruction/Riemann/RK windows to a light-cone-grown box around non-ambient flow; strict-subset golden-tested.
  • Hybrid reconstruction/flux sensorshybrid_weno and hybrid_riemann (+hybrid_weno_eps, hybrid_smooth_flux): linear-optimal weights / central-or-Rusanov flux in smooth cells, full WENO/HLLC at flagged discontinuities (Jameson sensor, stencil-dilated, per-level under AMR).
  • Block-structured AMRm_amr + m_amr_registers: two-level 2:1 refined block hierarchy; conservative restriction and conservative-linear prolongation with physics-specific closures; per-stage flux registers with Berger–Colella refluxing; Berger–Rigoutsos multi-block dynamic regrid; optional dt/2 subcycling; multi-rank (single-owner blocks assigned by Morton-SFC work balancing at each regrid, with migration; blocks may span rank seams via P2P coarse↔fine gather/scatter; same-level seam halo; distributed registers); restart (both IO modes, regridded-layout persistence); AMR-aware post-processing (fine blocks visualizable as Silo overlay domains); GPU-resident fine level on both OpenACC and OpenMP offload.

Full algorithm and user documentation: docs/documentation/amr.md (support matrix enforced at runtime by the checker — unsupported combinations abort with named messages, never silently).

AMR physics support matrix (abridged; authoritative table in amr.md)

Supported and golden-tested: single- and multi-fluid (5-eq, mpp_lim) · 6-eq with per-block pressure relaxation · viscous (refluxed) · phase change (relax) · chemistry incl. species diffusion · Euler–Euler bubbles (polytropic/non-polytropic, mono/polydisperse, QBMM incl. non-polytropic with per-block pb/mv side-state; dynamic regrid + subcycle) · acoustic sources (coarse-grid support with regrid exclusion) · immersed boundaries (multi-body, static or prescribed-motion, incl. dynamic regrid with body-containment expansion and per-substage guards) · 2D axisymmetric (per-block WENO-coefficient recompute) · stretched grids (exact parent-bisection ghost coordinates + per-swap coefficient recompute) · hybrid WENO/Riemann sensors (per-level) · Lagrangian bubbles (cloud excluded from blocks; two-way coupling on the coarse grid; regrid clips around the moving cloud) · active_box (blocks contained in the growing window; agrees with plain AMR to ~1e-14) · IGR (restriction-only coupling: fine sigma solve seeded/Dirichlet-bounded by the coarse solve; documented truncation-order seam, exact free-stream) · 1D MHD/RMHD (div(B)=0 by construction in 1D; HLL and HLLD, incl. relativistic).

Gated with named aborts (documented rationale): surface tension (seam force imbalance is structural — three fixes attempted and diagnosed in amr.md) · 2D/3D MHD (attempted and measured: the c/f seam is a continuous O(1) div(B) source GLM cleaning cannot remove — needs constrained-transport-class B prolongation/reflux) · hyperelasticity · 3D cylindrical (global azimuthal filter) · force-driven IB (moving_ibm=2) · STL bodies · Riemann-extrapolation BCs (bc=-4) · amr_subcycle under IGR · stretched grids with Lagrangian/IB-regrid (uniform-spacing index conversions).

Validation evidence

  • Conservation: reflux-exact (~1e-15 defects) across single/multi-fluid, viscous, bubbles, chemistry, phase change, through subcycled + regridding advances.
  • Free-stream preservation: exact (0.0) with subcycling + regrid armed, including 3D, IGR, and Lagrangian configs.
  • Decomposition exactness: np=1 == np=2 element-exact with blocks spanning rank seams (fields byte-identical incl. chemistry species).
  • Methodology: every feature validated by AMR-vs-no-AMR reference comparison with a control experiment calibrating the expected error scale, plus mechanism evidence from run logs (regrid counts, box coordinates, ghost-point counts) — a golden that does not mechanically exercise its path is treated as no coverage. Liveness goldens for the hybrid sensors are deliberately sized so a silently dead sensor fails them (~5e-4 signal vs 5e-5 tolerance) — they caught a real dead-sensor bug on Cray GPU during CI hardening.
  • GPU: full test suite with post-process on 2×V100 — OpenACC 644/644 executed pass, OpenMP offload 643/644 (one slurm launch flake). Speed spot-check (800² 2D blast, 2 ranks): AMR + dynamic regrid costs +3% wall over no-AMR at these sizes; ACC ≈ 12% faster than OMP; acc/omp solutions bitwise identical.
  • CI: one fully green 85-lane matrix on record (all GitHub-hosted lanes: gnu/intel × debug/no-debug/reldebug × double/single/mixed, macos, and the NVHPC 23.11–26.3 compat matrix). Fixes landed during hardening were each reproduced locally in the exact failing configuration first (intel FP drift, single-precision tolerance classification, gfortran bounds-check OOB, OpenACC present-table mapping ×3).

Known issues (all non-gating or in progress)

  • Frontier/Cray, in progress: two device-mapping issues in the IB machinery under CCE (plain-IBM descriptor corruption on gpu-omp; a present-table crash in the AMR-IB swap on gpu-acc). Being fixed with direct Cray iteration; the CCE bench/case-opt lane failures share the same root.
  • NVHPC 24.1/24.3 compat lanes (informational, continue-on-error): an intermittent post-detected NaN on the two Lagrangian+AMR goldens. Exhaustively unreproducible off GitHub's runners — the exact failing stack (NVHPC 24.3 SDK, -tp=px -Kieee, HPC-X MPI, and the CI docker image itself under apptainer) passes elsewhere, as do native/zen2 builds; 24.5+ green. Documented at the golden definitions.
  • Accuracy postures documented rather than hidden: multi-fluid alpha-sum deviation bounded ~5.7e-3 at historical block faces during shock crossing (non-growing); viscous prolongation-ghost seam ~1e-6 np-dependence; IGR seam conservation is truncation-order (no reflux capture from the fused IGR kernels — exact-conservation capture is noted future work).

Review guide

The commit history is arc-ordered (active-box → load-weight → SFC → weighted decomposition → rank timing → hybrid → m_box → AMR rungs → physics envelope → CI/GPU hardening); reviewing by arc is much easier than by file. The AMR arc builds stepwise: static hierarchy → restriction/prolongation → fine advance → refluxing → regrid → subcycling → multi-rank → GPU → each physics rung with its own validation. Commit messages carry the validation evidence for their change (measured defects, golden UUIDs, repro details for CI fixes).

All parameters ship default-off with case_validator entries, runtime checker gates, and case.md/amr.md documentation.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an opt-in (“default-off”) family of performance/diagnostic features (load-weight and SFC partition diagnostics, weighted init-time decomposition, rank timing), plus major simulation capabilities (active-box RHS windowing and block-structured AMR) and corresponding post-processing support and documentation/validation updates.

Changes:

  • Adds new runtime parameters and toolchain metadata/validation hooks for the experimental performance/AMR feature family.
  • Extends the simulation code with new modules for active-box restriction, load-weight diagnostics, SFC partition reporting, rank timing, and AMR integration points (including restart/output plumbing).
  • Updates post_process to read/write AMR fine-block overlays and adds/updates golden metadata plus documentation/indexing.

Reviewed changes

Copilot reviewed 82 out of 94 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
toolchain/mfc/params/descriptions.py Adds user-facing descriptions for new experimental/performance parameters.
toolchain/mfc/params/definitions.py Registers new parameters (AMR, hybrid sensors, load-balance diagnostics) and target applicability.
toolchain/mfc/lint_docs.py Treats new validator checks as non-physics doc checks.
tests/F980C769/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/ECABA006/golden-metadata.txt Adds golden metadata for active-box test coverage.
tests/DD4CD8F3/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/CC4213FD/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/BD21A5C0/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/BCBA6E74/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/ACE05393/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/987D9025/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/852CCB81/golden-metadata.txt Adds golden metadata for AMR-related golden tests.
tests/65C375B4/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/4DADE04B/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/454C565F/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/3A474BEE/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/2FC423D3/golden-metadata.txt Adds golden metadata for a new/updated test.
tests/13945217/golden-metadata.txt Adds golden metadata for moving-IB under AMR test coverage.
src/simulation/m_viscous.fpp Clamps FD coefficient indexing to avoid ghost-region coefficient OOB in IB drag gradient evaluation.
src/simulation/m_time_steppers.fpp Integrates active-box bounds into RK update loops and interleaves AMR fine-stage/subcycle operations.
src/simulation/m_start_up.fpp Wires up new modules (rank timing, active-box, load-weight, SFC partition, AMR) into init/timestep/finalize and restart I/O.
src/simulation/m_sfc_partition.fpp Adds analysis-only SFC tiling + weighted partition prediction and reporting.
src/simulation/m_rank_timing.fpp Adds per-rank wall-time imbalance measurement helpers and reporting.
src/simulation/m_load_weight.fpp Adds per-cell load-weight field construction and rank-level imbalance reporting.
src/simulation/m_hypoelastic.fpp Refactors FD coefficient setup into a callable update routine (supporting AMR grid swaps).
src/simulation/m_global_parameters.fpp Adds AMR working-state mirrors and slot selection helper plus defaults for new parameters.
src/simulation/m_data_output.fpp Adds output/report hooks for load-weight, SFC partition, and rank-time diagnostics.
src/simulation/m_checker.fpp Adds input validation/prohibits for active-box, hybrid sensors, load-balance, and AMR configurations.
src/simulation/m_active_box.fpp Adds active-box initialization/growth and debug envelope checking.
src/simulation/m_acoustic_src.fpp Adds AMR-aware handling of acoustic source support (bounding boxes and overlap abort).
src/post_process/m_start_up.fpp Calls AMR fine-data reader and AMR overlay writer when amr is enabled.
src/post_process/m_global_parameters.fpp Adds default-off amr flag for post_process overlay behavior.
src/post_process/m_data_output.fpp Implements AMR fine-block overlay mesh/variables output (Silo/binary) and multimesh registration.
src/common/m_phase_change.fpp Exposes per-cell Newton iteration count and threads it through relaxation to support load-weighting.
src/common/m_global_parameters_common.fpp Adjusts start_idx lifecycle/allocation and makes load_weight_wrt visible to GPU macros.
src/common/m_derived_types.fpp Introduces a simple t_box type used by new partitioning infrastructure.
src/common/m_box.fpp Adds box/partition arithmetic helpers (equal/weighted splits, box-from-splits).
src/common/m_boundary_common.fpp Skips BC buffer population during AMR fine advance to rely on coarse-driven ghost fill.
docs/module_categories.json Registers new modules under documentation categories.
docs/documentation/readme.md Adds AMR section link to the documentation index.
.typos.toml Adds project-specific abbreviations to the spelling allowlist.

Comment thread src/common/m_phase_change.fpp Outdated
D = ((gs_min(lp) - 1.0_wp)*cvs(lp))/((gs_min(vp) - 1.0_wp)*cvs(vp))

#ifdef MFC_SIMULATION
if (relax .and. load_weight_wrt) then
Comment thread src/simulation/m_sfc_partition.fpp
Comment thread src/simulation/m_sfc_partition.fpp Outdated
Comment thread src/simulation/m_rank_timing.fpp
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.65031% with 948 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.10%. Comparing base (d2d8cac) to head (f93fd33).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/simulation/m_amr_registers.fpp 71.18% 69 Missing and 103 partials ⚠️
src/simulation/m_riemann_solver_hlld.fpp 2.88% 100 Missing and 1 partial ⚠️
src/post_process/m_data_input.f90 51.32% 77 Missing and 15 partials ⚠️
src/simulation/m_amr_restart.fpp 61.18% 57 Missing and 28 partials ⚠️
src/simulation/m_riemann_solver_lf.fpp 65.91% 55 Missing and 21 partials ⚠️
src/simulation/m_rhs.fpp 68.92% 43 Missing and 12 partials ⚠️
src/simulation/m_riemann_solver_hll.fpp 78.22% 29 Missing and 25 partials ⚠️
src/simulation/m_load_balance.fpp 46.80% 42 Missing and 8 partials ⚠️
src/simulation/m_ibm.fpp 72.51% 19 Missing and 17 partials ⚠️
src/simulation/m_load_weight.fpp 21.05% 23 Missing and 7 partials ⚠️
... and 24 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1628      +/-   ##
==========================================
+ Coverage   62.81%   63.10%   +0.28%     
==========================================
  Files          84       96      +12     
  Lines       21975    30211    +8236     
  Branches     3224     4943    +1719     
==========================================
+ Hits        13804    19065    +5261     
- Misses       5949     7846    +1897     
- Partials     2222     3300    +1078     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sbryngelson

Copy link
Copy Markdown
Member Author

Upstream latent gap found during the MHD+AMR investigation (independent of this PR): m_riemann_solver_hlld.fpp contains no GLM terms, so hyper_cleaning = T with riemann_solver = 4 (HLLD) is silently inert — psi never evolves and no cleaning occurs. The existing hyper_cleaning golden uses HLL, which does carry the GLM coupling, so CI never sees this. Worth either adding GLM terms to HLLD or a validator prohibit on the combination.

Also for the record: MHD+AMR was attempted and re-gated on measured evidence rather than assumption — the coarse/fine seam is a continuous O(1) div(B) source that cleaning spreads but cannot remove (details in the amr.md support matrix row and commit ac203b1).

@sbryngelson sbryngelson changed the title Experimental performance family (default-off): load-balance infrastructure, active-box windowing, block-structured AMR, hybrid WENO/Riemann sensors Load-balance, active-box windowing, block-structured AMR Jul 21, 2026
@sbryngelson

Copy link
Copy Markdown
Member Author

active_box is single-rank-only — make it MPI-real, or defer it?

Raising this as a self-review point. active_box disables itself with a warning at num_procs > 1 and falls back to full-domain compute. Since production runs here are essentially always multi-rank, that makes the optimization a no-op (plus a warning line and some maintenance surface) in exactly the configurations that matter. We should decide what to do with it.

Why it's single-rank today. The active box is a single global, coarse-cell-indexed region. On one rank you just clip the RHS loops (convert / reconstruct / Riemann) to it. Under domain decomposition the geometry is easy — each rank intersects the global box with its subdomain and freezes the exterior — but it creates a load-imbalance problem: if the disturbance occupies only a few ranks' territory, those ranks do all the work while the rest idle, and the slowest rank sets wall-clock, so you save nothing. The current code sidesteps this by disabling rather than solving it.

The enabling pieces are already in this PR. A multi-rank active box needs (a) per-rank clipping of the box (straightforward), and (b) a way to keep otherwise-idle ranks busy — i.e. redistribute the domain so the active region spreads across ranks. That second part is exactly what this PR's load-balance arc (m_load_balance / m_sfc_partition / m_load_weight) provides, and the active box already produces the natural work-weight signal (active vs. frozen cells) that a rebalancer would consume. The two features are built to compose — active box marks where the work is, load-balance moves ranks to it — they're just not wired together yet.

Options:

  1. Make it MPI-real. Per-rank box clipping + feed the active/frozen cell count into the load-weight so load_balance redistributes toward the active region. This is the version that actually helps at scale, and it's the natural payoff of shipping the load-balance infrastructure in the same PR.
  2. Defer / gate it. Drop active_box from this PR (or keep it behind an explicit experimental flag with the single-rank limitation documented up front) and keep the performance family focused on AMR, which is already multi-rank-capable via the P2P device-path gather/scatter. Re-introduce active box once the load-balance integration is designed.

Leaning toward (1) if there's appetite, since the enabling infra is right here; otherwise (2) keeps the PR's surface honest — a default-off single-node optimization advertised in an exascale code invites confusion. At minimum, the docs should state plainly that it is single-rank-only today.

ffmt detects structure it cannot match and reports it as a warning on stderr while still exiting 0, and format.sh discarded that stderr. The combination meant malformed Fortran formatted clean and linted clean, so the compiler was the first thing that could say no: an unbalanced end do / end block passed both and only failed an 18-minute GPU build. Capture ffmt's stderr, always show it, and treat unmatched structure as fatal. Verified both directions: it fires on a reconstructed broken file and leaves all 303 clean files passing, so it cannot trip on well-formed code.
The level-1 tag ALLGATHERV is W4's largest term: every rank received the GLOBAL tagged-cell list, 92.77 MB per rank per regrid at np8 and doubling with every np-doubling. Delete it. s_amr_union_gtag is replaced by s_amr_local_tags, where each rank scans only its own disjoint interior, and s_amr_cluster gains a reduce mode that ALLREDUCEs one fused per-axis signature per tree node. Integer SUM is exact and order-independent, so every rank obtains a bit-identical signature and makes bit-identical trim, accept and split decisions.

One reduction per node suffices for all three uses, which is why this is not three collectives: s_amr_trim_box took the per-axis MIN/MAX of the contained tags, and that IS the first and last nonzero of the signature; its emptiness test IS sum > 0; and s_amr_find_split only ever touched the tag list to build the signature. So s_amr_box_sig / s_amr_trim_from_sig / s_amr_find_split_sig replace the two tag-rescanning routines, and the node makes one pass where it previously made up to num_dims + 1. Sizing the signature on the UNTRIMMED box is what lets the same buffer serve the trim, since trim only shrinks to the tags' own bbox and no tag leaves the box. Per-axis reductions would be WRONG: find_split returns early on the first axis yielding a zero-run split, so a per-axis collective would make the collective COUNT data-dependent and therefore rank-divergent.

A rank with no local tags must still walk the tree and enter every reduction, contributing zeros; the early return is now gated on .not. reduce, because returning early would deadlock the ranks that do have tags. Gates: restart output byte-identical at np8, the clustering tree reproduced exactly (lmax 8192, ldepth 31, nodes 66354 in both arms), ntag_bytes 185241240 -> 0, and 69/69 AMR goldens, which cover the 1D/2D, np=1 and multi-level paths a single np8 probe cannot. Adds a ppn=2 AMR+IB case, the suite's only distributed AMR+IB coverage. Measured after: the reduction grows 1.66x per np-doubling against the gather's 2.00x, so W4 is improved but NOT met; S3.2 and S3.3 remain.
… status, increment status

The planning docs had drifted from the code in ways that would misdirect the next reader. amr_action_plan.md gains ledger (38) and leads with a CURRENT STATE block so live status precedes 2400 lines of history. amr_endstate.md's invariant table had a today column describing the collective S3.1 deleted; all seven rows are rewritten against verified numbers. amr_plan_based_exchange.md gains a STATUS block: landed I0/I1a/I1b/I2a/I3/I4a/I4b/I5, outstanding I2b/I5b/I6/I7/I8.

Two corrections worth stating rather than silently fixing. W5 is not one increment: 19 of 41 AMR p2p call sites still tag per box, F1 retains an unconverted path passing amr_cur as the tag, and the subcycle sites are an explicit deferral to I8 -- so the ~28k-rank tag wall does not clear until I8. And m_amr.fpp claimed that term leaves with I7, which contradicts I7's own boundary that any family left per-box keeps its tables; the comment is corrected and the contradiction recorded. Also W1-W7 -> W1-W8, and W2 marked flat in P and therefore off the scaling critical path.
…ee is rank-local (S3.2a)

B0: amr_blocking_factor raises min_child from the algorithmic floor of 2, which is what stops the bisection over-generating. With the floor at 2 and amr_cluster_eff = 0.9 the recursion never converges on its own -- it splits until amr_max_blocks stops it (warning on every regrid at five different caps) and the min-separation merge then collapses the result back. Measured at np8, per regrid: bf=1 gives 38,255 nodes / 594 boxes; bf=8 gives 6,475 / 576; bf=16 gives 1,466 / 576. So 26x fewer tree nodes produce the SAME answer. Default bf=1 is exact identity, min_child = max(2,1) = 2.

Deliberately NOT AMReX's blocking factor. AMReX coarsens the TAG LATTICE, which also shrinks its global tag gather; S3.1 already deleted that gather here, and coarsening a rank-local sparse list cannot dedup coarse cells straddling a rank boundary without an extra exchange -- lower-corner ownership would DROP tags rather than dedup them. A size floor is simpler and is the part that actually stops over-generation.

S3.2a instrumentation: per tree node, does its box fit inside ONE rank's subdomain? If so that rank holds every tag in the subtree and the subtree needs no communication -- the property S3.2's design rests on. Counted separately for the reducing level-1 path ([amr-scope-r]), because the level-2 forest clusters inside a parent window and is rank-local by construction, which would inflate the fraction. rbytes is split the same way: rbytes_now counts only nodes that actually reduce, rbytes projects the finished design.

Result at np8: 7 of 16,383 level-1 nodes per regrid are shared, 99.96 percent rank-local, shr_maxdep 3 = log2(P). It holds at bf=8 (99.7 percent), so it is not a saturation artifact. np16 gives shared 11, shr_maxdep 5, and shared BYTES growing 1.86x per doubling -- which refutes doing the shared phase as plain allreduces over COMM_WORLD and keeps the sparse per-depth scoping in the S3.2 design.

Gated by bit-identity: at bf=1 the restart output is byte-identical to the S3.1 binary, the counters being read-only. The 69-test AMR suite passed on S3.1 and was re-running on this build when the node allocation expired; re-run it before relying on this beyond bf=1.
… retract the P^0.73 law

Three corrections the plan needed. (1) The two-point 'rbytes ~ P^0.73' law is RETRACTED: the np8/16/32 ladder shows the forest term growing 1.97x/1.99x = exactly P while the cap-fixed level-1 term stays constant, so the exponent climbs 0.73 -> 0.87 toward 1.0. S3.1 is asymptotically O(P), a ~27x COEFFICIENT cut, and W4 is not met. Published on two points, refuted by the third.

(2) S3.2's design contract, recorded before any of it is written, including the negative result that depth-fusion ALONE is still O(P) per rank: an allreduce delivers the whole buffer to every rank, so fusing a depth's nodes fixes latency and leaves the slope. The mechanism that works is a sparse per-depth exchange over the shallow shared phase, with the deep phase rank-local and free, and no MPI_Comm_split anywhere.

(3) B1, a prerequisite found by reading every use of nacc: the min-separation merge consumes the accepted boxes IN INSERTION ORDER, fusing the first too-close pair and removing by swapping with the last element. So the merged box set is a function of traversal order, and S3.2 -- which completes local subtrees in parallel -- cannot be bit-identical while that holds. B1 canonicalises the merge input by a deterministic key. It is the ONLY planned change that moves goldens, so it lands alone; folding it into S3.2 would confound 'did the scoping break something' with 'did canonicalisation move the boxes'.
… that decides S3.2

The suite finished after the commit that flagged it as pending: 69 passed, 0 failed. Records B0's pricing (26x fewer tree nodes for the same box set, saturation gone at bf >= 4) and the finding that B0 is NOT a W4 lever -- it prunes leaves, which are already rank-local and free, leaving the shared set untouched. Also records why big-machine allocation should be held: the need for the scoping technique is established here by mechanism (the forest term grows exactly with P while the cap-fixed level-1 term is constant), so Frontier would refine where S3.1 breaks rather than whether. The discriminating question is whether shr_maxdep grows +1 or +2 per doubling, and np32 answers it on this machine.
…trumentation

The min-separation merge scanned in list order and fused the FIRST too-close pair, so its output was a function of the order boxes were ACCEPTED -- the traversal order. Sort the accepted boxes by Morton of lo (stable insertion sort; accepted boxes are disjoint so their lo corners are distinct) before the merge, and remove a fused box by shifting down rather than swapping with the last entry. The merge now depends only on the box SET, which is the precondition for any scheme where ranks produce boxes in a different order -- both the scoped level-1 clusterer and the level-2 ownership split close with a gather whose arrival order is not deterministic.

Gate: 69/69 AMR goldens pass, bit-identical -- no golden regeneration. Note this means the suite does NOT exercise the property: the order-dependence needs ambiguous chains of too-close boxes, which need many boxes (tests carry a handful, the benchmark 594-2405). The evidence B1 is needed is the reading of nacc's three uses, not the suite.

Also adds amr_cl_me_nodes_r / amr_cl_me_rb_r ([amr-scope-me]): the shared clustering nodes whose box actually reaches into THIS rank's subdomain, max-reduced across ranks. Print-only and golden-neutral; it prices the sparse per-depth exchange against the allreduce form.
…t' was a cap artifact

np32 closes the three-point sweep. gwin_bytes (the level>=2 window ALLGATHERV) is 360/719/1440 MB per rank at np8/16/32 -- ratios 1.997 and 2.001, exactly O(P), and larger than the 185 MB gather S3.1 deleted. Level-1's shared set grows only as O(log P) (7/11/15 nodes per regrid). Records the S3.3 design contract (ownership + Alltoallv + one box gather) and reorders S3.3 ahead of S3.2b.

CORRECTION, prompted by auditing the input decks: the decks are correct (period-1 blob lattice, 8/16/32 blobs, constant 200 cells per unit, nboxes 594/1206/2405, restart files 6 vars x cells x 8 B) but amr_max_blocks = 8192 was FIXED across rungs and SATURATED in all three (lmax exactly 8192, capped warning 10/10, nodes = 2*8192-1). So 'level-1 collectives are flat in P' measured the CAP, not the clusterer. Confirmed directly: at bf=4 np8 gives 13,825 nodes and 0 capped warnings, np16 re-saturates at 16,383 with 5. gwin's O(P) survives (it is driven by tagged cells, ~40 MB/regrid at both bf=1 and bf=4); the claim that level 1 is asymptotically harmless does not. The ordering is marked provisional pending the uncapped arm.

Also records B0b (amr_blocking_factor default 1 -> 4, because the cap is load-bearing at the shipped default and a private subtree cannot see global nacc), and S3.3c: S3.3a/b do NOT close W1 -- pass 1 still allocates a dense window per parent on every rank and runs an O(parents x global blocks) = O(P^2) scan.
…oint-to-point

Every rank was clustering EVERY parent's nesting window. Pass 1 gathered all ranks' tagged cells with one MPI_ALLGATHERV per level -- gwin_bytes, measured at 360/719/1440 MB per rank at np8/16/32, ratios 1.997 and 2.001, i.e. exactly O(P) and larger than the 185 MB tag gather S3.1 deleted. Pass 2 then rebuilt each parent's window with 'do i = 1, ntot_g; if (gkb(i) /= kb) cycle' INSIDE 'do kb = plo, phi', an O(parents x global tags) rescan on every rank -- W1's pass-2 term.

S3.3a: assign each parent an owner, powner(kb) = mod(kb - plo, num_procs), a pure function of kb so every rank agrees without communicating; pass 2 processes only its own parents. The global 'nboxes + 1 > amr_max_fine' guard cannot be evaluated per rank once children are produced in parallel, so children are emitted uncapped into a local list and replayed after ONE ALLGATHERV of the child BOXES (7 ints each, ~67 KB against the 144 MB per-cell gather). Each parent has exactly one owner emitting in order, so a stable counting sort by kb reproduces the serial append order exactly -- same box list, same truncation at amr_max_fine, same box_level.

S3.3b: bucket the packed pairs by powner(skb(i)) and replace the ALLGATHERV with MPI_ALLTOALL + MPI_ALLTOALLV. Send volume becomes O(this rank's tagged cells), receive volume O(its assigned parents' tags). Pass 2's residual per-parent scan becomes O(parents/P x tags/P). Safe because pass 2 rebuilds a DENSE window and re-extracts in (k,j,i) order, so ctags does not depend on pair arrival order.

Gate: 69/69 AMR goldens pass, bit-identical -- including the np=2 multi-level dynamic regrid case, which is the golden that actually exercises the cross-rank nesting path this touches.

NOT closed by this: pass 1 still allocates a dense window per parent on every rank and runs 'do ob = 1, amr_num_blocks' inside 'do kb = plo, phi' (O(P^2) per rank). See the S3.3c and W1 root-cause sections in amr_action_plan.md.
…EP, and W5's wall is the tag base

Arm B (block cap raised to 65536 so it never binds, three points) overturns two of my own readings, both pure cap artifact. Level-1 tree nodes = global ALLREDUCEs per regrid: 13,825 / 27,537 / 54,961 = 1.992x, 1.996x -- O(P), not the flat 16,383 reported earlier (16,383 = 2*8192-1, a full binary tree pinned by amr_max_blocks and identical in every rung by construction). Level-1 shared nodes: 11 / 23 / 47 -- O(P), not the '+4 per doubling, O(log P)' read off cap-pinned trees. So S3.2b removes an O(P) collective count, not a latency constant, and ledger 41's reordering premise (forest O(P) vs level-1 flat) is dead -- S3.3 was still right to land first, on absolute size and readiness.

W1 sized from the code: 44 'do = 1, amr_num_blocks' sites across 22 routines, and the costly ones are on the RK STAGE path (m_time_steppers calls stage_fill/parent_fill/reflux_faces/freg waves), ~16 scans per stage = ~48 per step. Each walks the whole machine's block list to find this rank's ~75: ~360 M evaluations per rank per step at 1e5 ranks. Root cause is that amr_region_lo_all/hi_all/owns_all/block_level are all allocated to amr_max_blocks, so every rank holds every block (~240 MB/rank at 1e5) -- W1 cannot be closed by restructuring loops. Records W1a (owned-block index list, 10 sites grep-verified convertible) and W1b (distribute the metadata), plus a free deletion: reflux_faces_wave and freg_wave each rescan the global list purely to re-derive an order already stored in amr_fw_rblk.

W5 scoped from the tag audit: 24 of 57 p2p calls are already on the (family, epoch) form; the per-object stragglers are concentrated in the L0 tile family and the chunked gather. But the binding constraint is amr_tag_base(f) = amr_max_blocks + 100*f -- every family's base carries a global-block offset, so the MPI_TAG_UB assert caps ~28k ranks even for converted families. Only dropping that term removes the wall, and its precondition is that no family uses per-box tags.
…ock list

The RK stage path ran ~48 full scans of the GLOBAL block list per step (~16 per stage under RK3, from s_amr_stage_fill_wave, s_amr_parent_fill_wave, s_amr_restrict_l1_wave, s_amr_restrict_parent_wave, s_amr_reflux_faces_wave, s_amr_freg_wave and s_amr_convert_prim_batch), each walking every block in the machine to find this rank's ~75. At 1e5 ranks with the measured 75 blocks/rank that is ~360 M predicate evaluations per rank per step, linear in P while the real work is fixed -- the direct W1 violation. Six of those loops now iterate amr_my_blk, this rank's own block indices in ascending order, which visits the same set in the same order (they only skip blocks the scan would have cycled) so the paired MPI_SENDRECVs stay matched.

The list is refreshed LAZILY on a dirty flag, not rebuilt in s_amr_assign_block_owners. The first version did the latter, on my assumption that the assigner is the sole authority for ownership. It is not: a tiled level-2 block inherits its parent's owner (amr_block_owner(L2) = amr_block_owner(par)) and the restart and migration paths assign directly. The list went stale and the tiled-L2 multi-level dynamic-regrid golden -- the only test that exercises the inherit path -- failed. amr_myblk_dirty is now set at all four owner writes and the list rebuilds on first use, which is robust to a fifth writer appearing later.

Gate: 69/69 AMR goldens pass, bit-identical. Left unconverted: the five sites where s_amr_select_slot(k) runs BEFORE the owner filter, since that call has a side effect on amr_cur and skipping it is not provably equivalent.
… one

The module comment states the tag ceiling caps global blocks near 2**21 ~ 2.1e6, about 28k ranks at 75 blocks/rank, and the plan has treated that as the second scaling wall. Measured on hpcfund Open MPI 4.1.8: MPI_TAG_UB = 2147483647 (2**31 - 1), which admits amr_max_blocks up to ~2.1e9, about 28 MILLION ranks. The 2**21 figure is an assumption the code carried as fact. Records the probe and pends the same measurement on Frontier's Cray MPICH: if it also reports >= ~2**25, W5 leaves the critical path and the demonstration does not wait on it.

Also records two user answers that change the plan: subcycling IS required for the demonstration, which un-defers the I8 subcycle sites (checker-gated at np>1 today) and couples them to W5 whichever way the tag probe lands; and old checkpoints MUST stay readable, so the restart format fix keeps a legacy reader path -- version-stamp the new layout, read both, write only the new. The restart item stays first regardless: it is the only one whose failure mode is silent corruption of saved state rather than a crash.
…ANK vector

The parallel_io checkpoint wrote, for EVERY block, a 3*num_procs integer extent vector -- and read it back the same way. That made the FILE FORMAT O(global blocks x ranks): 3 x 75000 x 8192 x 4 B = 7.4 GB of header per checkpoint at Frontier scale, with the matching allocation on every rank in both directions. The array is almost entirely zeros, because only the OWNER of a block has a nonzero extent triple; the information content is (owner, m, n, p) per block. v2 stores exactly that, in 4 ints, so the header is O(blocks): 128 KB where v1 wrote 7.4 GB.

The benchmark restarts from file every run but never WRITES one at scale, and the two restart goldens run at np<=2, so nothing in the ledger touched this. It would have appeared for the first time on Frontier, on the first checkpoint.

Versioning without changing the 3-int global header: v2 writes a NEGATIVE rank count. A v1 reader meeting a v2 file hits its existing rank-mismatch error rather than misparsing block records, and the v2 reader keeps the full v1 path, so old checkpoints stay readable. owner is stored +1 because the record is recovered with a MAX reduction and rank 0 would otherwise be indistinguishable from an unwritten slot. The v1 same-rank-count condition on the consistency check is preserved exactly, so repartition-on-restart behaves as before.

Gates: 69/69 AMR goldens; an np=8 write-then-read roundtrip (the goldens run at np<=2, where a layout indexed by rank count barely differs from one that is not); a v1 checkpoint written before this change read back by the v2 binary; and header inspection confirming 8 vs -8. The size win is invisible at np=8 (5.7 KB of 1.5 GB) -- the term removed is O(blocks x RANKS), so this is justified asymptotically, like S3.3.
… deck at WENO3 (buff_size 3) has the same steady excess as at WENO5 (0.95-1.07 vs 0.94-1.04 s/step, two reps each, same node and hour) and the fill rows are flat, closing ledger 107's open question from MFC's side
…mr_snap coarse cells per face of a live block of the same level takes the live block's box, applied all-or-none under same-level disjointness and proper nesting. The [amr-keep] probe showed 6 of 10 rebuilds on the S0 deck re-create every block because the tag envelope drifted one cell and every tile shifted with it; snapped boxes are identical, and when every box snaps the existing boxes_unchanged fast path skips the rebuild. Validator bounds amr_snap <= amr_buf - 2 so two cells of tag padding survive every snap; the cadence audit is the runtime coverage check. On the S0 deck at np8, amr_snap = 2 turns 7 of 11 rebuilds into no-ops and the differenced step falls 15-18% with every skew wait roughly halved (ledger 109).
…within amr_snap cells per face of a live same-level block takes its box, so a feature drifting a cell no longer re-creates every block; on the S0 deck at np8 7 of 10 rebuilds become no-ops, the differenced step falls 15.5-17.7% with regrid -55% and every skew wait roughly halved, coverage audit escaped 0, fields differ from snap 0 by 1e-6 relative; goldens 71/71 on both lanes with the flag off; two-code rerun with the flag pending
…hing default under dynamic regrid (amr_regrid_int > 0, amr_buf >= 3); an explicit amr_snap is never overridden. Ledger 109 measured the two-cell snap at -15.5 to -17.7% per step on the S0 deck with the coverage audit at zero.
…odes, one node, one window: MFC 0.72 s/step (sd 0.10) vs AMReX 0.38, 1.90x over three reps and 1.79x over the two clean reps (my own CPU golden suite overlapped rep 3), per rep 1.61/1.96/2.13x: the 2x target is straddled, not met, by the pre-registration's own condition; caps 32/64/96 null/-16%/-19%; amr_snap = min(2, amr_buf - 2) becomes the toolchain default under dynamic regrid (goldens 71/71 on both lanes, none touched)
… of state (Mie-Gruneisen, JWL), CI fixes (Phoenix syscheck --bind-to none; faulted-node identification). Conflicts resolved in the three Riemann solvers (upstream's speed-of-sound signature, the 6-equation isentrope flux and its five new private variables carried into our opt-in kernels), m_variables_conversion's export list (both sides), m_start_up's finalize order (upstream's pressure-relaxation report before the AMR finalize block) and case.py's generated header (both case-optimization sets).
…ns of state, Phoenix syscheck bind fix, faulted-node CI identification); six conflicts carried both sides; full CPU suite 789/789 and GPU AMR goldens 71/71 with none touched
…t is the link, not the transport mode or the traffic: per-rank messages and bytes flat np8->np16, MPI wait 436->1007 s per 240 steps on the same bytes, and rdma_mpi on/off identical at np16 (163.7/163.1/164.5 s A/B/A); levers handed to the ladder (bytes across the boundary, placement, overlap); rerun the rung with amr_snap on first
…c to repeated launches -- five fresh 240-step arms back to back sit in a 1.3% band with temperatures, clocks, VRAM and the store flat -- but ledger 111's +3.4%/+5.3% climb is 6-10 of these standard deviations with no co-tenant on its AMR arms, so it stays unexplained with the interleaving of arms as the surviving candidate; item 4's clean rerun will run each code's arms consecutively
…x per doubling against the 1.20x bar, from 1.59x flag-off; the np16 step -22% with five sixths of it MPI wait on unchanged step-family bytes (migration traffic -58%, rebuilds 7 -> 4), np8 -2% across nodes; fewer rebuilds remove a third of the cross-node wait, correcting ledger 113's 'it is the link' to the link carrying rebuild-fed wait; rung with all shipped defaults queued
…ow, each code's arms consecutive: MFC 0.70 s/step (sd 0.08) vs AMReX 0.36, 1.94x -- under 2.0 at the mean on a 1.7-2.3x band, met by this run's pre-registered threshold and not by ledger 111's stricter AMReX clause; the consecutive MFC arms did not climb
…s/step and np16 6.48 -> 4.31, doubling 1.38x (snap-only 1.26x, flag-off 1.59x): the falsifier fired, batching takes proportionally more off the node-local step; gather, seam, halo and the base-grid solve carry 69% of the doubling's growth
…ack launches per short arm put the 40-step AMR arm at sd 3.1 %, the uniform 20/60 arms at 3.8 %/2.8 %; a differenced AMR step carries 0.7 %, a differenced uniform step 5.2 %, the excess 0.050 s/step (7-8 %), with the uniform pair 96 % of that variance; the 4.96 % figure is retired at every citation
…e budget the pools stay on the host and each column is staged through one device scratch column. Fixes the device OOM at migration-heavy rebuilds (every 240-step rung arm since the device wire landed); wire bytes and order unchanged
… 2 GiB (host pools plus one device scratch column above it) -- the device-resident wire of ledger 108 pushed every 240-step rung arm past 64 GB/GCD at migration-heavy rebuilds; the fixed binary runs the arm with the pre-wire pin's 53 GiB peak; wire bytes and order unchanged, byte-identical on the S0 deck
…r the buffered domain instead of exchanging prim ghosts a second time (36 -> 18 base-grid SENDRECVs per step; byte-identical; GOAL v7 item 2a)
…nd the RHS converts over the buffered domain instead of exchanging prim ghosts again -- 36 -> 18 base-grid SENDRECVs per step, byte-identical (GOAL v7 item 2a); timing read deferred to healthy nodes
…rent wave, s_amr_freg_wave deleted (one rendezvous per level in the fold; byte-identical; GOAL v7 item 2b step 1); timing read deferred to healthy nodes
…over owners) instead of ascending box id, so a gather chunk carries every rank's boxes; chunk-local indices, the same-chunk parent test and the early-free move to walk positions; ordering only, byte-identical (GOAL v7 item 3a)
…l so a gather chunk carries every rank's boxes instead of one rank's; chunk-local indices, the same-chunk parent test and the early-free move to walk positions; ordering only, byte-identical in two stages (GOAL v7 item 3a); timing read deferred to healthy nodes
…nteriors only) with private pools, drain after the parent fills where the seam stood; byte-identical, one rendezvous fewer in the fill chain (GOAL v7 item 2b, second half)
…te pools, drain after the parent fills where the seam stood; byte-identical (GOAL v7 item 2b, second half); timing read deferred to healthy nodes
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

Lines of Code

File Lines Diff
src/simulation/m_amr.fpp 8317 +8317
src/simulation/m_amr_regrid.fpp 2070 +2070
src/simulation/m_amr_registers.fpp 1048 +1048
src/simulation/m_amr_restart.fpp 383 +383
src/post_process/m_data_input.f90 722 +314
src/simulation/m_phase_timing.fpp 235 +235
src/simulation/m_ibm.fpp 1474 +221
src/simulation/m_amr_xchg_audit.fpp 210 +210
src/simulation/m_time_steppers.fpp 1016 +181
src/simulation/m_sfc_partition.fpp 169 +169
src/simulation/m_load_balance.fpp 164 +164
src/post_process/m_data_output.fpp 1338 +151
src/simulation/m_load_weight.fpp 121 +121
src/simulation/m_active_box.fpp 118 +118
src/simulation/m_rhs.fpp 2048 +92
src/simulation/m_global_parameters.fpp 857 +88
src/common/m_box.fpp 71 +71
src/simulation/m_start_up.fpp 1308 +59
src/simulation/m_rank_timing.fpp 50 +50
src/common/m_mpi_common.fpp 1522 +48
src/simulation/m_data_output.fpp 1373 +48
src/simulation/m_acoustic_src.fpp 551 +36
src/simulation/m_riemann_solver_hllc.fpp 1333 +32
src/simulation/m_riemann_solver_lf.fpp 548 +31
src/simulation/m_riemann_solvers.fpp 137 +25
src/simulation/m_weno.fpp 1364 +24
src/common/m_phase_change.fpp 306 +22
src/common/include/2dHardcodedIC.fpp 453 +21
src/simulation/m_bubbles_EL.fpp 1648 +17
src/common/include/omp_macros.fpp 372 +14
src/simulation/m_checker.fpp 84 +14
src/common/include/macros.fpp 141 +13
src/post_process/p_main.fpp 61 +7
src/simulation/m_hypoelastic.fpp 604 +7
src/simulation/m_igr.fpp 2295 +7
src/common/m_constants.fpp 98 +6
src/common/m_boundary_common.fpp 453 +5
src/common/m_derived_types.fpp 473 +4
src/simulation/m_riemann_solver_hll.fpp 599 +4
src/common/include/3dHardcodedIC.fpp 243 +3
src/common/m_boundary_io.fpp 271 -3
src/simulation/m_surface_tension.fpp 266 +3
src/common/m_variables_conversion.fpp 1451 +2
src/post_process/m_start_up.fpp 788 +2
src/simulation/m_cbc.fpp 1113 -2
src/post_process/m_global_parameters.fpp 400 +1
src/pre_process/m_data_output.fpp 625 +1
src/simulation/m_bubbles_EE.fpp 304 +1
src/simulation/m_riemann_solver_hlld.fpp 191 -1
src/simulation/m_thinc.fpp 313 +1
Directory Lines Diff
common 10514 +206
pre_process 4514 +1
simulation 41692 +13773
post_process 3851 +475
total 60571 +14455

… is a persistent rebuild-free skew plus one observed post-rebuild spike (2.5-4x for one interval; at np8 an exchange-side wait with no rhs skew); GOAL v6 item 3 (level-2 overlap copy) deferred, a stated departure from the pre-registered rule
… on the rung deck and is not byte-identical (few-ULP owner dependence in the stage-1 advance); negative result, not landed
…ld interleave (interleave -6.6 %, halo merge -7.5 %, fold merge +1.3 %; the seam-post union read unverified; afternoon repetitions void on a node with intermittent GPU stalls; pre-wire control 2.949 s/step)
… one window: MFC AMR excess 0.58 s/step vs AMReX 0.45 (1.30x; ledger 117: 1.94x); half wait, a third thin work, 0.09 residual per-block RHS inflation; the batching's fragmentation is the pre-registered lever (pad tolerance falsified, largest-first order identity-gated, wall read pending on the EOS pin)
…33x, the schedule increments do not move weak scaling); item 2(c) and item 3(c)/ledger 121 closed by written decision; the np32/np48 rungs void on an unexplained scale-dependent NaN with the mesh never refining, not node health; np64 cannot be placed on this machine
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants