Skip to content

Commit 31bbb10

Browse files
committed
Default DuckDB memory_limit to the job's allocation, not physical RAM
configureOutOfCore() set memory_limit only when the user configured it, so DuckDB fell back to its own default of 80% of *physical* RAM. That ignores a SLURM / cgroup allocation and over-commits: a large out-of-core aggregation (e.g. rowDeviances' ~27.9M-group per-cell GROUP BY) nearly OOMs on the first scan instead of spilling within the allocation. When neither DuckDBDataFrame.memory_limit nor BIOCDUCKDB_MEMORY_LIMIT is set, default memory_limit to 80% of the most-restrictive detected ceiling: a SLURM allocation (SLURM_MEM_PER_NODE, or SLURM_MEM_PER_CPU times SLURM_CPUS_ON_NODE), the cgroup limit (v2 memory.max then v1 memory.limit_in_bytes), then physical RAM (/proc/meminfo). Nothing detected (e.g. macOS) leaves DuckDB's default in place; an explicit option/env var still overrides. - R/DuckDBConnection.R: .slurmMemoryBytes/.cgroupMemoryBytes/ .physicalMemoryBytes/.defaultMemoryLimit; wire into configureOutOfCore; docs - DESCRIPTION, NEWS.md: 0.99.10
1 parent 2bbfd0d commit 31bbb10

4 files changed

Lines changed: 112 additions & 1 deletion

File tree

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Package: DuckDBDataFrame
2-
Version: 0.99.9
2+
Version: 0.99.10
33
Date: 2026-07-23
44
Title: DuckDB-Backed DataFrame and Table Structures
55
Description:

NEWS.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
# DuckDBDataFrame 0.99.10
2+
3+
## Bug fixes
4+
5+
- `configureOutOfCore()` now defaults the DuckDB `memory_limit` to 80% of the
6+
most-restrictive detected ceiling when neither `DuckDBDataFrame.memory_limit`
7+
nor `BIOCDUCKDB_MEMORY_LIMIT` is set: an explicit SLURM allocation
8+
(`SLURM_MEM_PER_NODE`, or `SLURM_MEM_PER_CPU` times `SLURM_CPUS_ON_NODE`), the
9+
cgroup limit (v2 `memory.max` then v1 `memory.limit_in_bytes`), then physical
10+
RAM (`/proc/meminfo`). DuckDB's own default is 80% of *physical* RAM, which
11+
ignores a SLURM / cgroup cap and over-commits — nearly OOM-ing on the first
12+
large scan of a big out-of-core aggregation instead of spilling. When nothing
13+
can be detected (e.g. macOS) DuckDB's default is left in place.
14+
115
# DuckDBDataFrame 0.99.9
216

317
## Bug fixes

R/DuckDBConnection.R

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ reg.finalizer(.duckdb, function(env) {
6868
#' mid-session and make a spill fail to create its directory. Point
6969
#' \code{BIOCDUCKDB_TEMP_DIRECTORY} at roomy scratch for large out-of-core sorts.
7070
#'
71+
#' The \code{memory_limit} likewise gets a package default when unset: 80\% of
72+
#' the most-restrictive detected ceiling -- an explicit SLURM allocation
73+
#' (\code{SLURM_MEM_PER_NODE}, or \code{SLURM_MEM_PER_CPU} times
74+
#' \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\%
76+
#' of *physical* RAM, which ignores a SLURM / cgroup cap and can over-commit
77+
#' (near-OOM on the first large scan); the detected default keeps a big
78+
#' aggregation spilling within the job's allocation. When nothing can be detected
79+
#' (e.g. macOS) DuckDB's default is left in place.
80+
#'
7181
#' @examples
7282
#' releaseDuckDBConn()
7383
#' conn <- acquireDuckDBConn()
@@ -195,11 +205,88 @@ setExtensionDirectory <- function(conn) {
195205
NULL
196206
}
197207

208+
# Memory ceiling (bytes) from an explicit SLURM allocation: per-node memory if
209+
# set, else per-CPU memory times the CPUs on this node. NULL when unset.
210+
.slurmMemoryBytes <- function() {
211+
mb <- suppressWarnings(as.numeric(Sys.getenv("SLURM_MEM_PER_NODE", "")))
212+
if (!is.na(mb) && mb > 0) {
213+
return(mb * 2^20)
214+
}
215+
pc <- suppressWarnings(as.numeric(Sys.getenv("SLURM_MEM_PER_CPU", "")))
216+
nc <- suppressWarnings(as.numeric(Sys.getenv("SLURM_CPUS_ON_NODE", "")))
217+
if (!is.na(pc) && pc > 0 && !is.na(nc) && nc > 0) {
218+
return(pc * nc * 2^20)
219+
}
220+
NULL
221+
}
222+
223+
# Memory ceiling (bytes) from the cgroup limit (v2 then v1). NULL when
224+
# unconstrained ("max", the v1 huge sentinel, or the files are absent).
225+
.cgroupMemoryBytes <- function() {
226+
read_limit <- function(path) {
227+
if (!file.exists(path)) {
228+
return(NULL)
229+
}
230+
val <- tryCatch(readLines(path, n = 1L, warn = FALSE),
231+
error = function(e) character())
232+
if (!length(val) || !nzchar(val[1L]) || val[1L] == "max") {
233+
return(NULL)
234+
}
235+
b <- suppressWarnings(as.numeric(val[1L]))
236+
# cgroup v1 "unlimited" is a huge sentinel (~PAGE_COUNTER_MAX); treat an
237+
# implausibly large value (>= 1 EiB) as unset.
238+
if (is.na(b) || b <= 0 || b >= 2^60) {
239+
return(NULL)
240+
}
241+
b
242+
}
243+
read_limit("/sys/fs/cgroup/memory.max") %||% # cgroup v2
244+
read_limit("/sys/fs/cgroup/memory/memory.limit_in_bytes") # cgroup v1
245+
}
246+
247+
# Physical RAM (bytes) from /proc/meminfo (Linux only). NULL elsewhere.
248+
.physicalMemoryBytes <- function() {
249+
if (!file.exists("/proc/meminfo")) {
250+
return(NULL)
251+
}
252+
ln <- tryCatch(readLines("/proc/meminfo", warn = FALSE),
253+
error = function(e) character())
254+
kb <- grep("^MemTotal:", ln, value = TRUE)
255+
if (!length(kb)) {
256+
return(NULL)
257+
}
258+
b <- suppressWarnings(as.numeric(sub("^MemTotal:[[:space:]]*([0-9]+).*",
259+
"\\1", kb[1L]))) * 1024
260+
if (is.na(b) || b <= 0) {
261+
return(NULL)
262+
}
263+
b
264+
}
265+
266+
# Default DuckDB memory_limit when the user has not configured one: 80% of the
267+
# most-restrictive detected ceiling (SLURM allocation, cgroup limit, physical
268+
# RAM), as a DuckDB byte size (e.g. "51539607552B"). NULL when nothing can be
269+
# detected (e.g. macOS), leaving DuckDB's own default in place. The 80% leaves
270+
# headroom for R and the driver so a large aggregation spills within the job's
271+
# allocation instead of over-committing against a cgroup cap the DuckDB default
272+
# (80% of *physical* RAM) ignores.
273+
.defaultMemoryLimit <- function() {
274+
caps <- c(.slurmMemoryBytes(), .cgroupMemoryBytes(), .physicalMemoryBytes())
275+
caps <- caps[!is.na(caps) & caps > 0]
276+
if (!length(caps)) {
277+
return(NULL)
278+
}
279+
sprintf("%.0fB", floor(min(caps) * 0.8))
280+
}
281+
198282
#' @export
199283
#' @importFrom DBI dbExecute
200284
#' @rdname DuckDBConnection
201285
configureOutOfCore <- function(conn) {
202286
ml <- .outOfCoreSetting("DuckDBDataFrame.memory_limit", "BIOCDUCKDB_MEMORY_LIMIT")
287+
if (is.null(ml)) {
288+
ml <- .defaultMemoryLimit()
289+
}
203290
if (!is.null(ml)) {
204291
try(dbExecute(conn, sprintf("SET memory_limit = '%s';",
205292
gsub("'", "''", ml))), silent = TRUE)

man/DuckDBConnection.Rd

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)