JuliaDiff / JuliaDiff/ForwardDiff.jl
hessian: the two nested dual layers share a tag, so results depend on the code path and chunk size
Nobody has claimed this yet.
- Dominant language
- Julia
- Stars
- 1k
- Forks
- 160
- PR merge metrics
- No merged PRs in 30d
Description
ForwardDiff.hessian evaluates f on nested duals that carry the same tag at both layers, Dual{T,Dual{T,V,N},N} — HessianConfig passes one tag = Tag(f, V) to both of its inner configs (src/config.jl#L221-L227) and Base.eltype reflects that (src/config.jl#L256).
The two layers are therefore type-indistinguishable, so ForwardDiff.value / ForwardDiff.partials called inside f cannot tell them apart, and the tag-ordering machinery (≺, DualMismatchError) has nothing to compare. Which layer gets peeled then depends on how a given code path happens to nest its evaluations.
1. One function, three Hessians and two gradients
The layer is dropped from an intermediate here: ForwardDiff.value(z[1]) is order 1, while f's return value is a full order-2 Dual{T,Dual{T,V,N},N}. Extraction therefore has both layers to read and nothing about the result type looks wrong — the damage is that the one-layer intermediate is re-absorbed at the inner layer on its way back up, the shared tag making the two indistinguishable.
using ForwardDiff, StaticArrays, DiffResults
f(z) = sum(abs2, z) + ForwardDiff.value(z[1]) * z[2]
x = [1.0, 2.0, 3.0]
sx = SVector(1.0, 2.0, 3.0)
ForwardDiff.hessian(f, x) # H[1,2]=1.0 H[2,1]=0.0
ForwardDiff.hessian(f, x, ForwardDiff.HessianConfig(f, x, ForwardDiff.Chunk{1}())) # H[1,2]=0.0 H[2,1]=0.0
ForwardDiff.hessian(f, sx) # H[1,2]=0.0 H[2,1]=1.0
DiffResults.gradient(ForwardDiff.hessian!(DiffResults.HessianResult(x), f, x)) # [4.0, 5.0, 6.0]
DiffResults.gradient(ForwardDiff.hessian!(DiffResults.HessianResult(sx), f, sx)) # [2.0, 5.0, 6.0]
Three Hessians and two gradients for the same function, varying with the chunk size, the array type and the result type.
2. A silently zeroed gradient
Here the output itself is order 1, so extraction has no outer layer to read, and the surviving first-order partials are discarded rather than used:
g(z) = ForwardDiff.value(sum(abs2, z))
DiffResults.gradient(ForwardDiff.hessian!(DiffResults.HessianResult(x), g, x)) # [2.0, 4.0, 6.0]
DiffResults.gradient(ForwardDiff.hessian!(DiffResults.HessianResult(sx), g, sx)) # [0.0, 0.0, 0.0]
ForwardDiff.gradient(z -> sum(abs2, z), x) # [2.0, 4.0, 6.0]
hessian!(::ImmutableDiffResult, f, ::StaticArray) builds dualize(T, dualize(T, x)) and extracts with nested value(T, ·). Because both layers share T, value(T, ·) peels a lone Dual{T,V,N} down to a plain Float64, and the following partials(T, ·, i) hits the zero(x) fallback. The zero Hessian is correct — no second-order information survived — but the gradient should be [2.0, 4.0, 6.0].
Cause and suggested fix
The two layers need distinct tags: an inner T = Tag(f, V) and an outer TO derived from it, so that T ≺ TO. Then value(TO, ::Dual{T}) correctly leaves an inner-layer value alone, and partials(TO, ::Dual{T}) is zero. Both cases above come out right with no special-casing, and the result stops depending on the chunk size.
The paths that already use two tags give the consistent answer today: hessian(f, ::StaticArray) goes through jacobian(Base.Fix1(gradient, f), x), which builds a fresh tag for each level.
Version: ForwardDiff master (v1.4.5), Julia 1.12.7, StaticArrays 1.9.19.
Contributor guide
No contributing guide indexed for this repository
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 src/config.jl#L221-L227 and #L256, then trace HessianConfig, hessian!, dualize, value, and partials through the examples in the issue. Reproduce the chunk-size, array-type, and result-type differences, then verify that nested layers remain distinguishable and the reported Hessians and gradients are consistent.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- julia
- Domain
- data
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 64/100