Stokes JIT: rank-0 compile + CSE before printing + configurable flags (#547) - #612
Stokes JIT: rank-0 compile + CSE before printing + configurable flags (#547)#612bknight1 wants to merge 3 commits into
Conversation
…#547) Four fixes for the HPC OOM in the runtime JIT compilation of Stokes pointwise functions (gcc: fatal error: Killed signal terminated program cc1 under mpirun -np N): 1. SNES_Stokes_SaddlePt._setup_pointwise_functions now calls getext with cache=True (the only call site that opted out), activating the existing rank-0-only compile + cross-rank disk handoff. Previously every rank compiled its own copy of the (potentially enormous) pointwise module, exhausting node memory (20x -> 1x concurrent compilers). 2. CSE before printing in generate_c_source: shared subexpressions become double xN = ...; temps evaluated in dependency order, shrinking the generated C ~63x on large rheologies (1.45 MB -> 23 KB header) and gcc peak RSS from 1.32 GB to 62-74 MB — comfortably inside a 900 MB per-rank cap. Semantics-preserving (temps are exact aliases); recovers _ccodestr on the new coordinate instances cse can mint; UW_JIT_NOCSE=1 escapes. 3. JIT compile flags configurable via UW3_JIT_CFLAGS (e.g. "-O1 -g0"); default adds -g0 to drop the debug info sysconfig injects. The env var is the memory lever for constrained HPC nodes. 4. Matrix-level sympy.diff for the Stokes uu/up Jacobian blocks instead of per-entry loops (~1.7x faster derivatives, bit-identical entries, flat PETSc [fc,gc,df,dg] layout preserved). Verified: 132 tests pass (incl. the FD-oracle Jacobian layout test, JIT determinism, constants routing) plus the CSE-off path; MPI rank-0-only compile and disk-cache reuse confirmed under mpirun -np 2. Underworld development team with AI support from Claude Code
There was a problem hiding this comment.
Pull request overview
This PR addresses MPI/HPC failures during runtime JIT compilation of Stokes pointwise functions by reducing redundant compilation work, shrinking generated C via SymPy common-subexpression elimination, and improving JIT compile configurability/performance.
Changes:
- Enable Stokes JIT disk caching so MPI runs use rank-0 compilation with cross-rank reuse.
- Add optional SymPy CSE prior to C printing to dramatically reduce generated code size for large expressions.
- Make JIT compile flags configurable via
UW3_JIT_CFLAGS(defaulting to-O3 -g0) and speed up Jacobian generation via matrix-level differentiation.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/underworld3/utilities/_jitextension.py |
Adds lowering gate, CSE-based code shrinking, and configurable JIT compile flags for generated extensions. |
src/underworld3/cython/petsc_generic_snes_solvers.pyx |
Switches Stokes pointwise-function JIT to cached/rank-0 compile and optimizes Jacobian construction with matrix-level sympy.diff. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if verbose: | ||
| print( | ||
| f"JIT compile flags: {extra_compile_args}" | ||
| f"{' (from UW3_JIT_CFLAGS)' if _jit_cflags_env is not None else ' (default)'}", | ||
| flush=True, | ||
| ) |
Adversarial reviewReviewed against the PR head. CI is green and the change does what it says; one 1. The
|
The collective, demonstrated — and a branch you can takeFollowing finding 1 above, we reproduced it and fixed it rather than leaving it The deadlock is realMonkeypatching the disk lookup so rank 0 sees the cache and the other ranks do
Nothing exotic is needed to provoke it: the divergence is in which branch each What the fix doesBoth predicates are reduced before they are used to choose a branch:
Ranks that already hold the module keep it — the rank-0 compile, the ScopeThe gate predates this PR; what this PR changes is the exposure. Stokes was the Verified: full @bknight1 — the branch is there to cherry-pick or ignore as you prefer; say the Underworld development team with AI support from Claude Code |
How to take the fix — pick one, all three are fineThe commit is Option A — cherry-pick onto this branch (recommended). git fetch origin bugfix/jit-collective-gate
git checkout bugfix/stokes-jit-oom-cse-cache
git cherry-pick aaba1417b7d9ecd359d5407ceedd12987a406cf5
./uw build
git push origin bugfix/stokes-jit-oom-cse-cacheOption B — merge the branch in. Same result, keeps the fix as its own git fetch origin bugfix/jit-collective-gate
git checkout bugfix/stokes-jit-oom-cse-cache
git merge origin/bugfix/jit-collective-gate
./uw build
git push origin bugfix/stokes-jit-oom-cse-cacheOption C — say so here and we will push it onto this branch for you. It adds Verifying it yourselfThe reproduction is a monkeypatch, not a real filesystem race, so it is import sympy
import underworld3 as uw
from underworld3.utilities import _jit_cache as _jc
_real = _jc.load_module
# Rank 0 sees the published module, the others do not — what NFS attribute
# caching or Lustre metadata lag produces just after rank 0 publishes.
_jc.load_module = lambda *a, **k: _real(*a, **k) if uw.mpi.rank == 0 else None
mesh = uw.meshing.StructuredQuadBox(elementRes=(4, 4))
v = uw.discretisation.MeshVariable("Vj", mesh, mesh.dim, degree=2)
p = uw.discretisation.MeshVariable("Pj", mesh, 1, degree=1)
s = uw.systems.Stokes(mesh, velocityField=v, pressureField=p)
s.constitutive_model = uw.constitutive_models.ViscousFlowModel
s.constitutive_model.Parameters.shear_viscosity_0 = 1
s.bodyforce = sympy.Matrix([0, -1])
s.add_dirichlet_bc((0.0, 0.0), "Bottom")
s.add_dirichlet_bc((0.0, 0.0), "Top")
print(f"[{uw.mpi.rank}] entering solve", flush=True)
s.solve()
print(f"[{uw.mpi.rank}] SOLVE RETURNED", flush=True)mpirun --timeout 120 -n 4 python -u jit_divergence.py ; echo "rc=$?"
Clear the JIT cache between runs if you want a genuinely cold start After applyingNothing else in the PR needs to change. Findings 2 and 3 in the review above — Underworld development team with AI support from Claude Code |
The disk-cache lookup is rank-local — its own comment says so — and the Barrier that follows sits inside `if module is None:`. Joining the two by a rank-local predicate means a rank that finds the published module skips the barrier while a rank that does not enters it alone and waits forever. That divergence is ordinary on the shared filesystems this path exists for: attribute caching, metadata lag, or a write not yet visible to every rank just after rank 0 publishes. It is what the barrier is meant to manage rather than something it can assume away. A second route is `disk_enabled`, which is `cache and get_cache_dir() is not None` — a rank whose cache directory cannot be resolved takes the else branch, which has no barrier, while its peers wait in one. Both predicates are now reduced. `needs_compile` is a global OR, so if any rank lacks the module every rank enters the branch and reaches the barrier; `disk_enabled` is a global AND, so a rank without a cache directory makes all ranks fall back together to local compiles, which is the safe direction. Ranks that already hold the module keep it: the rank-0 compile, the post-barrier load, and the no-disk fallback are each guarded on `module is None`. Demonstrated rather than argued. Monkeypatching the disk lookup so rank 0 sees the cache and the others do not — the shape NFS or Lustre lag produces — deadlocks this PR as it stands: at np=4 all four ranks enter the solve, none returns, mpirun times out at rc=241. With the reduction, rc=0 on four consecutive runs. This is not a defect introduced here: the gate predates the PR. What the PR changes is the exposure. Stokes was the one call site passing cache=False, so it always took the no-barrier branch; switching it to cache=True is the point of the change, and it puts the main solver path onto the branch with the conditional collective, on exactly the filesystems #547 is about. Full ./uw test 1556 passed; the four JIT test files 14 passed. Underworld development team with AI support from Claude Code
Change the CSE preprocessing step in generate_c_source from opt-out (UW_JIT_NOCSE=1) to opt-in (UW_JIT_CSE=1), so that default code generation behavior and output are preserved verbatim. Users on memory-constrained HPC nodes running very large/complex rheologies can set UW_JIT_CSE=1 to activate CSE header shrinking. Underworld development team with AI support from Claude Code
|
Thanks @lmoresi — great catch on the rank-local predicate / collective barrier divergence hazard on shared filesystems. Updates applied:
Pushed and ready for CI. Underworld development team with AI support from Claude Code |
…pe (#628) `snap_level_boundaries` guarded its inversion-count `allreduce` on `snapped_any.any()`, a mask over this rank's local vertices. A rank whose partition holds no vertex on a registered bounding surface skipped the block and never arrived, leaving its peers in the reduction. Initialise the count before the guard and reach the reduction unconditionally; the cell-orientation loop stays inside it, since that work really is local. Fixes #627. The same shape has now appeared four times, and it is invisible to `uw.mpi.selective_ranks()` — that catches deliberate rank selection, whereas these guards read as ordinary control flow (`module is None`, `undecided.size`, `snapped_any.any()`) and never enter the context manager. So test_0052 asks the question statically instead: of every `comm.<op>` call site, is it reached on all ranks? Predicates the whole communicator agrees on are recognised as such — the communicator geometry, the mesh's rank-invariant description, a function parameter, and a value that has itself been reduced, which is what makes the library's reduce-first-then-branch idiom come out clean. Across 210 call sites that leaves two entries in ACCEPTED, each with the reason it is uniform, and one in IN_FLIGHT for #612, whose fix is already in an open PR. A stale-entry test deletes either as soon as the site stops tripping the scan. The scan finds #612 and #627 unaided. Its controls cover both directions: the classifier on the predicates that actually deadlocked, and the walker on an injected defect together with its fix. Underworld development team with AI support from Claude Code
Summary
Fixes the HPC compiler OOM and wall-clock timeout during runtime JIT compilation of Stokes pointwise functions (e.g.
gcc: fatal error: Killed signal terminated program cc1undermpirun -np N) described in #547.This PR implements coordinated changes across Stokes pointwise JIT compilation:
Rank-0 Compile and Collective Gate:
SNES_Stokes_SaddlePt._setup_pointwise_functionsnow callsgetextwithcache=True(the only call site that previously opted out). Under MPI, this activates the rank-0-only compile gate and cross-rank disk handoff, preventing N concurrent compiler invocations per node. All compile decisions and cache viability checks are allreduced across communicator ranks (needs_compile = comm.allreduce(..., op=MPI.LOR),disk_enabled = comm.allreduce(..., op=MPI.LAND)) to prevent rank-divergence deadlocks on high-latency shared filesystems.Configurable SymPy Common Subexpression Elimination (CSE):
generate_c_sourcein_jitextension.pysupports CSE before C printing. Shared subexpressions become intermediatedouble xN = ...;statements evaluated in topological dependency order. This shrinks generated C headers by up to ~63x on complex rheologies (1.45 MB → 23 KB) and lowers peakgccRSS from ~1.32 GB to ~62–74 MB. CSE is opt-in viaUW_JIT_CSE=1(default is off to preserve original code generation behavior).Configurable JIT CFLAGS: Defaults to
-O3 -g0to eliminate debug symbol bloat from Pythonsysconfig. Customizable viaUW3_JIT_CFLAGS(e.g.-O1 -g0) for memory-constrained HPC nodes.Matrix-Level SymPy Differentiation: Replaces per-entry derivative loops with
sympy.diffacross whole block matrices (~1.7x faster Jacobian generation, PETSc[fc, gc, df, dg]layout preserved).Fixes #547
Verification
mpirun -n 4(deadlock divergence reproduction scriptjit_divergence.py,rc=0) andmpirun -np 2(ptest_jit_cache.py).test_1066_stokes_jacobian_layout.py).test_jit_cache.py,test_jit_deterministic_ordering.py,test_0103_jit_rampable_constants.py).UW_JIT_CSE=1) paths verified.Underworld development team with AI support from Claude Code