EnzymeAD / EnzymeAD/Enzyme.jl

Julia 1.11: forward-over-reverse dynamic loop fails in `nodecayed_phis!` GC analysis

Open
#3,329 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Julia
Stars
586
Forks
108
Avg merge
1d 5h
Merged PRs (30d)
44

Description

## Summary

On Julia 1.11, forward-over-reverse differentiation of a small Enzyme-only dynamic loop fails in `nodecayed_phis!` while analyzing garbage-collection behavior. The inner runtime-activity reverse gradient succeeds; only the outer runtime-activity forward pass fails.

With verbose errors enabled, the key diagnostic is:

```text
Could not analyze garbage collection behavior of
inst: %85 = phi {} addrspace(10)* addrspace(13)* ...
...
EnzymeInternalError: Enzyme compilation failed due to an internal error
getparent(...) @ Enzyme.Compiler/src/llvm/transforms.jl:1228
nodecayed_phis! @ Enzyme.Compiler/src/llvm/transforms.jl:1236
optimize! @ Enzyme.Compiler/src/compiler/optimize.jl:223
```

The same file succeeds on Julia 1.10 and 1.12. This is a minimized version of the dynamic mock-solver reduction suggested in #3299 while isolating the still-failing direct-through-solver forward-over-reverse path.

## Reproducer

```julia
import Enzyme

Enzyme.Compiler.VERBOSE_ERRORS[] = true

struct Problem
f::Any
jac::Any
u0::Vector{Float64}
p::Vector{Float64}
end

@noinline function solve(prob::Problem)
u = copy(prob.u0)
p = prob.p
f = prob.f
jac = prob.jac

for _ in 1:2
k1 = f(u[1], p[1])
u[1] += 0.1 * k1

jval = if jac !== nothing
Base.invokelatest(jac, u[1], p[1])::Float64
else
0.0
end
u[1] += 0.01 * jval
end
return u[1]
end

function loss(p)
f = (x, pval) -> -pval * x
y = solve(Problem(f, nothing, [1.0], p))
return y * y
end

const RA_R = Enzyme.set_runtime_activity(Enzyme.Reverse)
const RA_F = Enzyme.set_runtime_activity(Enzyme.Forward)

@noinline function grad(p)
dp = zero(p)
Enzyme.autodiff(RA_R, Enzyme.Const(loss), Enzyme.Active, Enzyme.Duplicated(p, dp))
return dp
end

@show grad([0.5])
@show Enzyme.autodiff(
RA_F,
Enzyme.Const(p -> sum(grad(p))),
Enzyme.Duplicated([0.5], [1.0]),
)
```

## Observed on Julia 1.11.9

The inner reverse succeeds:

```text
grad([0.5]) = [-0.34295]
```

The outer forward-over-reverse call then fails:

```text
ERROR: LoadError: EnzymeInternalError: Enzyme compilation failed due to an internal error.
getparent(...) @ Enzyme.Compiler/src/llvm/transforms.jl:1228
nodecayed_phis! @ Enzyme.Compiler/src/llvm/transforms.jl:1236
optimize! @ Enzyme.Compiler/src/compiler/optimize.jl:223
```

## Version matrix

Tested with Enzyme v0.13.182 / Enzyme_jll v0.0.282+0:

| Julia | Result |
|---|---|
| 1.10.11 | succeeds, outer result `(0.10830000000000001,)` |
| 1.11.9 | fails in `nodecayed_phis!` as above |
| 1.12.6 | succeeds, outer result `(0.10830000000000001,)` |

The failure also reproduces on Enzyme `main` at `23ff63088cd6532ea0d58bcdc279573bec6e2731` with Julia 1.11.9.

## Ablations

- One loop iteration succeeds on Julia 1.11; two are sufficient to fail.
- Removing the inactive `jac::Any` branch succeeds.
- Removing the `u0::Vector{Float64}` field and allocating `u` directly in `solve` succeeds.
- Julia 1.10 and 1.12 succeed without code changes.

## Context

The full `SensitivityADPassThrough` reproducer in #3299 still fails on the current registered stack (Enzyme 0.13.182, DiffEqBase 7.6.1, OrdinaryDiffEq 7.1.2, Julia 1.11.9) with the broken-module LLVM verifier error after its inner reverse gradient succeeds. The Enzyme-only reduction above exposes this smaller Julia-1.11-specific GC-analysis failure without SciML dependencies.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.