Skip to content

Forward-over-reverse: no forward rule for jl_field_isdefined_checked / jl_idset_peek_bp in make_zero/make_zero! (inner reverse shadow of value with undef-able/abstract field) #3135

Description

@ChrisRackauckas-Claude

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_zerojl_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    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