Skip to content

SimpleNonlinearSolve: single branchless loops that trace under Reactant - #1213

Draft
ChrisRackauckas-Claude wants to merge 2 commits into
SciML:masterfrom
ChrisRackauckas-Claude:reactant-simple
Draft

SimpleNonlinearSolve: single branchless loops that trace under Reactant#1213
ChrisRackauckas-Claude wants to merge 2 commits into
SciML:masterfrom
ChrisRackauckas-Claude:reactant-simple

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Aug 29, 2026

Copy link
Copy Markdown
Member

Draft. Ignore this PR until reviewed by @ChrisRackauckas. Stacked on #1197 (NonlinearSolveBase/first-order Reactant support); review the last commit only.

What changed and why

Each SimpleNonlinearSolve iteration (SimpleNewtonRaphson, SimpleTrustRegion, SimpleBroyden, SimpleKlement, SimpleHalley) is now written once as a branchless loop that runs unchanged on the host and under Reactant.@compile:

  • for _ in 1:maxiters with solved && return becomes ReactantCore.@trace track_numbers = false while (!solved) & (iterations < maxiters); the "initial guess is already a root" early return is subsumed (the loop does not execute and simple_solution reports Success). @trace is a compile-time no-op outside Reactant.
  • Decisions that used to return or branch (SimpleHalley's factorization failure, SimpleTrustRegion's shrink-threshold exit and step acceptance, SimpleKlement's Jacobian reset) are loop conditions or ifelse selections. dogleg_method!! keeps its two host early-outs behind !within_compile() with the selection as the fallthrough.
  • Utils.init_loop_state promotes the return code and counter with NonlinearSolveBase.maybe_traced and makes the loop-carried arrays distinct; Utils.fresh re-applies that after check_termination (whose best-iterate result aliases the termination cache) and after @bb copyto! (which rebinds rather than copies for traced arrays). Both are no-ops on the host.
  • Utils.simple_solution builds the NonlinearSolution from solved with ifelse/select; the previous _build_simple_solution, _copy_for_reactant, reactant.jl and the nonlinear_solution_new_alg shadow-solution hack are removed, as is the Base.@noinline on the safe-best check_termination (its tests pass on 1.10 without it).

SimpleTrustRegion's host loop now forms the candidate Jacobian on every iteration and selects it (previously only accepted steps recomputed it). The result-type assertions in simple_solution exist because SimpleTrustRegion with autodiff = nothing has a run-time-typed Jacobian (prepare_jacobian returns a non-concrete DIExtras), which is pre-existing but previously never reached the return type; with them @inferred solve(...) holds on 1.10 and 1.12.

Diff on top of the core PR: 9 files, +237/−102.

Verification

Same environment as the core PR (Reactant#3232, SciMLBase#1563, DI 571fc178), Julia 1.12.4 unless noted.

Reactant group with the full matrix (Simple solvers included):

Test Summary:        | Pass  Total     Time
Reactant Integration |  147    147  8m42.9s

SimpleNonlinearSolve's own groups with this branch's NonlinearSolveBase developed in:

GROUP=Core  Julia 1.10.11   Testing SimpleNonlinearSolve tests passed
GROUP=Core  Julia 1.12.4    Testing SimpleNonlinearSolve tests passed
GROUP=QA    Julia 1.12.4    Testing SimpleNonlinearSolve tests passed

Before the select/simple_solution result-type assertions, the 1.10 Core group failed @inferred solve(prob, SimpleTrustRegion()) in the root-finding snippet (return type NonlinearSolution{_A, _B, _C, _D, …}); with them it passes on both versions.

Formatted with Runic 1.10.0; typos clean.

Not verified

  • SimpleDFSane and SimpleLimitedMemoryBroyden are unchanged here (SimpleDFSane's line search branches on traced values and is still unsupported under Reactant; limited-memory Broyden already traced).
  • GPU groups; GROUP=Everything.

🤖 Generated with Claude Code (model: claude-fable-5) on behalf of Chris Rackauckas. Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Re-stacked on the rebased #1197 (after #1072); NonlinearSolveBase compat is now 2.50. Full Reactant matrix 147/147 and Simple Core 1.10/1.12 + QA pass locally on this stack.

🤖 Posted by an AI agent (Claude Code, model claude-fable-5) on behalf of Chris Rackauckas. Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

CI triage: the red OrdinaryDiffEq.jl/* lanes (Downstream workflow) fail during environment resolution — Unsatisfiable requirements detected for package AlgebraicMultigrid against NonlinearSolveBase's TimerOutputs = "1" compat — and they fail identically on master's own push run 33303882132 (and on #1072's last run), after having passed earlier the same day, so it is a registry-side change unrelated to this PR. It is being investigated separately.

🤖 Posted by an AI agent (Claude Code, model claude-fable-5) on behalf of Chris Rackauckas. Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE

@ChrisRackauckas-Claude
ChrisRackauckas-Claude force-pushed the reactant-simple branch 3 times, most recently from 4601664 to 458b609 Compare August 30, 2026 11:31
ChrisRackauckas-Claude and others added 2 commits September 6, 2026 02:55
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
Write each simple solver iteration once as a `ReactantCore.@trace while` loop
without early returns, so the same code runs on the host and under
`Reactant.@compile`. Loop state is prepared by `Utils.init_loop_state`/
`Utils.fresh` and the solution by `Utils.simple_solution`; the Reactant-only
shadow solution, copies and duplicated loops are removed.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant