Skip to content

Commit eec3574

Browse files
Merge pull request #289 from SciML/conditioning_assumption
Expand OperatorAssumptions to allow for choosing conditioning
2 parents 6cbe99d + d4f8a8c commit eec3574

File tree

9 files changed

+125
-11
lines changed

9 files changed

+125
-11
lines changed

Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ version = "1.40.0"
66
[deps]
77
ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9"
88
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
9+
EnumX = "4e289a0a-7415-4d19-859d-a7e5c4648b56"
910
FastLapackInterface = "29a986be-02c6-4525-aec4-84b980013641"
1011
GPUArraysCore = "46192b85-c4d5-4398-a991-12ede77f4527"
1112
IterativeSolvers = "42fd0dbc-a981-5370-80f2-aaf504508153"

docs/pages.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pages = ["index.md",
66
"Basics" => Any["basics/LinearProblem.md",
77
"basics/common_solver_opts.md",
88
"basics/CachingAPI.md",
9+
"basics/OperatorAssumptions.md",
910
"basics/Preconditioners.md",
1011
"basics/FAQ.md"],
1112
"Solvers" => Any["solvers/solvers.md"],

docs/src/basics/FAQ.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ Note that if `\` is good enough for you, great! We still tend to use `\` in the
99
However, if you're building a package, you may want to consider using LinearSolve.jl for the improved
1010
efficiency and ability to choose solvers.
1111

12+
## I'm seeing some dynamic dispatches in the default algorithm choice, how do I reduce that?
13+
14+
Make sure you set the `OperatorAssumptions` to get the full performance, especially the `issquare` choice
15+
as otherwise that will need to be determined at runtime.
16+
17+
## I found a faster algorithm that can be used than what LinearSolve.jl chose?
18+
19+
What assumptions are made as part of your method? If your method only works on well-conditioned operators, then
20+
make sure you set the `WellConditioned` assumption in the `assumptions`. See the
21+
[OperatorAssumptions page for more details](@ref assumptions). If using the right assumptions does not improve
22+
the performance to the expected state, please open an issue and we will improve the default algorithm.
23+
1224
## Python's NumPy/SciPy just calls fast Fortran/C code, why would LinearSolve.jl be any better?
1325

1426
This is addressed in the [JuliaCon 2022 video](https://www.youtube.com/watch?v=JWI34_w-yYw&t=182s). This happens in
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# [Linear Solve Operator Assumptions](@id assumptions)
2+
3+
```@docs
4+
OperatorAssumptions
5+
OperatorCondition
6+
```
7+
8+
## Condition Number Specifications
9+
10+
```@docs
11+
IllConditioned
12+
VeryIllConditioned
13+
SuperIllConditioned
14+
WellConditioned
15+
```

docs/src/basics/common_solver_opts.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ The following are the options these algorithms take, along with their defaults.
1414
algorithms can write and change `b` upon usage. Care must be taken as the
1515
original input will be modified. Default is `false`.
1616
- `verbose`: Whether to print extra information. Defaults to `false`.
17-
17+
- `assumptions`: Sets the assumptions of the operator in order to effect the default
18+
choice algorithm. See the [Operator Assumptions page for more details](@ref assumptions).
1819
## Iterative Solver Controls
1920

2021
Error controls are not used by all algorithms. Specifically, direct solves always

src/LinearSolve.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ using KLU
2020
using Sparspak
2121
using FastLapackInterface
2222
using DocStringExtensions
23+
using EnumX
2324
import GPUArraysCore
2425
import Preferences
2526

src/common.jl

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,67 @@
1-
struct OperatorAssumptions{issq} end
2-
function OperatorAssumptions(issquare = nothing)
1+
"""
2+
`OperatorCondition`
3+
4+
Specifies the assumption of matrix conditioning for the default linear solver choices. Condition number
5+
is defined as the ratio of eigenvalues. The numerical stability of many linear solver algorithms
6+
can be dependent on the condition number of the matrix. The condition number can be computed as:
7+
8+
```julia
9+
using LinearAlgebra
10+
cond(rand(100,100))
11+
```
12+
13+
However, in practice this computation is very expensive and thus not possible for most practical cases.
14+
Therefore, OperatorCondition lets one share to LinearSolve the expected conditioning. The higher the
15+
expected condition number, the safer the algorithm needs to be and thus there is a trade-off between
16+
numerical performance and stability. By default the method assumes the operator may be ill-conditioned
17+
for the standard linear solvers to converge (such as LU-factorization), though more extreme
18+
ill-conditioning or well-conditioning could be the case and specified through this assumption.
19+
"""
20+
EnumX.@enumx OperatorCondition begin
21+
"""
22+
`OperatorCondition.IllConditioned`
23+
24+
The default assumption of LinearSolve. Assumes that the operator can have minor ill-conditioning
25+
and thus needs to use safe algorithms.
26+
"""
27+
IllConditioned
28+
"""
29+
`OperatorCondition.VeryIllConditioned`
30+
31+
Assumes that the operator can have fairly major ill-conditioning and thus the standard linear algebra
32+
algorithms cannot be used.
33+
"""
34+
VeryIllConditioned
35+
"""
36+
`OperatorCondition.SuperIllConditioned`
37+
38+
Assumes that the operator can have fairly extreme ill-conditioning and thus the most stable algorithm
39+
is used.
40+
"""
41+
SuperIllConditioned
42+
"""
43+
`OperatorCondition.WellConditioned`
44+
45+
Assumes that the operator can have fairly contained conditioning and thus the fastest algorithm is
46+
used.
47+
"""
48+
WellConditioned
49+
end
50+
51+
"""
52+
OperatorAssumptions(issquare = nothing; condition::OperatorCondition.T = IllConditioned)
53+
54+
Sets the operator `A` assumptions used as part of the default algorithm
55+
"""
56+
struct OperatorAssumptions{issq,condition} end
57+
function OperatorAssumptions(issquare = nothing; condition::OperatorCondition.T = OperatorCondition.IllConditioned)
358
issq = something(_unwrap_val(issquare), Nothing)
4-
OperatorAssumptions{issq}()
59+
condition = _unwrap_val(condition)
60+
OperatorAssumptions{issq,condition}()
561
end
6-
__issquare(::OperatorAssumptions{issq}) where {issq} = issq
62+
__issquare(::OperatorAssumptions{issq,condition}) where {issq,condition} = issq
63+
__conditioning(::OperatorAssumptions{issq,condition}) where {issq,condition} = condition
64+
765

866
struct LinearCache{TA, Tb, Tu, Tp, Talg, Tc, Tl, Tr, Ttol, issq}
967
A::TA

src/default.jl

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,22 +66,30 @@ end
6666
end
6767
end
6868

69-
function defaultalg(A::GPUArraysCore.AbstractGPUArray, b, ::OperatorAssumptions{true})
69+
function defaultalg(A::GPUArraysCore.AbstractGPUArray, b, assump::OperatorAssumptions{true})
7070
if VERSION >= v"1.8-"
7171
LUFactorization()
7272
else
7373
QRFactorization()
7474
end
7575
end
7676

77-
function defaultalg(A, b::GPUArraysCore.AbstractGPUArray, ::OperatorAssumptions{true})
77+
function defaultalg(A::GPUArraysCore.AbstractGPUArray, b, assump::OperatorAssumptions{true,OperatorCondition.IllConditioned})
78+
QRFactorization()
79+
end
80+
81+
function defaultalg(A, b::GPUArraysCore.AbstractGPUArray, assump::OperatorAssumptions{true})
7882
if VERSION >= v"1.8-"
7983
LUFactorization()
8084
else
8185
QRFactorization()
8286
end
8387
end
8488

89+
function defaultalg(A, b::GPUArraysCore.AbstractGPUArray, assump::OperatorAssumptions{true,OperatorCondition.IllConditioned})
90+
QRFactorization()
91+
end
92+
8593
function defaultalg(A::SciMLBase.AbstractSciMLOperator, b,
8694
assumptions::OperatorAssumptions{true})
8795
if has_ldiv!(A)
@@ -121,6 +129,11 @@ function defaultalg(A::GPUArraysCore.AbstractGPUArray, b::GPUArraysCore.Abstract
121129
end
122130
end
123131

132+
function defaultalg(A::GPUArraysCore.AbstractGPUArray, b::GPUArraysCore.AbstractGPUArray,
133+
::OperatorAssumptions{true,OperatorCondition.IllConditioned})
134+
QRFactorization()
135+
end
136+
124137
function defaultalg(A::GPUArraysCore.AbstractGPUArray, b, ::OperatorAssumptions{false})
125138
QRFactorization()
126139
end
@@ -136,13 +149,13 @@ function defaultalg(A::GPUArraysCore.AbstractGPUArray, b::GPUArraysCore.Abstract
136149
end
137150

138151
# Allows A === nothing as a stand-in for dense matrix
139-
function defaultalg(A, b, ::OperatorAssumptions{true})
152+
function defaultalg(A, b, assump::OperatorAssumptions{true})
140153
# Special case on Arrays: avoid BLAS for RecursiveFactorization.jl when
141154
# it makes sense according to the benchmarks, which is dependent on
142155
# whether MKL or OpenBLAS is being used
143156
if (A === nothing && !(b isa GPUArraysCore.AbstractGPUArray)) || A isa Matrix
144157
if (A === nothing || eltype(A) <: Union{Float32, Float64, ComplexF32, ComplexF64}) &&
145-
ArrayInterface.can_setindex(b)
158+
ArrayInterface.can_setindex(b) && __conditioning(assump) === OperatorCondition.IllConditioned
146159
if length(b) <= 10
147160
alg = GenericLUFactorization()
148161
elseif (length(b) <= 100 || (isopenblas() && length(b) <= 500)) &&
@@ -154,6 +167,10 @@ function defaultalg(A, b, ::OperatorAssumptions{true})
154167
else
155168
alg = LUFactorization()
156169
end
170+
elseif __conditioning(assump) === OperatorCondition.VeryIllConditioned
171+
alg = QRFactorization()
172+
elseif __conditioning(assump) === OperatorCondition.SuperIllConditioned
173+
alg = SVDFactorization(false, LinearAlgebra.QRIteration())
157174
else
158175
alg = LUFactorization()
159176
end
@@ -170,10 +187,18 @@ function defaultalg(A, b, ::OperatorAssumptions{true})
170187
alg
171188
end
172189

173-
function defaultalg(A, b, ::OperatorAssumptions{false})
190+
function defaultalg(A, b, ::OperatorAssumptions{false,OperatorCondition.IllConditioned})
174191
QRFactorization()
175192
end
176193

194+
function defaultalg(A, b, ::OperatorAssumptions{false,OperatorCondition.VeryIllConditioned})
195+
QRFactorization()
196+
end
197+
198+
function defaultalg(A, b, ::OperatorAssumptions{false,OperatorCondition.SuperIllConditioned})
199+
SVDFactorization(false, LinearAlgebra.QRIteration())
200+
end
201+
177202
## Catch high level interface
178203

179204
function SciMLBase.solve(cache::LinearCache, alg::Nothing,

test/default_algs.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ using LinearSolve, LinearAlgebra, SparseArrays, Test
66
DiagonalFactorization
77

88
@test LinearSolve.defaultalg(nothing, zeros(5),
9-
LinearSolve.OperatorAssumptions{false}()) isa QRFactorization
9+
LinearSolve.OperatorAssumptions(false)) isa QRFactorization
1010

1111
@test LinearSolve.defaultalg(sprand(1000, 1000, 0.01), zeros(1000)) isa KLUFactorization
1212
@test LinearSolve.defaultalg(sprand(11000, 11000, 0.001), zeros(11000)) isa

0 commit comments

Comments
 (0)