Forward-over-reverse: make_zero/make_zero! shadow-init has no forward rule (jl_field_isdefined_checked / jl_idset_peek_bp) when the inner reverse shadows a value with an undef-able/abstract field
When nesting Enzyme forward over reverse (e.g. for HVPs/Hessians), the outer forward pass cannot differentiate the make_zero / make_zero! shadow-allocation the inner reverse uses — if the value the inner reverse differentiates contains an undef-able / abstract / isbits-Union field. Enzyme forward has no rule for the jl_field_isdefined_checked builtin (emitted by make_zero when it recurses into a type with possibly-undefined fields) nor for jl_idset_peek_bp (emitted by make_zero!'s IdSet aliasing check).
Reduced to Enzyme-only MWEs (no other packages). Julia 1.11.9, Enzyme v0.13.151.
MWE 1 — make_zero → jl_field_isdefined_checked
import Enzyme
RA_R = Enzyme.set_runtime_activity(Enzyme.Reverse)
RA_F = Enzyme.set_runtime_activity(Enzyme.Forward)
struct Par
w::Vector{Float64}
extra::Any # undef-able pointer slot <-- the necessary ingredient
end
loss1(p) = sum(abs2, p.w)
p = Par([1.0, 2.0, 3.0], "ignored")
dp = Par([1.0, 0.0, 0.0], "ignored")
Enzyme.autodiff(RA_F, Enzyme.Const(x -> Enzyme.gradient(RA_R, loss1, x)[1]), Enzyme.Duplicated(p, dp))
EnzymeNoDerivativeError: No forward mode derivative found for jl_field_isdefined_checked
at context: %51 = call i32 @jl_field_isdefined_checked(... i64 noundef 0) ...
[1] make_zero @ Enzyme/src/typeutils/make_zero.jl:255
[3] macro expansion @ Enzyme/src/sugar.jl:334 # inner reverse's shadow init
[4] gradient @ Enzyme/src/sugar.jl:274
MWE 2 — make_zero! → jl_idset_peek_bp
import Enzyme
RA_R = Enzyme.set_runtime_activity(Enzyme.Reverse)
RA_F = Enzyme.set_runtime_activity(Enzyme.Forward)
lossv(x) = sum(abs2, Float64[xi for xi in x])
x0 = Any[1.0, 2.0, 3.0]; dx = Any[0.0, 0.0, 0.0]
v = Any[1.0, 0.0, 0.0]; ddx = Any[0.0, 0.0, 0.0]
Enzyme.autodiff(RA_F, Enzyme.Const((d, y) -> (Enzyme.gradient!(RA_R, d, lossv, y); nothing)),
Enzyme.Duplicated(dx, ddx), Enzyme.Duplicated(x0, v))
EnzymeNoDerivativeError: No forward mode derivative found for jl_idset_peek_bp
at context: %235 = call i64 @jl_idset_peek_bp(...) ...
[1] haskey @ ./idset.jl:41
[3] make_zero! @ Enzyme/src/typeutils/make_zero.jl:485
Control — succeeds (no undef-able field)
lossc(x) = sum(abs2, x)
x0 = [1.0, 2.0, 3.0]; v = [1.0, 0.0, 0.0]
Enzyme.autodiff(RA_F, Enzyme.Const(x -> Enzyme.gradient(RA_R, lossc, x)[1]), Enzyme.Duplicated(x0, v))
# -> ([2.0, 0.0, 0.0],) no error
The plain Vector{Float64} case returns the correct HVP, so the undef-able/abstract field is the necessary-and-sufficient ingredient.
Where this bites in practice
This blocks Enzyme forward-over-reverse Hessians/HVPs of SciML neural-ODE losses (SciML/SciMLSensitivity.jl#1427): Lux / ComponentArray parameter structs carry abstract/Union/undef-able fields, so the inner reverse's make_zero recurses into them and the outer forward hits this gap. (For completeness, SecondOrder(AutoForwardDiff(), AutoEnzyme(Reverse)) fails differently — the inner Enzyme reverse can't return a ForwardDiff.Dual.)
Found while reducing the failure behind SciML/SciMLSensitivity.jl#1427. Filed by @ChrisRackauckas-Claude; please review before acting on it.
Forward-over-reverse:
make_zero/make_zero!shadow-init has no forward rule (jl_field_isdefined_checked/jl_idset_peek_bp) when the inner reverse shadows a value with an undef-able/abstract fieldWhen nesting Enzyme forward over reverse (e.g. for HVPs/Hessians), the outer forward pass cannot differentiate the
make_zero/make_zero!shadow-allocation the inner reverse uses — if the value the inner reverse differentiates contains an undef-able / abstract / isbits-Union field. Enzyme forward has no rule for thejl_field_isdefined_checkedbuiltin (emitted bymake_zerowhen it recurses into a type with possibly-undefined fields) nor forjl_idset_peek_bp(emitted bymake_zero!'sIdSetaliasing check).Reduced to Enzyme-only MWEs (no other packages). Julia 1.11.9, Enzyme v0.13.151.
MWE 1 —
make_zero→jl_field_isdefined_checkedMWE 2 —
make_zero!→jl_idset_peek_bpControl — succeeds (no undef-able field)
The plain
Vector{Float64}case returns the correct HVP, so the undef-able/abstract field is the necessary-and-sufficient ingredient.Where this bites in practice
This blocks Enzyme forward-over-reverse Hessians/HVPs of SciML neural-ODE losses (SciML/SciMLSensitivity.jl#1427): Lux / ComponentArray parameter structs carry abstract/Union/undef-able fields, so the inner reverse's
make_zerorecurses into them and the outer forward hits this gap. (For completeness,SecondOrder(AutoForwardDiff(), AutoEnzyme(Reverse))fails differently — the inner Enzyme reverse can't return aForwardDiff.Dual.)Found while reducing the failure behind SciML/SciMLSensitivity.jl#1427. Filed by @ChrisRackauckas-Claude; please review before acting on it.