Skip to content

Commit 27a9600

Browse files
Merge pull request #190 from ElOceanografo/sparsehess
Add sparse Hessian decompression
2 parents 74d9fc6 + e1c8eaf commit 27a9600

File tree

6 files changed

+232
-1
lines changed

6 files changed

+232
-1
lines changed

Project.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ Compat = "2.2, 3, 4"
2626
DataStructures = "0.17, 0.18"
2727
FiniteDiff = "2.8.1"
2828
ForwardDiff = "0.10"
29-
Graphs = "1.4"
3029
Requires = "0.5, 1.0"
3130
StaticArrays = "1"
3231
VertexSafeGraphs = "0.2"

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,35 @@ function, otherwise it will become a dense matrix. If `jac_prototype` and
207207
function has a *square* Jacobian matrix. If it is not the case, please specify
208208
the shape of output by giving `dx`.
209209

210+
Similar functionality is available for Hessians, using finite differences of forward derivatives. Given a scalar function `f(x)`, a vector value for `x`,
211+
and a color vector and sparsity pattern, this can be accomplished using
212+
`numauto_color_hessian` or its in-place form `numauto_color_hessian!`.
213+
214+
```julia
215+
H = numauto_color_hessian(fscalar, x, colorvec, sparsity)
216+
numauto_color_hessian!(H, fscalar, x, colorvec, sparsity)
217+
```
218+
219+
To avoid unnecessary allocations every time the Hessian is computed,
220+
construct a `ForwardColorHesCache` beforehand:
221+
222+
```julia
223+
hescache = ForwardColorHesCache(f, x, colorvec, sparsity)
224+
numauto_color_hessian!(H, f, x, hescache)
225+
```
226+
227+
By default, these methods use a mix of numerical and automatic differentiation,
228+
namely by taking finite differences of gradients calculated via ForwardDiff.jl.
229+
Alternatively, if you have your own custom gradient function `g!`, you can specify
230+
it as an argument to `ForwardColorHesCache`:
231+
232+
```julia
233+
hescache = ForwardColorHesCache(fscalar, x, colorvec, sparsity, g!)
234+
```
235+
Note that any user-defined gradient needs to have the signature `g!(G, x)`,
236+
i.e. updating the gradient `G` in place.
237+
238+
210239
### Jacobian-Vector and Hessian-Vector Products
211240

212241
Matrix-free implementations of Jacobian-Vector and Hessian-Vector products is

src/SparseDiffTools.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export contract_color,
2828
forwarddiff_color_jacobian!,
2929
forwarddiff_color_jacobian,
3030
ForwardColorJacCache,
31+
numauto_color_hessian!,
32+
numauto_color_hessian,
33+
ForwardColorHesCache,
3134
auto_jacvec,auto_jacvec!,
3235
num_jacvec,num_jacvec!,
3336
num_vecjac,num_vecjac!,
@@ -48,6 +51,7 @@ include("coloring/greedy_star1_coloring.jl")
4851
include("coloring/greedy_star2_coloring.jl")
4952
include("coloring/matrix2graph.jl")
5053
include("differentiation/compute_jacobian_ad.jl")
54+
include("differentiation/compute_hessian_ad.jl")
5155
include("differentiation/jaches_products.jl")
5256
include("differentiation/vecjac_products.jl")
5357

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
struct ForwardColorHesCache{THS, THC, TI<:Integer, TD, TGF, TGC, TG}
2+
sparsity::THS
3+
colors::THC
4+
ncolors::TI
5+
D::TD
6+
buffer::TD
7+
grad!::TGF
8+
grad_config::TGC
9+
G1::TG
10+
G2::TG
11+
end
12+
13+
function make_hessian_buffers(colorvec, x)
14+
ncolors = maximum(colorvec)
15+
D = hcat([float.(i .== colorvec) for i in 1:ncolors]...)
16+
buffer = similar(D)
17+
G1 = similar(x)
18+
G2 = similar(x)
19+
return (;ncolors, D, buffer, G1, G2)
20+
end
21+
22+
function ForwardColorHesCache(f,
23+
x::AbstractVector{<:Number},
24+
colorvec::AbstractVector{<:Integer}=eachindex(x),
25+
sparsity::Union{AbstractMatrix, Nothing}=nothing,
26+
g! = (G, x, grad_config) -> ForwardDiff.gradient!(G, f, x, grad_config))
27+
ncolors, D, buffer, G, G2 = make_hessian_buffers(colorvec, x)
28+
grad_config = ForwardDiff.GradientConfig(f, x)
29+
30+
# If user supplied their own gradient function, make sure it has the right
31+
# signature (i.e. g!(G, x) or g!(G, x, grad_config::ForwardDiff.GradientConfig))
32+
if ! hasmethod(g!, (typeof(G), typeof(G), typeof(grad_config)))
33+
if ! hasmethod(g!, (typeof(G), typeof(G)))
34+
throw(ArgumentError("Signature of `g!` must be either `g!(G, x)` or `g!(G, x, grad_config::ForwardDiff.GradientConfig)`"))
35+
end
36+
# define new method that takes a GradientConfig but doesn't use it
37+
g1!(G, x, grad_config) = g!(G, x)
38+
else
39+
g1! = g!
40+
end
41+
42+
if sparsity === nothing
43+
sparsity = sparse(ones(length(x), length(x)))
44+
end
45+
return ForwardColorHesCache(sparsity, colorvec, ncolors, D, buffer, g1!, grad_config, G, G2)
46+
end
47+
48+
function numauto_color_hessian!(H::AbstractMatrix{<:Number},
49+
f,
50+
x::AbstractArray{<:Number},
51+
hes_cache::ForwardColorHesCache;
52+
safe = true)
53+
ϵ = cbrt(eps(eltype(x)))
54+
for j in 1:hes_cache.ncolors
55+
x .+= ϵ .* @view hes_cache.D[:, j]
56+
hes_cache.grad!(hes_cache.G2, x, hes_cache.grad_config)
57+
x .-= 2ϵ .* @view hes_cache.D[:, j]
58+
hes_cache.grad!(hes_cache.G1, x, hes_cache.grad_config)
59+
hes_cache.buffer[:, j] .= (hes_cache.G2 .- hes_cache.G1) ./ 2ϵ
60+
x .+= ϵ .* @view hes_cache.D[:, j] #reset to original value
61+
end
62+
ii, jj, vv = findnz(hes_cache.sparsity)
63+
if safe
64+
fill!(H, false)
65+
end
66+
for (i, j) in zip(ii, jj)
67+
H[i, j] = hes_cache.buffer[i, hes_cache.colors[j]]
68+
end
69+
return H
70+
end
71+
72+
function numauto_color_hessian!(H::AbstractMatrix{<:Number},
73+
f,
74+
x::AbstractArray{<:Number},
75+
colorvec::AbstractVector{<:Integer}=eachindex(x),
76+
sparsity::Union{AbstractMatrix, Nothing}=nothing)
77+
hes_cache = ForwardColorHesCache(f, x, colorvec, sparsity)
78+
numauto_color_hessian!(H, f, x, hes_cache)
79+
return H
80+
end
81+
82+
function numauto_color_hessian(f,
83+
x::AbstractArray{<:Number},
84+
hes_cache::ForwardColorHesCache)
85+
H = convert.(eltype(x), hes_cache.sparsity)
86+
numauto_color_hessian!(H, f, x, hes_cache)
87+
return H
88+
end
89+
90+
function numauto_color_hessian(f,
91+
x::AbstractArray{<:Number},
92+
colorvec::AbstractVector{<:Integer}=eachindex(x),
93+
sparsity::Union{AbstractMatrix, Nothing}=nothing)
94+
hes_cache = ForwardColorHesCache(f, x, colorvec, sparsity)
95+
H = convert.(eltype(x), hes_cache.sparsity)
96+
numauto_color_hessian!(H, f, x, hes_cache)
97+
return H
98+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ if GROUP == "All"
1818
@time @safetestset "Acyclic coloring" begin include("test_acyclic.jl") end
1919
@time @safetestset "Matrix to graph conversion" begin include("test_matrix2graph.jl") end
2020
@time @safetestset "AD using colorvec vector" begin include("test_ad.jl") end
21+
@time @safetestset "Hessian colorvecs" begin include("test_sparse_hessian.jl") end
2122
@time @safetestset "Integration test" begin include("test_integration.jl") end
2223
@time @safetestset "Special matrices" begin include("test_specialmatrices.jl") end
2324
@time @safetestset "Jac Vecs and Hes Vecs" begin include("test_jaches_products.jl") end

test/test_sparse_hessian.jl

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
## Hessian tests
2+
using SparsityDetection, SparseDiffTools
3+
using ForwardDiff
4+
using LinearAlgebra, SparseArrays
5+
6+
function fscalar(x)
7+
return -dot(x, x) + 2 * x[2] * x[3]^2
8+
end
9+
10+
x = randn(5)
11+
sparsity = hessian_sparsity(fscalar, x)
12+
colors = matrix_colors(tril(sparsity))
13+
ncolors = maximum(colors)
14+
D = hcat([float.(i .== colors) for i in 1:ncolors]...)
15+
buffer = similar(D)
16+
G1 = zero(x)
17+
G2 = zero(x)
18+
19+
buffers_tup = SparseDiffTools.make_hessian_buffers(colors, x)
20+
@test buffers_tup.ncolors == ncolors
21+
@test buffers_tup.D == D
22+
@test size(buffers_tup.buffer) == size(buffer)
23+
@test eltype(buffers_tup.buffer) == eltype(buffer)
24+
@test typeof(buffers_tup.buffer) == typeof(buffer)
25+
@test size(buffers_tup.G1) == size(G1)
26+
@test eltype(buffers_tup.G1) == eltype(G1)
27+
@test size(buffers_tup.G2) == size(G2)
28+
@test eltype(buffers_tup.G2) == eltype(G2)
29+
30+
31+
gconfig = ForwardDiff.GradientConfig(fscalar, x)
32+
g(x) = ForwardDiff.gradient(fscalar, x) # allocating
33+
g!(G, x, gconfig) = ForwardDiff.gradient!(G, fscalar, x, gconfig) # non-allocating
34+
35+
hescache1 = ForwardColorHesCache(sparsity, colors, ncolors, D, buffer, g!, gconfig, G1, G2)
36+
hescache2 = ForwardColorHesCache(fscalar, x, colors, sparsity, g!)
37+
hescache3 = ForwardColorHesCache(fscalar, x, colors, sparsity)
38+
# custom gradient function
39+
hescache4 = ForwardColorHesCache(fscalar, x, colors, sparsity,
40+
(G, x) -> ForwardDiff.gradient!(G, fscalar, x))
41+
hescache5 = ForwardColorHesCache(fscalar, x)
42+
# custom gradient has to have 2 or 3 arguments...
43+
@test_throws ArgumentError ForwardColorHesCache(fscalar, x, colors, sparsity, (a) -> 1.0)
44+
@test_throws ArgumentError ForwardColorHesCache(fscalar, x, colors, sparsity, (a, b, c, d) -> 1.0)
45+
# ...and needs to accept (Vector, Vector, ForwardDiff.GradientConfig)
46+
@test_throws ArgumentError ForwardColorHesCache(fscalar, x, colors, sparsity, (a::Int, b::Int) -> 1.0,)
47+
@test_throws ArgumentError ForwardColorHesCache(fscalar, x, colors, sparsity, (a::Int, b::Int, c::Int) -> 1.0)
48+
49+
for name in [:sparsity, :colors, :ncolors, :D]
50+
@eval @test hescache1.$name == hescache2.$name
51+
@eval @test hescache1.$name == hescache3.$name
52+
@eval @test hescache1.$name == hescache4.$name
53+
# hescache5 is the default dense version, so only first axis will match
54+
@eval @test size(hescache1.$name, 1) == size(hescache5.$name, 1)
55+
end
56+
for name in [:buffer, :G1, :G2]
57+
@eval @test size(hescache1.$name) == size(hescache2.$name)
58+
@eval @test size(hescache1.$name) == size(hescache3.$name)
59+
@eval @test size(hescache1.$name) == size(hescache4.$name)
60+
# hescache5 is the default dense version, so only first axis will match
61+
@eval @test size(hescache1.$name, 1) == size(hescache5.$name, 1)
62+
63+
@eval @test eltype(hescache1.$name) == eltype(hescache2.$name)
64+
@eval @test eltype(hescache1.$name) == eltype(hescache3.$name)
65+
@eval @test eltype(hescache1.$name) == eltype(hescache4.$name)
66+
@eval @test eltype(hescache1.$name) == eltype(hescache5.$name)
67+
end
68+
69+
Hforward = ForwardDiff.hessian(fscalar, x)
70+
for (i, hescache) in enumerate([hescache1, hescache2, hescache3, hescache4, hescache5])
71+
H = numauto_color_hessian(fscalar, x, colors, sparsity)
72+
H1 = numauto_color_hessian(fscalar, x, hescache)
73+
H2 = numauto_color_hessian(fscalar, x)
74+
@test all(isapprox.(Hforward, H, rtol=1e-6))
75+
@test all(isapprox.(H, H1, rtol=1e-6))
76+
@test all(isapprox.(H2, H1, rtol=1e-6))
77+
78+
H1 = similar(H)
79+
numauto_color_hessian!(H1, fscalar, x, collect(hescache.colors), hescache.sparsity)
80+
@test all(isapprox.(H1, H))
81+
82+
numauto_color_hessian!(H2, fscalar, x)
83+
@test all(isapprox.(H2, H))
84+
85+
numauto_color_hessian!(H1, fscalar, x, hescache)
86+
@test all(isapprox.(H1, H))
87+
88+
numauto_color_hessian!(H1, fscalar, x, hescache, safe=false)
89+
@test all(isapprox.(H1, H))
90+
91+
# the following tests usually pass, but once in a while don't (it's not a big difference
92+
# in timing on these small matrices, and sometimes its less than the timing variability).
93+
# Commenting out for now to avoid rare stochastic test failures.
94+
# # confirm unsafe is faster
95+
# t_safe = minimum(@elapsed(numauto_color_hessian!(H1, fscalar, x, hescache, safe=true))
96+
# for _ in 1:100)
97+
# t_unsafe = minimum(@elapsed(numauto_color_hessian!(H1, fscalar, x, hescache, safe=false))
98+
# for _ in 1:100)
99+
# @test t_unsafe <= t_safe
100+
end

0 commit comments

Comments
 (0)