perf: NumPy/scipy application path for InterferometerSparseOperator (xp=np never imports JAX) - #544
Merged
Jammy2211 merged 1 commit intoSep 8, 2026
Conversation
…542) The operator now keeps the raw (2y, 2x) real preload as `nufft_precision_operator` and lazily builds both transforms of it: `Khat` (JAX `rfft2`, under `jax.ensure_compile_time_eval()`) and `khat_np` (`scipy.fft.rfft2`). An operator constructed and applied with `xp=np` therefore never imports JAX. All six public methods (`apply_operator`, `curvature_matrix_diag_from`, `curvature_matrix_off_diag_from`, `operated_matrix_slim_from`, `curvature_matrix_off_diag_func_list_from`, `curvature_matrix_func_list_from`) take an `xp` array module and branch: `jax.numpy` keeps the existing `lax.fori_loop` / `segment_sum` / `dynamic_update_slice` bodies unchanged, `numpy` (the default) runs scipy `rfft2`/`irfft2` with a `scipy.sparse` CSC projection and a plain Python block loop (no padding or masking, which exist only to keep traced shapes static). The diagonal NumPy body symmetrises like the JAX one so the two agree to the pin. `InversionInterferometerSparse` passes `xp=self._xp` to every operator call, so the application backend follows the inversion's `xp` — what the fit passes — rather than the dataset's `use_jax` kwarg, which after #541 only selects a brute-force *builder* for the preload. The `np.array(curvature_matrix)` workaround at sparse.py:171 (needed only because the operator used to return JAX arrays on a NumPy fit) is deleted. Delaunay's padded `col = -1` triplets are dropped explicitly when building the CSC matrix. Verified: parity NumPy vs JAX at <= 5e-16 relative on all six methods (7x7/K=5, 12x12/K=64 seeded, Delaunay duplicate-COO, and a `batch_size=4` case exercising the block loop); end-to-end inversion curvature parity 2.8e-14; a subprocess test asserts `"jax" not in sys.modules` after a full NumPy fit; NumPy `curvature_matrix_diag_from` is 3.8x faster than JAX-CPU on a 40x40 / S=400 probe. 1486 tests pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hLF3ZAcz5MmaSJBEcLkvF
Jammy2211
deleted the
feature/interferometer-sparse-operator-numpy-cpu-path
branch
September 8, 2026 18:45
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.
Summary
InterferometerSparseOperatorusedjax.numpyunconditionally in every method, so a CPUuser running
xp=npstill executed the whole curvature build through JAX — 1.1–1.3× slowerthan a NumPy/scipy
rfft2route, and it forced JAX into the process of a fit that had askedfor NumPy.
The operator now keeps the raw
(2y, 2x)real preload asnufft_precision_operatorandbuilds both transforms of it lazily:
Khat(JAXrfft2) andkhat_np(
scipy.fft.rfft2). All six public methods take anxparray module and branch on it:apply_operatorjnp.fft.rfft2×Khat→irfft2scipy.fft.rfft2×khat_np→irfft2curvature_matrix_diag_fromlax.fori_loop+segment_sumscipy.sparseCSCA, Python block loop,Aᵀ @ Gcurvature_matrix_off_diag_fromA1's columnsA1's columnsoperated_matrix_slim_from.at[].setscattercurvature_matrix_off_diag_func_list_fromdynamic_update_slicecurvature_matrix_func_list_fromsegment_sumAᵀ @Design: the backend follows the inversion's
xp.InversionInterferometerSparsepassesxp=self._xpto every operator call, so what the fit asked for decides the backend. Thedataset's
use_jaxkwarg only picks a brute-force builder for the preload (since #541) andnever touches the application path. This is the repo's existing convention
(
transformer.image_from(xp=)), and it removes the surprising split the issue names withoutchanging any dataset API. The JAX bodies are unchanged; the
np.array(curvature_matrix)workaround in
sparse.py(needed only because the operator used to hand JAX arrays back to aNumPy fit) is deleted.
No-JAX guarantee
A NumPy fit never imports JAX. The lazy
Khat/col_offsetsare the mechanism; asubprocess test runs a full
InversionInterferometerSparse(xp=np)fit and asserts"jax" not in sys.modulesafterwards. The new tests carry noimportorskip("jax"), so theunittest-nojaxCI leg runs them.Parity and speed
batch_size=4block loop, Delaunay duplicate-COO)InversionInterferometerSparsecurvature matrixcurvature_matrix_diag_from, 40×40 grid / S=400Pins are the repo idiom
assert_allclose(rtol=1e-10, atol=1e-10*max|ref|).Two traps worth recording
Khatlazily inside a method that is itself traced (the JAXcurvature bodies run under
lax.fori_loop) turns the preload transform into a tracedconstant.
jax.ensure_compile_time_eval()around the transform is what makes the lazinesssafe — without it the operator's static aux data leaks into the trace.
col = -1padding. Delaunay's triplets are padded withcol = -1entries. JAX'ssegment_sumdrops out-of-range segments silently;scipy.sparse.csc_matrixdoes not — itraises. The NumPy builder filters
0 <= col < Sexplicitly.API Changes
Additive only. Six
InterferometerSparseOperatormethods gain a keyword-onlyxp=npargument, defaulting to the NumPy branch. The dataclass gains a
nufft_precision_operatorfield (the raw preload, previously discarded after the FFT) andKhat/col_offsetsbecome cached properties rather than eager fields, with a newkhat_npalongside. No signature is broken and no symbol is removed.See full details below.
Test Plan
pytest test_autoarray -q→ 1486 passedpython -m black --checkclean on all five filescurvature_matrix,data_vector,reconstruction,log_evidence)"jax" not in sys.modulesafter a full NumPy fitDownstream note
PyAutoLens/autolens/potential_correction/fit_interferometer.pyanditerative_interferometer.pycallcurvature_matrix_diag_fromwithoutxp, so they nowtake the NumPy branch. Both already wrap the result in
np.asarray, so they work unchangedand get faster. One profiling script,
autolens_profiling/scripts/interferometer/likelihood_breakdown/datacube/delaunay.py:1011,also calls it without
xpbut passes JAX arrays inside ajit— it needsxp=jnpadded; aprofiling-repo follow-up, no library or shipped-workspace impact.
Full API Changes (for automation & release notes)
Added
InterferometerSparseOperator.nufft_precision_operator: np.ndarray— the raw(2y, 2x)real preload, now kept rather than discarded after the FFT (the NumPy and forthcoming
numba CPU paths index it directly).
InterferometerSparseOperator.khat_np— cached property,scipy.fft.rfft2of the preload.Changed Signature
InterferometerSparseOperator.apply_operator(Fbatch_flat, xp=np)InterferometerSparseOperator.curvature_matrix_diag_from(rows, cols, vals, *, S, xp=np)InterferometerSparseOperator.curvature_matrix_off_diag_from(rows0, cols0, vals0, rows1, cols1, vals1, *, S0, S1, xp=np)InterferometerSparseOperator.operated_matrix_slim_from(matrix_slim, extent_index_for_masked_pixel, xp=np)InterferometerSparseOperator.curvature_matrix_off_diag_func_list_from(..., xp=np)InterferometerSparseOperator.curvature_matrix_func_list_from(..., xp=np)All are additive:
xpdefaults tonumpy.Changed Behaviour
InterferometerSparseOperator.Khatand.col_offsetsare cached properties, not eagerdataclass fields; they are computed on first access under
jax.ensure_compile_time_eval(). Reading them still returns JAX arrays.InversionInterferometerSparsemethods return NumPy arrays underxp=np(they returnedJAX arrays before), so callers no longer need
np.asarray.Migration
sparse_operator.curvature_matrix_diag_from(rows, cols, vals, S=S)— always JAX.xp=jnpexplicitly for the JAX branch (neededonly if the arrays are traced).
Closes #542
Generated by the PyAutoLabs agent workflow.
🤖 Generated with Claude Code
https://claude.ai/code/session_018hLF3ZAcz5MmaSJBEcLkvF