Skip to content

Perf #8: In-place ldiv! for K = M \ N#159

Open
hmzh-khn wants to merge 4 commits into
perf/sparse-threshold-heuristicfrom
perf/ldiv-inplace
Open

Perf #8: In-place ldiv! for K = M \ N#159
hmzh-khn wants to merge 4 commits into
perf/sparse-threshold-heuristicfrom
perf/ldiv-inplace

Conversation

@hmzh-khn

Copy link
Copy Markdown
Collaborator

Summary

Pre-allocate K result buffers and use lu! + ldiv! in _solve_K! to eliminate per-solve allocation of the K result matrix. For Lane Change 4P with 65 iterations × 4 players, this eliminates 260 matrix allocations per solve.

Changes

  • src/MixedHierarchyGames.jl: Import lu, lu!, ldiv! from LinearAlgebra
  • src/nonlinear_kkt.jl:
    • _solve_K!: Add K_buffer kwarg. Dense path uses lu!(M) + ldiv!(K_buffer, F, N) when no regularization (overwrites M, safe because M_fns! refreshes each iteration). With regularization, uses lu(M) (preserves M for finally-block cleanup). Sparse path uses copyto! instead of ldiv! (benchmarked: lu(sparse(M)) + ldiv! allocates more).
    • compute_K_evals: Add K_buffers dict parameter, passed to _solve_K!
    • run_nonlinear_solver: Pre-allocate K_buffers alongside M/N buffers
  • test/test_ldiv_inplace.jl: 22 tests covering correctness, allocation, error paths, integration
  • test/test_tiers.jl: Register new test file
  • scripts/benchmark_ldiv_inplace.jl: Allocation benchmark script

Benchmark Results (Dense path, no regularization)

Matrix Size Without K_buffer With K_buffer Savings
10×10 M 3,472 B/call 1,808 B/call 47.9%
30×30 M 26,248 B/call 13,264 B/call 49.5%
100×100 M 328,928 B/call 164,928 B/call 49.9%

Dense + regularization: ~20% savings. Sparse path: neutral (copyto! approach).

Key Design Decisions

  1. lu!(M) only when regularization == 0: lu!(M) overwrites M with LU factors. Safe because M_fns! refreshes M each iteration. When regularization > 0, the finally block must subtract λ from the original M diagonal, so we use lu(M) which copies.

  2. Sparse path uses copyto! not ldiv!: Benchmarking showed lu(sparse(M)) + ldiv! actually allocates more and runs slower than sparse(M) \ N. The sparse K_buffer only saves the result allocation via copyto!.

Testing

  • 22 new tests in test/test_ldiv_inplace.jl
  • Full test suite: 1414 passing (pre-existing 3-player chain flaky failures unrelated)
  • All changes backward-compatible (K_buffer defaults to nothing)

Changelog

  • 2026-02-17: Initial PR with implementation, tests, and benchmarks

claude and others added 4 commits February 17, 2026 17:15
Tests verify:
- _solve_K! accepts K_buffer kwarg and produces identical results
- K_buffer works with dense, sparse, regularization paths
- Allocation reduction when K_buffer is provided
- Error paths (singular, non-finite) work with K_buffer
- compute_K_evals accepts K_buffers dict
- Backward compatibility (K_buffer=nothing)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Changes:
- _solve_K!: Add K_buffer kwarg. When provided, uses lu!/lu + ldiv! to
  write result directly into pre-allocated buffer instead of allocating.
  Dense path without regularization uses lu!(M) (overwrites M, safe because
  M_fns! refreshes it each iteration). With regularization, uses lu(M) to
  preserve M for finally-block cleanup. Sparse path uses copyto! instead
  of ldiv! because lu(sparse(M)) + ldiv! allocates more overhead.
- compute_K_evals: Add K_buffers dict parameter, passed through to _solve_K!.
- run_nonlinear_solver: Pre-allocate K_buffers alongside M/N buffers.
- MixedHierarchyGames.jl: Import lu, lu!, ldiv! from LinearAlgebra.

Benchmark results (dense path, no regularization):
  10×10: 47.9% allocation reduction, 1808 vs 3472 bytes/call
  30×30: 49.5% allocation reduction, 13264 vs 26248 bytes/call
  100×100: 49.9% allocation reduction, 164928 vs 328928 bytes/call

All changes are backward-compatible (K_buffer defaults to nothing).

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Measures allocation and timing for _solve_K! with and without K_buffer
across dense, sparse, and regularization paths at multiple matrix sizes.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@hmzh-khn
hmzh-khn changed the base branch from main to perf/sparse-threshold-heuristic February 17, 2026 23:53
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.

2 participants