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
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.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")),
Expand Down
24 changes: 24 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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()`,
Expand Down
76 changes: 53 additions & 23 deletions R/download_ltx23.R
Original file line number Diff line number Diff line change
Expand Up @@ -38,55 +38,80 @@ 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),
error = function(e) NULL
)
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)
}
Expand All @@ -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) {
Expand Down
49 changes: 49 additions & 0 deletions R/quantize_flux.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
25 changes: 24 additions & 1 deletion R/recommend.R
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion R/txt2img_flux.R
Original file line number Diff line number Diff line change
Expand Up @@ -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"))) {
Expand Down
7 changes: 4 additions & 3 deletions R/txt2img_flux2.R
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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"))) {
Expand Down
7 changes: 4 additions & 3 deletions R/txt2img_zimage.R
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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"))) {
Expand Down
40 changes: 40 additions & 0 deletions inst/tinytest/test_recommend.R
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<prefix>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"))

Loading
Loading