Skip to content

Block-structured adaptive mesh refinement (AMR) - #2117

Draft
sbryngelson wants to merge 27 commits into
MFlowCode:masterfrom
sbryngelson:amr
Draft

sbryngelson wants to merge 27 commits into
MFlowCode:masterfrom
sbryngelson:amr

Conversation

@sbryngelson

@sbryngelson sbryngelson commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Developed with AI assistance (Claude Code). Results were run on OLCF Frontier (MI250X) and an NVIDIA A100 node; commands
are listed so each number can be reproduced.

Stacked on #2116 (solver performance) and #2118 (small fixes). Until they merge this diff includes their commits;
the AMR changes are the last 8 commits.

Contribution Policy

All contributions are expected to demonstrate:

  • A clear understanding of the codebase
  • Alignment with product direction
  • Thoughtful reasoning behind changes
  • Evidence of real-world usage or hands-on experience with the problem

Summary

Adds block-structured adaptive mesh refinement to simulation, off by default (amr = F). With AMR off, results are
those of #2116 (every existing golden passes unchanged).

  • Refinement: density-gradient tagging, Berger-Rigoutsos clustering into blocks of at most amr_max_grid_size
    cells, refinement ratio 2 (or 4 for one level), up to amr_max_level properly nested levels, regrid every
    amr_regrid_int steps.
  • Time stepping: lock-step SSP-RK3, or subcycling (amr_subcycle, two fine steps per coarse step, up to two refined
    levels) with time-interpolated ghosts. Flux registers reflux every coarse/fine face, so the scheme is conservative.
  • Transfer: conservative limited-linear prolongation, or quartic (amr_prolong = 2) with a fallback to linear
    wherever the quartic state would have non-positive density or internal energy; conservative restriction.
  • Parallel: each block is owned whole by one rank (cost-weighted space-filling-curve cut, or owner-aware placement
    with amr_place_owner). Exchanges are nonblocking waves with keyed tags, so results do not depend on message timing.
  • GPU: block data is device-resident; blocks of a level advance in batches through one solver call.
  • Physics under AMR: 5- and 6-equation models, multi-fluid, viscous, hypoelastic, body forces, chemistry (lock-step),
    Lagrangian bubbles (lock-step, cloud on the base grid), acoustic sources (base grid), 1D MHD, IGR (single rank), and
    immersed boundaries (fixed, prescribed or force-driven motion).
  • I/O: restart (serial and MPI-IO) and post-process block overlays.
  • Docs: docs/documentation/amr.md (user guide), amr_implementation.md (internals), one AMR section in case.md.

The solver runs on a block by swapping its grid globals (m, n, p, index ranges, coordinates) in and out;
amr_implementation.md states the contract for module state that depends on the grid. AMR validation fails closed: with
amr = T the validator rejects any logical switch not in AMR_VETTED_FLAGS, so new physics is refused until it meets that
contract. Explicitly rejected with a message: model_eqns = 1, Euler-Euler bubbles, QBMM, phase change, surface tension,
synthetic turbulence, cylindrical or stretched grids, bc = -4 with null_weights; subcycling with IGR, chemistry,
Lagrangian bubbles or moving bodies.

Verification

Regression

Lane Result Commit
Frontier, amdflang OpenMP (-c frontier_amd, 4 shards) 786/786 final tree
Build without MPI (--no-mpi) builds final tree
NVHPC 25.11 OpenACC, 4x A100 786/786 before the review refactors; rerun pending
NVHPC 25.11 OpenMP offload, build every test variant builds before the review refactors; rerun pending
Frontier CCE OpenACC and OpenMP rerun pending

CCE found two local allocatable arrays named in kernel data clauses that its OpenMP runtime looked up in the present table
anyway; both are now automatic arrays.

New goldens: the AMR cases (52 tests: multi-rank, multi-level, subcycling, restart, IB), quartic prolongation (1D, and 2D
with IB), and rank_time_wrt diagnostics. No existing golden changes.

Accuracy and conservation

1D periodic Euler, WENO5, SSP-RK3, refinement ratio 2:

N AMR off static block (middle half) regrid every 4 steps
64 3.14e-7 5.43e-5 5.71e-5
128 2.80e-8 6.94e-6 6.94e-6
256 3.15e-9 8.81e-7 8.81e-7
512 3.82e-10 1.11e-7 1.11e-7
order 3.5, 3.2, 3.0 2.97, 2.98, 2.99 3.04, 2.98, 2.99

L1 density error after one period. Third order is kept; the error constant is larger because the coarse/fine ghost fill is
second order. Total mass, momentum and energy change by 1.4e-16, 4.2e-16 and 1.9e-16 (relative) over 200 steps with regrid
every 4 steps. The same run on 1 and 4 ranks is bitwise identical.

Performance (weak scaling, blast lattice, 8M base cells per GPU, one refined level, subcycled)

s/step (efficiency vs 8 GPUs):

GPUs MFC AMReX CNS PeleC
8 0.674 (1.00) 0.467 (1.00) 0.600 (1.00)
64 0.699 (0.96) 0.600 (0.78) 0.802 (0.75)
128 0.716 (0.94) 0.613 (0.76) 0.832 (0.72)
512 0.775 (0.87) 0.678 (0.69) 0.952 (0.63)
1024 0.944 (0.71) 0.712 (0.66) 0.994 (0.60)

MFC refines about 1.3x more volume than AMReX and PeleC here (40.5% vs 30.3%). The table is from an earlier commit of this
work; the 64-GPU rung rerun before the final cleanup gave 0.680 s/step against 0.698 for that commit, and a rerun on the
final tree is pending. Regrid cost grows faster than the step beyond 512 GPUs; that is a follow-up.

Use

Mach 2.5 shock through 297 fixed random spheres: 384 x 256 x 256 base grid plus one level, 128 GPUs, 1350 steps, up to
about 5300 blocks.

Known limitations and follow-ups

  • Regrid keeps the current blocks when no cell is tagged, so refinement does not retract until something is tagged again.

  • Level-2 and deeper blocks are re-prolonged from their parent at every regrid (only level-1 data is carried over or
    migrated), which costs accuracy in multi-level runs that regrid often.

  • The AMR restart header records the refinement ratio, maximum level and word size and is checked on read.

  • Regrid cost above 512 GPUs.

  • The solver reads grid state from module globals; AMR swaps them per block. Passing the grid explicitly would remove
    the swap but touches the whole solver.

  • m_amr_state holds many module variables; a block-table derived type would be cleaner.

…ecking, Open MPI oversubscribe, stage-guard lint, validator fixes

format.sh treats ffmt's unmatched-structure warnings as fatal; Cray debug builds get -h bounds like the other compilers;
the run template passes --oversubscribe to Open MPI when ranks exceed cores; a source lint rejects per-executable
preprocessor guards; the validator derives cfl_dt like the Fortran, rejects MHD with Euler bubbles and non-positive
rburn%pign.
On amdflang OpenMP offload a private local array is mapped tofrom on every launch; privateArrays maps it alloc instead
(OpenACC merges it into private). Opt-in present:allocatable per source file.
…ilds compile only the selected Riemann solver

Stiffened-gas builds drop the state-dependent EOS chain from every kernel. CI builds a third simulation variant for the
Mie-Gruneisen, JWL and Vinet cases, and a slug check guards the case-optimization pre-build.
…L/HLLC/LF/HLLD, copy primitives only for readers

The Riemann solvers write module flat buffers that CBC, QBMM, surface tension, conduction and chemistry read directly
(flux_n and flux_gsrc_n are gone). The reconstruction input is packed once per RHS. HLLC face work moves to routines with
explicit-shape arrays. q_prim_vf is copied out only for its readers (probes, IB, Lagrangian bubbles, synthetic turbulence,
run_time_info); hyper_cleaning's psi is always copied. Results are unchanged: every master golden passes.
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Claude Code Review

Head SHA: f08f835

Files changed:

  • 80+ (AMR feature PR; diff is about 26k lines)
  • .github/scripts/check_caseopt_shared_slugs.py
  • .github/scripts/prebuild-case-optimization.sh
  • .github/scripts/submit-slurm-job.sh
  • .github/workflows/common/build.sh
  • src/common/m_eos.fpp
  • src/common/m_global_parameters_common.fpp
  • src/common/include/omp_macros.fpp
  • src/common/include/macros.fpp
  • toolchain/mfc/case.py

Findings:

  • medium .github/scripts/submit-slurm-job.sh: The Phoenix GPU job now requests --ntasks-per-node=8 while -G${gpu_count} is unchanged. If gpu_count is below 8, 8 ranks share fewer GPUs on every GPU test shard, not just the AMR tests the comment cites. I could not tell from the diff whether this is intended. Check that the oversubscription is acceptable, or raise the task count only for the jobs that need 8 slots. Confidence: low-medium.
  • medium src/common/m_eos.fpp: any_state_dependent_eos is now an Fypp constant in every build, and the @:PROHIBIT runs in all builds. A plain ./mfc.sh build or ./mfc.sh run flow that reuses a binary built from an empty case (eos_state_dependent=False) aborts at startup when the case uses Mie-Gruneisen, JWL or Vinet. This is by design, since cmake/Fypp.cmake hard-codes False and case.py bakes the real value into the slug. It does mean any path that bypasses the per-case slug, for example prebuilt or shared staging dirs, now fails at runtime. check_caseopt_shared_slugs.py covers only the case-optimization benchmarks. The new eos variant in build.sh covers only the test suite. Confidence: medium-low.
  • medium src/common/include/omp_macros.fpp: For amdflang, OMP_PARALLEL_LOOP now always appends OMP_MAP_STR('alloc', privateArrays) to default_val. With privateArrays=None, this depends on OMP_MAP_STR returning an empty string for None. I did not verify that in the diff. Also, default_val.strip('\n') can join two clauses without a separating space. The AMD default is '' or 'defaultmap(present:allocatable) ', so the trailing space survives only if strip is on newlines. Confirm the generated directive text on the AMD path. Confidence: low.

Not reviewed in depth: the bulk of the src/simulation/m_amr*.fpp modules (about 20k lines) and the Riemann solver hunks. No line-level findings come from those.

…RALLEL_LOOP takes privateArrays; document MFC_OMP_PRESENT_ALLOCATABLE
…ents, unused s_lag_cloud_bbox_local and WENO public entry; fix WENO stencil comment
…eholder allocations and buffer zeroing out in place
…guards; emit privateArrays as its own private clause instead of a list-joining helper
…rmat on ffmt structure warnings; Cray debug bounds checking; Open MPI oversubscribe; stage-guard lint; validator fixes

bc_type is allocated (1:num_dims, 1:2) but simulation's m_data_output declared it (1:num_dims, -1:1). Serial readers need
bc_type.dat and bc_buffers.dat in every step directory. format.sh treats ffmt's unmatched-structure warnings as fatal; Cray
debug builds get -h bounds like the other compilers; the run template passes --oversubscribe to Open MPI when ranks exceed
cores; a source lint rejects per-executable preprocessor guards; the validator derives cfl_dt like the Fortran and rejects
MHD with Euler bubbles and a non-positive rburn%pign.
…iction/reflux, clustering, nesting, regrid, subcycling, restart)
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Lines of Code

File Lines Diff
src/simulation/m_amr_exchange.fpp 1273 +1273
src/simulation/m_amr_regrid.fpp 918 +918
src/simulation/m_amr_transfer.fpp 827 +827
src/simulation/m_amr_cluster.fpp 585 +585
src/simulation/m_amr_distribution.fpp 552 +552
src/simulation/m_amr_registers.fpp 536 +536
src/simulation/m_amr_store.fpp 478 +478
src/simulation/m_amr_nest.fpp 430 +430
src/simulation/m_amr_state.fpp 381 +381
src/simulation/m_ibm.fpp 2195 +350
src/simulation/m_amr_restart.fpp 303 +303
src/simulation/m_amr_frame.fpp 232 +232
src/simulation/m_amr_advance.fpp 227 +227
src/simulation/m_amr_wave.fpp 227 +227
src/post_process/m_data_input.f90 660 +221
src/simulation/m_amr.fpp 209 +209
src/simulation/m_amr_stage.fpp 195 +195
src/post_process/m_data_output.fpp 1412 +155
src/common/m_amr_restart_io.fpp 114 +114
src/simulation/m_phase_timing.fpp 91 +91
src/simulation/m_rhs.fpp 2060 +89
src/simulation/m_global_parameters.fpp 859 +80
src/common/m_box.fpp 39 +39
src/simulation/m_acoustic_src.fpp 551 +35
src/simulation/m_riemann_solver_lf.fpp 553 +32
src/simulation/m_riemann_solvers.fpp 141 +29
src/simulation/m_start_up.fpp 1300 +29
src/simulation/m_riemann_solver_hllc.fpp 1336 +28
src/simulation/m_igr.fpp 2316 +27
src/common/m_mpi_common.fpp 1517 +23
src/simulation/m_weno.fpp 1348 +22
src/common/include/2dHardcodedIC.fpp 453 +21
src/simulation/m_body_forces.fpp 415 +21
src/simulation/m_bubbles_EL.fpp 1656 +17
src/common/include/macros.fpp 141 +13
src/common/include/omp_macros.fpp 391 +11
src/post_process/p_main.fpp 61 +7
src/simulation/m_checker.fpp 76 +7
src/simulation/m_riemann_solver_hlld.fpp 199 +7
src/simulation/m_time_steppers.fpp 903 -7
src/simulation/m_hypoelastic.fpp 640 +6
src/simulation/m_riemann_state.fpp 1182 -6
src/common/m_boundary_common.fpp 453 +5
src/common/m_eos.fpp 520 -5
src/common/m_helper_basic.fpp 123 +5
src/common/m_constants.fpp 93 +4
src/common/m_derived_types.fpp 488 +4
src/common/include/3dHardcodedIC.fpp 250 +3
src/common/m_boundary_io.fpp 319 -3
src/simulation/m_riemann_solver_hll.fpp 624 +3
src/simulation/m_surface_tension.fpp 266 +3
src/common/m_global_parameters_common.fpp 248 -2
src/common/m_variables_conversion.fpp 968 +2
src/post_process/m_start_up.fpp 794 +2
src/simulation/m_cbc.fpp 1112 -2
src/simulation/m_data_output.fpp 1581 +2
src/simulation/m_muscl.fpp 189 +2
src/simulation/m_riemann_solver_hypo_hlld.fpp 785 +2
src/common/include/acc_macros.fpp 283 +1
src/common/include/parallel_macros.fpp 252 +1
src/post_process/m_global_parameters.fpp 400 +1
src/pre_process/m_data_output.fpp 677 +1
src/simulation/m_bubbles_EE.fpp 305 +1
src/simulation/m_thinc.fpp 313 +1
Directory Lines Diff
common 10780 +236
pre_process 5023 +1
simulation 36822 +8242
post_process 3870 +386
total 56495 +8865

This branch has not been deployed

No deployments
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.

1 participant