Share the SROCK tableau vectors instead of rebuilding them per cache - #4050
Merged
ChrisRackauckas merged 1 commit intoJul 29, 2026
Merged
Conversation
SROCK2ConstantCache, TangXiaoSROCK2ConstantCache, KomBurSROCK2ConstantCache and
SROCKC2ConstantCache each built `recf` -- a 4476-element Vector{Float64}, 35 KiB
-- as an array literal inside the constructor, so every integrator allocated its
own copy of a compile-time constant table. SKSROCKConstantCache did the same with
199 BigFloat literals for `mc` and `mα`.
Nothing mutates these tables; the perform_step! methods only index them. Hoisting
them to module-level consts lets every cache share one copy. `convert(Vector{T},
...)` returns its argument unchanged when the element type already matches, so
Float64 solves share the table and other element types convert exactly as the
struct constructor did before.
This is not only a test-suite concern: an SDE solution retains its cache through
the `interp` field, so every retained trajectory of an SROCK ensemble was
carrying its own 35 KiB tableau.
Per-solution retention in an ensemble (2000 trajectories, scalar linear SDE,
save_everystep = false):
SROCKC2 iip 38.72 -> 3.72 KiB SROCK2 iip 40.63 -> 4.15 KiB
SROCKC2 oop 37.59 -> 2.59 KiB SROCK2 oop 39.44 -> 2.96 KiB
SimplifiedEM (control, untouched) 2.25 -> 2.25 KiB
Allocation per cache construction:
SROCK2 39368 -> 1760 bytes SROCKC2 37208 -> 1296
TangXiaoSROCK2 39192 -> 1616 bytes KomBurSROCK2 40224 -> 1296
SKSROCK 45024 -> 3488 bytes
SKSROCK keeps the allocation win (398 BigFloats are no longer constructed per
cache) but not the sharing one, since converting BigFloat to the cache element
type necessarily allocates.
The change is pure code motion. Verified:
- The multiset of all 19329 numeric literals in the file is unchanged.
- `git diff -w` is 382 lines; the rest of the diff is the 4-space de-indent
from moving the literals out of the function bodies.
- Solver output is bitwise identical on 32 cases -- SROCK1, SROCK2, SROCKEM,
SKSROCK, SROCKC2, TangXiaoSROCK2 (versions 1 and 5) and KomBurSROCK2, each
in-place and out-of-place, at dt = 1/2^5 and 1/2^7, comparing every saved
state with `==`.
- GROUP=Core tests pass (2m19s, exit 0), Runic clean.
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WW2jGeoWZVZu7AiscUNSz
ChrisRackauckas
marked this pull request as ready for review
July 29, 2026 01:29
This was referenced Jul 29, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Four SROCK constant caches built their Chebyshev recurrence table as an array literal inside the constructor:
So every integrator allocated its own copy of a compile-time constant.
SKSROCKConstantCachedid the same with 199BigFloatliterals each formcandmα.Nothing mutates these —
perform_step!only indexes them (μ = recf[start],recf[start + 2*(i-2)+1], …), which I checked across the whole subpackage.This is not only a test-suite concern. An SDE solution retains its cache through the
interpfield, so every retained trajectory of an SROCK ensemble carried its own 35 KiB tableau:The change
Hoist the nine large
Vectortableaus to module-levelconsts and take them withconvert(Vector{T}, ...), which returns its argument unchanged when the element type already matches — soFloat64solves share one copy, and other element types convert exactly as the struct's inner constructor did before.Measured effect
Per-solution retention in an ensemble (2000 trajectories, scalar linear SDE,
save_everystep = false):Allocation per cache construction:
SKSROCK keeps the allocation win (398
BigFloats no longer constructed per cache) but not the sharing one, since convertingBigFloatto the cache element type necessarily allocates.Knock-on effect for the weak-convergence CI groups, which retain every trajectory solution for every
dt(ConvergenceSimulation.solutions):SROCKC2WeakConvergence)OOPWeakConvergence)SROCKC2WeakConvergence)IIPWeakConvergence)That does not on its own make those groups schedulable — the residual is the per-trajectory solution retention in
test_convergence, which is a separate problem — but it removes a ~10× multiplier from all of them.Reviewing this diff
It is 18.7k lines, but purely code motion. Three independent checks:
git diff -wis 382 lines — that is the whole substantive change. The rest is the 4-space de-indent from moving literals out of the function bodies.grep -oEthe literals,sort,diff).dt = 1/2^5and1/2^7, comparing every saved state with==. Includes the three solvers whose tables were not hoisted, as controls.GROUP=Corepasses (2m19s, 0.84 GiB peak, exit 0). Runic clean.Not in this PR
The
NTupletableaus (ms,mσ,mτ,mα) are stored inline by value in the mutable structs, so hoisting them would not save anything. Left alone.Links
🤖 Generated with Claude Code
https://claude.ai/code/session_013WW2jGeoWZVZu7AiscUNSz