[AI] OptimizationBase: use a supplied f.grad when synthesizing fg!#1283
Merged
ChrisRackauckas merged 1 commit intoJul 26, 2026
Merged
Conversation
Every AD extension gated `fg!` construction on `fg == true && f.fg === nothing` without checking `f.grad`, so a caller-supplied gradient was replaced by a fresh AD preparation over `f.f` and then never called. Harmless while `allowsfg` was off for Optim; enabling it in SciML#1200 made every first-order line search take the rebuilt path instead. Pathfinder.jl supplies `grad` from `LogDensityProblems.logdensity_and_gradient`, and its objective closure captures the whole log-density problem, so the rebuilt Mooncake preparation has to zero the entire model's tangent on every reverse pass. On a 66-parameter hierarchical Turing model: 0.37 ms/call for the supplied gradient vs 10.3 ms/call for the rebuilt one, with 96% of profile samples in `Mooncake.set_to_zero!!`/`_already_tracked!`. End to end, a second `pathfinder` run went 0.55 s -> 15.3 s and `multipathfinder(8)` 18.1 s -> 139.2 s; both are restored by this change. Also drops the duplicated `prepare_gradient` in the DI extension, whose two branches were identical despite the comment claiming one reused the gradient preparation. Fixes SciML#1282 Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Uj4Pu1LKHYqLSTLfPYDPo
ChrisRackauckas
marked this pull request as ready for review
July 26, 2026 09:34
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1282.
Root cause
Every AD extension gates
fg!construction onfg == true && f.fg === nothing— it never checksf.grad. So when a caller supplies a gradient but not anfg, OptimizationBase builds a brand-new AD preparation overf.fand the supplied gradient is never called.That was harmless while
allowsfgwas off for Optim. #1200 flipped it on, so every first-order line search started taking the rebuilt path.Pathfinder.jl hits this squarely: it supplies
gradwrappingLogDensityProblems.logdensity_and_gradient(Pathfinder/src/optimize.jl:26-28), i.e. Turing's own prepared AD path. Verified with a counter inside a user-supplied gradient over 100 LBFGS iterations:The replacement is far slower because Pathfinder's objective closure captures the whole
LogDensityProblem(model + data + VarInfo), so Mooncake's tangent is that entire object graph and has to be zeroed before every reverse pass.Measurements
66-parameter hierarchical Turing model, 2400 observations, Mooncake,
Optim.LBFGS(), Julia 1.11:logdensity_and_gradient(what Pathfinder supplied)value_and_gradient!Flat profile of the rebuilt path: 2948 of 3085 samples (96%) in
Mooncake.set_to_zero!!/set_to_zero_internal!!/_already_tracked!. Only ~4% is actual derivative work.End to end:
allowsfg=false(pre-#1200)allowsfg=true(master)pathfinder1stpathfinder2ndmultipathfinder(8)The first-run component is the extra
prepare_gradient— 70 s cold, 12.4 s warm — paid once persolve, i.e. once per Pathfinder path.Change
Prefer a supplied
f.gradwhen synthesizingfg!, inOptimizationDIExt.jl(in-place and out-of-place),OptimizationZygoteExt.jl(dense and sparse), andOptimizationEnzymeExt.jl(both):Also drops the duplicated
prepare_gradientin the DI extension, whose two branches were identical despite the comment claiming one reused the gradient preparation.This is not Optim-specific:
OptimizationOptimisers,OptimizationSophia, andOptimizationAuglagalso setallowsfg = trueand have been discarding user gradients the same way. They are covered too.OptimizationBase5.2.2 → 5.2.3.Test plan
New testset in
lib/OptimizationBase/test/AD/adtests.jlcovering ForwardDiff / ReverseDiff / Zygote / Enzyme /AutoSparse(AutoZygote())×g ∈ (false, true), asserting the supplied gradient is called exactly once and that the AD path still builds its own preparation when no gradient is supplied.Run locally on Julia 1.11:
OPTIMIZATION_TEST_GROUP=AD Pkg.test()for OptimizationBase — 775 pass, 0 failPkg.test()for OptimizationOptimJL — 6969 pass, 0 failA follow-up PR will fix two unrelated pre-existing bugs in
OptimizationEnzymeExt.jl's out-of-placeinstantiate_functionnoticed while working here.🤖 Generated with Claude Code
https://claude.ai/code/session_014Uj4Pu1LKHYqLSTLfPYDPo