Summary
Forward-over-reverse (nested Enzyme: outer Forward over inner Reverse) through an ODE solve — using direct differentiation through the solver (sensealg = SensitivityADPassThrough(), i.e. no continuous-adjoint) — fails with an internal Enzyme error while parsing the calling convention of its own type-unstable getfield rule:
CallingConventionMismatchError: Enzyme hit an internal error trying to parse the julia calling
convention definition for:
idx_jl_getfield_aug(::Val{@NamedTuple{1}}, ::SciMLBase.ODEProblem{Vector{Float64}, ...}, ...)
@ Enzyme.Compiler ~/.julia/packages/Enzyme/.../src/rules/typeunstablerules.jl:1261
expectLen != length(parameters(f))
{} addrspace(10)* ({} addrspace(10)*, {} addrspace(10)**, i32, {} addrspace(10)**)
expectLen=1
The inner reverse succeeds (gradient is correct); only the outer forward over it fails, when it differentiates a getfield of the Duplicated ODEProblem through the type-unstable / generic-runtime getfield rule idx_jl_getfield_aug, whose generated function signature Enzyme then can't match (expectLen=1 vs the 4-parameter LLVM signature).
MWE (SciMLSensitivity + OrdinaryDiffEq + DiffEqBase + Enzyme; manual nesting)
import SciMLSensitivity as SMS # loads the DiffEqBase Enzyme extension + SensitivityADPassThrough
import OrdinaryDiffEq as ODE
import DiffEqBase
import Enzyme
f(u, p, t) = -p[1] .* u
u0 = [1.0]
tspan = (0.0, 1.0)
ts = range(0.0, 1.0, length = 5)
function loss(p)
prob = ODE.ODEProblem(f, u0, tspan, p)
sol = ODE.solve(prob, ODE.Tsit5(); saveat = ts,
sensealg = DiffEqBase.SensitivityADPassThrough()) # differentiate THROUGH the solver
sum(abs2, Array(sol))
end
const RA_R = Enzyme.set_runtime_activity(Enzyme.Reverse)
const RA_F = Enzyme.set_runtime_activity(Enzyme.Forward)
@noinline function grad(p) # inner reverse — works
dp = zero(p)
Enzyme.autodiff(RA_R, Enzyme.Const(loss), Enzyme.Active, Enzyme.Duplicated(p, dp))
dp
end
grad([0.5]) # [-2.4402421907949057] (fine)
Enzyme.autodiff(RA_F, Enzyme.Const(p -> sum(grad(p))), # outer forward over the reverse gradient
Enzyme.Duplicated([0.5], [1.0]))
# CallingConventionMismatchError ... idx_jl_getfield_aug(::Val{@NamedTuple{1}}, ::ODEProblem, ...)
# @ typeunstablerules.jl:1261
Reduction attempts (why no dependency-free MWE)
I tried to reduce this to an Enzyme-only MWE without success (~6 shapes): forward-over-reverse of a getfield on an immutable/mutable struct with an active Vector field plus an ::Any field (ODEProblem-like); getfield returning the boxed Any field; and forcing the dynamic getfield path via Base.inferencebarrier, an ::Any-returning getter closure, and invokelatest. All return the correct derivative — the type-stable and even the dynamically-dispatched getfields I could construct don't land on the idx_jl_getfield_aug type-unstable-getfield rule the way the real solve_up reverse rule (getfielding a Duplicated ODEProblem inside runtime_generic_*) does. Filing with the SciML-stack MWE per maintainer preference; happy to keep reducing if a dependency-free repro is required.
Versions
- Enzyme v0.13.173
- SciMLSensitivity v7.112.2, OrdinaryDiffEq v7.1.1 / OrdinaryDiffEqCore v4.5.0, DiffEqBase v7.6.0, SciMLBase v3.30.1
- Julia 1.11.9
Context
Part of getting SecondOrder(AutoEnzyme(Forward), AutoEnzyme(Reverse)) forward-over-reverse Hessians/HVPs working through SciMLSensitivity + OrdinaryDiffEq (SciML/SciMLSensitivity.jl#1427). There are two routes to that second derivative, and each currently hits a distinct Enzyme forward-over-reverse blocker:
(I used a Float64 problem to get past the separate Float32-only _ode_init type-analysis blocker #3276.) Prior cleared blockers in this chain: #3135 → #3137, #3213, #3246 → #3251, #3253 → #3254, #3258 → #3261.
Summary
Forward-over-reverse (nested Enzyme: outer
Forwardover innerReverse) through an ODEsolve— using direct differentiation through the solver (sensealg = SensitivityADPassThrough(), i.e. no continuous-adjoint) — fails with an internal Enzyme error while parsing the calling convention of its own type-unstablegetfieldrule:The inner reverse succeeds (gradient is correct); only the outer forward over it fails, when it differentiates a
getfieldof theDuplicatedODEProblemthrough the type-unstable / generic-runtime getfield ruleidx_jl_getfield_aug, whose generated function signature Enzyme then can't match (expectLen=1vs the 4-parameter LLVM signature).MWE (SciMLSensitivity + OrdinaryDiffEq + DiffEqBase + Enzyme; manual nesting)
Reduction attempts (why no dependency-free MWE)
I tried to reduce this to an Enzyme-only MWE without success (~6 shapes): forward-over-reverse of a
getfieldon an immutable/mutable struct with an activeVectorfield plus an::Anyfield (ODEProblem-like); getfield returning the boxedAnyfield; and forcing the dynamic getfield path viaBase.inferencebarrier, an::Any-returning getter closure, andinvokelatest. All return the correct derivative — the type-stable and even the dynamically-dispatchedgetfields I could construct don't land on theidx_jl_getfield_augtype-unstable-getfield rule the way the realsolve_upreverse rule (getfielding aDuplicatedODEProbleminsideruntime_generic_*) does. Filing with the SciML-stack MWE per maintainer preference; happy to keep reducing if a dependency-free repro is required.Versions
Context
Part of getting
SecondOrder(AutoEnzyme(Forward), AutoEnzyme(Reverse))forward-over-reverse Hessians/HVPs working through SciMLSensitivity + OrdinaryDiffEq (SciML/SciMLSensitivity.jl#1427). There are two routes to that second derivative, and each currently hits a distinct Enzyme forward-over-reverse blocker:Not yet implemented, mixed activity for jl_new_structin runtime_generic_fwd when outer forward differentiates a dynamically-dispatched inner reverse rule (ODE adjoint) #3277 (mixed activity for jl_new_structinruntime_generic_fwd);SensitivityADPassThrough) → theidx_jl_getfield_augcalling-convention mismatch above.(I used a
Float64problem to get past the separateFloat32-only_ode_inittype-analysis blocker #3276.) Prior cleared blockers in this chain: #3135 → #3137, #3213, #3246 → #3251, #3253 → #3254, #3258 → #3261.