Add integrand_inplace option to the integrating sum callbacks#313
Merged
ChrisRackauckas merged 1 commit intoJun 10, 2026
Merged
Conversation
…umCallback
The integrating sum callbacks chose the integrand calling convention from the
problem's in-placeness: out-of-place problems always used the allocating
3-arg `integrand_func(u, t, integrator)` form, even when an
`integrand_prototype` was supplied. But the integrand output is often
parameter-shaped (e.g. the dG/dp quadrature in adjoint methods), so it can be
a mutable buffer even when the state is immutable (e.g. SVector) — and the
allocating form then forces an output allocation on every quadrature node of
every step.
Add an `integrand_inplace::Union{Nothing, Bool} = nothing` keyword:
- `nothing` (default) keeps the existing behavior (in-place form for
in-place problems with a prototype, allocating form otherwise),
- `true` forces the in-place 4-arg form into `integrand_cache` regardless
of problem in-placeness (requires an `integrand_prototype`),
- `false` forces the allocating form.
Benchmark (OOP SVector ODE, 10 states, cheap 100-element integrand,
Tsit5 over [0, 10], abstol=reltol=1e-10, full solve):
- allocating 3-arg integrand: 196.8 μs (865 allocations: 380.70 KiB)
- in-place 4-arg integrand: 123.3 μs (253 allocations: 103.38 KiB)
For expensive integrands (e.g. AD-based vjps) the difference is negligible
since the integrand itself dominates.
Motivated by SciML/SciMLSensitivity.jl#1479 (GaussAdjoint on immutable
SVector state), which currently has to use the allocating form for immutable
state.
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Important
This PR should be ignored until reviewed by @ChrisRackauckas.
Problem
The integrating sum callbacks (
IntegratingSumCallback,IntegratingGKSumCallback) pick the integrand calling convention purely from the problem's in-placeness: out-of-place problems always get the allocating 3-argintegrand_func(u, t, integrator)form — even when anintegrand_prototypewas supplied. But the integrand output is often parameter-shaped (e.g. the dG/dp quadrature in adjoint sensitivity methods), so it can be a perfectly mutable buffer even when the ODE state is immutable (e.g.SVector). The allocating form then forces an output allocation on every quadrature node of every accepted step.This came out of SciML/SciMLSensitivity.jl#1479 (GaussAdjoint support for immutable
SVectorstate), where the Gauss quadrature integrand is the parameter vjpλᵀ(∂f/∂p)— written into a mutable parameter-length buffer — but the out-of-place adjoint problem forces the allocating path.Change
Adds an opt-in
integrand_inplace::Union{Nothing, Bool} = nothingkeyword to both callbacks:nothing(default): existing behavior, fully backwards compatible — in-place 4-arg form for in-place problems with a prototype, allocating 3-arg form otherwise.true: force the in-placeintegrand_func(out, u, t, integrator)form intointegrand_cacheregardless of problem in-placeness (requiresintegrand_prototype;ArgumentErrorotherwise).false: force the allocating form.The default could not simply be changed to "use the cache whenever a prototype is given" because the existing documented behavior (and existing tests) pass a prototype with out-of-place problems and a 3-arg integrand — the prototype is also needed for the accumulation cache.
Benchmark (run locally)
Out-of-place
SVector{10}linear ODE, cheap 100-element integrand, fullsolvewithTsit5()over(0, 10),abstol = reltol = 1e-10:≈1.6× faster, 3.7× fewer allocations, identical results (
isapproxatrtol = 1e-12). For expensive integrands (AD-based vjps as in SciMLSensitivity'sEnzymeVJP/ZygoteVJP) the difference is negligible (<1%) since the integrand itself dominates; the win is for cheap integrands (analyticalvjp_p/paramjac, simple user integrands).Tests
Added tests for
integrand_inplace = trueon an out-of-placeSVectorproblem (both callbacks), theArgumentErroron a missing prototype, andintegrand_inplace = falseforcing the allocating form on an in-place problem. All fourintegrating*_tests.jlfiles pass locally with the change. Runic applied.A follow-up PR to SciMLSensitivity will switch GaussAdjoint's immutable-state path to
integrand_inplace = trueonce this is released.🤖 Generated with Claude Code