@@ -72,7 +72,8 @@ reg.finalizer(.duckdb, function(env) {
7272# ' the most-restrictive detected ceiling -- an explicit SLURM allocation
7373# ' (\code{SLURM_MEM_PER_NODE}, or \code{SLURM_MEM_PER_CPU} times
7474# ' \code{SLURM_CPUS_ON_NODE}), the cgroup limit (v2 \code{memory.max} then v1
75- # ' \code{memory.limit_in_bytes}), then physical RAM. DuckDB's own default is 80\%
75+ # ' \code{memory.limit_in_bytes}, read from the job's own cgroup resolved via
76+ # ' \code{/proc/self/cgroup} before the mount root), then physical RAM. DuckDB's own default is 80\%
7677# ' of *physical* RAM, which ignores a SLURM / cgroup cap and can over-commit
7778# ' (near-OOM on the first large scan); the detected default keeps a big
7879# ' aggregation spilling within the job's allocation. When nothing can be detected
@@ -81,7 +82,8 @@ reg.finalizer(.duckdb, function(env) {
8182# ' \code{threads} is likewise defaulted when unset: the most-restrictive detected
8283# ' CPU allocation -- a SLURM allocation (\code{SLURM_CPUS_PER_TASK}, or
8384# ' \code{SLURM_CPUS_ON_NODE}) then the cgroup CFS quota (v2 \code{cpu.max}, v1
84- # ' \code{cpu.cfs_quota_us}/\code{cpu.cfs_period_us}). DuckDB otherwise defaults to
85+ # ' \code{cpu.cfs_quota_us}/\code{cpu.cfs_period_us}, read from the job's own
86+ # ' cgroup subpath before the mount root). DuckDB otherwise defaults to
8587# ' hardware concurrency, which ignores a SLURM/cgroup cpuset and over-subscribes
8688# ' on a shared node (each extra thread also carries working memory against the
8789# ' memory cap). When no allocation is detected, DuckDB's default is left in place.
@@ -228,37 +230,73 @@ setExtensionDirectory <- function(conn) {
228230 NULL
229231}
230232
231- # Memory ceiling (bytes) from the cgroup limit (v2 then v1). NULL when
232- # unconstrained ("max", the v1 huge sentinel, or the files are absent).
233- .cgroupMemoryBytes <- function () {
234- read_limit <- function (path ) {
235- if (! file.exists(path )) {
236- return (NULL )
233+ # Ordered candidate cgroup directories for a controller, most specific first
234+ .cgroupDirs <- function (controller ,
235+ proc_cgroup = " /proc/self/cgroup" ,
236+ root = " /sys/fs/cgroup" ) {
237+ dirs <- character (0 )
238+ lines <- if (file.exists(proc_cgroup )) {
239+ tryCatch(readLines(proc_cgroup , warn = FALSE ), error = function (e ) character ())
240+ } else {
241+ character (0 )
242+ }
243+ for (ln in lines ) {
244+ parts <- strsplit(ln , " :" , fixed = TRUE )[[1L ]]
245+ if (length(parts ) < 3L ) {
246+ next
237247 }
238- val <- tryCatch(readLines(path , n = 1L , warn = FALSE ),
239- error = function (e ) character ())
240- if (! length(val ) || ! nzchar(val [1L ]) || val [1L ] == " max" ) {
241- return (NULL )
248+ ctrls <- parts [2L ]
249+ sub <- sub(" ^/" , " " , paste(parts [- (1 : 2 )], collapse = " :" )) # path may hold ':'
250+ if (! nzchar(ctrls )) {
251+ # v2 unified: files live directly under root/<path>
252+ dirs <- c(dirs , if (nzchar(sub )) file.path(root , sub ) else root )
253+ } else if (controller %in% strsplit(ctrls , " ," , fixed = TRUE )[[1L ]]) {
254+ # v1: files under root/<mount>/<path>; the mount may be the combined
255+ # controller string ("cpu,cpuacct") or a per-controller symlink.
256+ for (mnt in unique(c(ctrls , controller ))) {
257+ dirs <- c(dirs ,
258+ if (nzchar(sub )) file.path(root , mnt , sub ) else file.path(root , mnt ))
259+ }
242260 }
243- b <- suppressWarnings(as.numeric(val [1L ]))
244- # cgroup v1 "unlimited" is a huge sentinel (~PAGE_COUNTER_MAX); treat an
245- # implausibly large value (>= 1 EiB) as unset.
246- if (is.na(b ) || b < = 0 || b > = 2 ^ 60 ) {
247- return (NULL )
261+ }
262+ dirs <- c(dirs , root , file.path(root , controller )) # root fallbacks
263+ unique(dirs [dir.exists(dirs )])
264+ }
265+
266+ # Parse a cgroup byte-limit file (v2 memory.max / v1 memory.limit_in_bytes)
267+ .readCgroupBytes <- function (path ) {
268+ if (! file.exists(path )) {
269+ return (NULL )
270+ }
271+ val <- tryCatch(readLines(path , n = 1L , warn = FALSE ), error = function (e ) character ())
272+ if (! length(val ) || ! nzchar(val [1L ]) || val [1L ] == " max" ) {
273+ return (NULL )
274+ }
275+ b <- suppressWarnings(as.numeric(val [1L ]))
276+ if (is.na(b ) || b < = 0 || b > = 2 ^ 60 ) {
277+ return (NULL )
278+ }
279+ b
280+ }
281+
282+ # Memory ceiling (bytes) from the job's cgroup (v2 then v1)
283+ .cgroupMemoryBytes <- function (dirs = .cgroupDirs(" memory" )) {
284+ for (d in dirs ) {
285+ b <- .readCgroupBytes(file.path(d , " memory.max" )) %|| % # v2
286+ .readCgroupBytes(file.path(d , " memory.limit_in_bytes" )) # v1
287+ if (! is.null(b )) {
288+ return (b )
248289 }
249- b
250290 }
251- read_limit(" /sys/fs/cgroup/memory.max" ) %|| % # cgroup v2
252- read_limit(" /sys/fs/cgroup/memory/memory.limit_in_bytes" ) # cgroup v1
291+ NULL
253292}
254293
255294# Physical RAM (bytes) from /proc/meminfo (Linux only). NULL elsewhere.
256- .physicalMemoryBytes <- function () {
257- if (! file.exists(" /proc/meminfo " )) {
295+ .physicalMemoryBytes <- function (path = " /proc/meminfo " ) {
296+ if (! file.exists(path )) {
258297 return (NULL )
259298 }
260- ln <- tryCatch(readLines(" /proc/meminfo" , warn = FALSE ),
261- error = function (e ) character ())
299+ ln <- tryCatch(readLines(path , warn = FALSE ), error = function (e ) character ())
262300 kb <- grep(" ^MemTotal:" , ln , value = TRUE )
263301 if (! length(kb )) {
264302 return (NULL )
@@ -301,39 +339,41 @@ setExtensionDirectory <- function(conn) {
301339 NULL
302340}
303341
304- # CPU count from the cgroup CFS quota (v2 `cpu.max`, then v1
305- # `cpu.cfs_quota_us`/`cpu.cfs_period_us`) = floor(quota / period). NULL when
306- # unconstrained ("max", quota <= 0, or the files are absent).
307- .cgroupCpus <- function () {
308- quota_cpus <- function (q , p ) {
309- if (is.na(q ) || is.na(p ) || q < = 0 || p < = 0 ) {
310- return (NULL )
342+ # floor(quota / period) as a CPU count, or NULL when either is non-positive/NA.
343+ .cgroupQuotaCpus <- function (quota , period ) {
344+ if (is.na(quota ) || is.na(period ) || quota < = 0 || period < = 0 ) {
345+ return (NULL )
346+ }
347+ max(1L , as.integer(floor(quota / period )))
348+ }
349+
350+ # CPU count from the job's cgroup CFS quota
351+ .cgroupCpus <- function (dirs = .cgroupDirs(" cpu" )) {
352+ for (d in dirs ) {
353+ v2 <- file.path(d , " cpu.max" )
354+ if (file.exists(v2 )) {
355+ val <- tryCatch(strsplit(trimws(readLines(v2 , n = 1L , warn = FALSE )),
356+ " [[:space:]]+" )[[1L ]],
357+ error = function (e ) character ())
358+ if (length(val ) > = 2L && val [1L ] != " max" ) {
359+ n <- .cgroupQuotaCpus(suppressWarnings(as.numeric(val [1L ])),
360+ suppressWarnings(as.numeric(val [2L ])))
361+ if (! is.null(n )) {
362+ return (n )
363+ }
364+ }
311365 }
312- max(1L , as.integer(floor(q / p )))
313- }
314- v2 <- " /sys/fs/cgroup/cpu.max"
315- if (file.exists(v2 )) {
316- val <- tryCatch(strsplit(trimws(readLines(v2 , n = 1L , warn = FALSE )),
317- " [[:space:]]+" )[[1L ]],
318- error = function (e ) character ())
319- if (length(val ) > = 2L && val [1L ] != " max" ) {
320- n <- quota_cpus(suppressWarnings(as.numeric(val [1L ])),
321- suppressWarnings(as.numeric(val [2L ])))
366+ qf <- file.path(d , " cpu.cfs_quota_us" )
367+ pf <- file.path(d , " cpu.cfs_period_us" )
368+ if (file.exists(qf ) && file.exists(pf )) {
369+ n <- .cgroupQuotaCpus(
370+ suppressWarnings(as.numeric(readLines(qf , n = 1L , warn = FALSE ))),
371+ suppressWarnings(as.numeric(readLines(pf , n = 1L , warn = FALSE ))))
322372 if (! is.null(n )) {
323373 return (n )
324374 }
325375 }
326376 }
327- qf <- " /sys/fs/cgroup/cpu/cpu.cfs_quota_us"
328- pf <- " /sys/fs/cgroup/cpu/cpu.cfs_period_us"
329- if (file.exists(qf ) && file.exists(pf )) {
330- n <- quota_cpus(
331- suppressWarnings(as.numeric(readLines(qf , n = 1L , warn = FALSE ))),
332- suppressWarnings(as.numeric(readLines(pf , n = 1L , warn = FALSE ))))
333- if (! is.null(n )) {
334- return (n )
335- }
336- }
337377 NULL
338378}
339379
0 commit comments