Skip to content

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

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

Load-balance, active-box windowing, block-structured AMR#1628
sbryngelson wants to merge 724 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 64.93163% with 795 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.39%. Comparing base (41df185) to head (7a5fbd1).
⚠️ Report is 6 commits behind head on master.

Files with missing lines Patch % Lines
src/simulation/m_amr_registers.fpp 66.32% 95 Missing and 102 partials ⚠️
src/post_process/m_data_input.f90 51.87% 76 Missing and 14 partials ⚠️
src/simulation/m_amr_restart.fpp 61.18% 57 Missing and 28 partials ⚠️
src/simulation/m_load_balance.fpp 46.80% 42 Missing and 8 partials ⚠️
src/simulation/m_rhs.fpp 71.89% 33 Missing and 10 partials ⚠️
src/simulation/m_ibm.fpp 71.54% 19 Missing and 16 partials ⚠️
src/simulation/m_amr_xchg_audit.fpp 54.16% 29 Missing and 4 partials ⚠️
src/simulation/m_load_weight.fpp 21.05% 23 Missing and 7 partials ⚠️
src/simulation/m_sfc_partition.fpp 62.50% 23 Missing and 7 partials ⚠️
src/simulation/m_data_output.fpp 32.25% 16 Missing and 5 partials ⚠️
... and 27 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1628      +/-   ##
==========================================
+ Coverage   61.67%   62.39%   +0.72%     
==========================================
  Files          84       96      +12     
  Lines       21619    29068    +7449     
  Branches     3196     4778    +1582     
==========================================
+ Hits        13334    18138    +4804     
- Misses       6093     7753    +1660     
- Partials     2192     3177     +985     

☔ 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.

…on advisories

amr_max_blocks defaulted to 4, capping the GLOBAL box count at four: any real refinement bound immediately and silently truncated the refined region. It sizes replicated metadata only (slots are allocated lazily for owned blocks), so a large pool costs ~11 kB/box/rank. Default is now 1024; case.md corrected, since it documented the parameter as a slot count rather than metadata.

Adds four measured advisories at init (rank 0, warn-only - every setting is legal and sometimes correct): amr_regrid_int=0 is static AMR and never adapts; amr_max_grid_size=0 derives a cap from the decomposition that SHRINKS as ranks are added, making the box set rank-dependent (pinning it measured 3.0x faster in 3D); lock-step steps the coarse level amr_ref_ratio**level times more often than its CFL needs (subcycling measured 1.55x per unit physical time); and amr_regrid_int<4 regrids more often than the per-cell tag sweep justifies (interval 8 measured 1.39x).

Also reports per-slot memory DEMAND at init rather than guessing supply - there is no portable way to query free device memory across four compilers and three offload backends, so it prints what a slot costs and lets the user compare.

Gated on the full suite: 706 passed, 0 failed. precheck clean.
…rebalancing does not pay

s_l0_rebalance computed the post-cut load gap and then migrated without ever comparing it to the pre-cut gap. Because the SFC re-cut is restricted to CONTIGUOUS Morton ranges, its finest correction is one whole tile - 25% of a rank's load at 4 tiles/rank - which exceeds the 5% deadband it is chasing, so it can only overshoot. Measured: 5.738E-02 -> 3.898E-01, a 6.8x WORSE partition that never recovered. Now evaluated into a temporary and committed only if the gap does not worsen.

Rejects a strict worsening rather than accepting only a strict improvement: with near-uniform tile costs the Morton and cartesian partitions have EQUAL gaps, and rejecting those would silently remove the migration that golden A0968B15 exists to cover - it would still pass, since migration is bit-neutral, so the coverage loss would be invisible.

amr_owner_cut moves with newo or not at all: f_amr_owner resolves tile ownership against it, so refreshing the cut without migrating would leave it disagreeing with amr_block_owner.

The rebalance print now includes the deadband. Without it, 'deadband skipped the re-cut' and 'the re-cut ran and was rejected' emit byte-identical output, and the guard cannot be shown to execute. Verified by forcing the deadband to zero: 27 evaluations (9 windows x 3 reps), re-cut reached every time (deadband 0.000E+00 in-log), rejected every time, no gap ever worsened.

Docs record OPEN 1's answer. On a purpose-built imbalanced case (blob IC, refinement concentrated in one rank's half, fine_work 2097152 vs 206016) L0 tiling costs 35% and rebalancing recovers 0.4% of it, inside run-to-run spread, reproduced across three independent jobs. OPEN 2 is demoted as a consequence. Scope limit recorded: 2D, single refinement level, hydro with uniform per-cell coarse cost.

Also reconciles amr_block_batching.md: two headlines still named the packed super-grid as the live lever after it was disproved, and the cost law is confirmed with AMR active (time tracks TOTAL block advances, 3% at the largest point).

12/12 L0/AMR goldens pass. precheck clean.
…ting it is blocked

Attributing 337,431 device copies to 16,698 dispatches (rocprofv3, temporal attribution): kernels taking fields as DUMMY ARGUMENTS pay 33-54 argument maps per dispatch, while kernels reading module-level GPU_DECLARE'd state (m_amr_registers freg/creg) pay EXACTLY ZERO. Copy count matches the deep-member count (3*sys_size + q_T_sf + bc_type = 20 at sys_size=5; measured 20.2 per dispatch). Total copy time 2.0 s against ~1.5 s of kernel time. A 4-translation-unit reproducer isolates the penalty at 4.3-4.7x per region.

This kills two plausible plans: flattening the scalar_field interface (3,840 sites across common/) buys nothing, since flat-array dummies measure the same as deep-allocatable ones; and batching attacks dispatch COUNT while the tax is per-dispatch argument mapping.

Converting s_amr_fine_rk_update to module-level views was attempted and reverted at 74/76 goldens. A control experiment isolates the cause: keeping the attach calls but reverting the kernel to read its dummy arguments fails byte-identically, so the kernel is innocent and the ATTACH ITSELF corrupts the slot's device data. Neither available clause is a correct attach on this backend - map(always,to:) copies the stale host array over live device state, map(always,alloc:) forces a fresh uninitialised allocation. A device-side probe comparing view against dummy reads zero difference because both resolve to the same WRONG buffer.

A 6-phase standalone reproducer (attach-only, attach+detach, slot recycling, varying extents, realloc-at-new-extent) passes every phase, so this is an integration property rather than a property of the technique.

No source change: GPU_ENTER_DATA(attach=) has 8 existing users in m_rhs.fpp/m_igr.fpp that depend on its current expansion, so it cannot be changed without a validated replacement primitive. Records the next step - establish an attach that neither copies nor reallocates on present data, and validate against golden 00EB793A specifically.

Docs only; src unchanged. precheck clean.
…ule scope

The attach hunt was solving the wrong problem. The per-dispatch tax is not that pointers are slow, it is that fields arrive as DUMMY ARGUMENTS, so every target region re-maps their deep members. Nothing needs aliasing, attaching, or flattening - the fields need to BE module state. m_rhs already declares ~12 module-scope GPU_DECLARE'd derived-type arrays whose kernels measure zero maps per dispatch; only q_cons_vf/q_prim_vf/rhs_vf arrive as dummies, and they are exactly the taxed ones.

Promoting a dummy to a module variable of the SAME NAME leaves every loop body untouched - only declarations and call sites change. That is what makes it tractable and what makes it safe under a bit-identical requirement, in contrast to the flat-store alternative which threads a slot index through every reference.

The monolithic path then pays nothing (the module arrays are its arrays); the AMR path copies a block's fields in before the advance and rhs back after, device-to-device: ~1.3 us against the measured ~2.9 ms of argument mapping per s_compute_rhs invocation.

Re-entrancy verified, the one risk that could have killed it: s_amr_advance_children is recursive, but the recursion sits OUTSIDE the stage loop - every level-clev block completes all three stages before the routine recurses into clev+1 - and the L0/fine advance loops are serial over blocks. No nesting, so no working-set stack is needed.

