diff --git a/DESCRIPTION b/DESCRIPTION index 38abf16..d656efe 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: diffuseR Title: Functional Interface to Diffusion Models in R -Version: 0.2.1.2 +Version: 0.2.1.3 Authors@R: c( person("Troy", "Hernandez", email = "troy@cornball.ai", role = c("aut", "cre"), comment = c(ORCID = "0009-0005-4248-604X")), diff --git a/NEWS.md b/NEWS.md index 2c8652f..649c754 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,27 @@ +# diffuseR 0.2.1.3 + +* Every precision `recommend()` can return is now reachable. `bf16` was + advertised for flux1 at 24 GB and flux2/zimage at 16-24 GB while no + loader accepted it; the flux-family loaders take + `precision = "bf16"`, which resolves the unquantized transformer out + of the hfhub cache rather than an artifact directory (bf16 is the + source the quantizers read, not something built). `recommend()` + explains the tier instead of returning a bare string. On a 16 GB + card, flux2 at bf16 renders 1024x1024 in 6.4 s against 9.1 s at fp8 - + the highest-quality tier is also the fastest, since nothing + dequantizes per layer; it costs the 7.8 GB source staying on disk. +* `download_ltx2()` gains `precision = c("nf4", "fp8")` and defaults to + nf4, which is what `recommend("ltx")` returns for any card with 14 GB + or more. It previously built only fp8, so the recommended tier had to + be built by hand with `ltx23_quantize_nf4()`. Asking for fp8 without + float8 write support now warns and builds nf4 instead of failing + inside the quantizer. +* Quantized artifacts stay locally built. Prebuilt weights are not + hosted for any model: only flux2 and zimage could be redistributed + (Apache-2.0 and ungated), while LTX-2.3 is under the LTX-2 Community + License and FLUX.1-schnell is gated, so hosting would cover half the + catalog and leave two models on a different workflow. + # diffuseR 0.2.1.2 * Model residency: `resident_load()`, `resident_activate()`, diff --git a/R/download_ltx23.R b/R/download_ltx23.R index 4d10972..344e4c8 100644 --- a/R/download_ltx23.R +++ b/R/download_ltx23.R @@ -38,39 +38,64 @@ NULL isTRUE(utils::askYesNo(paste0("Download ", what, "?"))) } -#' Download the LTX-2.3 checkpoint and build the fp8 artifact +#' Download the LTX-2.3 checkpoint and build a quantized artifact #' -#' Skips work that is already done: a valid fp8 manifest short-circuits +#' Skips work that is already done: a valid manifest short-circuits #' everything; a cached 46 GB source skips the download. The source file #' may be deleted after quantization (it is never removed automatically). #' -#' @param quantize Logical. Build the fp8 artifact after downloading. -#' @param output_dir Directory for the fp8 artifact. +#' Both quantized tiers are buildable here. \code{\link{recommend}} +#' returns nf4 for LTX on any card with 14 GB or more (it prefers nf4 at +#' 1280 px over fp8 at 1024 px, since video trades weight precision for +#' resolution), so nf4 is the tier most users want. fp8 additionally +#' needs a safetensors that can \emph{write} float8; asking for it +#' without one warns and builds nf4 instead rather than failing inside +#' the quantizer. +#' +#' @param quantize Logical. Build the quantized artifact after downloading. +#' @param precision "nf4" (~19 GB, readable by every safetensors) or +#' "fp8" (~26 GB, needs float8 write support). +#' @param output_dir Directory for the artifact. NULL derives it from +#' \code{precision}. #' @param text_encoder Logical. Also fetch the Gemma3 text encoder and #' tokenizer (~25 GB, shared with LTX-2.0; from the Lightricks/LTX-2 #' repo). #' @param verbose Logical. #' #' @return Invisibly, a list with \code{checkpoint} (source path or NULL), -#' \code{fp8_dir}, and \code{text_encoder_dir}. +#' \code{artifact_dir}, \code{precision}, \code{text_encoder_dir}, and +#' \code{fp8_dir} for back-compatibility -- the artifact directory when +#' \code{precision} is "fp8", NULL otherwise, since a field named +#' \code{fp8_dir} pointing at an nf4 artifact would be a trap. #' #' @export -download_ltx2 <- function(quantize = TRUE, - output_dir = file.path(tools::R_user_dir("diffuseR", "data"), "ltx2.3-fp8"), +download_ltx2 <- function(quantize = TRUE, precision = c("nf4", "fp8"), + output_dir = NULL, text_encoder = TRUE, verbose = TRUE) { if (!requireNamespace("hfhub", quietly = TRUE)) { stop("The hfhub package is required to download model weights.") } - result <- list(checkpoint = NULL, fp8_dir = output_dir, + precision <- match.arg(precision) + # Explicit fp8 without float8 write support: warn and build nf4 + # instead of failing deep inside the quantizer. + precision <- .st_graceful_precision(precision, mode = "write") + if (is.null(output_dir)) { + output_dir <- file.path(tools::R_user_dir("diffuseR", "data"), + paste0("ltx2.3-", precision)) + } + art_gb <- if (identical(precision, "fp8")) 26 else 19 + result <- list(checkpoint = NULL, artifact_dir = output_dir, + precision = precision, + fp8_dir = if (identical(precision, "fp8")) output_dir, text_encoder_dir = NULL) manifest_path <- file.path(output_dir, "manifest.json") - have_fp8 <- file.exists(manifest_path) && { + have_artifact <- file.exists(manifest_path) && { m <- jsonlite::fromJSON(manifest_path) all(file.exists(file.path(output_dir, m$shards))) } - if (!have_fp8 || !quantize) { + if (!have_artifact || !quantize) { cached <- tryCatch( hfhub::hub_download(.ltx23_checkpoint_repo, .ltx23_checkpoint_file, local_files_only = TRUE), @@ -78,15 +103,15 @@ download_ltx2 <- function(quantize = TRUE, ) if (is.null(cached)) { free <- .ltx23_disk_free_gb(path.expand("~")) - if (!is.na(free) && free < 75) { - warning(sprintf( - "Only %.0f GB free; the download + fp8 artifact need ~75 GB.", free - )) + if (!is.na(free) && free < 46 + art_gb) { + warning(sprintf(paste0("Only %.0f GB free; the download + ", + "%s artifact need ~%d GB."), + free, precision, 46 + art_gb)) } - ok <- .ltx23_consent(paste0( - "the LTX-2.3 distilled checkpoint (46 GB) plus a ~26 GB local fp8 ", - "artifact from HuggingFace (weights under the LTX-2 Community License)" - )) + ok <- .ltx23_consent(sprintf(paste0( + "the LTX-2.3 distilled checkpoint (46 GB) plus a ~%d GB local %s ", + "artifact from HuggingFace (weights under the LTX-2 Community License)"), + art_gb, precision)) if (!ok) { stop("Download cancelled.", call. = FALSE) } @@ -97,21 +122,26 @@ download_ltx2 <- function(quantize = TRUE, } result$checkpoint <- cached - if (quantize && !have_fp8) { + if (quantize && !have_artifact) { if (verbose) { - message("Quantizing transformer linears to fp8 (one-time)...") + message("Quantizing transformer linears to ", precision, + " (one-time)...") + } + if (identical(precision, "fp8")) { + ltx23_quantize_fp8(cached, output_dir, verbose = verbose) + } else { + ltx23_quantize_nf4(cached, output_dir, verbose = verbose) } - ltx23_quantize_fp8(cached, output_dir, verbose = verbose) if (verbose) { message( - "FP8 artifact ready: ", output_dir, "\n", + toupper(precision), " artifact ready: ", output_dir, "\n", "The 46 GB source in the HuggingFace cache may be deleted if ", "you do not need bf16 weights." ) } } } else if (verbose) { - message("FP8 artifact already present: ", output_dir) + message(toupper(precision), " artifact already present: ", output_dir) } if (text_encoder) { diff --git a/R/quantize_flux.R b/R/quantize_flux.R index 948828d..f75d248 100644 --- a/R/quantize_flux.R +++ b/R/quantize_flux.R @@ -76,6 +76,55 @@ NULL } } +# Locate a flux-family model's unquantized transformer in the hfhub +# cache. bf16 is not a built artifact: it IS the source the quantizers +# read, so the "bf16 tier" means loading those weights directly +# (flux_load_transformer() handles them as format "full"). Resolves from +# the cache only -- a loader must never trigger a multi-GB download as a +# side effect of a precision choice. +.flux_source_dir <- function(model) { + if (!requireNamespace("hfhub", quietly = TRUE)) { + stop("The hfhub package is required to locate bf16 source weights.", + call. = FALSE) + } + spec <- switch(model, + flux1 = list(repo = .flux1_repo, + files = .flux1_transformer_files, + fn = "download_flux1"), + flux2 = list(repo = .flux2_repo, + files = .flux2_transformer_files, + fn = "download_flux2_klein"), + zimage = list(repo = .zimage_repo, + files = .zimage_transformer_files, + fn = "download_zimage_turbo"), + stop("No bf16 source known for model '", model, "'", + call. = FALSE)) + paths <- lapply(spec$files, function(f) { + tryCatch(hfhub::hub_download(spec$repo, f, local_files_only = TRUE), + error = function(e) NULL) + }) + if (any(vapply(paths, is.null, logical(1)))) { + stop(sprintf(paste0("bf16 needs the unquantized %s transformer, ", + "which is not in the HuggingFace cache. Run ", + "%s(quantize = FALSE) to fetch it, or pick a ", + "quantized precision."), + spec$repo, spec$fn), call. = FALSE) + } + # Every file list starts with transformer/config.json, so the parent + # of the first hit is the transformer directory. + dirname(paths[[1]]) +} + +# Where a loader should read weights for a resolved precision: the +# built artifact for nf4/fp8, the cached source for bf16. +.flux_model_dir <- function(model, precision, prefix) { + if (identical(precision, "bf16")) { + .flux_source_dir(model) + } else { + paste0(prefix, precision) + } +} + # Transformer constructor arguments from a diffusers config.json .flux_transformer_args <- function(config) { if (is.null(config)) { diff --git a/R/recommend.R b/R/recommend.R index c4e96ac..bc0d626 100644 --- a/R/recommend.R +++ b/R/recommend.R @@ -131,10 +131,33 @@ recommend <- function(model = c("sd21", "sdxl", "flux1", "flux2", "zimage", pinned_set_gb = pinned_set, host_ram_gb = host_ram_gb, fork_suggested = fork, - note = if (fork) .st_fork_note(want$precision) else NULL + note = if (fork) { + .st_fork_note(want$precision) + } else { + .bf16_note(model, chosen$precision) + } ) } +# bf16 is not a built artifact: it is the unquantized source the +# quantizers read. Recommending the tier without saying that leaves the +# user with a precision they cannot act on, so spell out the one call +# that makes it available. +.bf16_note <- function(model, precision) { + if (!identical(precision, "bf16")) { + return(NULL) + } + fn <- switch(model, flux1 = "download_flux1", + flux2 = "download_flux2_klein", + zimage = "download_zimage_turbo", NULL) + if (is.null(fn)) { + return(NULL) + } + sprintf(paste0("bf16 loads the unquantized source rather than a ", + "quantized artifact. Keep it with %s(quantize = FALSE), ", + "then load with precision = \"bf16\"."), fn) +} + # 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 diff --git a/R/txt2img_flux.R b/R/txt2img_flux.R index 17416ad..e2688cc 100644 --- a/R/txt2img_flux.R +++ b/R/txt2img_flux.R @@ -119,7 +119,7 @@ flux_load_pipeline <- function(model_dir = NULL, device = "cuda", precision <- .flux_resolve_precision("auto", prefix) } if (is.null(model_dir)) { - model_dir <- paste0(prefix, precision) + model_dir <- .flux_model_dir("flux1", precision, prefix) } ckpt <- if (file.exists(file.path(model_dir, "manifest.json"))) { diff --git a/R/txt2img_flux2.R b/R/txt2img_flux2.R index a0da374..2bd22d5 100644 --- a/R/txt2img_flux2.R +++ b/R/txt2img_flux2.R @@ -52,7 +52,7 @@ NULL #' #' @export flux2_load_pipeline <- function(model_dir = NULL, device = "cuda", - precision = c("auto", "fp8", "nf4"), + precision = c("auto", "fp8", "nf4", "bf16"), text_device = NULL, attn_chunk = NULL, phase_offload = TRUE, pin = NULL, verbose = TRUE) { @@ -66,8 +66,9 @@ flux2_load_pipeline <- function(model_dir = NULL, device = "cuda", } } if (is.null(model_dir)) { - model_dir <- file.path(tools::R_user_dir("diffuseR", "data"), - paste0("flux2-klein-4b-", precision)) + model_dir <- .flux_model_dir("flux2", precision, + file.path(tools::R_user_dir("diffuseR", "data"), + "flux2-klein-4b-")) } ckpt <- if (file.exists(file.path(model_dir, "manifest.json"))) { diff --git a/R/txt2img_zimage.R b/R/txt2img_zimage.R index 482e385..c2179f3 100644 --- a/R/txt2img_zimage.R +++ b/R/txt2img_zimage.R @@ -53,7 +53,7 @@ NULL #' #' @export zimage_load_pipeline <- function(model_dir = NULL, device = "cuda", - precision = c("auto", "fp8", "nf4"), + precision = c("auto", "fp8", "nf4", "bf16"), text_device = NULL, attn_chunk = NULL, phase_offload = TRUE, pin = NULL, verbose = TRUE) { @@ -67,8 +67,9 @@ zimage_load_pipeline <- function(model_dir = NULL, device = "cuda", } } if (is.null(model_dir)) { - model_dir <- file.path(tools::R_user_dir("diffuseR", "data"), - paste0("zimage-turbo-", precision)) + model_dir <- .flux_model_dir("zimage", precision, + file.path(tools::R_user_dir("diffuseR", "data"), + "zimage-turbo-")) } ckpt <- if (file.exists(file.path(model_dir, "manifest.json"))) { diff --git a/inst/tinytest/test_recommend.R b/inst/tinytest/test_recommend.R index ec09b14..1398723 100644 --- a/inst/tinytest/test_recommend.R +++ b/inst/tinytest/test_recommend.R @@ -177,3 +177,43 @@ expect_true(recommend("sdxl", vram_gb = 16, st_caps = cran, 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 + +# --- every recommended tier must be reachable ------------------------------------- + +# recommend() used to return "bf16" for big cards while no loader +# accepted it and no builder produced it, with note = NULL. bf16 is the +# unquantized source, so the tier is real; the note has to say so. +fork <- list(bfloat16 = TRUE, float8_e4m3fn = TRUE) +b <- recommend("flux2", vram_gb = 24, st_caps = fork) +expect_equal(b$precision, "bf16") +expect_true(is.character(b$note)) +expect_true(grepl("download_flux2_klein", b$note)) +expect_true(grepl("quantize = FALSE", b$note)) +expect_false(b$fork_suggested) + +# The flux-family loaders must accept it as a precision. +for (fn in c("flux2_load_pipeline", "zimage_load_pipeline")) { + choices <- eval(formals(getExportedValue("diffuseR", fn))$precision) + expect_true("bf16" %in% choices) +} + +# A quantized tier still resolves to its artifact directory, and bf16 +# resolves elsewhere (the hub cache), not to a "bf16" dir. +expect_equal(diffuseR:::.flux_model_dir("flux2", "nf4", "/tmp/x-"), "/tmp/x-nf4") +expect_false(identical( + tryCatch(diffuseR:::.flux_model_dir("flux2", "bf16", "/tmp/x-"), + error = function(e) "errored"), + "/tmp/x-bf16")) + +# An unknown model has no bf16 source and must say so rather than +# silently building a bogus path. +expect_error(diffuseR:::.flux_source_dir("nosuch"), pattern = "No bf16 source") + +# --- LTX's recommended tier must be downloadable ---------------------------------- + +# recommend("ltx") is nf4 on any card >= 14 GB, so download_ltx2() has +# to be able to build nf4, not just fp8. +expect_equal(recommend("ltx", vram_gb = 16)$precision, "nf4") +expect_true("precision" %in% names(formals(download_ltx2))) +expect_equal(eval(formals(download_ltx2)$precision), c("nf4", "fp8")) + diff --git a/man/download_ltx2.Rd b/man/download_ltx2.Rd index bddfd40..d773ee5 100644 --- a/man/download_ltx2.Rd +++ b/man/download_ltx2.Rd @@ -1,16 +1,19 @@ % tinyrox says don't edit this manually, but it can't stop you! \name{download_ltx2} \alias{download_ltx2} -\title{Download the LTX-2.3 checkpoint and build the fp8 artifact} +\title{Download the LTX-2.3 checkpoint and build a quantized artifact} \usage{ -download_ltx2(quantize = TRUE, - output_dir = file.path(tools::R_user_dir("diffuseR", "data"), "ltx2.3-fp8"), +download_ltx2(quantize = TRUE, precision = c("nf4", "fp8"), output_dir = NULL, text_encoder = TRUE, verbose = TRUE) } \arguments{ -\item{quantize}{Logical. Build the fp8 artifact after downloading.} +\item{quantize}{Logical. Build the quantized artifact after downloading.} -\item{output_dir}{Directory for the fp8 artifact.} +\item{precision}{"nf4" (~19 GB, readable by every safetensors) or +"fp8" (~26 GB, needs float8 write support).} + +\item{output_dir}{Directory for the artifact. NULL derives it from +\code{precision}.} \item{text_encoder}{Logical. Also fetch the Gemma3 text encoder and tokenizer (~25 GB, shared with LTX-2.0; from the Lightricks/LTX-2 @@ -20,10 +23,23 @@ repo).} } \value{ Invisibly, a list with \code{checkpoint} (source path or NULL), - \code{fp8_dir}, and \code{text_encoder_dir}. + \code{artifact_dir}, \code{precision}, \code{text_encoder_dir}, and + \code{fp8_dir} for back-compatibility -- the artifact directory when + \code{precision} is "fp8", NULL otherwise, since a field named + \code{fp8_dir} pointing at an nf4 artifact would be a trap. } \description{ -Skips work that is already done: a valid fp8 manifest short-circuits +Skips work that is already done: a valid manifest short-circuits everything; a cached 46 GB source skips the download. The source file may be deleted after quantization (it is never removed automatically). } +\details{ +Both quantized tiers are buildable here. \code{\link{recommend}} +returns nf4 for LTX on any card with 14 GB or more (it prefers nf4 at +1280 px over fp8 at 1024 px, since video trades weight precision for +resolution), so nf4 is the tier most users want. fp8 additionally +needs a safetensors that can \emph{write} float8; asking for it +without one warns and builds nf4 instead rather than failing inside +the quantizer. + +} diff --git a/man/flux2_load_pipeline.Rd b/man/flux2_load_pipeline.Rd index e6ebefc..34964a9 100644 --- a/man/flux2_load_pipeline.Rd +++ b/man/flux2_load_pipeline.Rd @@ -4,9 +4,9 @@ \title{Load the FLUX.2 klein pipeline} \usage{ flux2_load_pipeline(model_dir = NULL, device = "cuda", - precision = c("auto", "fp8", "nf4"), text_device = NULL, - attn_chunk = NULL, phase_offload = TRUE, pin = NULL, - verbose = TRUE) + precision = c("auto", "fp8", "nf4", "bf16"), + text_device = NULL, attn_chunk = NULL, + phase_offload = TRUE, pin = NULL, verbose = TRUE) } \arguments{ \item{model_dir}{Quantized artifact directory (default: the diff --git a/man/zimage_load_pipeline.Rd b/man/zimage_load_pipeline.Rd index b7496c8..95391c5 100644 --- a/man/zimage_load_pipeline.Rd +++ b/man/zimage_load_pipeline.Rd @@ -4,9 +4,9 @@ \title{Load the Z-Image-Turbo pipeline} \usage{ zimage_load_pipeline(model_dir = NULL, device = "cuda", - precision = c("auto", "fp8", "nf4"), text_device = NULL, - attn_chunk = NULL, phase_offload = TRUE, pin = NULL, - verbose = TRUE) + precision = c("auto", "fp8", "nf4", "bf16"), + text_device = NULL, attn_chunk = NULL, + phase_offload = TRUE, pin = NULL, verbose = TRUE) } \arguments{ \item{model_dir}{Quantized artifact directory (default: the