Skip to content

Commit da17c26

Browse files
OOP NonlinearSolveAlg: reuse a static W across stages
The out-of-place branch of `build_nlsolver` builds `J, W` through `build_J_W` and then throws both away: the inner `NonlinearProblem` gets no `jac` and the cache stores `nothing`, so every inner Newton iteration recomputes the Jacobian by AD with no reuse across stages or steps. Wire the `W` back in for the case that is safe today, a `StaticWOperator` problem with a `solve!`-only inner solver. The `W` lives in a `Ref` the inner `NonlinearFunction`'s analytic `jac` reads, and `initialize!` refreshes it under the same `always_new` / divergence / `new_W_dt_cutoff` policy `NLNewton` uses, assembling it through `calc_J` like the `StaticWOperator` branch of `calc_W`. A concrete-J `WOperator` needs the buffer-copy pattern from #3823 instead, so it stays on the existing no-reuse default. `njacs` and `nw` now count the assemblies, which is the only inner work the integrator can attribute on this path.
1 parent 1d7c48d commit da17c26

4 files changed

Lines changed: 87 additions & 9 deletions

File tree

lib/OrdinaryDiffEqNonlinearSolve/src/OrdinaryDiffEqNonlinearSolve.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ import OrdinaryDiffEqCore: _initialize_dae!,
6464
import OrdinaryDiffEqDifferentiation: update_W!, is_always_new, build_uf, build_J_W,
6565
WOperator, StaticWOperator, wrapprecs, default_krylov_warm_start,
6666
build_jac_config, dolinsolve,
67-
resize_jac_config!, jacobian2W!, jacobian!
67+
resize_jac_config!, jacobian2W!, jacobian!, calc_J
6868

6969
import StaticArraysCore: StaticArray
7070

