Repository navigation
Description
Status update 2026-09-30. This is a performance item, not a correctness one: gradients are already exact via the tape (see below). The upstream blocker still needs re-verification. The tracker named below, EnzymeAD/Enzyme.jl#2707, was closed as completed on 2026-02-01, before this issue was filed. No Manifest is committed any more; the test suite is currently on Enzyme 0.13.204 (the version cited in the 1.13 skip comments at
test/autodiff.jl:89,198andtest/enzyme_rules.jl:133), and the current release is 0.13.208. CI now runs Julia 1.10 and 1.13 only (1.12 dropped in d989b79), so the 1.12CallingConventionMismatchErrorbelow is never exercised in CI. So "blocked" is unverified: re-run the mixed pointer/isbits rule-argument MWE on current Enzyme under Julia 1.12 and 1.13 before treating this as blocked. If it still fails, EnzymeAD/Enzyme.jl#3322 (CallingConventionMismatchErrorinclassify_argumentsfor a mixed pointer/isbits struct, still open; its title scopes it to nested differentiation on Julia ≤ 1.11) is the closest live upstream issue to point at instead. Scope is Enzyme only: the Mooncakerrule!!half of §6 is out, since Mooncake rule parity (#44) was closed as not planned; Mooncake stays a test-time CPU cross-check.
DESIGN.md §6 specifies custom rules that route the reverse-mode pullback of a linear operator through its already-declared apply_adjoint!, instead of taping the leaf bodies. That is still the right design, and it is still unbuilt — ext/MatrixFreeOperatorsEnzymeCoreExt.jl registers rules only for the _exchange_storage!/_bc_storage! seams and their transposes — but the blocker turned out to be upstream, not us.
Why it is blocked
From Julia 1.12, an Enzyme custom-rule argument may not have a type that mixes GC-tracked pointers with inline floats. It is a hard error:
CallingConventionMismatchError: ... has mismatch between roots_activep (DFT_DUP_ARG) and activep (DFT_CONSTANT).
- Non-rooted types: (Int64, ..., Float64, Float64, ...)
- Rooted types: (Vector{Matrix{Float64}}, ..., Base.RefValue{...})
Hint: You are currently on Julia 1.12, which changed its calling convention.
Tracking issue: EnzymeAD/Enzyme.jl#2707
Field, BlockField, PackedBlockField and BlockForest all qualify, through the grid they embed. That disqualifies every seam the §6 plan named:
| Intended seam | Blocked because |
|---|---|
apply(L, x) |
takes and returns a Field — cannot be worked around by re-parameterising arguments |
apply!(y, L, x, g, α, β) |
Field arguments |
mul!(y, ::PreparedOperator, x, α, β) |
PreparedOperator argument |
_forest_sweep! / _forest_adjoint_sweep! |
AbstractBlockField arguments |
The halo/BC sweeps were reachable only because they could be re-cut against raw storage (_exchange_storage! / _bc_storage! over a block vector or packed array plus isbits descriptors) — see _storage/_layout/BlockLayout. apply cannot be re-cut that way while it returns a field.
What we lose meanwhile
Not correctness — the tape still produces the right gradients, verified against the declared adjoint throughout test/autodiff.jl (at rtol=1e-10 on single grids, 1e-8 on forests). What we lose is the efficiency win of skipping the leaf tape entirely, and the structural guarantee for @kernel-authored leaves that §6 calls mandatory (the exchange sweeps now have it; the operator sweeps do not).
When to revisit
Once the MWE above passes on current Enzyme (i.e. once Enzyme.jl#2707's fix is confirmed to cover this case). The scoping work is already done and should be reused:
- gate on operator type, one rule per parameter-free linear leaf (
Laplacian,Derivative,Gradient,Divergence,IdentityOp,Restriction,Prolongation) — a rule cannot decline and fall back to taping, so a trait branch inside the rule body is not an option; - deliberately exclude
ScalingOpandAdvection{_,<:AbstractField}, whose coefficient fields are differentiable parameters — a rule firing there would silently zerodκ(test/autodiff.jlguards this, but itsScalingOpκ testset is skipped on Julia 1.13, so in CI only the 1.10 job runs it until test(ad): re-enable Enzyme gradient tests skipped on Julia 1.13 #108 re-enables it); - accumulate with
apply_adjoint!(x̄, L, ȳ, g, α, true), and keep ghost cotangents exactly zero; - rule bodies must not allocate (see fix(ad): Enzyme rule on single-grid apply_bc! segfaults on Linux x86_64 #40).
—
🤖 Claude brought this status note forward from the Julia 1.12 era.
claude-opus-5-5[1m] (high) · claude-code 2.1.285 · unreviewed
Activity
[-]feat(ad): linear-operator pullback rules (DESIGN §6) — deferred, blocked on Enzyme.jl#2707[/-][+]feat(ad): linear-operator pullback rules (DESIGN §6) — re-verify against current Enzyme[/+]