Records four de-risking MWEs to be built as ONE matrix per the lesson that a 6-phase matrix compiled in ~20 s eliminated four hypotheses at once and predicted an MFC failure, three golden-gated phases, and the conditions that would falsify the plan.

Docs only; src unchanged. precheck clean.
…5, timing not measurable today

Stress matrix in amr-bench/attach/promote/: promotion through a 3-deep cross-TU call chain over a sub-range, poisoned halo verified untouched, 25 advances with host/device deliberately diverged plus present-table pressure, 48 blocks of varying extents through one shared working set, and mid-run reallocation. All five pass.

Mechanism detail for implementation: a module-scope deep type needs BOTH the parent descriptor and its members mapped - map(to:) on the parent alone leaves %sf null and the first kernel faults at nil. With promotion that setup runs once at init and the working set is never re-pointed, which is why this design sidesteps the attach problem that killed the view-based one.

No speedup is claimed: the node showed a ~134 us per-region launch floor uniform across all 8 GCDs with the GPUs idle, versus ~20 us for the same construct earlier the same day, so a ~53 us mapping delta is inside the floor. MFC's own profile implies ~2-3x per region; the earlier 4.4x is an upper bound pending an idle node.
… on this compiler

The premise was an attribution artifact. 'Dummy kernels pay 33-54 maps, module-state kernels pay 0' came from temporal attribution (correlation ids do not match across the two traces). Copies cluster: 11.5% of inter-dispatch intervals hold zero copies and one holds 2893, so a kernel dispatched in a burst reads as 0 regardless of need. The median of 20 per interval matches the deep-member count, so the ~20 maps/dispatch average stands - the per-kernel split does not.

Every mechanism then measured directly by copy count, one variant per process under rocprofv3, needing no attribution and immune to the launch-time floor: deep-type dummies 9.4, flat-array dummies 9.4, pointer dummies 9.1, defaultmap(present:aggregate) 9.1, map(present,alloc:) 9.1, declare mapper 39.1 (4x worse), module state in every form including the exact freg pattern 32.8 (3.5x worse).

Per-region map traffic is irreducible by interface or clause on this compiler, and module scope makes it worse. That retires promotion, interface flattening, declare mapper, and the view/attach design together.

What survives: the tax is real (2.0 s of copies against ~1.5 s of kernels), but it can only be attacked by fewer block advances (amr_max_grid_size, ~20x, already shipped) or fewer regions per advance (kernel fusion, untried).

Docs only; src unchanged. precheck clean.
BASE_CFG hardcodes precision = 2 (double post_process output), which case_validator rightly prohibits on a --single build. Every post_process case on the single-precision CI lane has been failing this validation invisibly -- the suite discarded post's exit code until 68bcaf1 -- and surfaced as 'Case validation failed for post_process' across the lane once the check landed. get_post_process_mods now requests precision = 1 when the build is single. Post output files are not golden-compared, and the lane's post step never succeeded before, so no goldens move.
…it-identical