lib/OrdinaryDiffEqNonlinearSolve/src/newton.jl

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ function initialize!(
4242

4343
(; ustep, tstep, k, invγdt) = cache
4444
# A `solve!`-only cache keeps no counters, and `reinit!` on it only remakes the problem
45-
# rather than re-evaluating the residual, so neither term below applies. The inner
46-
# solver on the remaining path owns its own Jacobian, so its `njacs` count stands.
45+
# rather than re-evaluating the residual, so neither term below applies. `W` reuse is
46+
# gated to those caches, which leaves the inner solver here owning its own Jacobian.
4747
if SciMLBase.has_stats(integrator) && !(cache.cache isa NonlinearSolveNoInitCache)
4848
# The `reinit!` below evaluates the residual at the new `z` and *then* zeroes the
4949
# inner cache's counters, so that evaluation is never visible in
@@ -52,6 +52,25 @@ function initialize!(
5252
integrator.stats.njacs += cache.cache.stats.njacs
5353
integrator.stats.nsolve += cache.cache.stats.nsolve
5454
end
55+
56+
if cache.W !== nothing
57+
dtgamma = method === DIRK ? γ * dt : γ * dt / α
58+
W_γdt = cache.W_γdt
59+
first_call = iszero(W_γdt)
60+
# No stored `J` on this path (`W` is rebuilt from a fresh Jacobian), so a
61+
# dt/gamma drift past the cutoff costs a Jacobian evaluation, not just a
62+
# reassembly as in the in-place `new_jac`/`new_w` split.
63+
should_update = first_call || alg.always_new ||
64+
nlsolver.status === Divergence ||
65+
abs(inv(dtgamma) / inv(W_γdt) - 1) > oftype(dtgamma, alg.new_W_dt_cutoff)
66+
if should_update
67+
_update_nlsolvealg_W_oop!(cache, integrator, dtgamma)
68+
cache.new_W = true
69+
else
70+
cache.new_W = false
71+
end
72+
end
73+
5574
if f isa DAEFunction
5675
nlp_params = (tmp, α, tstep, invγdt, p, dt, uprev, f)
5776
else
@@ -230,6 +249,17 @@ function rescale_stale_W_rhs!(nlcache, nlsolver, integrator, nlstep_data)
230249
return nothing
231250
end
232251

252+
function _update_nlsolvealg_W_oop!(nlcache, integrator, dtgamma)
253+
# Same construction as the `StaticWOperator` branch of `calc_W`: `calc_J` picks
254+
# user `jac` vs differentiation through `nlcache.uf` and records the evaluation
255+
# in `integrator.stats.njacs` — the only njacs the reused-`W` path can count.
256+
J_new = calc_J(integrator, nlcache)
257+
nlcache.W[] = J_new - integrator.f.mass_matrix * inv(dtgamma)
258+
nlcache.W_γdt = dtgamma
259+
integrator.stats.nw += 1
260+
return nothing
261+
end
262+
233263
## compute_step!
234264

235265
@muladd function compute_step!(nlsolver::NLSolver{<:NonlinearSolveAlg, false}, integrator)
@@ -243,7 +273,8 @@ end
243273
# No `step!` here, so each outer iteration runs a whole inner solve. Those solvers
244274
# also report no counters at all (SimpleNonlinearSolve builds every solution
245275
# without `stats`), so the residual and linear-solve work they do is not
246-
# attributable to the integrator.
276+
# attributable to the integrator; `_update_nlsolvealg_W_oop!` counts the Jacobian
277+
# and `W` assemblies, which are the part performed on this side.
247278
sol = SciMLBase.solve!(nlcache)
248279
# A `solve!` runs to its own termination every outer iteration, so an unsuccessful
249280
# return is a genuine failure and must reach the step controller the way `NLNewton`
@@ -811,7 +842,8 @@ function Base.resize!(nlcache::NonlinearSolveCache, ::AbstractNLSolver, integrat
811842
nlcache.weight === nothing || resize!(nlcache.weight, i)
812843
nlcache.dz === nothing || resize!(nlcache.dz, i)
813844
nlcache.jac_config === nothing || resize_jac_config!(nlcache, integrator)
814-
nlcache.W === nothing || resize_J_W!(nlcache, integrator, i)
845+
# The reused out-of-place `W` is a `Ref` over a static matrix, which has no `resize!`.
846+
nlcache.W === nothing || nlcache.W isa Ref || resize_J_W!(nlcache, integrator, i)
815847
# `nlcache.linsolve` is re-pointed by `initialize!` when it rebuilds the inner cache on the
816848
# next length mismatch, before any estimator solve — nothing to rebuild here.
817849
nlcache.W_γdt = zero(nlcache.W_γdt)

lib/OrdinaryDiffEqNonlinearSolve/src/utils.jl

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -663,18 +663,35 @@ function build_nlsolver(
663663
abstol = _inner_lintol(uTolType), reltol = _inner_lintol(uTolType),
664664
)
665665
)
666+
W_ref = nothing
666667
if cache isa NonlinearSolveNoInitCache
667668
# This cache only records the arguments a `solve!` will forward, so
668669
# building one is the cheapest way to ask whether the inner solver
669670
# iterates, and the answer stays right as SimpleNonlinearSolve grows.
670-
# Each outer iteration is then a whole inner solve that has to terminate
671-
# on its own, so it keeps the solver's own tolerances: the zeroed ones
671+
if !isdae && f.nlstep_data === nothing && W isa StaticWOperator
672+
# StaticWOperator stores inv(W) in its W field for n <= 7, so the
673+
# Ref holds the plain W matrix; initialize! rewrites it before the
674+
# first solve (W_γdt starts at zero).
675+
W_ref = Ref(W.W)
676+
nlf_jac = let W_ref = W_ref
677+
(z, p) -> W_ref[]
678+
end
679+
prob = NonlinearProblem(
680+
NonlinearFunction{false, SciMLBase.FullSpecialize}(
681+
nlf; jac = nlf_jac, jac_prototype = W.W
682+
),
683+
copy(ztmp), nlp_params
684+
)
685+
end
686+
# Each outer iteration is a whole inner solve that has to terminate on
687+
# its own, so it keeps the solver's own tolerances: the zeroed ones
672688
# above would send every one of them to `maxiters`.
673689
cache = init(prob, inner_alg; verbose = verbose.nonlinear_verbosity)
674690
end
675691
nlcache = NonlinearSolveCache(
676692
nothing, tstep, nothing, nothing, invγdt, prob, cache,
677-
nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing,
693+
nothing, W_ref, W_ref === nothing ? nothing : uf,
694+
nothing, nothing, nothing, nothing, nothing,
678695
zero(tstep), true
679696
)
680697
else

lib/OrdinaryDiffEqNonlinearSolve/test/linear_nonlinear_tests.jl

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
using OrdinaryDiffEqSDIRK, OrdinaryDiffEqBDF, OrdinaryDiffEqRosenbrock, Test, Random,
2-
LinearAlgebra, LinearSolve, ADTypes
2+
LinearAlgebra, LinearSolve, ADTypes, SciMLBase
33
using OrdinaryDiffEqNonlinearSolve: NonlinearSolveAlg
44
using NonlinearSolve: NewtonRaphson
5+
using SimpleNonlinearSolve: SimpleNewtonRaphson, SimpleTrustRegion
56
Random.seed!(123)
67

78
A = 0.01 * rand(3, 3)
@@ -208,3 +209,31 @@ let integ = init(
208209
step!(integ)
209210
@test !iszero(integ.cache.nlsolver.cache.weight)
210211
end
212+
213+
using StaticArrays
214+
vdp_static(u, p, t) = SVector(u[2], p[1] * ((1 - u[1]^2) * u[2] - u[1]))
215+
let
216+
prob = ODEProblem(
217+
vdp_static, SVector(2.0, 0.0), (0.0, 1.0), SVector(1.0e3)
218+
)
219+
solref = solve(prob, TRBDF2(); reltol = 1.0e-8, abstol = 1.0e-10)
220+
for inner in (
221+
SimpleNewtonRaphson(; autodiff = AutoForwardDiff()),
222+
SimpleTrustRegion(; autodiff = AutoForwardDiff()),
223+
)
224+
integ = init(
225+
prob,
226+
TRBDF2(nlsolve = NonlinearSolveAlg(inner), concrete_jac = true);
227+
reltol = 1.0e-8, abstol = 1.0e-10
228+
)
229+
@test integ.cache.nlsolver.cache.W isa Base.RefValue
230+
sol = solve!(integ)
231+
@test SciMLBase.successful_retcode(sol.retcode)
232+
@test maximum(abs.(sol.u[end] .- solref.u[end])) < 1.0e-3
233+
# These inner solvers report no counters of their own, so every Jacobian the
234+
# integrator sees comes from the reused `W`: one evaluation per assembly, and
235+
# fewer assemblies than nonlinear iterations because the `W` is held across them.
236+
@test sol.stats.njacs == sol.stats.nw
237+
@test 0 < sol.stats.nw < sol.stats.nnonliniter
238+
end
239+
end

0 commit comments

Comments
 (0)