Skip to content

Add integrand_inplace option to the integrating sum callbacks#313

Merged
ChrisRackauckas merged 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:oop-integrand-cache-gauss
Jun 10, 2026
Merged

Add integrand_inplace option to the integrating sum callbacks#313
ChrisRackauckas merged 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:oop-integrand-cache-gauss

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor

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-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 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 SVector state), 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} = nothing keyword 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-place integrand_func(out, u, t, integrator) form into integrand_cache regardless of problem in-placeness (requires integrand_prototype; ArgumentError otherwise).
  • 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, full solve with Tsit5() over (0, 10), abstol = reltol = 1e-10:

allocating 3-arg integrand:   196.838 μs (865 allocations: 380.70 KiB)
in-place 4-arg integrand:     123.309 μs (253 allocations: 103.38 KiB)

≈1.6× faster, 3.7× fewer allocations, identical results (isapprox at rtol = 1e-12). For expensive integrands (AD-based vjps as in SciMLSensitivity's EnzymeVJP/ZygoteVJP) the difference is negligible (<1%) since the integrand itself dominates; the win is for cheap integrands (analytical vjp_p/paramjac, simple user integrands).

Tests

Added tests for integrand_inplace = true on an out-of-place SVector problem (both callbacks), the ArgumentError on a missing prototype, and integrand_inplace = false forcing the allocating form on an in-place problem. All four integrating*_tests.jl files pass locally with the change. Runic applied.

A follow-up PR to SciMLSensitivity will switch GaussAdjoint's immutable-state path to integrand_inplace = true once this is released.

🤖 Generated with Claude Code

…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>
@ChrisRackauckas ChrisRackauckas marked this pull request as ready for review June 10, 2026 09:38
@ChrisRackauckas ChrisRackauckas merged commit 2de366d into SciML:master Jun 10, 2026
14 of 33 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