Skip to content

Fold RK46NL into the generic LowStorageRK2N dispatch - #3683

Merged
ChrisRackauckas merged 1 commit into
SciML:masterfrom
singhharsh1708:lowstorage-generic-perform-step
Aug 1, 2026
Merged

Fold RK46NL into the generic LowStorageRK2N dispatch#3683
ChrisRackauckas merged 1 commit into
SciML:masterfrom
singhharsh1708:lowstorage-generic-perform-step

Conversation

@singhharsh1708

@singhharsh1708 singhharsh1708 commented May 26, 2026

Copy link
Copy Markdown
Contributor

Rework after master's LowStorageRK consolidation landed. The scaffold this PR originally proposed (a unified 2N tableau plus a new generic perform_step) was superseded by 570dde8, which introduced the LowStorageRK2NAlgorithm union, per-method tableau constructors, and shared alg_cache methods for nine 2N methods. RK46NL was the one plain-2N method left out: it still carried its own constant cache struct, mutable cache struct, and four hand-unrolled initialize!/perform_step! methods. This PR is now exactly that migration, following the same pattern as the earlier 2N and 3S consolidations.

The change is mechanical. The tableau moves into a RK46NLConstantCache function returning LowStorageRK2NConstantCache with unchanged coefficients, RK46NL joins the LowStorageRK2NAlgorithm union, and the bespoke caches and kernels are deleted. The struct gains the williamson_condition keyword (default false) that the shared alg_cache reads, and isfsal/uses_uprev are declared false to match the rest of the family. The generic in-place kernel re-evaluates f at the start of each step instead of using FSAL storage, so the per-step f-call count stays at six. Net -134 lines of source.

One thing to be aware of: williamson_condition is now the struct's fourth field, so RK46NL(stage_limiter!, step_limiter!, thread) no longer resolves and positional construction takes four arguments. That is the same tradeoff the nine sibling 2N methods already made when they gained the keyword, and keyword construction is unchanged.

Version goes to 3.4.0. Master released OrdinaryDiffEqLowStorageRK 3.3.0 in #3241, and this adds a public keyword argument to an exported algorithm struct.

The previous revision of this branch was numerically broken in place: it declared isfsal(RK46NL) = false while keeping a kernel whose first stage read cache.fsalfirst, which is only refreshed by the FSAL copy, so every step after the first used a stale derivative and measured order collapsed to ~0. Deleting that kernel in favor of the shared one removes the failure mode instead of patching it.

A correction to the earlier text of this description, which claimed fixed-dt end states are bit-identical to the removed implementation. That holds for most but not all of the space. Running one script against master at 0be95df and against this branch, dt = 1/128 and adaptive = false: every in-place problem tested comes out bit-identical (2x2 linear, Lorenz, a nonlinear scalar wrapped in a one-element vector), and so does every out-of-place problem whose state is an array (2x2 linear, out-of-place Lorenz). Out-of-place problems whose state is a plain Number do not. Those land 3 ulp apart after 128 steps of u' = 1.01u (4.9e-16 relative) and 1 ulp apart after 256 steps of u' = 1.01u - 0.3u^2 + 0.1sin(t) (1.7e-16 relative). On the linear problem both trees sit 1.9e-12 from the analytic solution, so the disagreement between them is about 3900 times smaller than the method's own discretization error at that step size.

The cause is fused multiply-add contraction, which is also why only scalars are touched. Both the removed kernel and the shared one write the stage update as Atmp + dtk, and @muladd expands both to the same muladd(dt, k, Atmp). Compiled, the straight-line unrolled version folds the other product into the fma instead: putting fma(A, tmp, dtk) into a standalone replica of the recurrence reproduces master's bits exactly on both scalar problems, and fma(dt, k, A*tmp) reproduces this branch's. When the state is an array, muladd is not a fused operation at all, both forms round the same way, and every array case matches to the bit.

Testing. RK46NL had no williamson_condition coverage, so this adds an RK46NL testset next to ORK256 built the way the nine siblings are: williamson_condition = true and false both go through test_convergence on the only-time, linear, and nonlinear problem sets at dts = 1 ./ 2 .^ (7:-1:3), followed by the Base.summarysize register bounds and the alias_u0 check. Measured order is 3.97 on the only-time problems, 3.99 on the three linear ones and 4.03 on the nonlinear ones, identical for both settings of the keyword. The full ode_low_storage_rk_tests.jl file now reports 566 pass, 28 pre-existing broken and 0 failures, against 547 pass and the same 28 broken on master, so the 19 new tests all pass and nothing else moved. The 515/28 figure quoted earlier in this description predates the rebase onto #3241. AllocCheck finds perform_step! allocation-free for RK46NL on the new cache path with williamson_condition either way.

AI Disclosure

Claude assisted with this work.

Comment thread lib/OrdinaryDiffEqLowStorageRK/src/generic_low_storage_perform_step.jl Outdated
@ChrisRackauckas

Copy link
Copy Markdown
Member

family by family would probably be easier, yes.

