Repository navigation
Evaluate Jacobians into a private residual buffer - #1365
ChrisRackauckas-Claude wants to merge 2 commits into
Conversation
`construct_jacobian_cache` stored the solver's residual array as the `JacobianCache` output buffer, so every in-place AD Jacobian evaluation overwrote the solver's `fu` with the dual-number primal of f(u). That primal is not bitwise equal to a plain f call (e.g. `sin` of a Dual goes through `sincos`), so the residual used by the step and the termination check silently changed after each Jacobian evaluation. Use the cache's own `fu_cache`, which is already allocated for DI preparation. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Agent-Harness: Claude Code Agent-Model: claude-opus-5-5 Agent-Session: https://claude.ai/code/session_01LPHREnnonfLg1VcE1EJovv
Independent review (Devin CLI 3000.11.3, model fusion-claude-opus-5-5-high-sidekick-swe-2-medium): MERGE, risk low. Full reviewVERDICT: MERGE PR: #1365 (head 7dd4590, base origin/master 8a86577). No linked issue; stacked follow-up is #1367. The change is one line: Blocking findingsNone. Non-blocking findings
What I ranAll runs used Julia 1.12.7, with two scratch envs built from
What I did not verify
🤖 Posted by an AI agent — harness: Devin CLI 3000.11.3 (review), Claude Code 2.1.285 (posting) · model: fusion-claude-opus-5-5-high-sidekick-swe-2-medium |
Please ignore until reviewed by @ChrisRackauckas.
What changed and why
construct_jacobian_cachehanded the solver's own residual array toJacobianCacheas the output buffer, and(cache::JacobianCache)(u)passes that buffer asytoDI.jacobian!. For an in-place problem differentiated with ForwardDiff, every Jacobian evaluation therefore overwrote the solver'sfuwith the dual-number primal of f(u).That primal is not bitwise equal to a plain
f(u)call. For example,sinof aDualgoes throughsincos. On a 100-dim dense test problem (du = A*u + 150 .* sin.(u) .- p), 74 of 100 entries offuchange in the last bits afterjac_cache(u0). The Jacobian itself is bitwise identical. So after each Jacobian evaluation, the residual used by the Newton step and by the termination check silently became the AD primal.This PR passes the cache's existing private
fu_cacheinstead.fu_cacheis already allocated byconstruct_jacobian_cachefor DI preparation, so no buffer is added. The diff is one line inlib/NonlinearSolveBase/src/jacobian.jl, plus a test.This is also the prerequisite for the follow-up PR that reuses the init-time Jacobian on the first Newton step. That PR can only be bitwise identical to its base if a Jacobian evaluation no longer has this side effect.
Behavior change: in-place AD iterates change at roundoff level
In-place problems with an AD Jacobian now step from the plain residual instead of the AD primal, so their iterates change at roundoff level. I compared master and this branch on 98 solver/problem configurations:
jac, sparse 1000 with ajac_prototypeand with a sparsity detector, SVector{4}, scalar.alias_u0 = true.step!loop, andsolve!afterreinit!with a new u0/p, a new p, and the original values.87 of 98 configurations are bitwise identical in every iterate, residual, retcode and stat. That includes all OOP, SVector, scalar, sparse, user-
jacand matrix-free cases. The 11 that differ are all dense in-place ForwardDiff cases:The last row is a solve that diverges on both master and this branch: on master its iterates reach 1e259 before MaxIters. A roundoff-level change alters how it fails. No converging case changed retcode or step count.
Cost
The buffer already existed, so there is no new allocation. Pinned with
taskset, dense 100 IIP,solve(prob, alg):Verification
New test,
lib/NonlinearSolveBase/test/jacobian_private_fu.jl: afterjac_cache(u), the residual array equals a plainf!(fu, u, p)call bitwise. It also checks the Jacobian against the analytic one.Failing before the fix (origin/master
0055e314):Passing after:
Full suites on this branch (Julia 1.12):
Runic (
Runic --check) andtyposare clean on the changed files.Not verified
yor now write it into a private buffer, so they can only become more consistent.Risk assessment
JacobianCachefor in-place problems with an AD Jacobian (dense and sparse) in every solver built onconstruct_jacobian_cache. No public API change. Iterates of in-place AD solves change at roundoff level, as tabulated above.🤖 Generated with Claude Code (model: claude-opus-5-5) https://claude.ai/code/session_01LPHREnnonfLg1VcE1EJovv
Independent review: Devin Fusion (fusion-claude-opus-5-5-high-sidekick-swe-2-medium) rated it low, verdict MERGE: #1365 (comment)