Skip to content

Commit ceac9e1

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

9 files changed

Lines changed: 194 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
@generated function dualize(::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+
@inline function extract_hessian(::Type{T}, ydual::Partials, x::StaticArray) where {T}
111+
H = extract_jacobian(T, ydual, x)
112+
return typeof(H)(Symmetric(H, :U))
113+
end
114+
115+
@inline function extract_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+
return zero(R)
118+
end
119+
120+
@inline function 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+
return extract_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+
@inline function 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+
@inline function _seed!(make_dual::F, duals::AbstractArray{Dual{T,V,N}}, x, idxs) where {F,T,V,N}
9199
if isbitstype(V)
92-
for idx in idxs
93-
duals[idx] = Dual{T,V,N}(x[idx], seed)
100+
for (i, idx) in enumerate(idxs)
101+
duals[idx] = make_dual(x[idx], i)
94102
end
95103
else
96-
for idx in idxs
104+
for (i, idx) in enumerate(idxs)
97105
if isassigned(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
function seed!(duals::AbstractArray{Dual{T,V,N}}, x,
108116
seeds::NTuple{N,Partials{N,V}}) where {T,V,N}
109-
if isbitstype(V)
110-
for (i, idx) in zip(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) in zip(1:N, structural_eachindex(duals, x))
115-
if isassigned(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
function seed!(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-
if isbitstype(V)
130-
for (i, idx) in zip(1:chunksize, idxs)
131-
duals[idx] = Dual{T,V,N}(x[idx], seeds[i])
132-
end
133-
else
134-
for (i, idx) in zip(1:chunksize, idxs)
135-
if isassigned(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+
function seed_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: 25 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
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. Its size is `(structural_length(x), structural_length(x))`; for
11+
structured matrix inputs, only structurally stored elements are treated as variables.
912
1013
This method assumes that `isa(f(x), Real)`.
1114
@@ -21,8 +24,10 @@ end
2124
"""
2225
ForwardDiff.hessian!(result::AbstractArray, f, x::AbstractArray, cfg::HessianConfig = HessianConfig(f, x), check=Val{true}())
2326
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)`.
27+
Compute `H(f)` evaluated at `x` and store the result(s) in `result`, assuming `f` is
28+
called as `f(x)`. The stored Hessian is exactly symmetric: its two triangles are filled
29+
from the same derivative values. `result` must store a
30+
`structural_length(x)`-by-`structural_length(x)` matrix.
2631
2732
This method assumes that `isa(f(x), Real)`.
2833
@@ -32,17 +37,18 @@ function hessian!(result::AbstractArray, f::F, x::AbstractArray, cfg::HessianCon
3237
require_one_based_indexing(result, x)
3338
CHK && checktag(T, f, x)
3439
xlen = structural_length(x)
35-
H = result isa AbstractMatrix && size(result) == (xlen, xlen) ? result : reshape(result, xlen, xlen)
40+
H = result isa AbstractMatrix ? result : reshape(result, xlen, xlen)
3641
symmetric_hessian!(H, f, x, cfg, nothing)
3742
return result
3843
end
3944

4045
"""
4146
ForwardDiff.hessian!(result::DiffResult, f, x::AbstractArray, cfg::HessianConfig = HessianConfig(f, result, x), check=Val{true}())
4247
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)`.
48+
Exactly like `ForwardDiff.hessian!(result::AbstractArray, f, x::AbstractArray, cfg::HessianConfig)`,
49+
but also stores the value and gradient in `result`. The default `cfg` is constructed as
50+
`HessianConfig(f, result, x)`, though a config constructed as `HessianConfig(f, x)` may also
51+
be used.
4652
4753
Set `check` to `Val{false}()` to disable tag checking. This can lead to perturbation confusion, so should be used with care.
4854
"""
@@ -51,7 +57,7 @@ function hessian!(result::DiffResult, f::F, x::AbstractArray, cfg::HessianConfig
5157
CHK && checktag(T, f, x)
5258
xlen = structural_length(x)
5359
hess = DiffResults.hessian(result)
54-
H = hess isa AbstractMatrix && size(hess) == (xlen, xlen) ? hess : reshape(hess, xlen, xlen)
60+
H = hess isa AbstractMatrix ? hess : reshape(hess, xlen, xlen)
5561
_, ydual = symmetric_hessian!(H, f, x, cfg, DiffResults.gradient(result))
5662
result = DiffResults.value!(result, value(T, value(T, ydual)))
5763
return result
@@ -63,32 +69,6 @@ end
6369

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

66-
# Seed a chunk in either layer of the nested duals. A `nothing` seed clears that layer.
67-
function seed_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-
if isbitstype(V)
75-
for (i, idx) in zip(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) in zip(1:chunksize, idxs)
81-
if isassigned(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-
9272
# Copy a block from the nested partials and fill its transpose. On diagonal blocks, read
9373
# only the upper triangle so the result is exactly symmetric.
9474
function extract_hessian_chunk!(::Type{T}, H, ydual, roffset, coffset, rsize, csize) where {T}
@@ -118,38 +98,39 @@ function symmetric_hessian_expr(result_definition::Expr)
11898
throw(ArgumentError(lazy"chunk size cannot be greater than ForwardDiff.structural_length(x) ($(N) > $(structural_length(x)))"))
11999
end
120100

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

123105
xdual = cfg.gradient_config.duals
124106
iseeds = cfg.jacobian_config.seeds
125107
oseeds = cfg.gradient_config.seeds
126108

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.
109+
# The first evaluation determines the output type. Seeding the first block and clearing
110+
# the untouched tail partitions the fresh buffer, so every element is initialized once.
131111
seed_hessian_chunk!(xdual, x, 1, iseeds, oseeds)
112+
seed_hessian_chunk!(xdual, x, N + 1, nothing, nothing, xlen - N)
132113
ydual1 = f(xdual)
133114
ydual1 isa Real || throw(HESSIAN_ERROR)
134115
$(result_definition)
135116
extract_hessian_chunk!(T, H, ydual1, 0, 0, N, N)
136117
extract_hessian_gradient_chunk!(T, grad, ydual1, 1, N)
137-
seed_hessian_chunk!(xdual, x, 1, nothing, nothing)
118+
nblocks > 1 && seed_hessian_chunk!(xdual, x, 1, nothing, nothing)
138119

139120
for q in 2:nblocks
140121
qoffset = (q - 1) * N
141122
qsize = min(N, xlen - qoffset)
142-
# Off-diagonal blocks: p seeds columns and q seeds rows.
123+
# Off-diagonal blocks: p seeds columns and q seeds rows. The outer seeds for q
124+
# remain unchanged throughout this loop.
125+
seed_hessian_chunk!(xdual, x, qoffset + 1, nothing, oseeds, qsize)
143126
for p in 1:(q - 1)
144127
poffset = (p - 1) * N
145128
seed_hessian_chunk!(xdual, x, poffset + 1, iseeds, nothing)
146-
seed_hessian_chunk!(xdual, x, qoffset + 1, nothing, oseeds, qsize)
147129
ydual = f(xdual)
148130
extract_hessian_chunk!(T, H, ydual, qoffset, poffset, qsize, N)
149131
seed_hessian_chunk!(xdual, x, poffset + 1, nothing, nothing)
150-
seed_hessian_chunk!(xdual, x, qoffset + 1, nothing, nothing, qsize)
151132
end
152-
# Diagonal blocks seed both layers.
133+
# The diagonal block adds q's inner seeds while retaining its outer seeds.
153134
seed_hessian_chunk!(xdual, x, qoffset + 1, iseeds, oseeds, qsize)
154135
ydual = f(xdual)
155136
extract_hessian_chunk!(T, H, ydual, qoffset, qoffset, qsize, qsize)
@@ -162,7 +143,7 @@ function symmetric_hessian_expr(result_definition::Expr)
162143
end
163144

164145
@eval function symmetric_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))))
146+
$(symmetric_hessian_expr(:(H = similar(x, valtype(T, valtype(T, typeof(ydual1))), xlen, xlen))))
166147
end
167148

168149
@eval function symmetric_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
@test iszero(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+
@test iszero(allocs_hseed!(hduals, x, 1, iseeds, oseeds))
39+
allocs_hseed!(hduals, x, 1, nothing, nothing, 4)
40+
@test iszero(allocs_hseed!(hduals, x, 1, nothing, nothing, 4))
41+
3242
allocs_convert_test_574() = @allocated convert_test_574()
3343
allocs_convert_test_574()
3444
@test iszero(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)