Speed up multi-dimensional stencil assembly by 20-40x - #9
Conversation
build_2d_stencil and build_3d_stencil (shared by buildDelta_2D/3D and
build_gradient_2D/3D, for both periodic and non-periodic systems) built
their result by repeatedly assigning dense sub-blocks into an
already-existing large SparseMatrixCSC via range indexing:
matrix[range_i, range_j] = block
Each such assignment can touch and reallocate a large fraction of the
matrix's internal column-pointer/row-index/value storage, so the total
cost scales far worse than the O(grid points) one would expect - this
was the dominant cost of setting up a 3D problem, e.g. ~174s at a
51^3 grid (9-point stencil), independent of which eigensolver was
used downstream.
Replaced with the standard fix: accumulate (row, col, value) triplets
across all stencil offsets and construct the matrix with a single
sparse(...) call. A naive version of this (plain COO with the default
summing of duplicate indices) does NOT reproduce the original exactly:
a periodic wraparound can place a block at the same target as a
non-wrapped placement, and the original's sequential assignment
means the later write wins, whereas summing would double-count -
this can only happen for grids smaller than the stencil width, but is
reproduced exactly regardless via a small per-row-block Dict that
resolves which stencil offset wins a given placement before any
triplets are appended.
Verified bit-for-bit identical (SparseMatrixCSC ==) against a
preserved copy of the original block-assignment implementation across
1D/2D/3D, every stencil size, all periodicity combinations, and
grids at/below the stencil width where the wraparound collision is
possible (test_buildStencilMatrices, 162 assertions). The full
existing test suite (941 tests, including golden-file physics
comparisons that depend on Delta/nabla being exactly correct) passes
unchanged.
Measured (9-point stencil, non-periodic 3D, build_3d_stencil alone):
n= 15: 0.86s -> 0.14s ( 6.0x)
n= 25: 6.90s -> 0.37s (18.6x)
n= 35: 29.31s -> 1.15s (25.5x)
n= 51: 173.7s -> 4.17s (41.7x)
This benefits every solver (arpack/krylov/lu, and lobpcg once that
PR merges) and both Laplacian and gradient assembly.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9 +/- ##
==========================================
+ Coverage 96.94% 97.02% +0.07%
==========================================
Files 32 32
Lines 1639 1645 +6
==========================================
+ Hits 1589 1596 +7
+ Misses 50 49 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Benchmark Results (Julia v1)Time benchmarks
Memory benchmarks
|
|
All CI checks are green: 3-platform test matrix, Documentation, Separately verifying the two gated heavy 3D golden-file testsets (3D Harmonic Oscillator, 3D Kronig-Penney — these depend on |
|
Update: three attempts to run the two heavy 3D golden-file testsets directly (
If you'd like the belt-and-suspenders confirmation, |
…inations The 2D loop capped at stencil=11 (missing the documented, legal size 13); the 3D loop capped at stencil=9 (missing 11, which is already reachable in production via stencil∇, and 13); and the 3D periodicity list only covered 5 of 8 combinations, always skipping "exactly two axes periodic". None of these were live bugs, but the gaps meant a regression in exactly those configurations - including the collision-handling Dict this PR introduces - could have slipped through despite the module docstring's claim of covering "all stencil sizes, every periodicity combination".
|
Pushed a follow-up commit after a deeper self-review pass: broadened test_buildStencilMatrices to cover stencil=13 in 2D and stencil=11/13 in 3D (11 is already reachable in production via stencil∇, and both were legal, documented sizes the tests silently skipped despite the module docstring's "all stencil sizes" claim), plus the three missing "exactly two axes periodic" 3D combinations. No live bug in any of these - all previously-untested configurations check out bit-exact - but the gaps meant a regression in exactly the collision-handling logic this PR introduces could have slipped through untested. Full suite (1207 tests) passes. Heads-up for whoever merges this and #8: both branches independently add |
Preconditioned LOBPCG solver for large non-periodic problems (v0.4.0)
build_2d_stencil/build_3d_stencil(shared bybuildΔ_2D/_3Dandbuild∇_2D/_3D, for both periodic and non-periodic systems) built their result by repeatedly assigning dense sub-blocks into an already-largeSparseMatrixCSCvia range indexing (matrix[range_i, range_j] = block). Each such assignment can touch and reallocate a large fraction of the matrix's internal CSC storage, so total cost scales far worse than the O(grid points) one would expect from the actual math being done — this was the dominant cost of setting up a 3D problem, independent of which eigensolver is used downstream.The fix
Replaced with the standard approach: accumulate (row, col, value) triplets across all stencil offsets and build the matrix with a single
sparse(...)call.A naive version of this (plain COO with the default summing of duplicate indices) does not reproduce the original exactly: a periodic wraparound can place a block at the same target as a non-wrapped placement, and the original's sequential assignment means the later write wins, whereas summing would double-count. This can only happen for grids smaller than the stencil width (an edge case, but a real one — I hit it empirically at
stencil_size=11on a 6×6 grid before catching it), so it's reproduced exactly via a small per-row-blockDictthat resolves which stencil offset wins a given placement before any triplets are appended.Verification
Bit-for-bit identical (
SparseMatrixCSC ==) against a preserved copy of the original block-assignment implementation, across 1D/2D/3D, every stencil size, all periodicity combinations, and grids at/below the stencil width where the wraparound collision is possible —test_buildStencilMatrices, 162 assertions, all passing. The full existing test suite (941 tests, including golden-file physics comparisons that depend on Δ/∇ being exactly correct — any discrepancy would show up as a numerical mismatch there) passes unchanged.Measured (9-point stencil, non-periodic 3D,
build_3d_stencilalone)This benefits every solver (arpack/krylov/lu, and
lobpcgonce #8 merges) and both Laplacian and gradient assembly, 2D and 3D, periodic and non-periodic.Status
Fast suite: 941/941. The heavy suite (
NUMEROV_TEST_FULL=true, the 3D golden-file physics tests) is still running as I write this — Arpack's eigensolve time (untouched by this change) dominates the heavy suite's wall clock regardless of assembly speed, so it's simply slow, not stuck. I'll confirm here once it completes; I'm confident in the result given the bit-exact equivalence proof above is coefficient-agnostic (it doesn't matter what physics the stencil values encode, only that the assembly algorithm is unchanged).