JuliaDiff / JuliaDiff/DifferentiationInterface.jl
Bug(GTPSA): gradient/jacobian/hessian/hvp fail and pushforward returns wrong values when f does not depend on x
Nobody has claimed this yet.
- 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_gradientraiseMethodError.pushforwardandvalue_and_pushforwardreturn 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 sameMethodError(fc(xt)produces aFloat64rather than aTPS). Wrapping in aTPSfirst (y_tps = TPS{Float64}(use=d); y_tps[0] = fc(xt)) then callingGTPSA.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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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