From 6f41825cb94a6f2680be5c19638d5be38c75878a Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Wed, 22 Jul 2026 16:36:33 -0500 Subject: [PATCH 1/3] recommend(): host-RAM-aware pin decision; CLAUDE.md gpuctl claims corrected --- CLAUDE.md | 19 +++++---- NEWS.md | 10 +++++ R/recommend.R | 78 ++++++++++++++++++++++++++++++++-- inst/tinytest/test_recommend.R | 20 +++++++++ 4 files changed, 115 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d850fe4..24f72a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ Native R package for Stable Diffusion image generation. ```r library(diffuseR) torch::with_no_grad({ - # Auto-detect optimal device configuration (requires gpuctl) + # Auto-detect optimal device configuration (self-contained, via nvidia-smi) txt2img_sdxl("A sunset over mountains", filename = "output.png") }) ``` @@ -31,11 +31,12 @@ txt2img_sdxl("A sunset over mountains", devices = devices) ## GPU Poor Support -diffuseR integrates with gpuctl for automatic "GPU poor" support: +diffuseR ships self-contained automatic "GPU poor" support (VRAM and +architecture detection via nvidia-smi; no gpuctl dependency): ```r # Auto-detect VRAM and select optimal strategy -devices <- auto_devices("sdxl") # Uses gpuctl if available +devices <- auto_devices("sdxl") # nvidia-smi detection, base R only # Strategies (SDXL thresholds): # - full_gpu: All on CUDA (10GB+ VRAM) @@ -45,7 +46,7 @@ devices <- auto_devices("sdxl", strategy = "unet_gpu") ``` The `txt2img_*` and `img2img` functions default to `devices = "auto"`, which: -1. Uses gpuctl to detect VRAM and GPU architecture +1. Detects VRAM and GPU architecture via nvidia-smi (self-contained) 2. Selects optimal strategy (full_gpu, unet_gpu, or cpu_only) 3. Forces unet_gpu on Blackwell GPUs (TorchScript workaround) @@ -56,7 +57,7 @@ Profile-based memory optimization for constrained GPUs. ### API ```r -txt2img_sdxl("A cat", profile = "auto") # Default: auto-detect via gpuctl +txt2img_sdxl("A cat", profile = "auto") # Default: auto-detect via nvidia-smi txt2img_sdxl("A cat", profile = "gpu_poor") # Force low-memory mode txt2img_sdxl("A cat", profile = "full_gpu", vram_debug = TRUE) # Debug VRAM usage ``` @@ -104,7 +105,7 @@ txt2img_sdxl("A cat", profile = "full_gpu", vram_debug = TRUE) # Debug VRAM usa ```r resolve_profile <- function(profile = "auto", model = "sdxl") { if (profile == "auto") { - vram <- gpuctl::gpu_detect()$vram_gb + vram <- .detect_vram() profile <- if (vram >= 16) "full_gpu" else if (vram >= 10) "balanced" else if (vram >= 6) "gpu_poor" @@ -309,9 +310,9 @@ See cornyverse CLAUDE.md for safetensors package setup (use cornball-ai fork unt - [x] **UNet SD21**: 4 blocks, 686 parameters - [x] **UNet SDXL**: 3 blocks, variable transformer depths (0, 2, 10), 1680 parameters -### gpuctl Integration -- [x] **Auto-device configuration**: `auto_devices()` integrates with gpuctl - - Queries available VRAM via `gpuctl::gpu_detect()` +### Device auto-configuration +- [x] **Auto-device configuration**: `auto_devices()` and `recommend()` are self-contained + - Query available VRAM and architecture via nvidia-smi directly - Auto-selects optimal devices based on model requirements - Handles Blackwell workaround automatically diff --git a/NEWS.md b/NEWS.md index 3135005..33d7f7c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,13 @@ +# diffuseR 0.1.0.16 (development) + +* `recommend()` is now host-RAM-aware: it returns `pin` (page-lock the + phase-swapped host weight copies), `pinned_set_gb`, and + `host_ram_gb`. Pinned pages are unswappable, so the policy pins only + when available RAM covers the model's pinned set twice over — small- + RAM machines get `pin = FALSE` instead of OOM-kill risk, the cpu + tier never pins, and undetectable RAM leaves pinning on (it already + fails soft per component). + # diffuseR 0.1.0.15 (development) * Pinned staging now allocates page-locked host memory via diff --git a/R/recommend.R b/R/recommend.R index b0e7fe8..47e49ab 100644 --- a/R/recommend.R +++ b/R/recommend.R @@ -23,18 +23,33 @@ #' pipeline uses \code{\link{ltx23_memory_profile}} for frame-aware #' placement. #' +#' The pinning decision: phase-swapped weights are page-locked host +#' copies (see \code{\link{staging_ltx23}}) that transfer at DMA rate - +#' but pinned pages are unswappable, so on small-RAM machines they turn +#' memory pressure into OOM kills. \code{pin} is TRUE when available +#' host RAM covers the model's pinned set twice over, FALSE below that, +#' FALSE on the cpu tier (nothing stages), and TRUE when RAM cannot be +#' detected (page-locking already fails soft per component). Loaders +#' honor it through their \code{pin} arguments and +#' \code{options(diffuseR.pin_staging)}. +#' #' @param model "sd21", "sdxl", "flux1", "flux2", "zimage", or "ltx". #' @param vram_gb Numeric or NULL. Free VRAM in GB; auto-detected via #' nvidia-smi when NULL. #' @param st_caps NULL or a named logical list with \code{bfloat16} #' and/or \code{float8_e4m3fn} - the safetensors READ capabilities. #' NULL probes the installed safetensors. +#' @param host_ram_gb Numeric or NULL. Available host RAM in GB; +#' auto-detected (Linux \code{MemAvailable}) when NULL, NA where +#' undetectable. #' #' @return A list with \code{model}, \code{precision}, \code{devices} #' (named component -> device map), \code{offload} (phase-offloading #' logical), \code{max_pixels}, \code{text_device}, \code{attn_chunk}, -#' \code{vram_gb}, \code{fork_suggested} (logical), and \code{note} -#' (the fork suggestion string, or NULL). +#' \code{vram_gb}, \code{pin} (page-lock the phase-swapped host +#' copies), \code{pinned_set_gb} (estimated pinned bytes), +#' \code{host_ram_gb}, \code{fork_suggested} (logical), and +#' \code{note} (the fork suggestion string, or NULL). #' #' @export #' @@ -52,7 +67,7 @@ #' } recommend <- function(model = c("sd21", "sdxl", "flux1", "flux2", "zimage", "ltx"), - vram_gb = NULL, st_caps = NULL) { + vram_gb = NULL, st_caps = NULL, host_ram_gb = NULL) { model <- match.arg(model) if (is.null(vram_gb)) { vram_gb <- .detect_vram(use_free = TRUE) @@ -87,6 +102,19 @@ recommend <- function(model = c("sd21", "sdxl", "flux1", "flux2", "zimage", } fork <- !is.null(want) && !identical(want$precision, chosen$precision) + + if (is.null(host_ram_gb)) { + host_ram_gb <- .detect_host_ram() + } + pinned_set <- .pinned_set_gb(model, chosen$precision) + pin <- if (isTRUE(chosen$cpu)) { + FALSE # cpu tier: nothing phase-stages, nothing to page-lock + } else if (is.na(host_ram_gb)) { + TRUE # undetectable: page-locking fails soft per component + } else { + host_ram_gb >= 2 * pinned_set + } + list( model = model, precision = chosen$precision, @@ -96,11 +124,55 @@ recommend <- function(model = c("sd21", "sdxl", "flux1", "flux2", "zimage", text_device = chosen$text_device %||% "cpu", attn_chunk = chosen$attn_chunk, vram_gb = vram_gb, + pin = pin, + pinned_set_gb = pinned_set, + host_ram_gb = host_ram_gb, fork_suggested = fork, note = if (fork) .st_fork_note(want$precision) else NULL ) } +# Available host RAM in GB (Linux MemAvailable); NA where undetectable +# (macOS, Windows). NA feeds a keep-pinning decision: page-locking +# already falls back silently per component, and platforms without +# /proc rarely pair with CUDA. +.detect_host_ram <- function() { + if (!file.exists("/proc/meminfo")) { + return(NA_real_) + } + tryCatch({ + line <- grep("^MemAvailable:", readLines("/proc/meminfo", n = 20L), + value = TRUE) + if (!length(line)) { + return(NA_real_) + } + kb <- as.numeric(strsplit(trimws(sub("^MemAvailable:", "", line)), + "[[:space:]]+")[[1]][1]) + kb / 1024^2 + }, error = function(e) NA_real_) +} + +# Host GB that pinned staging would page-lock for a model at a given +# precision: the quantized checkpoint plus the text encoder's host +# copy. Coarse artifact-scale estimates - the pin decision needs the +# order of magnitude, not the byte. +.pinned_set_gb <- function(model, precision) { + sets <- list( + sd21 = c(fp16 = 3, nf4 = 2), + sdxl = c(fp16 = 8, nf4 = 4), + flux1 = c(bf16 = 43, fp8 = 31, nf4 = 26), # DiT + T5 fp32 host copy + flux2 = c(bf16 = 17, fp8 = 12, nf4 = 10), # DiT + Qwen3 bf16 + zimage = c(bf16 = 21, fp8 = 14, nf4 = 12), # DiT + Qwen3 bf16 + ltx = c(fp8 = 34, nf4 = 28) # checkpoint + Gemma3 NF4 + ) + v <- unname(sets[[model]][precision]) + if (length(v) != 1L || is.na(v)) { + 0 + } else { + v + } +} + # flux-family component placement: the big DiT and the VAE compute on # the GPU (or all on CPU for the cpu tier); the text encoder is resident # on the CPU and phase-onloaded during its own phase. diff --git a/inst/tinytest/test_recommend.R b/inst/tinytest/test_recommend.R index bf378f7..eafb74d 100644 --- a/inst/tinytest/test_recommend.R +++ b/inst/tinytest/test_recommend.R @@ -156,3 +156,23 @@ e <- tryCatch(brc(function() stop("plain boom"), small), expect_true(grepl("plain boom", e)) expect_false(grepl("2\\^31", e)) unlink(small) + +# --- pin decision ----------------------------------------------------------------- + +r <- recommend("ltx", vram_gb = 16, st_caps = cran, host_ram_gb = 125) +expect_true(all(c("pin", "pinned_set_gb", "host_ram_gb") %in% names(r))) +expect_true(r$pin) # 125 GB >> 2 x ~28 GB pinned set +expect_true(r$pinned_set_gb > 0) + +expect_false(recommend("ltx", vram_gb = 16, st_caps = cran, + host_ram_gb = 32)$pin) # 32 < 2 x pinned set + +expect_false(recommend("ltx", vram_gb = 0, st_caps = cran, + host_ram_gb = 125)$pin) # cpu tier: nothing stages + +expect_true(recommend("sdxl", vram_gb = 16, st_caps = cran, + host_ram_gb = NA)$pin) # undetectable: fail-soft on + +hr <- diffuseR:::.detect_host_ram() +expect_true(is.na(hr) || (is.numeric(hr) && hr > 0)) +expect_equal(diffuseR:::.pinned_set_gb("ltx", "bf16"), 0) # unknown tier -> 0 From 9d679c8a3e781e86c47a01db143afbcabee18f11 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Wed, 22 Jul 2026 16:36:57 -0500 Subject: [PATCH 2/3] rformat + document --- R/recommend.R | 14 +++++++------- man/recommend.Rd | 22 +++++++++++++++++++--- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/R/recommend.R b/R/recommend.R index 47e49ab..b89bb77 100644 --- a/R/recommend.R +++ b/R/recommend.R @@ -148,7 +148,7 @@ recommend <- function(model = c("sd21", "sdxl", "flux1", "flux2", "zimage", } kb <- as.numeric(strsplit(trimws(sub("^MemAvailable:", "", line)), "[[:space:]]+")[[1]][1]) - kb / 1024^2 + kb / 1024 ^ 2 }, error = function(e) NA_real_) } @@ -158,12 +158,12 @@ recommend <- function(model = c("sd21", "sdxl", "flux1", "flux2", "zimage", # order of magnitude, not the byte. .pinned_set_gb <- function(model, precision) { sets <- list( - sd21 = c(fp16 = 3, nf4 = 2), - sdxl = c(fp16 = 8, nf4 = 4), - flux1 = c(bf16 = 43, fp8 = 31, nf4 = 26), # DiT + T5 fp32 host copy - flux2 = c(bf16 = 17, fp8 = 12, nf4 = 10), # DiT + Qwen3 bf16 - zimage = c(bf16 = 21, fp8 = 14, nf4 = 12), # DiT + Qwen3 bf16 - ltx = c(fp8 = 34, nf4 = 28) # checkpoint + Gemma3 NF4 + sd21 = c(fp16 = 3, nf4 = 2), + sdxl = c(fp16 = 8, nf4 = 4), + flux1 = c(bf16 = 43, fp8 = 31, nf4 = 26), # DiT + T5 fp32 host copy + flux2 = c(bf16 = 17, fp8 = 12, nf4 = 10), # DiT + Qwen3 bf16 + zimage = c(bf16 = 21, fp8 = 14, nf4 = 12), # DiT + Qwen3 bf16 + ltx = c(fp8 = 34, nf4 = 28) # checkpoint + Gemma3 NF4 ) v <- unname(sets[[model]][precision]) if (length(v) != 1L || is.na(v)) { diff --git a/man/recommend.Rd b/man/recommend.Rd index 79ed32e..dd4e543 100644 --- a/man/recommend.Rd +++ b/man/recommend.Rd @@ -4,7 +4,7 @@ \title{Recommend a precision and device configuration for a model} \usage{ recommend(model = c("sd21", "sdxl", "flux1", "flux2", "zimage", "ltx"), - vram_gb = NULL, st_caps = NULL) + vram_gb = NULL, st_caps = NULL, host_ram_gb = NULL) } \arguments{ \item{model}{"sd21", "sdxl", "flux1", "flux2", "zimage", or "ltx".} @@ -15,13 +15,19 @@ nvidia-smi when NULL.} \item{st_caps}{NULL or a named logical list with \code{bfloat16} and/or \code{float8_e4m3fn} - the safetensors READ capabilities. NULL probes the installed safetensors.} + +\item{host_ram_gb}{Numeric or NULL. Available host RAM in GB; +auto-detected (Linux \code{MemAvailable}) when NULL, NA where +undetectable.} } \value{ A list with \code{model}, \code{precision}, \code{devices} (named component -> device map), \code{offload} (phase-offloading logical), \code{max_pixels}, \code{text_device}, \code{attn_chunk}, - \code{vram_gb}, \code{fork_suggested} (logical), and \code{note} - (the fork suggestion string, or NULL). + \code{vram_gb}, \code{pin} (page-lock the phase-swapped host + copies), \code{pinned_set_gb} (estimated pinned bytes), + \code{host_ram_gb}, \code{fork_suggested} (logical), and + \code{note} (the fork suggestion string, or NULL). } \description{ One VRAM-and-capability-aware recommendation for every diffuseR @@ -48,6 +54,16 @@ elsewhere. Video sizing for \code{"ltx"} is coarse here; the LTX pipeline uses \code{\link{ltx23_memory_profile}} for frame-aware placement. +The pinning decision: phase-swapped weights are page-locked host +copies (see \code{\link{staging_ltx23}}) that transfer at DMA rate - +but pinned pages are unswappable, so on small-RAM machines they turn +memory pressure into OOM kills. \code{pin} is TRUE when available +host RAM covers the model's pinned set twice over, FALSE below that, +FALSE on the cpu tier (nothing stages), and TRUE when RAM cannot be +detected (page-locking already fails soft per component). Loaders +honor it through their \code{pin} arguments and +\code{options(diffuseR.pin_staging)}. + } \examples{ \dontrun{ From d7a5bb2c70feee5a8912c444152f5c2b6985141e Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Wed, 22 Jul 2026 16:36:57 -0500 Subject: [PATCH 3/3] Bump version to 0.1.0.16 --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 12e7954..3ea4d54 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: diffuseR Title: Functional Interface to Diffusion Models in R -Version: 0.1.0.15 +Version: 0.1.0.16 Authors@R: c( person("Troy", "Hernandez", email = "troy@cornball.ai", role = c("aut", "cre"), comment = c(ORCID = "0009-0005-4248-604X")),