Skip to content

Commit 973676a

Browse files
authored
Respect pattern structures in sparse AD (#531)
1 parent b14ee60 commit 973676a

10 files changed

Lines changed: 154 additions & 120 deletions

File tree

DifferentiationInterface/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ PolyesterForwardDiff = "0.1.1"
6060
ReverseDiff = "1.15.1"
6161
SparseArrays = "<0.0.1,1"
6262
SparseConnectivityTracer = "0.5.0,0.6"
63-
SparseMatrixColorings = "0.4.0"
63+
SparseMatrixColorings = "0.4.4"
6464
Symbolics = "5.27.1, 6"
6565
Tracker = "0.2.33"
6666
Zygote = "0.6.69"

DifferentiationInterface/docs/src/explanation/advanced.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,18 @@ DifferentiationInterface does this automatically if you pass a backend of type [
3737

3838
### `AutoSparse` object
3939

40+
`AutoSparse` backends only support [`jacobian`](@ref) and [`hessian`](@ref) (as well as their variants), because other operators do not output matrices.
4041
An `AutoSparse` backend must be constructed from three ingredients:
4142

42-
1. An underlying (dense) backend
43+
1. An underlying (dense) backend, which can be [`SecondOrder`](@ref) or anything from [ADTypes.jl](https://github.com/SciML/ADTypes.jl)
4344
2. A sparsity pattern detector like:
4445
- [`TracerSparsityDetector`](@extref SparseConnectivityTracer.TracerSparsityDetector) from [SparseConnectivityTracer.jl](https://github.com/adrhill/SparseConnectivityTracer.jl)
4546
- [`SymbolicsSparsityDetector`](@extref Symbolics.SymbolicsSparsityDetector) from [Symbolics.jl](https://github.com/JuliaSymbolics/Symbolics.jl)
4647
- [`DenseSparsityDetector`](@ref) from DifferentiationInterface.jl (beware that this detector only gives a locally valid pattern)
47-
3. A coloring algorithm: [`GreedyColoringAlgorithm`](@extref SparseMatrixColorings.GreedyColoringAlgorithm) from [SparseMatrixColorings.jl](https://github.com/gdalle/SparseMatrixColorings.jl) is the only one we support. As a result, sparse AD is now located in a package extension which depends on SparseMatrixColorings.jl.
48-
49-
`AutoSparse` backends only support [`jacobian`](@ref) and [`hessian`](@ref) (as well as their variants), because other operators do not output matrices.
50-
To obtain sparse Hessians, you need to put the `SecondOrder` backend inside `AutoSparse`, and not the other way around.
48+
- [`KnownJacobianSparsityDetector`](@extref ADTypes.KnownJacobianSparsityDetector) or [`KnownHessianSparsityDetector`](@extref ADTypes.KnownHessianSparsityDetector) from [ADTypes.jl](https://github.com/SciML/ADTypes.jl) (if you already know the pattern)
49+
3. A coloring algorithm from [SparseMatrixColorings.jl](https://github.com/gdalle/SparseMatrixColorings.jl), such as:
50+
- [`GreedyColoringAlgorithm`](@extref SparseMatrixColorings.GreedyColoringAlgorithm) (our generic recommendation)
51+
- [`ConstantColoringAlgorithm`](@extref SparseMatrixColorings.ConstantColoringAlgorithm) (if you have already computed the optimal coloring and always want to return it)
5152

5253
!!! note
5354
Symbolic backends have built-in sparsity handling, so `AutoSparse(AutoSymbolics())` and `AutoSparse(AutoFastDifferentiation())` do not need additional configuration for pattern detection or coloring.
@@ -59,3 +60,10 @@ But after preparation, the more zeros are present in the matrix, the greater the
5960

6061
!!! danger
6162
The result of preparation for an `AutoSparse` backend cannot be reused if the sparsity pattern changes.
63+
64+
### Tuning the coloring algorithm
65+
66+
The complexity of sparse Jacobians or Hessians grows with the number of distinct colors in a coloring of the sparsity pattern.
67+
To reduce this number of colors, [`GreedyColoringAlgorithm`](@ref) has two main settings: the order used for vertices and the decompression method.
68+
Depending on your use case, you may want to modify either of these options to increase performance.
69+
See the documentation of [SparseMatrixColorings.jl](https://github.com/gdalle/SparseMatrixColorings.jl) for details.

DifferentiationInterface/docs/src/tutorials/advanced.md

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ using BenchmarkTools
77
using DifferentiationInterface
88
import ForwardDiff, Zygote
99
using SparseConnectivityTracer: TracerSparsityDetector
10-
using SparseMatrixColorings: GreedyColoringAlgorithm
10+
using SparseMatrixColorings
1111
```
1212

1313
## Contexts
@@ -88,13 +88,13 @@ The following are reasonable defaults:
8888

8989
```@example tuto_advanced
9090
sparse_first_order_backend = AutoSparse(
91-
AutoForwardDiff();
91+
dense_first_order_backend;
9292
sparsity_detector=TracerSparsityDetector(),
9393
coloring_algorithm=GreedyColoringAlgorithm(),
9494
)
9595
9696
sparse_second_order_backend = AutoSparse(
97-
SecondOrder(AutoForwardDiff(), AutoZygote());
97+
dense_second_order_backend;
9898
sparsity_detector=TracerSparsityDetector(),
9999
coloring_algorithm=GreedyColoringAlgorithm(),
100100
)
@@ -116,19 +116,73 @@ hessian(f_sparse_scalar, sparse_second_order_backend, x)
116116
In the examples above, we didn't use preparation.
117117
Sparse preparation is more costly than dense preparation, but it is even more essential.
118118
Indeed, once preparation is done, sparse differentiation is much faster than dense differentiation, because it makes fewer calls to the underlying function.
119-
The speedup becomes very visible in large dimensions.
119+
120+
Some result analysis functions from [SparseMatrixColorings.jl](https://github.com/gdalle/SparseMatrixColorings.jl) can help you figure out what the preparation contains.
121+
First, it records the sparsity pattern itself (the one returned by the detector).
122+
123+
```@example tuto_advanced
124+
jac_prep = prepare_jacobian(f_sparse_vector, sparse_first_order_backend, x)
125+
sparsity_pattern(jac_prep)
126+
```
127+
128+
In forward mode, each column of the sparsity pattern gets a color.
129+
130+
```@example tuto_advanced
131+
column_colors(jac_prep)
132+
```
133+
134+
And the colors in turn define non-overlapping groups (for Jacobians at least, Hessians are a bit more complicated).
135+
136+
```@example tuto_advanced
137+
column_groups(jac_prep)
138+
```
139+
140+
### Sparsity speedup
141+
142+
When preparation is used, the speedup due to sparsity becomes very visible in large dimensions.
120143

121144
```@example tuto_advanced
122-
n = 1000
123-
jac_prep_dense = prepare_jacobian(f_sparse_vector, dense_first_order_backend, zeros(n))
124-
jac_prep_sparse = prepare_jacobian(f_sparse_vector, sparse_first_order_backend, zeros(n))
145+
xbig = rand(1000)
125146
nothing # hide
126147
```
127148

128149
```@example tuto_advanced
129-
@benchmark jacobian($f_sparse_vector, $jac_prep_dense, $dense_first_order_backend, $(randn(n)))
150+
jac_prep_dense = prepare_jacobian(f_sparse_vector, dense_first_order_backend, zero(xbig))
151+
@benchmark jacobian($f_sparse_vector, $jac_prep_dense, $dense_first_order_backend, $xbig)
130152
```
131153

132154
```@example tuto_advanced
133-
@benchmark jacobian($f_sparse_vector, $jac_prep_sparse, $sparse_first_order_backend, $(randn(n)))
155+
jac_prep_sparse = prepare_jacobian(f_sparse_vector, sparse_first_order_backend, zero(xbig))
156+
@benchmark jacobian($f_sparse_vector, $jac_prep_sparse, $sparse_first_order_backend, $xbig)
157+
```
158+
159+
Better memory use can be achieved by pre-allocating the matrix from the preparation result (so that it has the correct structure).
160+
161+
```@example tuto_advanced
162+
jac_buffer = similar(sparsity_pattern(jac_prep_sparse), eltype(xbig))
163+
@benchmark jacobian!($f_sparse_vector, $jac_buffer, $jac_prep_sparse, $sparse_first_order_backend, $xbig)
164+
```
165+
166+
And for optimal speed, one should write non-allocating and type-stable functions.
167+
168+
```@example tuto_advanced
169+
function f_sparse_vector!(y::AbstractVector, x::AbstractVector)
170+
n = length(x)
171+
for i in eachindex(y)
172+
y[i] = abs2(x[i + 1]) - abs2(x[i]) + abs2(x[n - i]) - abs2(x[n - i + 1])
173+
end
174+
return nothing
175+
end
176+
177+
ybig = zeros(length(xbig) - 1)
178+
f_sparse_vector!(ybig, xbig)
179+
ybig ≈ f_sparse_vector(xbig)
180+
```
181+
182+
In this case, the sparse Jacobian should also become non-allocating (for our specific choice of backend).
183+
184+
```@example tuto_advanced
185+
jac_prep_sparse_nonallocating = prepare_jacobian(f_sparse_vector!, zero(ybig), sparse_first_order_backend, zero(xbig))
186+
jac_buffer = similar(sparsity_pattern(jac_prep_sparse_nonallocating), eltype(xbig))
187+
@benchmark jacobian!($f_sparse_vector!, $ybig, $jac_buffer, $jac_prep_sparse_nonallocating, $sparse_first_order_backend, $xbig)
134188
```

DifferentiationInterface/ext/DifferentiationInterfaceSparseMatrixColoringsExt/DifferentiationInterfaceSparseMatrixColoringsExt.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ using SparseMatrixColorings:
3737
row_colors,
3838
column_groups,
3939
row_groups,
40+
sparsity_pattern,
4041
decompress,
4142
decompress!
43+
import SparseMatrixColorings as SMC
4244

4345
include("jacobian.jl")
4446
include("hessian.jl")

DifferentiationInterface/ext/DifferentiationInterfaceSparseMatrixColoringsExt/hessian.jl

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ function SparseHessianPrep{B}(;
3333
)
3434
end
3535

36+
SMC.sparsity_pattern(prep::SparseHessianPrep) = sparsity_pattern(prep.coloring_result)
37+
SMC.column_colors(prep::SparseHessianPrep) = column_colors(prep.coloring_result)
38+
SMC.column_groups(prep::SparseHessianPrep) = column_groups(prep.coloring_result)
39+
3640
## Hessian, one argument
3741

3842
function DI.prepare_hessian(
@@ -68,29 +72,6 @@ function DI.prepare_hessian(
6872
)
6973
end
7074

71-
function DI.hessian(
72-
f::F, prep::SparseHessianPrep{B}, backend::AutoSparse, x, contexts::Vararg{Context,C}
73-
) where {F,B,C}
74-
@compat (; coloring_result, batched_seeds, hvp_prep) = prep
75-
dense_backend = dense_ad(backend)
76-
Ng = length(column_groups(coloring_result))
77-
78-
hvp_prep_same = prepare_hvp_same_point(
79-
f, hvp_prep, dense_backend, x, batched_seeds[1], contexts...
80-
)
81-
82-
compressed_blocks = map(eachindex(batched_seeds)) do a
83-
dg_batch = hvp(f, hvp_prep_same, dense_backend, x, batched_seeds[a], contexts...)
84-
stack(vec, dg_batch; dims=2)
85-
end
86-
87-
compressed_matrix = reduce(hcat, compressed_blocks)
88-
if Ng < size(compressed_matrix, 2)
89-
compressed_matrix = compressed_matrix[:, 1:Ng]
90-
end
91-
return decompress(compressed_matrix, coloring_result)
92-
end
93-
9475
function DI.hessian!(
9576
f::F,
9677
hess,
@@ -132,6 +113,13 @@ function DI.hessian!(
132113
return hess
133114
end
134115

116+
function DI.hessian(
117+
f::F, prep::SparseHessianPrep{B}, backend::AutoSparse, x, contexts::Vararg{Context,C}
118+
) where {F,B,C}
119+
hess = similar(sparsity_pattern(prep), eltype(x))
120+
return DI.hessian!(f, hess, prep, backend, x, contexts...)
121+
end
122+
135123
function DI.value_gradient_and_hessian!(
136124
f::F,
137125
grad,

DifferentiationInterface/ext/DifferentiationInterfaceSparseMatrixColoringsExt/jacobian.jl

Lines changed: 20 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ end
1010

1111
abstract type SparseJacobianPrep <: JacobianPrep end
1212

13+
SMC.sparsity_pattern(prep::SparseJacobianPrep) = sparsity_pattern(prep.coloring_result)
14+
SMC.column_colors(prep::SparseJacobianPrep) = column_colors(prep.coloring_result)
15+
SMC.column_groups(prep::SparseJacobianPrep) = column_groups(prep.coloring_result)
16+
SMC.row_colors(prep::SparseJacobianPrep) = row_colors(prep.coloring_result)
17+
SMC.row_groups(prep::SparseJacobianPrep) = row_groups(prep.coloring_result)
18+
1319
struct PushforwardSparseJacobianPrep{
1420
B,
1521
C<:AbstractColoringResult{:nonsymmetric,:column},
@@ -148,18 +154,19 @@ end
148154

149155
## One argument
150156

151-
function DI.jacobian(
152-
f::F, prep::SparseJacobianPrep, backend::AutoSparse, x, contexts::Vararg{Context,C}
153-
) where {F,C}
154-
return _sparse_jacobian_aux((f,), prep, backend, x, contexts...)
155-
end
156-
157157
function DI.jacobian!(
158158
f::F, jac, prep::SparseJacobianPrep, backend::AutoSparse, x, contexts::Vararg{Context,C}
159159
) where {F,C}
160160
return _sparse_jacobian_aux!((f,), jac, prep, backend, x, contexts...)
161161
end
162162

163+
function DI.jacobian(
164+
f::F, prep::SparseJacobianPrep, backend::AutoSparse, x, contexts::Vararg{Context,C}
165+
) where {F,C}
166+
jac = similar(sparsity_pattern(prep), eltype(x))
167+
return DI.jacobian!(f, jac, prep, backend, x, contexts...)
168+
end
169+
163170
function DI.value_and_jacobian(
164171
f::F, prep::SparseJacobianPrep, backend::AutoSparse, x, contexts::Vararg{Context,C}
165172
) where {F,C}
@@ -174,12 +181,6 @@ end
174181

175182
## Two arguments
176183

177-
function DI.jacobian(
178-
f!::F, y, prep::SparseJacobianPrep, backend::AutoSparse, x, contexts::Vararg{Context,C}
179-
) where {F,C}
180-
return _sparse_jacobian_aux((f!, y), prep, backend, x, contexts...)
181-
end
182-
183184
function DI.jacobian!(
184185
f!::F,
185186
y,
@@ -192,6 +193,13 @@ function DI.jacobian!(
192193
return _sparse_jacobian_aux!((f!, y), jac, prep, backend, x, contexts...)
193194
end
194195

196+
function DI.jacobian(
197+
f!::F, y, prep::SparseJacobianPrep, backend::AutoSparse, x, contexts::Vararg{Context,C}
198+
) where {F,C}
199+
jac = similar(sparsity_pattern(prep), promote_type(eltype(x), eltype(y)))
200+
return DI.jacobian!(f!, y, jac, prep, backend, x, contexts...)
201+
end
202+
195203
function DI.value_and_jacobian(
196204
f!::F, y, prep::SparseJacobianPrep, backend::AutoSparse, x, contexts::Vararg{Context,C}
197205
) where {F,C}
@@ -216,74 +224,6 @@ end
216224

217225
## Common auxiliaries
218226

219-
function _sparse_jacobian_aux(
220-
f_or_f!y::FY,
221-
prep::PushforwardSparseJacobianPrep{B},
222-
backend::AutoSparse,
223-
x,
224-
contexts::Vararg{Context,C},
225-
) where {FY,B,C}
226-
@compat (; coloring_result, batched_seeds, pushforward_prep) = prep
227-
dense_backend = dense_ad(backend)
228-
Ng = length(column_groups(coloring_result))
229-
230-
pushforward_prep_same = prepare_pushforward_same_point(
231-
f_or_f!y..., pushforward_prep, dense_backend, x, batched_seeds[1], contexts...
232-
)
233-
234-
compressed_blocks = map(eachindex(batched_seeds)) do a
235-
dy_batch = pushforward(
236-
f_or_f!y...,
237-
pushforward_prep_same,
238-
dense_backend,
239-
x,
240-
batched_seeds[a],
241-
contexts...,
242-
)
243-
stack(vec, dy_batch; dims=2)
244-
end
245-
246-
compressed_matrix = reduce(hcat, compressed_blocks)
247-
if Ng < size(compressed_matrix, 2)
248-
compressed_matrix = compressed_matrix[:, 1:Ng]
249-
end
250-
return decompress(compressed_matrix, coloring_result)
251-
end
252-
253-
function _sparse_jacobian_aux(
254-
f_or_f!y::FY,
255-
prep::PullbackSparseJacobianPrep{B},
256-
backend::AutoSparse,
257-
x,
258-
contexts::Vararg{Context,C},
259-
) where {FY,B,C}
260-
@compat (; coloring_result, batched_seeds, pullback_prep) = prep
261-
dense_backend = dense_ad(backend)
262-
Ng = length(row_groups(coloring_result))
263-
264-
pullback_prep_same = prepare_pullback_same_point(
265-
f_or_f!y..., pullback_prep, dense_backend, x, batched_seeds[1], contexts...
266-
)
267-
268-
compressed_blocks = map(eachindex(batched_seeds)) do a
269-
dx_batch = pullback(
270-
f_or_f!y...,
271-
pullback_prep_same,
272-
dense_backend,
273-
x,
274-
batched_seeds[a],
275-
contexts...,
276-
)
277-
stack(vec, dx_batch; dims=1)
278-
end
279-
280-
compressed_matrix = reduce(vcat, compressed_blocks)
281-
if Ng < size(compressed_matrix, 1)
282-
compressed_matrix = compressed_matrix[1:Ng, :]
283-
end
284-
return decompress(compressed_matrix, coloring_result)
285-
end
286-
287227
function _sparse_jacobian_aux!(
288228
f_or_f!y::FY,
289229
jac,

0 commit comments

Comments
 (0)