Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
```
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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
```
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -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")),
Expand Down
10 changes: 10 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
78 changes: 75 additions & 3 deletions R/recommend.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
#'
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions inst/tinytest/test_recommend.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 19 additions & 3 deletions man/recommend.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -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".}
Expand All @@ -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
Expand All @@ -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{
Expand Down
Loading