Skip to content

perf: skip Sibson row padding in adaptive signals and unique mappings (DelaunayNN imaging) - #637

Merged
Jammy2211 merged 1 commit into
mainfrom
feature/delaunaynn-wide-row-overhead
Oct 17, 2026
Merged

Jammy2211 merged 1 commit into
mainfrom
feature/delaunaynn-wide-row-overhead

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Summary

DelaunayNN (Sibson) mapping tables are SIBSON_MAX_NEIGHBORS = 32 columns wide but hold at most ~13–15 live entries per row. On the NumPy/numba path two steps worked over every padded slot. This PR makes them visit only the pix_sizes[i] live entries and fixes a pre-existing Sibson edge case where a failed row could silently lose its NaN.

  • adaptive_pixel_signals_from (NumPy branch): the np.add.at scatter over all 32 slots is replaced by a numba kernel, mapper_numba_util.adaptive_pixel_signal_sums_from, that loops only over live entries. When numba is absent or numba: use_numba: false, it falls back to np.bincount over the trimmed, compacted live entries. Both are bit-identical to the padded scatter: the same products are summed in the same row-major order from the same 0.0, and every skipped entry only ever reached the discarded out-of-bounds slot. The JAX branch is unchanged (it was moved into _pixel_signal_sums_jax).
  • data_slim_to_pixelization_unique_from: it now resets only the pix_check entries the previous data pixel touched, instead of all pix_pixels per data pixel, and the mapper passes its tables with np.asarray so the 32-wide tables are no longer copied. Outputs and shapes are unchanged and bit-identical.
  • Sibson failed rows keep size ≥ 1 (reference, numba kernel and JAX path). An overflow or degenerate row with no selected entry used to come out at size == 0. Every consumer reads only the first size entries, so its NaN weights were skipped and the evidence came out finite and wrong. Such a row is now size 1, mapped to the nearest seed vertex with a NaN weight, so the NaN reaches the curvature matrix, the regularization and the evidence. This deliberately changes behaviour from a finite, wrong evidence to NaN, so the sample is rejected.

Correction to the issue's profile: the 0.30 s attributed to unique_mappings was mostly the Sibson interpolation, which runs lazily the first time unique_mappings reads the mapper's tables. The numba unique-mappings kernel itself took ~22 ms at sub 4.

Timing (RAL)

RAL acceptance timing — job 412525, euclid-ral-compute-10-4, --partition=ral --exclusive, threads=1 (NUMBA/OMP/OPENBLAS/MKL=1), use_jax=False, load 0.4–1.0 (the benchmark process itself). Private checkout (/mnt/ral/jnightin/scratch/dnn_imaging_20261017, shared mirror untouched): PyAutoNerves/Fit/Galaxy/Lens at 2026.10.17.1 (7f3055b / 4b183e22 / 35100def / c90eceb0); PyAutoArray A = e59d5fa9 (2026.10.17.1 = main), B = 58a044eb (this PR). HST-scale simulated imaging: 150×150 @ 0.05″, r=3.5″ mask (15,380 pixels), 11×11 PSF, Hilbert 1000 + 30 edge = 1030 vertices, AdaptSplit, sparse operator → InversionImagingSparseNumba. Order A-B-B-A × 2 blocks per case (4 processes per variant), 10 timed log_likelihood_function calls per process after 2 warm-up calls (compile excluded); value = median over processes of per-process medians; step times = wrapped-module medians.

case A (main) B (PR) B speed-up ratio to Delaunay B Sibson unique_mappings A→B adaptive_pixel_signals A→B logL A == B
DelaunayNN sub 4 0.569 s 0.392 s 1.45× 2.20× (was 3.00×) 159→169 ms 22.5 → 17.0 ms 158.0 → 6.5 ms bit-identical (34898.34096822383)
Delaunay sub 4 0.189 s 0.178 s 1.06× 1 — 4.4 → 3.5 ms 10.0 → 1.2 ms bit-identical (34840.82970999481)
DelaunayNN sub 2 0.235 s 0.189 s 1.24× 1.75× (was 2.10×) 44→40 ms 4.3 → 2.7 ms 37.3 → 1.1 ms bit-identical (34891.78161640761)
Delaunay sub 2 0.112 s 0.108 s 1.03× 1 — 1.9 → 1.1 ms 2.5 → 0.4 ms bit-identical (34833.08741739975)

Spread within each variant ≤ ±2 % (sub 4 NN B 0.380–0.396 s). Table width 32, live entries ≤ 13 (mean 7.2) per DelaunayNN row.

Against the acceptance line: the padded-row overhead is gone (adaptive signals 24× faster, now ~1.6 % of a DelaunayNN likelihood), but DelaunayNN is still 2.20× Delaunay at sub 4, so the draft's ~1.5× is not met (the issue plan's revised ≈2× target is close but also slightly above). What remains is genuine work, not padding: the Sibson interpolation itself (~0.16 s at threads=1, ~41 % of the call) and the curvature build over the wider unique-mapping rows (up to 43 unique pixels per data pixel vs 27 for Delaunay). Neither is in this PR's scope.

API Changes

None to the public API. Behaviour change: a failed Sibson row (overflow or degenerate) with no selected natural neighbour now has size == 1 and a NaN weight, where it used to have size == 0. A model hitting it now gets a NaN evidence (rejected) instead of a finite, wrong one. The Delaunay and DelaunayNN likelihoods are otherwise bit-identical.
See full details below.

Test Plan

  • New test_autoarray/inversion/pixelization/mappers/test_mapper_util_wide_rows.py checks bit-parity of both functions against the padded scatter and the full-reset algorithm. It covers 32-wide DelaunayNN-shaped, 3-wide Delaunay and variable-width tables, both NumPy routes (numba kernel and bincount fallback) and real Delaunay / DelaunayNN mappers (the Delaunay-unchanged invariant). It also checks JAX against NumPy on wide rows, that NaN in padding only is still ignored, and that the NaN of a size-1 failed row reaches the signals and the unique weights (and was dropped at size 0).
  • New test_autoarray/inversion/pixelization/interpolator/test_sibson_failed_rows.py builds a size-0 failed row by overflowing the Watson arithmetic (tables at 1e150 scale) and checks it for the reference, numba (exact parity) and JAX Sibson paths.
  • Control run: the new Sibson tests fail on main.
  • Full suites against this branch (laptop): PyAutoArray 2088 passed / 4 xfailed; PyAutoGalaxy 1357 passed; PyAutoLens 832 passed / 1 xfailed.
  • Emulated the unittest-nojax leg (an import hook blocks the jax family and find_spec returns None): test_autoarray/inversion/pixelization/ 256 passed / 20 skipped / 3 xfailed.
  • RAL A/B timing above: logL bit-identical for A and B on every case.

Heart at ship time: STALE, with one reason: "release validation incomplete: no rehearsal for current source". It is not RED. Merge mode: human /prm (tier judge).

Full API Changes (for automation & release notes)

Added

  • autoarray.inversion.mappers.mapper_numba_util.adaptive_pixel_signal_sums_from(pixels, pixel_weights, pix_indexes_for_sub_slim_index, pix_size_for_sub_slim_index, slim_index_for_sub_slim_index, adapt_data, exclude_zero_weight_mappings): numba per-pixel signal sums and counts over live entries only (internal helper).
  • Private helpers in mapper_util: _use_numba, _pixel_signal_sums_jax, _pixel_signal_sums_numpy.

Changed Behaviour

  • sibson._sibson_single_from_tables / _sibson_numba._sibson_from_tables_parallel: a failed row with no selected entry now has size == 1, mappings[0] set to the nearest seed vertex and a NaN weight (previously size == 0).

Closes #629

🤖 Generated with Claude Code

https://claude.ai/code/session_01QBeyBLN7ntDChx9v1fmt6M

… (DelaunayNN imaging)

DelaunayNN (Sibson) mapping tables are SIBSON_MAX_NEIGHBORS (32) columns
wide but hold at most ~15 live entries per row. On the NumPy path:

- adaptive_pixel_signals_from: the np.add.at scatter over every padded slot
  is replaced by a numba kernel (mapper_numba_util.adaptive_pixel_signal_sums_from)
  visiting only the first pix_sizes[i] entries of every row, with a vectorised
  np.bincount fallback over the compacted live entries when numba is absent
  or disabled. Both are bit-identical to the padded scatter (same row-major
  summation order, same products, skipped entries only ever reached the
  discarded out-of-bounds slot). The JAX branch is unchanged.
- data_slim_to_pixelization_unique_from: reset only the pix_check entries the
  previous data pixel touched instead of all pix_pixels per data pixel; the
  mapper passes its tables with np.asarray (no 32-wide copies). Outputs and
  shapes unchanged, bit-identical.
- Sibson: a failed row (overflow / degenerate) with no selected entry used to
  come out at size 0, so every consumer skipped its NaN weights and the
  evidence was finite and wrong. Reference, numba kernel and JAX path now keep
  such a row at size 1 mapped to the nearest seed vertex, so NaN propagates.

Tests: bit-parity of both functions against the padded / full-reset
algorithms on 32-wide DelaunayNN-shaped, 3-wide Delaunay and variable-width
tables and on real Delaunay and DelaunayNN mappers (both NumPy routes), JAX
vs NumPy on wide rows, NaN-in-padding ignored, and the size-0 failed-row
case (constructed by overflowing the Watson arithmetic) for the reference,
numba and JAX Sibson paths plus its NaN reaching signals and unique weights.

Closes #629

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBeyBLN7ntDChx9v1fmt6M
@Jammy2211 Jammy2211 added the pending-release PR queued for the next release build label Oct 17, 2026
@Jammy2211
Jammy2211 merged commit 1dec833 into main Oct 17, 2026
3 checks passed
@Jammy2211
Jammy2211 deleted the feature/delaunaynn-wide-row-overhead branch October 17, 2026 16:26
@Jammy2211 Jammy2211 removed the pending-release PR queued for the next release build label Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: skip Sibson row padding in adaptive signals and unique mappings (DelaunayNN imaging)

1 participant