Skip to content

Commit b5e1c93

Browse files
Cache dense blocks with LinearSolve's own default solver
Follow-up on the concrete-typing commit: instead of resolving a dense algorithm ourselves and parameterising on the resulting cache type, each cached block now uses LinearSolve's default solver directly (`init(prob, nothing)`). Its cache type is `LinearCache{..., DefaultLinearSolver, DefaultLinearSolverInit{...}}`, which holds a slot for every candidate algorithm and picks between them with a runtime enum - so the type depends only on the element type, not on which algorithm the default happens to select on this machine. That makes the whole factorization type inferable from its inputs: `Core.Compiler.return_type(snlu, Tuple{SparseMatrixCSC{Float64,Int}})` is now concrete, and JET reports 0 runtime dispatches for construction as well as for solve! and snlu! (construction was 3 before). It is also simply what "use the LinearSolve default" should mean: the bespoke `_resolve_dense_alg` probe is gone. The extra slots cost nothing measurable - the block buffer dominates: 35 796 B vs 34 264 B at np = 64, and 2 112 724 B vs 2 105 816 B at np = 512. The factorization behind a block cache is pulled out of the slot the enum names, written as a literal-symbol chain so every branch stays concrete. src/SupernodalLU is now included after src/default.jl, since it references DefaultLinearSolver{,Init}. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 833c48b commit b5e1c93

4 files changed

Lines changed: 59 additions & 46 deletions

File tree

src/LinearSolve.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,6 @@ function defaultalg_symbol end
409409

410410
include("verbosity.jl")
411411
include("blas_logging.jl")
412-
include("SupernodalLU/SupernodalLU.jl")
413412
include("generic_lufact.jl")
414413
include("eigenvalue.jl")
415414
include("common.jl")
@@ -426,6 +425,9 @@ include("preconditioners.jl")
426425
include("preferences.jl")
427426
include("solve_function.jl")
428427
include("default.jl")
428+
# after default.jl: the vendored solver caches its dense diagonal blocks
429+
# with LinearSolve's own default solver, so it needs DefaultLinearSolver{,Init}
430+
include("SupernodalLU/SupernodalLU.jl")
429431
include("init.jl")
430432
include("adjoint.jl") # LinearSolveAdjoint struct definition only; rrules are in ChainRulesCore ext
431433

src/SupernodalLU/interface.jl

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -110,29 +110,16 @@ end
110110
# RFLU when RecursiveFactorization is loaded, MKL/LAPACK/generic otherwise;
111111
# resolved once at analysis time). Small blocks and generic element types
112112
# use the built-in static-perturbation kernel.
113-
# Size at which the dense default is probed. The dense algorithm is resolved
114-
# from the element type alone rather than per block, so the block-cache type -
115-
# and hence the factorization type - is a function of `(Tv, Ti, dense_alg)`
116-
# only. That is what lets `init_cacheval` build a prototype whose type matches
117-
# every real factorization (the same contract `PureUMFPACKFactorization`
118-
# follows), and it keeps `bcaches` concretely typed. `LinearCache` carries no
119-
# size parameter, so one algorithm gives one cache type for every block width.
120-
const DENSE_ALG_PROBE_SIZE = 128
121-
122-
# The dense algorithm used for every cached diagonal block. `A === nothing` is
123-
# LinearSolve's documented stand-in for "dense matrix" in `defaultalg` (a panel
124-
# block is a strided buffer, and we want the dense branch), so this resolves
125-
# exactly what the dense default would pick for this element type.
126-
function _resolve_dense_alg(::Type{Tv}, dense_alg) where {Tv}
127-
Tv <: LinearAlgebra.BlasFloat || return nothing
128-
dense_alg === nothing || return dense_alg
129-
b = zeros(Tv, DENSE_ALG_PROBE_SIZE)
130-
da = LinearSolve.defaultalg(nothing, b, LinearSolve.OperatorAssumptions(true))
131-
return da isa LinearSolve.DefaultLinearSolver ?
132-
LinearSolve.algchoice_to_alg(Symbol(da.alg)) : da
133-
end
134-
135-
# One dense block cache over an `np x np` buffer.
113+
# One dense block cache over an `np x np` buffer. `dense_alg === nothing`
114+
# means LinearSolve's default solver, which is the point: its cache type is
115+
# `LinearCache{..., DefaultLinearSolver, DefaultLinearSolverInit{...}}`, which
116+
# holds a slot for every candidate algorithm and picks between them with a
117+
# runtime enum. The type therefore depends only on the element type - not on
118+
# which algorithm the default happens to select on this machine - so the whole
119+
# factorization type is inferable from its inputs and `init_cacheval`'s empty
120+
# prototype matches every real factorization by construction. The extra slots
121+
# cost nothing measurable: the block buffer dominates (35 796 B vs 34 264 B at
122+
# np = 64, 2 112 724 B vs 2 105 816 B at np = 512).
136123
function _block_cache(::Type{Tv}, alg, np::Int) where {Tv}
137124
return SciMLBase.init(
138125
LinearSolve.LinearProblem(Matrix{Tv}(undef, np, np), zeros(Tv, np)),
@@ -142,10 +129,9 @@ function _block_cache(::Type{Tv}, alg, np::Int) where {Tv}
142129
)
143130
end
144131

