MRF, YSUNew and YSU: stay inside MFIter tiles (tiled runs abort or store a garbage PBLH); restore #3961's HOL_abs bases - #3972
Conversation
ComputeDiffusivityMRF and ComputeDiffusivityYSUNew build the corrected PBL height in a per-tile planar FArrayBox (growntilebox(1,1,0) under TileNoZ), then copied it into pblh_mf with a ParallelFor over mfi.validbox(). With the CPU default tile size (1024000 8 8) any box 16 or more cells wide in y is tiled, so the copy read past the tile's array. Debug and bound-check builds abort at step 1 with "(0,8,0,0) is out of bound (-1:16,-1:7,0:0,0:0)". Release builds silently store out-of-bounds values for every tile but the last in SurfaceLayer's pblh (the 2D plotfile pblh, the Beljaars w* term, the dust layer). Loop over mfi.tilebox() instead. It spans the full column under TileNoZ, and pblh_mf has no ghost cells, so the tiles together fill every cell before set_pblh copies it. The legacy ComputeDiffusivityYSU iterated with TilingIfNotGPU(), which also tiles in z (tile size 8), so its full-column AMREX_ALWAYS_ASSERT aborted at step 1 whenever nz >= 16. Use TileNoZ() like the other column schemes. Add CTest ABL_MRF_Tiling, ABL_YSUNew_Tiling and ABL_YSU_Tiling (Tests/RunTilingParity.cmake). The same 32x32x32 deck on four 16x16x32 boxes runs with tile size 1024000 8 8 and with tiling off, and the 3D and 2D plotfiles must agree. With the fix every field, pblh included, is bit-identical. On the old code the Release MRF and YSUNew runs give a pblh difference of 16 m (relative 1.0), the bound-check MRF run aborts in the tiled case, and YSU fails its assert in the tiled case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
erf-model#3961 wrote the unstable-arm bases in ComputeDiffusivityMRF as 1 + 16*|HOL|, which equals 1 - 16*HOL on that arm (HOL <= 0 there) and is at least 1 for every HOL, so the optimiser cannot hoist a pow of a negative base above the selection and raise FE_INVALID under amrex.fpe_trap_invalid. erf-model#3486 put back pow(max(1 - 16*HOL, 0.01), ...) in the Pass 4 wstar recompute and in the K-profile phiM and phit, leaving the HOL_abs declared for Pass 4 unused. That is an -Wunused-variable warning, which fails the CI warning check. Use HOL_abs again at those three sites. On the unstable arm max(1 - 16*HOL, 0.01) is 1 - 16*HOL (it is >= 1), so the results are unchanged; the tiling regression deck gives bit-identical plotfiles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code review: correctness pass over the diff
What I verified as correct in the three PBL source changes (no findings against them):
pblh_mfis built oneddyViscosity.boxArray()with 0 ghost cells, and the loop is anMFIter(..., TileNoZ()), whose tileboxes exactly partition the valid box. So switching the copy frommfi.validbox()tomfi.tilebox()inERF_ComputeDiffusivityMRF.cppandERF_ComputeDiffusivityYSUNew.cppboth (a) removes the out-of-bounds read of the per-tilepbl_height_correctorFAB, which is allocated only onPerpendicularBox<ZDir>(mfi.growntilebox(IntVect(1,1,0))), and (b) removes a real OpenMP write race — every tile previously wrote the whole valid box, so with#pragma omp parallelmultiple threads stored to the samepblh_mfcells. Coverage ofpblh_mfis unchanged (union of tileboxes == valid box), soset_pblh'sMultiFab::Copystill sees every valid cell.- The copy point is still the right one: nothing rewrites
pblh_corr_arrafter it. MRF writes it only at lines 545/548, YSUNew only at 581/583 and 971/973 — both strictly before the newtbxloop. ERF_ComputeDiffusivityYSU.cpp:TileNoZ()does satisfy the full-columnAMREX_ALWAYS_ASSERT(sbx.smallEnd(2) == dbx.smallEnd(2) && ...), and the remaining x/y tiling is safe —growntileboxonly grows where a tile abuts the valid-box boundary, so tiles never overlap and theK_turbwrites stay race-free while the union still covers the full one-cell ghost ring exactly as the untiled path did. On GPUTileNoZ()returns the zero vector, which AMReX treats as no tiling (AMReX_MFIter.cpp:311), so behaviour there is identical to the oldTilingIfNotGPU().- The "16+ cells" wording in the new YSU comment is accurate, not off by a factor: AMReX's
buildTileArrayusesmax(ncells/tilesize, 1), so a 9-cell-deep box stays one tile and 16 is indeed the first split.
All my findings are in the new test harness, posted inline, plus one pre-existing item below.
Source/PBL/ERF_PBLModels.H:517 — low (pre-existing; filed as #3978, not introduced by this PR).
Noting it here rather than inline because the file is not part of this diff. ApplyPBLHSmoothing allocates pblh_temp on xybx and clamps its 5-point stencil only against the domain edge:
const int i_xlo = (i > dom_lo.x) ? (i - 1) : i;xybx is the tile grown by one, so for any box or tile whose lo is interior to the domain, i == xybx.smallEnd(0) still satisfies i > dom_lo.x and the kernel reads pblh(i-1, j, 0) one column outside the FArrayBox. Concretely: with erf.enable_pblh_smoothing=true (default off) and more than one box in x or y, MRF and YSUNew abort under -DAMReX_BOUND_CHECK=ON and otherwise produce decomposition-dependent PBL heights. This matches the "Not changed here" section of the PR description. The new parity tests do not enable smoothing, so nothing in CI will catch a regression here — tracked in #3978 so the follow-up PR is not lost.
| add_test_r(ABL_MOST_SFC "" "erf_exec" "plt00010" RUNTIME_OPTIONS "erf.vert_implicit=false ") | ||
| add_test_r(ABL_MOST_SST "" "erf_exec" "plt00010" RUNTIME_OPTIONS "erf.vert_implicit=false ") | ||
| add_test_r(ABL_MYNN_PBL "" "erf_exec" "plt00100" INPUT_SOUNDING "input_sounding_GABLS1" RUNTIME_OPTIONS "erf.vert_implicit=false " ) | ||
| add_test_tiling_parity(ABL_MRF_Tiling ABL_MRF_Tiling "00010" "00010") |
There was a problem hiding this comment.
high — these three tests are registered unconditionally, unlike every other execute_process-driven test in this file (add_test_anelastic_wall_diffusion, add_test_cloud_chamber*), which all sit inside the if(ERF_ENABLE_MPI) block starting at line 491.
Concrete failure: .github/workflows/windows.yml configures with -DERF_ENABLE_MPI:BOOL=OFF -DERF_ENABLE_TESTS:BOOL=ON and then runs ctest -C Debug -L regression, which selects these by their regression label. With MPI off, MPIEXEC_EXECUTABLE and MPIEXEC_NUMPROC_FLAG are undefined, so add_test passes -DMPIEXEC= and -DMPIEXEC_NUMPROC_FLAG= (empty). In RunTilingParity.cmake those become empty elements of _mpi_run, and unquoted expansion in COMMAND ${_mpi_run} ... drops empty list elements — I confirmed with a standalone cmake -P that the list ;;1 expands to just 1, so execute_process tries to exec a program named 1 and returns "No such file or directory", tripping the FATAL_ERROR. All three tests fail on that job.
Independently, on Windows resolve_test_exe returns a glob (${CMAKE_BINARY_DIR}/Exec/*/erf_exec.exe) and FCOMPARE_EXE is also a glob (CMakeLists.txt:238). add_test_r gets away with those because it runs through sh -c, which expands them; execute_process does no globbing, so even with MPI enabled on Windows the exe and fcompare paths would not resolve.
Suggest gating the three registrations on ERF_ENABLE_MPI (and excluding WIN32), the same way the sibling parity tests are.
There was a problem hiding this comment.
Fixed in 53f5ad7. The three registrations are now inside if(ERF_ENABLE_MPI AND NOT WIN32), with a comment giving both reasons: execute_process drops the empty MPIEXEC, and it does not expand the Windows erf_exec/fcompare globs.
Checked by configuring with -DERF_ENABLE_MPI=OFF -DERF_ENABLE_TESTS=ON: ctest -N -R Tiling lists 0 tests. The MPI build still registers all three, and they pass with every field bit-identical.
| add_test_tiling_parity(ABL_MRF_Tiling ABL_MRF_Tiling "00010" "00010") | ||
| add_test_tiling_parity(ABL_YSUNew_Tiling ABL_MRF_Tiling "00010" "00010" RUNTIME_OPTIONS "erf.pbl_type=YSUNew erf.most.pblh_calc=YSU") | ||
| # Legacy YSU aborts in unstable conditions, so cool the surface | ||
| add_test_tiling_parity(ABL_YSU_Tiling ABL_MRF_Tiling "00010" "00010" RUNTIME_OPTIONS "erf.pbl_type=YSU erf.most.pblh_calc=YSU erf.most.surf_temp_flux=-0.05") |
There was a problem hiding this comment.
low — this case advertises a pblh parity check that cannot fail.
Only ComputeDiffusivityMRF and ComputeDiffusivityYSUNew call SurfLayer->set_pblh; legacy ComputeDiffusivityYSU never does. With erf.most.pblh_calc=YSU, SurfaceLayer::computes_pblh() is true, so ERF_Plotfile2D.cpp takes pblh_source = m_SurfaceLayer->get_pblh(lev) — but that MultiFab still holds the bogus_large_value (1e18) it was initialised with, in both the tiled and untiled run. The pblh column of the 2D comparison is therefore vacuous for this test (the other three 2D fields and all 3D fields are still meaningful, and the test does still cover the sbx full-column abort, which is the point of the case).
Worth a comment on the line so a future reader does not treat ABL_YSU_Tiling as pblh coverage.
There was a problem hiding this comment.
Agreed, and confirmed: SurfaceLayer::update_pblh does nothing for the YSU type, and only MRF and YSUNew call set_pblh. The 2D pblh therefore stays at bogus_large_value in both runs.
53f5ad7 adds a comment on the registration. It says ABL_YSU_Tiling covers only the full-column assert, and that its pblh comparison is vacuous.
|
|
||
| foreach(_required MPIEXEC MPIEXEC_NUMPROC_FLAG NRANKS TEST_EXE INPUT | ||
| WORKING_DIRECTORY FCOMPARE RTOL ATOL PLTFILE PLT2DFILE) | ||
| if(NOT DEFINED ${_required}) |
There was a problem hiding this comment.
medium — if(NOT DEFINED ${_required}) is satisfied by an empty value, so this guard does not catch the case it exists for.
add_test_tiling_parity always emits -DMPIEXEC=${MPIEXEC_EXECUTABLE} and -DFCOMPARE=${FCOMPARE_EXE}; when those CMake variables are unset the arguments still arrive as -DMPIEXEC= / -DFCOMPARE=, which defines the script variable as the empty string. DEFINED then returns true, the intended FATAL_ERROR "RunTilingParity.cmake requires MPIEXEC" never fires, and the failure surfaces much later as an opaque execute_process error (see the non-MPI Windows scenario on the registration line).
if(NOT DEFINED ${_required} OR "${${_required}}" STREQUAL "")There was a problem hiding this comment.
Fixed in 53f5ad7 with your condition, plus a comment explaining why an empty value has to be rejected too. cmake -DMPIEXEC= -DMPIEXEC_NUMPROC_FLAG= ... -P Tests/RunTilingParity.cmake now stops straight away with RunTilingParity.cmake requires MPIEXEC.
|
|
||
| foreach(_run IN ITEMS tiled untiled) | ||
| set(_tile_size "${_${_run}_size}") | ||
| file(REMOVE_RECURSE "${WORKING_DIRECTORY}/${_run}_plt" "${WORKING_DIRECTORY}/${_run}_plt2d") |
There was a problem hiding this comment.
low — this REMOVE_RECURSE never matches anything, so the intended cleanup does not happen.
AMReX names plotfiles with the step suffix, so the directories actually written are tiled_plt00000, tiled_plt00010, tiled_plt2d00010, … — there is never a directory called tiled_plt or tiled_plt2d. Stale plotfiles from an earlier configuration of the deck therefore survive into the next run. Since PLTFILE/PLT2DFILE are hard-coded to 00010, a later edit to max_step or erf.plot_int_1 that stops producing a step-10 plotfile would leave both runs' step-10 directories from a previous build in place, and fcompare would compare two stale trees and report a pass.
file(GLOB _stale "${WORKING_DIRECTORY}/${_run}_plt*")
if(_stale)
file(REMOVE_RECURSE ${_stale})
endif()There was a problem hiding this comment.
Fixed in 53f5ad7. Each run now globs <run>_plt* and removes what matches, so the step-suffixed 3D and 2D plotfiles are both covered.
Checked with a stale tiled_plt00010 and tiled_plt2d00010 in the working directory: both were gone before the run started.
…cript Review fixes for the tiling parity harness: - Tests/CTestList.cmake: register ABL_MRF_Tiling, ABL_YSUNew_Tiling and ABL_YSU_Tiling only under ERF_ENABLE_MPI AND NOT WIN32. The Windows job configures with MPI off and runs ctest -L regression, so MPIEXEC arrived empty, execute_process dropped it and tried to run "1". On Windows the erf_exec and fcompare paths are globs that only sh -c expands, and execute_process does not. - Tests/CTestList.cmake: note that ABL_YSU_Tiling covers the full-column assert only. Legacy YSU never calls set_pblh, so the 2D pblh holds the SurfaceLayer's initial value in both runs. - Tests/RunTilingParity.cmake: treat an empty -D value as missing, since add_test_tiling_parity always passes every argument. - Tests/RunTilingParity.cmake: remove the step-suffixed plotfiles (<run>_plt*) before each run. The old REMOVE_RECURSE named tiled_plt/tiled_plt2d, which never exist, so stale step-10 plotfiles could be compared. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t to Auditing the review findings on this PR showed a fifth weakness in the harness: amrex_fextrema on the untiled MRF and YSUNew plotfiles gave pblh (2D) and Lturb (3D) = 16 in every column. That is the floor 0.5*dz (dz is 32 m, not the 200 m the description implied): with a 15 m/s wind and the inversion above the domain top the bulk-Richardson search never crossed Ribcr. A parity check on a field that is one constant everywhere cannot see a tile bug that permutes columns, only one that reads garbage. - Tests/test_files/ABL_MRF_Tiling: put a 6 K inversion at 150-250 m, lower the wind to 5 m/s and use local MOST averaging (erf.most.average_policy = 1). The Rib crossing now lies inside the perturbed layer, so pblh ranges over 168-204 m (MRF) and 180-201 m (YSUNew), and u_star, t_star and Olen differ between columns too. - Tests/RunTilingParity.cmake: new VARYING_3D / VARYING_2D lists. For each named field the script runs amrex_fextrema on the untiled plotfile and fails unless min < max, so an agreement between two copies of a constant is reported as a failure, not a pass. On the step-0 2D plotfile, where pblh is still the 1e150 initial value, the guard stops with "pblh is uniform (1e+150)". - Tests/CTestList.cmake: pass FEXTREMA (built next to fcompare by the AMReX plotfile tools) and the lists. MRF and YSUNew check Lturb, Kmv, pblh and u_star. Legacy YSU never writes the surface-layer pblh, so its run drops pblh from erf.plot2d_vars_1 instead of comparing the constant, and checks Lturb, Kmv and u_star. Its cooling is -0.02 instead of -0.05: with the 5 m/s wind the stronger cooling stops the MOST iteration converging. With the old PBL sources (489a2aa) the tiled MRF and YSUNew runs still abort on the bound check and YSU still fails its full-column assert; with the fix all three pass with every field bit-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mrf-tilebox-pblh-upstream
|
Pushed 32d86ec, a follow-up from auditing the review findings above. Running
Re-checked with the three PBL sources from 489a2aa and this deck: the tiled MRF and YSUNew runs abort at step 1 ( |
Commit 6b6f8e0 removed the third-site HOL_abs in ERF_ComputeDiffusivityMRF.cpp while development had it unused; erf-model#3972 then restored pow(1 + 16*HOL_abs, -1/4) at that site, and the merge kept the use without the declaration, so every CI build job failed on an undeclared identifier. This takes development's copy of the file; that deletion was this branch's only change to it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Summary
MRF, YSUNew and YSU break when MFIter tiling splits a box. On CPU that is the default, because AMReX's CPU
fabarray.mfiter_tile_sizeis1024000 8 8.FArrayBox(PerpendicularBox<ZDir>(mfi.growntilebox(IntVect(1,1,0)))underTileNoZ()), then copy it intopblh_mfforSurfLayer->set_pblhwith aParallelForovermfi.validbox(). Once a box is 16 or more cells wide in y, the valid box extends past the tile, and the copy reads outside the tile's array.AMReX_BOUND_CHECKbuilds abort at step 1, e.g.(0,8,0,0) is out of bound (-1:16,-1:7,0:0,0:0)inComputeDiffusivityMRF.pblhholds out-of-bounds values for every tile but the last. That field feeds the 2D plotfilepblhand the Beljaars w* term (erf.most.include_wstar). The eddy diffusivities are unaffected.TilingIfNotGPU(), which on CPU also tiles in z. Its full-columnAMREX_ALWAYS_ASSERT(sbxmust span the domain in z) therefore aborts at step 1 whenever nz ≥ 16.Most MRF/YSUNew canonical decks set
fabarray.mfiter_tile_size = 1024 1024 1024, which is why this has not shown up.Changes
ERF_ComputeDiffusivityMRF.cpp,ERF_ComputeDiffusivityYSUNew.cpp: loop overmfi.tilebox()for thepblh_mfcopy. UnderTileNoZthe tile spans the full column, andpblh_mfhas no ghost cells. The tiles therefore partition the valid box, and every cell is written exactly once beforeset_pblhcopies the valid cells.ERF_ComputeDiffusivityYSU.cpp:TileNoZ()instead ofTilingIfNotGPU(), like the other column schemes.ERF_ComputeDiffusivityMRF.cpp: restore the1 + 16*HOL_absbases from Keep the MRF scheme from raising invalid floating-point flags it never uses #3961 in the Pass 4 wstar recompute and in the K-profilephiM/phit. PBLH Corrections #3486 replaced them withpow(max(1 - 16*HOL_bounded, 0.01), ...), which leftHOL_absunused in Pass 4.developmenttherefore builds withwarning: unused variable 'HOL_abs', which the CI warning check counts. It also brings back the pow-of-a-negative-base form that Keep the MRF scheme from raising invalid floating-point flags it never uses #3961 removed foramrex.fpe_trap_invalidruns. On the unstable arm HOL ≤ 0, somax(1 - 16*HOL, 0.01)equals1 - 16*HOL = 1 + 16*|HOL|and the results do not change.ABL_MRF_Tiling,ABL_YSUNew_Tiling,ABL_YSU_Tiling.add_test_tiling_parityinTests/CTestList.cmake,Tests/RunTilingParity.cmake, and the deckTests/test_files/ABL_MRF_Tiling. They are registered only whenERF_ENABLE_MPIis on and not on Windows, since the script runsmpiexecand the plotfile tools throughexecute_process.fabarray.mfiter_tile_size = 1024000 8 8(two tiles per box in y) and again with tiling off.amrex_fcomparethen requires the 3D plotfiles (ρ, u, v, w, p, θ, Kmv, Khv, Lturb) and the 2D plotfiles (pblh, u_star, t_star, Olen) to agree. No gold files are needed.VARYING_3D/VARYING_2Dname fields that must take more than one value in the untiled run (amrex_fextrema, min < max), so the test cannot pass by comparing a constant with itself. MRF and YSUNew checkLturb,Kmv,pblhandu_star.RUNTIME_OPTIONS. YSU gets a cooled surface, since it aborts in unstable conditions. Legacy YSU never writes the surface-layerpblh, so its run leavespblhout of the 2D plotfile and covers only the full-column assert (plusLturb,Kmv,u_star).Results
Built on
development, macOS, AppleClang, Release with-DAMReX_BOUND_CHECK=ON -DERF_ENABLE_MPI=ON -DERF_ENABLE_ALL_WARNINGS=ON -DERF_ENABLE_TESTS=ON, 2 MPI ranks. The "development" column uses the three PBL sources from 489a2aa with the final deck and harness.developmentPBL sourcesABL_MRF_Tiling(0,8,0,0) is out of bound (-1:16,-1:7,0:0,0:0)on rank 0,(0,24,0,0) is out of bound (-1:16,15:23,0:0,0:0)on rank 1, inComputeDiffusivityMRFABL_YSUNew_TilingComputeDiffusivityYSUNewABL_YSU_TilingAssertion 'sbx.smallEnd(2) == dbx.smallEnd(2) && sbx.bigEnd(2) == dbx.bigEnd(2)'Ranges of the fields the tests require to vary (untiled run, step 10):
Lturb(m)Kmv(m²/s)pblh(m)u_star(m/s)On
developmentthe untiled run of the same deck completes all 10 steps, so the failures come from tiling alone.With the
HOL_absrestoration,erf_execbuilds with no warnings outside Submodules (developmenthas one, the unusedHOL_abs). On the MRF deck (heated, so the unstable arm is taken) the plotfiles with and without the restoration are bit-identical (fcompare, zero tolerance, 3D and 2D).The same change was first made and tested in the hgopalan/ERF fork (hgopalan#393). There, a Release build of the old code (no bound checks) failed
ABL_MRF_TilingandABL_YSUNew_Tilingon the 2Dpblh: absolute difference 16 m, relative 1.0. The 3D fields were identical.Review changes
ERF_ENABLE_MPI AND NOT WIN32; treat an empty-Dvalue as missing in the script; remove the step-suffixed plotfiles (<run>_plt*) before each run; note that legacy YSU does not writepblh.amrex_fextremashowed the original deck'spblh/Lturbwere 16 m (the 0.5·dz floor) in every column, because with a 15 m/s wind and the inversion above the domain top the bulk-Richardson search never crossed Ribcr. The comparison was between two copies of one constant. The deck now puts the inversion inside the perturbed layer and the harness requires the named fields to vary; on the step-0 2D plotfile (pblh still 1e150) it stops withpblh is uniform (1e+150).Not changed here
ApplyPBLHSmoothing(ERF_PBLModels.H, opt-inerf.enable_pblh_smoothing, default off) has a related problem. It smooths over the grown tile but clamps its stencil only at the domain edge. So it reads one column outside the array at every interior box or tile edge, even without tiling. With bound checks and several boxes, MRF and YSUNew abort there. A decomposition-independent fix needs neighbouring columns from outside the tile, so it is left for a separate PR (#3978).🤖 Generated with Claude Code