Repository navigation
Description
Overview
Research task (batch 2026-08-31-pm, member numba-vs-jax-sparse, --auto, effective level supervised = min(header safe, research cap)). The subhalo_validation end-to-end runs put the numba CPU path ~8x faster than the old code. The human asked whether the linear-algebra approach behind that speed is the same as the sparse-operator mode already implemented for JAX — and if not, whether the numba approach is amenable to GPU-JAX speed-up or rests on sparsity exploitation that suits it to CPU. Deliverable is a written verdict posted on this issue; no library changes.
Plan
- Characterise the numba CPU positive-only lineage as shipped: the
SparseLinAlgImagingNumbaPSF-precision preload, the sparse F contraction (perf: numba CPU curvature matrix F at HST resolution (phase 1: split + FFT mapper×func block) #505/perf(numba): HST curvature matrix phase 2 — mapper×mapper block + MGE operated matrix #507 two-stage accumulator), and the fnnls active-set solve with the cross-evaluation warm-start memo (perf: cut positive-only solve active-set iterations — warm-start memo + diagnostics (numba CPU likelihood, phase 3a) #498/perf: warm-start the positive-only NNLS solve from the previous evaluation's passive set (#498) #501). - Characterise the JAX sparse-operator mode: the FFT-backed matrix-free
ImagingSparseOperator, batched F assembly, and the jaxnnls PDIP solve with its relaxed-KKT custom VJP. - Compare formulation: operator storage, curvature assembly, solve strategy — same normal-equations problem or different mathematics?
- For each point of difference, assess GPU-JAX portability (batched/dense enough? data-dependent control flow? cross-evaluation state?) against the closed solver ledger (
nnls_solver_ledger.md: PDIP/BPP/ADMM/warm-start already measured). - Verdict + recommendation: unify, port, or keep two deliberately different paths, with evidence.
Detailed implementation plan
Affected Repositories
- PyAutoArray (primary — read-only; no changes)
Branch Survey
| Repository | Current Branch | Dirty? |
|---|---|---|
| ./PyAutoArray | main | clean (read-only) |
Suggested branch: n/a — research verdict only, no code changes, no worktree.
Investigation Steps
- Read the solver dispatch:
autoarray/inversion/inversion/inversion_util.py::reconstruction_positive_only_from(xp dispatch: JAX →util/jax_nnls.py::solve_nnls_primalPDIP; NumPy →util/fnnls.py::fnnls_choleskyactive-set +inversion/inversion/nnls_memo.pywarm-start memo). - Read the two operator realisations:
inversion/inversion/imaging/inversion_imaging_util.py::ImagingSparseOperator(JAX, FFT matrix-free W = Hᵀ N⁻¹ H) vsinversion/inversion/imaging_numba/inversion_imaging_numba_util.py::psf_precision_operator_sparse_from+SparseLinAlgImagingNumba(numba, explicit per-pixel-pair preload); the dataset factoriesapply_sparse_operator/apply_sparse_operator_cpuand the inversion factory dispatch. - Read the two F assemblies:
ImagingSparseOperator.curvature_matrix_diag_from(COO scatter → batched FFT → segment_sum) vscurvature_matrix_via_sparse_operator_two_stage_from(perf(numba): HST curvature matrix phase 2 — mapper×mapper block + MGE operated matrix #507 sparse contraction over unique data→pix mappings). - Ground the numbers against
autolens_profiling/results/notes/nnls_warm_start_memo.md,nnls_warm_start_memo_matrix.md,nnls_solver_ledger.mdand the breakdown JSONs — no re-profiling. - Pull Memory context:
wiki/methods/concepts/autodiff-implicit-diff.md(gradients through the positive-only solve),wiki/methods/sources/linear-algebra.md(FNNLS lineage). - Write the verdict per the prompt's four questions as an issue comment; park for human review (judge-tier task — no decide-and-flag).
Key Files
autoarray/inversion/inversion/inversion_util.py— solver dispatch, both pathsautoarray/util/fnnls.py,autoarray/util/cholesky_funcs.py,autoarray/inversion/inversion/nnls_memo.py— numba/NumPy active-set solveautoarray/util/jax_nnls.py— JAX PDIP driver with custom VJPautoarray/inversion/inversion/imaging/{sparse.py,inversion_imaging_util.py}— JAX sparse-operator modeautoarray/inversion/inversion/imaging_numba/{sparse.py,inversion_imaging_numba_util.py}— numba sparse-operator modeautoarray/inversion/inversion/factory.py,autoarray/dataset/imaging/dataset.py— mode selection
Original Prompt
Click to expand starting prompt
Is the numba positive-only solve the same linear algebra as the JAX sparse-operator mode?
Type: research
Target: autoarray
Repos:
- PyAutoArray
Themes: - numba-cpu
- jax
Difficulty: medium
Autonomy: safe
Priority: normal
Status: formalised
Consequence: judge
Review-minutes: 20
Unattended: ready
Filed: 2026-08-31
Intake from the 2026-08-31-am batch review, prompted by the subhalo_validation
end-to-end runs (numba CPU path ~x8 faster than the old code end to end). Human,
verbatim:
"""
- End to end runs are good in general, with numba speed up run times roughly x8
faster than old code, incredible. Do an intake to investigate if the linear algebra
approach taken for this is the same as the sparse operator mode alreasdy implemented
for JAX, and if it is not ask whether it is amenable to GPU JAX speed up or if there
is some sparsity exploitation that means this is suited to CPU.
"""
Deliverable is a written verdict (no library changes):
- Characterise the linear algebra of the numba CPU positive-only solve as shipped
(the NNLS warm-start/memo lineage — PyAutoArray perf: warm-start the positive-only NNLS solve from the previous evaluation's passive set (#498) #501, epicnumba-cpu-likelihood;
curvature-matrix F work perf: numba CPU curvature matrix F at HST resolution (phase 1: split + FFT mapper×func block) #505/perf(numba): HST curvature matrix phase 2 — mapper×mapper block + MGE operated matrix #507; the 177 MB sparse operator per process). - Compare against the sparse-operator mode already implemented for JAX: same
formulation (operator storage, curvature assembly, solve strategy) or different? - If different: is the numba approach amenable to a GPU-JAX port (batched/dense
enough, no data-dependent control flow), or does its speed rest on sparsity
exploitation / branchy active-set iteration that is intrinsically CPU-suited? - Verdict + recommendation: unify, port, or keep two deliberately different paths —
with the evidence for whichever it is.
Ground against the installed stack and the numba-cpu epic's memory/notes; do not
re-profile from scratch — the breakdown JSONs and nnls_warm_start_memo.md in
autolens_profiling already carry the numbers.
Activity
Jammy2211 commentedon Aug 31, 2026
Verdict: same linear algebra, deliberately different machines — keep both, port nothing
TL;DR. The numba CPU positive-only solve and the JAX sparse-operator mode solve the identical mathematical problem — the same regularised normal equations
Q·s = q, s ≥ 0with the same precision operatorW = Hᵀ N⁻¹ H— but realise it differently at every layer: operator storage, curvature assembly, and solver. Each difference is a deliberate fit to its hardware, and each numba lever is either already superseded on GPU by the JAX mode's own formulation or has been measured to fail when transplanted (the closed solver ledger). Recommendation: keep two deliberately different paths. No port, no unification.1. The numba CPU positive-only solve as shipped
The lineage (epic
numba-cpu-likelihood, phases 1–3a + curvature-F successors #505/#507) is four CPU-specific levers stacked on one formulation:Formulation (shared with every inversion path):
F = Aᵀ W AwithW = Hᵀ N⁻¹ H(H = PSF convolution, N = diagonal noise covariance), data vectorq = Aᵀ Hᵀ N⁻¹ d(psf_weighted_data),Q = F + reg, then the positive-only reconstruction minimises|Z·s − x|²s.t.s ≥ 0whereQ = ZᵀZandq = Zᵀx(inversion_util.reconstruction_positive_only_fromdocstring; Warren & Dye 2003 / arXiv:astro-ph/0302587 eqs. 2 and 4).The four levers:
SparseLinAlgImagingNumba, built byImaging.apply_sparse_operator_cpu()): the nonzeros ofWare precomputed once per process — for every data pixel, every partner pixel within the(2K−1)²kernel-overlap footprint, value = the PSF-pair convolution againstN⁻¹(psf_precision_operator_sparse_from; upper triangle only, diagonal halved). Memory scales asimage_pixels × (2Ky−1)(2Kx−1) × 16 B(values + indexes) — the "177 MB per process" the subhalo_validation runs report is this preload at their mask/PSF geometry. It trades memory for never doing a PSF convolution during curvature assembly.curvature_matrix_via_sparse_operator_two_stage_from, perf(numba): HST curvature matrix phase 2 — mapper×mapper block + MGE operated matrix #507/perf(numba): the mapper x mapper block and the MGE operated matrix (#507) #508):Fis contracted directly from the preload and the unique data→pixelization mappings — data-dependent loop bounds (pix_lengths,psf_precision_lengths), irregular indexed accumulation, restructured in perf(numba): HST curvature matrix phase 2 — mapper×mapper block + MGE operated matrix #507 into a per-data-pixel L1-resident source-space accumulator + contiguous AXPYs (2.90× on the block). perf: numba CPU curvature matrix F at HST resolution (phase 1: split + FFT mapper×func block) #505/perf(numba): speed up the curvature matrix F at HST resolution (#505) #506 routed the mapper×linear-func block through the batched FFTConvolver. Together: HST rectangular eval 1.56 → 0.33 s (4.7×).fnnls_cholesky, Bro & De Jong 1997): takes(Q, q)directly; nested data-dependentwhileloops adding/removing passive-set entries; each subproblem solved by in-place rank-1 Cholesky up/downdates of the passive corner (perf: in-place Cholesky buffer + copy-free numba solves for fnnls_cholesky #453/fix: coerce fnnls_cholesky inputs to numpy — JAX ZTx broke the numba buffer kernels #463 preallocated buffer). Exact KKT optimum of a strictly convex problem — unique, so seed quality affects cost, never correctness.nnls_memo.py): a module-level dict keyed by the index space (never matrix values) seeds each solve from the previous evaluation's final passive set, with a self-calibrating relative-error guard (drop at 1.5× the dense-sign reference). Measured (nnls_warm_start_memo.md+ 32-cell robustness matrix): 9.9× (euclid) / 4.0× (hst) median iteration reduction in the sampler-like random-walk regime — 70 → 7 iterations — parity ≤ 1e-14; iid (uncorrelated) regime 0.52–1.09×, still never slower in wall-time because the memo also skips the dense sign-start solve. A sibling memo caches the MGE operated mapping matrix across evaluations (AUTOARRAY_NUMBA_OPERATED_MEMO).2. Comparison with the JAX sparse-operator mode
Same problem, three deliberate divergences. The factory dispatches purely on which operator the dataset carries (
factory.py:SparseLinAlgImagingNumba→InversionImagingSparseNumba;ImagingSparseOperator→InversionImagingSparse), and both classes assemble the sameQandqby construction; since the NNLS optimum is unique, both solvers converge to the same reconstruction to solver tolerance.imaging_numba/)imaging/sparse.py)W,O(N_pix · K²)memory (~177 MB/process class), computed once per process1/σ²map (few MB);Wapplied on demand as FFT → weight → FFTAto the rectangular grid, applyWto 128-column blocks via three FFTs,segment_sumback — fixed shapes, no data-dependent control flow, jit/vmap-safelax.while_loop, capped at 50)jax.gradthrough the positive-only solveSo the answer to the human's first question is: the formulation is the same; the "approach" — everything that makes the numba path 8× faster — is not.
3. Is the numba approach amenable to a GPU-JAX port?
No — each lever, examined:
Wpreload + gather/scatter contraction is a cache-locality optimisation. On GPU the sameFis the step that accelerates best under the existing JAX formulation — the solver ledger measured F at 3.01 s laptop-CPU vs 0.397 s on a consumer RTX 2060 ("F is one big GEMM and melts on GPU"). The GPU-appropriate realisation of this operator already exists and is the JAX mode. Porting the numba contraction would move a fix for a bottleneck the GPU does not have onto hardware that punishes its data-dependent loop bounds and irregular scatter.whileloops with data-dependent trip counts and passive-set-sized factor updates. The GPU-viable active-set analog was already tried and closed (nnls_solver_ledger.md, PyAutoArray#369/perf: BPP/ADMM NNLS solver experiment (PDIP alternative, #369 follow-up) #370): block principal pivoting needed 36–53 factorizations vs PDIP's 19–21 (strict Kim–Park: 151) on real extracted production systems — slower; ADMM fails to reach evidence tolerance; the ledger's conclusion 1: "PDIP at ~20 iterations × fresh Cholesky is near-optimal for this problem class among GPU-viable algorithms. Do not re-litigate without new structure."vmap-batched evaluation, wherelax.while_loopruns every lane to the slowest lane and batch members are not each other's predecessors — the memo's iid rows (0.52–0.91×) are the measured preview of that regime.jax.grad(relaxed-KKT VJP, FD-validated); an active-set solve with discrete set changes has no such machinery. Replacing PDIP with a ported active-set solver would cost the gradient path that JAX inference (HMC/NUTS, gradient optimisers) exists for.So the human's dichotomy resolves cleanly to the second horn: the numba speed rests on sparsity exploitation, cache-resident branchy iteration, and cross-evaluation state — all intrinsically CPU-suited — while the GPU already holds the right formulation for its own strengths (dense batched FFT + fixed-shape PDIP). One further CPU-only fact: the numba path also serves Delaunay meshes, whose construction is a structural JAX hole (
pure_callback, no JVP).4. Recommendation
Keep two deliberately different paths. Do not unify, do not port.
Q,q, regularisation, the single dispatch point inreconstruction_positive_only_from) and the parity bar (pinned likelihoods, rtol 1e-6).Scholar-mode candidates (not filed — for review;
/intakeif wanted)wiki/methods/or a PyAutoArray doc page) recording "two sparse-operator modes: same math, different machines" with the decision table above — the closed solver ledger lives in autolens_profiling where library sessions don't look, and this question will be re-asked.submit_probe_nnls_a100) were still pending when the ledger closed; if they never landed, a small follow-up to land or formally retire them would complete the GPU half of the evidence base this verdict cites.Evidence base: PyAutoArray
main@302d5df3(inversion_util.py,util/fnnls.py,util/jax_nnls.py,nnls_memo.py,imaging/sparse.py+inversion_imaging_util.py,imaging_numba/sparse.py+inversion_imaging_numba_util.py,factory.py,dataset/imaging/dataset.py); autolens_profilingresults/notes/{nnls_warm_start_memo,nnls_warm_start_memo_matrix,nnls_solver_ledger}.md; Mindcomplete/2026/08/{numba-cpu-nnls-iteration-reduction,numba-hst-curvature-matrix-speedup,numba-hst-curvature-matrix-phase2}.md+epics.md; PyAutoMemorywiki/methods/concepts/autodiff-implicit-diff.md,wiki/methods/sources/linear-algebra.md. No re-profiling performed, per the prompt.Task is
Consequence: judge— this verdict parks for human review (supervised checkpoint-and-continue; no decide-and-flag). Sign-off retires the prompt via the usual close-out; if the verdict doesn't pass, the follow-up is an ordinary/intake.Generated by Claude Code
Jammy2211 commentedon Sep 1, 2026
Batch review ruling (2026-08-31-pm, reviewed 2026-09-01 15:13): verdict ACCEPTED.
"Same linear algebra, deliberately different machines — keep both paths, port nothing" stands as the ruling of record. No library changes follow from it. The two scholar-mode candidates (a design note recording the two-modes decision; landing or retiring the solver ledger's pending A100 rows) remain unfiled — they can be /intake'd on demand.
Closing; the PyAutoMind prompt retires to complete/ via the batch close-out.
Generated by Claude Code