145-
# Concrete element type of `bcaches` for this element type and algorithm.
146-
# `Nothing` when no block will ever be cached (non-BLAS element types).
132+
# Concrete element type of `bcaches`; `Nothing` when no block is ever cached.
147133
function _block_cache_type(::Type{Tv}, alg) where {Tv}
148-
alg === nothing && return Nothing
134+
Tv <: LinearAlgebra.BlasFloat || return Nothing
149135
return typeof(_block_cache(Tv, alg, 1))
150136
end
151137

@@ -154,7 +140,7 @@ function _dense_block_caches(
154140
) where {Tv, C}
155141
nsuper = length(sym.sstart) - 1
156142
bcaches = Vector{Union{Nothing, C}}(nothing, nsuper)
157-
alg === nothing && return bcaches
143+
C === Nothing && return bcaches
158144
for s in 1:nsuper
159145
np = sym.sstart[s + 1] - sym.sstart[s]
160146
np >= dense_threshold || continue
@@ -187,16 +173,9 @@ function _snlu_build(
187173
end
188174
V = M[sym.qf, sym.qf]
189175
Vt, Tpos = _transpose_map(V)
190-
dalg = _resolve_dense_alg(Tv, dense_alg)
191-
# The dense block algorithm - and hence the cache type `C` - is a runtime
192-
# choice (it follows LinearSolve's dense default, which depends on which
193-
# packages are loaded). Everything downstream of it is funnelled through
194-
# this barrier so the factorization body is fully typed: the single
195-
# dynamic call here is the only one in the construction path, and the
196-
# resulting `SupernodalLUFactor{Tv, Ti, C}` is concrete.
197176
return _snlu_assemble(
198-
_block_cache_type(Tv, dalg), sym, A, M, ms, W, Z, rowsfac, V, Vt, Tpos,
199-
dalg, dense_threshold, eps_pivot, threaded, check
177+
_block_cache_type(Tv, dense_alg), sym, A, M, ms, W, Z, rowsfac, V, Vt,
178+
Tpos, dense_alg, dense_threshold, eps_pivot, threaded, check
200179
)
201180
end
202181

@@ -205,10 +184,11 @@ function _snlu_assemble(
205184
M::SparseMatrixCSC{Tv, Ti}, ms::Union{MatchScale, Nothing},
206185
W::Vector{Matrix{Tv}}, Z::Vector{Matrix{Tv}}, rowsfac::Vector{Vector{Int}},
207186
V::SparseMatrixCSC{Tv, Ti}, Vt::SparseMatrixCSC{Tv, Ti}, Tpos::Vector{Int},
208-
dalg, dense_threshold::Int, eps_pivot::Float64, threaded::Bool, check::Bool
187+
dense_alg, dense_threshold::Int, eps_pivot::Float64, threaded::Bool,
188+
check::Bool
209189
) where {C, Tv, Ti <: Integer}
210190
n = sym.n
211-
bcaches = _dense_block_caches(Tv, C, sym, dalg, dense_threshold)
191+
bcaches = _dense_block_caches(Tv, C, sym, dense_alg, dense_threshold)
212192
F = SupernodalLUFactor{Tv, Ti, C}(
213193
sym, W, Z, collect(1:n), collect(1:n), rowsfac, 0, NaN, eps_pivot,
214194
A, Vector{Tv}(undef, n), Vector{Tv}(undef, 64),

src/SupernodalLU/numeric.jl

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ function _cache_lu!(
256256
end
257257
bc.isfresh = true
258258
SciMLBase.solve!(bc)
259-
fact = _lu_from_cacheval(bc.cacheval)
259+
fact = _block_lu_of(bc)
260260
fac = fact.factors
261261
@inbounds for j in 1:np
262262
abs(fac[j, j]) >= epsv || return false
@@ -274,6 +274,35 @@ end
274274
_lu_from_cacheval(cv::LinearAlgebra.LU) = cv
275275
_lu_from_cacheval(cv::Tuple) = _lu_from_cacheval(first(cv))
276276

277+
# The default solver keeps one slot per candidate algorithm and records the
278+
# choice in a runtime enum, so pull the factorization out of the slot that was
279+
# actually used. Written as a literal-symbol chain rather than
280+
# `getfield(cv, Symbol(choice))` so every branch stays concrete; the result is
281+
# a small union of `LU` types, which Julia union-splits.
282+
function _lu_from_cacheval(cv::LinearSolve.DefaultLinearSolverInit, choice)
283+
DAC = LinearSolve.DefaultAlgorithmChoice
284+
if choice === DAC.LUFactorization
285+
return _lu_from_cacheval(cv.LUFactorization)
286+
elseif choice === DAC.RFLUFactorization
287+
return _lu_from_cacheval(cv.RFLUFactorization)
288+
elseif choice === DAC.MKLLUFactorization
289+
return _lu_from_cacheval(cv.MKLLUFactorization)
290+
elseif choice === DAC.AppleAccelerateLUFactorization
291+
return _lu_from_cacheval(cv.AppleAccelerateLUFactorization)
292+
elseif choice === DAC.GenericLUFactorization
293+
return _lu_from_cacheval(cv.GenericLUFactorization)
294+
elseif choice === DAC.BLISLUFactorization
295+
return _lu_from_cacheval(cv.BLISLUFactorization)
296+
end
297+
throw(ArgumentError("SupernodalLU: dense block solver resolved to $choice, which does not expose an LU factorization; pass `dense_alg` explicitly"))
298+
end
299+
300+
# Factorization behind a block cache, whichever solver the cache is using.
301+
@inline _block_lu_of(bc) = _lu_from_cacheval(bc.cacheval)
302+
@inline function _block_lu_of(bc::LinearSolve.LinearCache{<:Any, <:Any, <:Any, <:Any, <:LinearSolve.DefaultLinearSolver})
303+
return _lu_from_cacheval(bc.cacheval, bc.alg.alg)
304+
end
305+
277306
# Transpose of V with a position map: Vt.nzval[q] == V.nzval[Tpos[q]] forever
278307
# (structure is static), so refactorization refreshes Vt by one gather pass.
279308
function _transpose_map(V::SparseMatrixCSC{Tv, Ti}) where {Tv, Ti}

test/qa/jet.jl

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,12 @@ end
282282
@test eltype(F_snlu.bcaches) !== Any
283283
@test isconcretetype(typeof(F_snlu).parameters[3])
284284

285-
# Construction resolves the dense block algorithm at runtime (it follows
286-
# LinearSolve's dense default, which depends on what is loaded), so the
287-
# cache type - and hence the factorization type - is only known then. The
288-
# choice is funnelled through one function barrier, so this is a handful of
289-
# dispatches in a one-time path, not in the repeated ones asserted above.
290-
JET.@test_opt target_modules = (SNLU_MOD,) SNLU_MOD.snlu(A_snlu) broken = true
285+
# Construction is inferable too: the block caches use LinearSolve's own
286+
# default solver, whose cache type holds a slot per candidate algorithm and
287+
# selects with a runtime enum, so the type does not depend on which
288+
# algorithm the default picks on this machine.
289+
JET.@test_opt target_modules = (SNLU_MOD,) SNLU_MOD.snlu(A_snlu)
290+
@test isconcretetype(
291+
Core.Compiler.return_type(SNLU_MOD.snlu, Tuple{typeof(A_snlu)})
292+
)
291293
end

0 commit comments

Comments
 (0)