JuliaDiff / JuliaDiff/ForwardDiff.jl

hessian: the two nested dual layers share a tag, so results depend on the code path and chunk size

Open
#845 0 comments 0 reactions 0 assignees View on GitHub

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

  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 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.