Skip to content

fix: jax_grad assertions fail locally but pass in CI #260

Description

@Jammy2211

Overview

Running the jax_grad scripts locally under the resolved smoke profile produces
deterministic assertion failures in scripts that pass in CI on the same commit.
imaging/jax_grad/lp.py is the decisive control: it passes in CI on two independent
runs and fails locally, so the three local failures found during PyAutoHands#226 were
environment artefacts, not source defects.

This is an active trap for anyone validating these scripts locally — during #226 it
looked exactly like two fresh correctness regressions on current main. Whatever the
cause, either the scripts or the documented local-run recipe should make this
reproducible, so that a local FAIL means something.

Split out of PyAutoHands#226; see PyAutoMind/complete/2026/08/jax-grad-smoke-timeout-budget.md.

Plan

  • Capture both environments as data before anything drifts. pip freeze +
    np.show_config() from the local venv, and the same from a CI job. The prompt's
    table is currently the only record of the local side.
  • Run the decisive A/B against the control. Build the CI install set, confirm
    lp.py PASSES, then change only numpy 2.4.6 -> 2.2.6 and confirm it FAILS. Run
    it in both directions — the upgrade recovering PASS is what rules out
    install-ordering effects.
  • If numpy is not the discriminator, diff the two freezes and bisect the
    remaining delta against lp.py, cheapest-first.
  • Root-cause the mechanism, not the correlation. lp.py's failure is an
    active-set change in the positive-only NNLS solve, not a last-digit tolerance
    breach — that points at the BLAS/LAPACK backend the numpy wheel ships, not at
    numpy's Python layer.
  • Then decide the verdict — either the assertions are under-specified across the
    supported dependency range, or the local environment is out of spec. These call for
    different fixes, and only measurement chooses between them.
  • Make a local FAIL mean something: whichever verdict, land a documented,
    reproducible local-run recipe in AGENTS.md and cross-reference it from the
    existing profile_smoke.yaml warning.

What the code already tells us

  • scripts/misc/util.py:185assert_eager_jit_consistent(..., rtol=1e-10) is
    documented as a pure_callback constant-folding detector, not a numerics
    check. Constant-folding produces order-unity disagreement; the observed 8.9e-6 is
    ~5 orders of magnitude too small to be the thing this assert exists to catch.
  • scripts/imaging/jax_grad/lp.py:213 — the failing guard is
    any(|ad[source]| > 1e-3), i.e. the NNLS solve zeroed the source. The linear
    solve is landing on a different active set — the same signature as the
    pixelization.py 8.9e-6 gap and the regularization.py marginal FD breach.
  • .github/scripts/smoke_install.sh pins nothing but jax<0.7/jaxlib<0.7 — numpy
    is whatever the resolver picks that day. "The CI set" is not a fixed target, and
    nothing in the repo tells a local runner how to reproduce it.
  • AGENTS.md documents run_smoke.py and run_all_scripts.sh but has no
    local-environment recipe at all
    . That gap is the trap.
  • config/build/profile_smoke.yaml:74 already carries a "Do NOT re-derive these from
    local runs" warning naming this exact divergence — the right place to record the
    resolved cause.
Detailed implementation plan

Work Classification

Workspace (single repo) — routes to /start_workspace.

Affected Repositories

  • autolens_workspace_test (primary)

Branch Survey

Repository Current Branch Dirty?
PyAutoLabs/autolens_workspace_test main @ b18fd38 clean

worktree_check_conflict jax-grad-local-vs-ci-assertions autolens_workspace_test
-> exit 0, no conflict.

Warning (not a block): feature/jax-grad-param9-mismatch and
claude/rectangular-mesh-gradients-mh1j0z exist on the remote as unregistered
prior jax_grad work.

Suggested branch: feature/jax-grad-local-vs-ci-assertions

Worktree root: ~/Code/PyAutoLabs-wt/jax-grad-local-vs-ci-assertions/
(created later by /start_workspace)

Implementation Steps

  1. Capture ground truth. From the local machine: pip freeze > local.txt and
    python -c "import numpy; numpy.show_config()". From CI: the same two, pulled
    from a fresh main smoke run's job log, or reproduced by running
    .github/scripts/smoke_install.sh against fresh chain checkouts. Record jax,
    jaxlib, numpy, scipy and tfp-nightly explicitly.

  2. The A/B. In an isolated venv holding the CI set, run
    scripts/imaging/jax_grad/lp.py via env_config.build_env_for_script with the
    workspace root as cwd (the exact env the report used) -> expect PASS.
    pip install numpy==2.2.6 alone -> rerun -> expect the
    All source-parameter gradients are ~zero failure. Then pip install numpy==2.4.6
    -> rerun -> expect PASS to return. lp.py is the control because it is the only
    script known to pass in CI and fail locally, and at ~41s it is the cheapest.

  3. Widen if needed. diff local.txt ci.txt; bisect the remaining delta
    package-by-package against lp.py.

  4. Mechanism. Compare np.show_config() across the two numpy versions (OpenBLAS
    build and threading). Check whether the inversion's non-negative solve (the fnnls
    active set) selects a different set under each, by printing the active-set size at
    the lp.py evaluation point in both. This is what distinguishes "float noise"
    from "a different solution" — the evidence says it is the latter.

  5. The fix, chosen by the measurement:

    • If the supported dependency range genuinely admits this spread: the rtol=1e-10
      on assert_eager_jit_consistent is the outlier and the only tolerance worth
      changing — raised to a value that still detects constant-folding (which is
      order-unity), with the measured spread written in as its justification. Leave
      assert_gradients_match (rtol=1e-3/atol=1e-4) alone
      unless
      regularization.py's breach survives step 4.
    • If the local env is out of spec: no tolerance change at all. Add a
      "Local runs of jax_grad/" subsection to AGENTS.md with the exact install
      (mirroring smoke_install.sh plus the resolved pins), the build_env_for_script
      invocation, and the cwd requirement.
    • Either way: cross-reference the verdict from the
      config/build/profile_smoke.yaml "Do NOT re-derive these from local runs" note,
      so that warning points at a resolved cause instead of an open mystery.
  6. Validate. Run all 7 live jax_grad scripts under the fixed recipe and confirm
    they agree with CI. Ship as a PR; the per-PR smoke gate is the CI-side confirmation.

Explicitly out of bounds

Any of these masks the trap rather than removing it, and AGENTS.md already states
the rule (never edit a script to mask a real regression):

  • moving lp.py's evaluation point;
  • adding skip_indices to get green;
  • widening a tolerance without the measured basis.

Key Files

  • scripts/misc/util.pyassert_eager_jit_consistent (:185),
    assert_gradients_match (:157), fd_gradient (:55). Blast radius: all 8
    jax_grad scripts plus scripts/weak/jax_grad.py.
  • scripts/imaging/jax_grad/lp.py — the control; failing guard at :213.
  • scripts/imaging/jax_grad/pixelization.py, scripts/imaging/jax_grad/regularization.py
    — the two other local failures.
  • .github/scripts/smoke_install.sh — the CI install set (unpinned numpy).
  • config/build/profile_smoke.yaml:74 — the existing warning to cross-reference.
  • AGENTS.md — "Running Scripts" / "Testing"; where the local recipe lands.

Trade-off

A util.py tolerance change has repo-wide blast radius and needs the strongest
evidence; an AGENTS.md recipe has zero blast radius but only helps people who read
it. Default to documentation unless step 4 proves a real supported-range problem.

Open dependency

The local venv that produced the failures cannot be inspected from a cloud session.
If it still exists, capture its pip freeze + np.show_config() before it drifts —
otherwise step 2 has no ground truth to reproduce.

Sizing note

Brain sized this medium (declared) vs large (derived); the Bug Agent classifies it
severity=critical, scope=single-repo, type=wrong-result, confidence=low, strategy
investigate-first. Reproduce and confirm root cause before patching anything.

Original Prompt

Click to expand starting prompt

jax_grad scripts fail assertions locally that PASS in CI

Type: bug
Target: autolens_workspace_test
Repos:

  • autolens_workspace_test
    Difficulty: medium
    Autonomy: supervised
    Priority: medium
    Status: formalised

Running the jax_grad scripts locally under the resolved smoke profile produces
deterministic assertion failures in scripts that pass in CI on the same commit.
Found while measuring script durations for PyAutoHands#226.

Evidence

Run via env_config.build_env_for_script with the workspace root as CWD (i.e. the
exact env the runner builds — PYAUTO_SMALL_DATASETS unset, PYAUTO_DISABLE_JAX
unset, PYAUTO_TEST_MODE=2, verified by printing the resolved env):

script local CI (run 30858578587 / 30790463134)
imaging/jax_grad/lp.py FAIL 41.3s PASS 39.6s / 40.0s
imaging/jax_grad/knn.py PASS 141.6s PASS 200.0s / 175.8s
imaging/jax_grad/pixelization.py FAIL 57.5s PASS 244.8s (06:31Z)
imaging/jax_grad/regularization.py FAIL 131.5s (import gap, then TIMEOUT)
point_source/jax_grad/gradient.py PASS 665.9s TIMEOUT (300s cap)

lp.py is the decisive case: it passes in CI on both runs and fails locally.

Failures are deterministic and bit-identical across repeated runs, e.g.
pixelization.py:

AssertionError: Eager (-8354.484097835004) and jitted (-8354.55843260181) evaluations
disagree — possible pure_callback constant-folding; do not trust jitted gradients.

(relative difference ~8.9e-6 against assert_eager_jit_consistent's rtol=1e-10).

lp.py fails with All source-parameter gradients are ~zero — NNLS zeroed the source;
regularization.py with an AD-vs-FD mismatch marginally over tolerance
(abs_err=[0.045, 0.042, 0.057] vs tolerance=[0.031, 0.008, 0.003]).

What is ruled out

  • Not the small-datasets cap. full_datasets correctly unsets
    PYAUTO_SMALL_DATASETS; verified by resolving the env directly rather than
    inferring from mask sizes.
  • Not a JAX version difference. Local jax/jaxlib are 0.10.2 — identical to CI.
  • Not flake. Repeated runs give bit-identical values.

Prime remaining suspect: numpy 2.2.6 local vs 2.4.6 in CI, or another local venv
package differing from the CI install set. Not yet confirmed.

Why it matters

This is an active trap for anyone validating these scripts locally. During #226 it
looked exactly like two fresh correctness regressions on current main
(pure_callback constant-folding, and an FD tolerance breach). Only running a
controllp.py, known-passing in CI — revealed that the local environment
itself produces the failures, so none of the three local failures were evidence of
source defects.

Whatever the cause, either the scripts or the documented local-run recipe should make
this reproducible, so a local FAIL means something.

Suggested scope

  1. Bisect the local-vs-CI package delta (start with numpy 2.2.6 -> 2.4.6) against
    lp.py, the cleanest discriminator.
  2. If numpy is the cause, decide whether the tolerances are under-specified for the
    supported numpy range, or the local env should be pinned to the CI set.
  3. Record the outcome in the workspace's local-run instructions.

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