Skip to content

perf: numba CPU curvature matrix F at HST resolution (phase 1: split + FFT mapper×func block) #505

Description

@Jammy2211

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 densecurvature_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

  1. 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.
  2. 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.
  3. 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}.pySTEP_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).

  1. 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).
  2. 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.
  3. 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.
  4. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions