Trace the nonlinear solver loops under Reactant - #1197
Trace the nonlinear solver loops under Reactant#1197ChrisRackauckas-Claude wants to merge 1 commit into
Conversation
|
Rework plan for this draft:
QA currently fails during environment resolution on unchanged upstream master because the QA environment requires NLsolve 4.x while NonlinearSolve requires NLsolve 5.x; that baseline regression is being bisected separately. |
f55f873 to
a0b0c02
Compare
3541207 to
29bc671
Compare
6088b1c to
be5c212
Compare
be5c212 to
42d2b70
Compare
|
CI triage: the only red lanes so far are the three 🤖 Posted by an AI agent (Claude Code, model claude-fable-5) on behalf of Chris Rackauckas. Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE |
42d2b70 to
90ccb42
Compare
|
Rebuilt on current 🤖 Posted by an AI agent (Claude Code, model claude-fable-5) on behalf of Chris Rackauckas. Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE |
90ccb42 to
d06e095
Compare
d06e095 to
1b188d9
Compare
|
Rebased onto 🤖 Posted by an AI agent (Claude Code, model claude-fable-5) on behalf of Chris Rackauckas. Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE |
|
CI triage: the red 🤖 Posted by an AI agent (Claude Code, model claude-fable-5) on behalf of Chris Rackauckas. Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE |
53b9e7d to
cbfafa3
Compare
Run every algorithm's ordinary `solve` dispatch under `Reactant.@compile`. The shared solver loop is a `ReactantCore.@trace while` over `CommonSolve.step!`, return codes are traced enums (EnzymeAD/Reactant.jl#3232, SciMLBase#1563) so the solution is built by `SciMLBase.NonlinearSolution` on every path, and the loop-carried cache state is refreshed by one reflective `dealias_traced!` instead of per-cache field lists. Trust-region, Levenberg-Marquardt, geodesic and dogleg updates are written once with `ifelse`; polyalgorithms are traced as a chain of their members. Forward-mode `AutoEnzyme` is preferred during compilation. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude <noreply@anthropic.com> Agent-Harness: Claude Code 2.1.251 Agent-Model: claude-fable-5 Agent-Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE
cbfafa3 to
bfffd5d
Compare
Stack: SciML/SciMLBase.jl#1563 (retcode type parameter) → this PR → the SimpleNonlinearSolve PR (single branchless loops). Requires EnzymeAD/Reactant.jl#3232 (traced enums) — the Reactant test group pins the branch until it is released.
What changed and why
This replaces the previous content of this PR. Every nonlinear algorithm's
solvedispatch now runs underReactant.@compile/@jitby tracing the ordinary code path: there is one implementation per solver, and the Reactant-specific machinery is three helpers inNonlinearSolveBase/src/reactant.jlplus a few one-lineReactantCore.within_compile()gates where the host semantics genuinely cannot be traced.ReturnCode.T(Trace Base.Enum values as an enum wrapper around the traced base integer EnzymeAD/Reactant.jl#3232) andNonlinearSolution.retcodeparametric (SciMLBase#1563),cache.retcode = tc_cache.retcodeinside a@trace ifandifelse(converged, ReturnCode.Success, cache.retcode)work as written. The shadow_ReactantNonlinearSolution, its fabricatedNLStats,InternalAPI.build_reactant_solution,InternalAPI.prepare_reactant_loop_state!and theconverged/finitereconstruction are gone;build_nonlinear_solution(the positionalNonlinearSolutionconstructor) is used on every path. Under compilationstatsisnothing(NLStatsholdsInts and would only count trace-time evaluations) andprobisnothing(a problem's keyword arguments are aBase.Pairs, which Reactant's result codegen cannot rebuild — it treats it as anAbstractDictand callsD()).maybe_traced(x)promotes a scalar only during compilation; caches are@concrete, sonsteps/force_stop/retcodeare promoted where the cache is constructed (FirstOrder, QuasiNewton, SpectralMethods) and in the termination cache.dealias_traced!(cache)is one reflective walk that gives every traced leaf reachable from the cache a fresh copy before the loop and after each step, replacing eleven hand-written per-cache field lists (Newton, steepest, damped Newton, geodesic, dogleg, first-order, trust region, LM damping/trust region, pseudo-transient). Reactant records one path per traced object among loop-carried values and requires the same set after the body, so aliases such asu_cache === u(a@bb copyon a traced array rebinds), the sharedp, and the problem stored in the trace are exactly what breaks; the trace now storesnothingfor the problem under compilation since it only inspects its type._run_cache_to_completion!is a singleReactantCore.@trace whileoverCommonSolve.step!; the post-loopMaxIters/Successdecision is anifelse, so it is one path. Termination andcheck_and_update!setretcode/force_stopwithifelse; no@trace ifcaptures the mutable cache. Two Reactant facts shaped this and are recorded as comments: inside a@tracebodyReactantCore.within_compile()isfalse(the macro captures the module as a loop variable), so loop bodies only call self-gating helpers; and a@trace ifbinds its captured variables through a parameter namedargs, so nothing in scope may use that name.__Simpleradius update, Levenberg–Marquardt acceptance and damping, geodesic acceleration and the descent selection in dogleg are written withifelse/selectonce, valid on the host and traced. Where the host path has a real early-out that saves work (dogleg's Newton/Cauchy returns, theisnanshortcut in the trust-region ratio) it is kept behind!within_compile()with the branchless form as the fallthrough; dogleg's Cauchy step is factored intodogleg_cauchy_step!so it is not duplicated.make_new_jacobianstays a hostBool(within_compile() || accepted): under compilation the Jacobian is recomputed every step.__generated_polysolve. The previous branch silently ran only the first member.make_new_jacobianfrom residual-norm ratios, which are traced under Reactant, so__initswitches the policy off (without_jacobian_reuse) during compilation and every step recomputes the Jacobian there; on the host the policy is untouched, including the stale-Jacobian retries.AutoEnzymeduring compilation (unchanged from the previous branch);maybe_wrap_fand theReactantLinearSolveCacheare also kept.SciMLJacobianOperators.JacobianOperator{iip, T}acceptsT <: Number(wasReal) so that traced element types can build the trust-region operators; its tests pass unchanged.maybe_traced,dealias_traced!,selectandbuild_nonlinear_solutionarepublicand documented (NonlinearSolveBase 2.50.0 — 2.49.0 was released from master meanwhile; the sublibraries' compat and patch versions are bumped accordingly).ReactantCore.is_tracedis allow-listed in the QA check with the reason: it is the predicate behind@trace, not declared public upstream, and cannot be reproduced locally because Reactant defines its methods.AGENTS.mdrecords the Reactant conventions and the repository pitfalls hit while doing this (sublibrary[sources]paths,Pkg.developinto a sublibrary project,@tracebody rules), per the agent-memory rule.build_solution_less_specializeandSCCNonlinearSolvespelled out all ofNonlinearSolution's type parameters; they now pick the layout at load time (fieldtype(NonlinearSolution, :retcode)), so this works with both released SciMLBase and #1563.Deliberately unchanged semantics under compilation, still documented in
nonlinear_solve_gpus.md: the default termination mode isAbsNormTerminationModerather than theSafeBestvariant (its stall bookkeeping is host control flow), initialization failure is a host event and cannot be returned from a compiled solve, andLineSearch-globalized andDFSanepaths are not traced.Against
master(after #1072): 35 files, +876/−295 (the previous branch content was +1254/−212 againstmaster; this replaces it with a smaller and more uniform change).Verification
Local, Julia 1.12.4 unless noted, with EnzymeAD/Reactant.jl#3232 (
~/sandbox/Reactant.jl, branchtraced-enums) and SciML/SciMLBase.jl#1563 developed into the environments and DifferentiationInterface pinned to571fc178as intest/runtests.jl.Reactant group on this branch (
test/Reactant/reactant_tests.jlas in this PR, i.e. without the Simple solvers):The same environment with the stacked SimpleNonlinearSolve PR applied (full matrix):
Host groups, run in each sublibrary's own project:
Formatted with Runic 1.10.0;
typosclean on the changed files. The final commit differs from the tree the runs above used only bypublicdeclarations, version/compat bumps, docs entries and test comments.Not traced, and therefore not in the Reactant matrix, with the reason next to the matrix in the test file: the least-squares polyalgorithms and default least-squares solve (a line-search member calls
norm(x, Inf), whose Reactant overload scalar-indexes — a Reactant bug, one line insrc/stdlibs/LinearAlgebra.jl), andRobustMultiNewton(its Yuan/Bastin members need a reverse-mode vector-Jacobian product, which DifferentiationInterface's Enzyme backend does not route through Reactant). Both compiled under the previous branch only because the polyalgorithm collapsed to its first member.Not verified
GROUP=Everything; CUDA/GPU groups; theprejob.NonlinearSolution's explicit type parameters other than the two sites in this repo.Reviewer judgment points
sol.stats === nothingunder compilation rather than trace-time counts.@inferredsituation in SimpleNonlinearSolve is handled in the stacked PR.🤖 Generated with Claude Code (model: claude-fable-5) on behalf of Chris Rackauckas. Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE