Skip to content

perf: reuse one Cholesky factor of F+H for the solve and the log det #633

Closed

Description

@Jammy2211

Overview

With use_positive_only_solver=False on the NumPy backend, every likelihood call factorizes the same symmetric positive-definite matrix F + H twice: once with np.linalg.solve (LU) for the reconstruction and once with np.linalg.cholesky for log_det_curvature_reg_matrix_term. On the Aris SPT2319-55 interferometer setup (Delaunay + AdaptSplit, numba route) these two take 31% of a call at 1,405 vertices and 72% at 2,947 vertices. The positive-only path already reuses its factor. This task makes the positive-negative path factorize once and use that factor for both the solve and the log det.

Campaign: PyAutoLabs/PyAutoPulse#62 (task interferometer_large_mask_aris).

Plan

  • Add one cached Cholesky factor of F + H to the inversion, built only on the NumPy backend, with use_positive_only_solver=False and log_det_method == "cholesky".
  • reconstruction solves with that factor (cho_solve) in place of np.linalg.solve.
  • log_det_curvature_reg_matrix_term reads 2 * sum(log(diag(factor))) off the same factor when every linear object is regularized, so the reduced matrix is the full one.
  • If the Cholesky fails (matrix not positive-definite, or non-finite), nothing changes. The reconstruction falls back to the existing LU solve with its test-mode guard, and the log det takes the existing dense route, which raises or returns 0.0 in test mode as today. Error behaviour is therefore identical.
  • Leave untouched: the JAX path, the positive-only path, the slogdet opt-in, and the out-of-place curvature_reg_matrix (it is reused by the reduced matrix and the covariance, so it is not factorized in place). No float32.
  • Tests pin the solve and log det against the two-factorization values, the fallbacks, slogdet and positive-only. A laptop witness measures before and after at 1,405 and 2,947 vertices.

Tier: glance — merge mode: auto-merge on green if the Witness passes, post-merge summary

Detailed implementation plan

Affected Repositories

  • PyAutoArray (primary)

Branch Survey

Repository Current Branch Dirty?
array/PyAutoArray main (17caf22 = origin/main) clean

worktree_check_conflict reuse-cholesky-factor PyAutoArray → no conflict.

Suggested branch: feature/reuse-cholesky-factor

Implementation Steps

  1. autoarray/inversion/inversion/abstract.py: add @cached_property _curvature_reg_matrix_cho_factor. It returns scipy.linalg.cho_factor(curvature_reg_matrix, lower=True, check_finite=False) when self._xp is np, not settings.use_positive_only_solver and settings.log_det_method == "cholesky". It returns None under any other gate, on LinAlgError (scipy's is numpy's class), or when the factor's diagonal is non-finite. It never overwrites curvature_reg_matrix.
  2. reconstruction: in the positive-negative branch, if the factor is present, return scipy.linalg.cho_solve(factor, data_vector, check_finite=False). Otherwise call the unchanged inversion_util.reconstruction_positive_negative_from.
  3. log_det_curvature_reg_matrix_term: after the positive-only fast path, if all_linear_obj_have_regularization and the factor is present, return 2.0 * np.sum(np.log(np.diag(factor[0]))). Otherwise take the unchanged _log_det_symmetric_from(curvature_reg_matrix_reduced). The log det reads the factor property directly, so it still does not evaluate the reconstruction.
  4. Tests (new test_autoarray/inversion/inversion/test_positive_negative_shared_cholesky.py): imaging and interferometer fixture inversions, where the reconstruction equals np.linalg.solve to rtol 1e-10 and the log det equals the dense Cholesky to 1e-9, and the factor is computed once and shared; non-PD (indefinite, nonsingular) F + H where the reconstruction equals the LU solution and the log det raises LinAlgError (test mode off) / returns 0.0 (test mode on); slogdet unchanged and factor None; positive-only unaffected and factor None; a partially regularized system where the solve uses the factor and the log det takes the dense reduced route.
  5. Witness: Aris bd.py breakdown at 1,405 (--mesh step2) and 2,947 (--mesh sp) vertices, single thread, before (canonical main) and after (worktree). Record the full-call median and the solve+log-det share, logL agreement (1e-9) and the reconstruction max relative difference. Pass: ≥15% at 1,405 and ≥35% at 2,947.

