Skip to content

Commit 0c04e90

Browse files
committed
Address symmetric Hessian review feedback
1 parent 0091edd commit 0c04e90

9 files changed

Lines changed: 192 additions & 84 deletions

File tree

‎ext/ForwardDiffStaticArraysExt.jl‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ using ForwardDiff: Dual, partials, npartials, Partials, GradientConfig, Jacobian
77
gradient, hessian, jacobian, gradient!, hessian!, jacobian!,
88
extract_gradient!, extract_jacobian!, extract_value!,
99
vector_mode_gradient, vector_mode_gradient!,
10-
vector_mode_jacobian, vector_mode_jacobian!, valtype, value
10+
vector_mode_jacobian, vector_mode_jacobian!, HESSIAN_ERROR, valtype, value
1111
using DiffResults: DiffResult, ImmutableDiffResult, MutableDiffResult
1212

1313
@generatedfunctiondualize(::Type{T}, x::StaticArray) where T
@@ -107,11 +107,34 @@ end
107107
end
108108

109109
# Hessian
110-
ForwardDiff.hessian(f::F, x::StaticArray) where {F} =jacobian(Base.Fix1(gradient, f), x)
110+
@inlinefunctionextract_hessian(::Type{T}, ydual::Partials, x::StaticArray) where {T}
111+
H =extract_jacobian(T, ydual, x)
112+
returntypeof(H)(Symmetric(H, :U))
113+
end
114+
115+
@inlinefunctionextract_hessian(::Type{T}, ydual::Partials{0}, x::S) where {T,S<:StaticArray}
116+
R = StaticArrays.similar_type(S, valtype(T, eltype(ydual)), Size(length(x), length(x)))
117+
returnzero(R)
118+
end
119+
120+
@inlinefunction ForwardDiff.hessian(f::F, x::StaticArray) where {F}
121+
T =typeof(Tag(f, eltype(x)))
122+
ydual =f(dualize(T, dualize(T, x)))
123+
ydual isa Real ||throw(HESSIAN_ERROR)
124+
returnextract_hessian(T, partials(T, ydual), x)
125+
end
126+
111127
ForwardDiff.hessian(f::F, x::StaticArray, cfg::HessianConfig) where {F} =hessian(f, x)
112128
ForwardDiff.hessian(f::F, x::StaticArray, cfg::HessianConfig, ::Val) where {F} =hessian(f, x)
113129

114-
ForwardDiff.hessian!(result::AbstractArray, f::F, x::StaticArray) where {F} =jacobian!(result, Base.Fix1(gradient, f), x)
130+
@inlinefunction ForwardDiff.hessian!(result::AbstractArray, f::F, x::StaticArray) where {F}
131+
T =typeof(Tag(f, eltype(x)))
132+
ydual =f(dualize(T, dualize(T, x)))
133+
ydual isa Real ||throw(HESSIAN_ERROR)
134+
H = result isa AbstractMatrix ? result :reshape(result, length(x), length(x))
135+
ForwardDiff.extract_hessian_chunk!(T, H, ydual, 0, 0, length(x), length(x))
136+
return result
137+
end
115138

116139
ForwardDiff.hessian!(result::MutableDiffResult, f::F, x::StaticArray) where {F} =hessian!(result, f, x, HessianConfig(f, result, x))
117140

@@ -123,9 +146,10 @@ function ForwardDiff.hessian!(result::ImmutableDiffResult, f::F, x::StaticArray)
123146
d1 =dualize(T, x)
124147
d2 =dualize(T, d1)
125148
fd2 =f(d2)
149+
fd2 isa Real ||throw(HESSIAN_ERROR)
126150
val =value(T,value(T,fd2))
127151
grad =extract_gradient(T,value(T,fd2), x)
128-
hess =extract_jacobian(T,partials(T,fd2), x)
152+
hess =extract_hessian(T,partials(T,fd2), x)
129153
result = DiffResults.hessian!(result, hess)
130154
result = DiffResults.gradient!(result, grad)
131155
result = DiffResults.value!(result, val)

‎src/apiutils.jl‎

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,22 @@ end
8888

8989
function_seed_zero_partials!(duals::AbstractArray{Dual{T,V,N}}, x, idxs) where {T,V,N}
9090
seed =zero(Partials{N,V})
91+
return_seed!(duals, x, idxs) do value, _
92+
Dual{T,V,N}(value, seed)
93+
end
94+
end
95+
96+
# Write a sequence of duals while preserving unassigned entries in arrays whose element type is not
97+
# stored inline. `make_dual` receives the primal value and its one-based position in `idxs`.
98+
@inlinefunction_seed!(make_dual::F, duals::AbstractArray{Dual{T,V,N}}, x, idxs) where {F,T,V,N}
9199
ifisbitstype(V)
92-
for idx in idxs
93-
duals[idx] =Dual{T,V,N}(x[idx], seed)
100+
for(i, idx)inenumerate(idxs)
101+
duals[idx] =make_dual(x[idx], i)
94102
end
95103
else
96-
for idx in idxs
104+
for(i, idx)inenumerate(idxs)
97105
ifisassigned(x, idx)
98-
duals[idx] =Dual{T,V,N}(x[idx], seed)
106+
duals[idx] =make_dual(x[idx], i)
99107
else
100108
Base._unsetindex!(duals, idx)
101109
end
@@ -106,38 +114,31 @@ end
106114

107115
functionseed!(duals::AbstractArray{Dual{T,V,N}}, x,
108116
seeds::NTuple{N,Partials{N,V}}) where {T,V,N}
109-
ifisbitstype(V)
110-
for (i, idx) inzip(1:N, structural_eachindex(duals, x))
111-
duals[idx] =Dual{T,V,N}(x[idx], seeds[i])
112-
end
113-
else
114-
for (i, idx) inzip(1:N, structural_eachindex(duals, x))
115-
ifisassigned(x, idx)
116-
duals[idx] =Dual{T,V,N}(x[idx], seeds[i])
117-
else
118-
Base._unsetindex!(duals, idx)
119-
end
120-
end
117+
idxs = Iterators.take(structural_eachindex(duals, x), N)
118+
return_seed!(duals, x, idxs) do value, i
119+
Dual{T,V,N}(value, seeds[i])
121120
end
122-
return duals
123121
end
124122

125123
functionseed!(duals::AbstractArray{Dual{T,V,N}}, x, index,
126124
seeds::NTuple{N,Partials{N,V}}, chunksize = N) where {T,V,N}
127125
offset = index -1
128-
idxs = Iterators.drop(structural_eachindex(duals, x), offset)
129-
ifisbitstype(V)
130-
for (i, idx) inzip(1:chunksize, idxs)
131-
duals[idx] =Dual{T,V,N}(x[idx], seeds[i])
132-
end
133-
else
134-
for (i, idx) inzip(1:chunksize, idxs)
135-
ifisassigned(x, idx)
136-
duals[idx] =Dual{T,V,N}(x[idx], seeds[i])
137-
else
138-
Base._unsetindex!(duals, idx)
139-
end
140-
end
126+
idxs = Iterators.take(Iterators.drop(structural_eachindex(duals, x), offset), chunksize)
127+
return_seed!(duals, x, idxs) do value, i
128+
Dual{T,V,N}(value, seeds[i])
129+
end
130+
end
131+
132+
# Seed a chunk in either layer of nested duals. A `nothing` seed clears that layer.
133+
functionseed_hessian_chunk!(duals::AbstractArray{Dual{T,Dual{T,V,N},N}}, x, index,
134+
iseeds::Union{Nothing,NTuple{N,Partials{N,V}}},
135+
oseeds::Union{Nothing,NTuple{N,Partials{N,Dual{T,V,N}}}},
136+
chunksize = N) where {T,V,N}
137+
izero =zero(Partials{N,V})
138+
ozero =zero(Partials{N,Dual{T,V,N}})
139+
idxs = Iterators.take(Iterators.drop(structural_eachindex(duals, x), index -1), chunksize)
140+
return_seed!(duals, x, idxs) do value, i
141+
inner =Dual{T,V,N}(value, iseeds ===nothing? izero : iseeds[i])
142+
Dual{T,Dual{T,V,N},N}(inner, oseeds ===nothing? ozero : oseeds[i])
141143
end
142-
return duals
143144
end

‎src/config.jl‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,9 @@ Return a `HessianConfig` instance based on the type of `f` and type/shape of the
207207
vector `x`.
208208
209209
The returned `HessianConfig` instance contains all the work buffers required by
210-
`ForwardDiff.hessian` and `ForwardDiff.hessian!`. For the latter, the buffers are
211-
configured for the case where the `result` argument is an `AbstractArray`. If
212-
it is a `DiffResult`, the `HessianConfig` should instead be constructed via
213-
`ForwardDiff.HessianConfig(f, result, x, chunk)`.
210+
`ForwardDiff.hessian` and `ForwardDiff.hessian!`, including when the latter stores into a
211+
`DiffResult`. The `ForwardDiff.HessianConfig(f, result, x, chunk)` constructor may also be
212+
used with any of these methods.
214213
215214
If `f` is `nothing` instead of the actual target function, then the returned instance can
216215
be used with any target function. However, this will reduce ForwardDiff's ability to catch
@@ -234,7 +233,9 @@ Return a `HessianConfig` instance based on the type of `f`, types/storage in `re
234233
type/shape of the input vector `x`.
235234
236235
The returned `HessianConfig` instance contains all the work buffers required by
237-
`ForwardDiff.hessian!` for the case where the `result` argument is an `DiffResult`.
236+
`ForwardDiff.hessian` and `ForwardDiff.hessian!`. It is interchangeable with a config
237+
constructed via `ForwardDiff.HessianConfig(f, x, chunk)`; this constructor retains the
238+
result-aware form for compatibility.
238239
239240
If `f` is `nothing` instead of the actual target function, then the returned instance can
240241
be used with any target function. However, this will reduce ForwardDiff's ability to catch

‎src/hessian.jl‎

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
ForwardDiff.hessian(f, x::AbstractArray, cfg::HessianConfig = HessianConfig(f, x), check=Val{true}())
77
88
Return `H(f)` evaluated at `x`, assuming `f` is called as `f(x)`.
9+
The returned Hessian is exactly symmetric: its two triangles are filled from the same
10+
derivative values.
911
1012
This method assumes that `isa(f(x), Real)`.
1113
@@ -21,8 +23,9 @@ end
2123
"""
2224
ForwardDiff.hessian!(result::AbstractArray, f, x::AbstractArray, cfg::HessianConfig = HessianConfig(f, x), check=Val{true}())
2325
24-
Compute `H(f)` (i.e. `J(∇(f))`) evaluated at `x` and store the result(s) in `result`,
25-
assuming `f` is called as `f(x)`.
26+
Compute `H(f)` evaluated at `x` and store the result(s) in `result`, assuming `f` is
27+
called as `f(x)`. The stored Hessian is exactly symmetric: its two triangles are filled
28+
from the same derivative values.
2629
2730
This method assumes that `isa(f(x), Real)`.
2831
@@ -32,17 +35,18 @@ function hessian!(result::AbstractArray, f::F, x::AbstractArray, cfg::HessianCon
3235
require_one_based_indexing(result, x)
3336
CHK &&checktag(T, f, x)
3437
xlen =structural_length(x)
35-
H = result isa AbstractMatrix &&size(result) == (xlen, xlen) ? result :reshape(result, xlen, xlen)
38+
H = result isa AbstractMatrix ? result :reshape(result, xlen, xlen)
3639
symmetric_hessian!(H, f, x, cfg, nothing)
3740
return result
3841
end
3942

4043
"""
4144
ForwardDiff.hessian!(result::DiffResult, f, x::AbstractArray, cfg::HessianConfig = HessianConfig(f, result, x), check=Val{true}())
4245
43-
Exactly like `ForwardDiff.hessian!(result::AbstractArray, f, x::AbstractArray, cfg::HessianConfig)`, but
44-
because `isa(result, DiffResult)`, `cfg` is constructed as `HessianConfig(f, result, x)` instead of
45-
`HessianConfig(f, x)`.
46+
Exactly like `ForwardDiff.hessian!(result::AbstractArray, f, x::AbstractArray, cfg::HessianConfig)`,
47+
but also stores the value and gradient in `result`. The default `cfg` is constructed as
48+
`HessianConfig(f, result, x)`, though a config constructed as `HessianConfig(f, x)` may also
49+
be used.
4650
4751
Set `check` to `Val{false}()` to disable tag checking. This can lead to perturbation confusion, so should be used with care.
4852
"""
@@ -51,7 +55,7 @@ function hessian!(result::DiffResult, f::F, x::AbstractArray, cfg::HessianConfig
5155
CHK &&checktag(T, f, x)
5256
xlen =structural_length(x)
5357
hess = DiffResults.hessian(result)
54-
H = hess isa AbstractMatrix &&size(hess) == (xlen, xlen) ? hess :reshape(hess, xlen, xlen)
58+
H = hess isa AbstractMatrix ? hess :reshape(hess, xlen, xlen)
5559
_, ydual =symmetric_hessian!(H, f, x, cfg, DiffResults.gradient(result))
5660
result = DiffResults.value!(result, value(T, value(T, ydual)))
5761
return result
@@ -63,32 +67,6 @@ end
6367

6468
const HESSIAN_ERROR =DimensionMismatch("hessian(f, x) expects that f(x) is a real number. Perhaps you meant jacobian(f, x)?")
6569

66-
# Seed a chunk in either layer of the nested duals. A `nothing` seed clears that layer.
67-
functionseed_hessian_chunk!(duals::AbstractArray{Dual{T,Dual{T,V,N},N}}, x, index,
68-
iseeds::Union{Nothing,NTuple{N,Partials{N,V}}},
69-
oseeds::Union{Nothing,NTuple{N,Partials{N,Dual{T,V,N}}}},
70-
chunksize = N) where {T,V,N}
71-
izero =zero(Partials{N,V})
72-
ozero =zero(Partials{N,Dual{T,V,N}})
73-
idxs = Iterators.drop(structural_eachindex(duals, x), index -1)
74-
ifisbitstype(V)
75-
for (i, idx) inzip(1:chunksize, idxs)
76-
inner =Dual{T,V,N}(x[idx], iseeds ===nothing? izero : iseeds[i])
77-
duals[idx] =Dual{T,Dual{T,V,N},N}(inner, oseeds ===nothing? ozero : oseeds[i])
78-
end
79-
else
80-
for (i, idx) inzip(1:chunksize, idxs)
81-
ifisassigned(x, idx)
82-
inner =Dual{T,V,N}(x[idx], iseeds ===nothing? izero : iseeds[i])
83-
duals[idx] =Dual{T,Dual{T,V,N},N}(inner, oseeds ===nothing? ozero : oseeds[i])
84-
else
85-
Base._unsetindex!(duals, idx)
86-
end
87-
end
88-
end
89-
return duals
90-
end
91-
9270
# Copy a block from the nested partials and fill its transpose. On diagonal blocks, read
9371
# only the upper triangle so the result is exactly symmetric.
9472
functionextract_hessian_chunk!(::Type{T}, H, ydual, roffset, coffset, rsize, csize) where {T}
@@ -118,38 +96,39 @@ function symmetric_hessian_expr(result_definition::Expr)
11896
throw(ArgumentError(lazy"chunk size cannot be greater than ForwardDiff.structural_length(x) ($(N) > $(structural_length(x)))"))
11997
end
12098

121-
nblocks = xlen ==0?1:div(xlen + N -1, N)
99+
# `N == 0` only for empty inputs, which still need one evaluation to determine the
100+
# output type and value.
101+
nblocks = xlen ==0?1:cld(xlen, N)
122102

123103
xdual = cfg.gradient_config.duals
124104
iseeds = cfg.jacobian_config.seeds
125105
oseeds = cfg.gradient_config.seeds
126106

127-
# Keep all unseeded blocks at zero between evaluations.
128-
seed_hessian_chunk!(xdual, x, 1, nothing, nothing, xlen)
129-
130-
# The first evaluation determines the output type.
107+
# The first evaluation determines the output type. Seeding the first block and clearing
108+
# the untouched tail partitions the fresh buffer, so every element is initialized once.
131109
seed_hessian_chunk!(xdual, x, 1, iseeds, oseeds)
110+
seed_hessian_chunk!(xdual, x, N +1, nothing, nothing, xlen - N)
132111
ydual1 =f(xdual)
133112
ydual1 isa Real ||throw(HESSIAN_ERROR)
134113
$(result_definition)
135114
extract_hessian_chunk!(T, H, ydual1, 0, 0, N, N)
136115
extract_hessian_gradient_chunk!(T, grad, ydual1, 1, N)
137-
seed_hessian_chunk!(xdual, x, 1, nothing, nothing)
116+
nblocks >1&&seed_hessian_chunk!(xdual, x, 1, nothing, nothing)
138117

139118
for q in2:nblocks
140119
qoffset = (q -1) * N
141120
qsize =min(N, xlen - qoffset)
142-
# Off-diagonal blocks: p seeds columns and q seeds rows.
121+
# Off-diagonal blocks: p seeds columns and q seeds rows. The outer seeds for q
122+
# remain unchanged throughout this loop.
123+
seed_hessian_chunk!(xdual, x, qoffset +1, nothing, oseeds, qsize)
143124
for p in1:(q -1)
144125
poffset = (p -1) * N
145126
seed_hessian_chunk!(xdual, x, poffset +1, iseeds, nothing)
146-
seed_hessian_chunk!(xdual, x, qoffset +1, nothing, oseeds, qsize)
147127
ydual =f(xdual)
148128
extract_hessian_chunk!(T, H, ydual, qoffset, poffset, qsize, N)
149129
seed_hessian_chunk!(xdual, x, poffset +1, nothing, nothing)
150-
seed_hessian_chunk!(xdual, x, qoffset +1, nothing, nothing, qsize)
151130
end
152-
#Diagonal blocks seed both layers.
131+
#The diagonal block adds q's inner seeds while retaining its outer seeds.
153132
seed_hessian_chunk!(xdual, x, qoffset +1, iseeds, oseeds, qsize)
154133
ydual =f(xdual)
155134
extract_hessian_chunk!(T, H, ydual, qoffset, qoffset, qsize, qsize)
@@ -162,7 +141,7 @@ function symmetric_hessian_expr(result_definition::Expr)
162141
end
163142

164143
@evalfunctionsymmetric_hessian(f::F, x, cfg::HessianConfig{T,V,N}, grad) where {F,T,V,N}
165-
$(symmetric_hessian_expr(:(H =similar(x, typeof(value(T, value(T, ydual1))), xlen, xlen))))
144+
$(symmetric_hessian_expr(:(H =similar(x, valtype(T, valtype(T, typeof(ydual1))), xlen, xlen))))
166145
end
167146

168147
@evalfunctionsymmetric_hessian!(H, f::F, x, cfg::HessianConfig{T,V,N}, grad) where {F,T,V,N}

‎test/AllocationsTest.jl‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ convert_test_574() = convert(ForwardDiff.Dual{Nothing,ForwardDiff.Dual{Nothing,F
2929
allocs_szp!(duals, x, 1, 4)
3030
@testiszero(allocs_szp!(duals, x, 1, 4))
3131

32+
hcfg = ForwardDiff.HessianConfig(nothing, x)
33+
hduals = hcfg.gradient_config.duals
34+
iseeds = hcfg.jacobian_config.seeds
35+
oseeds = hcfg.gradient_config.seeds
36+
allocs_hseed!(args...) =@allocated ForwardDiff.seed_hessian_chunk!(args...)
37+
allocs_hseed!(hduals, x, 1, iseeds, oseeds)
38+
@testiszero(allocs_hseed!(hduals, x, 1, iseeds, oseeds))
39+
allocs_hseed!(hduals, x, 1, nothing, nothing, 4)
40+
@testiszero(allocs_hseed!(hduals, x, 1, nothing, nothing, 4))
41+
3242
allocs_convert_test_574() =@allocatedconvert_test_574()
3343
allocs_convert_test_574()
3444
@testiszero(allocs_convert_test_574())

‎test/GradientTest.jl‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ end
5656
cfgx = ForwardDiff.GradientConfig(sin, x)
5757
@test_throws ForwardDiff.InvalidTagException ForwardDiff.gradient(f, x, cfgx)
5858
@test ForwardDiff.gradient(f, x, cfgx, Val{false}()) == ForwardDiff.gradient(f,x)
59+
@test_throws ArgumentError ForwardDiff.gradient(f, x, ForwardDiff.GradientConfig(f, x, ForwardDiff.Chunk{length(x) + 1}()))
5960

6061

6162
########################
@@ -115,6 +116,10 @@ end
115116
ForwardDiff.gradient!(out, prod, sx, scfg)
116117
@test out == actual
117118

119+
out =similar(x)
120+
ForwardDiff.gradient!(out, prod, sx, scfg, Val{false}())
121+
@test out == actual
122+
118123
result = DiffResults.GradientResult(x)
119124
result = ForwardDiff.gradient!(result, prod, x)
120125

0 commit comments

Comments
 (0)