Skip to content

Commit 5eb00ed

Browse files
Add reusable homotopy solver caches
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
1 parent 6bfca70 commit 5eb00ed

10 files changed

Lines changed: 261 additions & 84 deletions

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "NonlinearSolve"
22
uuid = "8913a72c-1f9b-4ce2-8d82-65094dcecaec"
3-
version = "4.22.0"
3+
version = "4.23.0"
44
authors = ["SciML"]
55

66
[deps]
@@ -98,7 +98,7 @@ NLSolvers = "0.5"
9898
NLsolve = "4.5"
9999
NaNMath = "1"
100100
NonlinearProblemLibrary = "0.1.2"
101-
NonlinearSolveBase = "2.36"
101+
NonlinearSolveBase = "2.37"
102102
NonlinearSolveFirstOrder = "2"
103103
NonlinearSolveQuasiNewton = "1.12"
104104
NonlinearSolveSpectralMethods = "1.6"

lib/NonlinearSolveBase/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "NonlinearSolveBase"
22
uuid = "be0214bd-f91f-a760-ac4e-3421ce2b2da0"
3-
version = "2.36.1"
3+
version = "2.37.0"
44
authors = ["Avik Pal <avikpal@mit.edu> and contributors"]
55

66
[deps]

lib/NonlinearSolveBase/src/homotopy_sweep.jl

Lines changed: 173 additions & 74 deletions
Large diffs are not rendered by default.

