Skip to content

Commit 3e5c280

Browse files
committed
Merge remote-tracking branch 'upstream/main' into fix/1236-matrixfree-woperator
2 parents 7614323 + 1d99a9e commit 3e5c280

13 files changed

Lines changed: 195 additions & 5 deletions

File tree

Project.toml

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

66
[deps]
@@ -71,6 +71,7 @@ PartitionedSolvers = "11b65f7f-80ac-401b-9ef2-3db765482d62"
7171
ParU_jll = "9e0b026c-e8ce-559c-a2c4-6a3d5c955bc9"
7272
PETSc = "ace2c81b-2b5f-4b1e-a30d-d662738edfe0"
7373
PureUMFPACK = "b7e1f0a2-3c4d-4e5f-9a0b-1c2d3e4f5a6b"
74+
Reactant = "3c362404-f566-11ee-1572-e11a4b42c853"
7475
RecursiveFactorization = "f2c3362d-daeb-58d1-803e-2bc74f2840b4"
7576
SparseMatricesCSR = "a0a7dd2c-ebf4-11e9-1f05-cf50bc540ca1"
7677
Sparspak = "e56a9233-b9d6-4f03-8d0f-1825330902ac"
@@ -115,6 +116,7 @@ LinearSolveParUExt = "ParU_jll"
115116
LinearSolvePETScExt = ["PETSc", "SparseMatricesCSR"]
116117
LinearSolvePETScMPIExt = ["PETSc", "PartitionedArrays", "SparseMatricesCSR"]
117118
LinearSolvePureUMFPACKExt = "PureUMFPACK"
119+
LinearSolveReactantExt = "Reactant"
118120
LinearSolveRecursiveFactorizationExt = ["RecursiveFactorization", "TriangularSolve"]
119121
LinearSolveSparspakExt = "Sparspak"
120122
LinearSolveSpecializingFactorizationsExt = "SpecializingFactorizations"
@@ -186,6 +188,7 @@ PrecompileTools = "1.2"
186188
Preferences = "1.4"
187189
PureKLU = "1.4"
188190
PureUMFPACK = "1"
191+
Reactant = "0.2.230"
189192
Random = "1.10"
190193
RecursiveArrayTools = "4"
191194
RecursiveFactorization = "0.2.27"

ext/LinearSolveReactantExt.jl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
module LinearSolveReactantExt
2+
3+
using LinearSolve: LinearSolve, OperatorAssumptions
4+
using Reactant: Reactant
5+
using SciMLBase: SciMLBase, LinearProblem, LinearSolution, ReturnCode
6+
7+
Reactant._parent_type(::Type{T}) where {T <: LinearSolution} = T
8+
9+
function reactant_solve(prob::LinearProblem, args...; kwargs...)
10+
output_size = prob.u0 === nothing ? (size(prob.A, 2), Base.tail(size(prob.b))...) :
11+
size(prob.u0)
12+
output_eltype = prob.u0 === nothing ? eltype(prob.b) : eltype(prob.u0)
13+
inputs = prob.u0 === nothing ? (prob.A, prob.b) : (prob.A, prob.b, prob.u0)
14+
callback = let prob = prob, args = args, kwargs = kwargs
15+
function (output, A, b, u0...)
16+
runtime_prob = isempty(u0) ? SciMLBase.remake(prob; A, b) :
17+
SciMLBase.remake(prob; A, b, u0 = only(u0))
18+
sol = SciMLBase.solve(runtime_prob, args...; kwargs...)
19+
copyto!(output, sol.u)
20+
return nothing
21+
end
22+
end
23+
u = Reactant.Ops.julia_callback(
24+
callback, ((output_eltype, output_size),), inputs...
25+
)
26+
alg = if isempty(args) || first(args) === nothing
27+
assump = get(
28+
kwargs, :assump,
29+
OperatorAssumptions(size(prob.A, 1) == size(prob.A, 2))
30+
)
31+
LinearSolve.defaultalg(prob.A, prob.b, assump)
32+
else
33+
first(args)
34+
end
35+
return SciMLBase.build_linear_solution(
36+
alg, u, nothing, nothing; retcode = ReturnCode.Success
37+
)
38+
end
39+
40+
Reactant.@reactant_overlay function SciMLBase.solve(
41+
prob::LinearProblem, args...; kwargs...
42+
)
43+
return reactant_solve(prob, args...; kwargs...)
44+
end
45+
46+
end

lib/LinearSolveAutotune/Project.toml

Lines changed: 1 addition & 1 deletion
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.14.0"
4+
version = "1.14.1"
55

66
[deps]
77
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

lib/LinearSolvePyAMG/Project.toml

Lines changed: 1 addition & 1 deletion
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.3.2"
4+
version = "1.3.3"
55

66
[deps]
77
CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab"

