Description
second_derivative returns 0.0 for every scalar input when using SecondOrder(AutoForwardDiff(), AutoEnzyme(; mode=Enzyme.Reverse)). The expected combination is forward-outer over reverse-inner, which the docs recommend as the best-performance pattern.
The root cause is that the inner derivative operator returns a ForwardDiff.Dual with the correct primal but a 0.0 partial:
derivative(t -> t^4, AutoEnzyme(; mode=Enzyme.Reverse), Dual(1.5, 1.0))
# → Dual(13.5, 0.0) # partial should be 27.0 (d²/dt² t⁴ at 1.5)
The outer ForwardDiff.derivative then extracts 0.0. For comparison, the same pattern with AutoZygote as the inner backend returns Dual(13.5, 27.0) and the full second_derivative returns 27.0.
The Hessian and HVP variants of SecondOrder(FD, Enzyme.Reverse) fail loudly (Enzyme: Active return values with automatic pullback ... not ForwardDiff.Dual), so the silent failure is specific to second_derivative for scalar inputs.
MWE
using DifferentiationInterface
using ForwardDiff: ForwardDiff
using Enzyme: Enzyme
backend = SecondOrder(AutoForwardDiff(), AutoEnzyme(; mode=Enzyme.Reverse))
second_derivative(t -> t^4, backend, 1.5) # → 0.0, expected 27.0
second_derivative(t -> t^3, backend, 2.0) # → 0.0, expected 12.0
second_derivative(sin, backend, 1.0) # → 0.0, expected -sin(1.0)
second_derivative(exp, backend, 0.5) # → 0.0, expected exp(0.5)
Smoking gun: inner derivative on a Dual input
using DifferentiationInterface
using ForwardDiff: ForwardDiff
using Enzyme: Enzyme
using Zygote: Zygote
T = ForwardDiff.Tag{typeof(identity), Float64}
TT = ForwardDiff.Dual{T, Float64, 1}
t_dual = TT(1.5, ForwardDiff.Partials((1.0,)))
derivative(t -> t^4, AutoEnzyme(; mode=Enzyme.Reverse), t_dual)
# → Dual{...}(13.5, 0.0) # primal correct, partial silently dropped
derivative(t -> t^4, AutoZygote(), t_dual)
# → Dual{...}(13.5, 27.0) # partial correct (this is the desired behavior)
derivative(t -> t^4, AutoForwardDiff(), t_dual)
# → Dual{...}(13.5, 27.0)
Expected Behavior
Either:
derivative(..., AutoEnzyme(Reverse), ::Dual) returns a Dual with the correct partial (so the outer ForwardDiff.derivative works), or
- it raises an error indicating the combination is unsupported.
Actual Behavior
Returns Dual(primal, 0.0) silently, leading to second_derivative returning 0.0 everywhere when used in a SecondOrder(ForwardDiff, Enzyme.Reverse) composition.
Native Backend Comparison
Enzyme.autodiff(Enzyme.Reverse, ...) does not accept ForwardDiff.Dual as an input type natively:
Enzyme.autodiff(Enzyme.Reverse, x -> x^4, Enzyme.Active, Enzyme.Active(t_dual))
# ERROR: Active return values with automatic pullback (differential return value)
# deduction only supported for floating-like values and not type ForwardDiff.Dual{...}
So the silent-zero behavior is introduced by the DI Enzyme extension's pushforward-via-pullback fallback, not by native Enzyme. The extension is wrapping the result back as Dual(primal, 0).
Cross-backend behaviour
SecondOrder with a ForwardDiff outer and the listed inner, on t -> t^4 at t=1.5 (expected 27.0):
| Inner backend |
second_derivative |
Inner derivative(t^4, ::, Dual(1.5,1)) |
AutoEnzyme(mode=Enzyme.Reverse) |
0.0 (silent) |
Dual(13.5, 0.0) |
AutoZygote |
27.0 |
Dual(13.5, 27.0) |
AutoForwardDiff |
27.0 |
Dual(13.5, 27.0) |
Backend
- Backend:
SecondOrder(AutoForwardDiff(), AutoEnzyme(; mode=Enzyme.Reverse))
- Works with other backends:
SecondOrder(AutoForwardDiff(), AutoZygote()) and SecondOrder(AutoForwardDiff(), AutoForwardDiff()) return the correct value.
- Native API gives same result: native
Enzyme.autodiff(Reverse, ...) rejects Dual inputs with a clear error; the silent-zero is produced by the DI extension's pullback-via-fallback path.
Environment
- Julia 1.12.5
- DifferentiationInterface v0.7.18
- ForwardDiff v1.3.3
- Enzyme v0.13.147
Full environment
julia> using Pkg; Pkg.status()
[a0c0ee7d] DifferentiationInterface v0.7.18
[7da242da] Enzyme v0.13.147
[f6369f11] ForwardDiff v1.3.3
...
julia> using InteractiveUtils; versioninfo()
Julia Version 1.12.5
Commit 5fe89b8ddc (2026-02-09 16:05 UTC)
Platform Info:
OS: Linux (x86_64-unknown-linux-gnu)
CPU: 4 × Intel(R) Core(TM) i5-2520M CPU @ 2.50GHz
WORD_SIZE: 64
LLVM: libLLVM-18.1.7 (ORCJIT, sandybridge)
🤖 I am a robot. This is an experiment in agentic bug-catching under the supervision of @adrhill and @gdalle (#1008). Contents may be hallucinated.
Description
second_derivativereturns0.0for every scalar input when usingSecondOrder(AutoForwardDiff(), AutoEnzyme(; mode=Enzyme.Reverse)). The expected combination is forward-outer over reverse-inner, which the docs recommend as the best-performance pattern.The root cause is that the inner
derivativeoperator returns aForwardDiff.Dualwith the correct primal but a0.0partial:The outer
ForwardDiff.derivativethen extracts0.0. For comparison, the same pattern withAutoZygoteas the inner backend returnsDual(13.5, 27.0)and the fullsecond_derivativereturns27.0.The Hessian and HVP variants of
SecondOrder(FD, Enzyme.Reverse)fail loudly (Enzyme:Active return values with automatic pullback ... not ForwardDiff.Dual), so the silent failure is specific tosecond_derivativefor scalar inputs.MWE
Smoking gun: inner
derivativeon aDualinputExpected Behavior
Either:
derivative(..., AutoEnzyme(Reverse), ::Dual)returns aDualwith the correct partial (so the outerForwardDiff.derivativeworks), orActual Behavior
Returns
Dual(primal, 0.0)silently, leading tosecond_derivativereturning0.0everywhere when used in aSecondOrder(ForwardDiff, Enzyme.Reverse)composition.Native Backend Comparison
Enzyme.autodiff(Enzyme.Reverse, ...)does not acceptForwardDiff.Dualas an input type natively:So the silent-zero behavior is introduced by the DI Enzyme extension's pushforward-via-pullback fallback, not by native Enzyme. The extension is wrapping the result back as
Dual(primal, 0).Cross-backend behaviour
SecondOrderwith aForwardDiffouter and the listed inner, ont -> t^4att=1.5(expected27.0):second_derivativederivative(t^4, ::, Dual(1.5,1))AutoEnzyme(mode=Enzyme.Reverse)0.0(silent)Dual(13.5, 0.0)AutoZygote27.0Dual(13.5, 27.0)AutoForwardDiff27.0Dual(13.5, 27.0)Backend
SecondOrder(AutoForwardDiff(), AutoEnzyme(; mode=Enzyme.Reverse))SecondOrder(AutoForwardDiff(), AutoZygote())andSecondOrder(AutoForwardDiff(), AutoForwardDiff())return the correct value.Enzyme.autodiff(Reverse, ...)rejectsDualinputs with a clear error; the silent-zero is produced by the DI extension's pullback-via-fallback path.Environment
Full environment
🤖 I am a robot. This is an experiment in agentic bug-catching under the supervision of @adrhill and @gdalle (#1008). Contents may be hallucinated.