The min-separation merge restarted an all-pairs scan after every fusion and removed the absorbed box by an O(n) shift; at np=128 the gathered list is ~12,400 leaves collapsing through ~11,300 fusions per call, and rg:clus grows to 75% of wall at np=512. Survivors now live on a doubly-linked list (O(1) unlink) and each pass finds its fusion through a uniform bin grid over the LIVE boxes' bounding box, width max(ext_max + thr, clamped so nb_tot stays O(n)) -- sound because a tooclose pair is within ext_max + thr - 1 per dimension and enlarging cellw only widens the candidate set; bit-identical because for each i in list order the minimum surviving candidate index equals the first tooclose j the linear walk meets. Also fixes: the nacc == 0 walk read an unwritten nxt(1); the [amr-merge] fusion counter was declared and printed but never incremented (its 'zero fusions' reading was an artifact); and this rebuild restores the truncation warning, amr_n_shaped accumulation and grideff 'shaped' field that the first version of this commit reverted through a stale-base file copy (caught in review). Measured on mi2104x pinned-binary A/B: rg:clus 138.6->83.8 (np32), 1074.9->335.7 (np64), 8462.1->1076.1 ms/call (np128).
s_amr_st_reserve chose device-native vs host-staged growth by COLUMN COUNT (32), but a column is ~33 MB at the default 64^3 block cap and ~1.8 GB on the 57.3 GiB run that motivated the guard -- the same count means wildly different bytes. The store-capacity ratchet pushes production runs to 43-81 columns, sending every later growth on the full-store host PCIe round trip (measured 4.4 s/regrid vs 0.55 under the guard). The guard now budgets the transient itself: stage on-device while the extra copy (oldcap * column bytes, which is exactly the store size regardless of factorization) stays under 4 GiB, so near-limit stores of ANY column shape still take the OOM-safe host path. Review-noted exposure on 16 GB cards is recorded at the constant; the registers site keeps its count guard (per-array transient, two orders smaller) with its stale cross-reference corrected.
The previous fix (4d5f6ee) patched get_post_process_mods, but only coverage_build.py calls that function -- the test suite embeds POST_PROCESS_OUTPUT_PARAMS directly into the generated case program, so the single-precision CI lane kept failing every post_process validation unchanged. The guard now lives in the generated program itself, keyed on ARGS['mfc']['single'], which run/input.py already serializes. Validated end-to-end by regenerating a case through the harness and executing it both ways: single=true yields precision 1, single=false yields 2. The earlier fix was committed validated only by construction; its local check had failed on environment and was not repeated -- the same never-observed-effect class as the dead counters.
…(47)

glob_bytes counted 18 metadata ints per block (72 B) and ignored the amr_slots struct array, whose descriptors dominate at ~1 kB/slot -- a 10-20x under-report that silently propped up the metadata-distribution-deferred decision and would have steered the production-size rung. Ledger entry 47 records the landing, the toolchain-lock incident, and the forensic corrections.
… verdicts

The [amr-mig] counters counted only blocks rank 0 itself sends and printed them unreduced -- the zero-cost audit read a frozen 141 MB startup transient at every P off what looked like machine-wide migration. Now SUM-reduced across ranks (ml_g, mirroring the [amr-halo] reduce just above; rank0_bytes kept for cross-run continuity). Ledger 48 records: the rhs growth above np128 is ranks-per-node density (memory-bandwidth contention), not P, pending the pre-registered np1024 confirmation; the dirty-box payoff is 4/np independent of bin occupancy (127x at np512); ladder migration is a frozen startup transient so keep-tol requires a moving-feature deck; escaped is 0 through np512; and the memory probe measured 201 MB/rank replicated metadata plus a ~3.3 GB/rank fixed footprint that OOMs dense packings.
After a fusion only the surviving box changed, so instead of restarting the whole pass the merge now (a) re-tests earlier survivors against the grown box (minimum index first, which is provably what the restart would find), else (b) re-tests later survivors, else resumes at the survivor's live successor. Bins are built once per cellw epoch and maintained incrementally (absorbed boxes unlinked, the survivor re-filed when its lo crosses a bin; extent growth doubles cellw and rebuilds, amortized log). Sequence identity was proven by a differential control (amr-bench/tools/merge_ctl.f90) against the retained restart implementation: identical fusion sequences on directed + random suites spanning 864 back-fusion chains; mutation skip-(a) diverges on 57 cases proving the control can fail; stale-resume is proven benign by containment + (b)-exhaustion; the survivor refile is kept as O(1) insurance with the Morton-order masking argument documented in the control. The [amr-merge] mean_outer_pos field is renamed mean_visit because its semantics change with the restart's death (per-pass scan depth -> monotone visit index) -- old cbar formulas must not be applied to new logs. Expected from the counter laws (F ~ 90 np, mop = np/2): pair tests fall ~mop/2-fold, two orders of magnitude at np512, growing with P. Gates: CPU 9/9 bit-identical with counters exactly matching the restart implementation (pair_tests 496/fusions 31 and 171/18 on the churn control); GPU 66/66.
… NaN)

