Skip to content

Propagate set_runtime_activity through FWW Enzyme forward rules#45

Merged
ChrisRackauckas merged 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:enzyme-forward-rule
Apr 23, 2026
Merged

Propagate set_runtime_activity through FWW Enzyme forward rules#45
ChrisRackauckas merged 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:enzyme-forward-rule

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor

Summary

The existing FunctionWrappersWrappersEnzymeExt forward-mode rules
hard-coded plain Forward when delegating to Enzyme.autodiff,
silently dropping the outer caller's set_runtime_activity(Forward)
(and set_strong_zero) flags. Enzyme's static IR-level activity
analysis can't see through the cfunction indirection inside
FunctionWrappersWrapper, so the inner Enzyme.autodiff call then
raised EnzymeRuntimeActivityError at
broadcast_unaliasmightalias inside @. du = … — even though the
user had explicitly set runtime activity on the outer call.

Motivation

Confirmed reproducer from SciML/OrdinaryDiffEq.jl#3518:

Rosenbrock23(autodiff = AutoEnzyme(mode = set_runtime_activity(Enzyme.Forward)))

on any time-dependent in-place ODE RHS that DiffEqBase's
AutoSpecialize wraps via wrapfun_iip (a 4-arg
FunctionWrappersWrapper over (du, u, p, t) returning Nothing).
Dropping the runtime-activity flag through the extension's delegation
broke that end-to-end path.

Bare-f! vs wrapped-f! reproducer matrix (without this fix):

  • Bare f!(du, u, p, t) = @. du = p*u, set_runtime_activity(Forward) → OK, ddu == 0.
  • Same f! wrapped in FunctionWrappersWrapper, set_runtime_activity(Forward)EnzymeRuntimeActivityError.

Mechanism / fix

EnzymeRules.FwdConfig{NeedsPrimal, NeedsShadow, Width, RuntimeActivity, StrongZero}
carries the caller's ForwardMode flags as type parameters. The updated
rules extract RuntimeActivity and StrongZero and rebuild the
ForwardMode used for the delegated Enzyme.autodiff call via
Enzyme.set_runtime_activity / Enzyme.set_strong_zero. The reverse
rules' internal forward-mode helpers are updated analogously using
EnzymeRules.runtime_activity(config) /
EnzymeRules.strong_zero(config) accessors on RevConfig.

Numerical check

The added test set (test/enzyme_tests.jl) exercises the DiffEqBase
wrapfun_iip shape end-to-end:

  • f!(du, u, p, t) = @. du = p*u (∂/∂t = 0) — wrapped + set_runtime_activity(Forward)ddu ≈ 0, matches ForwardDiff baseline.
  • g!(du, u, p, t) = @. du = sin(t)*u — wrapped + set_runtime_activity(Forward)ddu ≈ cos(t) .* u, matches ForwardDiff baseline exactly.
  • h!(du, u, p, t) = (du[1] = u[1] * t) under set_strong_zero(Forward) → correct tangent, confirms the strong-zero flag is also propagated.

Unrelated test status: the pre-existing "Enzyme batch forward mode
(width > 1)" test errors on this Julia/Enzyme stack before and after
this change — it fails the shadow_result[1]::NTuple{W, T} typeassert
because Enzyme now returns a @NamedTuple for BatchDuplicated. Out
of scope for this PR.

Unblocks

Once a release with this fix is cut, Rosenbrock23(autodiff = AutoEnzyme(mode = set_runtime_activity(Enzyme.Forward))) (and the
same for other Rosenbrock / W-methods) works on any in-place ODE RHS
routed through DiffEqBase's AutoSpecialize / wrapfun_iip.

Test plan

  • FWW test suite passes for all the existing Enzyme tests.
  • New test set passes end-to-end.
  • CI green.

🤖 Generated with Claude Code

The forward-mode rules hard-coded plain `Forward` in their delegated
`Enzyme.autodiff` calls, silently dropping the outer caller's
`set_runtime_activity(Forward)` / `set_strong_zero(Forward)` flags.

Enzyme's IR-level activity analysis can't see through
FunctionWrappersWrapper's cfunction indirection, so the inner call
raised `EnzymeRuntimeActivityError` at `broadcast_unalias` / `mightalias`
inside `@. du = …` — even when the user had explicitly set runtime
activity on the outer `autodiff` call.

Reproducer (confirmed): `Rosenbrock23(autodiff =
AutoEnzyme(set_runtime_activity(Enzyme.Forward)))` on any time-dependent
in-place ODE RHS that DiffEqBase's `AutoSpecialize` wraps with
`wrapfun_iip`. See OrdinaryDiffEq.jl PR #3518.

Fix: extract `RuntimeActivity` and `StrongZero` from `FwdConfig` /
`RevConfig` type parameters and rebuild the `ForwardMode` used in the
delegated `Enzyme.autodiff` call with those flags set. The reverse
rules' internal forward-mode helpers are updated analogously.

Also adds a test exercising the exact DiffEqBase `wrapfun_iip` shape
— a 4-arg `(du, u, p, t)` IIP RHS with `@.` broadcast — under
`set_runtime_activity(Forward)`, and a `set_strong_zero(Forward)` case.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Comment thread ext/FunctionWrappersWrappersEnzymeExt.jl Outdated
@ChrisRackauckas ChrisRackauckas merged commit 3ea3778 into SciML:main Apr 23, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants