Skip to content

Bug(GTPSA): gradient/jacobian/hessian/hvp fail and pushforward returns wrong values when f does not depend on x #1015

@bdrhill

Description

@bdrhill

Description

When f does not propagate the input through GTPSA's TPS (e.g. f(x) = 42.0, or any function whose evaluation on a TPS input returns a plain Float64), the AutoGTPSA extension produces incorrect output. Two symptoms are visible, both rooted in the extension assuming f(xt) is always a TPS:

  • gradient, jacobian, hessian, hvp, second_derivative, value_and_gradient raise MethodError.
  • pushforward and value_and_pushforward return the function value as the pushforward, instead of zero.

Native GTPSA.gradient!/jacobian! also error when passed a plain Number/Vector{<:Number} (the wrapping into a TPS is something the DI extension would need to do).

MWE

using DifferentiationInterface
using GTPSA: GTPSA

backend = AutoGTPSA()
x = [1.0, 2.0]
v = [0.5, 0.7]

fc(x) = 42.0          # scalar-output, no dependence on x
fv(x) = [42.0, 7.0]   # vector-output, no dependence on x

# 1. Errors --------------------------------------------------------------
gradient(fc, backend, x)        # MethodError
jacobian(fv, backend, x)        # MethodError
hessian(fc, backend, x)         # MethodError
hvp(fc, backend, x, (v,))       # MethodError
second_derivative(t -> 42.0, backend, 1.5)  # BoundsError

# 2. Silently wrong values -----------------------------------------------
pushforward(fc, backend, x, (v,))[1]            # returns 42.0, expected 0.0
pushforward(fv, backend, x, (v,))[1]            # returns [42.0, 7.0], expected [0.0, 0.0]
value_and_pushforward(fc, backend, x, (v,))     # returns (42.0, (42.0,)), expected (42.0, (0.0,))

Workaround: force TPS propagation, e.g. f(x) = 0*x[1] + 42.0. Then gradient returns [0.0, 0.0] and pushforward returns 0.0, as expected.

Stacktraces
gradient(fc, AutoGTPSA(), [1.0, 2.0])

ERROR: MethodError: no method matching gradient!(::Vector{Float64}, ::Float64; include_params::Bool, unsafe_inbounds::Bool)
The function `gradient!` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  gradient!(::Any, !Matched::GTPSA.TPS; include_params, unsafe_inbounds)
   @ GTPSA ~/.julia/packages/GTPSA/IhdRA/src/getset.jl:362

Stacktrace:
 [1] gradient(::Function, ::DifferentiationInterfaceGTPSAExt.GTPSAOneArgGradientPrep{...}, ::AutoGTPSA{Nothing}, ::Vector{Float64})
   @ DifferentiationInterfaceGTPSAExt ~/dev/DifferentiationInterface.jl/DifferentiationInterface/ext/DifferentiationInterfaceGTPSAExt/onearg.jl:145
 [2] gradient(::var"#fc#fc##0", ::AutoGTPSA{Nothing}, ::Vector{Float64})
   @ DifferentiationInterface ~/dev/DifferentiationInterface.jl/DifferentiationInterface/src/first_order/gradient.jl:63
jacobian(fv, AutoGTPSA(), [1.0, 2.0])

ERROR: MethodError: no method matching jacobian!(::Matrix{Float64}, ::Vector{Float64}; include_params::Bool, unsafe_inbounds::Bool)
The function `jacobian!` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  jacobian!(::Any, !Matched::AbstractArray{<:GTPSA.TPS}; include_params, unsafe_inbounds)
   @ GTPSA ~/.julia/packages/GTPSA/IhdRA/src/getset.jl:417
second_derivative(t -> 42.0, AutoGTPSA(), 1.5)

ERROR: BoundsError

Expected Behavior

For f that does not depend on x:

  • All differentiation operators return zeros (of the appropriate shape and type), and value_and_* returns (f(x), zeros).

Actual Behavior

Operator Behavior
gradient, value_and_gradient MethodError
jacobian MethodError
hessian MethodError
hvp MethodError
second_derivative BoundsError
pushforward (scalar output) returns f(x) instead of 0
pushforward (vector output) returns f(x) instead of zeros
value_and_pushforward tangent equals f(x) instead of zeros
derivative (scalar input) returns f(x) (also incorrect for the same reason)

Root Cause

In ext/DifferentiationInterfaceGTPSAExt/onearg.jl, the pushforward implementation does:

yt = fc(prep.xt)
if yt isa Number
    return yt[1]                  # if yt is Float64, yt[1] == yt
else
    dy = map(t -> t[1], yt)       # if eltype(yt) is Number, t[1] == t
    return dy
end

When f does not propagate the TPS, yt is a plain Float64 (or Vector{Float64}), and yt[1] returns the value rather than the first-order coefficient.

The same path is taken by gradient/jacobian/hessian which then pass the plain Float64/Vector{Float64} to GTPSA.gradient!/GTPSA.jacobian!, neither of which accepts non-TPS input.

A check on whether yt isa TPS (or eltype(yt) <: TPS) and returning zero(...) in the non-TPS case would match the behavior of other backends. ForwardDiff, for instance, returns [0.0, 0.0] for gradient(x -> 42.0, AutoForwardDiff(), [1.0, 2.0]).

Backend

  • Backend: AutoGTPSA()
  • Works with other backends: ForwardDiff, Enzyme, FiniteDiff all return zeros for the same inputs
  • Native API behavior: GTPSA.gradient!(g, fc(xt)) errors with the same MethodError (fc(xt) produces a Float64 rather than a TPS). Wrapping in a TPS first (y_tps = TPS{Float64}(use=d); y_tps[0] = fc(xt)) then calling GTPSA.gradient!(g, y_tps) returns [0.0, 0.0].

This is the same class of issue as #1013 (AutoHyperHessians), but for AutoGTPSA it also affects first-order operators and produces silently-wrong values from pushforward.

Environment

  • Julia 1.12.5
  • DifferentiationInterface v0.7.18 (this branch)
  • GTPSA v1.5.3
  • ADTypes v1.22.0
Full environment
julia> using Pkg; Pkg.status()
Status `~/dev/DifferentiationInterface.jl/DifferentiationInterface/test/Back/GTPSA/Project.toml`
  [47edcb42] ADTypes v1.22.0
  [a0c0ee7d] DifferentiationInterface v0.7.18 `../../..`
  [a82114a7] DifferentiationInterfaceTest v0.11.0 `../../../../DifferentiationInterfaceTest`
  [7d51a73a] ExplicitImports v1.15.0
  [f6369f11] ForwardDiff v1.3.3
  [b27dd330] GTPSA v1.5.3
  [c3a54625] JET v0.11.3
  [9f842d2f] SparseConnectivityTracer v1.2.1
  [0a514795] SparseMatrixColorings v0.4.27
  [8dfed614] Test v1.11.0

julia> versioninfo()
Julia Version 1.12.5
Commit 5fe89b8ddc1 (2026-02-09 16:05 UTC)
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 4 × Intel(R) Core(TM) i5-2520M CPU @ 2.50GHz
  WORD_SIZE: 64
  LLVM: libLLVM-18.1.7 (ORCJIT, sandybridge)
Threads: 1 default, 1 interactive, 1 GC (on 4 virtual cores)

🤖 I am a robot. This is an experiment in agentic bug-catching under the supervision of @adrhill and @gdalle (#1008). Contents may be hallucinated.

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