lib/NonlinearSolveBase/src/polyalg.jl

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ residual cost from one evaluation per subalgorithm to one per subalgorithm actua
6060
attempted (usually just the retained one). A consequence is that the shared `stats`
6161
of a solution produced after mid-solve escalation only reflect the subalgorithms run
6262
since the last deferred reinitialization, i.e. effectively the winning subalgorithm's
63-
own effort.
63+
own effort. Updating solver options in the same `reinit!` eagerly updates every
64+
subcache so that later escalation observes the new values.
6465
"""
6566
@concrete struct NonlinearSolvePolyAlgorithm <: AbstractNonlinearSolveAlgorithm
6667
static_length <: Val
@@ -175,7 +176,7 @@ const RETAIN_REPROBE_INTERVAL = 8
175176

176177
function InternalAPI.reinit!(
177178
cache::NonlinearSolvePolyAlgorithmCache, args...; p = cache.prob.p, u0 = cache.u0,
178-
retain_best::Bool = false
179+
retain_best::Bool = false, kwargs...
179180
)
180181
cache.retain_best = retain_best
181182
cache.retain_count = retain_best ? cache.retain_count + 1 : 0
@@ -186,19 +187,21 @@ function InternalAPI.reinit!(
186187
cache.start_current = cache.current
187188
cache.wrapped = false
188189
if retain_best
190+
cache.deferred_u0 = u0
191+
cache.deferred_p = p
192+
end
193+
if retain_best && isempty(kwargs)
189194
# Lazy subcache reinitialization: every subcache `reinit!` evaluates the
190195
# residual at the new `u0` (`Utils.reinit_common!`), so eagerly
191196
# reinitializing all N subcaches costs N residual calls per warm-started
192197
# solve even though a retained solve usually runs only the starting
193198
# subalgorithm. Reinitialize just that one here and defer the rest to the
194199
# moment escalation actually reaches them (`deferred_subcache_reinit!`).
195-
cache.deferred_u0 = u0
196-
cache.deferred_p = p
197200
subcache = cache.caches[cache.current]
198-
InternalAPI.reinit!(subcache, args...; u = get_u(subcache), p, u0)
201+
InternalAPI.reinit!(subcache, args...; u = get_u(subcache), p, u0, kwargs...)
199202
else
200203
foreach(cache.caches) do cache
201-
InternalAPI.reinit!(cache, args...; u = get_u(cache), p, u0)
204+
InternalAPI.reinit!(cache, args...; u = get_u(cache), p, u0, kwargs...)
202205
end
203206
end
204207
InternalAPI.reinit!(cache.stats)
@@ -225,6 +228,10 @@ reinit_retaining!(cache, u0) = SciMLBase.reinit!(cache, u0)
225228
function reinit_retaining!(cache::NonlinearSolvePolyAlgorithmCache, u0)
226229
return SciMLBase.reinit!(cache, u0; retain_best = true)
227230
end
231+
reinit_retaining!(cache, u0, p) = SciMLBase.reinit!(cache, u0; p)
232+
function reinit_retaining!(cache::NonlinearSolvePolyAlgorithmCache, u0, p)
233+
return SciMLBase.reinit!(cache, u0; p, retain_best = true)
234+
end
228235

229236
function SciMLBase.__init(
230237
prob::AbstractNonlinearProblem, alg::NonlinearSolvePolyAlgorithm, args...;

test/Core/homotopy_alloc_tests__item1.jl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using NonlinearSolve
22

3+
using CommonSolve
34
using SciMLBase
45

56
import NonlinearSolveBase as NLB
@@ -53,6 +54,26 @@ cache_result, same_cache, cache_solve_allocs = cache_only_solve_allocations(inne
5354
@test SciMLBase.successful_retcode(cache_result.retcode)
5455
VERSION >= v"1.11" && @test cache_solve_allocs == 0
5556

57+
function cached_sweep_retcode!(cache, u0, p, abstol)
58+
SciMLBase.reinit!(cache, u0; p, abstol)
59+
return CommonSolve.solve!(cache).retcode
60+
end
61+
62+
function cached_sweep_allocations(prob)
63+
alg = HomotopySweep(; inner = NewtonRaphson(), adaptive = false, nsteps = 10)
64+
cache = SciMLBase.init(prob, alg; abstol = 0.0)
65+
abstol = 1.0e-10
66+
cached_sweep_retcode!(cache, prob.u0, prob.p, abstol)
67+
cached_sweep_retcode!(cache, prob.u0, prob.p, abstol)
68+
GC.gc()
69+
allocs = @allocated cached_sweep_retcode!(cache, prob.u0, prob.p, abstol)
70+
return cache, allocs
71+
end
72+
73+
sweep_cache, sweep_cache_allocs = cached_sweep_allocations(prob)
74+
@test SciMLBase.successful_retcode(CommonSolve.solve!(sweep_cache))
75+
VERSION >= v"1.11" && @test sweep_cache_allocs == 0
76+
5677
# --- end-to-end per-step allocation with a clean `NewtonRaphson` inner. The slope between two
5778
# fixed-step runs cancels compile/one-time-init cost, leaving pure per-step allocation. Gated
5879
# on Julia 1.11+: the LinearSolve factorization-workspace reuse that removes the last

test/Core/homotopy_effort_tests__item2.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ sol = solve(prob, HomotopySweep(; initial_step_factor = 1.0e-4, expand_factor =
1414
@test !SciMLBase.successful_retcode(sol)
1515
@test isfinite(sol.u[1])
1616
@test 0 < sol.u[1] < 1
17+
@test sol.resid !== nothing
1718

1819
Hs(u, p, λ) = SA[u[1] - λ]
1920
probs = HomotopyProblem(Hs, SA[0.0]; λspan = (0.0, 1.0))

test/Core/homotopy_sweep_tests__item10.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ prob = HomotopyProblem(H, [0.0]; λspan = (1.0e9, 2.0e9))
1212
sol = solve(prob, HomotopySweep(; inner = NewtonRaphson()); maxiters = 5)
1313
@test sol.retcode == SciMLBase.ReturnCode.Stalled
1414
@test !SciMLBase.successful_retcode(sol)
15-
@test sol.resid === nothing
15+
@test sol.resid !== nothing
16+
@test all(isfinite, sol.resid)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using NonlinearSolve
2+
using CommonSolve
3+
using SciMLBase
4+
using Test
5+
6+
function Hcache!(du, u, p, λ)
7+
du[1] = (1 - λ) * (u[1] - p[1]) + λ * (u[1]^2 - p[1])
8+
return nothing
9+
end
10+
11+
prob = HomotopyProblem(Hcache!, [4.0], [4.0])
12+
algs = (
13+
HomotopySweep(; inner = NewtonRaphson(), adaptive = false, nsteps = 10),
14+
KantorovichHomotopy(; inner = NewtonRaphson(), nsteps = 10, strict = false),
15+
HomotopySweep(; adaptive = false, nsteps = 10),
16+
)
17+
18+
for alg in algs
19+
cache = init(prob, alg; abstol = 0.0)
20+
@test reinit!(cache, prob.u0; p = prob.p, abstol = 1.0e-10) === cache
21+
sol = solve!(cache)
22+
@test SciMLBase.successful_retcode(sol)
23+
@test sol.u[1] 2.0 atol = 1.0e-10
24+
25+
u0 = [9.0]
26+
p = [9.0]
27+
@test reinit!(cache, u0; p, abstol = 1.0e-10) === cache
28+
sol = solve!(cache)
29+
@test SciMLBase.successful_retcode(sol)
30+
@test sol.u[1] 3.0 atol = 1.0e-10
31+
@test sol.prob.u0 === u0
32+
@test sol.prob.p === p
33+
end

test/Core/polyalg_retention_tests__item1.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# subcache reinitialization, upward escalation, wrap-around floored at the
44
# algorithm's `start_index`, and exact status-quo behavior when retention is off.
55
using NonlinearSolve, SciMLBase
6+
using NonlinearSolveBase: get_abstol
67

78
using Test
89

@@ -123,3 +124,16 @@ end
123124
@test SciMLBase.successful_retcode(sol2)
124125
@test sol2.u[1] ROOT atol = 1.0e-6
125126
end
127+
128+
@testset "solver option updates reach every retained subcache" begin
129+
alg = NonlinearSolvePolyAlgorithm((NewtonRaphson(), Broyden()))
130+
prob = NonlinearProblem(fcubic, [0.0])
131+
cache = init(prob, alg; abstol = 0.0)
132+
cache.best = 2
133+
134+
NF[] = 0
135+
reinit!(cache, [1.2]; retain_best = true, abstol = 1.0e-8)
136+
@test cache.current == 2
137+
@test NF[] == 2
138+
@test all(subcache -> get_abstol(subcache) == 1.0e-8, cache.caches)
139+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ else
8787
@time @safetestset "HomotopySweep secant predictor reduces corrector work" include("Core/homotopy_sweep_tests__item20.jl")
8888
@time @safetestset "HomotopySweep regrows the step after bisecting a hard region" include("Core/homotopy_sweep_tests__item21.jl")
8989
@time @safetestset "Continuation drivers return concretely-typed solutions (no Any-typed original)" include("Core/homotopy_sweep_tests__item22.jl")
90+
@time @safetestset "HomotopySweep init/reinit!/solve! cache reuse" include("Core/homotopy_sweep_tests__item23.jl")
9091
@time @safetestset "Kantorovich homotopy controller" include("Core/kantorovich_homotopy_tests__item1.jl")
9192
@time @safetestset "ArcLengthContinuation construction + defaults" include("Core/arclength_tests__item1.jl")
9293
@time @safetestset "ArcLengthContinuation happy path (fold-free, matches sweep)" include("Core/arclength_tests__item2.jl")

0 commit comments

Comments
 (0)