Skip to content

Accept Annotation{<:FunctionWrappersWrapper} in Enzyme rules (closes #48)#49

Merged
ChrisRackauckas merged 1 commit into
SciML:mainfrom
ChrisRackauckas-Claude:fix-issue-48-duplicated-fww
May 21, 2026
Merged

Accept Annotation{<:FunctionWrappersWrapper} in Enzyme rules (closes #48)#49
ChrisRackauckas merged 1 commit into
SciML:mainfrom
ChrisRackauckas-Claude:fix-issue-48-duplicated-fww

Conversation

@ChrisRackauckas-Claude
Copy link
Copy Markdown
Contributor

⚠️ Please ignore this PR until reviewed by @ChrisRackauckas.

Summary

Closes #48 (Missing EnzymeRuleMethodError: no method matching forward(...::Duplicated{FunctionWrappersWrapper...}...)).

The existing forward / augmented_primal / reverse rules in FunctionWrappersWrappersEnzymeExt dispatched only on func::Const{<:FunctionWrappersWrapper}. Issue #48's repro (NonlinearSolve + SciMLSensitivity + Enzyme.Forward) drives the rule with func::Duplicated{<:FunctionWrappersWrapper} instead, which fell off to custom_rule_method_error.

This PR relaxes the func argument type to Annotation{<:FunctionWrappersWrapper} across every rule (4 forward × 4 reverse), so Duplicated / BatchDuplicated function annotations dispatch through the same code path as Const.

Why is this safe?

A FunctionWrappersWrapper only carries FunctionWrappers plus cache storage — none of those fields have a meaningful tangent. The function shadow is therefore ignored: we still unwrap func.val (the primal wrapper) and delegate to Enzyme.autodiff(..., Const(f_orig), ...) exactly as the previous Const-only path did. The argument shadows, the return shadow, and the runtime-activity / strong-zero flags continue to flow through unchanged.

Enzyme drives the rule with a Duplicated function annotation when the outer autodiff call is differentiating through a closure that captures an FWW — Issue #48's repro is one such case.

Reproduction (minimal)

using FunctionWrappersWrappers, Enzyme, EnzymeCore
using EnzymeCore.EnzymeRules

f!(residual, u, p) = (residual[1] = u[1]^2 - p[1]; nothing)
fww = FunctionWrappersWrapper(
    f!, (Tuple{Vector{Float64}, Vector{Float64}, Vector{Float64}},), (Nothing,)
)

config = EnzymeRules.FwdConfig{false, false, 1, false, false}()
EnzymeRules.forward(
    config,
    Duplicated(fww, fww),                # <-- the failing dispatch
    EnzymeCore.Const{Nothing},
    Duplicated([0.0], [0.0]),
    Duplicated([2.0], [1.0]),
    Duplicated([1.0], [0.0]),
)

Before: MethodError: no method matching forward(::FwdConfigWidth{1,...}, ::Duplicated{FunctionWrappersWrapper{...}}, ::Type{Const{Nothing}}, ...)

After: residual=[3.0], dresidual=[4.0] — primal and shadow propagate correctly.

Tests

Adds four new testsets exercising each rule branch with Duplicated(fww, fww):

  • Enzyme forward, Duplicated FWW annotation — IIP Const return (the issue's failing pattern)
  • Enzyme forward, Duplicated FWW annotation — shadow-only return ({false, true} rule)
  • Enzyme forward, Duplicated FWW annotation — primal + shadow return ({true, true} rule)
  • Enzyme reverse, Duplicated FWW annotation — Const return IIP

Local test results on Julia 1.11 (Enzyme v0.13.147):

Before After
Enzyme extension passed 39 50 (+11)
Pre-existing failures 1 1

The 1 pre-existing failure (Enzyme batch forward mode (width > 1)TypeError: in typeassert, expected Tuple{Float64,Float64}, got @NamedTuple{1::Float64, 2::Float64} inside Enzyme's enzyme_call) reproduces on unmodified main and is unrelated to this change — looks like an Enzyme upstream issue with BatchDuplicated tuple shadows; investigating separately.

Note on the full SciML repro

Issue #48's NonlinearSolve + SciMLSensitivity repro will still hit a different (deeper) Enzyme runtime error after this fix — but that error is no longer the custom_rule_method_error the issue reports. The specific MethodError for forward(...::Duplicated{FunctionWrappersWrapper}...) is what's closed here; remaining downstream issues belong in NonlinearSolveBase / SciMLSensitivity and aren't fixable in this package.

Test plan

  • New testsets pass locally (Julia 1.11, Enzyme v0.13.147)
  • Existing testsets still pass (no regressions introduced)
  • Pre-existing Enzyme batch forward (width > 1) failure reproduces identically on main (not caused by this PR)
  • CI green on all jobs

🤖 Generated with Claude Code

Relaxes the `func` argument type in the forward, augmented_primal, and
reverse rules from `Const{<:FunctionWrappersWrapper}` to
`Annotation{<:FunctionWrappersWrapper}` so that callers passing
`Duplicated{<:FunctionWrappersWrapper}` also dispatch through these rules
instead of falling off to a MethodError.

Closes SciML#48.

Why this is safe: a `FunctionWrappersWrapper` only carries
`FunctionWrapper`s plus cache storage; none of those fields have a
meaningful tangent.  The function shadow is therefore ignored — `func.val`
(the primal wrapper) is unwrapped and the inner `Enzyme.autodiff` call
delegates with `Const(f_orig)` exactly as in the previous `Const`-only
path.  Enzyme drives the rule with a `Duplicated` function annotation
when the outer `autodiff` call is differentiating through a closure that
captures an FWW — the SciML `NonlinearSolve` + `SciMLSensitivity` path
in the issue is one such case.

Adds four testsets that drive each forward / augmented_primal / reverse
rule with `Duplicated(fww, fww)` to lock in the broader dispatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas ChrisRackauckas marked this pull request as ready for review May 21, 2026 09:04
@ChrisRackauckas ChrisRackauckas merged commit b1d8401 into SciML:main May 21, 2026
6 of 7 checks passed
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.

Missing EnzymeRule

2 participants