Overview
Successor to the numba-cpu-likelihood epic (COMPLETE 2026-08-28; 21.3 s → 1.25 s per HST evaluation). The one step left on the numba CPU (use_jax=False, apply_sparse_operator_cpu()) path is the curvature matrix F: 0.997 s = 78 % of a 1.28 s HST rectangular evaluation and 55 % on Delaunay-1250. Target: ≥ 2× on the F step (HST rectangular evaluation ≤ ~0.7 s) with the log-likelihood unchanged to pinned tolerance (hst rectangular pin 27661.910133664103).
Code exploration found the prompt's candidate list partly stale: the mapper block already exploits symmetry (half-stored PSF pairs, halved diagonal, fold + mirror in curvature_matrix_via_sparse_operator_from); the mapper × linear-func block is dense — curvature_matrix_off_diags_via_mapper_and_linear_func_curvature_vector_from expands the 60 MGE curvature weights onto the full native grid and runs a direct sliding-window correlation (convolve_with_kernel_native) over ny × nx × ky × kx × 60 every evaluation; and there are redundant per-evaluation passes (a global re-mirror of an already-symmetric matrix with a fresh O(P²) allocation, np.array copies of the unique-mapping arrays, per-pair / noise_map**2 of memoized operated matrices).
This is phase 1 of a split (Brain FeatureDecision: split-into-phases). Threading is not a lever: Nautilus samples with Python multiprocessing, one process per core, so prange would oversubscribe (N procs × N threads). Phase 1 uses single-thread algorithmic wins only; any threading lever would need to be measured under the real pool as well as OMP_NUM_THREADS=1 before it is even considered.
Plan
- Instrument F: expose its sub-blocks (mapper diag, mapper × linear-func, func × func, mirror) as separately timeable steps in the
autolens_profiling breakdown harness; record the split at hst + euclid, rectangular (Bilinear + RTU) + Delaunay-1250, with OMP_NUM_THREADS=1 AUTOARRAY_NUMBA_OPERATED_MEMO=0. Checkpoint: the split decides whether the FFT step runs.
- Commit the 2026-08-28 re-baseline artifacts (regenerated, not hand-copied) — the committed
*_v2026.8.17.1 artifacts pre-date PyAutoArray#462 and still show an 18.8 s "sparse triplets" step.
- Remove the redundant per-evaluation passes (global mirror, array copies, repeated noise-weighting). Bit-identical output expected.
- If the split confirms the dense mapper × linear-func convolution dominates: route it through the existing batched FFT
Convolver machinery (phase-1 lever, PyAutoArray#497) with the flipped kernel (the current kernel computes a correlation), keeping a numba kernel for the scatter/accumulate only. Parity pinned at rtol 1e-6 with a brute-force test on an asymmetric PSF.
test_autoarray green; smoke cpu_fast_modeling.py; measure after on the same four cells plus one Nautilus pool run (catches any implicit FFT/BLAS thread pool oversubscribing); record before/after in autolens_profiling results + a note; post the numbers here.
- Phase 2 is filed at close-out only if phase 1 misses 2× (mapper-independent hoisting the split reveals).
Detailed implementation plan
Work Classification
Both — library (PyAutoArray) first, workspace (autolens_profiling) follow-up once the private helpers exist.
Affected Repositories
- PyAutoArray (primary)
- autolens_profiling
Branch Survey
| Repository |
Current Branch |
Dirty? |
Claim |
| ./PyAutoArray |
main |
clean |
none |
| ./autolens_profiling |
main |
clean |
claimed by nuts-warm-start-driver-and-a100-probe — disjoint file set (scripts/misc/searches/, scripts/imaging/searches/nuts/, results/notes/inference/), so a parallel worktree is used |
Suggested branch: feature/numba-hst-curvature-matrix-speedup
Worktree root: ~/Code/PyAutoLabs-wt/numba-hst-curvature-matrix-speedup/ (created by /start_library)
Implementation Steps
Step 0 — instrument
PyAutoArray/autoarray/inversion/inversion/imaging_numba/sparse.py: split _curvature_matrix_func_list_and_mapper into private helpers that each return their block — _curvature_matrix_mapper_diag (exists), _curvature_matrix_mapper_func_blocks_from(curvature_matrix), _curvature_matrix_func_func_blocks_from(curvature_matrix). No behaviour change.
autolens_profiling/scripts/imaging/likelihood_breakdown/pixelization_numba.py + delaunay_numba.py: add STEP_ACCESSORS entries for each sub-block (mapper diag → mapper×func → func×func → mirror) ahead of the existing Curvature matrix F entry (incremental-cost scheme attributes the time); keep the F entry as the total.
- Run the four cells (
--instrument hst|euclid, --rect-mesh bilinear|rtu; _profile_cli.py:128-140) with OMP_NUM_THREADS=1 AUTOARRAY_NUMBA_OPERATED_MEMO=0; commit JSON+PNG under results/breakdown/imaging/; post the split table here. Checkpoint.
Step 1 — redundant passes (bit-identical)
4. sparse.py:347: drop the global curvature_matrix_mirrored_from — the x1-mapper path is already folded+mirrored (util.py:596-602); in the func-list path write the mapper×func block's transpose into [func, mapper] at sparse.py:540; multi-mapper path keeps off_diag_0 + off_diag_1.T and places both blocks. Grep other callers of curvature_matrix_mirrored_from before removing anything.
5. sparse.py:389-393: pass mapper.unique_mappings.* arrays directly (verify dtypes; drop the np.array copies unless a cast is needed).
6. sparse.py:527-530 / 552: form operated / noise_map**2 once per linear func, hoisted out of the mapper loop.
7. Fix the curvature_matrix docstring (sparse.py:336-338, says "not a cached property" under @cached_property).
Step 2 — FFT for the mapper × linear-func block (gated by step 0)
8. In sparse.py, compute blurred_slim for curvature_weights ([n_unmasked, n_funcs]) via the existing FFT Convolver path (autoarray/operators/convolver.py; reuse the dataset's preloaded ConvolverState, Convolver.state_from(mask) / is_for_mask) with the flipped PSF kernel — the current kernel computes a correlation (util.py:806-812). Zero outside the mask. scipy.fft workers=1.
9. Split the numba kernel: keep only the scatter/accumulate loop (util.py:770-777) as curvature_matrix_off_diags_via_mapper_and_blurred_curvature_weights_from(...); keep the old dense kernel + convolve_with_kernel_native as the reference for tests.
10. Parity test in test_autoarray/inversion/inversion/imaging/test_inversion_imaging_util.py: FFT-path block vs dense kernel, small masked grid, asymmetric PSF (catches the flip), rtol=1e-6; extend test_curvature_matrix_func_list_blocks.py::test__curvature_matrix_func_list_blocks__matches_brute_force if it lacks an asymmetric kernel.
Step 3 — measure + ship
11. Re-run the four cells + one Nautilus pool run (cpu_fast_modeling.py, number_of_cores = machine cores; per-iteration wall-clock must improve by ~the single-thread factor); commit artifacts; add autolens_profiling/results/notes/numba_curvature_matrix_f_split.md with the before/after table.
12. ship_library (PyAutoArray) first; ship_workspace (autolens_profiling) after the library PR's API summary.
Key Files
PyAutoArray/autoarray/inversion/inversion/imaging_numba/sparse.py — F assembly (curvature_matrix @319, mapper diag @362, mapper×func @520-545, func×func @550-577, global mirror @347)
PyAutoArray/autoarray/inversion/inversion/imaging_numba/inversion_imaging_numba_util.py — kernels: sparse-op diag @515-604, off-diag @607-694, dense mapper×func @697-778, convolve_with_kernel_native @781-815, mirror @495-512
PyAutoArray/autoarray/operators/convolver.py — batched FFT Convolver / ConvolverState (existing lever)
PyAutoArray/test_autoarray/inversion/inversion/imaging/test_inversion_imaging_util.py, test_autoarray/inversion/inversion/test_curvature_matrix_func_list_blocks.py — parity tests
autolens_profiling/scripts/imaging/likelihood_breakdown/{pixelization_numba,delaunay_numba}.py — STEP_ACCESSORS harness; autolens_profiling/_profile_cli.py — instrument / --rect-mesh selection; results/breakdown/imaging/ — artifacts
Verification
test_autoarray green in the worktree; new parity tests pass.
- Harness pins unchanged (hst rectangular 27661.910133664103) on the unperturbed instance.
autolens_workspace/scripts/imaging/features/pixelization/cpu_fast_modeling.py smoke profile.
- Before/after table hst/euclid × bilinear/rtu + Delaunay-1250 and the pool run posted here; F ≥ 2× or phase 2 filed with the measured residual.
Original Prompt
Click to expand starting prompt
Numba CPU likelihood at HST resolution: speed up the curvature matrix F (the 78% step)
Type: feature
Epic: none (successor to numba-cpu-likelihood, COMPLETE 2026-08-28)
Target: autoarray
Repos:
- @PyAutoArray
- @autolens_profiling
Themes:
- numba-cpu
- pixelization
- profiling
Difficulty: large
Autonomy: supervised
Priority: high
Status: formalised
Filed: 2026-08-28
Successor to epic numba-cpu-likelihood (epics.md; records complete/2026/08/numba-cpu-*.md).
That epic took the numba CPU (use_jax=False, apply_sparse_operator_cpu()) likelihood from
21.3 s → 1.25 s per evaluation at HST resolution on the default RectangularBilinearAdaptDensity
mesh and 4.5 → ~1.6 s on the Delaunay-1250 fiducial. What is left is one step.
Where the time goes now (2026-08-28, pixelization_numba.py / delaunay_numba.py breakdowns, OMP_NUM_THREADS=1, AUTOARRAY_NUMBA_OPERATED_MEMO=0, PyAutoArray main 1f5c636)
| step |
hst rectangular (1.28 s) |
euclid rectangular (0.36 s) |
hst Delaunay-1250 (3.22 s) |
| Curvature matrix F [numba sparse-op] |
0.997 s (78%) |
0.211 s (59%) |
1.77 s (55%) |
| MGE operated mapping matrix (60 funcs) |
0.189 s |
0.077 s |
— |
| Reconstruction solve (warm-started NNLS) |
0.008 s |
0.008 s |
0.54 s (warm: ~0.07 s) |
| everything else |
≤ 0.03 s each |
≤ 0.02 s each |
inversion build ~0.5 s |
F is the curvature_matrix of InversionImagingSparseNumba
(autoarray/inversion/inversion/imaging_numba/sparse.py, kernels in
inversion_imaging_numba_util.py): the mapper × PSF-precision-operator × mapper contraction over the
sparse-operator (w-tilde heritage) representation, plus the linear-func blocks (already noise-weighted
once and mirrored, phase 1). It is rebuilt every evaluation because the mapper changes with the mass
model — no cross-evaluation memo applies.
Goal
Make F substantially cheaper on the numba CPU path at HST resolution (target: ≥ 2× on the F step,
i.e. an HST rectangular evaluation ≤ ~0.7 s; Delaunay similar), with the log-likelihood unchanged to
pinned tolerance (delaunay_numba.py / pixelization_numba.py pins; hst rectangular pin
27661.910133664103).
- Step 0 — decompose F itself. The breakdown cells time
fit.inversion.curvature_matrix as one
step. Instrument the kernel(s) behind it (mapper-mapper block, mapper-linear-func blocks,
linear-func-linear-func block, any dense fill / symmetrisation / noise weighting) and record the
split at hst + euclid, rectangular (default) + Delaunay. Commit the re-baseline artifacts from the
2026-08-28 re-profile too (pixelization_numba_breakdown_{euclid,hst} incl. the _rtu variants —
numbers in complete/2026/08/numba-cpu-kernel-cdf-fast-path.md; regenerate, do not hand-copy).
- Pick the lever from the split, not from doctrine. Candidates to evaluate, cheapest first:
- the unique-mappings compression (
data_slim_to_pixelization_unique_from) — is F's inner loop
iterating over data pixels × PSF footprint × source pixels where a per-source-pixel / per-unique
-mapping formulation would be smaller;
- symmetry — compute the upper triangle only and mirror (the linear-func blocks already do);
numba.prange over the outer loop with the OMP thread count (the campaign runs one process per
core under Nautilus, so gains must be measured with OMP_NUM_THREADS=1 AND with the pool);
- reuse across evaluations of whatever does NOT depend on the mapper (PSF-precision-operator
products are already preloaded — verify nothing mapper-independent is recomputed);
- the MGE operated-matrix term is second (0.19 s at hst) — the phase-1 batching left the
per-evaluation convolution of the varying profiles; check whether it is FFT- or scatter-bound.
- Ship behind exact parity (bit-level where the summation order is unchanged, pinned rtol 1e-6
otherwise); test_autoarray green; smoke autolens_workspace/scripts/imaging/features/pixelization/ cpu_fast_modeling.py on the smoke profile.
- Measure before/after on the four cells above; record results + a note in
autolens_profiling.
Out of scope: the JAX path; the RTU / kernel-CDF meshes (GPU-only by decision, 2026-08-28); the NNLS
solve (done — nnls_warm_start_memo).
Overview
Successor to the
numba-cpu-likelihoodepic (COMPLETE 2026-08-28; 21.3 s → 1.25 s per HST evaluation). The one step left on the numba CPU (use_jax=False,apply_sparse_operator_cpu()) path is the curvature matrix F: 0.997 s = 78 % of a 1.28 s HST rectangular evaluation and 55 % on Delaunay-1250. Target: ≥ 2× on the F step (HST rectangular evaluation ≤ ~0.7 s) with the log-likelihood unchanged to pinned tolerance (hst rectangular pin 27661.910133664103).Code exploration found the prompt's candidate list partly stale: the mapper block already exploits symmetry (half-stored PSF pairs, halved diagonal, fold + mirror in
curvature_matrix_via_sparse_operator_from); the mapper × linear-func block is dense —curvature_matrix_off_diags_via_mapper_and_linear_func_curvature_vector_fromexpands the 60 MGE curvature weights onto the full native grid and runs a direct sliding-window correlation (convolve_with_kernel_native) overny × nx × ky × kx × 60every evaluation; and there are redundant per-evaluation passes (a global re-mirror of an already-symmetric matrix with a fresh O(P²) allocation,np.arraycopies of the unique-mapping arrays, per-pair/ noise_map**2of memoized operated matrices).This is phase 1 of a split (Brain FeatureDecision: split-into-phases). Threading is not a lever: Nautilus samples with Python
multiprocessing, one process per core, soprangewould oversubscribe (N procs × N threads). Phase 1 uses single-thread algorithmic wins only; any threading lever would need to be measured under the real pool as well asOMP_NUM_THREADS=1before it is even considered.Plan
autolens_profilingbreakdown harness; record the split at hst + euclid, rectangular (Bilinear + RTU) + Delaunay-1250, withOMP_NUM_THREADS=1 AUTOARRAY_NUMBA_OPERATED_MEMO=0. Checkpoint: the split decides whether the FFT step runs.*_v2026.8.17.1artifacts pre-date PyAutoArray#462 and still show an 18.8 s "sparse triplets" step.Convolvermachinery (phase-1 lever, PyAutoArray#497) with the flipped kernel (the current kernel computes a correlation), keeping a numba kernel for the scatter/accumulate only. Parity pinned at rtol 1e-6 with a brute-force test on an asymmetric PSF.test_autoarraygreen; smokecpu_fast_modeling.py; measure after on the same four cells plus one Nautilus pool run (catches any implicit FFT/BLAS thread pool oversubscribing); record before/after inautolens_profilingresults + a note; post the numbers here.Detailed implementation plan
Work Classification
Both — library (PyAutoArray) first, workspace (autolens_profiling) follow-up once the private helpers exist.
Affected Repositories
Branch Survey
nuts-warm-start-driver-and-a100-probe— disjoint file set (scripts/misc/searches/,scripts/imaging/searches/nuts/,results/notes/inference/), so a parallel worktree is usedSuggested branch:
feature/numba-hst-curvature-matrix-speedupWorktree root:
~/Code/PyAutoLabs-wt/numba-hst-curvature-matrix-speedup/(created by/start_library)Implementation Steps
Step 0 — instrument
PyAutoArray/autoarray/inversion/inversion/imaging_numba/sparse.py: split_curvature_matrix_func_list_and_mapperinto private helpers that each return their block —_curvature_matrix_mapper_diag(exists),_curvature_matrix_mapper_func_blocks_from(curvature_matrix),_curvature_matrix_func_func_blocks_from(curvature_matrix). No behaviour change.autolens_profiling/scripts/imaging/likelihood_breakdown/pixelization_numba.py+delaunay_numba.py: addSTEP_ACCESSORSentries for each sub-block (mapper diag → mapper×func → func×func → mirror) ahead of the existingCurvature matrix Fentry (incremental-cost scheme attributes the time); keep the F entry as the total.--instrument hst|euclid,--rect-mesh bilinear|rtu;_profile_cli.py:128-140) withOMP_NUM_THREADS=1 AUTOARRAY_NUMBA_OPERATED_MEMO=0; commit JSON+PNG underresults/breakdown/imaging/; post the split table here. Checkpoint.Step 1 — redundant passes (bit-identical)
4.
sparse.py:347: drop the globalcurvature_matrix_mirrored_from— the x1-mapper path is already folded+mirrored (util.py:596-602); in the func-list path write the mapper×func block's transpose into[func, mapper]atsparse.py:540; multi-mapper path keepsoff_diag_0 + off_diag_1.Tand places both blocks. Grep other callers ofcurvature_matrix_mirrored_frombefore removing anything.5.
sparse.py:389-393: passmapper.unique_mappings.*arrays directly (verify dtypes; drop thenp.arraycopies unless a cast is needed).6.
sparse.py:527-530/552: formoperated / noise_map**2once per linear func, hoisted out of the mapper loop.7. Fix the
curvature_matrixdocstring (sparse.py:336-338, says "not a cached property" under@cached_property).Step 2 — FFT for the mapper × linear-func block (gated by step 0)
8. In
sparse.py, computeblurred_slimforcurvature_weights([n_unmasked, n_funcs]) via the existing FFTConvolverpath (autoarray/operators/convolver.py; reuse the dataset's preloadedConvolverState,Convolver.state_from(mask)/is_for_mask) with the flipped PSF kernel — the current kernel computes a correlation (util.py:806-812). Zero outside the mask.scipy.fftworkers=1.9. Split the numba kernel: keep only the scatter/accumulate loop (
util.py:770-777) ascurvature_matrix_off_diags_via_mapper_and_blurred_curvature_weights_from(...); keep the old dense kernel +convolve_with_kernel_nativeas the reference for tests.10. Parity test in
test_autoarray/inversion/inversion/imaging/test_inversion_imaging_util.py: FFT-path block vs dense kernel, small masked grid, asymmetric PSF (catches the flip),rtol=1e-6; extendtest_curvature_matrix_func_list_blocks.py::test__curvature_matrix_func_list_blocks__matches_brute_forceif it lacks an asymmetric kernel.Step 3 — measure + ship
11. Re-run the four cells + one Nautilus pool run (
cpu_fast_modeling.py,number_of_cores= machine cores; per-iteration wall-clock must improve by ~the single-thread factor); commit artifacts; addautolens_profiling/results/notes/numba_curvature_matrix_f_split.mdwith the before/after table.12.
ship_library(PyAutoArray) first;ship_workspace(autolens_profiling) after the library PR's API summary.Key Files
PyAutoArray/autoarray/inversion/inversion/imaging_numba/sparse.py— F assembly (curvature_matrix@319, mapper diag @362, mapper×func @520-545, func×func @550-577, global mirror @347)PyAutoArray/autoarray/inversion/inversion/imaging_numba/inversion_imaging_numba_util.py— kernels: sparse-op diag @515-604, off-diag @607-694, dense mapper×func @697-778,convolve_with_kernel_native@781-815, mirror @495-512PyAutoArray/autoarray/operators/convolver.py— batched FFTConvolver/ConvolverState(existing lever)PyAutoArray/test_autoarray/inversion/inversion/imaging/test_inversion_imaging_util.py,test_autoarray/inversion/inversion/test_curvature_matrix_func_list_blocks.py— parity testsautolens_profiling/scripts/imaging/likelihood_breakdown/{pixelization_numba,delaunay_numba}.py—STEP_ACCESSORSharness;autolens_profiling/_profile_cli.py— instrument /--rect-meshselection;results/breakdown/imaging/— artifactsVerification
test_autoarraygreen in the worktree; new parity tests pass.autolens_workspace/scripts/imaging/features/pixelization/cpu_fast_modeling.pysmoke profile.Original Prompt
Click to expand starting prompt
Numba CPU likelihood at HST resolution: speed up the curvature matrix F (the 78% step)
Type: feature
Epic: none (successor to numba-cpu-likelihood, COMPLETE 2026-08-28)
Target: autoarray
Repos:
Themes:
Difficulty: large
Autonomy: supervised
Priority: high
Status: formalised
Filed: 2026-08-28
Where the time goes now (2026-08-28,
pixelization_numba.py/delaunay_numba.pybreakdowns,OMP_NUM_THREADS=1,AUTOARRAY_NUMBA_OPERATED_MEMO=0, PyAutoArray main 1f5c636)F is the
curvature_matrixofInversionImagingSparseNumba(
autoarray/inversion/inversion/imaging_numba/sparse.py, kernels ininversion_imaging_numba_util.py): the mapper × PSF-precision-operator × mapper contraction over thesparse-operator (w-tilde heritage) representation, plus the linear-func blocks (already noise-weighted
once and mirrored, phase 1). It is rebuilt every evaluation because the mapper changes with the mass
model — no cross-evaluation memo applies.
Goal
Make F substantially cheaper on the numba CPU path at HST resolution (target: ≥ 2× on the F step,
i.e. an HST rectangular evaluation ≤ ~0.7 s; Delaunay similar), with the log-likelihood unchanged to
pinned tolerance (
delaunay_numba.py/pixelization_numba.pypins; hst rectangular pin27661.910133664103).
fit.inversion.curvature_matrixas onestep. Instrument the kernel(s) behind it (mapper-mapper block, mapper-linear-func blocks,
linear-func-linear-func block, any dense fill / symmetrisation / noise weighting) and record the
split at hst + euclid, rectangular (default) + Delaunay. Commit the re-baseline artifacts from the
2026-08-28 re-profile too (
pixelization_numba_breakdown_{euclid,hst}incl. the_rtuvariants —numbers in
complete/2026/08/numba-cpu-kernel-cdf-fast-path.md; regenerate, do not hand-copy).data_slim_to_pixelization_unique_from) — is F's inner loopiterating over data pixels × PSF footprint × source pixels where a per-source-pixel / per-unique
-mapping formulation would be smaller;
numba.prangeover the outer loop with the OMP thread count (the campaign runs one process percore under Nautilus, so gains must be measured with
OMP_NUM_THREADS=1AND with the pool);products are already preloaded — verify nothing mapper-independent is recomputed);
per-evaluation convolution of the varying profiles; check whether it is FFT- or scatter-bound.
otherwise);
test_autoarraygreen; smokeautolens_workspace/scripts/imaging/features/pixelization/ cpu_fast_modeling.pyon the smoke profile.autolens_profiling.Out of scope: the JAX path; the RTU / kernel-CDF meshes (GPU-only by decision, 2026-08-28); the NNLS
solve (done —
nnls_warm_start_memo).