Segfault in Enzyme.gradient with ComponentArray active input + non-const captured global (1.10 & 1.11)
## Summary
`Enzyme.gradient(Reverse, ...)` segfaults (SIGSEGV inside Julia's GC mark loop) when the closure being differentiated **captures a non-`const` global** *and* the active input is a **`ComponentArray`**. Making the captured global `const`, replacing the `ComponentArray` with a plain `Vector`, or — when called via DI — passing the global as a `DI.Constant`, each independently avoids the crash. The fault is hit during Enzyme's compile pipeline, before any AD actually happens.
Reproduces on both Julia 1.10.11 and 1.11.9 with `Enzyme v0.13.147`, so this is not 1.11-specific.
Likely related to #2396 (Lux + ComponentArrays segfault that you noted reproduces on 1.11 but not 1.10 — this one reproduces on both).
Surfaced via JuliaDiff/DifferentiationInterface.jl#1006.
## MWE
Four packages, ~10 lines:
```julia
using OrdinaryDiffEq, SciMLSensitivity, Enzyme, ComponentArrays
function lorenz!(du, u, p, t)
du[1] = p[1] * (u[2] - u[1])
du[2] = u[1] * (p[2] - u[3]) - u[2]
du[3] = u[1] * u[2] - p[3] * u[3]
return nothing
end
u0 = ComponentVector(x=1.0, y=0.0, z=0.0)
p_init = ComponentArray(sigma=5.0, rho=15.0, beta=1.0)
prob = ODEProblem(lorenz!, u0, (0.0, 10.0), p_init) # <-- non-const global
loss(p) = sum(abs2, Array(solve(prob, Tsit5(); p, saveat=0.1)))
Enzyme.gradient(Reverse, loss, p_init) # SIGSEGV
```
**Change exactly one keyword** — `prob = ...` → `const prob = ...` — and the same script runs cleanly and returns
```
g = ((sigma = -153.74620246430592, rho = 3059.416632297161, beta = 1013.443993470765),)
```
## What changes make the crash go away (any one suffices)
| Change | Outcome |
|---|---|
| `prob = ODEProblem(...)` → `const prob = ODEProblem(...)` | ✅ works |
| `p_init` → `Vector(p_init)` (plain `Vector{Float64}`) | ✅ works |
| Refactor `loss(p, prob, data)` and call via `DI.gradient(loss, backend, p_init, DI.Constant(prob), ...)` | ✅ works |
| `AutoEnzyme(; mode = Enzyme.set_runtime_activity(Enzyme.Reverse))` alone, still closing over non-`const` globals | ❌ still segfaults |
So `set_runtime_activity` is not enough; the trigger is genuinely the interaction between (a) capturing a non-`const` binding via the closure and (b) the active input being a `ComponentArray`.
## Stack traces (top frames)
### Julia 1.11.9
```
[3725614] signal 11 (1): Segmentation fault
in expression starting at .../slim3.jl:24
gc_mark_obj16 at src/gc.c:2145
gc_mark_outrefs at src/gc.c:2895 [inlined]
gc_mark_loop_serial_ at src/gc.c:2938
gc_mark_loop_serial at src/gc.c:2961
gc_mark_loop at src/gc.c:3143 [inlined]
_jl_gc_collect at src/gc.c:3532
ijl_gc_collect at src/gc.c:3893
maybe_collect at src/gc.c:926 [inlined]
jl_gc_pool_alloc_inner at src/gc.c:1319
jl_gc_alloc_ at src/julia_internal.h:523 [inlined]
_new_genericmemory_ at src/genericmemory.c:56 [inlined]
jl_alloc_genericmemory at src/genericmemory.c:99
ijl_array_grow_end at src/array.c:229
ijl_module_names at src/module.c:1002
unsorted_names at ./reflection.jl:96 [inlined]
make_typealias at ./show.jl:624
show_typealias at ./show.jl:805
_show_type at ./show.jl:970
show at ./show.jl:965
print at ./strings/io.jl:35
print_to_string at ./strings/io.jl:148
string at ./strings/io.jl:189 [inlined]
compile_unhooked at .../Enzyme/src/compiler.jl:5669
...
thunk_generator at .../Enzyme/src/compiler.jl:7181
...
loss at .../slim3.jl:21 [inlined]
augmented_julia_loss_4595_inner_1wrap at .../slim3.jl:0
gradient at .../Enzyme/src/sugar.jl:274
```
(The original `DI`-based version of this MWE hits the same fault but a few frames earlier — in `sroa_pass! → IncrementalCompact → primal_return_type_generator` — same root pattern: GC mark loop during an Enzyme-compile-pipeline allocation.)
### Julia 1.10.11
```
[3944234] signal (11.1): Segmentation fault
in expression starting at .../slim3.jl:24
gc_mark_outrefs at src/gc.c:2510 [inlined]
gc_mark_loop_serial_ at src/gc.c:2690
gc_mark_loop_serial at src/gc.c:2713
gc_mark_loop at src/gc.c:2894 [inlined]
_jl_gc_collect at src/gc.c:3227
ijl_gc_collect at src/gc.c:3524
maybe_collect at src/gc.c:937 [inlined]
jl_gc_pool_alloc_inner at src/gc.c:1293 [inlined]
ijl_gc_pool_alloc at src/gc.c:1341
jfptr_GetElementPtrInst_7809 at .../LLVM/...so
Instruction at .../LLVM/src/core/instructions.jl:38
check_ir! at .../Enzyme/src/compiler/validation.jl:243
check_ir! at .../Enzyme/src/compiler/validation.jl:216
check_ir at .../Enzyme/src/compiler/validation.jl:185 [inlined]
compile_unhooked at .../Enzyme/src/compiler.jl:5298
...
thunk_generator at .../Enzyme/src/compiler.jl:7181
...
gradient at .../Enzyme/src/sugar.jl:274
```
## Environment
```
Julia 1.11.9 (and 1.10.11) – Linux x86_64
Enzyme v0.13.147
ComponentArrays v0.15.39
OrdinaryDiffEq v7.0.0
SciMLSensitivity v7.109.0
```
I can run further experiments if a more specific narrowing would help (e.g. trying to drop OrdinaryDiffEq entirely, or pinning specific Enzyme versions to bisect).
1 条评论