src/factorization.jl

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,7 +1054,13 @@ function do_factorization(alg::QRFactorization, A, b, u)
10541054
# `alg.pivot` so the return type is determined by the static
10551055
# `QRFactorization{P}` parameter (otherwise this branch returns
10561056
# `Union{QRCompactWY, QRPivoted}` depending on `alg.inplace`).
1057-
if A isa GPUArraysCore.AnyGPUArray || is_cusparse(A) || issparsematrixcsc(A)
1057+
if A isa GPUArraysCore.AnyGPUArray && !is_cusparse(A) && is_underdetermined(A)
1058+
# A GPU `qr` factors a wide matrix happily, but solving with the result
1059+
# builds `UpperTriangular(R)` on an `R` that is not square and throws.
1060+
# Going through `Aᵀ` turns it back into a triangular solve and gives the
1061+
# minimum-norm solution, which is what dense `\` returns on the CPU.
1062+
fact = MinNormQR(qr(copy(transpose(A))))
1063+
elseif A isa GPUArraysCore.AnyGPUArray || is_cusparse(A) || issparsematrixcsc(A)
10581064
fact = qr(A)
10591065
elseif alg.inplace
10601066
if A isa Symmetric
@@ -1076,7 +1082,13 @@ function init_cacheval(
10761082
maxiters::Int, abstol, reltol, verbose::Union{LinearVerbosity, Bool},
10771083
assumptions::OperatorAssumptions
10781084
)
1079-
return ArrayInterface.qr_instance(convert(AbstractMatrix, A), alg.pivot)
1085+
A_ = convert(AbstractMatrix, A)
1086+
# Matches the wide GPU branch of `do_factorization`: the slot has to be typed for
1087+
# what will be stored in it, not for a plain `QR`.
1088+
if A_ isa GPUArraysCore.AnyGPUArray && !is_cusparse(A_) && is_underdetermined(A_)
1089+
return MinNormQR(qr(copy(transpose(A_))))
1090+
end
1091+
return ArrayInterface.qr_instance(A_, alg.pivot)
10801092
end
10811093

