OOP NonlinearSolveAlg: reuse W with SimpleNonlinearSolve solvers - #3798
OOP NonlinearSolveAlg: reuse W with SimpleNonlinearSolve solvers#3798singhharsh1708 wants to merge 2 commits into
Conversation
| ) | ||
| prob = if use_w_reuse | ||
| nlf_jac = let W = W | ||
| (z, p) -> convert(AbstractMatrix, W) |
There was a problem hiding this comment.
We shouldn't unconditionally convert.
|
good catch — split the closure so StaticWOperator returns its .W field directly, only WOperator path converts now |
350a7e8 to
0f20284
Compare
| end | ||
| else | ||
| let Ww = W | ||
| (z, p) -> convert(AbstractMatrix, Ww) |
There was a problem hiding this comment.
What about non-static opertators
|
for non-static |
d85b000 to
98bbc94
Compare
| end | ||
| else | ||
| let Ww = W | ||
| (z, p) -> Ww._concrete_form |
|
fixed — now hands the operator through directly |
833ee15 to
e414df1
Compare
|
Hi @ChrisRackauckas — quick nudge on this stack when you have a moment. All three PRs (#3795, #3796, #3798) have addressed the earlier review comments and are rebased on the latest master:
Once the next SciMLOperators release tags, I'll open a follow-up here to route OOP + SciMLOperator mass matrix through No rush — just wanted to make sure these weren't waiting on me. Thanks! |
|
Ran this branch against an OOP nonlinear problem and hit two defects in the W-reuse path — posting reproductions so they can be addressed: 1. The using OrdinaryDiffEqSDIRK, OrdinaryDiffEqNonlinearSolve, NonlinearSolve, ADTypes
using OrdinaryDiffEqNonlinearSolve: NonlinearSolveAlg
vdp(u, p, t) = [u[2], p[1] * ((1 - u[1]^2) * u[2] - u[1])]
prob = ODEProblem(vdp, [2.0, 0.0], (0.0, 1.0), [1.0e3])
integ = init(prob, TRBDF2(nlsolve = NonlinearSolveAlg(NewtonRaphson(; autodiff = AutoForwardDiff())), concrete_jac = true); reltol = 1e-8, abstol = 1e-10)
step!(integ)
# ERROR: MethodError: Cannot `convert` an object of type WOperator{false, ...} to an object of type Matrix{Float64}The 2. Even with the convert fixed, the Jacobian values are never refreshed — J is frozen at its build-time state for the entire integration. The update in The IIP counterpart (#3753) avoids this because Also worth folding in while touching this: pass |
e414df1 to
936de56
Compare
|
Thanks for the review @ChrisRackauckas-Claude — both defects reproduced and fixed. New commit Defect 1 (crash on convert): the OOP closure now returns the concrete matrix directly — Defect 2 (frozen J): added Ref indirection: the closure captures Tests: added two regressions at the end of
Locally: both new tests pass; existing |
bd5af11 to
d89e8d7
Compare
| nlf_jac = let W_ref = W_ref | ||
| (z, p) -> convert(AbstractMatrix, W_ref[]) | ||
| end | ||
| jac_prototype = copy(convert(AbstractMatrix, W)) |
There was a problem hiding this comment.
no it's still concretizing.
c752622 to
9cfc718
Compare
| W_ref = use_w_reuse ? Ref{typeof(W)}(W) : nothing | ||
| prob = if use_w_reuse | ||
| if W isa StaticWOperator | ||
| nlf_jac = let W_ref = W_ref | ||
| (z, p) -> W_ref[].W | ||
| end | ||
| NonlinearProblem( | ||
| NonlinearFunction{false, SciMLBase.FullSpecialize}( | ||
| nlf; jac = nlf_jac, jac_prototype = W.W | ||
| ), | ||
| copy(ztmp), nlp_params | ||
| ) | ||
| else | ||
| nlf_jac = let W_ref = W_ref | ||
| (z, p) -> convert(AbstractMatrix, W_ref[]) |
There was a problem hiding this comment.
OOP is purposefully not mutable. We want to make things fold away when static. This will violate that. I think it's simply not possible to do similar W-reuse in the OOP path without violating the principle of making this path non-mutating.
There was a problem hiding this comment.
For StaticWOperator the payload is an immutable SMatrix — reassigning nlcache.W[] re-points the Ref cell, the SMatrix itself is never mutated, so fold-away on the value side still holds. The mutation you're objecting to only shows up on the WOperator branch (copyto!(W.J, …)).
And picking NonlinearSolveAlg already forces a Matrix{Float64} jac cache inside construct_jacobian_cache — the OOP path isn't fold-away in that mode regardless of what this PR does. If Static-only reuse is still off the table given that, I'll close.
There was a problem hiding this comment.
I guess it's fine if there's a simplediffeq path that fully non-allocates with simplenonlinearsolve. But the conversion stuff is never okay.
That should get fixed upstream. Making everything dense is not a valid answer. |
9cfc718 to
f4cf1d3
Compare
|
Dropped WOperator branch. W-reuse only fires on StaticWOperator + AbstractSimpleNonlinearSolveAlgorithm — SMatrix rebuilt, Ref re-pointed, no convert. |
|
Why no WOperator? |
141a00b to
3c296e7
Compare
|
@ChrisRackauckas Rebased on master and fixed two bugs the Static+Simple gate exposed. Both surfaced because that path introduces execution paths that weren't previously exercised. 1. Inner cache doesn't support
2.
The Ref now holds the plain Test change
Validation All 11 Remaining CI reds are unrelated to this branch — |
2416303 to
00e6067
Compare
d90e92d to
4bbd780
Compare
4bbd780 to
2e5c5d8
Compare
97dc593 to
da17c26
Compare
SimpleNonlinearSolve defines no `__init`, so all of its algorithms land in `NonlinearSolveBase.NonlinearSolveNoInitCache`: a cache that records the arguments a later `solve!` will forward and nothing else. It has no `step!`, no `stats` field, and none of the iteration state `not_terminated` reads, so the out-of-place path threw on the first `step!`. Drive those caches with `solve!`, skip the counter copy in `initialize!`, and keep the solver's own tolerances instead of the zeroed ones the `step!`-driven path uses, which would send every inner solve to `maxiters`. `_rejected_trial_step` skips them as well: a whole inner solve ran to its own termination, so a zero displacement there is convergence, and `compute_step!` already turned an unsuccessful return into `Inf`. Each branch keys off the cache type rather than a list of algorithm types, so a newly exported solver cannot quietly pick up the wrong path.
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 SciML#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.
da17c26 to
94c9207
Compare
The out-of-place
NonlinearSolveAlgbranch ofbuild_nlsolverbuildsJ, Wthroughbuild_J_Wand then throws both away: the innerNonlinearProblemgets nojac, andNonlinearSolveCachestoresnothingfor J and W. Every inner Newton iteration recomputes the Jacobian by AD, with no reuse across stages or steps.This wires the W back in for the one case that is safe today: a
StaticWOperatorproblem with a SimpleNonlinearSolve inner solver. The W lives in aRefthat the innerNonlinearFunction's analyticjacreads, andinitialize!refreshes it under the samealways_new/ divergence /new_W_dt_cutoffpolicyNLNewtonuses, assembling it throughcalc_Jthe way theStaticWOperatorbranch ofcalc_Wdoes.The gate is deliberately narrow, and narrower than the first revision of this PR claimed. A concrete-J
WOperatorcannot be handed to the inner solver as a plain matrix the same way; it needs the buffer-copy pattern from #3823, where the assembled W is copied into a Jacobian buffer the inner solver owns. Wrapping a matrix-freeWOperatorso the inner solver applies it bymul!is a possible follow-up as well. Neither is implemented here, so every other W type keeps the existing no-reuse default and this stays additive.Getting there first needed the out-of-place path to handle SimpleNonlinearSolve at all, which it did not. SimpleNonlinearSolve defines no
__init, so all of its algorithms land inNonlinearSolveBase.NonlinearSolveNoInitCache, a cache that records the arguments a latersolve!will forward and nothing else. It has nostep!, nostatsfield, and none of the iteration statenot_terminatedreads (force_stop,nsteps,maxiters). Socompute_step!drives it withsolve!,_rejected_trial_stepskips it instead of throwing the moment the displacement hits exactly zero, and the inner cache keeps the solver's own tolerances rather than the zeroed ones thestep!-driven path uses, which would send every inner solve tomaxiters. All three branch on the cache type, not on a list of algorithm types, so a newly exported SimpleNonlinearSolve algorithm cannot quietly pick up the wrong path.SimpleHomotopySweep, added in 2.13, is exactly what a hand-written list would have missed.Stats on this path are incomplete, and the code says so instead of pretending otherwise. SimpleNonlinearSolve builds every solution without
stats, sosol.stats === nothingand the residual and linear-solve work its solvers do is not attributable to the integrator. What is counted is what this side performs:njacsandnwfrom the W assemblies. The out-of-placeinitialize!skips the counter copy outright for these caches rather than reading fields they do not have, and thenjacsguard it inherited goes with it, since that guard's comment described aWReuseJacthat only exists on the in-place path and W reuse here only ever pairs with asolve!-only cache.Testing, against master at 0be95df:
linear_nonlinear_tests.jl54 pass, up from 44 (10 added)nsa_matrixfree_tests.jl14 pass, unchangednewton_tests.jl6 pass, unchangednsa_jacobian_reuse_tests.jl17 pass, unchangedThe added case solves a stiff van der Pol in
SVectorform withTRBDF2under bothSimpleNewtonRaphsonandSimpleTrustRegion, checks the result againstTRBDF2with the default nonlinear solver, and assertsnjacs == nwwith the assembly count strictly below the nonlinear iteration count, which is the reuse actually happening. On that problemSimpleNewtonRaphsongoes from 22532 nonlinear iterations without reuse to 14026 with it, over 1343 W assemblies, and the end state agrees with theNLNewtonrun to 2e-4 either way.nested_ad_nlsolvealg_tests.jlerrors the same way on master (LinearSolve/SciMLOperators version skew) and is untouched here.AI Disclosure
Claude assisted with this work.