Conversation
…fuse The JIT derives both the module name and the C symbol prefix from a hash of the generated source, so ranks that generate different text build disjoint artefacts and the rank-0-compiles/others-load protocol breaks. Agreement was REQUIRED, and a mismatch raised. It fires in practice. The lowering in generate_c_source is not deterministic across ranks (#752), and a Stokes solve with a power-law transversely isotropic viscosity under the consistent tangent trips it in roughly half of np=2 runs - measured 2 of 4 on feature/discrete-adjoint. The abort lands in the forward solve. Two measurements say adopting one rank's source is a sound repair rather than a way of ignoring a wrong answer: * the sources differ ONLY in the order of factors in commutative products. Diffing both ranks' output on a failing run: 12 of 225 lines differ, every one with an identical token multiset and identical length. Every rank's source is a correct kernel for the same equation. * the solver's own symbolic blocks - constitutive tensor, flux, and every Jacobian block - hash IDENTICALLY across ranks on the runs that abort. What differs is produced inside generate_c_source, not handed to it. So the disagreement is over which of several correct spellings to compile, and adopting one is enough. Rank 0's is taken and every rank rehashes from it, which restores the invariant that actually matters: one source, one hash, one module. Measured on the reproducing branch: the repair fired in 4 of 6 runs and all 6 succeeded, against a baseline of 2 aborts in 4. This is a REPAIR, not a fix. It is said out loud - one warning naming the issue - rather than papering over the upstream non-determinism, which is still a bug and still worth finding. The common path is untouched: ranks that already agree keep their own objects and pay one allgather of a 16-character string. Two dead ends recorded so they are not retried. The C printers do NOT use StrPrinter._print_Mul - C89CodePrinter delegates to CodePrinter._print_Mul, which has no stored-order fast path and does sort, and forcing that sort produces byte-identical C while a run still aborts. And it is not commutative ordering at all: recursively sorting every Mul/Add argument by default_sort_key still leaves the ranks differing. The repair is extracted as _agree_source_across_ranks so it can be tested directly. Reproducing a real disagreement means reproducing a non-deterministic bug at about one run in two, which is not a test - it would pass half the time with the repair removed. The tests build the disagreement themselves, so they keep working once the lowering is made deterministic. Underworld development team with AI support from Claude Code
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.
Closes nothing — #752 stays open. This stops the flaky abort; the non-determinism it is a symptom of is still there.
What was happening
The JIT derives the module name and the C symbol prefix from a hash of the generated source, so ranks that generate different text build disjoint artefacts and the rank-0-compiles/others-load protocol breaks. Agreement was required, and a mismatch raised.
It fires in practice. A Stokes solve with a power-law transversely isotropic viscosity under the consistent tangent trips it in roughly half of np=2 runs — measured 2 aborts in 4 on
feature/discrete-adjoint. The abort lands in the forward solve, so it looks like a solver failure.Why adopting one rank's source is sound
Two measurements, not an argument:
c_tensor,flux,_u_F1,_uu_G0–_uu_G3,_up_G0,_pu_G1— all identical. What differs is produced insidegenerate_c_source, not handed to it.So the disagreement is over which of several correct spellings to compile. Rank 0's is taken, every rank rehashes from it, and the invariant that matters is restored: one source, one hash, one module.
Measured on the reproducing branch: the repair fired in 4 of 6 runs and all 6 succeeded (baseline 2 aborts in 4).
This is a repair, not a fix
It warns, naming #752, rather than papering over the upstream bug silently. The common path is untouched — ranks that already agree keep their own objects and pay one
allgatherof a 16-character string.Two dead ends, recorded so they are not retried
Both were implemented and measured before being discarded:
StrPrinter._print_Mul—C89CodePrinterdelegates toCodePrinter._print_Mul, which has no stored-order fast path and does sort (printer.order is None). Forcing that sort produces byte-identical C across all five kernel bundles, and a run still aborted.Mul/Addargument bydefault_sort_keybefore hashing still leaves the ranks differing (2 of 3 runs).The remaining lead for #752: the lowering phases between the solver's pointwise functions and
printer.doprint—_reveal_constants,unwrap, theconstants_subs_mapxreplace,to_matrix, thefree_symswalk,cse. Several consume.atoms(), which is a set;_stable_sortedalready exists in that file.Tests
tests/parallel/test_0022_jit_rank_source_agreement.py. The repair is extracted as_agree_source_across_ranksso it can be tested directly — reproducing a real disagreement means reproducing a non-deterministic bug at about one run in two, which is not a test: it would pass half the time with the repair removed. The tests construct the disagreement themselves, so they keep working once the lowering is made deterministic.assert agreed is same— so the repair cannot quietly become an unconditional broadcast;Writing that first test immediately caught a real bug in the extraction:
hashlibis imported insidegenerate_c_source, not at module level, so the helper raisedNameErroron the path that matters.Serial band
tests/test_00[0-4]*py: 141 passed. Parallel at np=2 withtest_1069: 10 passed, 1 skipped.Note on scope
This is pre-existing on
development, not something arriving with a feature branch. Controlled measurement — one worktree, one environment, one set of caches, branch as the only variable, counting repair firings rather than aborts:developmentfeature/discrete-adjointAn earlier uncontrolled comparison (0/12 vs 2/4, in two different checkouts) suggested a branch regression. It was wrong: the rate is environment-sensitive, so samples from different worktrees are not comparable. Any future comparison here needs the controlled form.
Underworld development team with AI support from Claude Code