Skip to content

Put the extensions under QA's ExplicitImports checks (1/39 -> 26/39) - #1133

Merged
ChrisRackauckas merged 1 commit into
SciML:mainfrom
ChrisRackauckas-Claude:qa-check-extensions
Aug 1, 2026
Merged

ChrisRackauckas merged 1 commit into
SciML:mainfrom
ChrisRackauckas-Claude:qa-check-extensions

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Aug 1, 2026 •

Copy link
Copy Markdown
Member

Please ignore this PR until it has been reviewed by @ChrisRackauckas. It is
opened as a draft on purpose.

The gap

SciMLTesting.run_qa runs ExplicitImports' checks on the package. ExplicitImports
does know about extensions — it reads the [extensions] table out of Project.toml
and adds each extension module to the set it analyzes — but only if the module actually
exists:

ext_mod = Base.get_extension(mod, Symbol(ext))
ext_mod === nothing && continue

An extension module only exists once every one of its trigger packages has been loaded,
and test/qa/Project.toml loaded none of them. Verified on the base branch:

loaded extensions: 1 / 39
--- SCANNED MODULES ---
SCANNED: LinearSolve
SCANNED: LinearSolve.OperatorCondition
SCANNED: LinearSolve.WarmStart
SCANNED: LinearSolve.SupernodalLU.AMD
SCANNED: LinearSolve.DefaultAlgorithmChoice
SCANNED: LinearSolveSparseArraysExt.KLU
SCANNED: LinearSolve.NonstructuralZeros
SCANNED: LinearSolve.SupernodalLU
SCANNED: LinearSolveSparseArraysExt

Only LinearSolveSparseArraysExt was covered, and only incidentally — SparseArrays is
a hard dependency, so that extension always loads. The other 38 extensions were never
looked at.

What this does

Adds the resolvable weakdeps to test/qa/Project.toml (with compat bounds mirroring the
root Project.toml) and usings them in test/qa/qa.jl. That takes coverage from
1/39 to 26/39:

loaded extensions: 26 / 39

Now scanned (26): AMDGPU, AlgebraicMultigrid, ArnoldiMethod, Arpack, BLIS,
BandedMatrices, BlockDiagonals, CUDA (cuSOLVER), CUDSS, ChainRulesCore,
CliqueTrees, Enzyme, FastAlmostBandedMatrices, FastLapackInterface, ForwardDiff,
IterativeSolvers, JacobiDavidson, KernelAbstractions, KrylovKit, Metal,
Mooncake, PureUMFPACK, RecursiveFactorization, SparseArrays, Sparspak,
SpecializingFactorizations.

LinearSolveBLISExt came for free — blis_jll and LAPACK_jll were already deps of
the QA environment, they just were not being loaded.

The GPU backends do not need a GPU

ExplicitImports needs the extension module to exist, not a device. Tested one at a
time on this CPU-only x86-64 Linux host; every one precompiled and imported, and
Base.get_extension returned non-nothing:

weakdep resolve+precompile get_extension
AMDGPU 82 s loaded
cuSOLVER 96 s loaded
CUDSS 88 s loaded
CUSOLVERRF 157 s loaded
Metal 60 s loaded (logs Metal.jl is only supported on Apple Silicon at init)

Four of the five are in. CUSOLVERRF is left out for a resolution conflict, not a
hardware one:
CUSOLVERRF = "0.2.6" pins CUDA to 5.7.3 - 5.11.3, while
CUDSS = "0.7" requires CUDACore 6, so the root [compat] entries for the two cannot
both be satisfied in one environment:

ERROR: Unsatisfiable requirements detected for package CUDACore [bd0ed864]:
 ├─required (without additional version restrictions) by CUDSS [45b445bb]
 └─restricted by compatibility requirements with CUDA [052768ef] to versions: uninstalled
   ├─restricted by compatibility requirements with CUSOLVERRF [a8cc9031] to versions: [4.0.0 - 4.4.2, 5.7.3 - 5.11.3]

