Skip to content

Commit 950b68f

Browse files
Store the dense block caches sparsely, dropping the Union element type
Only the blocks at or above dense_threshold get a LinearSolve cache; everything narrower uses the built-in kernel. That is the overwhelming majority - 0.6 % of supernodes are cached on poisson2d_256 and 1.3 % on poisson3d_28, where the median supernode is 5 and 1 columns wide - so a full-length Vector{Union{Nothing,C}} was 99 % empty and forced a nullability check on every lookup. bcaches now holds only the blocks that have a cache, with bcacheidx[s] indexing into it (0 = use the built-in kernel). The element type is plainly concrete rather than a two-member union, the lookup is an integer test, and the factorization type stays inferable from its inputs (return_type(snlu, ...) concrete). For element types that never cache a block it degenerates to an empty Vector{Nothing} with an all-zero index. Caching every supernode instead was considered and rejected on the numbers: a default-solver cache costs ~1476 B even for a 4x4 block, so poisson2d_256 alone would spend ~8 MB to make small blocks slower than the built-in kernel - the same size effect that motivates PANEL_BLAS_CUTOFF in the solve sweeps. GROUP=Core and GROUP=QA both pass. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b5e1c93 commit 950b68f

4 files changed

Lines changed: 29 additions & 18 deletions

File tree

src/SupernodalLU/interface.jl

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,16 @@ function _dense_block_caches(
139139
::Type{Tv}, ::Type{C}, sym::SymbolicAnalysis, alg, dense_threshold::Int
140140
) where {Tv, C}
141141
nsuper = length(sym.sstart) - 1
142-
bcaches = Vector{Union{Nothing, C}}(nothing, nsuper)
143-
C === Nothing && return bcaches
142+
bcaches = C[]
143+
bcacheidx = zeros(Int, nsuper)
144+
C === Nothing && return bcaches, bcacheidx
144145
for s in 1:nsuper
145146
np = sym.sstart[s + 1] - sym.sstart[s]
146147
np >= dense_threshold || continue
147-
bcaches[s] = _block_cache(Tv, alg, np)
148+
push!(bcaches, _block_cache(Tv, alg, np))
149+
bcacheidx[s] = length(bcaches)
148150
end
149-
return bcaches
151+
return bcaches, bcacheidx
150152
end
151153

152154
# Allocate all numeric state (panels, V/Vt + position maps, every workspace)
@@ -188,7 +190,7 @@ function _snlu_assemble(
188190
check::Bool
189191
) where {C, Tv, Ti <: Integer}
190192
n = sym.n
191-
bcaches = _dense_block_caches(Tv, C, sym, dense_alg, dense_threshold)
193+
bcaches, bcacheidx = _dense_block_caches(Tv, C, sym, dense_alg, dense_threshold)
192194
F = SupernodalLUFactor{Tv, Ti, C}(
193195
sym, W, Z, collect(1:n), collect(1:n), rowsfac, 0, NaN, eps_pivot,
194196
A, Vector{Tv}(undef, n), Vector{Tv}(undef, 64),
@@ -201,7 +203,7 @@ function _snlu_assemble(
201203
Matrix{Tv}(undef, n, 0),
202204
Vector{Tv}(undef, n), Vector{Tv}(undef, n), Vector{Tv}(undef, n),
203205
threaded,
204-
bcaches
206+
bcaches, bcacheidx
205207
)
206208
_build_vpos!(F, A)
207209
_factor_core!(F)

src/SupernodalLU/numeric.jl

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,18 @@ mutable struct SupernodalLUFactor{Tv, Ti <: Integer, C}
7777
ir_dx::Vector{Tv} # iterative-refinement correction
7878
btmp::Vector{Tv} # in-place ldiv! RHS copy
7979
threaded::Bool # use the etree-parallel numeric phase
80-
# Dense LinearSolve caches for the large diagonal blocks; `nothing` marks a
81-
# block that uses the built-in kernel. `C` is the concrete cache type,
82-
# fixed by `(Tv, dense_alg)` alone (see `_resolve_dense_alg`), so the
83-
# element type is a two-member union Julia union-splits into a static call
84-
# in `_cache_lu!` and the whole factorization type is inferable from the
85-
# inputs. `C === Nothing` for element types that never cache a block.
86-
bcaches::Vector{Union{Nothing, C}}
80+
# Dense LinearSolve caches for the diagonal blocks at or above
81+
# `dense_threshold`; everything narrower uses the built-in kernel, which is
82+
# the overwhelming majority (0.6 % of supernodes are cached on
83+
# poisson2d_256, 1.3 % on poisson3d_28 - the median supernode is 1-5
84+
# columns wide). Stored sparsely: `bcaches` holds only the blocks that
85+
# have one, and `bcacheidx[s]` indexes into it, 0 meaning "no cache". That
86+
# keeps the element type concrete (`C`, not `Union{Nothing,C}`) and avoids
87+
# a mostly-empty full-length vector. `C` is fixed by `(Tv, dense_alg)`
88+
# alone, so the whole factorization type is inferable from the inputs;
89+
# `C === Nothing` for element types that never cache a block.
90+
bcaches::Vector{C}
91+
bcacheidx::Vector{Int}
8792
end
8893

8994
nperturbed(F::SupernodalLUFactor) = F.nperturbed
@@ -234,9 +239,9 @@ function _dense_block_factor!(
234239
F, s::Int, np::Int, epsv, ipiv::AbstractVector{Int}, scratch
235240
)
236241
Ws = F.W[s]
237-
bc = F.bcaches[s]
238-
bc === nothing && return _block_lu!(Ws, np, epsv, ipiv)
239-
ok = _cache_lu!(bc, Ws, np, epsv, ipiv)
242+
i = F.bcacheidx[s]
243+
i == 0 && return _block_lu!(Ws, np, epsv, ipiv)
244+
ok = _cache_lu!(F.bcaches[i], Ws, np, epsv, ipiv)
240245
ok && return 0
241246
@inbounds for j in 1:np
242247
ipiv[j] = j

test/Core/supernodal_lu.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ end
5353
# `LinearSolution` object each dense block cache's `solve!` returns —
5454
# a fixed ~64 B per cached supernode, independent of problem size.
5555
@test (@allocated SNLU.solve!(x, F, b)) == 0
56-
ncache = count(!isnothing, F.bcaches)
56+
ncache = length(F.bcaches)
5757
@test (@allocated SNLU.snlu!(F, A)) <= 128 * max(ncache, 1)
5858
# with the dense caches disabled the built-in kernel is fully allocation-free
5959
Fb = SNLU.snlu(A; dense_threshold = typemax(Int))
@@ -64,10 +64,12 @@ end
6464
@testset "block caches are concretely typed and inference-clean" begin
6565
A = poisson2d(70)
6666
F = SNLU.snlu(A)
67-
@test count(!isnothing, F.bcaches) > 0
67+
@test length(F.bcaches) > 0
6868
# the element type is a two-member union of Nothing and one concrete cache
6969
# type - never Any - so the whole factorization type is inferable
7070
@test eltype(F.bcaches) !== Any
71+
@test isconcretetype(eltype(F.bcaches))
72+
@test count(!iszero, F.bcacheidx) == length(F.bcaches)
7173
@test isconcretetype(typeof(F).parameters[3])
7274
@test isconcretetype(typeof(F))
7375
b = randn(size(A, 1))
@@ -78,6 +80,7 @@ end
7880
T = spdiagm(0 => fill(big"4.0", 30), 1 => fill(big"-1.0", 29), -1 => fill(big"-1.0", 29))
7981
Fb = SNLU.snlu(T)
8082
@test typeof(Fb.bcaches) === Vector{Nothing}
83+
@test all(iszero, Fb.bcacheidx)
8184
@test isconcretetype(typeof(Fb))
8285
# the LinearSolve cacheval prototype must have the same type as a real
8386
# factorization, for every dense_alg setting

test/qa/jet.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ end
280280
# is a two-member union rather than Any.
281281
@test isconcretetype(typeof(F_snlu))
282282
@test eltype(F_snlu.bcaches) !== Any
283+
@test isconcretetype(eltype(F_snlu.bcaches))
283284
@test isconcretetype(typeof(F_snlu).parameters[3])
284285

285286
# Construction is inferable too: the block caches use LinearSolve's own

0 commit comments

Comments
 (0)