JuliaDiff / JuliaDiff/DifferentiationInterface.jl

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

Open
#1,015 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bot
Dominant language
Julia
Stars
313
Forks
35
PR merge metrics
No merged PRs in 30d

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in ext/DifferentiationInterfaceGTPSAExt/onearg.jl and run the issue's MWE with AutoGTPSA() to reproduce the failures and incorrect pushforward values. Check the paths that consume plain Number or array results, then add regression coverage showing that every listed differentiation operator returns appropriately shaped zeros while value_and_* preserves f(x).

Written by the indexing model from the issue text.

Assessment

Tech stack
julia
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.