I kept CUDSS. Adding CUSOLVERRF back needs that upstream conflict resolved first.
Resolving + precompiling the whole QA environment takes ~128 s here; the CUDA/ROCm
toolkit artifacts stay lazy with no driver present.

Still unscanned (13), listed with reasons in a comment in qa.jl:

reason extensions
needs a real system install (MPI, vendor libraries) Elemental, Ginkgo, HSL, HYPRE, MUMPS, Pardiso, PETSc, PETScMPI, PartitionedSolvers, STRUMPACK, SuperLUDIST
needs Julia ≥ 1.12 (QA also runs on lts) ParU
cannot co-resolve with CUDSS (above) CUSOLVERRF

Load guard

Base.get_extension returning nothing makes ExplicitImports skip an extension
silently — the checks still report a clean pass. A future change that broke an
extension's precompilation would therefore turn QA green while coverage dropped back to
zero. qa.jl now asserts the 22 modules exist:

@testset "Extensions loaded" begin
    for ext in loaded_extensions
        @test Base.get_extension(LinearSolve, ext) !== nothing
    end
end

Test ordering

qa.jl now runs after jet.jl and the allocation tests. Package loading is
process-global, and several of the newly-loaded extensions change algorithm selection —
LinearSolveBLISExt defines LinearSolve.useblis(::Nothing) = true, making BLIS the
default LU. With qa.jl first, jet.jl started analyzing the BLIS path and failed on
runtime dispatch inside get_blas_operation_info. Running qa.jl last keeps the
pristine environment those files expect.

One consequence worth flagging: the QA group is already red on lts on main (see
"Pre-existing failures" below), and the group aborts at the first failing safetestset.
So while jet.jl is broken on lts, qa.jl — and therefore ExplicitImports — will not
run there. It runs and passes on Julia 1.

Fixes made in extension source (not ignored)

  • using Foo + Foo.bar replaced with explicit imports across 21 extensions.
  • LinearSolveCliqueTreesExt imported cholesky! / ldiv! from
    CliqueTrees.Multifrontal; owner is LinearAlgebra.
  • LinearSolveArpackExt / ArnoldiMethodExt / KrylovKitExt / JacobiDavidsonExt
    reached build_eigenvalue_solution through LinearSolve; owner is SciMLBase.
  • LinearSolveFastLapackInterfaceExt used LinearSolve.@set! (owner Setfield) and
    LinearSolve.ArrayInterface; LinearSolveRecursiveFactorizationExt used
    LinearSolve.ArrayInterface and RecursiveFactorization.ldiv! (owner
    TriangularSolve); LinearSolveForwardDiffExt used LinearSolve.@concrete (owner
    ConcreteStructs).
  • LinearSolveBLISExt imported require_one_based_indexing via
    LinearAlgebra.LAPACK; owner is Base.
  • Stale imports removed from the Mooncake, ChainRulesCore, ForwardDiff,
    RecursiveFactorization, Enzyme, CUDA, CUDSS, AMDGPU and Metal extensions.

Two real bugs fell out of this:

  1. LinearSolveMooncakeExt referenced OperatorAssumptions and defaultalg without
    importing them.
    Its rrule!!(..., _alg::CoDual{Nothing}, ...) method would have
    thrown UndefVarError on first call. Both are now imported.

  2. defaultalg_adjoint_eval's generator emitted a broken error message. The fallback
    branch was

    quote
        error("Default linear solver with algorithm $(alg) is currently not supported ...")
    end

    Inside quote, the $(alg) of a string literal is not interpolated at generator
    time — it is left as a reference to a runtime binding alg, which does not exist in
    the generated method, so the branch would throw UndefVarError: LinearSolve.alg
    instead of the intended message. The name is now interpolated when the method is
    generated. This is what run_qa's JET pass caught once the reordering above made JET
    available to it.

Ignore entries added, and why

All are in test/qa/qa.jl with a comment naming the owner. No check was disabled and no
@test_broken / @test_skip was added.

no_stale_explicit_imports — eval_generated_imports: the 15 algorithm types the
BandedMatrices / FastAlmostBandedMatrices extensions reference only from inside
for alg in (...) @eval ... end loops. They must be imported for the generated
init_cacheval methods to resolve (precompilation fails without them — that is how they
were found), but ExplicitImports analyzes source text rather than expanded code, so it
sees the imports as unused.

all_qualified_accesses_via_owners — CUDACore, cuSPARSE. LinearSolveCUDAExt
and LinearSolveCUDSSExt reach these through cuSOLVER / CUDSS, their declared
triggers. The usual fix is to add the owner as a second trigger, but that would stop the
extension loading for anyone who does a plain using cuSOLVER, so the aliases stay.

all_explicit_imports_via_owners — init, solve, solve!. Owner is CommonSolve,
reached via SciMLBase. CommonSolve is not a direct LinearSolve dependency, and only
the extensions use these names, so adding it would just make it a stale dep of the main
module.

all_explicit_imports_are_public, two groups:

  • backend_internal_imports — names with no public spelling at the backend the
    extension wraps: Mooncake's reverse-rule interface
    (@from_chainrules, @is_primitive, CoDual, MinimalCtx, NoRData, ReverseMode,
    fdata, primal, rdata, zero_fcodual), EnzymeCore.EnzymeRules, ForwardDiff's
    Dual / Partials, ArnoldiMethod's LM/LR/SR/LI/SI target singletons, and
    Sparspak's sparspaklu / sparspaklu!.
  • linearsolve_internal_imports — LinearSolve's own non-public names imported from
    LinearSolve by its extensions (@get_cacheval, LinearCache, defaultalg,
    init_cacheval, …). Same class as the qualified accesses already tracked in QA: ExplicitImports tracked @test_broken markers (qualified-accesses-public, no-implicit-imports) #1058;
    promoting them is a separate public-API decision.

The pre-existing ei_broken = (:all_qualified_accesses_are_public,) (also #1058) now
covers a lot more ground, since the extensions reach LinearSolve internals heavily.

Local runs

GROUP=QA julia --project=. -e 'using Pkg; Pkg.test()' on Julia 1.12.4:

Test Summary: | Pass  Broken  Total     Time
JET Tests     |   34       8     42  1m09.1s
Test Summary: | Pass  Total     Time
Allocation QA |   48     48  1m02.7s
Test Summary:              | Pass  Total   Time
SupernodalLU Allocation QA |    8      8  19.3s
Test Summary:     | Pass  Broken  Total     Time
Quality Assurance |   43       2     45  2m57.1s
     Testing LinearSolve tests passed

The two Broken are the pre-existing aqua: ambiguities and
all_qualified_accesses_are_public placeholders.

Also smoke-tested every refactored code path directly in the QA environment, since
narrowing using Foo to explicit imports can only go wrong for names ExplicitImports
cannot see (macro-generated code). All pass:

FastLUFactorization  OK    SparspakFactorization      OK    ArpackJL           OK
FastQRFactorization  OK    CliqueTreesFactorization   OK    ArnoldiMethodJL    OK
RFLUFactorization    OK    PureUMFPACKFactorization   OK    KrylovKitEigen     OK
BLISLUFactorization  OK    AlgebraicMultigridJL       OK    JacobiDavidsonJL   OK
SpecializedLU/QR     OK    BandedMatrix default       OK    ForwardDiff dual   OK
IterativeSolversJL   OK    KernelAbstractions _fast_sym_givens!  OK
KrylovKitJL_GMRES    OK    Mooncake pullback                     OK

The @eval-generated init_cacheval methods and the KernelAbstractions @kernel are
the two macro-generated spots; both are exercised above (and the @eval ones fail
precompilation if an import is missing, which is how they were caught).

The four GPU extensions cannot be exercised beyond loading on this host — no device.
What is verified for them is that they precompile, that Base.get_extension returns
non-nothing, and that ExplicitImports reports zero implicit imports, i.e. every name
their source refers to is explicitly available. Their runtime paths are untested here.

GROUP=Core also passes end to end on this branch (rerun after rebasing onto
b6d3f82, so no workaround of any kind is involved):

     Testing LinearSolve tests passed
Basic Tests   |  635    635              Default Alg Tests |  109    109
EigenvalueProblem |   27     27          FixedSizeArrays |   47     47
Batched RHS   |   98     98              ComponentArrays |   10     10
GESV Factorization |   24     24         Adjoint Sensitivity |  107    107
LU Refactorization Reuse |   56     56   ForwardDiff Overloads |  128    128
Direct BLAS Refactorization Reuse | 130  Traits        |    6      6
Lightweight Solution (no cache) |   43   Algorithm Interface |  136    136
Return codes  |   71  (+1 broken)   72   Verbosity     |   66     66
Re-solve      |  133    133              BandedMatrices |   11     11
SupernodalLU internals |   72     72     Butterfly Factorization |   44     44
Krylov warm start |   37     37          Mixed Precision |   14  (+2 broken)   16
Zero Initialization Tests |    8      8  Resize        |   70     70
Non-Square Tests |   46     46           SpecializingFactorizations |   18     18
SparseVector b Tests |   12     12
Nonstructural Zeros |  141    141

Pre-existing failures on main (not caused by this PR)

Both were confirmed by running against a pristine checkout of main:

  1. GROUP=Core failed in "Default Alg Tests" with
    UndefVarError: FunctionOperator not defined — pristine main
    47 passed, 0 failed, 1 errored, identical on this branch. Root cause was not a
    LinearSolve commit: SciMLBase dropped its @reexport using SciMLOperators in
    Make strict QA owner-contract clean SciMLBase.jl#1472 (shipped in SciMLBase v3.40.0), and
    LinearSolve's compat is SciMLBase = "3.33", so it broke purely on a dependency
    upgrade. Fixed and already merged in
    Import FunctionOperator from its owner in Core tests #1132; this branch is rebased on top of
    it.

  2. GROUP=QA fails on lts (Julia 1.10.11) in jet.jl — pristine main on 1.10:
    JET Tests | 19 pass, 2 fail, 21 broken, identical on this branch and unaffected by
    anything here (jet.jl runs before qa.jl loads any of the new packages).
    Fix two JET runtime dispatches that only show up on Julia LTS #1134 fixes the
    SupernodalLU.Int(refine::Symbol) half (a genuine MethodError for any refine
    other than :auto) and the KLU._extract! non-constant-keyword half, taking lts to
    19 pass, 2 fail → 20 pass, 1 fail. The last one turns out to be a Julia 1.10 Base
    wart on any log emission (current_logger_for_env → moduleroot →
    sprint(show, ::String; sizehint) reports runtime dispatch on 1.10 and not on 1.12),
    reached through @SciMLMessage in the KLU failure branch; it was left unmarked rather
    than papered over with broken =.

    Consequence for this PR while that is open: since the QA group aborts at the first
    failing safetestset and qa.jl now runs last, ExplicitImports will not execute on lts
    until that assertion is resolved. It runs and passes on Julia 1.

Notes for review

  • LinearSolveSTRUMPACKExt is a plausible 23rd: STRUMPACK_jll is already a base test
    dep and the Core suite probes that extension, so it may well resolve in the QA
    environment too. Left out here to keep the environment to pure-Julia packages; easy
    follow-up if wanted.
  • ext/LinearSolvePETScCSRExt.jl exists on disk but has no entry in [extensions], so it
    is dead code. Left alone here.
  • lib/LinearSolveAutotune and lib/LinearSolvePyAMG have no [weakdeps] /
    [extensions], so there is nothing analogous to do in the sublibraries.
  • defaultalg_adjoint_eval's Krylov branch reads cache.val.abstol / cache.val.reltol
    / cache.val.verbose, which looks like it should be cache.abstol etc. Not touched
    here since JET did not flag it and no test covers that path, but worth a look.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Yb5kCpT5SRzTrhppKSh1n7

ExplicitImports reads the [extensions] table out of Project.toml and analyzes
each extension module, but only once that module exists -- which requires every
one of its trigger packages to be loaded. The QA environment loaded none of
them, so 38 of LinearSolve's 39 extensions were silently unchecked (only
LinearSolveSparseArraysExt was covered, because SparseArrays is a hard dep).

Add the resolvable weakdeps to test/qa/Project.toml and load them in
test/qa/qa.jl, taking coverage from 1/39 to 26/39. That includes the GPU
backends: ExplicitImports needs the extension module to exist, not a device, and
AMDGPU, cuSOLVER, CUDSS and Metal all precompile and import fine on a CPU-only
host (Metal logs "only supported on Apple Silicon" at init and still loads).
The remaining 13 need real system installs, a newer Julia than the LTS that QA
also runs on, or cannot co-resolve; qa.jl lists them and why.
`Base.get_extension` returning `nothing` makes ExplicitImports skip an extension
silently, so a testset asserts the 26 modules actually exist rather than
trusting a green run_qa.

CUSOLVERRF is the one GPU weakdep left out: it loads on its own, but its 0.2.6
pin on CUDA 5.7-5.11 is unsatisfiable alongside CUDSS 0.7, which needs CUDACore
6. CUDSS is kept.

Fixes for what the new coverage found, in the extension sources:

* `using Foo` + `Foo.bar` replaced with explicit imports in 21 extensions.
* LinearSolveCliqueTreesExt imported cholesky!/ldiv! from CliqueTrees.Multifrontal
  rather than their owner LinearAlgebra.
* LinearSolveArpackExt / ArnoldiMethodExt / KrylovKitExt / JacobiDavidsonExt
  reached build_eigenvalue_solution through LinearSolve instead of SciMLBase.
* LinearSolveFastLapackInterfaceExt used LinearSolve.@set! and
  LinearSolve.ArrayInterface; LinearSolveRecursiveFactorizationExt used
  LinearSolve.ArrayInterface and RecursiveFactorization.ldiv! (owner
  TriangularSolve); LinearSolveForwardDiffExt used LinearSolve.@concrete.
* LinearSolveBLISExt imported require_one_based_indexing via LinearAlgebra.LAPACK
  rather than Base.
* Stale imports dropped from the Mooncake, ChainRulesCore, ForwardDiff,
  RecursiveFactorization, Enzyme, CUDA, CUDSS, AMDGPU and Metal extensions.
* LinearSolveMooncakeExt referenced OperatorAssumptions and defaultalg without
  importing them, so its `_alg::CoDual{Nothing}` rrule!! would have thrown
  UndefVarError.

Names with no public spelling at their owner are ignored with a comment naming
the owner: Mooncake's rule interface, EnzymeCore.EnzymeRules, ForwardDiff's
Dual/Partials, ArnoldiMethod's target singletons, Sparspak's sparspaklu, the
CommonSolve names reached through SciMLBase, and LinearSolve's own internals
(issue SciML#1058). The CUDA stack's CUDACore/cuSPARSE aliases are ignored rather
than made extra triggers, since that would stop the extension loading for a
plain `using cuSOLVER`. The @eval-generated init_cacheval imports in the banded
extensions are ignored for staleness since EI cannot see macro-generated uses.

qa.jl now runs after jet.jl and the allocation tests: package loading is
process-global and several extensions change algorithm selection
(LinearSolveBLISExt makes BLIS the default LU), which perturbed those
assertions. That reordering also activates run_qa's own JET pass, which caught a
real bug: the fallback branch of the `defaultalg_adjoint_eval` generator emitted
`error("... $(alg) ...")` inside a `quote`, leaving a reference to a nonexistent
runtime binding `alg` instead of interpolating the algorithm name.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the title Put the extensions under QA's ExplicitImports checks (1/39 -> 22/39) Put the extensions under QA's ExplicitImports checks (1/39 -> 26/39) Aug 1, 2026
@ChrisRackauckas
ChrisRackauckas marked this pull request as ready for review August 1, 2026 14:51
@ChrisRackauckas
ChrisRackauckas merged commit a507d85 into SciML:main Aug 1, 2026
59 of 62 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants