Overview
nufft_precision_operator_from builds the W~ preload by brute force — one cosine per (real-space offset, visibility) pair over the mask's bounding extent, O(N_pix·K) — and every w-tilde arm pays it once per dataset before a single likelihood is evaluated. That array is exactly Re of a type-1 (adjoint) NUFFT of the weights 1/σ² on the doubled extent grid, and should be built as one. Measured 289× on wall clock at alma (2101.5 s → 7.278 s) and 111× on CPU-seconds, with the log evidence bit-identical to the brute force at eps=1e-12. It removes a scaling wall rather than a constant, and it costs the library nothing new — nufftax is already a hard dependency of TransformerNUFFT.
Follow-up from autolens_profiling #229 (phase 3 of the retired epic numba-interferometer-revisit, merged as autolens_profiling PR #234; ledger PyAutoMind/complete/archive/epics/numba_interferometer_likelihood_revisit.md). Working: autolens_profiling/results/notes/numba_interferometer_verdict.md section 7 ("The preload") plus results/breakdown/interferometer/preload_breakdown_{sma,alma,alma_high}_v2026.8.17.1.json.
Measured saving
Wall seconds (CPU-seconds in brackets), single-threaded pinning where it takes:
| instrument |
K |
N_pix |
current numpy builder |
type-1 NUFFT (eps=1e-12) |
| sma |
190 |
3 852 |
0.1643 s (0.164) |
0.0210 s (0.031) |
| alma |
1 000 000 |
15 380 |
2101.5 s (2097.1) |
7.278 s (18.84) |
| alma_high |
5 000 000 |
61 572 |
refused (hours) |
22.15 s (89.99) |
The wall-clock ratio is not a single-core ratio: XLA's CPU runtime keeps its own intra-op pool that --xla_cpu_multi_thread_eigen=false does not close, so the NUFFT builder ran at 2.59× cores at alma and 4.06× at alma_high. Quote the CPU-seconds column (111×) before claiming a speed-up over a single-threaded loop.
Stacked on
This task is stacked on perf: apply the interferometer sparse operator with rfft2/irfft2 — #538, branch feature/interferometer-apply-operator-rfft2. Both tasks edit inversion_interferometer_util.py, in disjoint functions; to avoid two branches racing on one file, feature/interferometer-preload-nufft-type1 is cut from branch 538 and its PR is opened with base feature/interferometer-apply-operator-rfft2 (GitHub retargets it to main when #538 merges). /prm order is #538 first, then this one — this task's /prm must not run before #538 is merged.
Plan
- Add a type-1 NUFFT builder
nufft_precision_operator_via_nufft_from, ported from the pinned reference implementation in the profiling pack, with its derivation and the eight-mapping search condensed into the docstring.
- Give the dispatcher
nufft_precision_operator_from a method selector ("nufft" | "numpy" | "jax", default "nufft") that passes eps and chunk_size through; the two brute-force builders stay untouched as the test references.
- Fall back to
"numpy" loudly (never silently) in exactly two cases: JAX disabled via the existing kill switch, and nufftax not importable. Anything else raises.
- Plumb
method / eps / nufft_chunk_size through the interferometer dataset, defaulting to the transformer's own settings when it is a TransformerNUFFT, and rewrite the stale "N_vis·N_pix crossover" warning — the preload is now seconds, so the warning is about the DFT transformer, not the preload.
- Pin the mapping structurally, not just numerically: the parity rule, the exactly-zero padding row/column with non-zero neighbours, chunked == one-shot, and a control in which a wrong index permutation must fail the pin. Six of the eight candidate mappings are wrong by 21 % of the peak while being structurally plausible.
- Record in the docstring why
eps=1e-12 (it is the only value that also holds the array pin, and it saturates fp64), why chunking is mandatory (~15 GB one-shot at K=5e6), and the CPU-seconds caveat above.
Detailed implementation plan
Work Classification
Library
Affected Repositories
Branch Survey
| Repository |
Current Branch |
Dirty? |
| ./PyAutoArray |
main (47a00e8c) |
clean |
Suggested branch: feature/interferometer-preload-nufft-type1 (cut from feature/interferometer-apply-operator-rfft2, PR base the same until #538 merges)
Worktree root: ~/Code/PyAutoLabs-wt/interferometer-preload-nufft-type1/
Implementation Steps
- New
nufft_precision_operator_via_nufft_from(noise_map_real, uv_wavelengths, shape_masked_pixels_2d, grid_radians_2d, *, eps=1e-12, chunk_size=None) -> np.ndarray in autoarray/inversion/inversion/interferometer/inversion_interferometer_util.py, ported from autolens_profiling/scripts/misc/numba_interferometer/preload.py::nufft_preload_from: Δ_rad from the grid (square-pixel guard — port _pixel_scale_radians_from), x = 2π·u·Δ, y = 2π·v·Δ, w = σ⁻², chunked nufftax.nufft2d1(-x, y, w, (2Nx, 2Ny), eps, +1) accumulating Re f in float64, ifftshift, zero row Ny and column Nx, contiguous float64. Do not route this through image_from — that path applies a half-pixel shift and a row flip, and neither belongs here.
- Load
nufftax the way transformer.py does — reuse _load_nufftax() / nufftax_exception() by import, do not duplicate them.
- Dispatcher
nufft_precision_operator_from (line 80) gains method: str = "nufft" with values "nufft" | "numpy" | "jax", passing eps and chunk_size through. use_jax=True (existing kwarg, kept for compatibility) maps to method="jax". nufft_precision_operator_via_np_from / _via_jax_from stay untouched as reference builders.
- Two loud, logged fallbacks to
"numpy", never silent: disable_jax() is set (PYAUTO_DISABLE_JAX=1, the test-mode kill switch already honoured at dataset.py:318), or nufftax is not importable (logger.warning naming the O(N_pix·K) cost). Anything else raises.
autoarray/dataset/interferometer/dataset.py: psf_precision_operator_from (~line 398) and apply_sparse_operator (~line 229) accept method="nufft", eps=None, nufft_chunk_size=None; when None they take the transformer's own eps / chunk_size if it is a TransformerNUFFT, else 1e-12 / None.
- Rewrite the "N_vis·N_pix ≳ 1e7 crossover" warning text at
dataset.py:351-376: the preload is now seconds; the warning is about the DFT transformer, not the preload.
Tests
test_autoarray/inversion/inversion/interferometer/test_inversion_interferometer_util.py and test_autoarray/dataset/interferometer/test_dataset.py, porting the pins from the profiling pack's test_parity.py verbatim in spirit:
- NUFFT vs
_via_np_from on the shared 7×7 / K=5 fixture and a seeded 16×16 mask with K=300 (so the pin is not trivial), mixed tolerance assert_allclose(rtol=1e-10, atol=1e-10 * P[0, 0]), with a comment saying why mixed — a type-1 NUFFT bounds its error against Σ_k|c_k|, i.e. against the peak, so the preload's near-zero entries carry no relative guarantee.
- Padding row
Ny / column Nx exactly zero, and their neighbours non-zero (so the pin is not vacuous).
P[i, j] == P[-i, -j].
- Chunked (
chunk_size=64, K > 64) equals one-shot.
- Control: negating
uv[:, 0] must fail the pin (pytest.raises(AssertionError)).
- Dispatcher: default returns the NUFFT array;
method="numpy" returns the brute force; use_jax=True still routes to the JAX brute force; PYAUTO_DISABLE_JAX=1 falls back to numpy (extend the existing _disable_jax_overrides_an_explicit_use_jax test).
- End-to-end: the existing
test_interferometer.py sparse-vs-mapping comparisons run through the new default unchanged.
All under CPU JAX x64 (already enabled by autonerves on import); nufftax-gated tests use the conftest skip (pytest_collection_modifyitems, keep __dft__ in names where the fixture is DFT-based).
Verification
- Pins 1–6 pass;
test_dataset.py green.
pytest test_autoarray/inversion/inversion/interferometer test_autoarray/dataset/interferometer test_autoarray/operators -q green, plus the full test_autoarray/inversion tree once.
- The alma-scale timing is not re-measured here (no such dataset in PyAutoArray); the PR cites the phase-3 JSON numbers above.
Key Files
autoarray/inversion/inversion/interferometer/inversion_interferometer_util.py — dispatcher nufft_precision_operator_from (line 80), reference builders _via_np_from (249) / _via_jax_from (368), new _via_nufft_from.
autoarray/dataset/interferometer/dataset.py — apply_sparse_operator (~229), psf_precision_operator_from (~398), crossover warning (~351-376), JAX kill switch (~318).
autoarray/operators/transformer.py — _load_nufftax() / nufftax_exception() and the _x / _y convention (~334-335).
autolens_profiling/scripts/misc/numba_interferometer/preload.py — nufft_preload_from, the pinned reference implementation.
Related
Original Prompt
Click to expand starting prompt
Build nufft_precision_operator_from as a type-1 NUFFT — 35 minutes to 7 seconds
Type: feature
Target: autoarray
Repos:
- PyAutoArray
Themes:
- interferometer
- nufft
- likelihood-profiling
Difficulty: medium
Autonomy: supervised
Priority: high
Epic: numba-interferometer-revisit
Filed: 2026-09-08
Follow-up derived from autolens_profiling#229 (phase 3 of numba-interferometer-revisit)
— not a user request. Every number below is measured, and the working is in
autolens_profiling/results/notes/numba_interferometer_verdict.md section 7 ("The
preload") plus
results/breakdown/interferometer/preload_breakdown_{sma,alma,alma_high}_v2026.8.17.1.json.
What
nufft_precision_operator_from in
autoarray/inversion/inversion/interferometer/inversion_interferometer_util.py builds the
W~ preload by brute force: one cosine per (real-space offset, visibility) pair over the
mask's bounding extent, O(N_pix·K). Every w-tilde arm pays it once per dataset before a
single likelihood is evaluated.
That array is exactly Re of a type-1 (adjoint) NUFFT of the weights 1/σ² on the
doubled extent grid, and should be built as one:
x, y = 2π·u·Δ_rad, 2π·v·Δ_rad # the transformer's own _x/_y (transformer.py:334-335)
f = nufftax.nufft2d1(−x, y, w, n_modes=(2Nx, 2Ny), eps, isign=+1)
P = ifftshift(Re f)
P[Ny, :] = 0; P[:, Nx] = 0 # the FFT padding row/column the brute force never fills
Do not route this through image_from — that path applies a half-pixel shift and a row
flip, and neither belongs here.
Reference implementation, already written and pinned:
autolens_profiling/scripts/misc/numba_interferometer/preload.py::nufft_preload_from.
Why
Wall seconds (CPU-seconds in brackets), single-threaded pinning where it takes:
| instrument |
K |
N_pix |
current numpy builder |
type-1 NUFFT (eps=1e-12) |
| sma |
190 |
3 852 |
0.1643 s (0.164) |
0.0210 s (0.031) |
| alma |
1 000 000 |
15 380 |
2101.5 s (2097.1) |
7.278 s (18.84) |
| alma_high |
5 000 000 |
61 572 |
refused (hours) |
22.15 s (89.99) |
289× on wall clock at alma, 111× on CPU-seconds. It removes a scaling wall rather than a
constant — the brute force is linear in K, the NUFFT is K·nspread² + M log M — so the gap
widens with instrument size (predicted 1590× at alma_high). And it is not an approximation:
at eps=1e-12 the log evidence at alma is bit-identical to the brute force's
(-12050103.936303042, rel diff 0).
It costs the library nothing new: nufftax is already a hard dependency of
TransformerNUFFT, the mapping uses that transformer's own convention, and the same call
runs on GPU.
Requirements
- Keep
nufft_precision_operator_via_np_from as the test reference. Pin the new path
against it at a small K with the mixed tolerance
allclose(rtol=1e-10, atol=1e-10·P[0,0]) — a type-1 NUFFT bounds its error against
Σ_k|c_k|, i.e. against the peak, so the preload's near-zero entries (five orders below
it, at the fp64 noise floor) carry no relative guarantee and a relative-only test there
measures round-off, not the builder.
- Carry the structural pins, not just a smoke test. The exactly-zero padding row/column
(with non-zero neighbours, so the pin is not vacuous), P[i,j] == P[−i,−j], and a control
in which a wrong index permutation must fail the parity rule. Six of the eight candidate
mappings (axis swap × sign of x × sign of y) are wrong by 21 % of the peak while being
structurally plausible; the mapping is convention-bound and must be pinned as such.
- Default
eps=1e-12. Every value tested (1e-6, 1e-9, 1e-12) holds the rtol=1e-6
log-evidence pin by at least five orders of margin, so the dial is not load-bearing for
correctness — but 1e-12 is the only one that also holds the array pin at sma, it
saturates fp64 (1e-14 moves max|Δ| from 1.7e-17 only to 1.4e-17), and it costs
seconds. A preload is reused by every likelihood call in a fit; error budget spent here is
spent for the whole run.
- Visibility chunking is mandatory, not optional. The spreader's gather buffer is
K·nspread² complex128; alma_high's 5 M visibilities need ~15 GB in one shot and were
OOM-killed in the profiling run before chunking was added. Chunk at the transformer's
existing chunk_size (PyAutoArray#330) and pin chunked == one-shot.
- This applies to every backend. The JAX chunked builder becomes redundant, and the
dirty image already goes through the same adjoint — so the NUFFT path is the one builder,
not a GPU-only alternative. It also gives the GPU path a preload it does not currently
have.
Caveat to record
The wall-clock ratio is not a single-core ratio. XLA's CPU runtime keeps its own intra-op
pool that --xla_cpu_multi_thread_eigen=false does not close, so the NUFFT builder ran at
2.59× cores at alma and 4.06× at alma_high. Quote the CPU-seconds column (111×) before
claiming a speed-up over a single-threaded loop.
Side finding (not this task, but worth knowing)
The recovered numba brute-force builder is the only one of four that fails the alma array
pin, at 2.195e-11 of the peak against a 1e-11 bound. It is summation round-off, not an
algebraic disagreement: it accumulates all 10⁶ visibilities into one running scalar per
offset while the NumPy reference sums them in chunk_k = 2048 blocks, so the two diverge
with K (2e-15 at K = 190, 2e-11 at K = 10⁶, on 11 of 78 400 entries). The NUFFT
agrees with the reference a thousand times better than that numba builder does
(1.9e-14 vs 2.2e-11). A 10⁶-term naive accumulation is worth knowing about wherever
else one lives.
Overview
nufft_precision_operator_frombuilds theW~preload by brute force — one cosine per (real-space offset, visibility) pair over the mask's bounding extent,O(N_pix·K)— and every w-tilde arm pays it once per dataset before a single likelihood is evaluated. That array is exactlyReof a type-1 (adjoint) NUFFT of the weights1/σ²on the doubled extent grid, and should be built as one. Measured289×on wall clock at alma (2101.5 s → 7.278 s) and111×on CPU-seconds, with the log evidence bit-identical to the brute force ateps=1e-12. It removes a scaling wall rather than a constant, and it costs the library nothing new —nufftaxis already a hard dependency ofTransformerNUFFT.Follow-up from
autolens_profiling#229 (phase 3 of the retired epicnumba-interferometer-revisit, merged as autolens_profiling PR #234; ledgerPyAutoMind/complete/archive/epics/numba_interferometer_likelihood_revisit.md). Working:autolens_profiling/results/notes/numba_interferometer_verdict.mdsection 7 ("The preload") plusresults/breakdown/interferometer/preload_breakdown_{sma,alma,alma_high}_v2026.8.17.1.json.Measured saving
Wall seconds (CPU-seconds in brackets), single-threaded pinning where it takes:
KN_pixnumpybuildereps=1e-12)The wall-clock ratio is not a single-core ratio: XLA's CPU runtime keeps its own intra-op pool that
--xla_cpu_multi_thread_eigen=falsedoes not close, so the NUFFT builder ran at2.59×cores at alma and4.06×at alma_high. Quote the CPU-seconds column (111×) before claiming a speed-up over a single-threaded loop.Stacked on
This task is stacked on
perf: apply the interferometer sparse operator with rfft2/irfft2— #538, branchfeature/interferometer-apply-operator-rfft2. Both tasks editinversion_interferometer_util.py, in disjoint functions; to avoid two branches racing on one file,feature/interferometer-preload-nufft-type1is cut from branch 538 and its PR is opened with basefeature/interferometer-apply-operator-rfft2(GitHub retargets it tomainwhen #538 merges)./prmorder is #538 first, then this one — this task's/prmmust not run before #538 is merged.Plan
nufft_precision_operator_via_nufft_from, ported from the pinned reference implementation in the profiling pack, with its derivation and the eight-mapping search condensed into the docstring.nufft_precision_operator_fromamethodselector ("nufft" | "numpy" | "jax", default"nufft") that passesepsandchunk_sizethrough; the two brute-force builders stay untouched as the test references."numpy"loudly (never silently) in exactly two cases: JAX disabled via the existing kill switch, andnufftaxnot importable. Anything else raises.method/eps/nufft_chunk_sizethrough the interferometer dataset, defaulting to the transformer's own settings when it is aTransformerNUFFT, and rewrite the stale "N_vis·N_pix crossover" warning — the preload is now seconds, so the warning is about the DFT transformer, not the preload.eps=1e-12(it is the only value that also holds the array pin, and it saturates fp64), why chunking is mandatory (~15 GB one-shot at K=5e6), and the CPU-seconds caveat above.Detailed implementation plan
Work Classification
Library
Affected Repositories
Branch Survey
47a00e8c)Suggested branch:
feature/interferometer-preload-nufft-type1(cut fromfeature/interferometer-apply-operator-rfft2, PR base the same until #538 merges)Worktree root:
~/Code/PyAutoLabs-wt/interferometer-preload-nufft-type1/Implementation Steps
nufft_precision_operator_via_nufft_from(noise_map_real, uv_wavelengths, shape_masked_pixels_2d, grid_radians_2d, *, eps=1e-12, chunk_size=None) -> np.ndarrayinautoarray/inversion/inversion/interferometer/inversion_interferometer_util.py, ported fromautolens_profiling/scripts/misc/numba_interferometer/preload.py::nufft_preload_from:Δ_radfrom the grid (square-pixel guard — port_pixel_scale_radians_from),x = 2π·u·Δ,y = 2π·v·Δ,w = σ⁻², chunkednufftax.nufft2d1(-x, y, w, (2Nx, 2Ny), eps, +1)accumulatingRe fin float64,ifftshift, zero rowNyand columnNx, contiguous float64. Do not route this throughimage_from— that path applies a half-pixel shift and a row flip, and neither belongs here.nufftaxthe waytransformer.pydoes — reuse_load_nufftax()/nufftax_exception()by import, do not duplicate them.nufft_precision_operator_from(line 80) gainsmethod: str = "nufft"with values"nufft" | "numpy" | "jax", passingepsandchunk_sizethrough.use_jax=True(existing kwarg, kept for compatibility) maps tomethod="jax".nufft_precision_operator_via_np_from/_via_jax_fromstay untouched as reference builders."numpy", never silent:disable_jax()is set (PYAUTO_DISABLE_JAX=1, the test-mode kill switch already honoured atdataset.py:318), ornufftaxis not importable (logger.warningnaming theO(N_pix·K)cost). Anything else raises.autoarray/dataset/interferometer/dataset.py:psf_precision_operator_from(~line 398) andapply_sparse_operator(~line 229) acceptmethod="nufft",eps=None,nufft_chunk_size=None; whenNonethey take the transformer's owneps/chunk_sizeif it is aTransformerNUFFT, else1e-12/None.dataset.py:351-376: the preload is now seconds; the warning is about the DFT transformer, not the preload.Tests
test_autoarray/inversion/inversion/interferometer/test_inversion_interferometer_util.pyandtest_autoarray/dataset/interferometer/test_dataset.py, porting the pins from the profiling pack'stest_parity.pyverbatim in spirit:_via_np_fromon the shared 7×7 / K=5 fixture and a seeded 16×16 mask with K=300 (so the pin is not trivial), mixed toleranceassert_allclose(rtol=1e-10, atol=1e-10 * P[0, 0]), with a comment saying why mixed — a type-1 NUFFT bounds its error againstΣ_k|c_k|, i.e. against the peak, so the preload's near-zero entries carry no relative guarantee.Ny/ columnNxexactly zero, and their neighbours non-zero (so the pin is not vacuous).P[i, j] == P[-i, -j].chunk_size=64,K > 64) equals one-shot.uv[:, 0]must fail the pin (pytest.raises(AssertionError)).method="numpy"returns the brute force;use_jax=Truestill routes to the JAX brute force;PYAUTO_DISABLE_JAX=1falls back to numpy (extend the existing_disable_jax_overrides_an_explicit_use_jaxtest).test_interferometer.pysparse-vs-mapping comparisons run through the new default unchanged.All under CPU JAX x64 (already enabled by autonerves on import); nufftax-gated tests use the conftest skip (
pytest_collection_modifyitems, keep__dft__in names where the fixture is DFT-based).Verification
test_dataset.pygreen.pytest test_autoarray/inversion/inversion/interferometer test_autoarray/dataset/interferometer test_autoarray/operators -qgreen, plus the fulltest_autoarray/inversiontree once.Key Files
autoarray/inversion/inversion/interferometer/inversion_interferometer_util.py— dispatchernufft_precision_operator_from(line 80), reference builders_via_np_from(249) /_via_jax_from(368), new_via_nufft_from.autoarray/dataset/interferometer/dataset.py—apply_sparse_operator(~229),psf_precision_operator_from(~398), crossover warning (~351-376), JAX kill switch (~318).autoarray/operators/transformer.py—_load_nufftax()/nufftax_exception()and the_x/_yconvention (~334-335).autolens_profiling/scripts/misc/numba_interferometer/preload.py—nufft_preload_from, the pinned reference implementation.Related
PyAutoMind/complete/archive/epics/numba_interferometer_likelihood_revisit.mdOriginal Prompt
Click to expand starting prompt
Build
nufft_precision_operator_fromas a type-1 NUFFT — 35 minutes to 7 secondsType: feature
Target: autoarray
Repos:
Themes:
Difficulty: medium
Autonomy: supervised
Priority: high
Epic: numba-interferometer-revisit
Filed: 2026-09-08
Follow-up derived from
autolens_profiling#229(phase 3 ofnumba-interferometer-revisit)— not a user request. Every number below is measured, and the working is in
autolens_profiling/results/notes/numba_interferometer_verdict.mdsection 7 ("Thepreload") plus
results/breakdown/interferometer/preload_breakdown_{sma,alma,alma_high}_v2026.8.17.1.json.What
nufft_precision_operator_frominautoarray/inversion/inversion/interferometer/inversion_interferometer_util.pybuilds theW~preload by brute force: one cosine per (real-space offset, visibility) pair over themask's bounding extent,
O(N_pix·K). Every w-tilde arm pays it once per dataset before asingle likelihood is evaluated.
That array is exactly
Reof a type-1 (adjoint) NUFFT of the weights1/σ²on thedoubled extent grid, and should be built as one:
Do not route this through
image_from— that path applies a half-pixel shift and a rowflip, and neither belongs here.
Reference implementation, already written and pinned:
autolens_profiling/scripts/misc/numba_interferometer/preload.py::nufft_preload_from.Why
Wall seconds (CPU-seconds in brackets), single-threaded pinning where it takes:
KN_pixnumpybuildereps=1e-12)289×on wall clock at alma,111×on CPU-seconds. It removes a scaling wall rather than aconstant — the brute force is linear in
K, the NUFFT isK·nspread² + M log M— so the gapwidens with instrument size (predicted
1590×at alma_high). And it is not an approximation:at
eps=1e-12the log evidence at alma is bit-identical to the brute force's(
-12050103.936303042, rel diff0).It costs the library nothing new:
nufftaxis already a hard dependency ofTransformerNUFFT, the mapping uses that transformer's own convention, and the same callruns on GPU.
Requirements
nufft_precision_operator_via_np_fromas the test reference. Pin the new pathagainst it at a small
Kwith the mixed toleranceallclose(rtol=1e-10, atol=1e-10·P[0,0])— a type-1 NUFFT bounds its error againstΣ_k|c_k|, i.e. against the peak, so the preload's near-zero entries (five orders belowit, at the fp64 noise floor) carry no relative guarantee and a relative-only test there
measures round-off, not the builder.
(with non-zero neighbours, so the pin is not vacuous),
P[i,j] == P[−i,−j], and a controlin which a wrong index permutation must fail the parity rule. Six of the eight candidate
mappings (axis swap × sign of
x× sign ofy) are wrong by 21 % of the peak while beingstructurally plausible; the mapping is convention-bound and must be pinned as such.
eps=1e-12. Every value tested (1e-6,1e-9,1e-12) holds thertol=1e-6log-evidence pin by at least five orders of margin, so the dial is not load-bearing for
correctness — but
1e-12is the only one that also holds the array pin at sma, itsaturates fp64 (
1e-14movesmax|Δ|from1.7e-17only to1.4e-17), and it costsseconds. A preload is reused by every likelihood call in a fit; error budget spent here is
spent for the whole run.
K·nspread²complex128; alma_high's 5 M visibilities need ~15 GB in one shot and wereOOM-killed in the profiling run before chunking was added. Chunk at the transformer's
existing
chunk_size(PyAutoArray#330) and pin chunked == one-shot.dirty image already goes through the same adjoint — so the NUFFT path is the one builder,
not a GPU-only alternative. It also gives the GPU path a preload it does not currently
have.
Caveat to record
The wall-clock ratio is not a single-core ratio. XLA's CPU runtime keeps its own intra-op
pool that
--xla_cpu_multi_thread_eigen=falsedoes not close, so the NUFFT builder ran at2.59×cores at alma and4.06×at alma_high. Quote the CPU-seconds column (111×) beforeclaiming a speed-up over a single-threaded loop.
Side finding (not this task, but worth knowing)
The recovered numba brute-force builder is the only one of four that fails the alma array
pin, at
2.195e-11of the peak against a1e-11bound. It is summation round-off, not analgebraic disagreement: it accumulates all
10⁶visibilities into one running scalar peroffset while the NumPy reference sums them in
chunk_k = 2048blocks, so the two divergewith
K(2e-15atK = 190,2e-11atK = 10⁶, on 11 of 78 400 entries). The NUFFTagrees with the reference a thousand times better than that numba builder does
(
1.9e-14vs2.2e-11). A10⁶-term naive accumulation is worth knowing about whereverelse one lives.