ab11150 put the same scratch arrays in both private() and map(alloc:) on one combined OMP construct in the HLLC and WENO kernels -- non-conforming OpenMP that its own commit message flagged as unverified on the other CI compilers. nvfortran resolves the conflict map-wins: cross-compiled PTX shows every scratch-array store with no thread index in the address chain, i.e. all threads share one vel_L etc., and the resulting races produce the deterministic first-step 'NaN(s) in timestep output' that has held the Phoenix NVHPC gpu-omp lane red on basic 1D tests since Aug 13 (lane green Aug 2; master unaffected -- it never carried the clause). amdflang resolves it private-wins, which is why the local gate could never see it, and an explicit conforming split (map on an enclosing target data region) breaks amdflang instead -- no conforming formulation reproduces its fast path. Measured tonight, interleaved 3x3 on the production case: the hack is worth 6.3 +/- 3% on amdflang (not the 26.4% recorded at introduction; the sequential-order measurement that suggested 12.6% was drift-confounded). Decision: pay the ~6% for conforming code on all four compilers; block-local scratch (option B) can recover it later as pure performance work.
…gesort

The Morton sort ran an insertion sort over R concatenated per-rank runs -- O(n^2) in inversions, measured growing 5.1x over the first rank doubling (2.2x the next), of the same order as the merge residual beside it. Now a stable bottom-up mergesort on an index permutation with the payload gathered once at the end. Stability at key ties (fall back to acceptance order) is load-bearing for B1: proven by a differential control (amr-bench/tools/sort_ctl.f90, verdict saved in logs/sort_ctl_final.log -- identical orders on tie-heavy suites, an unstable mutation diverges on all 48 cases), and ties cannot occur in production anyway since accepted boxes have distinct lo corners. Sort-then-gather equals sort-with-payload because the stable order is unique (independently audited, including the gather-vs-scatter direction). Block-locals renamed msw/mslo/msmid/mshi to avoid shadowing the host's BR-stack arrays. Gates: CPU 9/9 with all 20 output files byte-identical to the pre-change binary; GPU gate follows.
Per-site XOR folds of mix(unordered pair id, per-channel sequence, block/dim/face key), BXOR-reduced at finalize and compared BY FAMILY (sends and receives of the same traffic live under paired audit sites -- a per-site compare mismatches structurally, found by exactly that false positive). Under the FIFO order contract the sender's and receiver's triples coincide message-for-message so the family hashes cancel; any cross-rank ordering divergence misaligns key against sequence and the finalize check aborts with the family named. The mixing hash is load-bearing: a raw packed-field XOR is provably blind to pairwise transpositions -- the property control (amr-bench/tools/oracle_ctl.f90, verdict saved in logs/oracle_ctl_final.log) demonstrates raw-XOR blind on 50/50 transposition trials and the splitmix64 fold catching 50/50. Wired at the eight F5 wave sites (faces + freg, lo/hi distinguished in the key, loop-scope of every peer/block variable verified); other sites opt in incrementally via the new optional peer/key arguments. A seeded canary (MFC_XA_SEED=1 corrupts exactly one fold, latched -- an XOR accumulator can return to zero, so testing it would allow self-cancelling double fires) proves the wiring can fail: it aborts fatally with family 5 named. The per-(site,dir,peer) sequence counters are O(P) per rank -- accepted for M0, replaced by plan-derived sequences at M1. Gates: healthy churn np=2 silent; canary aborts; 9/9 subset.
…ecv-plan scan

The stage-fill wave rebuilt its coarse-owner receive plan by scanning every block in the machine at every RK stage -- at 1e5 ranks x 75 boxes that is ~360 M predicate evaluations per rank per step to find ~75 survivors. The surviving set is O(local) (level + not-mine + interior-overlap are all regrid-invariant), so it is now cached in amr_l1r_blk, rebuilt lazily when amr_mesh_epoch moves (NOT the owner-only dirty flag, which regrid and restart never set). List order is ascending block id, identical to the scan it replaces, so the wave's message order is unchanged -- verified live by the M0 order oracle (silent on the converted wave; the seeded canary still aborts). Resurrected from the never-landed stash pilot, adjudicated against the W1 conversion recipe: this site carries no safety calls between loop head and filters (the s_amr_check_lag_clear trap lives in other routines). Gates: 9/9 goldens, oracle silent at np=2, reldebug bounds arm clean. First of the ten per-stage scan conversions -- the 8.5-12% O(P)-growing family behind the weak-scaling gap.
Three more per-stage global scans converted (4 of 10 done). The stage-fill gather scan is the recipe's documented trap site: s_amr_check_lag_clear ran for EVERY level-1 block above the owner filter, so it keeps a dedicated scan gated on bubbles_lagrange (coverage unchanged, cost only in that configuration) while the gather loop walks a NEW padded list -- the same epoch-keyed walk now builds both flavors: raw region vs my interior (amr_l1r, the batch-1 list) and region +/- amr_cpat_mar vs my coarse range (amr_l1p, this loop's exact predicate); the body keeps its own intersection and empty-cycle as belt-and-braces. The two owner-side scans (stage-fill send prep, reflux-faces owner side) walk amr_my_blk with the level filter kept (the list carries all owned levels) and the ownership test retained defensively. List orders are ascending block id, identical to the scans replaced -- wave message order unchanged, verified by the M0 oracle (silent at np=2). Gates: 9/9 goldens, oracle silent, reldebug arm zero violations.
reflux-recv rides amr_l1p (the participates predicate is provably inside the padded-list predicate); freg recv, parent-fill send, and restrict-parent recv share the new amr_fch_blk list (level>=2, my parent, foreign child); freg send walks amr_my_blk; the parent-fill consume loop gets its own amr_own_blk list because it tests the MULTI-owner amr_owns_all notion, which amr_my_blk would silently narrow. The bigger find: f_amr_parent_block is itself an O(global blocks) scan and these waves called it per block per RK stage, making them quadratic in the global block count -- it is now cached per mesh epoch in the same builder walk, alongside a children adjacency (CSR) that replaces the identical hidden scan inside s_amr_sibling_face_weights. The lag guard keeps its full-coverage scan, gated on bubbles_lagrange. Survivor sets and ascending order are preserved at every site (exact predicates kept in bodies), so MPI posting order is unchanged by construction. Independently audited: the audit added the orphan-parent query guard (an unguarded amr_child_ptr(-1) would have turned a broken mesh into silent weight corruption), removed a re-entrant list refresh from inside a loop over that list, and keyed the cache on (epoch, num_blocks). Gates: 69/69 + 14/14 GPU goldens (isolated worktree), multi-level np=2 order oracle silent with all families balanced and the F2 traffic proven per-stage by 10-vs-20-step differencing (20 vs 51 msgs, 3 per marginal step = one per RK stage), seeded canary aborts as required, reldebug clean.
Every message in the reflux-faces and freg waves (payloads and the debug identity headers, both directions) now carries its own tag: base + band*2^16 + gen*2^12 + seq, with the band space starting at the next 65536 boundary above every legacy tag and asserted against MPI_TAG_UB at init. gen (mod 16) bumps at wave entry on every rank -- both call sites are rank-unconditional, and the subcycle path skips them on all ranks together -- separating successive waves that share a band. seq is the message's position in the pair's canonically ordered transfer list (ascending block id, then dim, lo before hi), derived independently by each end from the same replicated metadata, so message matching no longer depends on posting order or MPI non-overtaking; the in-wave per-peer counters reset touched entries only (O(active peers), never O(P)). The audit's xa_seq array -- the order oracle's per-peer O(P) interim state, 368 B x P per rank -- is deleted: call sites pass the plan-derived seq and the oracle folds it directly. New seeded gate MFC_XA_SEED=2 shifts one plan seq on rank 0's send side, modeling a sender deriving a different plan order; the finalize oracle must abort. Gates: family counts byte-identical to the pre-M1 baseline (130/51/6/32/120/16 on the multi-level np=2 oracle deck), seeds 1 and 2 both abort, reldebug clean, 16/16 goldens at np=1/2/4 including wide-L2 and force-migrated.
…W1 wall-neutral at this scale; rs:rfp claim retracted
CCE 19.0.0 cannot codegen the HLL kernel for benchmarks/viscous_weno5_sgb_acoustic:
llc asserts in StackSlotColoring (LiveInterval.cpp:391, "empty range"). That case
runs HLLC, so the kernel it dies on is one the case can never call. The Case Opt |
Frontier (CCE) (gpu-acc) lane has never passed on this branch.

Case optimization now bakes riemann_solver in and guards each solver body and its
dispatch arm on it, so an unreachable solver is never emitted. riemann_solver = -1
means the case did not pin one, and every solver is compiled as before.

Non-case-optimized builds are unchanged: all four solvers, all four dispatch arms.

The alternative, -Oipa0 on m_riemann_solver_hll.fpp.f90, also cleared the crash but
cost 14.4% on HLL cases (hypo_hll, 3 reps, gbpp 4: 1.2174 -> 1.3928 ns/gp/eq/rhs,
non-overlapping ranges) and applied to every Cray GPU build.

Verified on Frontier: the crashing case builds clean, and both it and hypo_hll pass
check_case_optimization_output.py. The large diff is ffmt reindenting the guarded
bodies (sbryngelson/ffmt#9); git diff -w is far smaller.

Claude-Session: https://claude.ai/code/session_01XZjDp6ch827LVJRrvVb8hC
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Lines of Code

File Lines Diff
src/simulation/m_amr.fpp 7338 +7338
src/simulation/m_amr_regrid.fpp 1938 +1938
src/simulation/m_amr_registers.fpp 1021 +1021
src/simulation/m_amr_restart.fpp 383 +383
src/post_process/m_data_input.f90 718 +310
src/simulation/m_amr_xchg_audit.fpp 205 +205
src/simulation/m_ibm.fpp 1449 +200
src/simulation/m_sfc_partition.fpp 169 +169
src/simulation/m_phase_timing.fpp 167 +167
src/simulation/m_load_balance.fpp 164 +164
src/simulation/m_time_steppers.fpp 985 +157
src/post_process/m_data_output.fpp 1336 +146
src/simulation/m_load_weight.fpp 121 +121
src/simulation/m_active_box.fpp 118 +118
src/common/m_box.fpp 71 +71
src/simulation/m_rhs.fpp 2022 +69
src/simulation/m_global_parameters.fpp 813 +66
src/simulation/m_start_up.fpp 1303 +59
src/simulation/m_rank_timing.fpp 50 +50
src/simulation/m_data_output.fpp 1363 +46
src/common/m_mpi_common.fpp 1515 +41
src/simulation/m_acoustic_src.fpp 557 +36
src/simulation/m_riemann_solver_lf.fpp 574 +33
src/simulation/m_riemann_solvers.fpp 139 +27
src/common/m_phase_change.fpp 299 +22
src/common/include/2dHardcodedIC.fpp 453 +21
src/simulation/m_bubbles_EL.fpp 1676 +17
src/simulation/m_riemann_solver_hllc.fpp 1289 +17
src/simulation/m_checker.fpp 84 +14
src/common/include/macros.fpp 141 +13
src/simulation/m_weno.fpp 1350 +10
src/post_process/p_main.fpp 62 +7
src/simulation/m_hypoelastic.fpp 597 +7
src/simulation/m_igr.fpp 2342 +7
src/common/m_constants.fpp 91 +6
src/simulation/m_riemann_solver_hll.fpp 586 +5
src/common/m_derived_types.fpp 443 +4
src/common/include/3dHardcodedIC.fpp 243 +3
src/simulation/m_surface_tension.fpp 243 +3
src/post_process/m_start_up.fpp 770 +2
src/simulation/m_cbc.fpp 1122 -2
src/common/m_boundary_common.fpp 449 +1
src/post_process/m_global_parameters.fpp 358 +1
src/simulation/m_bubbles_EE.fpp 317 +1
src/simulation/m_riemann_solver_hlld.fpp 202 -1
src/simulation/m_thinc.fpp 313 +1
Directory Lines Diff
common 9960 +182
simulation 40464 +12446
post_process 3803 +466
total 58723 +13094

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