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

    No labels
    No labels

    Type

    No type
    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