Repository navigation
Skip FunctionWrappersWrapper in maybe_wrap_nonlinear_f under Enzyme AD - #940
Merged
ChrisRackauckas merged 4 commits intoMay 29, 2026
Conversation
… Enzyme AD The FunctionWrappersWrapper construction emits ptrtoint/store patterns that defeat Enzyme's static activity analysis when an outer reverse-mode pass differentiates through solve(::NonlinearProblem, ...). The wrapper exists for ForwardDiff dispatch in the inner Jacobian and is unnecessary on the outer-AD path, so short-circuit via EnzymeCore.within_autodiff() and return the raw function. Fixes SciML#939. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
…group Enzyme-bearing tests live in the top-level NonlinearSolve.jl test suite under the :nopre tag (kept out of the precompile-light core path). Reverts the NonlinearSolveBase sublib test additions and adds the regression as a sibling @testitem next to the existing OOP Enzyme adjoint test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
…ity works on 1.12 The sibling "Adjoint Tests" testitem gates on `VERSION < v"1.12"` because that testitem loads ForwardDiff + ReverseDiff + Tracker + Zygote + Enzyme + Mooncake together, and parts of that combination are not 1.12-ready. The SciML#939 regression only needs Enzyme + SciMLSensitivity, which both work on Julia 1.12.6 — verified locally that the patched `maybe_wrap_nonlinear_f` returns the correct gradient `dp ≈ [1.0, -1.0]` on both 1.11 and 1.12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
…uption The previous heavy form drove `Enzyme.autodiff(Reverse, simple_loss, ...)` through a full `solve(::NonlinearProblem, NewtonRaphson())` call. Compiling the `solve_up` Enzyme augmented_primal rule for the IIP path with deeply nested NamedTuple tape types abort the worker on Julia LTS + macOS with "GC error (probable corruption)" — a separate Enzyme/Julia 1.10 GC issue, not a NonlinearSolveBase bug. The other 4 nopre combinations (Linux LTS, Linux/macOS 1.11, Linux/macOS 1.12) all pass. Replace with a probe-style test that calls `maybe_wrap_nonlinear_f` from inside `Enzyme.autodiff(ReverseWithPrimal, ...)` and asserts the result is unwrapped. This verifies the fix directly without exercising the heavy tape-compilation path. Confirmed locally on Julia 1.10.11 (matches LTS). End-to-end gradient correctness was manually verified on 1.11.9 and 1.12.6 and is documented in the testitem docstring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Member
Author
|
Filed upstream Enzyme issue for the macOS LTS GC corruption that the heavier test form triggered: EnzymeAD/Enzyme.jl#3130. The probe-style regression test in this PR avoids the trigger and verifies the NonlinearSolveBase fix directly. |
ChrisRackauckas
added a commit
that referenced
this pull request
May 28, 2026
* docs: fix three CI failures (linkcheck, cross_references, example_block) The Documentation workflow on master was failing with three independent errors, all addressed here: 1. **linkcheck** — `https://iopscience.iop.org/article/10.1088/1757-899X/1276/1/012010/` returns a 302 to a PerimeterX bot validator from GitHub Actions runners. Added the URL to the existing `linkcheck_ignore` list in `docs/make.jl`. 2. **cross_references** — `docs/src/solvers/bracketing_solvers.md` referenced `[`modAB`](@ref)` but the exported binding (per commits `e856faf` / `1da0421`) is `ModAB` (uppercase). Fixed the casing. 3. **example_block** — the `@example ill_conditioned_nlprob` block at `docs/src/tutorials/large_systems.md:322-337` tripped `NoFunctionWrapperFoundError` (`No matching function wrapper was found!`) when running `solve(prob_brusselator_2d_approx_di, NewtonRaphson())` against a `NonlinearFunction` whose `sparsity` was `DifferentiationInterface.DenseSparsityDetector(AutoForwardDiff(); atol=1e-4)`. Root cause: `maybe_wrap_nonlinear_f` wraps IIP problem functions with a `FunctionWrappersWrapper` whose signatures are keyed off `ForwardDiff.Dual{Tag{NonlinearSolveTag, Float64}, ...}`. `DenseSparsityDetector{AutoForwardDiff}` runs the wrapped function with ForwardDiff duals whose tag is generated by DI (`Tag{DifferentiationInterface.FixTail{NonlinearFunction{...}}, Float64}`), not `NonlinearSolveTag`. Those duals are *isbits*, so FWW's `AllowNonIsBits` fallback does not kick in, and the dispatch trips `NoFunctionWrapperFoundError`. (Tracer-based detectors happen to work because Tracer types are non-isbits and hit the `AllowNonIsBits` fallback.) Fix: extend the same "skip wrapping in incompatible contexts" pattern introduced by #940 (Enzyme reverse-mode) to the sparsity-detector path. In `maybe_wrap_nonlinear_f`, when `prob.f.sparsity` is a non-`NoSparsityDetector` `AbstractSparsityDetector`, return the raw function unwrapped. The autospecialize gain is also less significant on the sparse path (per-color Jacobian columns are small), so this is the right tradeoff. Test added in `lib/NonlinearSolveBase/test/runtests.jl` asserting that `maybe_wrap_nonlinear_f` wraps under `NoSparsityDetector` (the default) and skips when an `AbstractSparsityDetector` is supplied. Verified locally (Julia 1.12.6): - `Pkg.test("NonlinearSolveBase")`: 40/40 pass (+3 new test cases). - Standalone repro of the docs example_block: both the TracerSparsityDetector and DenseSparsityDetector+AutoForwardDiff variants now `solve` successfully (`retcode = Success`). Without the fix the DenseSparsityDetector variant trips `NoFunctionWrapperFoundError`. - Full docs build with `julia +1.12 --project=docs -e 'include("docs/make.jl")'` (with `linkcheck = false` locally to skip the slow linkcheck pass) runs end-to-end through `Populate: populating indices.` / `RenderDocument: rendering document.` / `HTMLWriter: rendering HTML pages.` with no `Cannot resolve @ref`, no `failed to run @example block`, and no `makedocs encountered errors`. The `linkcheck = false` override is local-only and is not in the committed diff. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * maybe_wrap_nonlinear_f: narrow skip to DenseSparsityDetector The prior version skipped FunctionWrappers wrapping for *any* `AbstractSparsityDetector` other than `NoSparsityDetector`. This is overbroad: Tracer/Symbolics-style detectors emit non-isbits eltypes (`Tracer`, `Num`, …) which `FunctionWrappersWrappers`' `AllowNonIsBits` fallback already handles correctly — wrapping works for them. Only `DI.DenseSparsityDetector` is the actual failure mode, because it runs the user function with isbits ForwardDiff duals carrying DI's own `FixTail`-tagged Tag, for which no wrapper signature is pre-built and which bypasses `AllowNonIsBits`. Narrow the predicate to `prob.f.sparsity isa DI.DenseSparsityDetector` so Tracer/Symbolics paths keep the AutoSpecialize precompile benefit. The test now exercises three cases: default (wraps), `DenseSparsityDetector` (does not wrap — the regression), and an `AbstractSparsityDetector` proxy for Tracer/Symbolics (wraps). Locally verified that `NLS.solve(prob_brusselator_2d_approx_di, NewtonRaphson())` from docs/src/tutorials/large_systems.md returns `ReturnCode.Success` and that the new testset passes 4/4 on Julia 1.12.6. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * Revert maybe_wrap_nonlinear_f change; scope PR to docs-only fixes The FunctionWrappers/ForwardDiff `NoFunctionWrapperFoundError` triggered by `DenseSparsityDetector(AutoForwardDiff())` is a separate, deeper issue that shouldn't be addressed in a docs-CI PR. Restore autospecialize.jl and its test to master; this PR now only carries the linkcheck and cross-reference fixes. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> --------- Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ChrisRackauckas
marked this pull request as ready for review
May 29, 2026 02:57
ChrisRackauckas
added a commit
that referenced
this pull request
May 31, 2026
…nator adjoint path (#946) Follow-up to #944. When Enzyme reverse-mode differentiates through an MTK DAE solve, the initialization solves a `NonlinearProblem` via `solve_up`. The custom `solve_up` Enzyme rule re-runs the solve through `_solve_adjoint` -> `get_concrete_problem`, which `maybe_wrap_nonlinear_f`-wraps the IIP function in a `FunctionWrappersWrapper` (`AutoSpecializeCallable`). That wrapping is type-unstable: Enzyme's traced forward solve unwraps it (`maybe_unwrap_prob_for_enzyme`, #940) so its inferred return type carries the bare function, but the adjoint re-wraps it, so the rule's returned primal type no longer matches and Enzyme aborts with `EnzymeRuntimeException: Expected return type of primal to be NonlinearSolution{...}`. `maybe_unwrap_prob_for_enzyme` keys off the solver's own autodiff, which for an MTK DAE init is ForwardDiff even when the outer differentiation is Enzyme, so it does not fire on this path. Key off the originator instead: unwrap `_prob.f.f` via `get_raw_f` when `originator isa EnzymeOriginator`, matching the (unwrapped) type Enzyme's traced forward produces. Companion to SciML/SciMLSensitivity#1463. Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Note
Please ignore until reviewed by @ChrisRackauckas.
Summary
Fixes #939:
Enzyme.autodiff(Reverse, ...)over a function that callsNonlinearSolve.solvewas failing withEnzymeRuntimeActivityErrororiginating fromNonlinearSolveBase.maybe_wrap_nonlinear_f. TheFunctionWrappersWrapperit builds (for AutoSpecialize + IIP + array-state + non-Dual eltype problems) emitsptrtoint/storepatterns that defeat Enzyme's static activity analysis, andset_runtime_activityis not sufficient to recover correctness.The wrapper exists so the inner
ForwardDiffJacobian gets precompiled dispatch signatures. It is unnecessary on the outer-AD path — the unwrapped function works fine for Enzyme reverse mode.Fix
Add an
EnzymeCore.within_autodiff()short-circuit at the top ofmaybe_wrap_nonlinear_fthat returns the rawprob.f.fwhenever we're being called from inside Enzyme reverse-mode AD.EnzymeCoreis already a hard dependency, so no new packages are added.The same opt-out pattern is consistent with the existing Enzyme-aware infrastructure in
autospecialize.jl(_uses_enzyme_ad,maybe_unwrap_prob_for_enzyme) — those handle the inner AD case; this handles the outer AD case.Effect
EnzymeRuntimeActivityError: Detected potential need for runtime activityraised atautospecialize.jl:134via aFunctionWrappers/FunctionWrappersWrapperactivity-mismatch._concrete_solve_adjoint— "Compatibility with reverse-mode automatic differentiation requires SciMLSensitivity.jl." (the standard SciML message users already know)using SciMLSensitivity(+using ChainRulesCoreto activateNonlinearSolveBaseEnzymeExt): the MWE from Enzyme reverse-mode broken onNonlinearSolveBase.maybe_wrap_nonlinear_f— autospecialize FunctionWrappers defeats activity analysis (Julia 1.11) #939 returns the correct gradientdp == [1.0, -1.0].MWE verification
The reproducer from #939:
Verified locally on Julia 1.11.9 with Enzyme v0.13.150 and dev-checkout of this branch.
Test
Added a regression test in
lib/NonlinearSolveBase/test/runtests.jlthat probesmaybe_wrap_nonlinear_ffrom insideEnzyme.autodiff(ReverseWithPrimal, ...)and asserts the result is unwrapped. Test deps gainEnzyme. FullNonlinearSolveBasetest suite passes (25/25).Related
NonlinearSolveBase.maybe_wrap_nonlinear_f— autospecialize FunctionWrappers defeats activity analysis (Julia 1.11) #939🤖 Generated with Claude Code