Skip to content

Commit 18f5b37

Browse files
ChrisRackauckas-ClaudeChrisRackauckasclaude
authored
Batched (matrix) right-hand sides: LinearProblem(A, B) ≡ A \ B (v4.0.0) (#1072)
* Support batched (matrix) right-hand sides in factorization algorithms `solve(LinearProblem(A, B))` with `B::AbstractMatrix` now computes the equivalent of `A \ B`: `u0` is initialized as a `size(A, 2) × size(B, 2)` matrix and the factorization-based algorithms solve all columns against a single factorization of `A`. This is a breaking change (v4.0.0): previously a matrix `b` initialized a flattened vector `u` and errored downstream. - `__init_u0_from_Ab` gains matrix-`b` methods (including SMatrix disambiguation). - Iterative/Krylov algorithms (`KrylovJL`, `SimpleGMRES`, and the other `AbstractKrylovSubspaceMethod`s, plus the default algorithm when it selects a Krylov method for an operator) throw an informative `ArgumentError` at `init` time for matrix `b`; `SimpleLUFactorization` (vector-only workspace) does the same. - The default polyalgorithm's unused Krylov cacheval slots initialize to `nothing` for matrix `b` so structured-matrix defaults (Diagonal, SymTridiagonal, ...) work. - `_check_residual_safety` sizes its residual buffer shape-generically. - Default-algorithm size heuristics use `size(b, 1)` instead of `length(b)` so batched right-hand sides don't inflate the apparent problem size (same for the sparse `use_klulike_sparse_structure` heuristic). - MKL/OpenBLAS/AppleAccelerate LU copy solution columns correctly for matrix `b` in the rectangular (`m > n`) path. - SparseArrays extension: CHOLMOD pre-1.12 `_ldiv!` fallback widened to matrices, SPQR gains a matrix `_ldiv!` via `\` (no in-place matrix ldiv! on any Julia version), and SparseColumnPivotedQR solves matrix `b` column-by-column against the one factorization. Part of #552 Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh * Add batched right-hand side tests Covers dense LU/GenericLU/QR (NoPivot + ColumnNorm)/SVD/Cholesky/ BunchKaufman/NormalCholesky vs A \ B for Float64 and ComplexF64, the default algorithm (dense, sparse, structured, non-square with k != n), sparse UMFPACK/KLU/CHOLMOD and the non-square sparse QR default, cache reuse via `cache.b = B2` and `cache.A = A2`, failure retcodes on singular A (including the default's QR fallback), residual-safety with matrix B, informative errors for Krylov methods and SimpleLUFactorization, static arrays, BigFloat, and single-column-matrix vs vector consistency. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh * Document batched right-hand sides Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYp371jx6LurezUDhKcYRh * Widen in-repo LinearSolve compat for v4 and source docs env locally All sublibrary, test, qa, GPU, Trim, and docs environments pinned LinearSolve = "3[.x.y]", so every CI job died at resolution against the 4.0.0 bump ("empty intersection between LinearSolve@4.0.0 and project compatibility"). Widen each to include 4. The docs environment also gains [sources] entries for LinearSolve and LinearSolveAutotune (the registered Autotune restricts LinearSolve to 3, so the docs build must use the local copies, matching the pattern the test environments already use). Verified locally: docs, lib/LinearSolveAutotune, and test/qa environments all instantiate against 4.0.0. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Runic-format forwarddiff_overloads.jl The repo-wide Runic Format Check fails on main because this file landed unformatted in aabf007 (#1069); format it here alongside the dispatch fix for the same commit's fallout. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Allow AlgebraicMultigrid 0.5 so the test target resolves against v4 Every registered AlgebraicMultigrid >= 0.6 caps LinearSolve at 3, so the root test target (which all CI test groups resolve) was unsatisfiable against the 4.0.0 bump. AMG 0.5.x predates its LinearSolve dependency and exposes the identical API surface the extension uses (SmoothedAggregationAMG, RugeStubenAMG, aspreconditioner, ruge_stuben); the AlgebraicMultigridJL testset passes on 0.5.1 (verified locally, including the tight-tolerance case). The resolver will pick AMG 2.x again automatically once AlgebraicMultigrid widens its own compat; the compat comment marks the entry for removal then. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Retrigger CI (self-hosted runner disconnect on Downgrade Core) Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Drop LinearSolve v3 from in-repo lower bounds All in-repo environments and both registered sublibraries now require LinearSolve 4: the environments always develop the local copy, and v4-era releases of LinearSolveAutotune/LinearSolvePyAMG should require the batched-RHS LinearSolve rather than straddle the major boundary. Minor-bump both sublibraries (Autotune 1.12.0, PyAMG 1.2.0) for the dependency floor raise. All environment classes re-verified to resolve. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent b59c16b commit 18f5b37

33 files changed

Lines changed: 384 additions & 45 deletions

File tree

Project.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "LinearSolve"
22
uuid = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae"
3-
version = "3.87.0"
3+
version = "4.0.0"
44
authors = ["SciML"]
55

66
[deps]
@@ -110,7 +110,10 @@ LinearSolveSpecializingFactorizationsExt = "SpecializingFactorizations"
110110
[compat]
111111
AMD = "0.5"
112112
AMDGPU = "1.2, 2"
113-
AlgebraicMultigrid = "1, 2"
113+
# 0.5 allowed so the test target resolves against LinearSolve 4 until
114+
# AlgebraicMultigrid widens its own LinearSolve compat (its 1.x/2.x cap at 3);
115+
# the ext API is identical and the AMG testset passes on 0.5.1.
116+
AlgebraicMultigrid = "0.5, 1, 2"
114117
ArrayInterface = "7.19"
115118
BandedMatrices = "1.8"
116119
BlockDiagonals = "0.2"

docs/Project.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ SciMLOperators = "c0aeaf25-5076-4817-a8d5-81caf7dfa961"
66

77
[compat]
88
Documenter = "1"
9-
LinearSolve = "3"
9+
LinearSolve = "4"
1010
LinearSolveAutotune = "1.1"
1111
SciMLOperators = "1"
12+
13+
[sources]
14+
LinearSolve = {path = ".."}
15+
LinearSolveAutotune = {path = "../lib/LinearSolveAutotune"}

docs/src/release_notes.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Release Notes
22

3+
## v4.0
4+
5+
- Batched (matrix) right-hand sides are now supported: `solve(LinearProblem(A, B))` with
6+
`B::AbstractMatrix` computes the equivalent of `A \ B`, factorizing `A` once and
7+
returning `sol.u` as a `size(A, 2) × size(B, 2)` matrix. This is a breaking change:
8+
previously a matrix `b` initialized a vector-shaped `u` and generally errored downstream.
9+
Batched right-hand sides are supported by the factorization-based algorithms; iterative
10+
(Krylov) methods throw an informative `ArgumentError` for matrix `b`.
11+
312
## Upcoming Changes
413

514
- `CudaOffloadFactorization` has been split into two algorithms:

docs/src/tutorials/linear.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,26 @@ pass in an algorithm struct and all wrapped linear solvers are immediately
3636
available as tweaks to the general algorithm. For more information on the
3737
available solvers, see [the solvers page](@ref linearsystemsolvers)
3838

39+
## Batched Right-Hand Sides
40+
41+
A matrix right-hand side `B` solves all of its columns against the same `A` at
42+
once, just like `A \ B`: `solve(LinearProblem(A, B))` factorizes `A` a single
43+
time and returns `sol.u` as the `size(A, 2) × size(B, 2)` matrix satisfying
44+
`A * sol.u ≈ B`.
45+
46+
```@example linsys1
47+
B = rand(4, 2)
48+
prob = LS.LinearProblem(A, B)
49+
sol = LS.solve(prob)
50+
sol.u ≈ A \ B
51+
```
52+
53+
This works with the caching interface as well (`cache.b = B2; solve!(cache)`
54+
re-solves all columns against the cached factorization). Batched right-hand
55+
sides are only supported by factorization-based algorithms; iterative (Krylov)
56+
methods such as `LS.KrylovJL_GMRES()` accept only vector `b` and throw an
57+
informative error for matrix `B` — solve column-by-column instead in that case.
58+
3959
## Sparse and Structured Matrices
4060

4161
There is no difference in the interface for LinearSolve.jl on sparse

ext/LinearSolveSparseArraysExt.jl

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -778,12 +778,24 @@ function SciMLBase.solve!(
778778
cache.isfresh = false
779779
end
780780
F = LinearSolve.@get_cacheval(cache, :SparseColumnPivotedQRFactorization)
781-
y = ldiv!(cache.u, F, cache.b)
781+
y = LinearSolve._ldiv!(cache.u, F, cache.b)
782782
return SciMLBase.build_linear_solution(
783783
alg, y, nothing, cache; retcode = ReturnCode.Success
784784
)
785785
end
786786

787+
# SparseColumnPivotedQR's ldiv! only accepts vector right-hand sides; batched
788+
# (matrix) right-hand sides solve column-by-column against the one factorization.
789+
function LinearSolve._ldiv!(
790+
x::AbstractMatrix,
791+
F::SCPQR.SparseColumnPivotedQRFactorization, b::AbstractMatrix
792+
)
793+
for j in axes(b, 2)
794+
ldiv!(view(x, :, j), F, view(b, :, j))
795+
end
796+
return x
797+
end
798+
787799
# Build a column-pivoted sparse QR factorization for the default sparse-LU
788800
# singular fallback (`_do_sparse_qr_fallback` in src/default.jl).
789801
function LinearSolve.sparse_colpivqr_factorize(A)
@@ -874,8 +886,8 @@ end
874886
x .= A \ b
875887
end
876888
function LinearSolve._ldiv!(
877-
x::AbstractVector,
878-
A::SparseArrays.CHOLMOD.Factor, b::AbstractVector
889+
x::AbstractVecOrMat,
890+
A::SparseArrays.CHOLMOD.Factor, b::AbstractVecOrMat
879891
)
880892
x .= A \ b
881893
end
@@ -897,6 +909,15 @@ end
897909
end
898910
end
899911

912+
# SPQR has no in-place matrix (batched) ldiv! on any current Julia version,
913+
# so route batched right-hand sides through the allocating `\`.
914+
function LinearSolve._ldiv!(
915+
x::AbstractMatrix,
916+
A::SparseArrays.SPQR.QRSparse, b::AbstractMatrix
917+
)
918+
x .= A \ b
919+
end
920+
900921
function LinearSolve._ldiv!(
901922
::SVector,
902923
A::Union{SparseArrays.CHOLMOD.Factor, SparseArrays.SPQR.QRSparse},
@@ -940,11 +961,11 @@ end
940961
# medium and very sparse / "less structure") favors the pure-Julia KLU-style
941962
# solvers: PureKLU for LU and SparseColumnPivotedQR for QR. `false` ("more
942963
# structure") favors the SuiteSparse solvers: UMFPACK for LU and SPQR for QR.
943-
# The `length(b) <= 1_000` branch is the "small enough" fast path; the second is
964+
# The `size(b, 1) <= 1_000` branch is the "small enough" fast path; the second is
944965
# the "medium and sufficiently sparse" rule.
945966
function LinearSolve.use_klulike_sparse_structure(A::AbstractSparseMatrixCSC, b)
946-
return length(b) <= 1_000 ||
947-
(length(b) <= 10_000 && length(nonzeros(A)) / length(A) < 2.0e-4)
967+
return size(b, 1) <= 1_000 ||
968+
(size(b, 1) <= 10_000 && length(nonzeros(A)) / length(A) < 2.0e-4)
948969
end
949970

950971
@static if Base.USE_GPL_LIBS

lib/LinearSolveAutotune/Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "LinearSolveAutotune"
22
uuid = "67398393-80e8-4254-b7e4-1b9a36a3c5b6"
33
authors = ["SciML"]
4-
version = "1.11.1"
4+
version = "1.12.0"
55

66
[deps]
77
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
@@ -44,7 +44,7 @@ FastLapackInterface = "2.0.4"
4444
GitHub = "5"
4545
LAPACK_jll = "3.12"
4646
LinearAlgebra = "1"
47-
LinearSolve = "3.39.2"
47+
LinearSolve = "4"
4848
MKL_jll = "2025.2.0"
4949
Metal = "1.5"
5050
OpenBLAS_jll = "0.3"

lib/LinearSolveAutotune/test/qa/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ LinearSolveAutotune = {path = "../.."}
1313
[compat]
1414
Aqua = "0.8"
1515
JET = "0.9, 0.10, 0.11"
16-
LinearSolve = "3"
16+
LinearSolve = "4"
1717
LinearSolveAutotune = "1"
1818
SciMLTesting = "1.6"
1919
Test = "1"

lib/LinearSolvePyAMG/Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "LinearSolvePyAMG"
22
uuid = "7a56c47d-7ab1-4e99-b0e3-2952e463d64a"
33
authors = ["SciML"]
4-
version = "1.1.1"
4+
version = "1.2.0"
55

66
[deps]
77
CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab"
@@ -17,7 +17,7 @@ LinearSolve = {path = "../.."}
1717
[compat]
1818
CondaPkg = "0.2"
1919
LinearAlgebra = "1.10"
20-
LinearSolve = "3"
20+
LinearSolve = "4"
2121
Pkg = "1"
2222
PythonCall = "0.9"
2323
SafeTestsets = "0.1"

lib/LinearSolvePyAMG/test/qa/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ LinearSolvePyAMG = {path = "../.."}
1313
[compat]
1414
Aqua = "0.8"
1515
JET = "0.9, 0.10, 0.11"
16-
LinearSolve = "3"
16+
LinearSolve = "4"
1717
LinearSolvePyAMG = "1"
1818
SciMLTesting = "1.6"
1919
Test = "1"

src/appleaccelerate.jl

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,11 @@ function SciMLBase.solve!(
382382
if m > n
383383
Bc = copy(cache.b)
384384
aa_getrs!('N', A.factors, A.ipiv, Bc; info)
385-
copyto!(cache.u, 1, Bc, 1, n)
385+
if cache.b isa AbstractMatrix
386+
copyto!(cache.u, @view(Bc[1:n, :]))
387+
else
388+
copyto!(cache.u, 1, Bc, 1, n)
389+
end
386390
else
387391
copyto!(cache.u, cache.b)
388392
aa_getrs!('N', A.factors, A.ipiv, cache.u; info)
@@ -468,7 +472,11 @@ function SciMLBase.solve!(
468472
if m > n
469473
aa_getrs!('N', A_lu.factors, A_lu.ipiv, b_32; info)
470474
# Convert back to original precision
471-
cache.u[1:n] .= Torig.(b_32[1:n])
475+
if cache.b isa AbstractMatrix
476+
cache.u .= Torig.(@view(b_32[1:n, :]))
477+
else
478+
cache.u[1:n] .= Torig.(@view(b_32[1:n]))
479+
end
472480
else
473481
copyto!(u_32, b_32)
474482
aa_getrs!('N', A_lu.factors, A_lu.ipiv, u_32; info)

0 commit comments

Comments
 (0)