Skip to content

JIT C-source generation is non-deterministic across MPI ranks (flaky hard abort at np>1) #752

Description

@lmoresi

_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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions