Skip to content

Restart the Newton convergence rate on a new W - #4986

Open
visr wants to merge 3 commits into
SciML:masterfrom
visr:mv/reset-prev-theta-new-W
Open

visr wants to merge 3 commits into
SciML:masterfrom
visr:mv/reset-prev-theta-new-W

Conversation

@visr

@visr visr commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #4983.

nlsolve! accepts a Newton solve after one iteration when θ / (1 - θ) * ndz < κ, with θ = prev_θ remembered from an earlier solve. Only solves with two or more iterations update prev_θ, and nothing made it expire. This PR resets prev_θ to one when a Newton solve gets a new W, as CVODE does (crate = ONE in cvNlsLSetup), so the rate is measured again.

What goes wrong on master

In the issue's reproducer (NordsieckBDF, a stiff square-root nonlinearity, RMS norm over N states) the Jacobian is evaluated once, at the first step, and never again (njacs = 1 over 2793 steps). A rate of 0.024 is measured at step 6. The state then enters the steep part of the square root, where that Jacobian is far off, but every later step still stops after one iteration on the remembered rate. Each step overshoots, the error flips sign, and the step size stalls.

With the restart the rate is measured again after every W rebuild. In this run the solve at step 7 then no longer passes on its first iteration and gets a fresh Jacobian, and the stall never develops (4 Jacobians over 16 steps).

Accepted steps and final level (converged: −0.05 mm):

N 1 2000 8000 16000
master 32, −0.05 mm 2793, +12.2 mm 1023, −18.6 mm 656, +12.0 mm
this PR 31, −0.05 mm 16, −0.05 mm 18, −0.05 mm 17, −0.05 mm

The same stall occurs matrix-free (linsolve = KrylovJL_GMRES(), N = 8000: 3584 steps on master, 20 here), so it is not only a stale stored Jacobian.

Where the restart applies

Only for NLNewton on a new W, and not in these cases:

  • Functional and Anderson iteration report get_new_W! == true on every solve, so a restart there would floor their second-iteration θ at 0.3 every time.
  • Linear functions (islinearfunction). W is rebuilt on every change of γdt there, so a restart forced a second iteration on nearly every step: 111 to 190 Newton iterations for IMEX KenCarp4 with a linear implicit part, 99 to 166 for FBDF on a linear operator. For a linear function the rate does not depend on the state, so the remembered rate stays valid. With the exemption these counts equal master's.
  • always_new = true, where every W is new. This is a known limitation: NordsieckBDF(nlsolve = NLNewton(always_new = true)) at N = 8000 still takes 3607 steps, as on master. Restarting there too fixes it but costs ×1.38 Newton iterations on standard problems, each with a Jacobian and a factorization.

The restart bounds how long a stale rate can be used, to the next W rebuild. It does not remove staleness between rebuilds. CVODE additionally forces a setup every 20 steps.

Cost

This PR against master, Julia 1.13.4, minimum of repeated solves:

Class Cases Newton iterations Wall time
Nonlinear stiff (Robertson, Orego, HIRES, Pollution, Van der Pol, 1D Brusselator; FBDF, QNDF, TRBDF2, KenCarp4; tol 1e-4 to 1e-8) 78 ×1.053 ×1.019
Matrix-free Newton–Krylov (GMRES) 12 ×1.153 ×1.068
Fixed step size (adaptive = false) 8 ×1.805 ×1.021
Linear operator and IMEX-linear 2 ×1.000 ×1.000

Short solves that rebuild W on nearly every step are at the high end, for example FBDF on the reproducer: 114 to 197 RHS calls over 24 steps. Three other ways of expiring the rate (restart only when the Jacobian is not current, CVODE's min(1, crate) first-iteration test, a Hairer-style fading rate) also fix the reproducer and cost within two percentage points of this rule on the stiff set.

One side effect: at tolerances near 1e-12 the restart adds iterations whose residual is at the rounding floor, where the finite-difference slope of the relax line search is noise. Trapezoid(nlsolve = NLNewton(relax = BackTracking())) then hits #4681 at more nearby tolerances (Orego: 7 of 8 instead of 3 of 8). At exactly 1e-12 nothing changes.

Tests

In newton_tests.jl, above the Lorenz/Orego BackTracking loop, which errors on master (#4681):

  • "prev_θ restarts on a new W": the reproducer at N = 2000. It asserts fewer than 100 accepted steps (2793 on master) and that the pit's total outflow equals its inflow within 1% (off by at most 0.05% here, −8.0 on master).
  • "a linear function keeps its rate": fewer than 1.5 Newton iterations per implicit stage for IMEX KenCarp4 on a linear operator (1.19 here and on master, 2.0 without the exemption).
  • BackTracking budget: nf <= 1.1 * nf_plain instead of + 20. stats.nf leaves out the line search's own evaluations, so this compares Newton iteration counts. The gap is noise from two diverged step sequences, about ±1.5% over nearby tolerances on master as well, while shortening every step by 10% costs 70% to 100%. Report non-descent BackTracking directions as a failed Newton step instead of crashing #4894 edits the same assertion.
  • The tests need OrdinaryDiffEqBDF 2.4.2, the first release with NordsieckBDF.

Verification, detailed in the reply to the round 1 review below:

  • Downgrade job, reconstructed from the versions in its CI log (Julia 1.11.9): all 32 Core test files pass.
  • Latest stack as CI resolves it (Julia 1.13.4, --check-bounds=yes): 30 of 32 files pass. newton_tests.jl (Newton BackTracking test fails on master since 498c765ccc (calc_dt_propose! dt rounding) #4681) and nsa_noinit_tests.jl (4 MaxIters) fail identically on master.
  • QA group and the dependent suites (SDIRK, StabilizedIRK, TaylorSeriesImplicit, SDC, MultirateImplicit, StochasticDiffEqImplicit, DelayDiffEq, IMEXMultistep, PDIRK) pass.
  • OrdinaryDiffEqBDF Core passed on Julia 1.13.3. On 1.13.4 it aborts in Enzyme 0.13.219 on master too (EnzymeAD/Enzyme.jl#4051); its other 18 files pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_011fkoQWACM13p7zeDc6Rwcj

nlsolve! accepts a Newton solve after one iteration when
θ / (1 - θ) * ndz < κ with θ = prev_θ from an earlier solve. Only solves
with two or more iterations update prev_θ, and nothing reset it when W
was rebuilt, so a small rate measured once could let later solves stop
after a single iteration regardless of their W. With NordsieckBDF on a
stiff square-root nonlinearity that left an error flipping sign every
step, which passed the error test but stalled the step size.

Reset prev_θ to one when a Newton solve gets a new W, as CVODE does
(crate = ONE in cvNlsLSetup). Functional and Anderson iteration report a
new W on every solve and always_new rebuilds W every iteration, so they
keep their rate.

Add the reproducer from the issue as a regression test, above the
BackTracking loop that errors on master (SciML#4681) so that it runs. The fix
lowers nf for both plain and BackTracking Trapezoid on Lorenz, but
widens the gap between them from 3 to 27 of ~370k evaluations, so that
budget becomes relative (0.1%) instead of an absolute 20.

Fixes SciML#4983

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011fkoQWACM13p7zeDc6Rwcj
@ChrisRackauckas

Copy link
Copy Markdown
Member

🤖 Automated comment from an AI agent running as @ChrisRackauckas — not written or reviewed by Chris.

PR: #4986

Reviewed head: 8df2490
Raw SHA-256: b8f0835fc7fb1eb58312b89e6433ba9ec9cd9c02e58399352cb3ab0eb1ffffe7
Source SHA-256: 2cafeb85ab862b0aea1bd946b3ece922b704e3b152bf687c29462f90c813de2e
Source: standard-review-v1 (harness-log)
Reviewer session: 01a1d257-e2f2-7fc0-a477-10da3d2c697e
Slot: 1
Confidence: unknown
Own risk fixed before author rating/discussion: yes

Risk assessment

  • Risk: high (H2)
  • Verdict: CHANGES
  • Reviewed head: 8df2490

Review report

VERDICT: CHANGES
Round: 1
Head: 8df2490
Slot: 1
CI: CI is not green (regress:downgrade-sublibraries / test (lib/OrdinaryDiffEqNonlinearSolve); sublibrary-ci / lib/OrdinaryDiffEqTaylorSeriesImplicit / Julia 1 / Tests - Core (Julia 1); sublibrary-ci / lib/OrdinaryDiffEqTaylorSeriesImplicit [QA] / Julia 1 / Tests - QA (Julia 1))
Session: 01a1d257-e2f2-7fc0-a477-10da3d2c697e

Risk

high (H2: shared Newton stopping behavior changes default FBDF solves, including automatic stiff selection; H4: an existing test bound is relaxed without a derivation).

Blocking findings

The head fixes both accuracy and step-count failures in the pit reproduction. Two test issues remain:

  • [P2] Define and justify the relaxed regression guard. lib/OrdinaryDiffEqNonlinearSolve/test/newton_tests.jl:53 widens nf_plain + 20 to 1.001 * nf_plain. I observed plain/BackTracking Lorenz counts of 373840/373843 on base and 370828/370855 on head. The head's 27-call gap fails the existing bound; the replacement permits 370 extra reported calls for this run. The comment states a goal (“marginally more”) without explaining the basis for 0.1% or what regression that boundary detects. Moreover, this statistic omits BackTracking objective evaluations, as measured below. Specify the intended metric and give a present-tense rationale for a budget that preserves a meaningful regression check.

  • [P2] Set a dependency bound that provides the newly used solver. lib/OrdinaryDiffEqNonlinearSolve/test/newton_tests.jl:4 imports NordsieckBDF, used at line 32, but the package permits OrdinaryDiffEqBDF = "2". I resolved and loaded tagged BDF v2.4.1 with the current dependency/test environment; an import/use probe printed BDF_VERSION=2.4.1 and failed with UndefVarError: NordsieckBDF not defined in Main. The v2.4.2 tag provides the type; v2.4.1 does not. Require an appropriately tested release containing the solver, or place this solver-specific regression in the BDF tests. This establishes an unsupported admitted dependency independently of the supplied CI failure names.

  • Verdict: CHANGES

  • Round: 1

  • Blocking priorities: P2, P2

Non-blocking findings

  • lib/OrdinaryDiffEqNonlinearSolve/test/newton_tests.jl:52, src/newton.jl:1031: the added cost description needs qualification. A direct counter on the test's Lorenz RHS measured 370828 total calls for plain Newton and 1340979 for BackTracking, while the respective nf values were 370828/370855. BackTracking leaves 970124 calls unreported. These measurements are confirmed on head; the omitted accounting is in unchanged relax! code. Scope the comment to the reported statistic, or instrument actual calls for a physical RHS-work guard; handle the bookkeeping repair separately.
  • lib/OrdinaryDiffEqNonlinearSolve/test/newton_tests.jl:41: the endpoint check accepts h = 0, where the model still has dh/dt = -0.0022689075630252103. Independent bisection of the flow-balance equation gives h = -5.1910259784151963e-5 (balance residual 2.22e-16). Consider a balance-derived reference and accuracy bound, or an independent convergence check at tighter integration tolerances. This is a test-strength observation; the head's measured equilibrium error is only 9.81e-8.

What I ran

AI reviewer: Codex, model gpt-6.1-sol; codex --version reported codex-cli 0.164.0. Local session 01a1d257-e2f2-7fc0-a477-10da3d2c697e (no session URL).

Commands set the requested TMPDIR and preserve JULIA_DEPOT_PATH. No repository lockfile/interpreter pin was present, including ignored files. A scratch manifest was resolved from the declared dependencies, test extras, compat, and local path sources, then shared by the head/base comparisons. Julia was 1.13.2. Resolved versions include SciMLBase 3.58.0, NonlinearSolve 4.32.0, NonlinearSolveBase 2.55.1, SimpleNonlinearSolve 2.14.6, LinearSolve 5.18.3, ForwardDiff 1.4.7, and LineSearches 7.8.2.

  • git diff origin/master...HEAD; source/test inspection; root instruction-file checks. No root AGENTS.md or CLAUDE.md was present. At review start, origin/master equalled merge base 4460ff80dad621418e6e3a9b63e4afb2e1f5fc02.
  • git diff --check origin/master...HEAD and typos on both changed files: exit 0.
  • uv run --locked python --version: Python 3.15.0, with the warning that --locked has no effect outside a project. No system-Python substitution was used.
  • GROUP=Core julia --startup-file=no --check-bounds=yes --project=../scratch/env ../scratch/head/lib/OrdinaryDiffEqNonlinearSolve/test/runtests.jl: Julia exit 1. Developer API 27/27, Anderson resize 352/352, Newton 7 pass/1 error, including 3/3 new regression assertions. The error is LineSearchException: Search direction is not a direction of descent in the existing Orego BackTracking case.
  • Supplemental remaining_core.jl ran all 28 unreached Core files in isolated SafeTestsets, with unmodified test bodies and explicit failure reporting/nonzero exit. Result: 1111 pass, 4 fail, 40 broken. Combined coverage of all 31 Core files: 1497 pass, 4 fail, 1 error, 40 broken.
  • Base-driver noinit_baseline.jl: 98 pass/4 fail, matching the head group's totals. Both drivers produce four MaxIters success-retcode failures in the Robertson mass-matrix section at test/nsa_noinit_tests.jl:78. The head/base diagnostic runs also reproduce the Orego BackTracking exception. These failure patterns are not unique to head.
  • Head/base probes.jl runs completed their diagnostics. Base uses the unmodified base nlsolve.jl method with otherwise identical code/dependencies. All 24 manufactured-solution cases per mode passed: Float32/Float64, in-place/out-of-place, FBDF/QNDF/Trapezoid, Newton always_new, functional, and Anderson iteration. Maximum endpoint error was 8.94e-7 in either mode.
Pit check Base driver Head driver
NordsieckBDF accepted steps / RHS calls 2793 / 2812 16 / 123
NordsieckBDF equilibrium error 1.22904e-2 9.80771e-8
FBDF accepted steps / RHS calls 23 / 114 24 / 197
FBDF equilibrium error 1.98149e-9 1.11206e-9

The new step-count and accuracy assertions fail on the base reproduction and pass on head. The FBDF results also demonstrate default behavior changes and increased RHS work on an already accurate path; no wall-time benchmark was run.

  • gradient_probe.jl, head/base: ForwardDiff through default FBDF on a stiff nonlinear system with exact solution u₁=exp(p*t), u₂=exp(2p*t). At p=-2, the independent derivative reference is 2exp(-4). Both modes passed the predeclared checks at all three tolerance levels:
Solve tolerance Base derivative error Head derivative error
1e-6 1.82e-8 1.70e-7
1e-8 3.57e-10 3.63e-10
1e-10 5.98e-12 2.99e-12
  • rhs_counter.jl: both solutions returned Success; the direct RHS counters establish the accounting discrepancy above. AD calls were counted separately and included in the totals.
  • uv run --locked python ../scratch/algebra_probe.py: independently computed the balance reference and zero-level counterexample.
  • Tagged-source checks and scratch Pkg.instantiate runs admit BDF v2.0.0 and v2.4.1. Loading v2.0.0 with the current SDIRK source first fails on the older ImplicitEulerConstantCache interface. The cleaner v2.4.1 import/use probe reaches the missing NordsieckBDF binding and exits 1.
  • Final HEAD matches the requested SHA; git status --porcelain --untracked-files=all is empty. All started commands finished, and no review Julia process remains.

What I did not verify

  • QA, ModelingToolkit, full downgrade CI, TaylorSeriesImplicit Core/QA, GPU, other Julia versions, and broader downstream tests were not rerun. TaylorSeriesImplicit sources were inspected, but its reported CI failures remain unclassified. Existing broken/skipped tests were preserved.
  • The shared Orego and Robertson failures were reproduced but not bisected, fixed, or filed; this review is read-only. No assertion of a fully green Core suite or CI is made.
  • No PR body, discussion, reviews, author risk rating, or sibling review artifact was consulted. Newest-commit Agent-Harness and Agent-Model trailers are absent, so both author attribution fields below are unknown.

AUTHOR_GRADE: {"task":"bugfix","score":4,"difficulty":4,"rounds":1,"author_harness":"unknown","author_model":"unknown","note":"Verified the pit accuracy and step-count fix, broad analytic cases, and default-FBDF parameter gradients. Core failure patterns also reproduce with the base driver. The new test's dependency range and the relaxed evaluation-count guard still need correction or justification."}

Head link: 8df2490


🤖 Posted by an AI agent — harness: codex-cli 0.164.0 · model: gpt-6.1-sol
Conversation: local session 01a1d257-e2f2-7fc0-a477-10da3d2c697e (no session URL)

visr and others added 2 commits November 14, 2026 14:47
Changes from review round 1 of SciML#4986.

A linear function gets a new W on every change of γdt, so the restart
forced a second Newton iteration on nearly every step there: 111 to 190
iterations for KenCarp4 on a split problem with a linear implicit part,
99 to 166 for FBDF on a linear operator. Its convergence rate does not
depend on the state, so it now keeps the rate, and those counts are back
at their values on master. A new test asserts fewer than 1.5 iterations
per implicit stage on the IMEX case.

The pit test now checks the balance itself, total outflow equal to the
inflow within 1%, instead of a level window that also accepted h = 0.
Its tests need OrdinaryDiffEqBDF 2.4.2, the first release with
NordsieckBDF.

The BackTracking guard compares Newton iteration counts, because
stats.nf leaves out the line search's own evaluations. Its gap to plain
Newton is noise of about ±1.5% from two diverged step sequences, on
master too, so the 0.1% budget failed on Orego in the downgrade job.
The budget is now 10%; shortening every step by 10% costs 70% to 100%.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011fkoQWACM13p7zeDc6Rwcj
@visr

visr commented Nov 14, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Written by an AI agent (Claude Code) on behalf of the PR author.

Reply to the round 1 review of head 8df2490.

New head: 3b73913. It merges master at 535e41f (clean; brings in #4794 and #4994) and adds one commit with the changes below. The PR description is updated to match.

Blocking findings

[P2] Relaxed regression guard: valid, and the 0.1% budget was wrong. It failed in this PR's own downgrade job on Orego (902999 <= 902346.445, +0.172%).

  • Metric. stats.nf gets one residual evaluation per Newton iteration plus the per-step evaluations; relax! adds none. The assertion therefore compares Newton iteration counts, not work. A direct RHS counter on head gives 370705 calls for plain Newton and 1340853 for BackTracking on Lorenz (3.62×) against nf = 370746 / 370771, which confirms your measurement.

  • Why there is a gap at all. BackTracking returns α < 1 in at most 0.02% of line searches: Orego 9 of 721968 on base and 127 of 761189 on head (downgrade stack), Lorenz 0 to 2 of about 325000. Each shortened step changes the step-size sequence (the accepted-step counts of the two solves differ), so the gap is the difference between two diverged trajectories.

  • Noise. The same comparison at tol = 1e-12 * (1 + k * 1e-3), k = 0:7, as relative gap in nf:

    Stack Problem Base Head
    downgrade Orego −0.65% … +0.94% −1.47% … +0.68%
    downgrade Lorenz within ±0.001% −1.04% … +0.002%
    latest Orego −0.26% … +0.27% (3 of 8 throw Newton BackTracking test fails on master since 498c765ccc (calc_dt_propose! dt rounding) #4681) 7 of 8 throw Newton BackTracking test fails on master since 498c765ccc (calc_dt_propose! dt rounding) #4681

    Base passes + 20 at k = 0 only because the noise is negative there (−807 calls). At k = 4 base is at +0.94%.

  • Signal. Shortening every step by 10% (relax = 1 // 10) raises nf by 68% to 101% over both problems, both stacks and both branches. relax = 1 // 2 ends Unstable.

  • Budget. 10%, about 7× the largest noise observed and about 7× below the smallest effect of 10% damping. Limitation: 1% damping moves nf by −4% to +24%, so no nf bound detects damping at that level.

The comment above the assertion now states the metric and these figures. #4894 edits the same assertion (0.1% for Orego, + 20 for Lorenz). On this branch Lorenz is at +25 to +27 on the latest stack, so the two need reconciling in whichever lands second.

[P2] Dependency bound: valid. The downgrade job resolved OrdinaryDiffEqBDF v2.2.1 and failed with UndefVarError: NordsieckBDF. The tags confirm the solver first ships in v2.4.2. Master raised the test floor to 2.4.1 in #4794; it is now "2.4.2". BDF 2.4.2's own lower bounds are all at or below the versions the job resolves (Core 4.14.0, Differentiation 3.10.0, SDIRK 2.9.0, SciMLBase 3.46.0). In that stack the regression test gives 16 accepted steps with the fix and 2793 without.

Non-blocking findings

  • Cost description. Reworded as above: the comment is scoped to the reported statistic. The missing accounting in relax! is untouched here and has no issue yet as far as I could find.
  • Endpoint check. Replaced by the balance itself: pit! is evaluated at the final state and the test asserts du[1] + du[2] ≈ 1 with rtol = 1e-2 (total outflow against the inflow of 1). Bisection gives the same h* = -5.1910259784151963e-5 as yours, and the outflow slope there is −4683 per meter, so 1% corresponds to |h - h*| < 2.1e-6 m. Head is off by at most 4.6e-4 in outflow (N = 2000). Base gives −8.01. h = 0 gives 0.73 and is now rejected.

Change not requested by the review: linear functions keep their rate

Checking H2 turned up a regression in the fix as pushed. For a linear function do_newJW reports a new W on every change of γdt, so every such step restarted the rate and took a second iteration. nnonliniter, base against old head, 40-state diffusion operator, tol 1e-6, latest stack:

Case Base Old head New head
IMEX KenCarp4, SplitODEProblem(MatrixOperator(A), reaction!) 111 190 111
FBDF, ODEFunction(MatrixOperator(A)) 99 166 99
QNDF, same 88 166 88
Trapezoid, same 149 274 149

The restart now skips islinearfunction(integrator) (public in OrdinaryDiffEqDifferentiation since at least 3.10.0, the compat floor). It is safe to skip: for a linear function the iteration matrix I - W̃⁻¹W does not depend on the state, so a remembered rate cannot go stale when the state moves. A new testset "a linear function keeps its rate" asserts fewer than 1.5 iterations per implicit stage on the IMEX case. Measured over n ∈ {20, 40, 80} and tol ∈ {1e-4, 1e-6, 1e-8} on both stacks: at most 1.27 with the exemption, at least 1.98 without.

Two cases I tested and deliberately did not exempt or include:

  • Matrix-free Newton–Krylov is not exempt. NordsieckBDF(linsolve = KrylovJL_GMRES()) (W is a WOperator over a JVPCache) stalls on base too: N = 8000 takes 3584 accepted steps and ends 6.5e-2 m off; head takes 20. So the stall is not only a stale stored Jacobian.
  • always_new = true remains exempt, which is a known limitation. NordsieckBDF(nlsolve = NLNewton(always_new = true)) at N = 8000 takes 3607 accepted steps on base and head alike. Restarting there as well fixes it (20 steps) but costs ×1.38 Newton iterations, each with a Jacobian and factorization (geometric mean over 49 standard cases, max ×1.99). Left out on purpose and stated in the PR description.

Risk H2: measured cost

Head against base, Julia 1.13.4, in-repo sibling packages, no bounds checking, interleaved runs, minimum of repeated solves:

Class Cases nnonliniter Wall time
Nonlinear stiff: Robertson (0, 1e5), Orego, HIRES, Pollution, Van der Pol, 1D Brusselator with 200 states; FBDF, QNDF, TRBDF2, KenCarp4; plus the 1000-state Brusselator with FBDF and QNDF; tol 1e-4, 1e-6, 1e-8 78 ×1.053 ×1.019 (0.88 to 1.34)
Matrix-free (KrylovJL_GMRES), FBDF and TRBDF2, Brusselator 12 ×1.153 ×1.068 (0.99 to 1.22)
adaptive = false; FBDF, ImplicitEuler, TRBDF2, KenCarp4 8 ×1.805 ×1.021 (1.01 to 1.04)
Linear operator and IMEX-linear 2 ×1.000 ×1.000

Julia 1.13.4 was released during this round. Everything from the latest stacks was measured on 1.13.3 first and rerun on 1.13.4. Solver statistics and end-point errors are identical in all 546 solves of the count benchmark (182 cases under base, old head and new head) and in every probe quoted here. The time ratios on 1.13.3 were ×1.018, ×1.076, ×1.037 and ×0.997.

  • Your FBDF pit numbers (114 to 197 RHS calls) are the high end. In that 24-step solve dt grows by orders of magnitude, W is rebuilt on nearly every step, and each rebuild restarts the rate. With κ = 1/100 and the max(0.3 * prev_θ, …) floor it takes about three two-iteration solves for the estimate to decay again. The endpoint error there goes from 2.0e-9 to 1.1e-9 m (N = 2000) and from 4.2e-8 to 2.7e-15 m (N = 8000).
  • Fixed step size. do_newJW returns a new Jacobian and W on every step when !opts.adaptive, so every step restarts and takes two iterations per stage instead of one. nf rises 2% because the Jacobians dominate. On a 100-state Brusselator at dt = 0.05 the error goes from 9.9e-8 to 4.4e-8 for KenCarp4 and from 5.6e-4 to 3.5e-4 for Trapezoid, and is unchanged for ImplicitEuler.
  • Alternatives. Restarting only when the Jacobian is not current, CVODE's min(1, crate) first-iteration test, and a Hairer-style fading rate all fix the pit and cost within two percentage points of this rule on the stiff set, so the cost is not specific to this variant.

Two further observations:

  • Mechanism. On base the pit's Jacobian is evaluated once (njacs = 1 for all 2793 steps). The rate that does the damage (0.0239) is measured at step 6 with the W that stays in use until step 699; it goes stale because the state enters the relaxed part of the square root, where the stored Jacobian is far off. With the restart, step 7 of the traced run (default NordsieckBDF()) takes five iterations, re-evaluates the Jacobian and cuts dt; that run finishes in 20 steps with 5 Jacobians, and the test's configuration in 16 steps. The restart bounds the staleness to the next W rebuild and does not remove it between rebuilds; CVODE additionally forces a setup every 20 steps.
  • Interaction with relax. At these tolerances the restart adds iterations whose residual is at the rounding floor, where the finite-difference slope in relax! is noise (non-negative in 161303 of 326435 line searches on Lorenz, base). That is why head shortens more steps (127 against 9 above) and why, with LineSearches 7.8.2, Newton BackTracking test fails on master since 498c765ccc (calc_dt_propose! dt rounding) #4681 fires at more of the eight tolerances (Orego 3 to 7, Lorenz 2 to 4). At exactly 1e-12 nothing changes: Orego throws and Lorenz passes on both.

CI on the reviewed head

Of the 13 failing checks, one was caused by this PR:

  • downgrade-sublibraries / test (lib/OrdinaryDiffEqNonlinearSolve): the two failures above, both fixed.
  • TaylorDiff 0.3.5 precompile error (TypeError: non-boolean (Symbolics.Num)): Documentation, downgrade TaylorSeries and TaylorSeriesImplicit, sublibrary-ci TaylorSeriesImplicit Core and QA. Fixed on master by Raise TaylorDiff compat floors to 0.3.6 #4994, now merged in. TaylorSeriesImplicit's tests pass locally on this branch.
  • Identical on master, compared log against log: NonlinearSolve Core on Julia 1 and lts (Newton BackTracking test fails on master since 498c765ccc (calc_dt_propose! dt rounding) #4681 abort after this PR's testsets pass), ProbNumDiffEq Downstream (6 failed), SciMLSensitivity Core2 (4 errored), tests / AD on lts (1 errored), tests / GPU and OrdinaryDiffEqDefault [GPU] (@test_nowarn hit by a GPUCompiler deprecation warning).

To expect on the next run: once CI's Julia 1 jobs pick up 1.13.4, Enzyme-based tests abort there regardless of this PR. Enzyme 0.13.219, the latest release, hits unsupported tag julia.gc_alloc_ptr_offsets (EnzymeAD/Enzyme.jl#4051). Locally this takes down the BDF Core suite at bdf_convergence_tests.jl:15 on base and head alike.

What I ran

All on the new head's tree, one Julia process per test file unless noted.

  • Downgrade stack. Julia 1.11.9, --check-bounds=yes, the 196 package versions printed by master's downgrade job 131814851919 with BDF moved to 2.4.2, local OrdinaryDiffEqNonlinearSolve. This is a pinned reconstruction, not the julia-downgrade-compat action. It reproduces the old head's CI counts exactly (901445 / 902999). All 32 Core files pass; newton_tests.jl 11/11.
  • Latest stack as CI resolves it. Registered siblings (Core 4.19.1, Differentiation 3.12.5, SDIRK 2.9.8, BDF 2.4.14, LineSearches 7.8.2), Julia 1.13.4, --check-bounds=yes. 30 of 32 files pass. newton_tests.jl: 9 pass and the Newton BackTracking test fails on master since 498c765ccc (calc_dt_propose! dt rounding) #4681 error. nsa_noinit_tests.jl: 98 pass, 4 fail (MaxIters, rober-mm iip, line 78). Base in the same stack fails the same two files the same way. newton_tests.jl gives the same result on Julia 1.10.13.
  • In-repo siblings (Core 4.20.1, Differentiation 3.13.0), Julia 1.13.4, --check-bounds=yes: also 30 of 32, with the same two failures on both branches.
  • Bounds checking matters for one file. Without --check-bounds=yes, nsa_jacobian_reuse_tests.jl:66 fails on both branches in the registered stack (base 45869 < 44710.0, head 45869 < 37098.75, because the NLNewton reference drops from 35768 to 29679 accepted steps). With the flag it passes on both (21/21).
  • QA and format. GROUP=QA passes (JET 13 pass / 33 broken, Aqua 41/41). Runic 1.13.0 --check is clean on the three changed Julia files.
  • Dependents (Pkg.test, in-repo sources, Julia 1.13.4): SDIRK, StabilizedIRK, TaylorSeriesImplicit, SDC Core, MultirateImplicit, StochasticDiffEqImplicit Core, DelayDiffEq Interface and Integrators pass. IMEXMultistep and PDIRK, which do not source the in-repo NonlinearSolve, pass in an environment that develops it. BDF Core passed in full on 1.13.3. On 1.13.4 it aborts in Enzyme as described above, on base too; its other 18 Core files pass when run one by one.
  • New assertions against base source. Both pit assertions fail (2793 < 100, -8.009 ≈ 1). The linear testset passes on base, as expected.

What I did not verify

  • The real downgrade action and its resolver; typos; the root-level test groups; GPU and downstream jobs; Julia pre.
  • bdf_convergence_tests.jl on Julia 1.13.4, because of the Enzyme abort.
  • The Robertson no-init MaxIters failures are not bisected or filed.
  • Timings are from one machine.

Author attribution: harness Claude Code. This round's changes and this reply: model Fable 5.2 (claude-fable-5-2), reasoning effort max. The first commit (8df2490) was written with Opus 5.5, as its trailer says.


🤖 Posted by an AI agent on behalf of the PR author — harness: Claude Code · model: Fable 5.2 (claude-fable-5-2) · reasoning effort: max
Conversation: https://claude.ai/code/session_011fkoQWACM13p7zeDc6Rwcj

This branch has not been deployed

No deployments
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.

nlsolve! keeps a stale convergence rate prev_θ when W is rebuilt

2 participants