Skip to content

Perf T1-11: Pre-compute sparse pattern for use_sparse mode#152

Open
hmzh-khn wants to merge 3 commits into
perf/21-sparse-pattern-cachefrom
perf/21-sparse-pattern-cache-impl
Open

Perf T1-11: Pre-compute sparse pattern for use_sparse mode#152
hmzh-khn wants to merge 3 commits into
perf/21-sparse-pattern-cachefrom
perf/21-sparse-pattern-cache-impl

Conversation

@hmzh-khn

Copy link
Copy Markdown
Collaborator

Summary

Pre-compute SparseMatrixCSC sparsity patterns once per player during solver setup,
then reuse them across iterations by updating nzval in-place instead of calling
sparse(M) each time.

Changes

  • src/MixedHierarchyGames.jl: Import SparseMatrixCSC, nonzeros, rowvals, nzrange from SparseArrays
  • src/nonlinear_kkt.jl:
    • Add _update_sparse_from_dense!() helper for zero-alloc nzval update
    • Add sparse_cache kwarg to _solve_K!() to use cached sparsity patterns
    • Add sparse_M_caches kwarg to compute_K_evals() to thread caches through
    • Pre-compute patterns in run_nonlinear_solver() via probe evaluation at initial z_est
  • test/test_sparse_pattern_cache.jl: New test file (12 tests) verifying:
    • Structural sparsity pattern consistency across different z values
    • Numerical equivalence of cached vs fresh sparse solve
    • run_nonlinear_solver works correctly with sparse caching
    • Allocation reduction from cached nzval update vs fresh sparse() (27x reduction: 1311→47 bytes/call)
  • test/test_tiers.jl: Add new test file to slow test tier
  • test/test_test_tiers.jl: Update test tier self-test expectations

Benchmark Results

3-player chain (T=3, s=2, c=2), 20 iterations:

  • Sparse (cached): 1.01 MB alloc, 55.5 ms
  • Dense: 1.27 MB alloc, 123.3 ms
  • 20% allocation reduction, 55% time reduction

Sparse construction allocations (isolated):

  • Fresh sparse(M): 1311 bytes/call
  • Cached nzval update: 47 bytes/call
  • 27x reduction per-call

Testing

  • All 1353 tests pass (julia --project=. -e 'using Pkg; Pkg.test()')
  • All existing sparse solve tests pass unchanged
  • No tolerance changes

Changelog

  • 2026-02-16: Initial PR with sparse pattern caching implementation

🤖 Generated with Claude Code

claude and others added 3 commits February 16, 2026 11:51
Tests verify:
- Sparse pattern (rowval/colptr) is structurally identical across z values
- Cached sparse solve matches fresh sparse solve numerically
- run_nonlinear_solver works with use_sparse=:always
- Cached nzval update allocates less than fresh sparse() construction

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Pre-compute SparseMatrixCSC sparsity patterns once per player during solver
setup, then reuse them across iterations by updating nzval in-place instead
of calling sparse(M) each time.

Changes:
- Add _update_sparse_from_dense!() helper for zero-alloc nzval update
- Add sparse_cache kwarg to _solve_K!() to use cached patterns
- Add sparse_M_caches kwarg to compute_K_evals() to thread caches through
- Pre-compute patterns in run_nonlinear_solver() via probe evaluation at z_est
- Import SparseMatrixCSC, nonzeros, rowvals, nzrange from SparseArrays

The sparse pattern is structurally fixed (determined by symbolic derivatives)
so the CSC structure (rowval, colptr) never changes — only nzval varies.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
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