From e6864950a0e51c6596895af8638cc85fe09e5d15 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Wed, 8 Jul 2026 17:14:07 -0500 Subject: [PATCH 1/4] CRAN prep: drop gpu.ctl, capability-gate quantization dtypes gpu.ctl leaves Suggests (and the Remotes field with it, which CRAN rejects): auto_devices() inlines the VRAM-threshold strategy logic, and is_blackwell_gpu() / .detect_vram() / vram_report() use their torch and nvidia-smi paths directly. Quantization now probes what the installed safetensors can write: fp8 artifacts require float8 support (pending in mlverse/safetensors#13) and fail early with an actionable message without it; pass-through residents fall back from bfloat16 to float32 on CRAN safetensors (mlverse/safetensors#11). The precision arguments of the FLUX.2/Z-Image downloaders and loaders gain an "auto" default: reuse an existing artifact, else fp8 when supported, else nf4. --- DESCRIPTION | 3 - R/auto_devices.R | 47 +++++++++------ R/download_flux2.R | 11 +++- R/download_zimage.R | 11 +++- R/memory_flux.R | 2 +- R/quantize_flux.R | 66 ++++++++++++++++++++- R/txt2img_flux2.R | 10 +++- R/txt2img_zimage.R | 10 +++- R/vram.R | 53 ++++++----------- inst/tinytest/test_auto_devices.R | 2 +- inst/tinytest/test_st_caps.R | 96 +++++++++++++++++++++++++++++++ man/auto_devices.Rd | 15 ++--- man/dot-detect_vram.Rd | 2 +- man/download_flux2_klein.Rd | 7 ++- man/download_zimage_turbo.Rd | 7 ++- man/flux2_load_pipeline.Rd | 5 +- man/flux_memory_profile.Rd | 2 +- man/vram_report.Rd | 2 +- man/zimage_load_pipeline.Rd | 5 +- 19 files changed, 265 insertions(+), 91 deletions(-) create mode 100644 inst/tinytest/test_st_caps.R diff --git a/DESCRIPTION b/DESCRIPTION index 28d9771..cb2aada 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -28,10 +28,7 @@ Imports: jpeg Suggests: av, - gpu.ctl, hfhub, safetensors, tinytest -Remotes: - cornball-ai/gpu.ctl RoxygenNote: 7.3.3 diff --git a/R/auto_devices.R b/R/auto_devices.R index 6a22057..b9e0ae4 100644 --- a/R/auto_devices.R +++ b/R/auto_devices.R @@ -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", @@ -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 #' @@ -39,20 +36,36 @@ #' 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) } diff --git a/R/download_flux2.R b/R/download_flux2.R index 05171bd..20a9a71 100644 --- a/R/download_flux2.R +++ b/R/download_flux2.R @@ -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). @@ -44,10 +45,14 @@ 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)) diff --git a/R/download_zimage.R b/R/download_zimage.R index 08edd4c..7029d18 100644 --- a/R/download_zimage.R +++ b/R/download_zimage.R @@ -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). @@ -50,10 +51,14 @@ 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)) diff --git a/R/memory_flux.R b/R/memory_flux.R index 3e7c7ec..2b73f62 100644 --- a/R/memory_flux.R +++ b/R/memory_flux.R @@ -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 diff --git a/R/quantize_flux.R b/R/quantize_flux.R index ce1f3bb..60c4897 100644 --- a/R/quantize_flux.R +++ b/R/quantize_flux.R @@ -16,6 +16,54 @@ 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 +.flux_resolve_precision <- function(precision, artifact_prefix = NULL) { + if (!identical(precision, "auto")) { + return(precision) + } + if (!is.null(artifact_prefix)) { + for (p in c("fp8", "nf4")) { + if (file.exists(file.path(paste0(artifact_prefix, p), + "manifest.json"))) { + return(p) + } + } + } + if (.st_can_write("float8_e4m3fn")) "fp8" else "nf4" +} + # Transformer constructor arguments from a diffusers config.json .flux_transformer_args <- function(config) { if (is.null(config)) { @@ -151,6 +199,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)) @@ -223,7 +287,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) diff --git a/R/txt2img_flux2.R b/R/txt2img_flux2.R index 7f42705..2c60795 100644 --- a/R/txt2img_flux2.R +++ b/R/txt2img_flux2.R @@ -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. @@ -47,10 +48,13 @@ 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" diff --git a/R/txt2img_zimage.R b/R/txt2img_zimage.R index ea51105..f633db4 100644 --- a/R/txt2img_zimage.R +++ b/R/txt2img_zimage.R @@ -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. @@ -48,10 +49,13 @@ 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" diff --git a/R/vram.R b/R/vram.R index a835ffa..f7975cc 100644 --- a/R/vram.R +++ b/R/vram.R @@ -21,12 +21,7 @@ NULL #' } #' } is_blackwell_gpu <- function() { - # Use gpuctl if available - if (requireNamespace("gpu.ctl", quietly = TRUE)) { - return(gpu.ctl::gpu_is_blackwell()) - } - - # Fallback: check compute capability via torch + # Check compute capability via torch if (torch::cuda_is_available()) { cap <- tryCatch(torch::cuda_get_device_capability(0L), error = function(e) NULL) @@ -41,27 +36,13 @@ is_blackwell_gpu <- function() { #' Detect Available VRAM #' -#' Uses gpuctl if available. +#' Asks nvidia-smi. #' #' @param use_free Logical. If TRUE, return free VRAM. If FALSE, return total. #' #' @return Numeric. VRAM in GB, or 0 if no GPU detected. #' @keywords internal .detect_vram <- function(use_free = FALSE) { - # Try gpuctl (preferred - uses nvidia-smi) - if (requireNamespace("gpu.ctl", quietly = TRUE)) { - info <- gpu.ctl::gpu_detect() - if (!is.null(info)) { - if (use_free && !is.null(info$vram_free_gb)) { - return(info$vram_free_gb) - } - if (!is.null(info$vram_total_gb)) { - return(info$vram_total_gb) - } - } - } - - # Fallback: ask nvidia-smi directly smi <- suppressWarnings(tryCatch( system2("nvidia-smi", c(paste0("--query-gpu=memory.", @@ -78,7 +59,7 @@ is_blackwell_gpu <- function() { # Fallback: check if CUDA available but can't determine VRAM if (torch::cuda_is_available()) { # Conservative estimate - assume 8GB if we can't detect - message("Could not detect VRAM. Install gpuctl for accurate detection.") + message("Could not detect VRAM via nvidia-smi; assuming 8 GB.") return(8) } @@ -136,7 +117,7 @@ load_to_gpu <- function(module, device = "cuda") { #' Report VRAM Usage #' -#' Prints current VRAM usage using gpuctl. +#' Prints current VRAM usage from nvidia-smi. #' #' @param label Character. Label for the report. #' @@ -154,19 +135,23 @@ vram_report <- function(label = "") { return(invisible(list(used = 0, free = 0))) } - # Use gpuctl for accurate reporting - if (requireNamespace("gpu.ctl", quietly = TRUE)) { - info <- gpu.ctl::gpu_detect() - if (!is.null(info)) { - used <- info$vram_used_gb - free <- info$vram_free_gb - message(sprintf("[%s] VRAM: %.2f GB used, %.2f GB free", label, - used, free)) - return(invisible(list(used = used, free = free))) - } + smi <- suppressWarnings(tryCatch( + system2("nvidia-smi", + c("--query-gpu=memory.used,memory.free", + "--format=csv,noheader,nounits"), + stdout = TRUE, stderr = FALSE), + error = function(e) character(0) + )) + vals <- suppressWarnings(as.numeric(strsplit(smi[1], ",")[[1]])) + if (length(vals) == 2L && all(is.finite(vals))) { + used <- vals[1] / 1024 + free <- vals[2] / 1024 + message(sprintf("[%s] VRAM: %.2f GB used, %.2f GB free", label, + used, free)) + return(invisible(list(used = used, free = free))) } - message("[", label, "] VRAM: (install gpuctl for detailed stats)") + message("[", label, "] VRAM: (nvidia-smi unavailable)") invisible(list(used = NA, free = NA)) } diff --git a/inst/tinytest/test_auto_devices.R b/inst/tinytest/test_auto_devices.R index f249cc9..9018a86 100644 --- a/inst/tinytest/test_auto_devices.R +++ b/inst/tinytest/test_auto_devices.R @@ -24,7 +24,7 @@ expect_equal(devices_sd21$unet, "cpu") devices_full <- auto_devices("sdxl", strategy = "full_gpu") expect_equal(devices_full$unet, "cuda") # On Blackwell, full_gpu gets overridden to unet_gpu -if (requireNamespace("gpu.ctl", quietly = TRUE) && gpu.ctl::gpu_is_blackwell()) { +if (is_blackwell_gpu()) { expect_equal(devices_full$decoder, "cpu") } else { expect_equal(devices_full$decoder, "cuda") diff --git a/inst/tinytest/test_st_caps.R b/inst/tinytest/test_st_caps.R new file mode 100644 index 0000000..75d24e2 --- /dev/null +++ b/inst/tinytest/test_st_caps.R @@ -0,0 +1,96 @@ +# safetensors capability gating: dtype write probes, precision "auto" +# resolution, and the quantizer's resident-dtype fallback for CRAN +# safetensors (no bfloat16 write, no float8). + +if (!requireNamespace("torch", quietly = TRUE) || !torch::torch_is_installed()) { + exit_file("torch not fully installed") +} +if (!requireNamespace("safetensors", quietly = TRUE)) { + exit_file("safetensors not installed") +} + +library(diffuseR) + +can_write <- diffuseR:::.st_can_write +resolve_precision <- diffuseR:::.flux_resolve_precision + +# --- capability probe ------------------------------------------------------------- + +expect_true(is.logical(can_write("bfloat16"))) +expect_true(is.logical(can_write("float8_e4m3fn"))) + +# Option override wins over the probe (and its cache) +options(diffuseR.st_caps = list(bfloat16 = FALSE, float8_e4m3fn = FALSE)) +expect_false(can_write("bfloat16")) +expect_false(can_write("float8_e4m3fn")) +options(diffuseR.st_caps = list(float8_e4m3fn = TRUE)) +expect_true(can_write("float8_e4m3fn")) +options(diffuseR.st_caps = NULL) + +# --- precision "auto" resolution --------------------------------------------------- + +# Explicit precision passes through untouched +expect_equal(resolve_precision("nf4"), "nf4") +expect_equal(resolve_precision("fp8"), "fp8") + +# An existing artifact wins, fp8 preferred +prefix <- file.path(tempdir(), "stcaps-test-") +nf4_dir <- paste0(prefix, "nf4") +fp8_dir <- paste0(prefix, "fp8") +dir.create(nf4_dir, showWarnings = FALSE) +writeLines("{}", file.path(nf4_dir, "manifest.json")) +expect_equal(resolve_precision("auto", prefix), "nf4") +dir.create(fp8_dir, showWarnings = FALSE) +writeLines("{}", file.path(fp8_dir, "manifest.json")) +expect_equal(resolve_precision("auto", prefix), "fp8") +unlink(c(nf4_dir, fp8_dir), recursive = TRUE) + +# No artifact: capability decides +options(diffuseR.st_caps = list(float8_e4m3fn = FALSE)) +expect_equal(resolve_precision("auto", prefix), "nf4") +options(diffuseR.st_caps = list(float8_e4m3fn = TRUE)) +expect_equal(resolve_precision("auto", prefix), "fp8") +options(diffuseR.st_caps = NULL) + +# --- quantizer gates (tiny checkpoint, CRAN-safetensors emulation) ------------------ + +ckpt_dir <- system.file("tinytest", "fixtures", "zimage_tiny_ckpt", + package = "diffuseR") +if (ckpt_dir == "") ckpt_dir <- "fixtures/zimage_tiny_ckpt" +if (!dir.exists(ckpt_dir)) exit_file("zimage tiny checkpoint missing") + +options(diffuseR.st_caps = list(bfloat16 = FALSE, float8_e4m3fn = FALSE)) + +# fp8 quantization is refused with an actionable error +expect_error( + flux_quantize(ckpt_dir, file.path(tempdir(), "stcaps-fp8"), + format = "fp8", verbose = FALSE), + pattern = "float8" +) + +# NF4 quantization falls back to float32 residents +nf4_out <- file.path(tempdir(), "stcaps-nf4") +unlink(nf4_out, recursive = TRUE) +expect_message( + manifest <- flux_quantize(ckpt_dir, nf4_out, format = "nf4", + verbose = FALSE), + pattern = "float32" +) +ck <- flux_open_quantized(nf4_out) +resident <- ck$handle$get_tensor("cap_embedder.1.weight") +expect_equal(as.character(resident$dtype), "Float") + +# And the artifact still loads and runs +model <- flux_load_transformer(ck, device = "cpu", dtype = "float32", + verbose = FALSE) +out <- torch::with_no_grad(model( + torch::torch_randn(4L, 1L, 12L, 20L), + torch::torch_tensor(0.5)$reshape(1L), + torch::torch_randn(37L, 24L) +)) +expect_equal(out$shape, c(4L, 1L, 12L, 20L)) +ltx23_release_dequant_buffers() + +options(diffuseR.st_caps = NULL) +options(diffuseR.block_gc = NULL) +unlink(c(nf4_out, file.path(tempdir(), "stcaps-fp8")), recursive = TRUE) diff --git a/man/auto_devices.Rd b/man/auto_devices.Rd index 0557af2..4f23c5b 100644 --- a/man/auto_devices.Rd +++ b/man/auto_devices.Rd @@ -16,23 +16,20 @@ A named list of device assignments suitable for `models2devices()`. } \description{ 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. } \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. } \examples{ \dontrun{ diff --git a/man/dot-detect_vram.Rd b/man/dot-detect_vram.Rd index c31670d..d8b5df6 100644 --- a/man/dot-detect_vram.Rd +++ b/man/dot-detect_vram.Rd @@ -12,6 +12,6 @@ Numeric. VRAM in GB, or 0 if no GPU detected. } \description{ -Uses gpuctl if available. +Asks nvidia-smi. } \keyword{internal} diff --git a/man/download_flux2_klein.Rd b/man/download_flux2_klein.Rd index 1c9e4ad..299cee8 100644 --- a/man/download_flux2_klein.Rd +++ b/man/download_flux2_klein.Rd @@ -3,14 +3,15 @@ \alias{download_flux2_klein} \title{Download FLUX.2-klein-4B and build the quantized artifact} \usage{ -download_flux2_klein(quantize = TRUE, precision = c("fp8", "nf4"), +download_flux2_klein(quantize = TRUE, precision = c("auto", "fp8", "nf4"), output_dir = NULL, text_encoders = TRUE, verbose = TRUE) } \arguments{ \item{quantize}{Logical. Build the quantized artifact.} -\item{precision}{"fp8" (~4 GB, GPU-resident; near-bf16 quality) or -"nf4" (~2.3 GB).} +\item{precision}{"auto" (default: fp8 when safetensors supports +float8, else nf4), "fp8" (~4 GB, GPU-resident; near-bf16 quality), +or "nf4" (~2.3 GB).} \item{output_dir}{Directory for the quantized artifact.} diff --git a/man/download_zimage_turbo.Rd b/man/download_zimage_turbo.Rd index 2cb8cd4..4b7a109 100644 --- a/man/download_zimage_turbo.Rd +++ b/man/download_zimage_turbo.Rd @@ -3,14 +3,15 @@ \alias{download_zimage_turbo} \title{Download Z-Image-Turbo and build the quantized artifact} \usage{ -download_zimage_turbo(quantize = TRUE, precision = c("fp8", "nf4"), +download_zimage_turbo(quantize = TRUE, precision = c("auto", "fp8", "nf4"), output_dir = NULL, text_encoders = TRUE, verbose = TRUE) } \arguments{ \item{quantize}{Logical. Build the quantized artifact.} -\item{precision}{"fp8" (~6.3 GB, GPU-resident; near-bf16 quality) or -"nf4" (~3.6 GB).} +\item{precision}{"auto" (default: fp8 when safetensors supports +float8, else nf4), "fp8" (~6.3 GB, GPU-resident; near-bf16 +quality), or "nf4" (~3.6 GB).} \item{output_dir}{Directory for the quantized artifact.} diff --git a/man/flux2_load_pipeline.Rd b/man/flux2_load_pipeline.Rd index 65a1dcc..0fbc531 100644 --- a/man/flux2_load_pipeline.Rd +++ b/man/flux2_load_pipeline.Rd @@ -4,7 +4,7 @@ \title{Load the FLUX.2 klein pipeline} \usage{ flux2_load_pipeline(model_dir = NULL, device = "cuda", - precision = c("fp8", "nf4"), text_device = NULL, + precision = c("auto", "fp8", "nf4"), text_device = NULL, attn_chunk = NULL, phase_offload = TRUE, verbose = TRUE) } \arguments{ @@ -14,7 +14,8 @@ raw diffusers transformer directory.} \item{device}{Character. Compute device.} -\item{precision}{"fp8" (default) or "nf4".} +\item{precision}{"auto" (default: reuse an existing artifact, else +fp8 when safetensors supports float8, else nf4), "fp8", or "nf4".} \item{text_device}{Device for the Qwen3 encoder (default: \code{device}; it encodes in its own phase and offloads).} diff --git a/man/flux_memory_profile.Rd b/man/flux_memory_profile.Rd index 4d5c7df..a9474fe 100644 --- a/man/flux_memory_profile.Rd +++ b/man/flux_memory_profile.Rd @@ -7,7 +7,7 @@ flux_memory_profile(vram_gb = NULL) } \arguments{ \item{vram_gb}{Numeric or NULL. Available VRAM; auto-detected when -NULL (via gpu.ctl or nvidia-smi).} +NULL (via nvidia-smi).} } \value{ List with \code{name}, \code{precision} ("nf4"/"fp8"), diff --git a/man/vram_report.Rd b/man/vram_report.Rd index 84fecf4..647ecff 100644 --- a/man/vram_report.Rd +++ b/man/vram_report.Rd @@ -12,7 +12,7 @@ vram_report(label = "") Invisibly returns a list with used and free VRAM in GB. } \description{ -Prints current VRAM usage using gpuctl. +Prints current VRAM usage from nvidia-smi. } \examples{ \dontrun{ diff --git a/man/zimage_load_pipeline.Rd b/man/zimage_load_pipeline.Rd index 6438623..9466ef5 100644 --- a/man/zimage_load_pipeline.Rd +++ b/man/zimage_load_pipeline.Rd @@ -4,7 +4,7 @@ \title{Load the Z-Image-Turbo pipeline} \usage{ zimage_load_pipeline(model_dir = NULL, device = "cuda", - precision = c("fp8", "nf4"), text_device = NULL, + precision = c("auto", "fp8", "nf4"), text_device = NULL, attn_chunk = NULL, phase_offload = TRUE, verbose = TRUE) } \arguments{ @@ -14,7 +14,8 @@ raw diffusers transformer directory.} \item{device}{Character. Compute device.} -\item{precision}{"fp8" (default) or "nf4".} +\item{precision}{"auto" (default: reuse an existing artifact, else +fp8 when safetensors supports float8, else nf4), "fp8", or "nf4".} \item{text_device}{Device for the Qwen3 encoder (default: \code{device}; it encodes in its own phase and offloads).} From 2617f9256d1c607028e6b1de762e731ef1ef510f Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Wed, 8 Jul 2026 17:14:07 -0500 Subject: [PATCH 2/4] Bump version to 0.1.0.5 --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index cb2aada..cd707a3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -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")), From 93b0b416dee30a01ac9f2887e5bfeb18f0cd8f52 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Wed, 8 Jul 2026 17:14:29 -0500 Subject: [PATCH 3/4] rformat + document --- R/auto_devices.R | 9 +++------ R/download_flux2.R | 6 ++---- R/download_zimage.R | 6 ++---- R/quantize_flux.R | 9 ++++++--- R/txt2img_flux2.R | 6 ++---- R/txt2img_zimage.R | 6 ++---- 6 files changed, 17 insertions(+), 25 deletions(-) diff --git a/R/auto_devices.R b/R/auto_devices.R index b9e0ae4..80c5b7d 100644 --- a/R/auto_devices.R +++ b/R/auto_devices.R @@ -37,10 +37,8 @@ #' } auto_devices <- function(model = "sdxl", strategy = "auto") { # 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) - ) + 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: ", @@ -58,8 +56,7 @@ auto_devices <- function(model = "sdxl", strategy = "auto") { } else { "cpu_only" } - message(sprintf("auto_devices: %s (%.1f GB free VRAM)", strategy, - vram)) + 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") diff --git a/R/download_flux2.R b/R/download_flux2.R index 20a9a71..a3a0fe2 100644 --- a/R/download_flux2.R +++ b/R/download_flux2.R @@ -49,10 +49,8 @@ download_flux2_klein <- function(quantize = TRUE, 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-") - ) + 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)) diff --git a/R/download_zimage.R b/R/download_zimage.R index 7029d18..fedfa22 100644 --- a/R/download_zimage.R +++ b/R/download_zimage.R @@ -55,10 +55,8 @@ download_zimage_turbo <- function(quantize = TRUE, 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-") - ) + 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)) diff --git a/R/quantize_flux.R b/R/quantize_flux.R index 60c4897..43deb5c 100644 --- a/R/quantize_flux.R +++ b/R/quantize_flux.R @@ -33,8 +33,7 @@ NULL return(cached) } ok <- requireNamespace("safetensors", quietly = TRUE) && tryCatch({ - target <- switch(dtype, - bfloat16 = torch::torch_bfloat16(), + 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") @@ -61,7 +60,11 @@ NULL } } } - if (.st_can_write("float8_e4m3fn")) "fp8" else "nf4" + if (.st_can_write("float8_e4m3fn")) { + "fp8" + } else { + "nf4" + } } # Transformer constructor arguments from a diffusers config.json diff --git a/R/txt2img_flux2.R b/R/txt2img_flux2.R index 2c60795..330622d 100644 --- a/R/txt2img_flux2.R +++ b/R/txt2img_flux2.R @@ -51,10 +51,8 @@ flux2_load_pipeline <- function(model_dir = NULL, device = "cuda", precision = c("auto", "fp8", "nf4"), text_device = NULL, attn_chunk = NULL, phase_offload = TRUE, verbose = TRUE) { - precision <- .flux_resolve_precision( - match.arg(precision), - file.path(tools::R_user_dir("diffuseR", "data"), "flux2-klein-4b-") - ) + 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" diff --git a/R/txt2img_zimage.R b/R/txt2img_zimage.R index f633db4..3036bce 100644 --- a/R/txt2img_zimage.R +++ b/R/txt2img_zimage.R @@ -52,10 +52,8 @@ zimage_load_pipeline <- function(model_dir = NULL, device = "cuda", precision = c("auto", "fp8", "nf4"), text_device = NULL, attn_chunk = NULL, phase_offload = TRUE, verbose = TRUE) { - precision <- .flux_resolve_precision( - match.arg(precision), - file.path(tools::R_user_dir("diffuseR", "data"), "zimage-turbo-") - ) + 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" From 2c218ec456145a545f462dd064c0fb8b960b71b4 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Wed, 8 Jul 2026 18:01:12 -0500 Subject: [PATCH 4/4] Gate fp8 artifact selection and loading on float8 read capability .flux_resolve_precision("auto") no longer selects an existing fp8 artifact when the installed safetensors cannot read float8 (a fork-built fp8 artifact on a CRAN-safetensors machine would be picked and then fail at read); it falls through to nf4. flux_load_transformer also refuses an fp8 checkpoint up front with an actionable message instead of a raw "Unsupported data type Float8_e4m3fn" mid-load. Covers the fork -> CRAN downgrade transition. --- R/quantize_flux.R | 19 +++++++++++++++++-- inst/tinytest/test_st_caps.R | 30 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/R/quantize_flux.R b/R/quantize_flux.R index 43deb5c..c47cc9d 100644 --- a/R/quantize_flux.R +++ b/R/quantize_flux.R @@ -47,20 +47,29 @@ NULL } # Resolve precision = "auto": prefer an existing quantized artifact -# (fp8 first), else pick by float8 write capability +# (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 (.st_can_write("float8_e4m3fn")) { + if (fp8_ok) { "fp8" } else { "nf4" @@ -365,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(...)) diff --git a/inst/tinytest/test_st_caps.R b/inst/tinytest/test_st_caps.R index 75d24e2..50a6fe7 100644 --- a/inst/tinytest/test_st_caps.R +++ b/inst/tinytest/test_st_caps.R @@ -52,6 +52,18 @@ options(diffuseR.st_caps = list(float8_e4m3fn = TRUE)) expect_equal(resolve_precision("auto", prefix), "fp8") options(diffuseR.st_caps = NULL) +# An fp8 artifact present but unreadable (CRAN safetensors) is NOT +# selected - it would fail at read time. Only nf4 (or a build) is safe. +dir.create(fp8_dir, showWarnings = FALSE) +writeLines("{}", file.path(fp8_dir, "manifest.json")) +options(diffuseR.st_caps = list(float8_e4m3fn = FALSE)) +expect_equal(resolve_precision("auto", prefix), "nf4") +# With float8 support the same fp8 artifact IS selected +options(diffuseR.st_caps = list(float8_e4m3fn = TRUE)) +expect_equal(resolve_precision("auto", prefix), "fp8") +options(diffuseR.st_caps = NULL) +unlink(fp8_dir, recursive = TRUE) + # --- quantizer gates (tiny checkpoint, CRAN-safetensors emulation) ------------------ ckpt_dir <- system.file("tinytest", "fixtures", "zimage_tiny_ckpt", @@ -94,3 +106,21 @@ ltx23_release_dequant_buffers() options(diffuseR.st_caps = NULL) options(diffuseR.block_gc = NULL) unlink(c(nf4_out, file.path(tempdir(), "stcaps-fp8")), recursive = TRUE) + +# --- load-path guard: fp8 artifact opened without float8 support -------------------- +# Build a real fp8 artifact (needs actual float8 support), then force the +# probe to "no float8" and confirm loading errors actionably rather than +# with a raw dtype failure. +if (can_write("float8_e4m3fn")) { + fp8_out <- file.path(tempdir(), "stcaps-loadguard-fp8") + unlink(fp8_out, recursive = TRUE) + flux_quantize(ckpt_dir, fp8_out, format = "fp8", verbose = FALSE) + options(diffuseR.st_caps = list(float8_e4m3fn = FALSE)) + expect_error( + flux_load_transformer(flux_open_quantized(fp8_out), device = "cpu", + verbose = FALSE), + pattern = "float8" + ) + options(diffuseR.st_caps = NULL) + unlink(fp8_out, recursive = TRUE) +}