Skip to content

Fix nested ForwardDiff tag through wrapfun_iip + UJacobianWrapper#3412

Closed
ChrisRackauckas-Claude wants to merge 1 commit intoSciML:masterfrom
ChrisRackauckas-Claude:fix-nested-forwarddiff-tag-in-wrapfun
Closed

Fix nested ForwardDiff tag through wrapfun_iip + UJacobianWrapper#3412
ChrisRackauckas-Claude wants to merge 1 commit intoSciML:masterfrom
ChrisRackauckas-Claude:fix-nested-forwarddiff-tag-in-wrapfun

Conversation

@ChrisRackauckas-Claude
Copy link
Copy Markdown
Contributor

Fix for #3381. Alternative to #3406 addressing the review feedback at #3406 (comment) — handles the nested tag at the source (wrapfun_iip + UJacobianWrapper) instead of unwrapping the FunctionWrappersWrapper.

Summary

  • wrapfun_iip now uses SciMLBase.anyeltypedual to detect a Dual-carrying p, promotes the inner T so dualgen(T) yields the properly nested dual, and promotes the compiled signature's p type for Jacobian variants.
  • New UJacobianWrapper{true}(du1, uprev::AbstractArray{<:Dual{<:Any,<:Dual}}) override eagerly lifts p into the seeded DualT so u*p stays within one tag hierarchy and FunctionWrappersWrapper dispatches on the compiled nested-Dual signature (no raw-call fallback).
  • Downstream regression test reproducing the original MRE (NonlinearLeastSquares over Rosenbrock).

Root cause

NonlinearSolve seeds p with Dual{NonlinearSolveTag, Float64, 2}, then inside resid! the ODE solve seeds u with Dual{OrdinaryDiffEqTag, Dual{NonlinearSolveTag, Float64, 2}, CS}. Before this patch wrapfun_iip compiled the Jacobian signature with p::Vector{Dual{NLTag}} (the original T3), so in the user function u*p crossed sibling tag hierarchies — ForwardDiff's tag precedence returned a triple-nested Dual{NLTag, Dual{OrdEqTag, …}, …} that cannot be stored back into du::Vector{Dual{OrdEqTag, …, CS}}, producing:

MethodError: no method matching Float64(::ForwardDiff.Dual{Tag{OrdinaryDiffEqTag, Dual{Tag{NonlinearSolveTag, Float64}, Float64, 2}}, Dual{…}, 2})

Test plan

  • ODEDIFFEQ_TEST_GROUP=Core Pkg.test(\"DiffEqBase\") passes locally
  • Original MRE from ForwardDiff errors for NonlinearSolve over ODE solve #3381 succeeds with the fix; reverts to the MethodError above when the patch is removed
  • CI (Downstream group) picks up the new dual_detection_solution.jl regression test

🤖 Generated with Claude Code

…ciML#3381)

When a Rosenbrock integrator runs with a `Vector{<:Dual}` `p` (i.e. we are
inside an outer ForwardDiff layer — e.g. a NonlinearLeastSquares parameter
fit), the inner Jacobian widens `u` into a deeper nested-Dual type via its
prepared `JacobianConfig`, but `p` in `UJacobianWrapper` is still at the
*outer* Dual level. The user body then multiplies `p[i] * u[i]` across two
different Dual nesting levels and dispatches through ForwardDiff's
`@generated tagcount` precedence — whose literal value is baked at first
compile and varies with precompile ordering. That ordering can invert the
nesting and crash the eventual `setindex!(du, ...)` with
`MethodError: no method matching Float64(::nested_dual)`.

Fix at the inner solver's side by lifting `uf.p` into the inner nested-Dual
type once, before delegating to `DI.jacobian!`. The widened `p` carries zero
inner partials (`p` does not depend on `u`). One `convert.(inner_T, p)`
allocation per `jacobian!` call is amortized across every chunk evaluation DI
performs in that call — no per-call allocation inside the hot loop, unlike
a `UJacobianWrapper`-level override.

Skips widening when `uf.f` holds a `FunctionWrappersWrapper`
(`AutoSpecialize`): DiffEqBase already precompiles the
`(nested_u, outer_p, t)` FW signature directly, and a widened `p` would
produce `(nested_u, nested_p, t)` that matches no precompiled slot.

Adds `test/nested_forwarddiff_tests.jl`, exercising both the MRE call graph
and a hand-rolled outer tag (the latter deterministically reproduces the
crash pre-fix).

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
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.

2 participants