Skip to content

Share the SROCK tableau vectors instead of rebuilding them per cache - #4050

Merged
ChrisRackauckas merged 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:srock-const-tableaus
Jul 29, 2026
Merged

Share the SROCK tableau vectors instead of rebuilding them per cache#4050
ChrisRackauckas merged 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:srock-const-tableaus

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member

Please ignore this PR until it has been reviewed by @ChrisRackauckas.

The problem

Four SROCK constant caches built their Chebyshev recurrence table as an array literal inside the constructor:

function SROCK2ConstantCache{T}(zprev) where {T}
    ...
    recf = [
        0.1794612899156781e+0,
        ...            # 4476 entries, 35 KiB
    ]

So every integrator allocated its own copy of a compile-time constant. SKSROCKConstantCache did the same with 199 BigFloat literals each for mc and .

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 interp field, so every retained trajectory of an SROCK ensemble carried its own 35 KiB tableau:

SimplifiedEM  total= 2.31 KiB   interp= 0.62
SROCK2        total=40.69 KiB   interp=39.00
SROCKC2       total=38.78 KiB   interp=37.09

The change

Hoist the nine large Vector tableaus to module-level consts and take them with convert(Vector{T}, ...), which returns its argument unchanged when the element type already matches — so Float64 solves 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):

before after
SROCKC2 iip 38.72 KiB 3.72
SROCKC2 oop 37.59 KiB 2.59
SROCK2 iip 40.63 KiB 4.15
SROCK2 oop 39.44 KiB 2.96
SimplifiedEM (control, untouched) 2.25 KiB 2.25

Allocation per cache construction:

solver before after
SROCK2 39,368 B 1,760 B
SROCKC2 37,208 B 1,296 B
TangXiaoSROCK2 39,192 B 1,616 B
KomBurSROCK2 40,224 B 1,296 B
SKSROCK 45,024 B 3,488 B

SKSROCK keeps the allocation win (398 BigFloats no longer constructed per cache) but not the sharing one, since converting BigFloat to 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):

study traj × dts before after
SROCKC2 iip (SROCKC2WeakConvergence) 3e6 × 5 553.9 GiB ~53
SROCK2 oop (OOPWeakConvergence) 7e5 × 8 210.6 GiB ~16
SROCKC2 oop (SROCKC2WeakConvergence) 1e6 × 5 179.3 GiB ~12
SROCK2 iip (IIPWeakConvergence) 5e5 × 8 155.0 GiB ~16

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 -w is 382 lines — that is the whole substantive change. The rest is the 4-space de-indent from moving literals out of the function bodies.
  • The multiset of all 19,329 numeric literals in the file is byte-identical before and after (grep -oE the literals, sort, diff).
  • Solver output is bitwise identical on 32 cases: SROCK1, SROCK2, SROCKEM, SKSROCK, SROCKC2, TangXiaoSROCK2 (versions 1 and 5), KomBurSROCK2 — each in-place and out-of-place, at dt = 1/2^5 and 1/2^7, comparing every saved state with ==. Includes the three solvers whose tables were not hoisted, as controls.

GROUP=Core passes (2m19s, 0.84 GiB peak, exit 0). Runic clean.

Not in this PR

The NTuple tableaus (ms, , , ) 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

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
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