Skip to content

Commit 4c5b1ff

Browse files
ChrisRackauckas-ClaudeChrisRackauckasclaude
authored
Route the structured-sparse default LU to SupernodalLUFactorization (#1109)
Renames the DefaultAlgorithmChoice enum entry UMFPACKFactorization -> SupernodalLUFactorization (and the matching DefaultLinearSolverInit cacheval field), so the structured-sparse slot of the default polyalgorithm is the pure-Julia supernodal left-right-looking LU. Explicit UMFPACKFactorization() requests are unchanged; the enum entry is not public API (users select algorithms by type, not by enum), so this is not a breaking change. Both sparse LU default slots now resolve to pure-Julia solvers, so the LU side of the sparse default no longer depends on Base.USE_GPL_LIBS; previously the non-GPL build sent structured systems to the scalar KLU path (131-195s on poisson3d_48 vs 3.8s here). The QR side keeps its SPQR gating. Benchmark basis (SciMLBenchmarks SparsePDE fdmatrix sweep 1D/2D/3D to n~164k, plus 18 SuiteSparse-collection matrices, accuracy-aware with res <= 1e-6 required to count as a win): UMFPACK won 0 of 45 PDE-sweep rows and 1 of 18 collection matrices (bayer01: fully unsymmetric pattern, numerically tame - the one regime where COLAMD-style unsymmetric ordering beats pattern symmetrization; SupernodalLU still won its refactorization). KLU keeps banded/1D, circuits, and small n, which is the klulike side of the split and is unaffected. Known boundary case: large circuit matrices (scircuit, n=171k) fall on the structured side of use_klulike_sparse_structure but KLU is ~1.9x faster there. Density alone cannot separate circuits from 2D meshes (torso2 is sparser than scircuit yet SupernodalLU wins it 3x); a predicted-fill probe would fix the routing and is left as a follow-up. GROUP=Core passes. Co-authored-by: Chris Rackauckas <accounts@chrisrackauckas.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 6df384d commit 4c5b1ff

5 files changed

Lines changed: 41 additions & 43 deletions

File tree

docs/src/solvers/solvers.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,12 @@ when reduced precision is acceptable for the factorization step.
6363

6464
For sparse LU-factorizations, `PureKLUFactorization` (a pure-Julia KLU with no
6565
SuiteSparse dependency, the default) if there is less structure to the sparsity
66-
pattern and `UMFPACKFactorization` if there is more structure. The SuiteSparse-backed
67-
`KLUFactorization` remains available as an explicit alternative.
66+
pattern and `SupernodalLUFactorization` (a pure-Julia supernodal
67+
left–right-looking LU implementing the Schenk & Gärtner method, vendored in
68+
`src/SupernodalLU`, no binary dependency) if there is more structure — so the
69+
default sparse LU stack is entirely pure Julia and no longer depends on
70+
`Base.USE_GPL_LIBS`. The SuiteSparse-backed `KLUFactorization` and
71+
`UMFPACKFactorization` remain available as explicit alternatives.
6872
For sparse QR-factorizations (used for non-square, rank-deficient, or least-squares
6973
systems, and as the fallback when the sparse LU hits a (near-)singular matrix), the
7074
same less-structure/more-structure split as the LU case applies:
@@ -248,8 +252,9 @@ SparseColumnPivotedQRFactorization
248252
left–right-looking sparse LU method of Schenk & Gärtner (vendored
249253
self-contained in `src/SupernodalLU`, no binary dependency). It is the
250254
strongest choice for "more structured" (PDE-mesh-like) sparse systems,
251-
where it outperforms both `UMFPACKFactorization` and `KLUFactorization`.
252-
It is not yet wired into the default polyalgorithm; request it explicitly.
255+
where it outperforms both `UMFPACKFactorization` and `KLUFactorization`,
256+
and it is the default for such systems in the polyalgorithm (the
257+
`UMFPACKFactorization` slot resolves to it).
253258

254259
!!! note
255260

ext/LinearSolveSparseArraysExt.jl

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,42 +1102,35 @@ function LinearSolve.use_klulike_sparse_structure(A::AbstractSparseMatrixCSC, b)
11021102
(size(b, 1) <= 10_000 && length(nonzeros(A)) / length(A) < 2.0e-4)
11031103
end
11041104

1105-
@static if Base.USE_GPL_LIBS
1106-
function LinearSolve.defaultalg(
1107-
A::AbstractSparseMatrixCSC{<:Union{Float64, ComplexF64}, Ti}, b,
1108-
assump::OperatorAssumptions{Bool}
1109-
) where {Ti}
1110-
klulike = LinearSolve.use_klulike_sparse_structure(A, b)
1111-
if assump.issq
1112-
# Less structure → PureKLU (pure-Julia); more structure → UMFPACK.
1113-
if klulike
1114-
LinearSolve.DefaultLinearSolver(LinearSolve.DefaultAlgorithmChoice.KLUFactorization)
1115-
else
1116-
LinearSolve.DefaultLinearSolver(LinearSolve.DefaultAlgorithmChoice.UMFPACKFactorization)
1117-
end
1105+
function LinearSolve.defaultalg(
1106+
A::AbstractSparseMatrixCSC{<:Union{Float64, ComplexF64}, Ti}, b,
1107+
assump::OperatorAssumptions{Bool}
1108+
) where {Ti}
1109+
klulike = LinearSolve.use_klulike_sparse_structure(A, b)
1110+
return if assump.issq
1111+
# Less structure → PureKLU; more structure → the supernodal LU. Both
1112+
# are pure Julia, so the sparse LU default does not depend on
1113+
# Base.USE_GPL_LIBS.
1114+
if klulike
1115+
LinearSolve.DefaultLinearSolver(LinearSolve.DefaultAlgorithmChoice.KLUFactorization)
11181116
else
1119-
# Same split for sparse QR: less structure → SparseColumnPivotedQR
1120-
# (pure-Julia); more structure → SuiteSparse SPQR (`QRFactorization`).
1117+
LinearSolve.DefaultLinearSolver(LinearSolve.DefaultAlgorithmChoice.SupernodalLUFactorization)
1118+
end
1119+
else
1120+
@static if Base.USE_GPL_LIBS
1121+
# Sparse QR: less structure → SparseColumnPivotedQR (pure-Julia);
1122+
# more structure → SuiteSparse SPQR (`QRFactorization`).
11211123
if klulike
11221124
LinearSolve.DefaultLinearSolver(LinearSolve.DefaultAlgorithmChoice.SparseColumnPivotedQRFactorization)
11231125
else
11241126
LinearSolve.DefaultLinearSolver(LinearSolve.DefaultAlgorithmChoice.QRFactorization)
11251127
end
1126-
end
1127-
end
1128-
else
1129-
function LinearSolve.defaultalg(
1130-
A::AbstractSparseMatrixCSC{<:Union{Float64, ComplexF64}, Ti}, b,
1131-
assump::OperatorAssumptions{Bool}
1132-
) where {Ti}
1133-
# No SuiteSparse (UMFPACK/SPQR) available: always use the pure-Julia solvers.
1134-
if assump.issq
1135-
LinearSolve.DefaultLinearSolver(LinearSolve.DefaultAlgorithmChoice.KLUFactorization)
11361128
else
1129+
# No SuiteSparse SPQR available: pure-Julia sparse QR throughout.
11371130
LinearSolve.DefaultLinearSolver(LinearSolve.DefaultAlgorithmChoice.SparseColumnPivotedQRFactorization)
11381131
end
11391132
end
1140-
end # @static if Base.USE_GPL_LIBS
1133+
end
11411134

11421135
# SPQR Handling
11431136
function LinearSolve.init_cacheval(

src/LinearSolve.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ EnumX.@enumx DefaultAlgorithmChoice begin
310310
DirectLdiv!
311311
SparspakFactorization
312312
KLUFactorization
313-
UMFPACKFactorization
313+
SupernodalLUFactorization
314314
KrylovJL_GMRES
315315
GenericLUFactorization
316316
RFLUFactorization

src/default.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ mutable struct DefaultLinearSolverInit{
1010
DirectLdiv!::T4
1111
SparspakFactorization::T5
1212
KLUFactorization::T6
13-
UMFPACKFactorization::T7
13+
SupernodalLUFactorization::T7
1414
KrylovJL_GMRES::T8
1515
GenericLUFactorization::T9
1616
RFLUFactorization::T10
@@ -35,7 +35,7 @@ mutable struct DefaultLinearSolverInit{
3535
a_backup_allocated::Bool # true once A_backup has been replaced with a private buffer
3636
fell_back_to_qr::Bool # true after QR fallback; reuse QR until matrix is refreshed
3737
# Persistent-nonstructural-zero reduction state, shared across the sparse
38-
# sub-algorithm slots (KLU/UMFPACK + the QR fallback all factor the one reduced
38+
# sub-algorithm slots (the two sparse LU slots + the QR fallback all factor
3939
# matrix). `nothing` when inactive / non-sparse. See `init_sparse_reduction`.
4040
sparse_reduction::TR
4141
end
@@ -458,8 +458,8 @@ function algchoice_to_alg(alg::Symbol)
458458
# PureKLU. The SuiteSparse `KLUFactorization` is unchanged and remains
459459
# available when requested explicitly.
460460
PureKLUFactorization()
461-
elseif alg === :UMFPACKFactorization
462-
UMFPACKFactorization()
461+
elseif alg === :SupernodalLUFactorization
462+
SupernodalLUFactorization()
463463
elseif alg === :KrylovJL_GMRES
464464
KrylovJL_GMRES()
465465
elseif alg === :GenericLUFactorization
@@ -602,7 +602,7 @@ defaultalg_symbol(::Type{<:QRFactorization{ColumnNorm}}) = :QRFactorizationPivot
602602
const _SPARSE_ONLY_ALGORITHMS = Symbol.(
603603
(
604604
DefaultAlgorithmChoice.KLUFactorization,
605-
DefaultAlgorithmChoice.UMFPACKFactorization,
605+
DefaultAlgorithmChoice.SupernodalLUFactorization,
606606
DefaultAlgorithmChoice.SparspakFactorization,
607607
DefaultAlgorithmChoice.CHOLMODFactorization,
608608
DefaultAlgorithmChoice.SparseColumnPivotedQRFactorization,
@@ -616,7 +616,7 @@ const _SPARSE_ONLY_ALGORITHMS = Symbol.(
616616
const _SPARSE_LU_FALLBACK_ALGORITHMS = Symbol.(
617617
(
618618
DefaultAlgorithmChoice.KLUFactorization,
619-
DefaultAlgorithmChoice.UMFPACKFactorization,
619+
DefaultAlgorithmChoice.SupernodalLUFactorization,
620620
DefaultAlgorithmChoice.SparspakFactorization,
621621
)
622622
)
@@ -717,7 +717,7 @@ end
717717
_do_sparse_qr_fallback(cache::LinearCache, alg, sol, reason::Symbol)
718718
719719
Perform column-pivoted sparse QR (`SparseColumnPivotedQRFactorization`) fallback
720-
after a sparse LU (`KLUFactorization`, `UMFPACKFactorization`,
720+
after a sparse LU (`KLUFactorization`, `SupernodalLUFactorization`,
721721
`SparspakFactorization`) solve failed or produced non-finite output.
722722
723723
Sparse LU does not modify `cache.A` in place — UMFPACK and KLU wrap a
@@ -1125,7 +1125,7 @@ end
11251125
DefaultAlgorithmChoice.LUFactorization,
11261126
DefaultAlgorithmChoice.QRFactorization,
11271127
DefaultAlgorithmChoice.KLUFactorization,
1128-
DefaultAlgorithmChoice.UMFPACKFactorization,
1128+
DefaultAlgorithmChoice.SupernodalLUFactorization,
11291129
DefaultAlgorithmChoice.LDLtFactorization,
11301130
DefaultAlgorithmChoice.SparspakFactorization,
11311131
DefaultAlgorithmChoice.BunchKaufmanFactorization,

test/Core/default_algs.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ prob = LinearProblem(sprand(1000, 1000, 0.5), zeros(1000))
121121
solve(prob)
122122

123123
@test LinearSolve.defaultalg(sprand(11000, 11000, 0.001), zeros(11000)).alg ===
124-
LinearSolve.DefaultAlgorithmChoice.UMFPACKFactorization
124+
LinearSolve.DefaultAlgorithmChoice.SupernodalLUFactorization
125125
prob = LinearProblem(sprand(11000, 11000, 0.5), zeros(11000))
126126
solve(prob)
127127

@@ -491,7 +491,7 @@ let
491491
# Just past the fast-path boundary on a dense matrix → UMFPACK
492492
A_past = sprand(1_001, 1_001, 0.5) + I
493493
@test LinearSolve.defaultalg(A_past, rand(1_001), LinearSolve.OperatorAssumptions(true)).alg ===
494-
LinearSolve.DefaultAlgorithmChoice.UMFPACKFactorization
494+
LinearSolve.DefaultAlgorithmChoice.SupernodalLUFactorization
495495

496496
# Medium-size, very sparse (density < 2e-4) → density branch picks KLU
497497
n = 9_000
@@ -503,7 +503,7 @@ let
503503
# Medium-size, dense sparse → UMFPACK
504504
A_med_dense = sprand(5_000, 5_000, 0.5) + I
505505
@test LinearSolve.defaultalg(A_med_dense, rand(5_000), LinearSolve.OperatorAssumptions(true)).alg ===
506-
LinearSolve.DefaultAlgorithmChoice.UMFPACKFactorization
506+
LinearSolve.DefaultAlgorithmChoice.SupernodalLUFactorization
507507
end
508508

509509
# === Sparse LU → SPQR fallback ===
@@ -559,7 +559,7 @@ let
559559
A_u[1, :] .= 0
560560
A_u = sparse(A_u)
561561
@test LinearSolve.defaultalg(A_u, ones(n_u), LinearSolve.OperatorAssumptions(true)).alg ===
562-
LinearSolve.DefaultAlgorithmChoice.UMFPACKFactorization
562+
LinearSolve.DefaultAlgorithmChoice.SupernodalLUFactorization
563563
sol_u = solve(LinearProblem(A_u, ones(n_u)))
564564
@test sol_u.retcode === ReturnCode.Success
565565
# Sanity: a non-singular matrix should NOT fall back.

0 commit comments

Comments
 (0)