Summary
Forward-mode differentiation (with runtime activity) of a Float32 computation that constructs a large heterogeneous mutable struct fails type analysis on the struct's GC zero-initialization:
EnzymeNoTypeError: Enzyme cannot statically prove the type of a value being differentiated ...
Cannot deduce single type of store store i64 0, i64 addrspace(11)* %503, align 8,
{[0]:Float@float, [4]:Integer, [5]:Integer, [6]:Integer, [7]:Integer} size: 8
The offending store i64 0 is the zero-init of a mutable-struct allocation whose fields include an 8-byte word laid out as a Float32 (bytes 0–3) immediately followed by four Bools (bytes 4–7). Enzyme's type analysis has already established [0:4) as Float and [4:8) as Integer from the individual field accesses, so the single wide 8-byte zeroing store i64 0 over that word can't be assigned one type.
It is Float32-specific: with Float64 the same field occupies its own 8-byte word (the Bools move to the next word), so no zeroing store straddles a float/int boundary and forward mode succeeds.
Reproduction
I was not able to reduce this to a dependency-free Enzyme-only MWE (see "Reduction attempts" below), so this uses OrdinaryDiffEq — but the repro is small (2 imports, plain forward mode, no ODE step is even taken — the failure is entirely in the integrator construction, init):
import OrdinaryDiffEq as ODE
import Enzyme
f(u, p, t) = -p[1] .* u
u0 = Float32[1.0]
tspan = (0.0f0, 1.0f0)
function initcost(p)
prob = ODE.ODEProblem(f, u0, tspan, p)
integ = ODE.init(prob, ODE.Tsit5())
integ.dt
end
RA_F = Enzyme.set_runtime_activity(Enzyme.Forward)
Enzyme.autodiff(RA_F, Enzyme.Const(initcost), Enzyme.Duplicated(Float32[0.5], Float32[1.0]))
Result:
EnzymeNoTypeError: ... Cannot deduce single type of store store i64 0, ...
{[0]:Float@float, [4]:Integer, [5]:Integer, [6]:Integer, [7]:Integer} size: 8
[1] ODEIntegrator @ OrdinaryDiffEqCore/src/integrators/type.jl:130
[2] #_ode_init#81 @ OrdinaryDiffEqCore/src/solve.jl:695
Float64 control — works (correct derivative), same code with u0=[1.0], tspan=(0.0,1.0), p=[0.5]:
initcost(p) = ODE.init(ODE.ODEProblem((u,p,t)->-p[1].*u, [1.0], (0.0,1.0), p), ODE.Tsit5()).dt
Enzyme.autodiff(RA_F, Enzyme.Const(initcost), Enzyme.Duplicated([0.5], [1.0]))
# @NamedTuple{1}((-0.045957120113090597,))
Root cause (field-level)
ODEIntegrator is a mutable struct with many fields, several of them pointers/heap references — so Julia zero-initializes the whole allocation (GC safety) before writing fields, via wide integer stores. For the Float32 problem the concrete layout contains this 8-byte word:
| offset |
field |
type |
| 264 |
last_event_error |
Float32 |
| 268 |
accept_step |
Bool |
| 269 |
isout |
Bool |
| 270 |
reeval_fsal |
Bool |
| 271 |
derivative_discontinuity |
Bool |
That word is exactly {[0]:Float@float, [4]:Integer, [5]:Integer, [6]:Integer, [7]:Integer} — matching the error signature byte-for-byte. The store i64 0 zeroing this word can't be typed as either float or int.
With Float64, last_event_error is 8 bytes and 8-aligned, so it occupies its own word and the zeroing store over it is unambiguously Float — hence forward mode succeeds.
Reduction attempts (why no dependency-free MWE)
I tried ~20 Enzyme-only reductions of "forward-mode over a mutable struct whose GC zero-init emits a wide store i64 0 over a Float32-then-4×Bool word," including:
- the exact
{ptr::Vector, x::Float32, b1::Bool, b2::Bool, b3::Bool, b4::Bool} layout;
- large (un-SROA-able) structs padded with
NTuple{32,Float64} and pointer / #undef-able Any fields (to force the zero-init memset);
- forcing the allocation to escape via a global
Ref{Any} sink;
- type-unstable / fully-generic (
::DataType) construction so it goes through the runtime jl_new_structv path.
All return the correct derivative — the isolated structs either get SROA'd (so no zeroing store exists) or Enzyme types the wide store fine. The real trigger seems to need the specific ~600-byte, deeply type-unstable ODEIntegrator construction that _ode_init produces, which I couldn't reproduce standalone. Happy to keep trying if a dependency-free repro is required — but filing with the minimal SciML-stack repro per maintainer request.
Versions
- Enzyme v0.13.173
- OrdinaryDiffEq v7.1.1 / OrdinaryDiffEqCore v4.5.0
- SciMLBase v3.30.1, DiffEqBase v7.6.0
- Julia 1.11.9
Context
This is the current (and, on that workload, last-known) blocker for SecondOrder(AutoEnzyme(Forward), AutoEnzyme(Reverse)) forward-over-reverse Hessians/HVPs through SciMLSensitivity + OrdinaryDiffEq, tracked in SciML/SciMLSensitivity.jl#1427 — following the now-resolved deepcopy/make_zero cascade (#3135 → #3137, #3213, #3246 → #3251, #3253 → #3254, #3258 → #3261). Note the failure is plain forward-mode (not the nesting): Enzyme forward-mode through OrdinaryDiffEq's init is broken for Float32 independent of the second-order question.
Summary
Forward-mode differentiation (with runtime activity) of a
Float32computation that constructs a large heterogeneous mutable struct fails type analysis on the struct's GC zero-initialization:The offending
store i64 0is the zero-init of a mutable-struct allocation whose fields include an 8-byte word laid out as aFloat32(bytes 0–3) immediately followed by fourBools (bytes 4–7). Enzyme's type analysis has already established[0:4)asFloatand[4:8)asIntegerfrom the individual field accesses, so the single wide 8-byte zeroingstore i64 0over that word can't be assigned one type.It is
Float32-specific: withFloat64the same field occupies its own 8-byte word (theBools move to the next word), so no zeroing store straddles a float/int boundary and forward mode succeeds.Reproduction
I was not able to reduce this to a dependency-free Enzyme-only MWE (see "Reduction attempts" below), so this uses OrdinaryDiffEq — but the repro is small (2 imports, plain forward mode, no ODE step is even taken — the failure is entirely in the integrator construction,
init):Result:
Float64control — works (correct derivative), same code withu0=[1.0],tspan=(0.0,1.0),p=[0.5]:Root cause (field-level)
ODEIntegratoris amutable structwith many fields, several of them pointers/heap references — so Julia zero-initializes the whole allocation (GC safety) before writing fields, via wide integer stores. For theFloat32problem the concrete layout contains this 8-byte word:last_event_errorFloat32accept_stepBoolisoutBoolreeval_fsalBoolderivative_discontinuityBoolThat word is exactly
{[0]:Float@float, [4]:Integer, [5]:Integer, [6]:Integer, [7]:Integer}— matching the error signature byte-for-byte. Thestore i64 0zeroing this word can't be typed as either float or int.With
Float64,last_event_erroris 8 bytes and 8-aligned, so it occupies its own word and the zeroing store over it is unambiguouslyFloat— hence forward mode succeeds.Reduction attempts (why no dependency-free MWE)
I tried ~20 Enzyme-only reductions of "forward-mode over a mutable struct whose GC zero-init emits a wide
store i64 0over aFloat32-then-4×Boolword," including:{ptr::Vector, x::Float32, b1::Bool, b2::Bool, b3::Bool, b4::Bool}layout;NTuple{32,Float64}and pointer /#undef-ableAnyfields (to force the zero-init memset);Ref{Any}sink;::DataType) construction so it goes through the runtimejl_new_structvpath.All return the correct derivative — the isolated structs either get SROA'd (so no zeroing store exists) or Enzyme types the wide store fine. The real trigger seems to need the specific ~600-byte, deeply type-unstable
ODEIntegratorconstruction that_ode_initproduces, which I couldn't reproduce standalone. Happy to keep trying if a dependency-free repro is required — but filing with the minimal SciML-stack repro per maintainer request.Versions
Context
This is the current (and, on that workload, last-known) blocker for
SecondOrder(AutoEnzyme(Forward), AutoEnzyme(Reverse))forward-over-reverse Hessians/HVPs through SciMLSensitivity + OrdinaryDiffEq, tracked in SciML/SciMLSensitivity.jl#1427 — following the now-resolveddeepcopy/make_zerocascade (#3135 → #3137, #3213, #3246 → #3251, #3253 → #3254, #3258 → #3261). Note the failure is plain forward-mode (not the nesting): Enzyme forward-mode through OrdinaryDiffEq'sinitis broken forFloat32independent of the second-order question.