10821094
function init_cacheval(
@@ -1192,6 +1204,12 @@ function init_cacheval(
11921204
alg::CholeskyFactorization, A::GPUArraysCore.AnyGPUArray, b, u, Pl,
11931205
Pr, maxiters::Int, abstol, reltol, verbose::Union{LinearVerbosity, Bool}, assumptions::OperatorAssumptions
11941206
)
1207+
# `cholesky` needs a square matrix, and the default solver initializes this slot for
1208+
# every `A` before it knows which algorithm it will use. Instance an empty
1209+
# factorization for a non-square `A`, rather than returning `nothing`, so the return
1210+
# type does not depend on the runtime value of `assumptions.issq`.
1211+
# See https://github.com/SciML/NonlinearSolve.jl/issues/746
1212+
assumptions.issq || return cholesky(similar(A, 0, 0); check = false)
11951213
return cholesky(A; check = false)
11961214
end
11971215

src/sparsearrays.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,13 @@ function LinearSolve.init_cacheval(
11331133
nothing
11341134
elseif LinearSolve.is_cusparse_csr(A) && !LinearSolve.cudss_loaded(A)
11351135
nothing
1136+
elseif A isa LinearSolve.GPUArraysCore.AnyGPUArray && !assumptions.issq
1137+
# `cholesky_instance` needs a square matrix, and the default solver reaches this
1138+
# slot for every `A` before it knows which algorithm it will use. A 0x0 is square,
1139+
# so instance that instead of returning `nothing`. Sparse CPU input is left alone:
1140+
# `cholesky_instance` handles a non-square sparse `A` and `solve!` stores a real
1141+
# factorization into the slot afterwards.
1142+
ArrayInterface.cholesky_instance(convert(AbstractMatrix, similar(A, 0, 0)))
11361143
else
11371144
ArrayInterface.cholesky_instance(convert(AbstractMatrix, A))
11381145
end

test/GPU/cuda.jl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,39 @@ end
219219
@test LinearSolve.defaultalg(Wc, bc, OperatorAssumptions(true)).alg ===
220220
LinearSolve.DefaultAlgorithmChoice.LHLFactorization
221221
end
222+
223+
# Two separate failures kept a non-square GPU `A` from solving at all.
224+
#
225+
# `_init_default_cacheval` builds a cacheval for every algorithm slot before it knows
226+
# which one it will use, and two of those slots called `cholesky` on `A` itself, so
227+
# `init` threw `DimensionMismatch` before any algorithm ran. Then for a wide `A`, the
228+
# QR solve itself threw: a GPU `qr` factors a wide matrix, but solving with the result
229+
# builds `UpperTriangular(R)` on a non-square `R`.
230+
# See https://github.com/SciML/NonlinearSolve.jl/issues/746 and #857.
231+
@testset "Non-square GPU matrices" begin
232+
tall = CUDACore.adapt(CuArray, Float32[1 2; 3 4; 5 6; 7 8])
233+
btall = CUDACore.adapt(CuArray, Float32[1, 2, 3, 4])
234+
wide = CUDACore.adapt(CuArray, Float32[1 2 3 4; 5 6 7 8])
235+
bwide = CUDACore.adapt(CuArray, Float32[1, 2])
236+
237+
@testset "$name" for (name, A, b) in (("tall", tall, btall), ("wide", wide, bwide))
238+
ref = Array(A) \ Array(b)
239+
240+
# `init` is where every slot gets built, and where this used to throw.
241+
cache = init(LinearProblem(A, b))
242+
sol = solve!(cache)
243+
@test SciMLBase.successful_retcode(sol)
244+
@test Array(sol.u) ref rtol = 1.0e-4
245+
246+
@test LinearSolve.defaultalg(A, b, OperatorAssumptions(false)).alg ===
247+
LinearSolve.DefaultAlgorithmChoice.QRFactorization
248+
@test Array(solve(LinearProblem(A, b), QRFactorization()).u) ref rtol = 1.0e-4
249+
end
250+
251+
# The wide solve has to be the minimum-norm one, which is what dense `\` gives on
252+
# the CPU. Agreeing on the residual alone would not distinguish it from any other
253+
# point on the solution manifold.
254+
xwide = Array(solve(LinearProblem(wide, bwide)).u)
255+
@test norm(xwide) norm(Array(wide) \ Array(bwide)) rtol = 1.0e-4
256+
@test Array(wide) * xwide Array(bwide) rtol = 1.0e-4
257+
end

test/Reactant/Project.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[deps]
2+
LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae"
3+
Reactant = "3c362404-f566-11ee-1572-e11a4b42c853"
4+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
5+
6+
[sources]
7+
LinearSolve = {path = "../.."}
8+
9+
[compat]
10+
LinearSolve = "5"
11+
Reactant = "0.2.280"
12+
Test = "1.10"
13+
julia = "1.10"

test/Reactant/reactant.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using LinearSolve
2+
using Reactant
3+
using Test
4+
5+
function solve_once(A, b)
6+
return solve(LinearProblem(A, b)).u
7+
end
8+
9+
A = Float32[4 1; 2 3]
10+
b = Float32[1, 2]
11+
expected = A \ b
12+
A_reactant = Reactant.to_rarray(A)
13+
b_reactant = Reactant.to_rarray(b)
14+
15+
sol = @jit solve(LinearProblem(A_reactant, b_reactant))
16+
@test Array(sol.u) expected
17+
@test Array(@jit solve_once(A_reactant, b_reactant)) expected
18+
@testset "preserves the LinearSolve operation" begin
19+
@test occursin(
20+
"reactant_julia_callback", repr(@code_hlo solve_once(A_reactant, b_reactant))
21+
)
22+
23+
qr_sol = @jit solve(LinearProblem(A_reactant, b_reactant), QRFactorization())
24+
@test qr_sol.alg isa QRFactorization
25+
@test Array(qr_sol.u) expected
26+
end
27+
28+
@testset "default LU preserves QR safety fallback" begin
29+
A_singular = Float32[1 1; 1 1]
30+
b_singular = Float32[1, 2]
31+
expected_singular = Float32[0.75, 0.75]
32+
33+
lu_sol = solve(LinearProblem(A_singular, b_singular), LUFactorization())
34+
@test lu_sol.retcode == ReturnCode.Failure
35+
@test solve(LinearProblem(A_singular, b_singular)).u expected_singular
36+
37+
A_singular_reactant = Reactant.to_rarray(A_singular)
38+
b_singular_reactant = Reactant.to_rarray(b_singular)
39+
lu_sol_reactant = @jit solve(
40+
LinearProblem(A_singular_reactant, b_singular_reactant), LUFactorization()
41+
)
42+
@test Array(lu_sol_reactant.u) == lu_sol.u
43+
44+
sol_singular = @jit solve(
45+
LinearProblem(A_singular_reactant, b_singular_reactant)
46+
)
47+
@test Array(sol_singular.u) expected_singular
48+
end

test/qa/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Metal = "dde4c033-4e86-420c-a63e-0dd931031962"
2626
Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6"
2727
PureUMFPACK = "b7e1f0a2-3c4d-4e5f-9a0b-1c2d3e4f5a6b"
2828
RecursiveFactorization = "f2c3362d-daeb-58d1-803e-2bc74f2840b4"
29+
Reactant = "3c362404-f566-11ee-1572-e11a4b42c853"
2930
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
3031
SciMLTesting = "09d9d899-5365-40a9-917a-5f67fddea283"
3132
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
@@ -67,6 +68,7 @@ Metal = "1.4"
6768
Mooncake = "0.5.15"
6869
PureUMFPACK = "1"
6970
RecursiveFactorization = "0.2.26"
71+
Reactant = "0.2.230"
7072
SafeTestsets = "0.1, 1"
7173
SciMLTesting = "2.8"
7274
SparseArrays = "1.10"

0 commit comments

Comments
 (0)