Skip to content

Bug(Enzyme): jl_call/_compute_sparams in aug_forward inside ForwardDiff prepare_jacobian (two-arg) on Julia 1.12 #1020

@ChrisRackauckas-Claude

Description

@ChrisRackauckas-Claude

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions