Fold the SparseArrays extension into the root module - #1191
Merged
ChrisRackauckas merged 1 commit intoAug 9, 2026
Conversation
SparseArrays is a hard `[deps]` entry -- `src/SupernodalLU` loads it at package load time -- so `LinearSolveSparseArraysExt` always loaded, but it loaded *after* the root module precompiled. Inserting its `init_cacheval` methods invalidated the default-path specializations the root `@compile_workload` had just cached, so the workload's `solve(prob)` coverage was thrown away before any user reached it: a first `solve(prob)` on a 4x4 dense Float64 problem cost 82 ms rather than the 0.09 ms it costs once the methods are in place before the workload runs. `ext/LinearSolveSparseArraysExt.jl` moves to `src/sparsearrays.jl` unchanged apart from its module header and import list, and is included just before the root workload. It stays one self-contained file so it can be excised again if SparseArrays ever becomes genuinely optional; the file header spells out the steps. The `getcolptr`/`rowvals`/`nonzeros` stubs in `src/LinearSolve.jl` are dropped -- nothing referenced them, and they collided with the moved file's `using SparseArrays` import. Project.toml drops the `LinearSolveSparseArraysExt` entry and removes SparseArrays from the fourteen extensions that listed it as a co-trigger; it was never in `[weakdeps]`, so those entries were already no-ops. `LinearSolveSparseArraysExt.KLU` becomes `LinearSolve.KLU` in the tests, the QA extension inventory and the docs. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed and why
ext/LinearSolveSparseArraysExt.jlmoves tosrc/sparsearrays.jland isincluded fromsrc/LinearSolve.jl. SparseArrays is an unconditional[deps]entry —src/SupernodalLUdoes
using SparseArraysat package load time — so the extension always loaded, but itloaded after the root module precompiled. Inserting its
init_cachevalmethodsinvalidated the default-path specializations the root
@compile_workloadhad just cached,so that coverage was thrown away before any user reached it.
Measured,
using LinearSolvealone, RecursiveFactorization not loaded, Julia 1.12.6:solve(prob), 4×4 denseFloat64(median of 7 fresh processes)MethodInstances onusing LinearSolve@elapsed using LinearSolve(min of 7)Invalidation evidence
SnoopCompileCore.@snoop_invalidations using LinearSolve, min-identical across two runs perarm. The four trees rooted at the extension's
init_cachevalare gone; nothing else movesexcept
SparseColumnPivotedQRAMDExtshedding 5 children.TTFX, two independent runs of 7 fresh processes per arm. Run 1 is the headline above; run 2
alternates base/branch rep-by-rep so machine load hits both equally (host load average ~39
throughout). Raw
@elapsedof the firstsolve(prob), in ms:Run 2 is uniformly a few percent slower on both arms (busier host); the ratio is ~550-950x
either way.
Precompile cost, same paired protocol,
JULIA_NUM_PRECOMPILE_TASKS=1, 4 reps(a unique comment is appended to
src/LinearSolve.jleach rep because Julia ≥ 1.11content-hashes sources):
LinearSolve15.4 s +→ SparseArraysExt6.0 sLinearSolve19.4 sFolding is precompile-neutral on CPU and slightly cheaper on wall: the root module gets
bigger, but there is no longer a second module to load, hash and write.
What moved, and how to move it back
src/sparsearrays.jlis the extension file verbatim apart from itsmoduleheader and itsusing LinearSolve: ...import list. Git sees it as a rename (98% similarity), so the reviewdiff is 64 added lines, not 1500. It is deliberately one self-contained unit, not
scattered into the existing
src/files, and its header states the excise-later path:Placement in
src/LinearSolve.jlis load-bearing in both directions: after theneeds_square_A/useblis/usecudatraits, which the file's own@compile_workloadcalls, and before the root
@compile_workload, which it would otherwise invalidate.Project.toml
LinearSolveSparseArraysExtis deleted from[extensions], and SparseArrays is removed fromthe trigger list of the fourteen extensions that co-triggered on it (CUSOLVERRF, CliqueTrees,
Enzyme, Ginkgo, HSL, MUMPS, PETSc, PETScMPI, ParU, PureUMFPACK, Pardiso, SuperLUDIST,
STRUMPACK, Sparspak). Correction to what I was told going in: SparseArrays was in
[deps]and
[extensions]but never in[weakdeps], so those fourteen entries were already no-ops —a trigger that names a hard dep is always satisfied. Removing them is tidying, not a
prerequisite. SparseArrays stays in
[deps]and keeps its[compat]entry.Collateral, all forced by the move
getcolptr/rowvals/nonzerosstubs deleted fromsrc/LinearSolve.jl. They collidedwith the moved file's
using SparseArrays: ..., getcolptr, rowvals, nonzeros. They weredead: nothing in
src/,ext/,lib/,test/ordocs/ever calledLinearSolve.getcolptrand friends, and the extension never defined methods on them —src/SupernodalLUimports the real ones from SparseArrays directly.LinearSolveSparseArraysExt.KLU→LinearSolve.KLUintest/Core/basictests.jl,test/qa/qa.jl,docs/make.jlanddocs/src/solvers/solvers.md.src/KLU/klu.jlwasalready in
src/; it wasincluded into the extension over../src/.init_cacheval(::NormalCholeskyFactorization, ...)loses itswhere {T <: BLASELTYPES}in favour of inline
<:BLASELTYPESbounds.Twas unbound on theAnyGPUArraymember ofthe
Aunion and unused in the body; Aqua'stest_unbound_argsonly scans the packagemodule, so it never saw this method while it lived in an extension. Dispatch is unchanged
for every sparse case; the only widening is that
Symmetric's eltype and its parent'seltype are no longer tied to each other, which
Symmetric{T, S<:AbstractMatrix{<:T}}already guarantees.
const libamd/libbtfadded tosrc/KLU/klu.jl. JET'sreport_package(LinearSolve; target_modules = (LinearSolve,))now reachesLinearSolve.KLUand reported 30 findings ofthe form
`LinearSolve.KLU.libamd` is not defined. Upstream KLU.jl does not definethem either — every AMD/BTF wrapper in
src/KLU/wrappers.jlis a latentUndefVarError.Two constants in the same
:libklustyle fix the bindings. The wrappers stay uncalled, soI have not verified that they now work end to end, only that the bindings resolve.
Verification
Julia 1.12.6, x86_64 Linux, OpenBLAS. Commands and output tails:
Quality Assuranceincludes Aqua's ambiguity and piracy checks, which matter here because themove changes which module owns these methods: Method ambiguity 1/1, Piracy 1/1, Unbound type
parameters 1/1, ExplicitImports 6/6, Public API documentation 2/2 — all green (the unbound
one only after the
NormalCholeskyFactorizationfix above; it fails without it, see below).Failing-before / passing-after for the QA fixes
The three QA findings above are all caused by the move (they are checks that only apply to
the package module), so they discriminate. On the first
GROUP=QArun of this branch, beforethe fixes:
After:
Quality Assurance | 48 48,Testing LinearSolve tests passed(quoted above).Pre-existing failure this PR does not cause
GROUP=QAon the 1.10 LTS is red on unmodifiedorigin/maintoo:I ran
GROUP=QAon 1.10 against bothorigin/mainand this branch. The two reports arebyte-identical apart from one stack frame naming the file that moved
(
LinearSolveSparseArraysExt.jl:518→src/sparsearrays.jl:531), and the testset totals arethe same 20/1/21/42. The whole reported dispatch chain is inside
Base.CoreLoggingandBase.typejoin. 1.12 is clean, and it is invisible to CI (SciML/.github's matrix scriptclamps QA to
["1"], so noQA (julia lts)job is ever emitted) — it only bites local LTSruns.
Tracked as #1190. Bisected to
#1083, which removed a
broken = truethat hadbeen covering a pre-existing Base-1.10 inference limitation:
@test_optand log emission aremutually exclusive on 1.10 for any package. Zero-SciML reproducer, in an environment holding
only JET:
f() = (@info "hello"; nothing); JET.@report_opt f()gives 2 reports on 1.10.11 and0 on 1.11.9 and 1.12.6. Not this PR's to fix.
CI
55 pass, 0 fail at the time of writing; 4 still running (ModelingToolkit,
SciMLSensitivity and BoundaryValueDiffEq downstream, and GPU). The jobs that cover what I
could not run locally are green, including every group whose
[extensions]trigger listthis PR edited:
Not verified
[compat]conflicts with CUDSS, see the note intest/qa/qa.jl), so its trigger-list edit is unexercised. It keeps"CUSOLVERRF"as atrigger and SparseArrays is a hard dep, so it should trigger exactly as before, but nothing
proves it.
LinearSolvePETScran.Trim,AD,Downgradeand the downstream jobs were not run locally either, but CI covers themand they are green (above). GPU is still running.
libamd/libbtfccall wrappers resolve their bindings now but are still never called,so "they work" is not claimed.
GROUP=QAon 1.10 cannot go green until the pre-existing JET failure above is resolved.Things a reviewer should push back on
LinearSolve.getcolptr/rowvals/nonzeros. Undocumented, unexported,error-throwing stubs with no callers anywhere in the repo — but they are technically a
removal, and a downstream package could in principle name them.
NormalCholeskyFactorizationsignature widening. It is the one behavioural edit inthis PR; I argue it is a no-op for dispatch, but it is a real signature change.
those are the mandated file header documenting the excise-later path, and 5 are edits to
existing comments that named "the SparseArrays extension". Against the 1504-line file the
header sits on, it is 1.2%. Happy to cut it further if you would rather it lived in the PR
description only.
dropped from this PR if you would rather keep the diff to the fold itself.
Relationship to #1188
#1188 ("Move default-path precompile coverage into the extensions that invalidate it") was
closed unmerged. Its sparse half is subsumed by this PR: it added a per-eltype
solve(denseprob)loop inside the SparseArrays extension to re-cache the default path thatextension invalidated. With SparseArrays in the root module there is nothing to re-cache — the
root workload's own
solve(prob)now survives, which is the 82.0 ms → 0.087 ms above. No codefrom #1188 is carried over here.
What #1188 raised and this PR does not address, left for you to decide:
optional, so it cannot be fixed by folding. Move default-path precompile coverage into the extensions that invalidate it #1188 measured a first default
solve(prob)at6.63 s with RF loaded and proposed a workload inside
ext/LinearSolveRecursiveFactorizationExt.jl. I have not re-measured that number on thisbranch and have not implemented it; it deserves its own small PR judged on its own numbers.
Float32) coverage in the root workload: a separate axis with its ownmeasured precompile cost, unrelated to the fold.
🤖 Generated with Claude Code