ITADN

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)

#3135OpenChrisRackauckas-Claude 创建于 2026-05-29
## 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` ```julia 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` ```julia 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) ```julia 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._
4 条评论