Skip to content

perf: interferometer sparse data vector without the dense mapping matrix #626

Closed

Description

@Jammy2211

Overview

InversionInterferometerSparse.data_vector computes mapping_matrix.T @ dirty_image, which materialises the dense (masked pixels × source pixels) mapping matrix via hstack — 4.2 GB at 91,507 × 5,746 on the Aris 512×512 mask. A memory trace attributes all RSS growth from ~3 GB to 7.2 GB in one likelihood call to this line, on both the numba direct_conv and NumPy FFT routes, and it likely dominates the ~9 GB XLA temp memory of the JAX path. The curvature routes already work from the sparse COO triplets of the same operator, so the data vector can be a scatter/segment-sum over those triplets and the dense matrix never needs to exist in the likelihood.

Plan

  • Compute each mapper's data-vector block from its sparse triplets (segment-sum of vals * dirty_image[rows] over source columns) on both the NumPy and JAX backends.
  • Keep linear function lists (MGE / linear light profiles) on their small dense mapping_matrix.T @ dirty_image block and concatenate blocks in linear-object order.
  • Audit every other per-call mapping_matrix touch in the sparse interferometer inversion and its numba subclass; leave visualisation-only paths alone.
  • Add tests pinning the sparse data vector to the dense one (over-sampled mapper, mapper + func list, JAX backend, DatasetInterface with sparse_dirty_image).
  • Measure peak RSS and time before/after on the cached 512×512 setup (5,746 vertices, numba route) and JAX compile memory.

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 (a84f724) clean

Suggested branch: feature/interferometer-sparse-data-vector

Implementation Steps

  1. autoarray/inversion/inversion/interferometer/sparse.py — rewrite data_vector: resolve dirty_image as today (slim, masked-pixel indexed); for each linear object in self.linear_obj_list, if it is a Mapper build triplets with mapper_util.sparse_triplets_from(..., return_rows_slim=True, xp=self._xp) (slim rows index the slim dirty image; the existing _sparse_triplets_curvature_from uses extent-flat rows and is NOT the right indexing for this product — add a sibling helper _sparse_triplets_slim_from or a rows_slim flag) and compute the block as np.bincount(cols, weights=vals * dirty_image[rows], minlength=mapper.params) on NumPy and jnp.zeros(S).at[cols].add(vals * dirty_image[rows]) (or jax.ops.segment_sum with num_segments=S) on JAX; if it is an AbstractLinearObjFuncList keep xp.dot(linear_obj.mapping_matrix.T, dirty_image). Concatenate with xp.concatenate in the order of self.linear_obj_list so the result matches param_range_list_from(cls=LinearObj).
  2. Confirm sparse_triplets_from includes the same sub_fraction normalisation as mapper_util.mapping_matrix_from (the curvature parity tests already rely on this); if the mapper has pix_sizes_for_sub_slim_index padding (invalid stencil slots with zero weight), the helper's masking must drop or zero them — verify, do not assume.
  3. Audit per-call mapping_matrix use: abstract.py mapping_matrix cached property (now only reached from visualisation / mapped_reconstructed_data_dict), interferometer_numba/sparse.py (no touch per grep), _curvature_matrix_func_list_and_mapper (func-list matrices only — fine). Record findings in the PR body.
  4. Tests in test_autoarray/inversion/inversion/interferometer/test_interferometer.py (and the numba subclass tests under interferometer_numba/): sparse data_vector equals mapping_matrix.T @ dirty_image to 1e-12 for (a) a single over-sampled Delaunay/rectangular mapper, (b) mapper + linear func list, (c) xp=jnp (importorskip jax), (d) a DatasetInterface whose sparse_dirty_image differs from the operator's. Keep the log_evidence parity tests between InversionInterferometerSparse and InversionInterferometerMapping green.
  5. Witness measurement with the session scratchpad copy of the Aris evidence (aris_evidence/step8/bench.py --mesh step1 --mode numba style run against dataset_sparse_512.pkl, 5,746 vertices, 1 thread): record peak RSS before (7.17 GB) and after (target ≤ ~3.5 GB), steady s/call, and that the log likelihood at the same parameter vector is unchanged to 1e-8. Also record JAX jit peak RSS / XLA compile temp at 1,459 vertices (3.69 GB / 2.17 GB before) and attempt the 5,746-vertex JAX run that was previously infeasible (8.95 GB compile temp). Put the numbers in the PR body.

Key Files

  • autoarray/inversion/inversion/interferometer/sparse.py — data_vector (line ~104), _sparse_triplets_curvature_from
  • autoarray/inversion/mappers/mapper_util.py — sparse_triplets_from (return_rows_slim)
  • autoarray/inversion/inversion/abstract.py — mapping_matrix hstack (line ~270)
  • autoarray/inversion/inversion/interferometer_numba/sparse.py — numba subclass (audit only)
  • test_autoarray/inversion/inversion/interferometer/test_interferometer.py — parity tests

Evidence

Prompt measurements (PROGRESS4 step8.4a): dense mapping matrix 1.07 GB @1459, 1.9 GB @2577, 4.2 GB @5746 vertices (+ hstack copy); route-independent. Cached dataset and bench script copied to this session's scratchpad aris_evidence/step8/ (temporary).

Heart RED development override

Heart was RED at launch (2026-10-15 evening): 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.

