From 49122e126574ce42d1b1e9b3a7421a2eb8ac7a0b Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Wed, 7 Oct 2026 17:39:10 +0200 Subject: [PATCH 01/12] Add max_default_jobs to bound the default number of jobs The default job count only accounts for CPU threads and host memory. Callers whose workers share another limited resource, such as GPU memory, had no way to express it short of forcing the count with --jobs, which bypasses the memory check, or rebuilding ParsedArgs. max_default_jobs caps the automatic default and, like memory_per_worker, is ignored when --jobs or PTR_NUM_JOBS is set. --- docs/src/advanced.md | 8 +++++++- src/ParallelTestRunner.jl | 19 +++++++++++++++++-- test/core.jl | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/docs/src/advanced.md b/docs/src/advanced.md index 3ad324eb..dbfb686c 100644 --- a/docs/src/advanced.md +++ b/docs/src/advanced.md @@ -238,7 +238,13 @@ runtests(MyPackage, ARGS; memory_per_worker = 2^30) # 1 GiB per worker Workers are recycled once they exceed the `JULIA_TEST_MAXRSS_MB` environment variable (or the `max_worker_rss` keyword). Lowering it keeps memory pressure in check, which can make it possible to run more workers than would otherwise be safe. -The number of jobs can also be forced with `--jobs=N` or the `PTR_NUM_JOBS` environment variable, but this bypasses the memory check entirely. +Resources that PTR does not know about, such as the memory of a GPU shared by all workers, can be accounted for by passing an upper bound on the default with `max_default_jobs`: + +```julia +runtests(MyPackage, ARGS; max_default_jobs = gpu_free ÷ gpu_memory_per_worker) +``` + +The number of jobs can also be forced with `--jobs=N` or the `PTR_NUM_JOBS` environment variable, but this bypasses the memory check and `max_default_jobs` entirely. More jobs is not always faster: once the machine runs out of memory the whole suite slows down, sometimes enough to time out a CI job. This has been especially problematic on low-memory macOS machines (notably CI runners), where memory compression makes each garbage collection slower. diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 4f6105ca..63b4eefb 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -56,7 +56,8 @@ include("execute.jl") stdout = Base.stdout, stderr = Base.stderr, max_worker_rss = get_max_worker_rss(), - memory_per_worker = 2 * 2^30) + memory_per_worker = 2 * 2^30, + max_default_jobs = nothing, serial = String[], serial_position::Symbol = :before, recycle_on_failure::Bool = false, @@ -111,6 +112,10 @@ Several keyword arguments are also supported: Packages whose tests use less memory can pass a smaller value to increase the default parallelism. Ignored when the number of jobs is set explicitly via `--jobs=N` or the `PTR_NUM_JOBS` environment variable. +- `max_default_jobs`: Upper bound on the default number of jobs, for resources that PTR + does not account for itself, such as the memory of a GPU shared between workers + (default: `nothing`, no bound). Like `memory_per_worker`, it is ignored when the + number of jobs is set explicitly via `--jobs=N` or the `PTR_NUM_JOBS` environment variable. - `serial`: A vector of test names (keys of `testsuite`) that should be run one at a time instead of in parallel. - `serial_position`: When to run serial tests relative to the parallel batch. @@ -233,6 +238,7 @@ function runtests(mod::Module, args::ParsedArgs; stderr = Base.stderr, max_worker_rss = get_max_worker_rss(), memory_per_worker = DEFAULT_MEMORY_PER_WORKER, + max_default_jobs = nothing, recycle_on_failure::Bool = false, retries::Integer = 0, history_flush_every::Integer = 20, @@ -301,6 +307,7 @@ function runtests(mod::Module, args::ParsedArgs; stderr, max_worker_rss, memory_per_worker, + max_default_jobs, recycle_on_failure, retries, history_flush_every, @@ -334,6 +341,7 @@ function _runtests(mod::Module, args::ParsedArgs; stderr = Base.stderr, max_worker_rss = get_max_worker_rss(), memory_per_worker = DEFAULT_MEMORY_PER_WORKER, + max_default_jobs = nothing, recycle_on_failure::Bool = false, retries::Integer = 0, history_flush_every::Integer = 20, @@ -344,7 +352,14 @@ function _runtests(mod::Module, args::ParsedArgs; # determine parallelism env_jobs = tryparse(Int, get(ENV, "PTR_NUM_JOBS", "")) - _jobs = @something args.jobs env_jobs default_njobs(; memory_per_worker) + if max_default_jobs !== nothing + max_default_jobs isa Integer && !(max_default_jobs isa Bool) && max_default_jobs >= 1 || + throw(ArgumentError("`max_default_jobs` must be a positive integer, got $(repr(max_default_jobs))")) + end + _jobs = @something args.jobs env_jobs begin + default_jobs = default_njobs(; memory_per_worker) + max_default_jobs === nothing ? default_jobs : min(default_jobs, max_default_jobs) + end jobs = clamp(_jobs, 1, max(1, length(parallel_tests))) worker_pool = Channel{Union{Nothing, PTRWorker}}(jobs) for _ in 1:jobs diff --git a/test/core.jl b/test/core.jl index aba37ef0..588ae5ec 100644 --- a/test/core.jl +++ b/test/core.jl @@ -69,6 +69,40 @@ end runtests(ParallelTestRunner, ["--jobs=1"]; testsuite, stdout=io, stderr=io) end @test contains(String(take!(io)), "using 1 parallel jobs") + + # max_default_jobs bounds the default, but not explicit settings + if ParallelTestRunner.default_njobs() >= 2 + io = IOBuffer() + withenv("PTR_NUM_JOBS" => nothing) do + runtests(ParallelTestRunner, String[]; testsuite, stdout=io, stderr=io) + end + @test !contains(String(take!(io)), "using 1 parallel jobs") + end + io = IOBuffer() + withenv("PTR_NUM_JOBS" => nothing) do + runtests(ParallelTestRunner, String[]; testsuite, stdout=io, stderr=io, + max_default_jobs=1) + end + @test contains(String(take!(io)), "using 1 parallel jobs") + + io = IOBuffer() + withenv("PTR_NUM_JOBS" => nothing) do + runtests(ParallelTestRunner, ["--jobs=2"]; testsuite, stdout=io, stderr=io, + max_default_jobs=1) + end + @test contains(String(take!(io)), "using 2 parallel jobs") + + io = IOBuffer() + withenv("PTR_NUM_JOBS" => "2") do + runtests(ParallelTestRunner, String[]; testsuite, stdout=io, stderr=io, + max_default_jobs=1) + end + @test contains(String(take!(io)), "using 2 parallel jobs") + + for bad in (0, -1, true, 1.5, "2") + @test_throws ArgumentError runtests(ParallelTestRunner, String[]; testsuite, + stdout=io, stderr=io, max_default_jobs=bad) + end end @testset "subdir use" begin From abb5d71b763c6bf194f0fcd115a56658d80e7e25 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Wed, 7 Oct 2026 17:50:26 +0200 Subject: [PATCH 02/12] Bump version to 2.10.0 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index cbb40373..0de96714 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ParallelTestRunner" uuid = "d3525ed8-44d0-4b2c-a655-542cee43accc" -version = "2.9.4" +version = "2.10.0" authors = ["Valentin Churavy "] [deps] From 5feac825e5baffb68aa3a3c4f052c74da88392f6 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:25:46 -0300 Subject: [PATCH 03/12] Move `max_default_jobs` to `default_njobs` --- src/ParallelTestRunner.jl | 16 +++++----------- src/resources.jl | 7 ++++++- test/core.jl | 6 +++++- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 63b4eefb..5283475e 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -57,7 +57,7 @@ include("execute.jl") stderr = Base.stderr, max_worker_rss = get_max_worker_rss(), memory_per_worker = 2 * 2^30, - max_default_jobs = nothing, + max_default_jobs = typemax(Int64), serial = String[], serial_position::Symbol = :before, recycle_on_failure::Bool = false, @@ -238,7 +238,7 @@ function runtests(mod::Module, args::ParsedArgs; stderr = Base.stderr, max_worker_rss = get_max_worker_rss(), memory_per_worker = DEFAULT_MEMORY_PER_WORKER, - max_default_jobs = nothing, + max_default_jobs = DEFAULT_MAX_DEFAULT_JOBS, recycle_on_failure::Bool = false, retries::Integer = 0, history_flush_every::Integer = 20, @@ -341,7 +341,7 @@ function _runtests(mod::Module, args::ParsedArgs; stderr = Base.stderr, max_worker_rss = get_max_worker_rss(), memory_per_worker = DEFAULT_MEMORY_PER_WORKER, - max_default_jobs = nothing, + max_default_jobs = DEFAULT_MAX_DEFAULT_JOBS, recycle_on_failure::Bool = false, retries::Integer = 0, history_flush_every::Integer = 20, @@ -352,14 +352,8 @@ function _runtests(mod::Module, args::ParsedArgs; # determine parallelism env_jobs = tryparse(Int, get(ENV, "PTR_NUM_JOBS", "")) - if max_default_jobs !== nothing - max_default_jobs isa Integer && !(max_default_jobs isa Bool) && max_default_jobs >= 1 || - throw(ArgumentError("`max_default_jobs` must be a positive integer, got $(repr(max_default_jobs))")) - end - _jobs = @something args.jobs env_jobs begin - default_jobs = default_njobs(; memory_per_worker) - max_default_jobs === nothing ? default_jobs : min(default_jobs, max_default_jobs) - end + + _jobs = _jobs = @something args.jobs env_jobs default_njobs(; memory_per_worker, max_default_jobs) jobs = clamp(_jobs, 1, max(1, length(parallel_tests))) worker_pool = Channel{Union{Nothing, PTRWorker}}(jobs) for _ in 1:jobs diff --git a/src/resources.jl b/src/resources.jl index f309531a..177c513e 100644 --- a/src/resources.jl +++ b/src/resources.jl @@ -35,6 +35,7 @@ end # memory). Packages whose tests are heavier can pass a larger # `memory_per_worker` to `runtests`. const DEFAULT_MEMORY_PER_WORKER = 2 * Int64(2)^30 +const DEFAULT_MAX_DEFAULT_JOBS = typemax(Int64) """ default_njobs(; memory_per_worker = 2 * 2^30) @@ -45,10 +46,14 @@ bytes of the available system memory. """ function default_njobs(; memory_per_worker = DEFAULT_MEMORY_PER_WORKER, + max_default_jobs::Integer = DEFAULT_MAX_DEFAULT_JOBS, # Just use Sys.EFFECTIVE_CPU_THREADS when min VERSION >= v"1.13" _cpu_threads = (@static isdefined(Sys, :EFFECTIVE_CPU_THREADS) ? Sys.EFFECTIVE_CPU_THREADS : Sys.CPU_THREADS), _free_memory = available_memory(), ) + !(max_default_jobs isa Bool) && max_default_jobs >= 1 || + throw(ArgumentError("`max_default_jobs` must be a positive integer, got $(repr(max_default_jobs))")) + memory_jobs = Int64(_free_memory) ÷ memory_per_worker - return max(1, min(_cpu_threads, memory_jobs)) + return max(1, min(_cpu_threads, memory_jobs, max_default_jobs)) end diff --git a/test/core.jl b/test/core.jl index 588ae5ec..5e28a136 100644 --- a/test/core.jl +++ b/test/core.jl @@ -99,8 +99,12 @@ end end @test contains(String(take!(io)), "using 2 parallel jobs") - for bad in (0, -1, true, 1.5, "2") + for badint in (0, -1, true) @test_throws ArgumentError runtests(ParallelTestRunner, String[]; testsuite, + stdout=io, stderr=io, max_default_jobs=badint) + end + for bad in (1.5, "2") + @test_throws TypeError runtests(ParallelTestRunner, String[]; testsuite, stdout=io, stderr=io, max_default_jobs=bad) end end From cecc3b266b92a5e487a230302639549d69b1c997 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:10:57 -0300 Subject: [PATCH 04/12] More consistent error checkind and tests --- src/resources.jl | 6 ++++-- test/core.jl | 26 +++++++++++++++++--------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/resources.jl b/src/resources.jl index 177c513e..fa9d90a3 100644 --- a/src/resources.jl +++ b/src/resources.jl @@ -45,14 +45,16 @@ number of CPU threads, clamped such that each worker can be assumed to use `memo bytes of the available system memory. """ function default_njobs(; - memory_per_worker = DEFAULT_MEMORY_PER_WORKER, + memory_per_worker::Integer = DEFAULT_MEMORY_PER_WORKER, max_default_jobs::Integer = DEFAULT_MAX_DEFAULT_JOBS, # Just use Sys.EFFECTIVE_CPU_THREADS when min VERSION >= v"1.13" _cpu_threads = (@static isdefined(Sys, :EFFECTIVE_CPU_THREADS) ? Sys.EFFECTIVE_CPU_THREADS : Sys.CPU_THREADS), _free_memory = available_memory(), ) - !(max_default_jobs isa Bool) && max_default_jobs >= 1 || + max_default_jobs >= 1 || throw(ArgumentError("`max_default_jobs` must be a positive integer, got $(repr(max_default_jobs))")) + memory_per_worker >= 1 || + throw(ArgumentError("`memory_per_worker` must be a positive integer, got $(repr(memory_per_worker))")) memory_jobs = Int64(_free_memory) ÷ memory_per_worker return max(1, min(_cpu_threads, memory_jobs, max_default_jobs)) diff --git a/test/core.jl b/test/core.jl index 5e28a136..03375580 100644 --- a/test/core.jl +++ b/test/core.jl @@ -47,6 +47,23 @@ end # lighter per-worker memory estimate increases the default @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 32, memory_per_worker=1 * Int64(2) ^ 30) == 4 + + # max_num_jobs respected even when other parameters would select more + @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 32, + memory_per_worker=1 * Int64(2) ^ 30, + max_default_jobs=1) == 1 + @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 32, + memory_per_worker=1 * Int64(2) ^ 30, + max_default_jobs=2) == 2 + + for badint in (0, -1) + @test_throws ArgumentError ParallelTestRunner.default_njobs(; memory_per_worker=badint) + @test_throws ArgumentError ParallelTestRunner.default_njobs(; max_default_jobs=badint) + end + for bad in (1.5, "2") + @test_throws TypeError ParallelTestRunner.default_njobs(; memory_per_worker=bad) + @test_throws TypeError ParallelTestRunner.default_njobs(; max_default_jobs=bad) + end end @testset "number of jobs" begin @@ -98,15 +115,6 @@ end max_default_jobs=1) end @test contains(String(take!(io)), "using 2 parallel jobs") - - for badint in (0, -1, true) - @test_throws ArgumentError runtests(ParallelTestRunner, String[]; testsuite, - stdout=io, stderr=io, max_default_jobs=badint) - end - for bad in (1.5, "2") - @test_throws TypeError runtests(ParallelTestRunner, String[]; testsuite, - stdout=io, stderr=io, max_default_jobs=bad) - end end @testset "subdir use" begin From e0535afbd75b40e40cafcd8873da0938b848dd83 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:22:04 -0300 Subject: [PATCH 05/12] Typo --- src/ParallelTestRunner.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 5283475e..e51c30ea 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -353,7 +353,7 @@ function _runtests(mod::Module, args::ParsedArgs; # determine parallelism env_jobs = tryparse(Int, get(ENV, "PTR_NUM_JOBS", "")) - _jobs = _jobs = @something args.jobs env_jobs default_njobs(; memory_per_worker, max_default_jobs) + _jobs = @something args.jobs env_jobs default_njobs(; memory_per_worker, max_default_jobs) jobs = clamp(_jobs, 1, max(1, length(parallel_tests))) worker_pool = Channel{Union{Nothing, PTRWorker}}(jobs) for _ in 1:jobs From 329f42b347b0ad514ad3fb8ac4316d1c2aff5664 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:43:04 -0300 Subject: [PATCH 06/12] Docstring tweaks --- src/ParallelTestRunner.jl | 2 +- src/resources.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index e51c30ea..7541e260 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -114,7 +114,7 @@ Several keyword arguments are also supported: `PTR_NUM_JOBS` environment variable. - `max_default_jobs`: Upper bound on the default number of jobs, for resources that PTR does not account for itself, such as the memory of a GPU shared between workers - (default: `nothing`, no bound). Like `memory_per_worker`, it is ignored when the + (default: no bound). Like `memory_per_worker`, it is ignored when the number of jobs is set explicitly via `--jobs=N` or the `PTR_NUM_JOBS` environment variable. - `serial`: A vector of test names (keys of `testsuite`) that should be run one at a time instead of in parallel. diff --git a/src/resources.jl b/src/resources.jl index fa9d90a3..9b0b70fc 100644 --- a/src/resources.jl +++ b/src/resources.jl @@ -38,11 +38,11 @@ const DEFAULT_MEMORY_PER_WORKER = 2 * Int64(2)^30 const DEFAULT_MAX_DEFAULT_JOBS = typemax(Int64) """ - default_njobs(; memory_per_worker = 2 * 2^30) + default_njobs(; memory_per_worker = 2 * 2^30, [max_default_jobs]) A function used to determine the default number of parallel jobs. Calculated as the number of CPU threads, clamped such that each worker can be assumed to use `memory_per_worker` -bytes of the available system memory. +bytes of the available system memory, and, if specified, further bounded by `max_default_jobs`. """ function default_njobs(; memory_per_worker::Integer = DEFAULT_MEMORY_PER_WORKER, From 2e8ea596dd5dcdf3fccf2dcd3600e6621de38fc3 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:07:54 -0300 Subject: [PATCH 07/12] Remove unnecessary restriction --- src/resources.jl | 4 ++-- test/core.jl | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/resources.jl b/src/resources.jl index 9b0b70fc..ba35b049 100644 --- a/src/resources.jl +++ b/src/resources.jl @@ -45,7 +45,7 @@ number of CPU threads, clamped such that each worker can be assumed to use `memo bytes of the available system memory, and, if specified, further bounded by `max_default_jobs`. """ function default_njobs(; - memory_per_worker::Integer = DEFAULT_MEMORY_PER_WORKER, + memory_per_worker::Real = DEFAULT_MEMORY_PER_WORKER, max_default_jobs::Integer = DEFAULT_MAX_DEFAULT_JOBS, # Just use Sys.EFFECTIVE_CPU_THREADS when min VERSION >= v"1.13" _cpu_threads = (@static isdefined(Sys, :EFFECTIVE_CPU_THREADS) ? Sys.EFFECTIVE_CPU_THREADS : Sys.CPU_THREADS), @@ -54,7 +54,7 @@ function default_njobs(; max_default_jobs >= 1 || throw(ArgumentError("`max_default_jobs` must be a positive integer, got $(repr(max_default_jobs))")) memory_per_worker >= 1 || - throw(ArgumentError("`memory_per_worker` must be a positive integer, got $(repr(memory_per_worker))")) + throw(ArgumentError("`memory_per_worker` must be >= 1, got $(repr(memory_per_worker))")) memory_jobs = Int64(_free_memory) ÷ memory_per_worker return max(1, min(_cpu_threads, memory_jobs, max_default_jobs)) diff --git a/test/core.jl b/test/core.jl index 03375580..bd2bba77 100644 --- a/test/core.jl +++ b/test/core.jl @@ -56,14 +56,14 @@ end memory_per_worker=1 * Int64(2) ^ 30, max_default_jobs=2) == 2 - for badint in (0, -1) - @test_throws ArgumentError ParallelTestRunner.default_njobs(; memory_per_worker=badint) - @test_throws ArgumentError ParallelTestRunner.default_njobs(; max_default_jobs=badint) - end - for bad in (1.5, "2") - @test_throws TypeError ParallelTestRunner.default_njobs(; memory_per_worker=bad) - @test_throws TypeError ParallelTestRunner.default_njobs(; max_default_jobs=bad) - end + @test_throws ArgumentError ParallelTestRunner.default_njobs(; memory_per_worker=0) + @test_throws ArgumentError ParallelTestRunner.default_njobs(; memory_per_worker=-1) + @test_throws TypeError ParallelTestRunner.default_njobs(; memory_per_worker="2") + + @test_throws ArgumentError ParallelTestRunner.default_njobs(; max_default_jobs=0) + @test_throws ArgumentError ParallelTestRunner.default_njobs(; max_default_jobs=-1) + @test_throws TypeError ParallelTestRunner.default_njobs(; max_default_jobs=1.5) + @test_throws TypeError ParallelTestRunner.default_njobs(; max_default_jobs="2") end @testset "number of jobs" begin From d0350ab83c2e83cdc472341deac42843d5369c86 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:13:37 -0300 Subject: [PATCH 08/12] Ensure `default_njobs` always returns an integer --- src/resources.jl | 2 +- test/core.jl | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/resources.jl b/src/resources.jl index ba35b049..a4470f9c 100644 --- a/src/resources.jl +++ b/src/resources.jl @@ -57,5 +57,5 @@ function default_njobs(; throw(ArgumentError("`memory_per_worker` must be >= 1, got $(repr(memory_per_worker))")) memory_jobs = Int64(_free_memory) ÷ memory_per_worker - return max(1, min(_cpu_threads, memory_jobs, max_default_jobs)) + return Int(max(1, min(_cpu_threads, memory_jobs, max_default_jobs))) end diff --git a/test/core.jl b/test/core.jl index bd2bba77..a4c446d3 100644 --- a/test/core.jl +++ b/test/core.jl @@ -56,6 +56,10 @@ end memory_per_worker=1 * Int64(2) ^ 30, max_default_jobs=2) == 2 + # always returns an Int, regardless of the argument types + @test ParallelTestRunner.default_njobs(; memory_per_worker=1.5 * 2 ^ 30) isa Int + + # invalid argument values @test_throws ArgumentError ParallelTestRunner.default_njobs(; memory_per_worker=0) @test_throws ArgumentError ParallelTestRunner.default_njobs(; memory_per_worker=-1) @test_throws TypeError ParallelTestRunner.default_njobs(; memory_per_worker="2") From 10c646db75de79ecd40e063a74bdc44ce2fdaa56 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:17:26 -0300 Subject: [PATCH 09/12] Typo --- test/core.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/core.jl b/test/core.jl index a4c446d3..2ae7796e 100644 --- a/test/core.jl +++ b/test/core.jl @@ -48,7 +48,7 @@ end @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 32, memory_per_worker=1 * Int64(2) ^ 30) == 4 - # max_num_jobs respected even when other parameters would select more + # max_default_jobs respected even when other parameters would select more @test ParallelTestRunner.default_njobs(; _cpu_threads=4, _free_memory=UInt64(2) ^ 32, memory_per_worker=1 * Int64(2) ^ 30, max_default_jobs=1) == 1 From 868c82cac8ad8a880a56a27a6378c12056023f24 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:58:50 -0300 Subject: [PATCH 10/12] Accept max_default_jobs = 0 so users don't need to litter their code with `min` --- src/resources.jl | 10 +++++++--- test/core.jl | 4 +++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/resources.jl b/src/resources.jl index a4470f9c..6fd1bb0f 100644 --- a/src/resources.jl +++ b/src/resources.jl @@ -42,7 +42,9 @@ const DEFAULT_MAX_DEFAULT_JOBS = typemax(Int64) A function used to determine the default number of parallel jobs. Calculated as the number of CPU threads, clamped such that each worker can be assumed to use `memory_per_worker` -bytes of the available system memory, and, if specified, further bounded by `max_default_jobs`. +bytes of the available system memory, bounded by `max_default_jobs` if specified. + +Always returns at least `1`. """ function default_njobs(; memory_per_worker::Real = DEFAULT_MEMORY_PER_WORKER, @@ -51,8 +53,10 @@ function default_njobs(; _cpu_threads = (@static isdefined(Sys, :EFFECTIVE_CPU_THREADS) ? Sys.EFFECTIVE_CPU_THREADS : Sys.CPU_THREADS), _free_memory = available_memory(), ) - max_default_jobs >= 1 || - throw(ArgumentError("`max_default_jobs` must be a positive integer, got $(repr(max_default_jobs))")) + # Accept max_default_jobs = 0 since a common user pattern + # is to limit the number of jobs with a flooring division + max_default_jobs >= 0 || + throw(ArgumentError("`max_default_jobs` cannot be negative, got $(repr(max_default_jobs))")) memory_per_worker >= 1 || throw(ArgumentError("`memory_per_worker` must be >= 1, got $(repr(memory_per_worker))")) diff --git a/test/core.jl b/test/core.jl index 2ae7796e..7381009b 100644 --- a/test/core.jl +++ b/test/core.jl @@ -56,6 +56,9 @@ end memory_per_worker=1 * Int64(2) ^ 30, max_default_jobs=2) == 2 + # max_default_jobs = 0 returns 1 + @test ParallelTestRunner.default_njobs(; max_default_jobs=0) == 1 + # always returns an Int, regardless of the argument types @test ParallelTestRunner.default_njobs(; memory_per_worker=1.5 * 2 ^ 30) isa Int @@ -64,7 +67,6 @@ end @test_throws ArgumentError ParallelTestRunner.default_njobs(; memory_per_worker=-1) @test_throws TypeError ParallelTestRunner.default_njobs(; memory_per_worker="2") - @test_throws ArgumentError ParallelTestRunner.default_njobs(; max_default_jobs=0) @test_throws ArgumentError ParallelTestRunner.default_njobs(; max_default_jobs=-1) @test_throws TypeError ParallelTestRunner.default_njobs(; max_default_jobs=1.5) @test_throws TypeError ParallelTestRunner.default_njobs(; max_default_jobs="2") From 7855c860655b9bc7b9f8b95b7a9a4e5ec9210f34 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:07:07 -0300 Subject: [PATCH 11/12] Add test --- test/core.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/core.jl b/test/core.jl index 7381009b..e51a6f15 100644 --- a/test/core.jl +++ b/test/core.jl @@ -121,6 +121,14 @@ end max_default_jobs=1) end @test contains(String(take!(io)), "using 2 parallel jobs") + + # Run with 1 job despite a system not having enough resources for 1 job + io = IOBuffer() + withenv("PTR_NUM_JOBS" => nothing) do + runtests(ParallelTestRunner, String[]; testsuite, stdout=io, stderr=io, + max_default_jobs=2÷4) + end + @test contains(String(take!(io)), "using 1 parallel jobs") end @testset "subdir use" begin From 617424714d8e15b0512f522ff22b8dce1a74ec2a Mon Sep 17 00:00:00 2001 From: Christian <28689358+christiangnrd@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:02:27 -0300 Subject: [PATCH 12/12] feedback --- src/ParallelTestRunner.jl | 2 +- src/resources.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 7541e260..9611e746 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -57,7 +57,7 @@ include("execute.jl") stderr = Base.stderr, max_worker_rss = get_max_worker_rss(), memory_per_worker = 2 * 2^30, - max_default_jobs = typemax(Int64), + max_default_jobs = typemax(Int), serial = String[], serial_position::Symbol = :before, recycle_on_failure::Bool = false, diff --git a/src/resources.jl b/src/resources.jl index 6fd1bb0f..90c68073 100644 --- a/src/resources.jl +++ b/src/resources.jl @@ -35,7 +35,7 @@ end # memory). Packages whose tests are heavier can pass a larger # `memory_per_worker` to `runtests`. const DEFAULT_MEMORY_PER_WORKER = 2 * Int64(2)^30 -const DEFAULT_MAX_DEFAULT_JOBS = typemax(Int64) +const DEFAULT_MAX_DEFAULT_JOBS = typemax(Int) """ default_njobs(; memory_per_worker = 2 * 2^30, [max_default_jobs])