Fix an OpenMP data race in flow_dep_bdy called from moist_tile_loop_2 in solve_em - #2382
Open
pp1457 wants to merge 1 commit into
Open
Fix an OpenMP data race in flow_dep_bdy called from moist_tile_loop_2 in solve_em#2382pp1457 wants to merge 1 commit into
pp1457 wants to merge 1 commit into
Conversation
pp1457
force-pushed
the
fix/omp-race-flow-dep-bdy-moist
branch
from
August 18, 2026 13:58
4e9d7d8 to
85a9d6c
Compare
Collaborator
|
The regression test results: |
Author
|
The regression suite has come back clean on this, so it's ready for review whenever convenient. Two things are ready on our side whenever you want them:
No urgency from us — just flagging that neither is waiting on us. |
Collaborator
|
@pp1457 I'd suggest that you started the other PRs. Once the PRs are created, we will review them. Thanks! |
flow_dep_bdy is called from inside the moist_tile_loop_2 !$OMP PARALLEL DO in solve_em. Its writes are correctly bounded by the tile (its:ite, jts:jte), but it READS a fixed global row/column -- field(i_inner,k,jbe-spec_zone) and the three analogous edges -- which another tile may still be updating in rk_update_scalar within the same parallel loop. That is a data race: identical inputs give different results run to run. The default contiguous static schedule hides it, because the boundary tile and the tile owning the shared row usually land on the same thread and the accesses serialise. Any schedule that interleaves tiles across threads exposes it. Hoist the call into its own tile loop, moist_tile_loop_3, so the implicit barrier at the end of moist_tile_loop_2 separates the tile updates from the read. One barrier suffices: within the new loop the rows written are jbe-spec_zone+1 .. jbe while the row read is jbe-spec_zone, so reads and writes there do not overlap. Verified on the CONUS 2.5 km case: two runs of an identical configuration differed in 113 restart fields before the fix and 0 after, and the fixed build is bit-identical to the unmodified build under the default schedule.
pp1457
force-pushed
the
fix/omp-race-flow-dep-bdy-moist
branch
from
August 28, 2026 00:22
85a9d6c to
7342f81
Compare
Author
|
Force-pushed a reworded commit message. The tree is byte-identical to the previously tested commit — same diff, no code change — so nothing should need re-verifying. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix an OpenMP data race in flow_dep_bdy called from moist_tile_loop_2 in solve_em
TYPE: bug fix
KEYWORDS: OpenMP, data race, reproducibility, lateral boundary conditions, flow_dep_bdy, tiles, solve_em, thread safety
SOURCE: Liao Yun Yang (National Taiwan University)
DESCRIPTION OF CHANGES:
Problem:
When WRF is built with OpenMP (
smparordm+sm),flow_dep_bdyis called frominside the
moist_tile_loop_2!$OMP PARALLEL DOindyn_em/solve_em.F. Itswrites are correctly bounded by the tile (
its:ite,jts:jte), but it readsfrom a fixed global row/column that a different tile may be writing
concurrently in the same parallel loop.
In
flow_dep_bdy(share/module_bc.F, north-edge branch; the other three edgesare analogous):
jbe = jde-1is the global domain edge, sojbe-spec_zoneis a fixed global rowindex unrelated to this tile's
jts:jte;i_inneris likewise clamped to globalbounds. A tile in the boundary zone therefore reads a row that another tile owns
and is concurrently updating in
rk_update_scalar. Both happen inside the samePARALLEL DO, with no synchronisation between them.The consequence is that WRF produces different results on every run from
identical inputs. With the default contiguous
staticschedule each threadreceives one contiguous block of tiles, so the boundary tile and the tile owning
jbe-spec_zoneare usually the same thread and the accesses serialise — whichis why this is normally invisible. Any schedule that interleaves tiles across
threads puts them on different threads and the race fires. Because it is a timing
window it is intermittent, so a clean run does not demonstrate absence.
Solution:
Hoist the call out of
moist_tile_loop_2into its own tile loop,moist_tile_loop_3. The implicit barrier at the end ofmoist_tile_loop_2thenguarantees every tile's write has landed before any tile reads the shared row. One
barrier suffices: within the new loop the rows written are
jbe-spec_zone+1 .. jbewhile the row read isjbe-spec_zone, so reads andwrites inside the new loop do not overlap.
The boundary block was already the last statement in the tile-loop body, so the
hoist moves it verbatim — no call arguments, conditions or statements are altered,
only the placement of the barrier. The loop-invariant guards
(
config_flags%specified,have_bcs_moist,im .ne. P_QV) stay outside the newloop so no parallel region is entered when the boundary update is not needed.
Scope of this PR. This is the one call site for which we have end-to-end
before/after evidence, submitted separately per the discussion in #2377. Seven
further call sites (
tke_2, chem, tracer x2, scalar x3) have the identical defectand are addressed in a companion PR. A ninth,
zero_grad_bdy, is discussed in#2377 and deliberately held back — its enclosing loop is the acoustic
small-timestep loop, so hoisting there would add a parallel region to the
innermost time loop and warrants its own performance measurement.
ISSUE:
Part of #2377
LIST OF MODIFIED FILES:
M dyn_em/solve_em.F
TESTS CONDUCTED:
Yes. Characterised on v4.4, where
flow_dep_bdyis byte-identical to the currentcode and is called from
moist_tile_loop_2with the same structure, so the samereasoning and the same fix apply.
Configuration: CONUS 2.5 km real-data case,
specified = .true.,have_bcs_moist = .false., builtdm+smwith GCC 13.3.0 / OpenMPI 5.0.9 andparallel HDF5 / netCDF-4, run with several OpenMP threads per MPI rank and with
enough tiles per patch that tiles interleave across threads rather than each
thread receiving a single contiguous block. 8 time steps, restart files compared
with
diffwrf. To make the schedule selectable at runtime,SCHEDULE(RUNTIME)wasadded to the
solve_emtile loops as instrumentation only; withOMP_SCHEDULE=staticthat instrumented build is bit-identical to theuninstrumented one.
OMP_SCHEDULE=static,1vsstaticOMP_SCHEDULE=static,1vs itselfOMP_SCHEDULE=dynamicvs itselfOMP_SCHEDULE=dynamicvsstaticstatic, vs unmodified build, defaultstaticThe last row is the important one for existing users: the change is inert
under the default contiguous-static schedule, so it does not alter results.
The corruption is spatially localised exactly as the code predicts. After only 4
time steps, 10 points of
MU_1differ between two identical runs, and every onelies in the four rows against the north domain edge (the domain has 1200 rows) —
precisely the
jbebranch quoted above:dyn_em/solve_em.Fwas preprocessed exactly as the build does it(
sed->cpp->standard.exe->cpp) and compiled withgfortran -fsyntax-only -fopenmpagainst a built module set: 0 errors.Diffing the preprocessed output against the unpatched file shows the only
changes are the relocated
ENDDO/!$OMP END PARALLEL DOand the newmoist_tile_loop_3; no call argument, condition or statement differs.!$OMP PARALLEL DO/END PARALLEL DOcounts balance (52/52).The race was characterised at a single configuration. How the failure rate varies
with thread count or tile size was not measured.
Not run on our side — we do not have access. This change is confined to the
moisture path and is bit-identical under the default schedule, so we would expect
the regression suite to be unchanged, but it has not been verified.
RELEASE NOTE:
Fixed an OpenMP data race in the lateral boundary update for moisture variables in
solve_em. flow_dep_bdy writes within its assigned tile but reads a fixed global
row or column that another tile may be updating concurrently, so OpenMP builds
could produce different results from identical inputs. The boundary call is now
made from its own tile loop, so the implicit barrier separates the tile updates
from the shared read. Results are unchanged under the default OpenMP schedule.