Skip to content

Bug(Enzyme): SecondOrder(ForwardDiff, AutoEnzyme(Reverse)) silently returns 0 from second_derivative #1017

Description

@bdrhill

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    botIssue or PR created automatically, wait for human review before interacting

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions