Skip to content

Commit 61f07fd

Browse files
Jammy2211claude
authored andcommitted
fix: use_jax no longer demotes the NUFFT preload to the JAX brute force (#539)
Every existing workspace `apply_sparse_operator(use_jax=True)` call was mapped onto `method="jax"`, silently putting the production path on the brute force; `use_jax` is now only honoured when a brute-force method is already selected. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hLF3ZAcz5MmaSJBEcLkvF
1 parent 8720521 commit 61f07fd

3 files changed

Lines changed: 65 additions & 20 deletions

File tree

autoarray/dataset/interferometer/dataset.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,10 @@ def apply_sparse_operator(
251251
252252
The default builder (`method="nufft"`) computes the precision operator as a type-1 NUFFT, so
253253
it costs `O(N_vis * nspread^2 + M log M)` for `M = 4 * Ny * Nx` — seconds even at a million
254-
visibilities. The brute-force builders (`method="numpy"` / `"jax"`, and the `use_jax` kwarg)
255-
are `O(N_vis * N_pix)` and can take minutes to hours; they are kept as the reference the
256-
NUFFT builder is pinned against. Either way the result can be cached to disk and reloaded
257-
via `nufft_precision_operator=`.
254+
visibilities. The brute-force builders (`method="numpy"` / `"jax"`) are `O(N_vis * N_pix)`
255+
and can take minutes to hours; they are kept as the reference the NUFFT builder is pinned
256+
against. Either way the result can be cached to disk and reloaded via
257+
`nufft_precision_operator=`.
258258
259259
Parameters
260260
----------
@@ -284,15 +284,21 @@ def apply_sparse_operator(
284284
show_memory
285285
If `True`, memory usage statistics are printed while computing the NUFFT precision matrix.
286286
use_jax
287-
If `True`, JAX is used to accelerate the NUFFT precision matrix computation.
287+
Only honoured when a brute-force builder is selected: `method="numpy"` with
288+
`use_jax=True` runs the JAX brute force (equivalent to `method="jax"`). Under the
289+
default `method="nufft"` it is ignored, because the NUFFT already runs on JAX --
290+
so an existing `use_jax=True` call keeps the fast path rather than being demoted
291+
to the `O(N_vis * N_pix)` brute force.
288292
289293
`PYAUTO_DISABLE_JAX=1` overrides this to `False`. That variable is a
290294
harness-level switch, not a preference: it is the documented way to force the
291295
NumPy path (the workspace `start_here` guides name it beside `use_jax=False`),
292296
and the smoke profiles set it so a fast run does not pay a JIT compile. An
293297
explicit `use_jax=True` in a script -- which is the right thing for a script
294298
demonstrating the production path to say -- must therefore not defeat it, or
295-
the harness pays 2.3-3.2 s of compile for a backend it asked to disable.
299+
the harness pays 2.3-3.2 s of compile for a backend it asked to disable. (The
300+
same switch demotes the `"nufft"` builder to the NumPy brute force inside
301+
`nufft_precision_operator_from`.)
296302
297303
Precondition
298304
------------
@@ -317,6 +323,9 @@ def apply_sparse_operator(
317323
If any visibility has unequal real and imaginary noise sigma.
318324
"""
319325

326+
# `use_jax` now only selects between the two brute forces (the `"nufft"` builder
327+
# runs on JAX whatever it says), so this clears it before it can upgrade
328+
# `method="numpy"` to the JAX brute force under the kill switch.
320329
if disable_jax():
321330
use_jax = False
322331

@@ -357,7 +366,7 @@ def apply_sparse_operator(
357366
n_vis = self.uv_wavelengths.shape[0]
358367
n_pix = self.real_space_mask.pixels_in_mask
359368

360-
if method != "nufft" or use_jax:
369+
if method != "nufft":
361370
logger.info(
362371
f"INTERFEROMETER - The precision operator is being built by a brute-force "
363372
f"builder, which is O(N_vis x N_pix) = O({n_vis * n_pix:.1e}) and can take "
@@ -432,9 +441,9 @@ def psf_precision_operator_from(
432441
433442
The default builder (`method="nufft"`) computes this as a type-1 (adjoint) NUFFT, which is
434443
`O(N_vis * nspread^2 + M log M)` for `M = 4 * Ny * Nx` — seconds even at a million
435-
visibilities. The brute-force builders (`method="numpy"` / `"jax"`, and the `use_jax`
436-
kwarg) are `O(N_vis * N_pix)` and can take minutes to hours on a CPU for a
437-
high-resolution mask; they are kept as the reference the NUFFT builder is pinned against.
444+
visibilities. The brute-force builders (`method="numpy"` / `"jax"`) are
445+
`O(N_vis * N_pix)` and can take minutes to hours on a CPU for a high-resolution mask;
446+
they are kept as the reference the NUFFT builder is pinned against.
438447
The result can still be saved to disk and reloaded rather than recomputed on each run —
439448
use `apply_sparse_operator(nufft_precision_operator=...)` to attach a cached result.
440449
@@ -448,7 +457,9 @@ def psf_precision_operator_from(
448457
show_memory
449458
If `True`, memory usage statistics are printed during computation.
450459
use_jax
451-
If `True`, the JAX brute-force builder is used (equivalent to `method="jax"`).
460+
Only honoured when a brute-force builder is selected: `method="numpy"` with
461+
`use_jax=True` runs the JAX brute force (equivalent to `method="jax"`). It is
462+
ignored under the default `method="nufft"`, which already runs on JAX.
452463
method
453464
Which builder computes the operator: `"nufft"` (default), `"numpy"` or `"jax"`.
454465
eps

autoarray/inversion/inversion/interferometer/inversion_interferometer_util.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,15 @@ def nufft_precision_operator_from(
122122
matrix construction without performing a NUFFT per source pixel.
123123
124124
-------------------------------------------------------------------------------
125-
Backend behaviour
125+
Backend behaviour (the two brute-force builders)
126126
-------------------------------------------------------------------------------
127-
- NumPy backend (use_jax=False, default):
127+
- NumPy backend (`method="numpy"`):
128128
* CPU execution
129129
* Explicit Python loop over visibility chunks
130130
* Supports progress bars and optional memory reporting
131131
* Numerically closest to the original reference implementation
132132
133-
- JAX backend (use_jax=True):
133+
- JAX backend (`method="jax"`, or `method="numpy"` with `use_jax=True`):
134134
* JIT-compilable and GPU/TPU capable
135135
* Uses fixed-size chunking and lax.fori_loop
136136
* No Python-side loops during execution
@@ -240,7 +240,13 @@ def nufft_precision_operator_from(
240240
`O(N_pix*K)` reference builder. Kept as the reference the NUFFT builder is
241241
pinned against, and used as the fallback below.
242242
- `"jax"` -- `nufft_precision_operator_via_jax_from`, the same brute force on
243-
JAX. `use_jax=True` is kept for backwards compatibility and maps to this.
243+
JAX. `method="jax"` is the explicit way to ask for it.
244+
245+
`use_jax` is only honoured when a brute-force method is selected: it upgrades
246+
`method="numpy"` to `method="jax"` and is otherwise ignored. Under the default
247+
`method="nufft"` it does nothing, because the NUFFT already runs on JAX -- an
248+
existing `use_jax=True` caller therefore keeps the fast path rather than being
249+
demoted to the `O(N_pix*K)` brute force.
244250
245251
Two fallbacks to `"numpy"` are taken, both logged loudly (never silently),
246252
because they cost `O(N_pix*K)` where the NUFFT is `O(K*nspread^2 + M log M)`:
@@ -273,8 +279,12 @@ def nufft_precision_operator_from(
273279
optimisation), used by `method="nufft"` only. `None` is one shot.
274280
chunk_k
275281
The visibility chunk size of the two brute-force builders.
282+
use_jax
283+
Only honoured when a brute-force method is selected: `method="numpy"` with
284+
`use_jax=True` runs the JAX brute force (equivalent to `method="jax"`). It is
285+
ignored under the default `method="nufft"`, which already runs on JAX.
276286
"""
277-
if use_jax:
287+
if method == "numpy" and use_jax:
278288
method = "jax"
279289

280290
if method not in ("nufft", "numpy", "jax"):

test_autoarray/inversion/inversion/interferometer/test_inversion_interferometer_util.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -577,16 +577,33 @@ def test__nufft_precision_operator_from__method_routes_to_each_builder():
577577
operator_via_jax,
578578
)
579579

580-
# `use_jax=True` is kept for backwards compatibility and maps onto `method="jax"`.
580+
# `use_jax=True` only upgrades a brute-force method, so `method="numpy"` with it set is
581+
# the JAX brute force.
581582
np.testing.assert_array_equal(
582583
np.asarray(
583584
aa.util.inversion_interferometer.nufft_precision_operator_from(
584-
use_jax=True, **inputs
585+
method="numpy", use_jax=True, **inputs
585586
)
586587
),
587588
operator_via_jax,
588589
)
589590

591+
# Under the default `method="nufft"` it is ignored -- the NUFFT already runs on JAX, so
592+
# honouring it there would demote every existing `use_jax=True` caller (the workspace
593+
# `apply_sparse_operator(use_jax=True)` calls) from seconds to the O(N_pix * K) brute force.
594+
operator_use_jax = np.asarray(
595+
aa.util.inversion_interferometer.nufft_precision_operator_from(
596+
use_jax=True, **inputs
597+
)
598+
)
599+
600+
np.testing.assert_array_equal(operator_use_jax, operator_via_nufft)
601+
_assert_matches_brute_force(operator_use_jax, operator_via_np)
602+
603+
# The control: the NUFFT array is not the JAX brute-force array, so the assertion above is
604+
# testing the routing and not two builders that happen to agree bitwise.
605+
assert not np.array_equal(operator_use_jax, operator_via_jax)
606+
590607
# The default is the NUFFT builder, and it agrees with the brute force.
591608
operator_default = np.asarray(
592609
aa.util.inversion_interferometer.nufft_precision_operator_from(**inputs)
@@ -616,10 +633,17 @@ def test__nufft_precision_operator_from__disable_jax_falls_back_to_the_numpy_bui
616633

617634
# `PYAUTO_DISABLE_JAX=1` is the harness-level kill switch. Both the default NUFFT builder and
618635
# the `"jax"` brute force run on JAX, so both must fall back -- and loudly, because the NumPy
619-
# brute force is O(N_pix * K) where the NUFFT is O(K * nspread^2 + M log M).
636+
# brute force is O(N_pix * K) where the NUFFT is O(K * nspread^2 + M log M). `use_jax=True`
637+
# falls back either way: ignored under the default, and demoted again when it upgrades
638+
# `method="numpy"` to the JAX brute force.
620639
monkeypatch.setenv("PYAUTO_DISABLE_JAX", "1")
621640

622-
for kwargs in ({}, {"method": "jax"}, {"use_jax": True}):
641+
for kwargs in (
642+
{},
643+
{"method": "jax"},
644+
{"use_jax": True},
645+
{"method": "numpy", "use_jax": True},
646+
):
623647
caplog.clear()
624648

625649
with caplog.at_level("WARNING"):

0 commit comments

Comments
 (0)