@singhharsh1708
singhharsh1708 force-pushed the lowstorage-generic-perform-step branch 2 times, most recently from 087bdb7 to eedc675 Compare May 27, 2026 05:38
@ChrisRackauckas

ChrisRackauckas commented May 27, 2026

Copy link
Copy Markdown
Member

Steps don't matter for non-adaptive methods, are the solutions identical?

Comment thread lib/OrdinaryDiffEqLowStorageRK/src/low_storage_tableaus.jl Outdated
@singhharsh1708

singhharsh1708 commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

@ChrisRackauckas Yes, solutions are identical — validated by running each algorithm on a Lorenz problem and comparing the final state vector between the branch and master.

Comment thread lib/OrdinaryDiffEqLowStorageRK/src/low_storage_rk_perform_step.jl Outdated
c5 = convert(T2, 0.8)
c2end = (c2, c3, c4, c5)

return LowStorageRK2NConstantCache(A2end, B1, B2end, c2end)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these all already hit the same code, so it's not a substantive change. Do any other caches hit the same form?

@singhharsh1708

Copy link
Copy Markdown
Contributor Author

Updated: helper indirection removed — perform_step! bodies are now directly in generic_low_storage_perform_step.jl with no intermediate _perform_step_oop!/_perform_step_iip! layer.

Re 'do any other caches hit the same form': yes — RK46NLConstantCache and SHLDDRK52ConstantCache both use the same 2N update formula (tmp = αᵢ * tmp + dt * f(u,...); u += βᵢ * tmp) but store coefficients as individual scalar fields rather than tuples. They could be folded into LowStorageRKTableau{TwoN} by converting those fields to (A2end, B1, B2end, c2end) tuples — that's planned for PR #6. SHLDDRK_2N has a different step structure (odd/even stage alternation) so it stays separate.

@singhharsh1708

Copy link
Copy Markdown
Contributor Author

@ChrisRackauckas i think once its merged we can do that then

@ChrisRackauckas

Copy link
Copy Markdown
Member

see the current comment.

@singhharsh1708
singhharsh1708 force-pushed the lowstorage-generic-perform-step branch 2 times, most recently from 44cb756 to fd240f0 Compare May 27, 2026 21:48
@singhharsh1708

singhharsh1708 commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

@ChrisRackauckas ohh i see In previously merged #3590, we unified the 9 low-storage 2N methods under a shared LowStorageRK2NConstantCache, reducing alg_cache methods from 82 → 18
Would it make sense to fold RK46NL into the shared LowStorageRK2NConstantCache path as well? It follows the same 2N formulation; only SHLDDRK52 still seems special because of kshortsize = 2.

@singhharsh1708
singhharsh1708 force-pushed the lowstorage-generic-perform-step branch from fd240f0 to 5562000 Compare July 30, 2026 21:25
@singhharsh1708 singhharsh1708 changed the title scaffold unified LowStorageRKTableau + generic perform_step (2N form) Fold RK46NL into the generic LowStorageRK2N dispatch Jul 30, 2026
@singhharsh1708
singhharsh1708 force-pushed the lowstorage-generic-perform-step branch from 5562000 to 075cc7c Compare July 31, 2026 10:29
RK46NL was the last plain-2N method still carrying a bespoke constant
cache struct, mutable cache struct, and four hand-unrolled
initialize!/perform_step! methods. Its recurrence is the same Williamson
form the shared LowStorageRK2N kernels already run, so the tableau moves
into a RK46NLConstantCache function returning a
LowStorageRK2NConstantCache with unchanged coefficients, RK46NL joins
the LowStorageRK2NAlgorithm union, and the bespoke caches and kernels
are deleted.

The struct gains the williamson_condition keyword (default false) that
the shared alg_cache reads, and isfsal/uses_uprev are declared false to
match the rest of the family. The generic in-place kernel re-evaluates f
at the start of each step instead of using FSAL storage, keeping the
per-step f-call count at six. The new field is fourth, so positional
construction now takes four arguments, the same break the nine sibling
2N methods already took when they gained the keyword.

Fixed-dt end states are bit-identical to the removed implementation for
in-place problems and for out-of-place problems whose state is an array.
Out-of-place problems with a scalar state differ in the last bits: 3 ulp
after 128 steps of u' = 1.01u, 1 ulp after 256 steps of a nonlinear
scalar problem, roughly four thousand times below the method's own
discretization error there. Both kernels expand to the same
muladd(dt, k, A*tmp), but the straight-line code folds the other product
into the fused multiply-add, and only scalar states are affected because
muladd over arrays never fuses.

RK46NL gains the williamson_condition testset the nine siblings already
have. Version goes to 3.4.0 for the new public keyword.
@singhharsh1708
singhharsh1708 force-pushed the lowstorage-generic-perform-step branch from 075cc7c to 4a6bfc8 Compare July 31, 2026 20:09
@ChrisRackauckas
ChrisRackauckas merged commit 3ef1504 into SciML:master Aug 1, 2026
106 of 131 checks passed
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