From 15775f136e5ec9e1c27770e661225dcc1ea28b15 Mon Sep 17 00:00:00 2001 From: Michel Schanen Date: Tue, 6 Oct 2026 12:45:22 -0500 Subject: [PATCH 1/2] =?UTF-8?q?CI:=20parallel=20test=20runner,=20CPU=20job?= =?UTF-8?q?=20on=20kkt,=20LDL=E1=B5=80=20compiled=20only=20when=20used?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI jobs went from ~21 to ~52 min with the regime-B/C LDLᵀ kernels (#87-#89). Almost all of it is compilation: on the CPU backend the KA kernels compile with their caller, and the solver's numeric phase called factorize!, which branches on the structure at run time and so compiled the LDLᵀ path (all its kernels) for every Cholesky solver as well. test_api (SPD/HPD only, 4 T x 2 INT) went from 658 s to 2416 s. * src/solver.jl: the numeric phase dispatches on the structure behind an inference barrier that takes the mutable solver handle only (no boxing, so the numeric-phase allocation budgets hold); factorize_cholesky! is the Cholesky path of factorize!, which keeps its static branch. First solver factorization per (T, INT) on the CPU backend: 42-84 s -> 20-36 s. * test/runtests.jl: ParallelTestRunner (as CUDA.jl and oneAPI.jl); every test_*.jl runs in its own module on a worker pool with the shared helpers as init_code. The seed 666 goes with each test file since the runner reseeds with 1 after init_code. SDS_TEST_ONLY/SDS_TEST_SKIP still work. * ci.yml: the CPU-only job runs on the self-hosted kkt machine (same check name, it is required by the ruleset); PTR_NUM_JOBS caps the workers (8 CPU, 4 GPU) since the four kkt runners share the machine. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QSF5MG6xxpF2D1TCFkb3UZ --- .github/workflows/ci.yml | 14 ++++++-- AGENTS.md | 18 ++++++++-- src/numeric/factorize.jl | 14 ++++++-- src/solver.jl | 30 +++++++++++++--- test/Project.toml | 2 ++ test/backends.jl | 10 +++++- test/runtests.jl | 78 +++++++++++++++++++++++----------------- 7 files changed, 121 insertions(+), 45 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0a4fa3..998c200 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,16 @@ on: - cron: '0 0 * * 0' jobs: + # Runs on the self-hosted kkt machine (more cores than a GitHub runner for the parallel test workers). The + # job id and the matrix stay as they were: the check name "test-github-cpuonly (ubuntu-latest, 1, x64)" is a + # required check of the branch ruleset. test-github-cpuonly: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SDS_TEST_GPU: '0' - runs-on: ${{ matrix.os }} + # ParallelTestRunner workers (test/runtests.jl); the four kkt runners share the machine + PTR_NUM_JOBS: '8' + runs-on: [self-hosted, linux, X64, kkt] strategy: fail-fast: false matrix: @@ -28,7 +33,8 @@ jobs: - uses: julia-actions/setup-julia@latest with: version: ${{ matrix.julia-version }} - - uses: julia-actions/cache@v2 + - name: Machine + run: nproc && free -g - uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-runtest@latest # the GPU jobs skip the CPU backend, so its coverage comes from here @@ -45,6 +51,8 @@ jobs: # CPU-only jobs; testing them again here doubled the GPU jobs' time. SDS_TEST_CPU: '0' SDS_TEST_SKIP: 'test_aqua' + # ParallelTestRunner workers, each with its own CUDA context on the runner's GPU + PTR_NUM_JOBS: '4' strategy: fail-fast: false matrix: @@ -62,6 +70,8 @@ jobs: with: version: ${{ matrix.julia-version }} arch: ${{ matrix.julia-arch }} + - name: Machine + run: nproc && free -g && (nvidia-smi -L || true) - name: Add CUDA.jl to the test environment if: matrix.backend == 'cuda' run: | diff --git a/AGENTS.md b/AGENTS.md index 75da653..252906f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,9 +125,9 @@ reviewing agent must pass. The moving parts: * Linux (WSL2). One NVIDIA RTX 4080 (16 GB); CUDA.jl is functional. * **No AMDGPU, oneAPI or Metal hardware here.** Local tests run on the KernelAbstractions CPU backend and on CUDA only. GitHub CI - (`.github/workflows/ci.yml`, modelled on `../ExaPF.jl`) runs the CPU suite on - GitHub runners and the GPU suite on the exanauts self-hosted runners labelled - `cuda` and `amdgpu`. oneAPI and Metal extension files are written by analogy + (`.github/workflows/ci.yml`, modelled on `../ExaPF.jl`) runs the CPU suite and + the GPU suite on the exanauts self-hosted runners (CPU suite: the `kkt` + machine; GPU suite: the runners labelled `cuda` and `amdgpu`). oneAPI and Metal extension files are written by analogy (task T23) and only precompile-checked. * CUDA.jl 6.x is split into packages: `CUDACore` (`CuArray`, `CUDABackend`), `cuSPARSE` (`CuSparseMatrixCSR`, `CuSparseMatrixCSC`), `cuBLAS`, `cuSOLVER`. @@ -151,6 +151,10 @@ SDS_TEST_CPU=0 julia --project=. -e 'using Pkg; Pkg.test()' SDS_TEST_ONLY="test_symbolic_etree,test_options" julia --project=. -e 'using Pkg; Pkg.test()' SDS_TEST_SKIP="test_aqua" julia --project=. -e 'using Pkg; Pkg.test()' +# the same with ParallelTestRunner arguments (prefix match, `!` excludes), and the number of workers +julia --project=. -e 'using Pkg; Pkg.test(; test_args = ["test_symbolic", "!test_symbolic_etree"])' +PTR_NUM_JOBS=4 julia --project=. -e 'using Pkg; Pkg.test()' + # instantiate / update after editing Project.toml julia --project=. -e 'using Pkg; Pkg.instantiate()' @@ -170,6 +174,14 @@ CUDA jobs run with `SDS_TEST_CPU=0 SDS_TEST_SKIP=test_aqua`, since the CPU-only jobs already cover the CPU backend and Aqua. A task's "passes on CPU and CUDA" still means the default run (both backends) on the owner's machine. +The test files run in parallel through ParallelTestRunner.jl: each `test_*.jl` is +evaluated in its own module on a pool of worker processes, after the packages, +`test/utils.jl`, `test/matrices.jl`, `test/backends.jl` and `Random.seed!(666)` +(`init_code` in `test/runtests.jl`). A test file therefore cannot use definitions +from another test file; shared code belongs in the helpers. Balance matters: the +slowest file bounds the wall time, so split a test file that grows far beyond the +others. + ## Code conventions * Julia style: 4-space indentation, no tabs, `snake_case` functions, `CamelCase` diff --git a/src/numeric/factorize.jl b/src/numeric/factorize.jl index 3e1332e..5358a07 100644 --- a/src/numeric/factorize.jl +++ b/src/numeric/factorize.jl @@ -228,9 +228,19 @@ the factor is incomplete. Structure `"SPD"` (real) or `"HPD"`. Nothing is allocated on the device; the panels are bitwise reproducible for a fixed `impl` and backend (regimes A and B are deterministic by construction). """ -function factorize!(N::Numeric{T}, S::Symbolic, nzval::AbstractVector; impl::Symbol = :auto, - opts::Options = Options()) where {T} +function factorize!(N::Numeric, S::Symbolic, nzval::AbstractVector; impl::Symbol = :auto, opts::Options = Options()) _is_ldlt_structure(S.structure) && return factorize_ldlt!(N, S, nzval; impl, opts) + return factorize_cholesky!(N, S, nzval; impl, opts) +end + +""" + factorize_cholesky!(numeric, symbolic, nzval; impl = :auto, opts = Options()) -> info::Int + +The Cholesky path of [`factorize!`](@ref) (structures `"SPD"` and `"HPD"`), +callable without compiling the LDLᵀ/LDLᴴ path ([`factorize_ldlt!`](@ref)). +""" +function factorize_cholesky!(N::Numeric{T}, S::Symbolic, nzval::AbstractVector; impl::Symbol = :auto, + opts::Options = Options()) where {T} _check_numeric(N, S, nzval) p = _front_impls(N, S, impl) plan = N.plan diff --git a/src/solver.jl b/src/solver.jl index 38cd5a7..75bcc94 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -409,7 +409,7 @@ function _factorize!(solver::DirectSolver, p::Phase) end tic = time_ns() try - _numeric_phase!(N, S, solver.A.nzval, solver.options) + _numeric_phase!(solver) catch err if err isa InterruptedError # the panels are partly overwritten: back to "analyzed", a "factorization" must follow @@ -446,9 +446,31 @@ _info_value(solver::DirectSolver) = solver.nbatch == 1 ? solver.info[1] : copy(s _elapsed(tic) = string(round((time_ns() - tic) / 1.0e6; digits = 3), " ms") -# function barrier: concrete storage types for the numeric phase -_numeric_phase!(N::Numeric, S::Symbolic, nzval::AbstractVector, opts::Options) = factorize!(N, S, nzval; opts) -_numeric_phase!(N::Numeric, S::Symbolic, nzval::AbstractMatrix, opts::Options) = factorize!(N, S, vec(nzval); opts) +# The numeric phase of the structure, behind a barrier: the structure is a run-time value, and a static call +# would compile the Cholesky and the LDLᵀ/LDLᴴ paths with every solver, on the CPU backend all their kernels (most +# of the test suite's compile time). The barrier takes the mutable handle only: the dynamic call boxes nothing +# (an immutable `Numeric` or `Symbolic` argument would be copied to the heap on every call). +function _numeric_phase!(solver::DirectSolver) + if _is_ldlt_structure(solver.structure) + Base.inferencebarrier(_numeric_phase_ldlt!)(solver) + else + Base.inferencebarrier(_numeric_phase_cholesky!)(solver) + end + return nothing +end + +function _numeric_phase_cholesky!(solver::DirectSolver) + factorize_cholesky!(solver.numeric, solver.symbolic, _flat(solver.A.nzval); opts = solver.options) + return nothing +end + +function _numeric_phase_ldlt!(solver::DirectSolver) + factorize_ldlt!(solver.numeric, solver.symbolic, _flat(solver.A.nzval); opts = solver.options) + return nothing +end + +_flat(nzval::AbstractVector) = nzval +_flat(nzval::AbstractMatrix) = vec(nzval) # user data of a right-hand side / solution: (array, transposed); an `n × nrhs × nbatch` array is the # `n × (nrhs nbatch)` matrix with the same memory (uniform batch) diff --git a/test/Project.toml b/test/Project.toml index a0d9f1f..8fe74e3 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -6,6 +6,7 @@ KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" Krylov = "ba0b0d4f-ebba-5204-a429-3ac8c609bfb7" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Metis = "2679e427-3c69-5b7f-982b-ece356f1e94b" +ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" @@ -20,6 +21,7 @@ KernelAbstractions = "0.9" Krylov = "0.10" LinearAlgebra = "1.13" Metis = "1" +ParallelTestRunner = "2" Printf = "1.13" Random = "1.13" SparseArrays = "1.13" diff --git a/test/backends.jl b/test/backends.jl index b568867..516c37b 100644 --- a/test/backends.jl +++ b/test/backends.jl @@ -172,7 +172,14 @@ if AMDGPU_LOADED (n, n)) end -let gpus = String[] +""" + print_backends() + +Print the backends under test and why GPU backends are missing (once per run, by +`runtests.jl`). +""" +function print_backends() + gpus = String[] CUDA_FUNCTIONAL && push!(gpus, "CUDA ($(CUDA.name(CUDA.device())))") AMDGPU_FUNCTIONAL && push!(gpus, "ROCm ($(AMDGPU.device()))") CUDA_LOADED && !CUDA_FUNCTIONAL && push!(gpus, "CUDA installed but not functional: skipped") @@ -181,6 +188,7 @@ let gpus = String[] TEST_CPU || push!(gpus, "CPU backend disabled by SDS_TEST_CPU=0") println("Backends under test: ", join(backend_name.(BACKENDS), ", "), isempty(gpus) ? "" : " [" * join(gpus, "; ") * "]") + return nothing end """ diff --git a/test/runtests.jl b/test/runtests.jl index ba60b4c..00abd83 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,26 +1,17 @@ -# Test driver. Every `test_*.jl` file in this directory is a test file; run a -# subset with SDS_TEST_ONLY="test_options,test_aqua", leave files out with -# SDS_TEST_SKIP="test_aqua", skip the GPU backends with SDS_TEST_GPU=0 and the -# CPU backend with SDS_TEST_CPU=0 (see backends.jl). - -using Test -using Random -using LinearAlgebra -using SparseArrays -using Aqua -using KernelAbstractions -using SparseDirectSolver -using Metis # activates SparseDirectSolverMetisExt (nested-dissection orderings, T05) -using Krylov # activates SparseDirectSolverKrylovExt (ir_mode = "fgmres", T18) +# Test driver on ParallelTestRunner: every `test_*.jl` file in this directory is a test file and runs in its own +# sandbox module on a pool of worker processes (one compilation of the solver per worker, the files in parallel). +# +# Run a subset with SDS_TEST_ONLY="test_options,test_aqua" or with test arguments (prefix match, `!name` excludes: +# `Pkg.test(; test_args = ["test_symbolic", "!test_symbolic_etree"])`), leave files out with SDS_TEST_SKIP="test_aqua", +# skip the GPU backends with SDS_TEST_GPU=0 and the CPU backend with SDS_TEST_CPU=0 (see backends.jl). The number of +# workers is ParallelTestRunner's default (CPU threads and free memory); set it with `--jobs=N` or PTR_NUM_JOBS. -const SDS = SparseDirectSolver +using ParallelTestRunner +using SparseDirectSolver -include("utils.jl") -include("matrices.jl") -include("backends.jl") +const TEST_DIR = @__DIR__ -const TEST_FILES = sort!([first(splitext(f)) for f in readdir(@__DIR__) - if startswith(f, "test_") && endswith(f, ".jl")]) +const TEST_FILES = sort!([first(splitext(f)) for f in readdir(TEST_DIR) if startswith(f, "test_") && endswith(f, ".jl")]) function test_file_list(var::String) names = [first(splitext(strip(s))) for s in split(get(ENV, var, ""), ',') if !isempty(strip(s))] @@ -30,19 +21,40 @@ function test_file_list(var::String) return names end -function selected_test_files() - only = test_file_list("SDS_TEST_ONLY") - skip = test_file_list("SDS_TEST_SKIP") - return setdiff(isempty(only) ? TEST_FILES : only, skip) +# what every test file sees: the packages and the shared helpers (TASKS.md "Shared test conventions") +const init_code = quote + using Test + using Random + using LinearAlgebra + using SparseArrays + using Aqua + using KernelAbstractions + using SparseDirectSolver + using Metis # activates SparseDirectSolverMetisExt (nested-dissection orderings, T05) + using Krylov # activates SparseDirectSolverKrylovExt (ir_mode = "fgmres", T18) + const SDS = SparseDirectSolver + include($(joinpath(TEST_DIR, "utils.jl"))) + include($(joinpath(TEST_DIR, "matrices.jl"))) + include($(joinpath(TEST_DIR, "backends.jl"))) end -@testset "SparseDirectSolver.jl" begin - for name in selected_test_files() - @testset "$name" begin - Random.seed!(666) - tic = time() - include("$name.jl") - println("$name: $(round(time() - tic; digits = 1)) s") - end - end +# the seed goes with the file, not into `init_code`: ParallelTestRunner seeds with 1 after `init_code` +testsuite = Dict(name => quote + Random.seed!(666) + include($(joinpath(TEST_DIR, "$name.jl"))) + end for name in TEST_FILES) + +args = parse_args(ARGS) +if filter_tests!(testsuite, args) + only, skip = test_file_list("SDS_TEST_ONLY"), test_file_list("SDS_TEST_SKIP") + isempty(only) || filter!(t -> first(t) in only, testsuite) + filter!(t -> !(first(t) in skip), testsuite) end + +# the backend banner once, from the main process (also fails early when no backend is left to test) +module BackendBanner end +Core.eval(BackendBanner, init_code) +BackendBanner.print_backends() + +runtests(SparseDirectSolver, args; testsuite, init_code, + history_key = get(ENV, "SDS_TEST_CPU", "1") == "0" ? "gpu" : nothing) From ff5bf683585e9408691d753dec0fd23e56adf53d Mon Sep 17 00:00:00 2001 From: Michel Schanen Date: Tue, 6 Oct 2026 14:18:23 -0500 Subject: [PATCH 2/2] Tests: run the long test files once per element type A worker's cold compilation sets the wall time, and it grows with the element types a test file covers. The long files (SPLIT_FILES in test/runtests.jl) now run as four parts, test_api[Float32] etc., each in a worker of its own: ELTYPES/REAL_ELTYPES/COMPLEX_ELTYPES are the part's subset (test/utils.jl, set through Main.SDS_TEST_PART before the helpers are included), and the testsets that do not loop over the element types run in the Float64 part only (RUN_SHARED). Test counts are unchanged (CPU 62938, CUDA 69499, 1 broken). Owner's machine: CPU suite 13m52s -> 9m30s (16 workers), CUDA 16m23s -> 10m31s (8 workers); test_numeric_ldlt's slowest part 226 s against 814 s unsplit. ci.yml: 32 CPU workers and 12 CUDA workers on kkt. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QSF5MG6xxpF2D1TCFkb3UZ --- .github/workflows/ci.yml | 6 ++-- AGENTS.md | 11 +++++--- test/runtests.jl | 50 ++++++++++++++++++++++++++------- test/test_dense.jl | 10 ++++--- test/test_fgmres.jl | 9 +++--- test/test_numeric_cholesky_a.jl | 2 +- test/test_numeric_cholesky_b.jl | 2 +- test/test_numeric_cholesky_c.jl | 2 +- test/test_refinement.jl | 5 ++-- test/test_solve.jl | 2 +- test/utils.jl | 19 +++++++++++-- 11 files changed, 84 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 998c200..d1d1c3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,8 +18,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SDS_TEST_GPU: '0' - # ParallelTestRunner workers (test/runtests.jl); the four kkt runners share the machine - PTR_NUM_JOBS: '8' + # ParallelTestRunner workers (test/runtests.jl): kkt has 192 threads and 2 TB, shared by its four runners + PTR_NUM_JOBS: '32' runs-on: [self-hosted, linux, X64, kkt] strategy: fail-fast: false @@ -52,7 +52,7 @@ jobs: SDS_TEST_CPU: '0' SDS_TEST_SKIP: 'test_aqua' # ParallelTestRunner workers, each with its own CUDA context on the runner's GPU - PTR_NUM_JOBS: '4' + PTR_NUM_JOBS: '12' strategy: fail-fast: false matrix: diff --git a/AGENTS.md b/AGENTS.md index 252906f..fae6a6a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -177,10 +177,13 @@ still means the default run (both backends) on the owner's machine. The test files run in parallel through ParallelTestRunner.jl: each `test_*.jl` is evaluated in its own module on a pool of worker processes, after the packages, `test/utils.jl`, `test/matrices.jl`, `test/backends.jl` and `Random.seed!(666)` -(`init_code` in `test/runtests.jl`). A test file therefore cannot use definitions -from another test file; shared code belongs in the helpers. Balance matters: the -slowest file bounds the wall time, so split a test file that grows far beyond the -others. +(`test/runtests.jl`). A test file therefore cannot use definitions from another +test file; shared code belongs in the helpers. A worker's cold compilation, not the +tests, sets the wall time, so the long files (`SPLIT_FILES` in `test/runtests.jl`) +run once per element type (`test_api[Float64]`, …): `ELTYPES`, `REAL_ELTYPES` and +`COMPLEX_ELTYPES` are then that part's subset, and a testset that does not loop over +the element types runs in the `Float64` part only (`RUN_SHARED && @testset …`). In +a split file, write every testset either over `ELTYPES` or behind `RUN_SHARED`. ## Code conventions diff --git a/test/runtests.jl b/test/runtests.jl index 00abd83..cf4a777 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,6 @@ # Test driver on ParallelTestRunner: every `test_*.jl` file in this directory is a test file and runs in its own -# sandbox module on a pool of worker processes (one compilation of the solver per worker, the files in parallel). +# sandbox module on a pool of worker processes (one compilation of the solver per worker, the files in parallel); the +# long files run once per element type (`SPLIT_FILES`). # # Run a subset with SDS_TEST_ONLY="test_options,test_aqua" or with test arguments (prefix match, `!name` excludes: # `Pkg.test(; test_args = ["test_symbolic", "!test_symbolic_etree"])`), leave files out with SDS_TEST_SKIP="test_aqua", @@ -21,7 +22,7 @@ function test_file_list(var::String) return names end -# what every test file sees: the packages and the shared helpers (TASKS.md "Shared test conventions") +# what every test file sees: the packages (the shared helpers come with the test, see `test_expr`) const init_code = quote using Test using Random @@ -33,28 +34,57 @@ const init_code = quote using Metis # activates SparseDirectSolverMetisExt (nested-dissection orderings, T05) using Krylov # activates SparseDirectSolverKrylovExt (ir_mode = "fgmres", T18) const SDS = SparseDirectSolver +end + +# the element types of the running test (`test/utils.jl`), set by the test before the helpers are included +const init_worker_code = quote + const SDS_TEST_PART = Ref{Any}(nothing) +end + +# Long test files run once per element type, "test_api[Float64]" etc., each part in a worker of its own: a +# worker compiles the solver for every element type it tests, and that compilation, not the tests, sets the wall +# time. The testsets that do not loop over the element types run in the Float64 part (`RUN_SHARED`). +const SPLIT_FILES = ["test_api", "test_dense", "test_fgmres", "test_numeric_cholesky_a", "test_numeric_cholesky_b", + "test_numeric_cholesky_c", "test_numeric_ldlt", "test_ported", "test_refinement", "test_solve", + "test_ubatch"] +const SPLIT_ELTYPES = (Float32, Float64, ComplexF32, ComplexF64) + +# the seed goes with the test, not into `init_code`: ParallelTestRunner seeds with 1 after `init_code` +test_expr(name, part) = quote + Main.SDS_TEST_PART[] = $part include($(joinpath(TEST_DIR, "utils.jl"))) include($(joinpath(TEST_DIR, "matrices.jl"))) include($(joinpath(TEST_DIR, "backends.jl"))) + Random.seed!(666) + include($(joinpath(TEST_DIR, "$name.jl"))) +end + +testsuite = Dict{String, Expr}() +for name in TEST_FILES + if name in SPLIT_FILES + for T in SPLIT_ELTYPES + testsuite["$name[$T]"] = test_expr(name, ((T,), T == Float64)) + end + else + testsuite[name] = test_expr(name, nothing) + end end -# the seed goes with the file, not into `init_code`: ParallelTestRunner seeds with 1 after `init_code` -testsuite = Dict(name => quote - Random.seed!(666) - include($(joinpath(TEST_DIR, "$name.jl"))) - end for name in TEST_FILES) +# SDS_TEST_ONLY/SDS_TEST_SKIP name test files: a split file stands for all its parts +names_test(names, test) = any(n -> test == n || startswith(test, n * "["), names) args = parse_args(ARGS) if filter_tests!(testsuite, args) only, skip = test_file_list("SDS_TEST_ONLY"), test_file_list("SDS_TEST_SKIP") - isempty(only) || filter!(t -> first(t) in only, testsuite) - filter!(t -> !(first(t) in skip), testsuite) + isempty(only) || filter!(t -> names_test(only, first(t)), testsuite) + filter!(t -> !names_test(skip, first(t)), testsuite) end # the backend banner once, from the main process (also fails early when no backend is left to test) module BackendBanner end Core.eval(BackendBanner, init_code) +Core.eval(BackendBanner, :(include($(joinpath(TEST_DIR, "backends.jl"))))) BackendBanner.print_backends() -runtests(SparseDirectSolver, args; testsuite, init_code, +runtests(SparseDirectSolver, args; testsuite, init_code, init_worker_code, history_key = get(ENV, "SDS_TEST_CPU", "1") == "0" ? "gpu" : nothing) diff --git a/test/test_dense.jl b/test/test_dense.jl index 7d01bb5..2d6f451 100644 --- a/test/test_dense.jl +++ b/test/test_dense.jl @@ -11,9 +11,11 @@ dense_tri(A, uplo::Char, diag::Char) = dense_part(C, uplo::Char) = uplo == 'L' ? tril(C) : triu(C) @testset "capability audit ($(backend_name(backend)))" for backend in BACKENDS - SDS.print_capabilities(stdout, backend) - table = sprint(io -> SDS.print_capabilities(io, backend)) - @test occursin("generic_mul", table) && occursin("ComplexF64", table) + if RUN_SHARED # the table covers every element type + SDS.print_capabilities(stdout, backend) + table = sprint(io -> SDS.print_capabilities(io, backend)) + @test occursin("generic_mul", table) && occursin("ComplexF64", table) + end for T in ELTYPES caps = SDS.capabilities(backend, T) @test caps isa SDS.DenseCapabilities @@ -255,7 +257,7 @@ end @test to_host(C) ≈ to_host(A) * to_host(A) end -@testset "dense interface errors" begin +RUN_SHARED && @testset "dense interface errors" begin A = zeros(4, 4) @test_throws InvalidValueError SDS.gemm!(A, A, A; impl = :bogus) @test_throws InvalidValueError SDS.gemm!(A, A, A; transA = :X) diff --git a/test/test_fgmres.jl b/test/test_fgmres.jl index 65a1c87..569b172 100644 --- a/test/test_fgmres.jl +++ b/test/test_fgmres.jl @@ -7,8 +7,8 @@ const FGMRES_STALL_PARAMS = (("pivot_pairs", "none"), ("pivot_epsilon", 1.0)) fgmres_stall_matrix() = kkt_matrix(Float64, 150, 5, 0.0) -@testset "FGMRES-IR where plain IR stalls ($(backend_name(backend)), $INT)" for backend in BACKENDS, - INT in INTTYPES +RUN_SHARED && @testset "FGMRES-IR where plain IR stalls ($(backend_name(backend)), $INT)" for backend in BACKENDS, + INT in INTTYPES Random.seed!(666) K = fgmres_stall_matrix() b = K * rand(size(K, 1)) @@ -72,7 +72,8 @@ end end end -@testset "FGMRES-IR: uniform batch ($(backend_name(backend)), $T)" for backend in BACKENDS, T in (Float64, ComplexF32) +@testset "FGMRES-IR: uniform batch ($(backend_name(backend)), $T)" for backend in BACKENDS, + T in filter(in((Float64, ComplexF32)), ELTYPES) n, nb, nrhs = 120, 3, 2 members = batch_members(random_symindef(T, n, 0.03), nb) solver = DirectSolver(api_batch_matrix(backend, members, 'L'), sym_structure(T), 'L') @@ -95,7 +96,7 @@ end @test iszero(Xh[:, :, 1]) && iszero(Xh[:, :, 3]) end -@testset "FGMRES-IR: interrupt and missing Krylov ($(backend_name(backend)))" for backend in BACKENDS +RUN_SHARED && @testset "FGMRES-IR: interrupt and missing Krylov ($(backend_name(backend)))" for backend in BACKENDS Random.seed!(666) K = fgmres_stall_matrix() b = K * rand(size(K, 1)) diff --git a/test/test_numeric_cholesky_a.jl b/test/test_numeric_cholesky_a.jl index 4838365..d7186b7 100644 --- a/test/test_numeric_cholesky_a.jl +++ b/test/test_numeric_cholesky_a.jl @@ -30,7 +30,7 @@ function subtree_move_rounds(S, v) return cld(m * (m + 1) ÷ 2, d) end -@testset "plan: regime-A groups" begin +RUN_SHARED && @testset "plan: regime-A groups" begin for (name, A, opts) in numeric_a_matrices(Float64) S, _, _, _, Nd, _ = numeric_setup(CPU(), A; opts) sc, plan = S.schedule, Nd.plan diff --git a/test/test_numeric_cholesky_b.jl b/test/test_numeric_cholesky_b.jl index 4566d64..b9b3004 100644 --- a/test/test_numeric_cholesky_b.jl +++ b/test/test_numeric_cholesky_b.jl @@ -17,7 +17,7 @@ numeric_b_allocated(N, S, nz) = @allocated SDS.factorize!(N, S, nz) nregime(S, r) = count(==(r), S.schedule.regime) -@testset "plan: regime-B groups" begin +RUN_SHARED && @testset "plan: regime-B groups" begin for opts in (NUMERIC_B_OPTS, NUMERIC_BC_OPTS, Options(subtree_budgets = Int[], regime_c_width = 128)) A = laplacian3d(Float64, 10, 10, 10) S, _, _, _, Nd, _ = numeric_setup(CPU(), A; opts) diff --git a/test/test_numeric_cholesky_c.jl b/test/test_numeric_cholesky_c.jl index 22372c7..4e999fe 100644 --- a/test/test_numeric_cholesky_c.jl +++ b/test/test_numeric_cholesky_c.jl @@ -15,7 +15,7 @@ numeric_c_setup(backend, A, INT = Int32; opts = NUMERIC_C_OPTS, kw...) = numeric numeric_c_allocated(N, S, nz) = @allocated SDS.factorize!(N, S, nz) -@testset "storage and plan" begin +RUN_SHARED && @testset "storage and plan" begin A = laplacian2d(Float64, 20, 20) S, _, _, _, Nd, _ = numeric_c_setup(CPU(), A) @test Nd isa SDS.Numeric{Float64, Vector{Float64}, Vector{Int64}, Vector{Int32}} diff --git a/test/test_refinement.jl b/test/test_refinement.jl index a32f85e..6d05d4d 100644 --- a/test/test_refinement.jl +++ b/test/test_refinement.jl @@ -15,7 +15,7 @@ function Base.CoreLogging.handle_message(L::InterruptAtStepLogger, level, messag return nothing end -@testset "refinement on a badly scaled SPD matrix ($(backend_name(backend)))" for backend in BACKENDS +RUN_SHARED && @testset "refinement on a badly scaled SPD matrix ($(backend_name(backend)))" for backend in BACKENDS T = Float64 A = badly_scaled_spd(T, 300, 0.02) # rows scaled by up to 10^(±8) b = A * rand(T, 300) @@ -37,6 +37,7 @@ end @test getparam(solver.options, "ir_n_steps") == 10 end +RUN_SHARED && @testset "refinement with static pivot perturbation ($(backend_name(backend)), $INT)" for backend in BACKENDS, INT in INTTYPES # KKT matrix without 2×2 pairs: the zero (2,2) block is perturbed (pivot_epsilon), and refinement @@ -255,7 +256,7 @@ end @test getparam(DirectSolver(api_matrix(backend, tril(A), Int32), spd_structure(T), 'L'), "ir_n_steps") == 0 end -@testset "logging" begin +RUN_SHARED && @testset "logging" begin backend = first(BACKENDS) A = random_spd(Float64, 80, 0.05) b = rand(80) diff --git a/test/test_solve.jl b/test/test_solve.jl index efa605e..c2fc3c8 100644 --- a/test/test_solve.jl +++ b/test/test_solve.jl @@ -15,7 +15,7 @@ nregime(S, r) = count(==(r), S.schedule.regime) solve_allocated(x, ws, S, N, b; kwargs...) = @allocated SDS.sweep_solve!(x, ws, S, N, b; kwargs...) -@testset "solve plan and workspace" begin +RUN_SHARED && @testset "solve plan and workspace" begin for (name, A) in solve_matrices(Float64) C = SDS.CSR(tril(A)) S = SDS.symbolic_analysis(C, "SPD", 'L'; opts = SOLVE_OPTS) diff --git a/test/utils.jl b/test/utils.jl index 10d867f..feefe1f 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -4,9 +4,22 @@ using LinearAlgebra using SparseArrays -const ELTYPES = (Float32, Float64, ComplexF32, ComplexF64) -const REAL_ELTYPES = (Float32, Float64) -const COMPLEX_ELTYPES = (ComplexF32, ComplexF64) +# The element types under test. `runtests.jl` runs a long test file once per element type, each part in its own +# worker: the part sets `Main.SDS_TEST_PART[] = (eltypes, shared)` before these helpers are included (`nothing`: +# all element types, one part). +const TEST_PART = isdefined(Main, :SDS_TEST_PART) ? Main.SDS_TEST_PART[] : nothing +const ELTYPES = TEST_PART === nothing ? (Float32, Float64, ComplexF32, ComplexF64) : TEST_PART[1] +const REAL_ELTYPES = filter(T -> T <: Real, ELTYPES) +const COMPLEX_ELTYPES = filter(T -> T <: Complex, ELTYPES) + +""" + RUN_SHARED + +`true` when this run of a test file runs the testsets that do not loop over the +element types (`RUN_SHARED && @testset …`): in the one part of a test file split +by element type that owns them, and always in an unsplit test file. +""" +const RUN_SHARED = TEST_PART === nothing || TEST_PART[2] const INTTYPES = (Int32, Int64) """