Skip to content

Commit 2de366d

Browse files
ChrisRackauckas-ClaudeChrisRackauckasclaude
authored
Add integrand_inplace option to IntegratingSumCallback/IntegratingGKSumCallback (#313)
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 (Claude) <accounts@chrisrackauckas.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 0b80be5 commit 2de366d

4 files changed

Lines changed: 142 additions & 41 deletions

File tree

src/integrating_GK_sum.jl

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mutable struct SavingIntegrandGKSumAffect{
1010
gk_step_cache::IntegrandCacheType
1111
gk_err_cache::IntegrandCacheType
1212
tol::Float64
13+
integrand_inplace::Union{Nothing, Bool}
1314
end
1415

1516
function integrate_gk!(
@@ -18,44 +19,39 @@ function integrate_gk!(
1819
)
1920
affect!.gk_step_cache = recursive_zero!(affect!.gk_step_cache)
2021
affect!.gk_err_cache = recursive_zero!(affect!.gk_err_cache)
22+
isinplace_prob = DiffEqBase.isinplace(integrator.sol.prob)
23+
inplace_integrand = affect!.integrand_inplace === nothing ?
24+
(isinplace_prob && affect!.integrand_cache !== nothing) :
25+
affect!.integrand_inplace
2126
for i in 1:(2 * order + 1)
2227
t_temp = (gk_points[order][i] + 1) * ((bound_r - bound_l) / 2) + bound_l
23-
if DiffEqBase.isinplace(integrator.sol.prob)
28+
if isinplace_prob
2429
curu = first(get_tmp_cache(integrator))
2530
integrator(curu, t_temp)
26-
if affect!.integrand_cache == nothing
27-
recursive_axpy!(
28-
gk_weights[order][i],
29-
affect!.integrand_func(curu, t_temp, integrator), affect!.gk_step_cache
30-
)
31-
if i % 2 == 0
32-
recursive_axpy!(
33-
g_weights[order][div(i, 2)],
34-
affect!.integrand_func(curu, t_temp, integrator), affect!.gk_err_cache
35-
)
36-
end
37-
else
38-
affect!.integrand_func(affect!.integrand_cache, curu, t_temp, integrator)
31+
else
32+
curu = integrator(t_temp)
33+
end
34+
if inplace_integrand
35+
affect!.integrand_func(affect!.integrand_cache, curu, t_temp, integrator)
36+
recursive_axpy!(
37+
gk_weights[order][i],
38+
affect!.integrand_cache, affect!.gk_step_cache
39+
)
40+
if i % 2 == 0
3941
recursive_axpy!(
40-
gk_weights[order][i],
41-
affect!.integrand_cache, affect!.gk_step_cache
42+
g_weights[order][div(i, 2)],
43+
affect!.integrand_cache, affect!.gk_err_cache
4244
)
43-
if i % 2 == 0
44-
recursive_axpy!(
45-
g_weights[order][div(i, 2)],
46-
affect!.integrand_cache, affect!.gk_err_cache
47-
)
48-
end
4945
end
5046
else
5147
recursive_axpy!(
5248
gk_weights[order][i],
53-
affect!.integrand_func(integrator(t_temp), t_temp, integrator), affect!.gk_step_cache
49+
affect!.integrand_func(curu, t_temp, integrator), affect!.gk_step_cache
5450
)
5551
if i % 2 == 0
5652
recursive_axpy!(
5753
g_weights[order][div(i, 2)],
58-
affect!.integrand_func(integrator(t_temp), t_temp, integrator), affect!.gk_err_cache
54+
affect!.integrand_func(curu, t_temp, integrator), affect!.gk_err_cache
5955
)
6056
end
6157
end
@@ -108,6 +104,18 @@ returns Integral(integrand_func(u(t),t)dt over the problem tspan.
108104
that `integrand_func` will output (or higher compatible type).
109105
- `integrand_prototype` is a prototype of the output from the integrand.
110106
107+
## Keyword Arguments
108+
109+
- `integrand_inplace = nothing`: controls which form of `integrand_func` is called.
110+
With the default `nothing`, the in-place `integrand_func(out, u, t, integrator)`
111+
form is used for in-place problems (when an `integrand_prototype` is given) and
112+
the allocating `integrand_func(u, t, integrator)` form for out-of-place problems.
113+
Pass `integrand_inplace = true` to force the in-place form even for out-of-place
114+
problems — the integrand output (e.g. a parameter-shaped buffer) may be mutable
115+
even when the state is immutable, which avoids allocating the output on every
116+
quadrature node. Requires an `integrand_prototype`. Pass `integrand_inplace = false`
117+
to force the allocating form.
118+
111119
The outputted values are saved into `integrand_values`. The values are found
112120
via `integrand_values.integrand`.
113121
@@ -119,11 +127,21 @@ via `integrand_values.integrand`.
119127
solvers are required.
120128
"""
121129
function IntegratingGKSumCallback(
122-
integrand_func, integrand_values::IntegrandValuesSum, integrand_prototype, tol = 1.0e-7
130+
integrand_func, integrand_values::IntegrandValuesSum, integrand_prototype,
131+
tol = 1.0e-7;
132+
integrand_inplace::Union{Nothing, Bool} = nothing
123133
)
134+
if integrand_inplace === true && integrand_prototype === nothing
135+
throw(
136+
ArgumentError(
137+
"integrand_inplace = true requires an integrand_prototype to use as the output buffer."
138+
)
139+
)
140+
end
124141
affect! = SavingIntegrandGKSumAffect(
125142
integrand_func, integrand_values, integrand_prototype,
126-
allocate_zeros(integrand_prototype), allocate_zeros(integrand_prototype), allocate_zeros(integrand_prototype), tol
143+
allocate_zeros(integrand_prototype), allocate_zeros(integrand_prototype),
144+
allocate_zeros(integrand_prototype), tol, integrand_inplace
127145
)
128146
condition = true_condition
129147
return DiscreteCallback(condition, affect!, save_positions = (false, false))

src/integrating_sum.jl

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ mutable struct SavingIntegrandSumAffect{IntegrandFunc, integrandType, IntegrandC
6969
integrand_values::IntegrandValuesSum{integrandType}
7070
integrand_cache::IntegrandCacheType
7171
accumulation_cache::IntegrandCacheType
72+
integrand_inplace::Union{Nothing, Bool}
73+
end
74+
75+
function SavingIntegrandSumAffect(
76+
integrand_func, integrand_values, integrand_cache, accumulation_cache
77+
)
78+
return SavingIntegrandSumAffect(
79+
integrand_func, integrand_values, integrand_cache, accumulation_cache, nothing
80+
)
7281
end
7382

7483
function (affect!::SavingIntegrandSumAffect)(integrator)
@@ -79,26 +88,27 @@ function (affect!::SavingIntegrandSumAffect)(integrator)
7988
n = div(SciMLBase.alg_order(integrator.alg) + 1, 2)
8089
end
8190
accumulation_cache = recursive_zero!(affect!.accumulation_cache)
91+
isinplace_prob = DiffEqBase.isinplace(integrator.sol.prob)
92+
inplace_integrand = affect!.integrand_inplace === nothing ?
93+
(isinplace_prob && affect!.integrand_cache !== nothing) :
94+
affect!.integrand_inplace
8295
for i in 1:n
8396
t_temp = ((integrator.t - integrator.tprev) / 2) * gauss_points[n][i] + (integrator.t + integrator.tprev) / 2
84-
if DiffEqBase.isinplace(integrator.sol.prob)
97+
if isinplace_prob
8598
curu = first(get_tmp_cache(integrator))
8699
integrator(curu, t_temp)
87-
if affect!.integrand_cache === nothing
88-
recursive_axpy!(
89-
gauss_weights[n][i],
90-
affect!.integrand_func(curu, t_temp, integrator), accumulation_cache
91-
)
92-
else
93-
affect!.integrand_func(affect!.integrand_cache, curu, t_temp, integrator)
94-
recursive_axpy!(
95-
gauss_weights[n][i], affect!.integrand_cache, accumulation_cache
96-
)
97-
end
100+
else
101+
curu = integrator(t_temp)
102+
end
103+
if inplace_integrand
104+
affect!.integrand_func(affect!.integrand_cache, curu, t_temp, integrator)
105+
recursive_axpy!(
106+
gauss_weights[n][i], affect!.integrand_cache, accumulation_cache
107+
)
98108
else
99109
recursive_axpy!(
100110
gauss_weights[n][i],
101-
affect!.integrand_func(integrator(t_temp), t_temp, integrator), accumulation_cache
111+
affect!.integrand_func(curu, t_temp, integrator), accumulation_cache
102112
)
103113
end
104114
end
@@ -128,6 +138,18 @@ returns Integral(integrand_func(u(t),t)dt over the problem tspan.
128138
that `integrand_func` will output (or higher compatible type).
129139
- `integrand_prototype` is a prototype of the output from the integrand.
130140
141+
## Keyword Arguments
142+
143+
- `integrand_inplace = nothing`: controls which form of `integrand_func` is called.
144+
With the default `nothing`, the in-place `integrand_func(out, u, t, integrator)`
145+
form is used for in-place problems (when an `integrand_prototype` is given) and
146+
the allocating `integrand_func(u, t, integrator)` form for out-of-place problems.
147+
Pass `integrand_inplace = true` to force the in-place form even for out-of-place
148+
problems — the integrand output (e.g. a parameter-shaped buffer) may be mutable
149+
even when the state is immutable, which avoids allocating the output on every
150+
quadrature node. Requires an `integrand_prototype`. Pass `integrand_inplace = false`
151+
to force the allocating form.
152+
131153
The outputted values are saved into `integrand_values`. The values are found
132154
via `integrand_values.integrand`.
133155
@@ -137,11 +159,19 @@ via `integrand_values.integrand`.
137159
solvers are required.
138160
"""
139161
function IntegratingSumCallback(
140-
integrand_func, integrand_values::IntegrandValuesSum, integrand_prototype
162+
integrand_func, integrand_values::IntegrandValuesSum, integrand_prototype;
163+
integrand_inplace::Union{Nothing, Bool} = nothing
141164
)
165+
if integrand_inplace === true && integrand_prototype === nothing
166+
throw(
167+
ArgumentError(
168+
"integrand_inplace = true requires an integrand_prototype to use as the output buffer."
169+
)
170+
)
171+
end
142172
affect! = SavingIntegrandSumAffect(
143173
integrand_func, integrand_values, integrand_prototype,
144-
allocate_zeros(integrand_prototype)
174+
allocate_zeros(integrand_prototype), integrand_inplace
145175
)
146176
condition = true_condition
147177
return DiscreteCallback(condition, affect!, save_positions = (false, false))

test/integrating_GK_sum_tests.jl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,22 @@ dGdp_analytical = analytical_derivative(p, tspan[end])
8080
@test isapprox(
8181
dGdp_analytical, integrand_values_inplace.integrand, atol = 1.0e-11, rtol = 1.0e-11
8282
)
83+
84+
# integrand_inplace = true: in-place integrand with an out-of-place problem
85+
using StaticArrays
86+
prob_oop = ODEProblem((u, p, t) -> SVector(1.0), SVector(0.0), (0.0, 1.0))
87+
integrated = IntegrandValuesSum(zeros(1))
88+
sol = solve(
89+
prob_oop, Euler(),
90+
callback = IntegratingGKSumCallback(
91+
(out, u, t, integrator) -> (out[1] = u[1]; nothing), integrated, Float64[0.0];
92+
integrand_inplace = true
93+
),
94+
dt = 0.1
95+
)
96+
@test integrated.integrand[1] == 0.5
97+
98+
@test_throws ArgumentError IntegratingGKSumCallback(
99+
(out, u, t, integrator) -> nothing, IntegrandValuesSum(zeros(1)), nothing;
100+
integrand_inplace = true
101+
)

test/integrating_sum_tests.jl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,37 @@ sol = solve(
2020
dt = 0.1
2121
)
2222
@test integrated.integrand[1] == 0.5
23+
24+
# integrand_inplace = true: in-place integrand with an out-of-place problem
25+
# (e.g. immutable state with a mutable, parameter-shaped integrand buffer)
26+
using StaticArrays
27+
prob_oop = ODEProblem((u, p, t) -> SVector(1.0), SVector(0.0), (0.0, 1.0))
28+
integrated = IntegrandValuesSum(zeros(1))
29+
sol = solve(
30+
prob_oop, Euler(),
31+
callback = IntegratingSumCallback(
32+
(out, u, t, integrator) -> (out[1] = u[1]; nothing), integrated, Float64[0.0];
33+
integrand_inplace = true
34+
),
35+
dt = 0.1
36+
)
37+
@test integrated.integrand[1] == 0.5
38+
39+
# integrand_inplace = true requires a prototype
40+
@test_throws ArgumentError IntegratingSumCallback(
41+
(out, u, t, integrator) -> nothing, IntegrandValuesSum(zeros(1)), nothing;
42+
integrand_inplace = true
43+
)
44+
45+
# integrand_inplace = false forces the allocating form even for in-place problems
46+
prob_iip = ODEProblem((du, u, p, t) -> (du[1] = 1.0; nothing), [0.0], (0.0, 1.0))
47+
integrated = IntegrandValuesSum(zeros(1))
48+
sol = solve(
49+
prob_iip, Euler(),
50+
callback = IntegratingSumCallback(
51+
(u, t, integrator) -> [u[1]], integrated, Float64[0.0];
52+
integrand_inplace = false
53+
),
54+
dt = 0.1
55+
)
56+
@test integrated.integrand[1] == 0.5

0 commit comments

Comments
 (0)