_jitextension.py guards against generate_c_source producing different C on different ranks, and raises a hard error when it does. That guard fires intermittently — about half of runs — on a Stokes solve with a power-law transversely isotropic viscosity under the consistent tangent.
RuntimeError: JIT C-source hash differs across MPI ranks:
{'471c0c17f9aba995', '5e640ad7875d8e9f'}. This indicates non-determinism in
generate_c_source — likely a set or dict whose iteration order leaks into the C
output. Treating this as a hard error since cache reuse would be unsound.
src/underworld3/utilities/_jitextension.py:680
Repro
On feature/adjoint-rotated-bc (PR #751):
pixi run -e amr-dev mpirun -n 2 python -m pytest \
--config-file=tests/pytest.ini tests/test_0022_rotated_adjoint.py -q
Roughly 1 in 2 runs aborts; exit codes over six attempts: 1, 0, 1 and, with PYTHONHASHSEED=0 pinned on both ranks, 1, 0, 0. When it aborts it does so in about 6.7 s, in the fixture's first forward solve.
The setup is an annulus, TransverseIsotropicFlowModel, shear_viscosity_0 = (0.01 + eII)**(-1/3), shear_viscosity_1 = η₁·shear_viscosity_0 with η₁ a uw.expression, radial director, consistent_jacobian = True.
What we ruled out
- Not hash randomisation. Pinning
PYTHONHASHSEED=0 on both ranks still aborts (1 of 3).
- Not a general regression.
tests/test_0021_adjoint_misfit_through_gradient.py, which also uses TransverseIsotropicFlowModel, is stable at np=2 (3/3). Its viscosity is constant rather than power-law.
- Not the expression on its own. Building the same solver outside pytest and solving never diverges (3/3), across four variants that isolate the features: isotropic power-law; TI constant; TI power-law with a plain
sympy.Float coefficient; TI power-law with a uw.expression coefficient. All ok.
- Not the adjoint. The abort lands in the first forward solve, before any adjoint code runs. The same file passes 7/7 serially.
So the trigger needs the pytest process context — several solvers and meshes built in one interpreter — on top of an expression of this shape. The guard's own hypothesis (a set or dict whose iteration order leaks into the emitted C) is still the likely mechanism, but it is not driven by PYTHONHASHSEED, which points at ordering derived from something like object identity or insertion order that differs between ranks rather than from string hashing.
Why it is not currently visible
CI's parallel pass collects only tests/parallel/test_*.py (scripts/test.sh:199), and every file that exercises this expression shape sits in the serial band. Any solve of this kind at np>1 in user code is exposed.
Impact
The guard is doing its job — failing loudly rather than letting ranks compile different kernels — so this is a correctness-preserving abort, not a wrong answer. But it makes a legitimate parallel run fail at random, and the underlying non-determinism would be unsound if the guard were ever relaxed.
Underworld development team with AI support from Claude Code
_jitextension.pyguards againstgenerate_c_sourceproducing different C on different ranks, and raises a hard error when it does. That guard fires intermittently — about half of runs — on a Stokes solve with a power-law transversely isotropic viscosity under the consistent tangent.src/underworld3/utilities/_jitextension.py:680Repro
On
feature/adjoint-rotated-bc(PR #751):Roughly 1 in 2 runs aborts; exit codes over six attempts:
1, 0, 1and, withPYTHONHASHSEED=0pinned on both ranks,1, 0, 0. When it aborts it does so in about 6.7 s, in the fixture's first forward solve.The setup is an annulus,
TransverseIsotropicFlowModel,shear_viscosity_0 = (0.01 + eII)**(-1/3),shear_viscosity_1 = η₁·shear_viscosity_0withη₁auw.expression, radial director,consistent_jacobian = True.What we ruled out
PYTHONHASHSEED=0on both ranks still aborts (1 of 3).tests/test_0021_adjoint_misfit_through_gradient.py, which also usesTransverseIsotropicFlowModel, is stable at np=2 (3/3). Its viscosity is constant rather than power-law.sympy.Floatcoefficient; TI power-law with auw.expressioncoefficient. Allok.So the trigger needs the pytest process context — several solvers and meshes built in one interpreter — on top of an expression of this shape. The guard's own hypothesis (a set or dict whose iteration order leaks into the emitted C) is still the likely mechanism, but it is not driven by
PYTHONHASHSEED, which points at ordering derived from something like object identity or insertion order that differs between ranks rather than from string hashing.Why it is not currently visible
CI's parallel pass collects only
tests/parallel/test_*.py(scripts/test.sh:199), and every file that exercises this expression shape sits in the serial band. Any solve of this kind at np>1 in user code is exposed.Impact
The guard is doing its job — failing loudly rather than letting ranks compile different kernels — so this is a correctness-preserving abort, not a wrong answer. But it makes a legitimate parallel run fail at random, and the underlying non-determinism would be unsound if the guard were ever relaxed.
Underworld development team with AI support from Claude Code