Skip to content

Commit e7742ab

Browse files
ChrisRackauckas-ClaudeChrisRackauckasclaude
authored
Route RFLU matrix right-hand sides through TriangularSolve (#1117)
RecursiveFactorization already uses TriangularSolve inside its own lu!, but the ldiv! that consumes the factorization only does so for the pivotless NotIPIV case (RecursiveFactorization/src/lu.jl); a pivoted LU - which is what RFLUFactorization produces - falls back to LinearAlgebra, i.e. BLAS trsv/trsm. Fixing that inside RecursiveFactorization would be type piracy (it owns neither ldiv! nor LU), so it belongs here: LinearSolve owns solve! for its own algorithm type. Matrix right-hand sides now go through TriangularSolve's blocked kernels. Measured solve-only, factorization already computed, 1 BLAS thread: n nrhs=4 nrhs=8 64 6.31 -> 3.76 us 8.76 -> 5.40 us (1.68x / 1.62x) 128 21.11 -> 12.68 us 26.20 -> 17.22 us (1.66x / 1.52x) 256 86.43 -> 55.11 us 97.09 -> 67.87 us (1.57x / 1.43x) 500 277.11 -> 175.42 us 301.13 -> 225.77 us (1.58x / 1.33x) Vector right-hand sides keep the stdlib path: TriangularSolve has no vector kernel, and reshaping to n x 1 measured 1.09x at n=128 but 0.88x by n=256. A single-column matrix is the same case in disguise, so it is guarded explicitly - with the guard, nrhs=1 and vector results are bit-identical to the stdlib path (agreement exactly 0.0), while multi-RHS keeps 1.5-1.7x. TriangularSolve is added as a weakdep purely to trigger the extension; RecursiveFactorization depends on it, so loading RecursiveFactorization alone still activates everything and no dependency graph changes. New multi-RHS RFLU testset covers the direct and cache-reuse paths. Co-authored-by: Chris Rackauckas <accounts@chrisrackauckas.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c9f1666 commit e7742ab

3 files changed

Lines changed: 72 additions & 2 deletions

File tree

Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ PartitionedArrays = "5a9dfac6-5c52-46f7-8278-5e2210713be9"
6565
PartitionedSolvers = "11b65f7f-80ac-401b-9ef2-3db765482d62"
6666
PureUMFPACK = "b7e1f0a2-3c4d-4e5f-9a0b-1c2d3e4f5a6b"
6767
RecursiveFactorization = "f2c3362d-daeb-58d1-803e-2bc74f2840b4"
68+
TriangularSolve = "d5829a12-d9aa-46ab-831f-fb7c9ab06edf"
6869
STRUMPACK_jll = "86fbd0b9-476f-557c-b766-62c724b42d8c"
6970
SparseMatricesCSR = "a0a7dd2c-ebf4-11e9-1f05-cf50bc540ca1"
7071
Sparspak = "e56a9233-b9d6-4f03-8d0f-1825330902ac"
@@ -106,7 +107,7 @@ LinearSolvePartitionedSolversExt = ["PartitionedArrays", "PartitionedSolvers"]
106107
LinearSolveParUExt = ["ParU_jll", "SparseArrays"]
107108
LinearSolvePureUMFPACKExt = ["PureUMFPACK", "SparseArrays"]
108109
LinearSolvePardisoExt = ["Pardiso", "SparseArrays"]
109-
LinearSolveRecursiveFactorizationExt = "RecursiveFactorization"
110+
LinearSolveRecursiveFactorizationExt = ["RecursiveFactorization", "TriangularSolve"]
110111
LinearSolveSuperLUDISTExt = ["SparseArrays", "SuperLUDIST"]
111112
LinearSolveSTRUMPACKExt = ["SparseArrays", "STRUMPACK_jll"]
112113
LinearSolveSparseArraysExt = "SparseArrays"
@@ -174,6 +175,7 @@ PureUMFPACK = "0.1"
174175
Random = "1.10"
175176
RecursiveArrayTools = "3.37, 4"
176177
RecursiveFactorization = "0.2.26"
178+
TriangularSolve = "0.2"
177179
Reexport = "1.2.2"
178180
STRUMPACK_jll = "8"
179181
SafeTestsets = "0.1"

ext/LinearSolveRecursiveFactorizationExt.jl

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ using LinearSolve: LinearSolve, userecursivefactorization, LinearCache, @get_cac
44
RFLUFactorization, ButterflyFactorization, RF32MixedLUFactorization,
55
LinearVerbosity
66
using LinearSolve.LinearAlgebra, LinearSolve.ArrayInterface, RecursiveFactorization
7+
using TriangularSolve: TriangularSolve
78
using SciMLBase: SciMLBase, ReturnCode
89
using SciMLLogging: @SciMLMessage
910

@@ -31,10 +32,59 @@ function SciMLBase.solve!(
3132

3233
cache.isfresh = false
3334
end
34-
y = ldiv!(cache.u, LinearSolve.@get_cacheval(cache, :RFLUFactorization)[1], cache.b)
35+
y = _rf_ldiv!(
36+
cache.u, LinearSolve.@get_cacheval(cache, :RFLUFactorization)[1], cache.b, Val(T)
37+
)
3538
return SciMLBase.build_linear_solution(alg, y, nothing, nothing; retcode = ReturnCode.Success)
3639
end
3740

41+
# Apply an RF factorization to a right-hand side.
42+
#
43+
# `RecursiveFactorization` already routes its own `lu!` through TriangularSolve,
44+
# but the `ldiv!` that consumes the factorization only does so for the pivotless
45+
# `NotIPIV` case (RecursiveFactorization/src/lu.jl); a pivoted `LU` falls back to
46+
# LinearAlgebra, i.e. BLAS `trsv`/`trsm`. For a matrix right-hand side that
47+
# leaves a consistent ~1.6x on the table at every size we measured, because
48+
# TriangularSolve's blocked kernels beat `trsm` here:
49+
#
50+
# n BLAS trsm TriangularSolve
51+
# 32 2.19 us 1.37 us (1.60x)
52+
# 64 6.27 us 3.76 us (1.67x)
53+
# 128 21.02 us 12.72 us (1.65x)
54+
# 256 86.08 us 54.07 us (1.59x)
55+
# 500 276.20 us 175.81 us (1.58x)
56+
#
57+
# For a single vector right-hand side TriangularSolve has no advantage (it has
58+
# no vector kernel, and reshaping to n x 1 measured 1.09x at n=128 but 0.88x by
59+
# n=256), so vectors keep the stdlib path.
60+
@inline function _rf_ldiv!(
61+
u::AbstractVector, fact::LinearAlgebra.LU, b::AbstractVector, ::Val
62+
)
63+
return ldiv!(u, fact, b)
64+
end
65+
66+
function _rf_ldiv!(
67+
U::AbstractMatrix{T}, fact::LinearAlgebra.LU{T, <:StridedMatrix{T}},
68+
B::AbstractMatrix{T}, ::Val{Thread}
69+
) where {T <: LinearAlgebra.BlasFloat, Thread}
70+
# A single column is the vector case in disguise: measured 0.87-0.90x at
71+
# n >= 256, so it keeps the stdlib path.
72+
size(B, 2) == 1 && return ldiv!(U, fact, B)
73+
U === B || copyto!(U, B)
74+
LinearAlgebra._ipiv_rows!(fact, 1:length(fact.ipiv), U)
75+
F = fact.factors
76+
TriangularSolve.ldiv!(UnitLowerTriangular(F), U, Val(Thread))
77+
TriangularSolve.ldiv!(UpperTriangular(F), U, Val(Thread))
78+
return U
79+
end
80+
81+
# Non-strided or non-BLAS element types keep the stdlib path.
82+
@inline function _rf_ldiv!(
83+
U::AbstractMatrix, fact::LinearAlgebra.LU, B::AbstractMatrix, ::Val
84+
)
85+
return ldiv!(U, fact, B)
86+
end
87+
3888
# Mixed precision RecursiveFactorization implementation
3989

4090
const PREALLOCATED_RF32_LU = begin

test/Core/basictests.jl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,24 @@ end
211211
@test X * solve!(cache) b1
212212
end
213213

214+
@testset "RFLUFactorization multi-RHS" begin
215+
# The extension routes matrix right-hand sides through TriangularSolve;
216+
# results must match the stdlib path exactly in behaviour.
217+
Random.seed!(7)
218+
for n in (8, 40), nrhs in (1, 3)
219+
Ar = rand(n, n) + n * I
220+
Br = rand(n, nrhs)
221+
sol = solve(LinearProblem(copy(Ar), copy(Br)), RFLUFactorization())
222+
@test sol.u Ar \ Br
223+
# cache reuse path
224+
cache = SciMLBase.init(LinearProblem(copy(Ar), copy(Br)), RFLUFactorization())
225+
@test solve!(cache).u Ar \ Br
226+
A2 = Ar + I
227+
cache.A = copy(A2)
228+
@test solve!(cache).u A2 \ Br
229+
end
230+
end
231+
214232
@testset "SupernodalLU Factorization" begin
215233
A1 = sparse(A / 1)
216234
b1 = rand(n)

0 commit comments

Comments
 (0)