Skip to content

Drastically reduce selinv allocations and match Cholesky performance - #10

Merged
timweiland merged 10 commits into
mainfrom
feature/reduce-selinv-allocations
Mar 20, 2026
Merged

timweiland merged 10 commits into
mainfrom
feature/reduce-selinv-allocations

Conversation

@timweiland

Copy link
Copy Markdown
Owner

Summary

Systematic elimination of allocations and overhead in selinv_supernodal, bringing selected inversion time down to roughly match CHOLMOD Cholesky factorization time.

Key changes:

  • Lift transposed_chunks, symmetric_access, depermuted_access from runtime Bool fields to type parameters on SupernodalMatrix{Tr,Sym,Dep}, eliminating Union return types and enabling compile-time specialization
  • Replace all BLAS/LAPACK calls with direct ccall using pointer arithmetic — no reshape, no SubArray, no heap allocations in the hot path
  • Non-allocating _partition_Sj_direct! that works directly with Z.rows offsets
  • Zero-alloc gather_update_matrix! using direct Z.vals index arithmetic
  • Optimized _transpose_chunks! with single pre-allocated buffer

Benchmarks

Problem N Before After Speedup
bcsstk18 12k 47 ms 18 ms 2.6x
crystm03 25k 296 ms 103 ms 2.9x
shipsec1 141k 7,706 ms 1,144 ms 6.7x
pwtk 218k 2,054 ms 1,292 ms 1.6x
ecology2 1M 13,786 ms 1,385 ms 10.0x

Allocations on a 50k 2D Laplacian: 490k → 12k.

Test plan

  • All existing tests pass (691 SuiteSparse SPD matrices, precision matrix, LDL support, Aqua)
  • Benchmarked across 13 SuiteSparse matrices — no regressions
  • Profiled allocation sources and time breakdown to guide each optimization

🤖 Generated with Claude Code

timweiland and others added 10 commits March 20, 2026 11:15
- Lift transposed_chunks, symmetric_access, depermuted_access from
  runtime Bool fields to type parameters on SupernodalMatrix{Tr,Sym,Dep}.
  This eliminates Union return types from get_chunk/get_split_chunk,
  enabling the compiler to specialize all code paths.

- Replace allocating partition_Sj with partition_Sj! that writes into
  a pre-allocated buffer. The old version allocated a new Vector plus
  per-block arrays on every supernode.

- Fix get_Sj to return a view instead of a copy.

- Use clear_indmap! (O(|Sj|)) instead of fill!(indmap, 0) (O(N)).

Reduces selinv time on 50k 2D Laplacian from 101ms to 78ms (23%).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rewrite gather_update_matrix! to use direct Z.vals index arithmetic
  instead of get_chunk/SubArray views (eliminates ~38k allocs)
- Fix indmap[block] indexing: use indmap[first(block)] instead of
  indmap[block][1] which copied the entire range (~370k allocs eliminated)
- Set GATHER_MIN_BLOCKS=1 (always gather) since the zero-alloc gather
  no longer has overhead that justified the scattered path

On 50k 2D Laplacian: 414k allocs → 44k allocs, 66ms → 57ms.
On ecology2 (1M nodes): ~12s → 2.3s (5x speedup from reduced GC pressure).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove nothing return from get_split_chunk (return empty SubArray
  for root instead), eliminating Union{Nothing, SubArray} return type
- Inline LL_to_LDL! in selinv_supernodal with direct chunk access,
  avoiding type-unstable get_split_chunk calls

On 50k 2D Laplacian: 44k → 38k allocations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace per-chunk copy+transpose+copyback in _transpose_chunks! with
  single pre-allocated buffer, eliminating ~6k temporary allocations
- Revert fused LL_to_LDL (forward pass has better cache locality)

On 50k 2D Laplacian: 38k → 19k allocations, 57ms → 52ms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move per-supernode logic into a separate @inline function to help
the compiler's escape analysis with SubArray views, reducing BLAS
dispatch overhead.

On 50k 2D Laplacian: 57ms → 52ms, 38k → 18.5k allocations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Eliminate all SubArray/ReshapedArray allocations from the selinv main
loop by calling BLAS/LAPACK routines directly via ccall with computed
pointers and leading dimensions. No reshape, no views, no heap
allocations in the hot path.

Results on 50k 2D Laplacian: 52ms → 47ms, 18.5k → 6.3k allocations.
Results on SuiteSparse ecology2 (1M): ~12s → 1.3s (10x speedup).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add _partition_Sj_direct! that works directly with Z.rows array
using offset arithmetic, avoiding the SubArray allocation from get_Sj.

On 50k 2D Laplacian: 47ms → 44.5ms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dead code since gather is now always used with direct BLAS ccalls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@timweiland
timweiland merged commit 36497b1 into main Mar 20, 2026
3 checks passed
@timweiland
timweiland deleted the feature/reduce-selinv-allocations branch March 20, 2026 13:22
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.

1 participant