From f6da462b1034fb117768e4a11817f14d5b0f7887 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 14 May 2026 08:40:43 -0400 Subject: [PATCH 1/6] Fix type instability in solve!(cache) through DefaultLinearSolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `solve!(cache)` where `cache = init(LinearProblem(A, b))` was returning `LinearSolution{_A, _B, _C, _D, DefaultLinearSolver, _E, _F} where {...}` — a UnionAll over 6 free type parameters instead of a concrete LinearSolution. Two stacked causes: 1. `do_factorization(::QRFactorization, A, b, u)` returned a `Union` for `QRFactorization{ColumnNorm}` because the non-inplace branch called `qr(A)` (dropping the pivot, returning `QRCompactWY`) while the inplace branch called `qr!(A, alg.pivot)` (returning `QRPivoted`). Routes sparse-CSC/GPU/cusparse to `qr(A)` explicitly and uses `qr(A, alg.pivot)` for all other CPU paths so all branches agree on the return type for a given pivot type. 2. `_default_lu_solve_with_fallback`, `_do_qr_fallback`, `_reuse_qr_fallback` and the non-LU branches of the `solve!(cache, ::DefaultLinearSolver)` @generated function read `sol.u`/`sol.resid`/`sol.cache`/`sol.stats` from an inner `sol`. During precompile, inference of those inner `solve!(cache, $specific_alg)` calls hits the inference complexity cap and gets cached with `rettype = Any` — so `sol` is `Any`, `typeof(sol.u)`/`typeof(sol.resid)`/`typeof(sol.cache)`/`typeof(sol.stats)` all become abstract type parameters in the constructed LinearSolution. Fix: build the wrapping LinearSolution from the statically-typed `cache` (`cache.u`, `nothing` for resid, `cache` itself, `nothing` for stats). `retcode` and `iters` are fixed-type fields of LinearSolution (not type parameters), so we keep `sol.retcode`/`sol.iters` to preserve that runtime info. Also drops unused `args...; kwargs...` forwarding through the helpers — the inner `solve!` methods ignore those. Trade-off: `stats` is now `nothing` instead of `sol.stats` when wrapping through the default solver. For all current LinearSolve factorization solvers `stats` is already `nothing`, so nothing is lost in practice. Co-Authored-By: Chris Rackauckas --- src/default.jl | 86 ++++++++++++++++++++++++-------------------- src/factorization.jl | 13 +++++-- 2 files changed, 58 insertions(+), 41 deletions(-) diff --git a/src/default.jl b/src/default.jl index fd18c6ef9..4b2f323c4 100644 --- a/src/default.jl +++ b/src/default.jl @@ -603,22 +603,31 @@ function _is_gpu_sparse(A) end """ - _do_qr_fallback(cache::LinearCache, alg, sol, reason::Symbol, args...; kwargs...) + _do_qr_fallback(cache::LinearCache, alg, sol, reason::Symbol) Perform QR fallback after LU failure or residual check failure. Restores `cache.A` from `A_backup` (since LU may have modified it in-place) and solves with column-pivoted QR (or NoPivot for GPU arrays which don't support scalar indexing). `reason` is `:lu_failure` or `:residual_check` for appropriate log messages. """ -function _do_qr_fallback(cache::LinearCache, alg, sol, reason::Symbol, args...; kwargs...) +function _do_qr_fallback(cache::LinearCache, alg, sol, reason::Symbol) + # Always extract solution data from `cache` rather than `sol`. The QR + # fallback path calls `solve!(cache, QRFactorization(...))` recursively; + # during precompile inference, that inner call's return type gets capped + # to a non-concrete UnionAll (Julia's inference complexity limit). Reading + # `cache.u` (statically typed) and using `cache` for the solution cache + # field keeps the return type of this helper concrete, which propagates + # up through `_default_lu_solve_with_fallback` and the @generated + # `solve!(cache, ::DefaultLinearSolver)` body. + rc = sol.retcode + iters = sol.iters if is_cusparse(cache.A) @SciMLMessage( "LU factorization failed for GPU sparse matrix but QR fallback is not supported for CuSparse. Returning LU failure.", cache.verbose, :default_lu_fallback ) return SciMLBase.build_linear_solution( - alg, sol.u, sol.resid, sol.cache; - retcode = sol.retcode, iters = sol.iters, stats = sol.stats + alg, cache.u, nothing, cache; retcode = rc, iters = iters, stats = nothing ) end if cache.A === cache.cacheval.A_backup @@ -627,8 +636,7 @@ function _do_qr_fallback(cache::LinearCache, alg, sol, reason::Symbol, args...; cache.verbose, :default_lu_fallback ) return SciMLBase.build_linear_solution( - alg, sol.u, sol.resid, sol.cache; - retcode = sol.retcode, iters = sol.iters, stats = sol.stats + alg, cache.u, nothing, cache; retcode = rc, iters = iters, stats = nothing ) end if reason === :residual_check @@ -645,33 +653,34 @@ function _do_qr_fallback(cache::LinearCache, alg, sol, reason::Symbol, args...; copyto!(cache.A, cache.cacheval.A_backup) cache.isfresh = true pivot = _qr_fallback_pivot(cache.A) - sol = SciMLBase.solve!(cache, QRFactorization(pivot), args...; kwargs...) + qr_sol = SciMLBase.solve!(cache, QRFactorization(pivot)) cache.cacheval.fell_back_to_qr = true return SciMLBase.build_linear_solution( - alg, sol.u, sol.resid, sol.cache; - retcode = sol.retcode, iters = sol.iters, stats = sol.stats + alg, cache.u, nothing, cache; + retcode = qr_sol.retcode, iters = qr_sol.iters, stats = nothing ) end """ - _reuse_qr_fallback(cache::LinearCache, alg, args...; kwargs...) + _reuse_qr_fallback(cache::LinearCache, alg) Reuse the cached QR factorization from a previous QR fallback. Called when `fell_back_to_qr` is `true` and `isfresh` is `false`, meaning the matrix hasn't changed since the QR fallback and we should keep using QR instead of the (potentially corrupted) LU factorization. """ -function _reuse_qr_fallback(cache::LinearCache, alg, args...; kwargs...) +function _reuse_qr_fallback(cache::LinearCache, alg) pivot = _qr_fallback_pivot(cache.A) - sol = SciMLBase.solve!(cache, QRFactorization(pivot), args...; kwargs...) + qr_sol = SciMLBase.solve!(cache, QRFactorization(pivot)) + # Use cache directly for type-stable inference (see _do_qr_fallback). return SciMLBase.build_linear_solution( - alg, sol.u, sol.resid, sol.cache; - retcode = sol.retcode, iters = sol.iters, stats = sol.stats + alg, cache.u, nothing, cache; + retcode = qr_sol.retcode, iters = qr_sol.iters, stats = nothing ) end """ - _default_lu_solve_with_fallback(cache::LinearCache, alg::DefaultLinearSolver, sol, args...; kwargs...) + _default_lu_solve_with_fallback(cache::LinearCache, alg::DefaultLinearSolver, sol) Post-process an LU solve result: if LU explicitly failed, the solution contains NaN/Inf, or the residual check returned `APosterioriSafetyFailure`, fall back to column-pivoted QR. @@ -682,26 +691,27 @@ The NaN/Inf check catches floating-point-near-singular matrices where LU "succee near-zero pivots. This is O(n) and has zero false positives. """ function _default_lu_solve_with_fallback( - cache::LinearCache, alg::DefaultLinearSolver, sol, args...; kwargs... + cache::LinearCache, alg::DefaultLinearSolver, sol ) if alg.safetyfallback if sol.retcode === ReturnCode.Failure - return _do_qr_fallback(cache, alg, sol, :lu_failure, args...; kwargs...) + return _do_qr_fallback(cache, alg, sol, :lu_failure) end if sol.retcode === ReturnCode.Success && any(!isfinite, sol.u) @SciMLMessage( "LU solve produced non-finite values (NaN/Inf), falling back to QR. Matrix is likely near-singular.", cache.verbose, :default_lu_fallback ) - return _do_qr_fallback(cache, alg, sol, :lu_failure, args...; kwargs...) + return _do_qr_fallback(cache, alg, sol, :lu_failure) end if sol.retcode === ReturnCode.APosterioriSafetyFailure - return _do_qr_fallback(cache, alg, sol, :residual_check, args...; kwargs...) + return _do_qr_fallback(cache, alg, sol, :residual_check) end end + # Use cache directly for type-stable inference (see _do_qr_fallback). return SciMLBase.build_linear_solution( - alg, sol.u, sol.resid, sol.cache; - retcode = sol.retcode, iters = sol.iters, stats = sol.stats + alg, cache.u, nothing, cache; + retcode = sol.retcode, iters = sol.iters, stats = nothing ) end @@ -762,8 +772,8 @@ end # its own residual check and returns APosterioriSafetyFailure if needed. inner_alg_expr = _algchoice_to_alg_with_safety(alg) newex = quote - sol = SciMLBase.solve!(cache, $inner_alg_expr, args...; kwargs...) - _default_lu_solve_with_fallback(cache, alg, sol, args...; kwargs...) + sol = SciMLBase.solve!(cache, $inner_alg_expr) + _default_lu_solve_with_fallback(cache, alg, sol) end elseif alg == Symbol(DefaultAlgorithmChoice.RFLUFactorization) inner_alg_expr = _algchoice_to_alg_with_safety(alg) @@ -771,8 +781,8 @@ end if !userecursivefactorization(nothing) error("Default algorithm calling solve on RecursiveFactorization without the package being loaded. This shouldn't happen.") end - sol = SciMLBase.solve!(cache, $inner_alg_expr, args...; kwargs...) - _default_lu_solve_with_fallback(cache, alg, sol, args...; kwargs...) + sol = SciMLBase.solve!(cache, $inner_alg_expr) + _default_lu_solve_with_fallback(cache, alg, sol) end elseif alg == Symbol(DefaultAlgorithmChoice.BLISLUFactorization) inner_alg_expr = _algchoice_to_alg_with_safety(alg) @@ -780,8 +790,8 @@ end if !useblis(nothing) error("Default algorithm calling solve on BLISLUFactorization without the extension being loaded. This shouldn't happen.") end - sol = SciMLBase.solve!(cache, $inner_alg_expr, args...; kwargs...) - _default_lu_solve_with_fallback(cache, alg, sol, args...; kwargs...) + sol = SciMLBase.solve!(cache, $inner_alg_expr) + _default_lu_solve_with_fallback(cache, alg, sol) end elseif alg == Symbol(DefaultAlgorithmChoice.CudaOffloadLUFactorization) inner_alg_expr = _algchoice_to_alg_with_safety(alg) @@ -789,8 +799,8 @@ end if !usecuda(nothing) error("Default algorithm calling solve on CudaOffloadLUFactorization without CUDA.jl being loaded. This shouldn't happen.") end - sol = SciMLBase.solve!(cache, $inner_alg_expr, args...; kwargs...) - _default_lu_solve_with_fallback(cache, alg, sol, args...; kwargs...) + sol = SciMLBase.solve!(cache, $inner_alg_expr) + _default_lu_solve_with_fallback(cache, alg, sol) end elseif alg == Symbol(DefaultAlgorithmChoice.MetalLUFactorization) inner_alg_expr = _algchoice_to_alg_with_safety(alg) @@ -798,18 +808,18 @@ end if !usemetal(nothing) error("Default algorithm calling solve on MetalLUFactorization without Metal.jl being loaded. This shouldn't happen.") end - sol = SciMLBase.solve!(cache, $inner_alg_expr, args...; kwargs...) - _default_lu_solve_with_fallback(cache, alg, sol, args...; kwargs...) + sol = SciMLBase.solve!(cache, $inner_alg_expr) + _default_lu_solve_with_fallback(cache, alg, sol) end else if alg in LinearSolve._SPARSE_ONLY_ALGORITHMS newex = quote if !(cache.A isa Array) - sol = SciMLBase.solve!(cache, $(algchoice_to_alg(alg)), args...; kwargs...) + sol = SciMLBase.solve!(cache, $(algchoice_to_alg(alg))) SciMLBase.build_linear_solution( - alg, sol.u, sol.resid, sol.cache; + alg, cache.u, nothing, cache; retcode = sol.retcode, - iters = sol.iters, stats = sol.stats + iters = sol.iters, stats = nothing ) else error( @@ -820,11 +830,11 @@ end end else newex = quote - sol = SciMLBase.solve!(cache, $(algchoice_to_alg(alg)), args...; kwargs...) + sol = SciMLBase.solve!(cache, $(algchoice_to_alg(alg))) SciMLBase.build_linear_solution( - alg, sol.u, sol.resid, sol.cache; + alg, cache.u, nothing, cache; retcode = sol.retcode, - iters = sol.iters, stats = sol.stats + iters = sol.iters, stats = nothing ) end end @@ -843,7 +853,7 @@ end return quote if cache.cacheval isa DefaultLinearSolverInit && cache.cacheval.fell_back_to_qr && !cache.isfresh - _reuse_qr_fallback(cache, alg, args...; kwargs...) + _reuse_qr_fallback(cache, alg) else $alg_dispatch end diff --git a/src/factorization.jl b/src/factorization.jl index f345a978d..0b68ef1e5 100644 --- a/src/factorization.jl +++ b/src/factorization.jl @@ -456,15 +456,22 @@ end function do_factorization(alg::QRFactorization, A, b, u) A = convert(AbstractMatrix, A) if ArrayInterface.can_setindex(typeof(A)) - if alg.inplace && !issparsematrixcsc(A) && !(A isa GPUArraysCore.AnyGPUArray) && - !is_cusparse(A) + # Sparse CSC (SPQR) does not accept a pivoting strategy, and CUDA's + # `qr` does not accept extra args either. Use the no-arg `qr(A)` + # form in those cases. For other CPU matrices, always pass + # `alg.pivot` so the return type is determined by the static + # `QRFactorization{P}` parameter (otherwise this branch returns + # `Union{QRCompactWY, QRPivoted}` depending on `alg.inplace`). + if A isa GPUArraysCore.AnyGPUArray || is_cusparse(A) || issparsematrixcsc(A) + fact = qr(A) + elseif alg.inplace if A isa Symmetric fact = qr(A, alg.pivot) else fact = qr!(A, alg.pivot) end else - fact = qr(A) # CUDA.jl does not allow other args! + fact = qr(A, alg.pivot) end else fact = qr(A, alg.pivot) From 6c20135489a0647595becf1669cb3cd91213f52d Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 14 May 2026 08:41:00 -0400 Subject: [PATCH 2/6] Add JET test group for solve!(cache) type-inference QA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a dedicated test/jet/ group (mirroring the layout Sundials.jl uses for its JET QA tests) that pins the type-inference contract for `solve!(cache)` across the default solver and many algorithms. Layout follows Sundials.jl test/jet/: separate Project.toml with `JET` under `compat = "0.9, 0.10, 0.11"`, and a self-contained runtests.jl that the GROUP=JET branch of the top-level runtests.jl activates and includes. What the tests pin: - `Core.Compiler.return_type(solve!, Tuple{cacheT})` for the default solver must be concrete and a subtype of `LinearSolution{Float64, 1, Vector{Float64}}`. This is the user-visible regression these tests guard against. - `solve!(init(prob, alg))` infers concretely for each of: LUFactorization, GenericLUFactorization, QRFactorization (ColumnNorm and NoPivot), DiagonalFactorization, SVDFactorization, CholeskyFactorization, NormalCholeskyFactorization. - BunchKaufmanFactorization and LDLtFactorization have unrelated inference issues; tracked as `@test_broken` so this group ratchets forward if they're fixed. - `JET.@test_opt` on the default solver is `broken = true` for now — the @generated default solver dispatches to every algorithm branch at inference time and several of those still have unrelated runtime dispatch sites inside Krylov/LinearAlgebra. The concrete-rettype checks are the load-bearing test in this group. Wires GROUP=JET into both test/runtests.jl and .github/workflows/Tests.yml. Co-Authored-By: Chris Rackauckas --- .github/workflows/Tests.yml | 1 + test/jet/Project.toml | 9 ++++ test/jet/runtests.jl | 84 +++++++++++++++++++++++++++++++++++++ test/runtests.jl | 7 ++++ 4 files changed, 101 insertions(+) create mode 100644 test/jet/Project.toml create mode 100644 test/jet/runtests.jl diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 05936e810..a86f225da 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -44,6 +44,7 @@ jobs: - "LinearSolvePyAMG" - "Preferences" - "Trim" + - "JET" os: - ubuntu-latest - macos-latest diff --git a/test/jet/Project.toml b/test/jet/Project.toml new file mode 100644 index 000000000..5edbbc9aa --- /dev/null +++ b/test/jet/Project.toml @@ -0,0 +1,9 @@ +[deps] +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +JET = "0.9, 0.10, 0.11" diff --git a/test/jet/runtests.jl b/test/jet/runtests.jl new file mode 100644 index 000000000..9f3892f98 --- /dev/null +++ b/test/jet/runtests.jl @@ -0,0 +1,84 @@ +using LinearSolve +using LinearAlgebra +using SparseArrays +using JET +using Test + +# Type-inference QA for `solve!(cache)`. The regression this guards against: +# `solve!(cache)` going through `DefaultLinearSolver` returned +# `LinearSolution{_A, _B, _C, _D, DefaultLinearSolver, _E, _F} where {...}` +# (a UnionAll over 6 free type parameters). The expected concrete return is +# `LinearSolution{Float64, 1, Vector{Float64}, Nothing, DefaultLinearSolver, +# , Nothing}`. + +# solve!(init(prob, alg)) is the full chain the user sees from solve(prob, alg). +# We re-init each time so the test exercises both `init` and `solve!`. +_solve_alg(A, b, alg) = solve!(init(LinearProblem(A, b), alg)) +_solve_default(A, b) = solve!(init(LinearProblem(A, b))) + +@testset "JET / type-inference" begin + @testset "Default solver — solve!(cache) returns concrete LinearSolution" begin + # Headline case: `solve!(cache)` after `init(LinearProblem(A, b))` must + # not return a UnionAll-typed LinearSolution. Was broken by the + # `_default_lu_solve_with_fallback`/`_do_qr_fallback` helpers reading + # `sol.u`/`sol.resid`/`sol.cache`/`sol.stats` from an inner `sol` whose + # rettype got capped to `Any` during precompile. + rt = Core.Compiler.return_type( + _solve_default, Tuple{Matrix{Float64}, Vector{Float64}} + ) + @test isconcretetype(rt) + @test rt <: LinearSolve.SciMLBase.LinearSolution{Float64, 1, Vector{Float64}} + end + + @testset "solve!(cache) is concrete for each algorithm" begin + # Each algorithm passed directly through `init(prob, alg)` must give a + # concrete LinearSolution out of `solve!(cache)`. + algs_concrete = ( + LUFactorization(), + GenericLUFactorization(), + QRFactorization(LinearAlgebra.ColumnNorm()), + QRFactorization(LinearAlgebra.NoPivot()), + DiagonalFactorization(), + SVDFactorization(), + CholeskyFactorization(), + NormalCholeskyFactorization(), + ) + for alg in algs_concrete + @testset "$(nameof(typeof(alg)))" begin + rt = Core.Compiler.return_type( + _solve_alg, + Tuple{Matrix{Float64}, Vector{Float64}, typeof(alg)} + ) + @test isconcretetype(rt) + end + end + + # These have known unrelated inference issues (see test/nopre/jet.jl). + # Tracked separately; not what this group is guarding against. + algs_broken = ( + BunchKaufmanFactorization(), + LDLtFactorization(), + ) + for alg in algs_broken + @testset "$(nameof(typeof(alg))) (broken)" begin + rt = Core.Compiler.return_type( + _solve_alg, + Tuple{Matrix{Float64}, Vector{Float64}, typeof(alg)} + ) + @test_broken isconcretetype(rt) + end + end + end + + @testset "JET.@test_opt on the default solver" begin + # Marked broken: the default-solver @generated function dispatches to + # every algorithm branch at inference time. Several of those branches + # (LDLt, Krylov, etc.) still have unrelated runtime-dispatch sites + # inside LinearSolve and Krylov that JET reports. The concrete-rettype + # tests above are the load-bearing check for this group; the @test_opt + # is here to ratchet down the remaining dispatch issues over time. + JET.@test_opt target_modules=(LinearSolve,) broken=true _solve_default( + rand(4, 4), rand(4) + ) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index a24c9eef5..fe3fa86be 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -127,3 +127,10 @@ if GROUP == "Trim" && VERSION >= v"1.12.0" Pkg.instantiate() @time @safetestset "Trim Tests" include("trim/runtests.jl") end + +if GROUP == "JET" + Pkg.activate("jet") + Pkg.develop(PackageSpec(path = dirname(@__DIR__))) + Pkg.instantiate() + @time @safetestset "JET type-inference QA" include("jet/runtests.jl") +end From ca9dfaffd5f24201891053b3aa07d8e5f778f19c Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 14 May 2026 08:57:32 -0400 Subject: [PATCH 3/6] fixup: Runic formatting + exclude JET group on pre-release Julia MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add spaces around `=` in `JET.@test_opt` keyword args (Runic). - Exclude `group: JET, version: pre` in the CI matrix — JET 0.9–0.11 (the versions pinned to match Sundials.jl) don't support Julia 1.13.0-rc1 yet, mirroring the existing NoPre group exclusion. Co-Authored-By: Chris Rackauckas --- .github/workflows/Tests.yml | 5 +++++ test/jet/runtests.jl | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index a86f225da..bd9fd5522 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -72,6 +72,11 @@ jobs: # Elemental_jll has no Windows binary - group: LinearSolveElemental os: windows-latest + # JET 0.9–0.11 (pinned to match Sundials.jl) doesn't support + # Julia pre-release (1.13.0-rc1) yet; same exclusion strategy as + # the existing NoPre group. + - group: JET + version: "pre" uses: "SciML/.github/.github/workflows/tests.yml@v1" with: group: "${{ matrix.group }}" diff --git a/test/jet/runtests.jl b/test/jet/runtests.jl index 9f3892f98..1ea7945de 100644 --- a/test/jet/runtests.jl +++ b/test/jet/runtests.jl @@ -77,7 +77,7 @@ _solve_default(A, b) = solve!(init(LinearProblem(A, b))) # inside LinearSolve and Krylov that JET reports. The concrete-rettype # tests above are the load-bearing check for this group; the @test_opt # is here to ratchet down the remaining dispatch issues over time. - JET.@test_opt target_modules=(LinearSolve,) broken=true _solve_default( + JET.@test_opt target_modules = (LinearSolve,) broken = true _solve_default( rand(4, 4), rand(4) ) end From 68e2b7d3346298638ecead90602489768293ec79 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Fri, 15 May 2026 14:52:29 -0400 Subject: [PATCH 4/6] Fold JET tests into existing test/nopre/ group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the separate test/jet/ group introduced earlier in this PR. The existing NoPre group already has test/nopre/jet.jl with JET tests for LinearSolve, so the new concrete-rettype checks belong there alongside the existing @test_opt checks rather than in a parallel group with its own Project.toml and CI matrix entry. Moves the contents of test/jet/runtests.jl into the bottom of test/nopre/jet.jl as two new testsets: - "solve!(cache) returns concrete LinearSolution — default solver": `Core.Compiler.return_type(_solve_default, Tuple{Matrix{Float64}, Vector{Float64}})` must be concrete and a subtype of `LinearSolution{Float64, 1, Vector{Float64}}`. - "solve!(cache) is concrete for each algorithm": same check for LUFactorization, GenericLUFactorization, QRFactorization (ColumnNorm and NoPivot), DiagonalFactorization, SVDFactorization, CholeskyFactorization, NormalCholeskyFactorization. BunchKaufman and LDLt remain `@test_broken` for unrelated inference issues. Removes the test/jet/ directory, the GROUP == "JET" branch in test/runtests.jl, and the JET entries (matrix value + pre-release exclude) from .github/workflows/Tests.yml. JET is already a dep of the NoPre Project.toml, so no Project.toml changes are needed. Verified locally on Julia 1.12.4: GROUP=NoPre passes with 27 passing / 8 broken in the JET Tests safetestset (the 8 broken match the pre-PR broken set plus BunchKaufman/LDLt). Co-Authored-By: Chris Rackauckas --- .github/workflows/Tests.yml | 6 --- test/jet/Project.toml | 9 ---- test/jet/runtests.jl | 84 ------------------------------------- test/nopre/jet.jl | 58 +++++++++++++++++++++++++ test/runtests.jl | 6 --- 5 files changed, 58 insertions(+), 105 deletions(-) delete mode 100644 test/jet/Project.toml delete mode 100644 test/jet/runtests.jl diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index bd9fd5522..05936e810 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -44,7 +44,6 @@ jobs: - "LinearSolvePyAMG" - "Preferences" - "Trim" - - "JET" os: - ubuntu-latest - macos-latest @@ -72,11 +71,6 @@ jobs: # Elemental_jll has no Windows binary - group: LinearSolveElemental os: windows-latest - # JET 0.9–0.11 (pinned to match Sundials.jl) doesn't support - # Julia pre-release (1.13.0-rc1) yet; same exclusion strategy as - # the existing NoPre group. - - group: JET - version: "pre" uses: "SciML/.github/.github/workflows/tests.yml@v1" with: group: "${{ matrix.group }}" diff --git a/test/jet/Project.toml b/test/jet/Project.toml deleted file mode 100644 index 5edbbc9aa..000000000 --- a/test/jet/Project.toml +++ /dev/null @@ -1,9 +0,0 @@ -[deps] -JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae" -SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[compat] -JET = "0.9, 0.10, 0.11" diff --git a/test/jet/runtests.jl b/test/jet/runtests.jl deleted file mode 100644 index 1ea7945de..000000000 --- a/test/jet/runtests.jl +++ /dev/null @@ -1,84 +0,0 @@ -using LinearSolve -using LinearAlgebra -using SparseArrays -using JET -using Test - -# Type-inference QA for `solve!(cache)`. The regression this guards against: -# `solve!(cache)` going through `DefaultLinearSolver` returned -# `LinearSolution{_A, _B, _C, _D, DefaultLinearSolver, _E, _F} where {...}` -# (a UnionAll over 6 free type parameters). The expected concrete return is -# `LinearSolution{Float64, 1, Vector{Float64}, Nothing, DefaultLinearSolver, -# , Nothing}`. - -# solve!(init(prob, alg)) is the full chain the user sees from solve(prob, alg). -# We re-init each time so the test exercises both `init` and `solve!`. -_solve_alg(A, b, alg) = solve!(init(LinearProblem(A, b), alg)) -_solve_default(A, b) = solve!(init(LinearProblem(A, b))) - -@testset "JET / type-inference" begin - @testset "Default solver — solve!(cache) returns concrete LinearSolution" begin - # Headline case: `solve!(cache)` after `init(LinearProblem(A, b))` must - # not return a UnionAll-typed LinearSolution. Was broken by the - # `_default_lu_solve_with_fallback`/`_do_qr_fallback` helpers reading - # `sol.u`/`sol.resid`/`sol.cache`/`sol.stats` from an inner `sol` whose - # rettype got capped to `Any` during precompile. - rt = Core.Compiler.return_type( - _solve_default, Tuple{Matrix{Float64}, Vector{Float64}} - ) - @test isconcretetype(rt) - @test rt <: LinearSolve.SciMLBase.LinearSolution{Float64, 1, Vector{Float64}} - end - - @testset "solve!(cache) is concrete for each algorithm" begin - # Each algorithm passed directly through `init(prob, alg)` must give a - # concrete LinearSolution out of `solve!(cache)`. - algs_concrete = ( - LUFactorization(), - GenericLUFactorization(), - QRFactorization(LinearAlgebra.ColumnNorm()), - QRFactorization(LinearAlgebra.NoPivot()), - DiagonalFactorization(), - SVDFactorization(), - CholeskyFactorization(), - NormalCholeskyFactorization(), - ) - for alg in algs_concrete - @testset "$(nameof(typeof(alg)))" begin - rt = Core.Compiler.return_type( - _solve_alg, - Tuple{Matrix{Float64}, Vector{Float64}, typeof(alg)} - ) - @test isconcretetype(rt) - end - end - - # These have known unrelated inference issues (see test/nopre/jet.jl). - # Tracked separately; not what this group is guarding against. - algs_broken = ( - BunchKaufmanFactorization(), - LDLtFactorization(), - ) - for alg in algs_broken - @testset "$(nameof(typeof(alg))) (broken)" begin - rt = Core.Compiler.return_type( - _solve_alg, - Tuple{Matrix{Float64}, Vector{Float64}, typeof(alg)} - ) - @test_broken isconcretetype(rt) - end - end - end - - @testset "JET.@test_opt on the default solver" begin - # Marked broken: the default-solver @generated function dispatches to - # every algorithm branch at inference time. Several of those branches - # (LDLt, Krylov, etc.) still have unrelated runtime-dispatch sites - # inside LinearSolve and Krylov that JET reports. The concrete-rettype - # tests above are the load-bearing check for this group; the @test_opt - # is here to ratchet down the remaining dispatch issues over time. - JET.@test_opt target_modules = (LinearSolve,) broken = true _solve_default( - rand(4, 4), rand(4) - ) - end -end diff --git a/test/nopre/jet.jl b/test/nopre/jet.jl index b890d3e63..25c484187 100644 --- a/test/nopre/jet.jl +++ b/test/nopre/jet.jl @@ -197,3 +197,61 @@ end JET.@test_opt init(dual_prob) end end + +# Concrete-return-type QA for `solve!(cache)`. Guards against the regression +# where `solve!(cache)` through `DefaultLinearSolver` returned +# `LinearSolution{_A, _B, _C, _D, DefaultLinearSolver, _E, _F} where {...}` +# (a UnionAll over 6 free type parameters) instead of a concrete LinearSolution. +_solve_alg(A, b, alg) = solve!(init(LinearProblem(A, b), alg)) +_solve_default(A, b) = solve!(init(LinearProblem(A, b))) + +@testset "solve!(cache) returns concrete LinearSolution — default solver" begin + # Headline case: `solve!(cache)` after `init(LinearProblem(A, b))` must not + # return a UnionAll-typed LinearSolution. Was broken by the + # `_default_lu_solve_with_fallback`/`_do_qr_fallback` helpers reading + # `sol.u`/`sol.resid`/`sol.cache`/`sol.stats` from an inner `sol` whose + # rettype got capped to `Any` during precompile. + rt = Core.Compiler.return_type( + _solve_default, Tuple{Matrix{Float64}, Vector{Float64}} + ) + @test isconcretetype(rt) + @test rt <: LinearSolve.SciMLBase.LinearSolution{Float64, 1, Vector{Float64}} +end + +@testset "solve!(cache) is concrete for each algorithm" begin + algs_concrete = ( + LUFactorization(), + GenericLUFactorization(), + QRFactorization(LinearAlgebra.ColumnNorm()), + QRFactorization(LinearAlgebra.NoPivot()), + DiagonalFactorization(), + SVDFactorization(), + CholeskyFactorization(), + NormalCholeskyFactorization() + ) + for alg in algs_concrete + @testset "$(nameof(typeof(alg)))" begin + rt = Core.Compiler.return_type( + _solve_alg, + Tuple{Matrix{Float64}, Vector{Float64}, typeof(alg)} + ) + @test isconcretetype(rt) + end + end + + # Known unrelated inference issues — tracked separately, not what this + # group is guarding against. + algs_broken = ( + BunchKaufmanFactorization(), + LDLtFactorization() + ) + for alg in algs_broken + @testset "$(nameof(typeof(alg))) (broken)" begin + rt = Core.Compiler.return_type( + _solve_alg, + Tuple{Matrix{Float64}, Vector{Float64}, typeof(alg)} + ) + @test_broken isconcretetype(rt) + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index fe3fa86be..cc58e6cd0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -128,9 +128,3 @@ if GROUP == "Trim" && VERSION >= v"1.12.0" @time @safetestset "Trim Tests" include("trim/runtests.jl") end -if GROUP == "JET" - Pkg.activate("jet") - Pkg.develop(PackageSpec(path = dirname(@__DIR__))) - Pkg.instantiate() - @time @safetestset "JET type-inference QA" include("jet/runtests.jl") -end From ac096e3ae8c7e5a08a249b53226f979e02c04ccb Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sun, 17 May 2026 17:48:13 -0400 Subject: [PATCH 5/6] fixup: Runic formatting in test/nopre/jet.jl and test/runtests.jl - Add trailing comma after `NormalCholeskyFactorization()` and `LDLtFactorization()` in the algs_concrete / algs_broken tuples in test/nopre/jet.jl. - Drop trailing blank line at end of test/runtests.jl. Both flagged by the runic CI job (#984). Verified locally with Runic.jl v1.7 via `Runic.format_string` round-trip on all PR-changed files. Co-Authored-By: Chris Rackauckas --- test/nopre/jet.jl | 4 ++-- test/runtests.jl | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/nopre/jet.jl b/test/nopre/jet.jl index 25c484187..660c908d4 100644 --- a/test/nopre/jet.jl +++ b/test/nopre/jet.jl @@ -227,7 +227,7 @@ end DiagonalFactorization(), SVDFactorization(), CholeskyFactorization(), - NormalCholeskyFactorization() + NormalCholeskyFactorization(), ) for alg in algs_concrete @testset "$(nameof(typeof(alg)))" begin @@ -243,7 +243,7 @@ end # group is guarding against. algs_broken = ( BunchKaufmanFactorization(), - LDLtFactorization() + LDLtFactorization(), ) for alg in algs_broken @testset "$(nameof(typeof(alg))) (broken)" begin diff --git a/test/runtests.jl b/test/runtests.jl index cc58e6cd0..a24c9eef5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -127,4 +127,3 @@ if GROUP == "Trim" && VERSION >= v"1.12.0" Pkg.instantiate() @time @safetestset "Trim Tests" include("trim/runtests.jl") end - From e23ffa39826ceccc70900c9422e439e8b669e267 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Wed, 20 May 2026 09:48:12 -0400 Subject: [PATCH 6/6] Re-trigger CI to pick up AllocCheck v0.2.4 AllocCheck v0.2.4 was released 2026-05-20 with the fix from JuliaLang/AllocCheck.jl#102 ("Use 3-arg LLVM.lookup to avoid deprecation warning"). This unblocks the NoPre test group on LinearSolve, which had been red on every PR for 8 days due to AllocCheck 0.2.3 triggering an LLVM.jl 9.8.1 depwarn that `@test_nowarn` in test/nopre/static_arrays.jl correctly captured. Co-Authored-By: Chris Rackauckas