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
5 changes: 1 addition & 4 deletions 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.4
Version: 0.1.0.5
Authors@R: c(
person("Troy", "Hernandez", email = "troy@cornball.ai", role = c("aut", "cre"),
comment = c(ORCID = "0009-0005-4248-604X")),
Expand Down Expand Up @@ -28,10 +28,7 @@ Imports:
jpeg
Suggests:
av,
gpu.ctl,
hfhub,
safetensors,
tinytest
Remotes:
cornball-ai/gpu.ctl
RoxygenNote: 7.3.3
44 changes: 27 additions & 17 deletions R/auto_devices.R
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#' Auto-Configure Device Assignment
#'
#' Automatically determines optimal device configuration for diffusion model
#' components based on available VRAM and GPU architecture. Uses gpuctl for
#' detection if available, otherwise falls back to sensible defaults.
#' components based on available VRAM (via nvidia-smi) and GPU architecture.
#'
#' @param model Character. Model type: "sd21" or "sdxl".
#' @param strategy Character. Memory strategy: "auto" (default), "full_gpu",
Expand All @@ -13,17 +12,15 @@
#' @details
#' Strategies:
#' \describe{
#' \item{"auto"}{Detect VRAM and choose best strategy (requires gpuctl)}
#' \item{"full_gpu"}{All components on CUDA (16GB+ VRAM for SDXL)}
#' \item{"unet_gpu"}{Only unet on CUDA, rest on CPU (8GB+ VRAM)}
#' \item{"auto"}{Detect free VRAM and choose the best strategy}
#' \item{"full_gpu"}{All components on CUDA (10GB+ free VRAM for SDXL)}
#' \item{"unet_gpu"}{Only unet on CUDA, rest on CPU (6GB+ for SDXL)}
#' \item{"cpu_only"}{All components on CPU}
#' }
#'
#' If gpuctl is not installed, "auto" falls back to "unet_gpu" which works on
#' most modern GPUs (8GB+ VRAM).
#'
#' On Blackwell GPUs (RTX 50xx), "unet_gpu" is forced due to TorchScript
#' compatibility issues, regardless of available VRAM.
#' compatibility issues, regardless of available VRAM. The native modules
#' (`use_native_unet` and friends) do not have this restriction.
#'
#' @export
#'
Expand All @@ -39,20 +36,33 @@
#' devices <- auto_devices("sdxl", strategy = "cpu_only")
#' }
auto_devices <- function(model = "sdxl", strategy = "auto") {
# If gpuctl is available, use it

if (requireNamespace("gpu.ctl", quietly = TRUE)) {
return(gpu.ctl::recommended_devices(model = model, strategy = strategy))
# Free-VRAM requirements in GB (float16 component sizes + overhead)
requirements <- list(sd21 = list(full_gpu = 4, unet_gpu = 3),
sdxl = list(full_gpu = 10, unet_gpu = 6))
req <- requirements[[model]]
if (is.null(req)) {
stop("Unsupported model: ", model, ". Supported: ",
paste(names(requirements), collapse = ", "))
}

# Fallback when gpuctl not available
if (strategy == "auto") {
message("gpuctl not installed - using unet_gpu strategy as default")
message("Install gpuctl for auto-detection: install.packages('gpuctl')")
vram <- .detect_vram(use_free = TRUE)
strategy <- if (is_blackwell_gpu()) {
"unet_gpu"
} else if (vram >= req$full_gpu) {
"full_gpu"
} else if (vram >= req$unet_gpu) {
"unet_gpu"
} else {
"cpu_only"
}
message(sprintf("auto_devices: %s (%.1f GB free VRAM)", strategy, vram))
} else if (identical(strategy, "full_gpu") && is_blackwell_gpu()) {
# TorchScript workaround: full_gpu is not supported on Blackwell
message("Blackwell GPU detected - overriding full_gpu to unet_gpu")
strategy <- "unet_gpu"
}

# Build device config manually
.build_fallback_devices(model, strategy)
}

Expand Down
9 changes: 6 additions & 3 deletions R/download_flux2.R
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ NULL
#' (~7.8 GB in the HuggingFace cache) may be deleted after quantization.
#'
#' @param quantize Logical. Build the quantized artifact.
#' @param precision "fp8" (~4 GB, GPU-resident; near-bf16 quality) or
#' "nf4" (~2.3 GB).
#' @param precision "auto" (default: fp8 when safetensors supports
#' float8, else nf4), "fp8" (~4 GB, GPU-resident; near-bf16 quality),
#' or "nf4" (~2.3 GB).
#' @param output_dir Directory for the quantized artifact.
#' @param text_encoders Logical. Also fetch the Qwen3 text encoder,
#' tokenizer, VAE, and scheduler config (~8.3 GB).
Expand All @@ -44,10 +45,12 @@ NULL
#'
#' @export
download_flux2_klein <- function(quantize = TRUE,
precision = c("fp8", "nf4"),
precision = c("auto", "fp8", "nf4"),
output_dir = NULL, text_encoders = TRUE,
verbose = TRUE) {
precision <- match.arg(precision)
precision <- .flux_resolve_precision(precision,
file.path(tools::R_user_dir("diffuseR", "data"), "flux2-klein-4b-"))
if (is.null(output_dir)) {
output_dir <- file.path(tools::R_user_dir("diffuseR", "data"),
paste0("flux2-klein-4b-", precision))
Expand Down
9 changes: 6 additions & 3 deletions R/download_zimage.R
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ NULL
#' quantization.
#'
#' @param quantize Logical. Build the quantized artifact.
#' @param precision "fp8" (~6.3 GB, GPU-resident; near-bf16 quality) or
#' "nf4" (~3.6 GB).
#' @param precision "auto" (default: fp8 when safetensors supports
#' float8, else nf4), "fp8" (~6.3 GB, GPU-resident; near-bf16
#' quality), or "nf4" (~3.6 GB).
#' @param output_dir Directory for the quantized artifact.
#' @param text_encoders Logical. Also fetch the Qwen3-4B text encoder,
#' tokenizer, VAE, and scheduler config (~8.2 GB).
Expand All @@ -50,10 +51,12 @@ NULL
#'
#' @export
download_zimage_turbo <- function(quantize = TRUE,
precision = c("fp8", "nf4"),
precision = c("auto", "fp8", "nf4"),
output_dir = NULL, text_encoders = TRUE,
verbose = TRUE) {
precision <- match.arg(precision)
precision <- .flux_resolve_precision(precision,
file.path(tools::R_user_dir("diffuseR", "data"), "zimage-turbo-"))
if (is.null(output_dir)) {
output_dir <- file.path(tools::R_user_dir("diffuseR", "data"),
paste0("zimage-turbo-", precision))
Expand Down
2 changes: 1 addition & 1 deletion R/memory_flux.R
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ NULL
#' Resolve a FLUX memory profile
#'
#' @param vram_gb Numeric or NULL. Available VRAM; auto-detected when
#' NULL (via gpu.ctl or nvidia-smi).
#' NULL (via nvidia-smi).
#'
#' @return List with \code{name}, \code{precision} ("nf4"/"fp8"),
#' \code{attn_chunk}, \code{text_device}, \code{phase_offload}, and
Expand Down
84 changes: 83 additions & 1 deletion R/quantize_flux.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,66 @@ NULL
stop("Unsupported dtype: ", dtype))
}

# Capability probe: can the installed safetensors round-trip this dtype?
# CRAN safetensors (<= 0.2.1) can read bfloat16 but not write it, and has
# no float8 support; the fixes are upstream PRs. Cached per session;
# options(diffuseR.st_caps = list(bfloat16 = FALSE, ...)) overrides for
# tests.
.st_caps <- new.env(parent = emptyenv())
.st_can_write <- function(dtype = c("bfloat16", "float8_e4m3fn")) {
dtype <- match.arg(dtype)
override <- getOption("diffuseR.st_caps")
if (!is.null(override) && !is.null(override[[dtype]])) {
return(isTRUE(override[[dtype]]))
}
cached <- .st_caps[[dtype]]
if (!is.null(cached)) {
return(cached)
}
ok <- requireNamespace("safetensors", quietly = TRUE) && tryCatch({
target <- switch(dtype, bfloat16 = torch::torch_bfloat16(),
float8_e4m3fn = torch::torch_float8_e4m3fn())
x <- torch::torch_zeros(2L)$to(dtype = target)
tmp <- tempfile(fileext = ".safetensors")
on.exit(unlink(tmp), add = TRUE)
safetensors::safe_save_file(list(w = x), tmp)
y <- safetensors::safe_load_file(tmp, framework = "torch")
!is.null(y$w)
}, error = function(e) FALSE)
.st_caps[[dtype]] <- ok
ok
}

# Resolve precision = "auto": prefer an existing quantized artifact
# (fp8 first), else pick by float8 write capability. An fp8 artifact is
# only chosen if the installed safetensors can actually read float8 -
# otherwise a fork-built fp8 artifact on a CRAN-safetensors machine
# would be selected and then fail at read time. Write capability is a
# sound proxy for read capability (nothing writes fp8 but cannot read
# it).
.flux_resolve_precision <- function(precision, artifact_prefix = NULL) {
if (!identical(precision, "auto")) {
return(precision)
}
fp8_ok <- .st_can_write("float8_e4m3fn")
if (!is.null(artifact_prefix)) {
for (p in c("fp8", "nf4")) {
if (p == "fp8" && !fp8_ok) {
next
}
if (file.exists(file.path(paste0(artifact_prefix, p),
"manifest.json"))) {
return(p)
}
}
}
if (fp8_ok) {
"fp8"
} else {
"nf4"
}
}

# Transformer constructor arguments from a diffusers config.json
.flux_transformer_args <- function(config) {
if (is.null(config)) {
Expand Down Expand Up @@ -151,6 +211,22 @@ flux_quantize <- function(transformer_dir, output_dir = NULL,
format = c("nf4", "fp8"), shard_bytes = 4e9,
force = FALSE, verbose = TRUE) {
format <- match.arg(format)
if (format == "fp8" && !.st_can_write("float8_e4m3fn")) {
stop("The installed safetensors package cannot write float8 ",
"tensors (needs the float8 support pending in ",
"mlverse/safetensors#13; until it lands on CRAN, install ",
"remotes::install_github(\"cornball-ai/safetensors\") or ",
"use format = \"nf4\").", call. = FALSE)
}
# Residents load into the compute dtype either way; bf16 halves the
# artifact but CRAN safetensors (<= 0.2.1) cannot write it yet
resident_dtype <- if (.st_can_write("bfloat16")) {
torch::torch_bfloat16()
} else {
message("safetensors cannot write bfloat16; storing resident ",
"tensors as float32 (larger artifact, same results)")
torch::torch_float32()
}
if (is.null(output_dir)) {
output_dir <- file.path(tools::R_user_dir("diffuseR", "data"),
paste0("flux1-schnell-", format))
Expand Down Expand Up @@ -223,7 +299,7 @@ flux_quantize <- function(transformer_dir, output_dir = NULL,
# Residents load into the compute dtype anyway; storing bf16
# keeps fp32-shipped checkpoints (Z-Image) from doubling the
# artifact. Identity for bf16 sources.
shard[[key]] <- tensor$to(dtype = torch::torch_bfloat16())
shard[[key]] <- tensor$to(dtype = resident_dtype)
shard_size <- shard_size + prod(tensor$shape) * 2
}
rm(tensor)
Expand Down Expand Up @@ -298,6 +374,12 @@ flux_load_transformer <- function(ckpt, device = "cuda", dtype = "bfloat16",
verbose = TRUE, ...) {
stopifnot(inherits(ckpt, "ltx23_checkpoint"))
format <- ckpt$format %||% "full"
if (identical(format, "fp8") && !.st_can_write("float8_e4m3fn")) {
stop("This fp8 artifact needs float8 support the installed ",
"safetensors lacks (pending in mlverse/safetensors#13). ",
"Install remotes::install_github(\"cornball-ai/safetensors\"), ",
"or rebuild the artifact as nf4.", call. = FALSE)
}
hooks <- .flux_family_hooks(ckpt$config)

args <- utils::modifyList(hooks$args_fn(ckpt$config), list(...))
Expand Down
8 changes: 5 additions & 3 deletions R/txt2img_flux2.R
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ NULL
#' \code{download_flux2_klein} location for \code{precision}), or a
#' raw diffusers transformer directory.
#' @param device Character. Compute device.
#' @param precision "fp8" (default) or "nf4".
#' @param precision "auto" (default: reuse an existing artifact, else
#' fp8 when safetensors supports float8, else nf4), "fp8", or "nf4".
#' @param text_device Device for the Qwen3 encoder (default:
#' \code{device}; it encodes in its own phase and offloads).
#' @param attn_chunk Integer or NULL. Attention query-chunk override.
Expand All @@ -47,10 +48,11 @@ NULL
#'
#' @export
flux2_load_pipeline <- function(model_dir = NULL, device = "cuda",
precision = c("fp8", "nf4"),
precision = c("auto", "fp8", "nf4"),
text_device = NULL, attn_chunk = NULL,
phase_offload = TRUE, verbose = TRUE) {
precision <- match.arg(precision)
precision <- .flux_resolve_precision(match.arg(precision),
file.path(tools::R_user_dir("diffuseR", "data"), "flux2-klein-4b-"))
if (is.null(text_device)) {
if (device == "cuda") {
text_device <- "cuda"
Expand Down
8 changes: 5 additions & 3 deletions R/txt2img_zimage.R
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ NULL
#' \code{download_zimage_turbo} location for \code{precision}), or a
#' raw diffusers transformer directory.
#' @param device Character. Compute device.
#' @param precision "fp8" (default) or "nf4".
#' @param precision "auto" (default: reuse an existing artifact, else
#' fp8 when safetensors supports float8, else nf4), "fp8", or "nf4".
#' @param text_device Device for the Qwen3 encoder (default:
#' \code{device}; it encodes in its own phase and offloads).
#' @param attn_chunk Integer or NULL. Attention query-chunk override.
Expand All @@ -48,10 +49,11 @@ NULL
#'
#' @export
zimage_load_pipeline <- function(model_dir = NULL, device = "cuda",
precision = c("fp8", "nf4"),
precision = c("auto", "fp8", "nf4"),
text_device = NULL, attn_chunk = NULL,
phase_offload = TRUE, verbose = TRUE) {
precision <- match.arg(precision)
precision <- .flux_resolve_precision(match.arg(precision),
file.path(tools::R_user_dir("diffuseR", "data"), "zimage-turbo-"))
if (is.null(text_device)) {
if (device == "cuda") {
text_device <- "cuda"
Expand Down
Loading
Loading