Key Files

  • autoarray/inversion/inversion/abstract.py: reconstruction, log_det_curvature_reg_matrix_term, new factor property
  • autoarray/inversion/inversion/inversion_util.py: reconstruction_positive_negative_from (unchanged; fallback)

Heart RED development override

Heart was RED at launch (2026-10-15 late): Heart: RED — Release RED; monitoring RED · 10/100 · 415 unresolved; [red] release validation FAILED (stage integrate); [red] PyAutoHands/workflows/Python Version Matrix: failure; [red] PyAutoHeart/workflows/Release Integrate: failure; [red] PyAutoMind/workflows/Lifecycle Drift: failure; [red] PyAutoArray/Tests: red; [red] autofit_workspace_test/Smoke Tests: red; [red] autogalaxy_workspace_test/Smoke Tests: red; [red] autolens_workspace_test/Smoke Tests: red; [red] Release validation: NOT release_ready — v2026.10.15.1.dev81701; [red] Worktree drift: 5 orphan / 0 missing / 4 dirty; [red] autolens_inference: red; [red] autolens_profiling: red; [red] euclid_strong_lens_modeling_pipeline: red (as listed on PyAutoArray#626).

The live human authorized the development-only override in session 3e3dea38 (AskUserQuestion answer: "L1 now under override (Recommended)", 2026-10-15 late). Permitted: commit, push, PR-open behind the autonomous-ship gate (tests, smoke and review must still pass). Merge stays a separate human act. Heart remains RED for release purposes. Launched --auto.

Original Prompt

Click to expand starting prompt

Reuse one Cholesky factor of F+H for the reconstruction solve and the log det (numpy, non-positive-only path)

Type: feature
Target: PyAutoArray
Repos:

  • PyAutoArray
    Difficulty: small
    Autonomy: safe
    Priority: high
    Status: formalised
    Consequence: glance
    Witness: on the Aris 125x125 interferometer setup (Delaunay + AdaptSplit, use_positive_only_solver=False, numba route) one likelihood call drops by >=15% at 1,405 vertices and >=35% at 2,947 vertices, with reconstruction unchanged to 1e-10 relative and log_det_curvature_reg_matrix_term unchanged to 1e-9
    Review-minutes: 3
    Unattended: ready

Profiled 2026-10-15 (session 3e3dea38, PyAutoArray main 17caf22) on Aris's SPT2319-55 data with
use_positive_only_solver=False: the numpy path factorizes the same SPD matrix F + H twice per
likelihood call — np.linalg.solve (LU) in inversion_util.reconstruction_positive_negative_from
(called from AbstractInversion.reconstruction, abstract.py ~760) and np.linalg.cholesky in
_log_det_symmetric_from for log_det_curvature_reg_matrix_term. Together 31% of the call at 1,405
vertices (100 + 70 ms of 0.63 s) and 72% at 2,947 vertices (976 + 741 ms of 2.0–2.4 s). The
positive-only path already reuses its factor (log_det_from_passive_cholesky_from); the non-positive
path does not.

Do: when use_positive_only_solver=False on the numpy backend, compute one scipy.linalg.cho_factor
of F + H, solve with cho_solve, and take the log det as 2 * sum(log(diag)) of the same factor
(keep the existing LinAlgError test-mode guard and the slogdet opt-in; JAX path unchanged).
Measured on the saved matrices: 65 ms vs 113+81 ms (1,405) and 612 ms vs 948+654 ms (2,947);
solution relative change 7e-14 / 2e-11; log det identical; non-PD still raises. Optionally fold in
an in-place F + H add to drop one S×S copy (10–30 ms). Do NOT use float32 (0.027 nats log-det
error at 2,947; cond(F+H) up to 7e12). Add a test pinning solve + log det against the two-factorization
values. Evidence: session 3e3dea38 scratchpad report_aris_speedups.md + aris_research/la.py
(temporary); campaign PyAutoLabs/PyAutoPulse#62.

Activity

  1. Jammy2211 commented on Oct 15, 2026

    @Jammy2211
    CollaboratorAuthor

    Workspace impact: (iii) none

    API changes: none (internal only). The new _curvature_reg_matrix_cho_factor is private, and reconstruction / log_det_curvature_reg_matrix_term keep their signatures and semantics.

    • A grep of the workspace scripts (autofit / autogalaxy / autolens / autolens_workspace_test / euclid / HowToLens) for the changed symbols finds 0 matches.
    • no_run.yaml was not checked: it is not present at PyAutoHands/autohands/config/ in this checkout. The symbols are private and unused by scripts, so this is moot.
    • Confirmed by smoke: 165/165 passed (159 scripts and 6 notebooks across autofit, autogalaxy, autolens, autolens_test, euclid and howtolens). The autocti smoke environments were not built locally (libgsl-dev missing).

    Recommendation: no /start_workspace follow-up.

  2. Jammy2211 commented on Oct 15, 2026

    @Jammy2211
    CollaboratorAuthor

    Library PR Created

    Gate

    • Tests: PyAutoArray 2065 passed / 4 xfailed, PyAutoGalaxy 1357 passed, PyAutoLens 832 passed / 1 xfailed.
    • Smoke: 165/165 passed.
    • Review: CLEAN, from an independent Opus reader.
    • Heart: RED, proceeding under the development-only override (session 3e3dea38, "L1 now under override (Recommended)").

    Witness

    • 2,947 vertices: the full call drops 1.635 → 0.801 s (−51%).
    • 1,405 vertices: −22.4% on the repeat run and −24.3% under the profiler. The first run measured −7.7% under machine load.
    • The log det is bit-identical, and logL agrees to ≤1.1e-11 relative.
    • The reconstruction clause "1e-10 relative" holds at 1,405 vertices (5.2e-14) but cannot hold at 2,947, where cond(F+H) = 7.7e14. There the new Cholesky solve is closer to the refined reference solution than the old LU (5.1e-4 vs 1.5e-3), and its residual is about 6× smaller. This is flagged in the PR for the human's merge decision.

    Next: the human runs /prm. No workspace follow-up is needed (option iii). The run stops at PR-open.

  3. Jammy2211 commented on Oct 15, 2026

    @Jammy2211
    CollaboratorAuthor

    Shipped

    Merged in #634 (e59d5fa). It is pending release and reaches users with the next PyAutoArray release.

    • What changed: on the NumPy path, with the positive-only solver off and log_det_method cholesky, one cached scipy.linalg.cho_factor of a copy of F+H now serves both reconstruction (via cho_solve) and log_det_curvature_reg_matrix_term. Every other configuration keeps the old path, and so does a factorization failure or a non-finite diagonal. Error semantics are unchanged. 10 new tests.
    • Not done: the optional in-place F+H add, because curvature_reg_matrix and curvature_matrix are reused elsewhere.
    • Witness (as reworded): solve+log-det 144.5→50.9 ms at 1,405 vertices and 1,268→373 ms at 2,947. The full likelihood call is −22–24% at 1,405 and −51% at 2,947. The log det is bit-identical and logL agrees to ≤1.1e-11 relative. The reconstruction agrees within conditioning: 1.6e-3 relative at 2,947 vertices, where cond(F+H)=7.7e14, and the new solve is closer to the refined reference. The original "reconstruction unchanged to 1e-10" clause cannot be met at that conditioning, and the reworded witness was accepted at merge.
    • Edge case: for a numerically singular matrix with tiny positive pivots, the solve now returns a huge solution where the LU used to raise. chi² rejects that sample either way.
    • Campaign: campaign: interferometer large-mask likelihood — memory and routing (Aris 512×512) PyAutoPulse#62 (interferometer_large_mask_aris).
    • Mind record: complete/2026/10/reuse-cholesky-factor.md.
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