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