The live human authorized the development-only override in session 3e3dea38 (AskUserQuestion answer: "Yes, override for development (Recommended)") for the three PyAutoArray prompts filed from the Aris interferometer profiling. Permitted: commit, push, PR-open behind the autonomous-ship gate (tests, smoke, 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

Interferometer sparse inversion: compute data vector without the dense mapping matrix

Type: feature
Target: PyAutoArray
Repos:

  • PyAutoArray
    Difficulty: medium
    Autonomy: safe
    Priority: normal
    Memory: wiki/lensing/sources/dark-matter-substructure.md; reading-queue.md; wiki/galaxies/sources/cosmos-survey.md
    Status: formalised
    Consequence: glance
    Witness: on the Aris 512x512 setup (91,507 masked pixels, 5,746 Delaunay vertices, use_jax=False) peak RSS of one likelihood call drops from 7.2 GB to <= ~3.5 GB with log likelihood unchanged to 1e-8
    Review-minutes: 3
    Unattended: ready

autoarray/inversion/inversion/interferometer/sparse.py:104 computes
data_vector = xp.dot(self.mapping_matrix.T, dirty_image), which materialises the dense
(masked pixels x source pixels) mapping matrix via hstack — 4.2 GB at 91,507 x 5,746. The step only
needs the sparse mapping operator (the same triplets the curvature routes already use). A memory
trace on the 512x512 mask attributes all RSS growth from ~3 GB to 7.2 GB to this line, on both the
numba direct_conv and NumPy FFT routes. It probably also explains most of the ~9 GB XLA temp
memory of the JAX path at that size.

Do: compute the data vector from the sparse triplets (scatter/segment-sum: D_j = sum over nonzeros
in column j of val * dirty_image[row]) on both the NumPy and JAX paths, so no dense mapping matrix
is built in the likelihood. Check other places in the interferometer sparse inversion (and
the numba subclass) that touch mapping_matrix per call. Keep outputs identical; add a test.
Measure peak RSS and time before/after on the 512x512 cache (and JAX compile memory).

Evidence: /tmp/claude-1000/-home-jammy-Code-PyAutoLabs/50c8c6cd-6ac2-47c2-9290-7ee73f9c4315/scratchpad/aris/PROGRESS4.md and step8/ (dataset_sparse_512.pkl cache, bench.py). PyAutoArray ccddfba.

Activity

  1. Jammy2211 commented on Oct 15, 2026

    @Jammy2211
    CollaboratorAuthor

    Campaign membership: this task is logged and monitored under the PyAutoPulse interferometer campaign as task interferometer_large_mask_aris — PyAutoLabs/PyAutoPulse#62 (ledger rows queued in Mind behind the PyAutoPulse claim; the issue holds the campaign intent and baseline numbers until they land). Record the before/after witness table there at close-out.

  2. Jammy2211 commented on Oct 15, 2026

    @Jammy2211
    CollaboratorAuthor

    Library PR Created

    PR: #630. Labels: pending-release, decision-taken. This --auto run stops at PR-open, and the human runs /prm.

    API changes: none. Internal changes only: two private helpers on InversionInterferometerSparse. One behaviour note: in JAX mixed precision the data vector now uses a float64 data-vector block instead of a float32 mapping matrix.

    Gate

    • Tests: 2033 passed, 4 xfailed. Downstream library suites not run, because the API change is private.
    • Smoke: pyauto-heart smoke autolens autogalaxy autolens_test ran 92/92 scripts and 4/4 notebooks with 0 failures. That includes 15 interferometer scripts.
    • Review: CLEAN, from an independent Opus reader.
    • Heart: RED under the development override recorded above. At ship time the reasons were release validation FAILED (stage integrate) and PyAutoFit: open PR 8d old.

    Witness: PASS on memory. Peak RSS went from 7.17 GB to 3.14 GB. On logL, see the PR's "Decision taken" section: the data vector matches the dense product to 5.8e-16, and logL moves by at most 2.5e-11 relative (fp64 rounding at |logL| ≈ 1.4e7). Revert: git revert 8676b6a0.

    Workspace impact: recommendation (iii), no workspace impact. There is no public API change, and no workspace, test or pipeline script references the changed helpers. The smoke subsets above confirm this. No /start_workspace needed.

    Campaign: at close-out, copy the before/after witness table from PR #630 into the PyAutoPulse#62 task interferometer_large_mask_aris.

  3. Jammy2211 commented on Oct 15, 2026

    @Jammy2211
    CollaboratorAuthor

    Shipped

    Merged in #630 (merge 7cc71238, head 8676b6a0), 2026-10-15. Labelled pending-release. It is not in a published release yet.

    • The sparse-operator interferometer inversion now builds its data vector directly from the mapper's COO triplets, so the likelihood never builds the dense mapping matrix. New private helpers _sparse_triplets_slim_from and _data_vector_mapper_from. Tests compare the result with the dense product.
    • Witness: numba at 5,746 vertices, peak RSS 7.17 → 3.14 GB. JAX jit at 1,459 vertices, 3.65 → 2.60 GB. XLA compile temp at 5,746 vertices, 8.95 → 1.86 GB, and JAX at 5,746 vertices now runs at 3.24 GB. No timing claims: machine load was 2–14.
    • Decision taken: I read "logL unchanged to 1e-8" as fp64 precision. Measured agreement is ≤2.5e-11 relative. To revert: git revert 8676b6a0.
    • Behaviour note: under JAX mixed precision this is now a float64 block, so results are not bit-identical to before.
    • Heart was RED. Shipped under the live-human development override recorded above (commit, push and PR-open only). A human merged it with /prm.
    • Campaign: campaign: interferometer large-mask likelihood — memory and routing (Aris 512×512) PyAutoPulse#62, task interferometer_large_mask_aris.
    • Next on the PyAutoArray claim: fix: AdaptSplit regularization log-det parity between numba and JAX #627 (patch is on that issue), then feat: scale the interferometer numba route gate with mask size #628.
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