Summary
When Enzyme reverse-mode tries to differentiate through a function that internally calls DI.prepare_jacobian(f!, y, AutoForwardDiff(), x, Constant(p)) (the two-arg ForwardDiff path), it fails with
Enzyme: jl_call calling convention not implemented in aug_forward
for %jl_f__compute_sparams_ret = call ...
@julia.call(@jl_f__compute_sparams,
null,
@"ejl_inserted$_DifferentiationInterfaceForwardDiffExt_dual_type_*",
@"ejl_inserted$jl_global_*",
...)
inside prepare_jacobian_nokwarg at ext/DifferentiationInterfaceForwardDiffExt/twoarg.jl:381 (line 388: contexts_dual = translate_toprep(dual_type(config), contexts)).
The triggering site is the JacobianConfig overload of dual_type in utils.jl:
# ext/DifferentiationInterfaceForwardDiffExt/utils.jl, lines 29-32
dual_type(config::DerivativeConfig) = eltype(config.duals)
dual_type(config::GradientConfig) = eltype(config.duals)
dual_type(config::JacobianConfig{T, V, N}) where {T, V, N} = Dual{T, V, N} # <-- this one
dual_type(config::HessianConfig) = dual_type(config.gradient_config)
The JacobianConfig{T, V, N} form pattern-matches the parametric type. On Julia 1.12, the resulting LLVM IR emits a runtime jl_f__compute_sparams call (via Julia's generic jl_call calling convention) to materialize the type parameters. Enzyme's aug_forward pass has no handler for that specific jl_call, and it errors out.
The other three overloads use eltype(config.duals) and do not trigger this codegen.
MWE
This reproduces fully isolated (no MTK, no RGF needed — just NonlinearSolve calling DI):
using NonlinearSolve, Enzyme
using SciMLBase: NonlinearProblem, remake
function f_plain!(du, u, p)
du[1] = u[1] - p[1] + p[2]
return nothing
end
prob = NonlinearProblem(f_plain!, [0.0], [2.0, 1.0])
loss(p) = sum(solve(remake(prob; p = p), NewtonRaphson()).u)
Enzyme.gradient(
Enzyme.set_runtime_activity(Enzyme.Reverse),
Enzyme.Const(loss),
[2.0, 1.0],
)
# EnzymeRuntimeException: Enzyme: jl_call calling convention not implemented in aug_forward
# at prepare_jacobian_nokwarg(...)
# @ DifferentiationInterfaceForwardDiffExt .../ext/.../twoarg.jl:381
Versions:
- Julia 1.12.6
- DifferentiationInterface 0.7.18
- ForwardDiff 1.3.3
- Enzyme 0.13.148
- NonlinearSolve 4.19.1
Setting NewtonRaphson() explicitly is just to avoid the polyalgorithm Union; Const(loss) + set_runtime_activity(Reverse) are the documented Enzyme annotations for a closure capturing a mutable NonlinearProblem.
Proposed fix (one line)
Use the same nested-eltype shape the other dispatches already use:
# ext/DifferentiationInterfaceForwardDiffExt/utils.jl
dual_type(config::JacobianConfig) = eltype(eltype(config.duals))
For a JacobianConfig{T, V, N}, eltype(config.duals) is Vector{Dual{T, V, N}} (a tuple of two such vectors) and eltype of that is Dual{T, V, N} — same result, different (Enzyme-tractable) codegen.
Verification
With a runtime monkey-patch of exactly the above one line:
import ForwardDiff, DifferentiationInterface as DI
DIExtMod = Base.get_extension(DI, :DifferentiationInterfaceForwardDiffExt)
Core.eval(DIExtMod,
:(dual_type(config::ForwardDiff.JacobianConfig) = eltype(eltype(config.duals))))
the _compute_sparams / jl_call error in prepare_jacobian_nokwarg disappears. The original MWE then advances four steps further into the stack before hitting an unrelated downstream Enzyme/LinearSolve issue (NonConstantKeywordArgException in LinearSolve.init) — confirming this issue is exactly localized to the dual_type(::JacobianConfig) codegen pattern on Julia 1.12.
Context
This is the upstream layer of SciML/SciMLSensitivity.jl#1359, which tracks Enzyme reverse-mode through solve(remake(prob; p), NewtonRaphson()) for MTK-built problems. After four layers of user-side and library-side fixes (closing the closure with Const, enabling set_runtime_activity, passing an explicit alg, fixing the MTK supports_initialization mutation), this DI ForwardDiff dual_type codegen pattern is the next blocker.
Happy to open a PR if the proposed one-liner looks right.
Summary
When Enzyme reverse-mode tries to differentiate through a function that internally calls
DI.prepare_jacobian(f!, y, AutoForwardDiff(), x, Constant(p))(the two-arg ForwardDiff path), it fails withinside
prepare_jacobian_nokwargatext/DifferentiationInterfaceForwardDiffExt/twoarg.jl:381(line 388:contexts_dual = translate_toprep(dual_type(config), contexts)).The triggering site is the
JacobianConfigoverload ofdual_typeinutils.jl:The
JacobianConfig{T, V, N}form pattern-matches the parametric type. On Julia 1.12, the resulting LLVM IR emits a runtimejl_f__compute_sparamscall (via Julia's genericjl_callcalling convention) to materialize the type parameters. Enzyme'saug_forwardpass has no handler for that specificjl_call, and it errors out.The other three overloads use
eltype(config.duals)and do not trigger this codegen.MWE
This reproduces fully isolated (no MTK, no RGF needed — just NonlinearSolve calling DI):
Versions:
Setting
NewtonRaphson()explicitly is just to avoid the polyalgorithm Union;Const(loss)+set_runtime_activity(Reverse)are the documented Enzyme annotations for a closure capturing a mutableNonlinearProblem.Proposed fix (one line)
Use the same nested-eltype shape the other dispatches already use:
For a
JacobianConfig{T, V, N},eltype(config.duals)isVector{Dual{T, V, N}}(a tuple of two such vectors) andeltypeof that isDual{T, V, N}— same result, different (Enzyme-tractable) codegen.Verification
With a runtime monkey-patch of exactly the above one line:
the
_compute_sparams/jl_callerror inprepare_jacobian_nokwargdisappears. The original MWE then advances four steps further into the stack before hitting an unrelated downstream Enzyme/LinearSolve issue (NonConstantKeywordArgExceptioninLinearSolve.init) — confirming this issue is exactly localized to thedual_type(::JacobianConfig)codegen pattern on Julia 1.12.Context
This is the upstream layer of SciML/SciMLSensitivity.jl#1359, which tracks Enzyme reverse-mode through
solve(remake(prob; p), NewtonRaphson())for MTK-built problems. After four layers of user-side and library-side fixes (closing the closure withConst, enablingset_runtime_activity, passing an explicit alg, fixing the MTKsupports_initializationmutation), this DI ForwardDiffdual_typecodegen pattern is the next blocker.Happy to open a PR if the proposed one-liner looks right.