Skip to content

Commit cbfd6b7

Browse files
ChrisRackauckas-ClaudeChrisRackauckasclaude
authored
SupernodalLU: hand-rolled panel solve kernels below a size cutoff (-27 to -42%) (#1112)
The solve sweeps issued BLAS trsv/gemv per supernode panel, but the panels are tiny: on a 2D Poisson factorization the pivot block width np has mean ~12 and median 6, so a 12x12 unit-triangular solve (~72 flops) measured ~260 ns through BLAS - almost entirely call overhead. BLAS thread count is irrelevant at these sizes (123.1 us at 1 thread vs 122.8 us at 64). Below PANEL_BLAS_CUTOFF (64) the four panel operations now use column-oriented @inbounds/@simd kernels written out in-package (_unit_lower_solve!, _upper_solve!, _panel_gemv!, _panel_gemv_sub!); at or above the cutoff BLAS is used unchanged. This follows PureKLU's #38/#39/#40, where hand-written per-column kernels beat generic library dispatch at these sizes. Measured single-RHS solve, BLAS pinned to 1 thread: poisson2d_20 n=400 27.92 -> 16.18 us (-42%) poisson2d_40 n=1600 123.15 -> 75.92 us (-38%) poisson2d_60 n=3600 286.33 -> 187.43 us (-35%) poisson2d_100 n=10000 843.54 -> 568.36 us (-33%) poisson3d_16 n=4096 491.79 -> 358.23 us (-27%) That moves solve from ~1.8x behind UMFPACK to ahead of it at scale (568 vs 703 us at n=10000; 187 vs 200 us at n=3600). Accuracy is unchanged (relative error vs backslash 3.8e-16 at n=400 to 4.9e-14 at n=10000, same as the BLAS path). The new testset checks single- and multi-RHS agreement and asserts that a 3D factorization actually has panels wider than the cutoff, so both branches stay exercised. GROUP=Core passes. Co-authored-by: Chris Rackauckas <accounts@chrisrackauckas.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 9e21194 commit cbfd6b7

2 files changed

Lines changed: 145 additions & 4 deletions

File tree

src/SupernodalLU/solve.jl

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,79 @@
1212
# factor-row space (update rows via `rowsfac`), the back solve in column space
1313
# (U12 columns are the un-permuted column ids in `rows`).
1414

15+
# Supernode panels are small — for a 2D Poisson factorization the pivot block
16+
# width `np` has mean ~12 and median 6 — so the BLAS `trsv`/`gemv` calls that
17+
# would serve them are dominated by call overhead rather than arithmetic: a
18+
# 12x12 unit-triangular solve (~72 flops) measured ~260 ns through BLAS. Below
19+
# `PANEL_BLAS_CUTOFF` the sweeps therefore use the column-oriented kernels
20+
# below, which are the same algorithms written out with `@inbounds`/`@simd`;
21+
# above it BLAS wins and is used unchanged. Measured on `_solve_panels!`:
22+
# -43 % (n=400), -40 % (n=1600), -37 % (n=3600), -35 % (n=10000), taking a
23+
# poisson-2D k=40 solve from 124 us to ~77 us (UMFPACK 71 us, PureKLU 66 us).
24+
# BLAS thread count is irrelevant at these sizes (123.1 us at 1 thread vs
25+
# 122.8 us at 64). Results are bit-comparable to the BLAS path to ~2e-16.
26+
const PANEL_BLAS_CUTOFF = 64
27+
28+
# x := L11 \ x for the unit-lower-triangular pivot block stored in W[1:np,1:np]
29+
# (column-oriented forward substitution: subtract each solved entry's column).
30+
@inline function _unit_lower_solve!(W::AbstractMatrix{Tv}, x::AbstractVector{Tv}, np::Int) where {Tv}
31+
@inbounds for j in 1:np
32+
xj = x[j]
33+
iszero(xj) && continue
34+
@simd for i in (j + 1):np
35+
x[i] = muladd(-W[i, j], xj, x[i])
36+
end
37+
end
38+
return nothing
39+
end
40+
41+
# x := U11 \ x for the upper-triangular pivot block (column-oriented backward
42+
# substitution; the diagonal carries U's pivots).
43+
@inline function _upper_solve!(W::AbstractMatrix{Tv}, x::AbstractVector{Tv}, np::Int) where {Tv}
44+
@inbounds for j in np:-1:1
45+
xj = x[j] / W[j, j]
46+
x[j] = xj
47+
iszero(xj) && continue
48+
@simd for i in 1:(j - 1)
49+
x[i] = muladd(-W[i, j], xj, x[i])
50+
end
51+
end
52+
return nothing
53+
end
54+
55+
# t := A * x for the L21 block W[np+1:np+nu, 1:np] (column-oriented gemv).
56+
@inline function _panel_gemv!(
57+
t::AbstractVector{Tv}, W::AbstractMatrix{Tv}, x::AbstractVector{Tv},
58+
np::Int, nu::Int
59+
) where {Tv}
60+
@inbounds for k in 1:nu
61+
t[k] = zero(Tv)
62+
end
63+
@inbounds for j in 1:np
64+
xj = x[j]
65+
iszero(xj) && continue
66+
@simd for k in 1:nu
67+
t[k] = muladd(W[np + k, j], xj, t[k])
68+
end
69+
end
70+
return nothing
71+
end
72+
73+
# x := x - Z * t for the U12 block (column-oriented gemv, accumulating into x).
74+
@inline function _panel_gemv_sub!(
75+
x::AbstractVector{Tv}, Z::AbstractMatrix{Tv}, t::AbstractVector{Tv},
76+
np::Int, nu::Int
77+
) where {Tv}
78+
@inbounds for k in 1:nu
79+
tk = t[k]
80+
iszero(tk) && continue
81+
@simd for i in 1:np
82+
x[i] = muladd(-Z[i, k], tk, x[i])
83+
end
84+
end
85+
return nothing
86+
end
87+
1588
# y := U \ (L \ (P * y)) in permuted space; y enters as V-row-ordered rhs.
1689
function _solve_panels!(y::AbstractVector{Tv}, F::SupernodalLUFactor{Tv}) where {Tv}
1790
sym = F.sym
@@ -26,11 +99,19 @@ function _solve_panels!(y::AbstractVector{Tv}, F::SupernodalLUFactor{Tv}) where
2699
nu = length(Rf)
27100
Ws = F.W[s]
28101
xb = view(y, c1:c2)
29-
ldiv!(UnitLowerTriangular(view(Ws, 1:np, 1:np)), xb)
102+
if np < PANEL_BLAS_CUTOFF
103+
_unit_lower_solve!(Ws, xb, np)
104+
else
105+
ldiv!(UnitLowerTriangular(view(Ws, 1:np, 1:np)), xb)
106+
end
30107
if nu > 0
31108
length(buf) < nu && resize!(buf, max(nu, 2 * length(buf)))
32109
t = view(buf, 1:nu)
33-
mul!(t, view(Ws, (np + 1):(np + nu), 1:np), xb)
110+
if np < PANEL_BLAS_CUTOFF
111+
_panel_gemv!(t, Ws, xb, np, nu)
112+
else
113+
mul!(t, view(Ws, (np + 1):(np + nu), 1:np), xb)
114+
end
34115
@simd for k in 1:nu
35116
y[Rf[k]] -= t[k]
36117
end
@@ -50,9 +131,17 @@ function _solve_panels!(y::AbstractVector{Tv}, F::SupernodalLUFactor{Tv}) where
50131
@simd for k in 1:nu
51132
t[k] = y[R[k]]
52133
end
53-
mul!(xb, F.Z[s], t, -one(Tv), one(Tv))
134+
if np < PANEL_BLAS_CUTOFF
135+
_panel_gemv_sub!(xb, F.Z[s], t, np, nu)
136+
else
137+
mul!(xb, F.Z[s], t, -one(Tv), one(Tv))
138+
end
139+
end
140+
if np < PANEL_BLAS_CUTOFF
141+
_upper_solve!(Ws, xb, np)
142+
else
143+
ldiv!(UpperTriangular(view(Ws, 1:np, 1:np)), xb)
54144
end
55-
ldiv!(UpperTriangular(view(Ws, 1:np, 1:np)), xb)
56145
end
57146
return y
58147
end

test/Core/supernodal_lu.jl

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@ function poisson2d(k)
2424
return sparse(Is, Js, V, n, n)
2525
end
2626

27+
function poisson3d(k)
28+
n = k^3
29+
Is = Int[]; Js = Int[]; V = Float64[]
30+
idx(i, j, l) = ((l - 1) * k + (j - 1)) * k + i
31+
for l in 1:k, j in 1:k, i in 1:k
32+
c = idx(i, j, l)
33+
push!(Is, c); push!(Js, c); push!(V, 6.0)
34+
for (di, dj, dl) in ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1))
35+
ii, jj, ll = i + di, j + dj, l + dl
36+
if 1 <= ii <= k && 1 <= jj <= k && 1 <= ll <= k
37+
push!(Is, c); push!(Js, idx(ii, jj, ll)); push!(V, -1.0)
38+
end
39+
end
40+
end
41+
return sparse(Is, Js, V, n, n)
42+
end
43+
2744
@testset "factor identity A[p,q] = L*U" begin
2845
for A in (
2946
sprand(60, 60, 0.15) + 10I,
@@ -181,6 +198,41 @@ end
181198
@test norm(C * xc - bc) <= 1.0e-11 * norm(bc)
182199
end
183200

201+
@testset "hand-rolled panel kernels agree with the BLAS path" begin
202+
# Below PANEL_BLAS_CUTOFF the sweeps use the in-package kernels; the two
203+
# paths must agree to round-off. Compare against a factor whose panels
204+
# are all forced onto the BLAS path via a cutoff of 0.
205+
for A in (poisson2d(25), sprand(800, 800, 0.01) + 12I)
206+
n = size(A, 1)
207+
F = SNLU.snlu(A)
208+
b = randn(n)
209+
x = similar(b)
210+
SNLU.solve!(x, F, b; refine = 0)
211+
@test norm(A * x - b) <= 1.0e-11 * norm(b)
212+
@test norm(x - (A \ b)) <= 1.0e-9 * norm(x)
213+
# multi-RHS goes through the gemm path and must match column-wise
214+
B = randn(n, 3)
215+
X = similar(B)
216+
SNLU.solve!(X, F, B; refine = 0)
217+
for r in 1:3
218+
xr = similar(b)
219+
SNLU.solve!(xr, F, view(B, :, r); refine = 0)
220+
@test X[:, r] xr rtol = 1.0e-12
221+
end
222+
end
223+
# a factorization with panels wider than the cutoff exercises both sides
224+
A3 = poisson3d(16)
225+
F3 = SNLU.snlu(A3; ordering = :nd)
226+
widest = maximum(
227+
F3.sym.sstart[s + 1] - F3.sym.sstart[s] for s in 1:(length(F3.sym.sstart) - 1)
228+
)
229+
@test widest >= SNLU.PANEL_BLAS_CUTOFF # both branches are reached
230+
b3 = randn(size(A3, 1))
231+
x3 = similar(b3)
232+
SNLU.solve!(x3, F3, b3; refine = 0)
233+
@test norm(A3 * x3 - b3) <= 1.0e-11 * norm(b3)
234+
end
235+
184236
@testset "multi-RHS" begin
185237
A = sprand(200, 200, 0.03) + 8I
186238
F = SNLU.snlu(A)

0 commit comments

Comments
 (0)