Skip to content

Commit dfc5a98

Browse files
Jammy2211claude
authored andcommitted
fix: lift nufftax to 0.6.x + rank-guard its vmap batching (batched nufft2d2 grad)
Two stacked fixes for differentiating through TransformerNUFFT's batched transform_mapping_matrix (PyAutoArray#424, the nightly release blocker): 1. nufftax 0.4.0's _nufft2d2_bwd assumed 2-D f, so any gradient of an interferometer inversion crashed. 0.6.x supports batched input natively with primitive-based autodiff; its forward transform is bit-identical to 0.4.0 at eps=1e-12 (2-D and batched, x64), so pinned baselines are safe. The old <0.5.0 cap only existed for the Python 3.11 floor, now gone. 2. nufftax 0.6.x's own batching fast path re-binds a primitive unchanged whenever only the source arg is vmapped, with no rank guard — nested batching (jax.vmap(jax.value_and_grad(...)) over an already-batched mapping matrix, i.e. MultiStartProdigy) accumulates dims until tracing crashes. _patch_nufftax_batchers() re-registers every primitive's batcher with the guard: within native rank bind as before, beyond it collapse the stacked batch axes into the one native axis and unflatten the output. Applied only for nufftax 0.6.x; drop when fixed upstream. Verified: vmap(value_and_grad) matches a per-item loop exactly (jit(vmap) to ~1e-12); autogalaxy_workspace scripts/interferometer/start_here.py completes end-to-end where it previously crashed; full suite 929 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 71cb762 commit dfc5a98

2 files changed

Lines changed: 69 additions & 2 deletions

File tree

autoarray/operators/transformer.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,71 @@ class NUFFTPlaceholder:
2929
_nufftax = None
3030

3131

32+
def _patch_nufftax_batchers():
33+
"""Rank-guard nufftax's vmap batching rules (shim for nufftax <0.7).
34+
35+
nufftax's batching fast path re-binds a primitive unchanged whenever only
36+
the source argument is vmapped, assuming the impl's single native batch
37+
axis can absorb it. Under nested batching — e.g. `jax.vmap(jax.value_and_grad(...))`
38+
over a fit whose `transform_mapping_matrix` already passes a batched stack —
39+
the dims accumulate past what the impl accepts and tracing crashes
40+
("too many values to unpack"). Until that is fixed upstream
41+
(github.com/GragasLab/nufftax), re-register every primitive's batcher with
42+
the missing guard: within native rank, bind as before; beyond it, collapse
43+
the stacked batch axes into the one native axis and unflatten the output.
44+
"""
45+
import jax
46+
from jax.interpreters import batching
47+
48+
P = _nufftax.transforms.primitives
49+
50+
# (primitive, impl, index of the source argument, unbatched source rank)
51+
table = [
52+
(P.nufft1d1_p, P._impl_1d1, 1, 1),
53+
(P.nufft1d2_p, P._impl_1d2, 1, 1),
54+
(P.nufft2d1_p, P._impl_2d1, 2, 1),
55+
(P.nufft2d2_p, P._impl_2d2, 2, 2),
56+
(P.nufft3d1_p, P._impl_3d1, 3, 1),
57+
(P.nufft3d2_p, P._impl_3d2, 3, 3),
58+
(P.nufft1d3_p, P._impl_1d3, 1, 1),
59+
(P.nufft2d3_p, P._impl_2d3, 2, 1),
60+
(P.nufft3d3_p, P._impl_3d3, 3, 1),
61+
]
62+
63+
def guarded_batcher(prim, impl_fn, source_idx, base_rank):
64+
def batcher(args, dims, **kwargs):
65+
batched = [i for i, d in enumerate(dims) if d is not None]
66+
if batched == [source_idx] and dims[source_idx] == 0:
67+
src = args[source_idx]
68+
extra = src.ndim - (base_rank + 1)
69+
if extra <= 0:
70+
return prim.bind(*args, **kwargs), 0
71+
lead = src.shape[: extra + 1]
72+
flat = src.reshape((-1,) + src.shape[extra + 1 :])
73+
new_args = list(args)
74+
new_args[source_idx] = flat
75+
out = prim.bind(*new_args, **kwargs)
76+
return out.reshape(lead + out.shape[1:]), 0
77+
out = jax.vmap(lambda *a: impl_fn(*a, **kwargs), in_axes=tuple(dims))(
78+
*args
79+
)
80+
return out, 0
81+
82+
return batcher
83+
84+
for prim, impl, source_idx, base_rank in table:
85+
batching.primitive_batchers[prim] = guarded_batcher(
86+
prim, impl, source_idx, base_rank
87+
)
88+
89+
90+
if _nufftax is not None:
91+
_version = tuple(int(v) for v in _nufftax.__version__.split(".")[:2])
92+
# Only the 0.6.x series both has the primitives module and needs the shim.
93+
if (0, 6) <= _version < (0, 7):
94+
_patch_nufftax_batchers()
95+
96+
3297
def pynufft_exception():
3398
raise ModuleNotFoundError(
3499
"\n--------------------\n"

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ jax = ["autonerves[jax]"]
5353
optional = [
5454
"autoarray[jax]",
5555
"numba",
56-
"nufftax>=0.4.0,<0.5.0",
56+
# 0.6.1 floor: nufftax <0.6 cannot differentiate a batched nufft2d2
57+
# (_nufft2d2_bwd assumed 2-D f), which transform_mapping_matrix relies on.
58+
"nufftax>=0.6.1,<0.7.0",
5759
"pynufft",
5860
# tfp provides the modified-Bessel `bessel_kve` used by the JAX Matern-kernel
5961
# regularization path (autoarray/inversion/regularization/matern_kernel.py).
@@ -64,7 +66,7 @@ optional = [
6466
"tfp-nightly==0.26.0.dev20260713"
6567
]
6668
test = ["pytest"]
67-
dev = ["pytest", "black", "numba", "nufftax>=0.4.0,<0.5.0", "pynufft==2022.2.2"]
69+
dev = ["pytest", "black", "numba", "nufftax>=0.6.1,<0.7.0", "pynufft==2022.2.2"]
6870

6971
[tool.pytest.ini_options]
7072
testpaths = ["test_autoarray"]

0 commit comments

Comments
 (0)