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)
## 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 条评论