Skip to content

Commit d244e1b

Browse files
authored
Use jl_effective_threads for container-aware CPU detection (#59916)
👨 It seems to me we should use the effective thread number basically everywhere when defaulting to running some threaded workload. Running with the raw cpu threads will explode quickly when running in a constrained environment. ----- 🤖 Julia was not respecting container CPU limits (Docker --cpus, Kubernetes CPU limits, cgroups) in several places, leading to oversubscription when determining parallelism. Changes: - src/aotcompile.cpp:2013: Use jl_effective_threads() for LLVM compilation threads - src/threading.c:760: Use jl_effective_threads() for GC thread capping/warnings - src/threading.c:842: Use jl_effective_threads() for JULIA_EXCLUSIVE mode validation - base/sysinfo.jl: Add Sys.EFFECTIVE_CPU_THREADS constant for user access - test/runtests.jl:119: Use Sys.EFFECTIVE_CPU_THREADS for test worker processes - base/precompilation.jl:529: Use Sys.EFFECTIVE_CPU_THREADS for precompilation tasks - base/util.jl:697: Use Sys.EFFECTIVE_CPU_THREADS for Base.runtests() default The existing jl_effective_threads() function returns min(jl_cpu_threads(), uv_available_parallelism()), where uv_available_parallelism() respects cgroup limits. --------
1 parent efe4535 commit d244e1b

File tree

7 files changed

+29
-7
lines changed

7 files changed

+29
-7
lines changed

base/precompilation.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ function _precompilepkgs(pkgs::Union{Vector{String}, Vector{PkgId}},
526526

527527
# Windows sometimes hits a ReadOnlyMemoryError, so we halve the default number of tasks. Issue #2323
528528
# TODO: Investigate why this happens in windows and restore the full task limit
529-
default_num_tasks = Sys.iswindows() ? div(Sys.CPU_THREADS::Int, 2) + 1 : Sys.CPU_THREADS::Int + 1
529+
default_num_tasks = Sys.iswindows() ? div(Sys.EFFECTIVE_CPU_THREADS::Int, 2) + 1 : Sys.EFFECTIVE_CPU_THREADS::Int + 1
530530
default_num_tasks = min(default_num_tasks, 16) # limit for better stability on shared resource systems
531531

532532
num_tasks = max(1, something(tryparse(Int, get(ENV, "JULIA_NUM_PRECOMPILE_TASKS", string(default_num_tasks))), 1))

base/sysinfo.jl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Provide methods for retrieving information about hardware and the operating syst
88
export BINDIR,
99
STDLIB,
1010
CPU_THREADS,
11+
EFFECTIVE_CPU_THREADS,
1112
CPU_NAME,
1213
WORD_SIZE,
1314
ARCH,
@@ -73,9 +74,28 @@ CPU cores, for example, in the presence of
7374
[hyper-threading](https://en.wikipedia.org/wiki/Hyper-threading).
7475
7576
See Hwloc.jl or CpuId.jl for extended information, including number of physical cores.
77+
78+
See also: [`Sys.EFFECTIVE_CPU_THREADS`](@ref) for a container-aware CPU count that respects
79+
cgroup limits.
7680
"""
7781
global CPU_THREADS::Int = 1 # for bootstrap, changed on startup
7882

83+
"""
84+
Sys.EFFECTIVE_CPU_THREADS::Int
85+
86+
The effective number of logical CPU cores available to the Julia process, taking into
87+
account container limits (e.g., Docker `--cpus`, Kubernetes CPU limits, cgroup quotas).
88+
This is the minimum of the hardware CPU thread count and any imposed CPU limits.
89+
90+
In non-containerized environments, this typically equals `Sys.CPU_THREADS`. In containerized
91+
environments, it respects cgroup CPU limits and provides a more accurate measure of
92+
available parallelism.
93+
94+
Use this constant when determining default thread pool sizes or parallelism levels to
95+
ensure proper behavior in containerized deployments.
96+
"""
97+
global EFFECTIVE_CPU_THREADS::Int = 1 # for bootstrap, changed on startup
98+
7999
"""
80100
Sys.ARCH::Symbol
81101
@@ -167,6 +187,7 @@ function __init__()
167187
else
168188
Int(ccall(:jl_cpu_threads, Int32, ()))
169189
end
190+
global EFFECTIVE_CPU_THREADS = min(CPU_THREADS, Int(ccall(:jl_effective_threads, Int32, ())))
170191
global SC_CLK_TCK = ccall(:jl_SC_CLK_TCK, Clong, ())
171192
global CPU_NAME = ccall(:jl_get_cpu_name, Ref{String}, ())
172193
global JIT = ccall(:jl_get_JIT, Ref{String}, ())

base/util.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,7 @@ end
680680
# testing
681681

682682
"""
683-
Base.runtests(tests=["all"]; ncores=ceil(Int, Sys.CPU_THREADS / 2),
683+
Base.runtests(tests=["all"]; ncores=ceil(Int, Sys.EFFECTIVE_CPU_THREADS / 2),
684684
exit_on_error=false, revise=false, propagate_project=true, [seed], [julia_args::Cmd])
685685
686686
Run the Julia unit tests listed in `tests`, which can be either a string or an array of
@@ -694,7 +694,7 @@ If a seed is provided via the keyword argument, it is used to seed the
694694
global RNG in the context where the tests are run; otherwise the seed is chosen randomly.
695695
The argument `julia_args` can be used to pass custom `julia` command line flags to the test process.
696696
"""
697-
function runtests(tests = ["all"]; ncores::Int = ceil(Int, Sys.CPU_THREADS / 2),
697+
function runtests(tests = ["all"]; ncores::Int = ceil(Int, Sys.EFFECTIVE_CPU_THREADS / 2),
698698
exit_on_error::Bool=false,
699699
revise::Bool=false,
700700
propagate_project::Bool=false,

doc/src/base/constants.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Base.DEPOT_PATH
1010
Base.LOAD_PATH
1111
Base.Sys.BINDIR
1212
Base.Sys.CPU_THREADS
13+
Base.Sys.EFFECTIVE_CPU_THREADS
1314
Base.Sys.WORD_SIZE
1415
Base.Sys.KERNEL
1516
Base.Sys.ARCH

src/aotcompile.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2010,7 +2010,7 @@ static unsigned compute_image_thread_count(const ModuleInfo &info) {
20102010
return 1;
20112011
}
20122012

2013-
unsigned threads = std::max(jl_cpu_threads() / 2, 1);
2013+
unsigned threads = std::max(jl_effective_threads() / 2, 1);
20142014

20152015
auto max_threads = info.globals / 100;
20162016
if (max_threads < threads) {

src/threading.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ void jl_init_threading(void)
757757
}
758758
}
759759

760-
int cpu = jl_cpu_threads();
760+
int cpu = jl_effective_threads();
761761
jl_n_markthreads = jl_options.nmarkthreads - 1;
762762
jl_n_sweepthreads = jl_options.nsweepthreads;
763763
if (jl_n_markthreads == -1) { // --gcthreads not specified
@@ -839,7 +839,7 @@ void jl_start_threads(void)
839839
// default pool according to a 'compact' policy
840840
// non-exclusive: no affinity settings; let the kernel move threads about
841841
if (exclusive) {
842-
if (ndefault_threads > jl_cpu_threads()) {
842+
if (ndefault_threads > jl_effective_threads()) {
843843
jl_printf(JL_STDERR, "ERROR: Too many threads requested for %s option.\n", MACHINE_EXCLUSIVE_NAME);
844844
exit(1);
845845
}

test/runtests.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ cd(@__DIR__) do
116116
# multiple worker processes regardless of the value of `net_on`.
117117
# Otherwise, we use multiple worker processes if and only if `net_on` is true.
118118
if net_on || JULIA_TEST_USE_MULTIPLE_WORKERS
119-
n = min(Sys.CPU_THREADS, length(tests))
119+
n = min(Sys.EFFECTIVE_CPU_THREADS, length(tests))
120120
n > 1 && addprocs_with_testenv(n)
121121
LinearAlgebra.BLAS.set_num_threads(1)
122122
end

0 commit comments

Comments
 (0)