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.2.2
Version: 0.2.2.3
Authors@R: c(
person("Troy", "Hernandez", email = "troy@cornball.ai", role = c("aut", "cre"),
comment = c(ORCID = "0009-0005-4248-604X")),
Expand Down
15 changes: 15 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
# diffuseR 0.2.2.3

* Prebuilt NF4 artifacts for flux2 (2.1 GB) and zimage (3.5 GB) are now
hosted on the cornball-ai HuggingFace org, and
`download_flux2_klein()` / `download_zimage_turbo()` fetch them by
default when the resolved precision is nf4 (`prebuilt = FALSE` forces
a local build). This narrowly reverses 0.2.2's no-hosting decision:
the CRAN safetensors cannot read any of the multi-GB upstream sources
(the fix is merged upstream, mlverse/safetensors#14, but unreleased),
which left a stock CRAN install unable to build a quantized artifact
at all. Hosting the two redistributable models (both Apache-2.0,
ungated) gives a plain `install.packages("diffuseR")` setup something
to generate with right away. FLUX.1-schnell (gated repo) and LTX-2.3
(LTX-2 Community License) still download sources and build locally.

# diffuseR 0.2.2.2

* `recommend()` tier selection now applies a 0.5 GB tolerance to the
Expand Down
20 changes: 16 additions & 4 deletions R/download_flux2.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
#'
#' Downloads FLUX.2-klein-4B from HuggingFace (Apache-2.0, ungated) and
#' quantizes the 4B transformer to a local fp8 (~4 GB) or NF4 (~2.3 GB)
#' artifact.
#' artifact. For nf4 the prebuilt hosted artifact is fetched instead of
#' building, when reachable (see \link{download_prebuilt}).
#'
#' @name download_flux2
NULL
Expand Down Expand Up @@ -34,10 +35,15 @@ NULL
#' @param quantize Logical. Build the quantized artifact.
#' @param precision "auto" (default: fp8 when safetensors supports
#' float8, else nf4), "fp8" (~4 GB, GPU-resident; near-bf16 quality),
#' or "nf4" (~2.3 GB).
#' or "nf4" (~2.3 GB, fetched prebuilt from the cornball-ai
#' HuggingFace dataset when available).
#' @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).
#' @param prebuilt Logical. When the resolved precision is nf4, fetch
#' the hosted prebuilt artifact (~2.3 GB) instead of downloading the
#' 7.8 GB bf16 source and quantizing locally. \code{FALSE} forces a
#' local build.
#' @param verbose Logical.
#'
#' @return Invisibly, a list with \code{transformer_dir},
Expand All @@ -47,7 +53,7 @@ NULL
download_flux2_klein <- function(quantize = TRUE,
precision = c("auto", "fp8", "nf4"),
output_dir = NULL, text_encoders = TRUE,
verbose = TRUE) {
prebuilt = TRUE, verbose = TRUE) {
precision <- match.arg(precision)
precision <- .flux_resolve_precision(precision,
file.path(tools::R_user_dir("diffuseR", "data"), "flux2-klein-4b-"))
Expand All @@ -69,6 +75,12 @@ download_flux2_klein <- function(quantize = TRUE,
all(file.exists(file.path(output_dir, m$shards)))
}

fetched <- FALSE
if (prebuilt && quantize && !have_artifact && identical(precision, "nf4")) {
fetched <- .flux_fetch_prebuilt("flux2", output_dir, verbose)
have_artifact <- have_artifact || fetched
}

if (!have_artifact || !quantize) {
cached <- tryCatch(
hfhub::hub_download(.flux2_repo, .flux2_transformer_files[[2]],
Expand Down Expand Up @@ -115,7 +127,7 @@ download_flux2_klein <- function(quantize = TRUE,
)
}
}
} else if (verbose) {
} else if (verbose && !fetched) {
message(toupper(precision), " artifact already present: ", output_dir)
}

Expand Down
120 changes: 120 additions & 0 deletions R/download_prebuilt.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#' Hosted prebuilt NF4 artifacts
#'
#' diffuseR hosts prebuilt NF4 artifacts for the two models whose
#' licenses permit redistribution: FLUX.2-klein-4B and Z-Image-Turbo
#' (both Apache-2.0, ungated). The artifacts are the exact output of
#' \code{\link{flux_quantize}} (sub-2 GB shards, bf16 residents), so
#' stock CRAN safetensors reads them. Nothing else in the catalog is
#' hosted: FLUX.1-schnell sits behind a HuggingFace license gate and
#' LTX-2.3's license does not permit redistribution, so both still
#' download their sources and quantize locally.
#'
#' @name download_prebuilt
#' @keywords internal
NULL

# model_name -> hosted dataset repo + artifact directory name. The repo
# follows the cornball-ai/<model>-R dataset convention (see
# hf_download_pt); the base mirrors the local artifact directory name so
# a fetched artifact is indistinguishable from a locally built one.
.prebuilt_nf4_spec <- list(
flux2 = list(repo = "cornball-ai/flux2-R", base = "flux2-klein-4b-nf4",
size = "~2.1 GB"),
zimage = list(repo = "cornball-ai/zimage-R", base = "zimage-turbo-nf4",
size = "~3.5 GB")
)

# TRUE when the hosted artifact is already fully in the hfhub cache, so
# fetching it needs no network and therefore no consent.
.prebuilt_all_cached <- function(spec) {
cached <- function(f) {
!is.null(tryCatch(
hfhub::hub_download(spec$repo, paste0(spec$base, "/", f),
repo_type = "dataset",
local_files_only = TRUE),
error = function(e) NULL))
}
if (!cached("manifest.json")) {
return(FALSE)
}
m <- jsonlite::fromJSON(hfhub::hub_download(spec$repo,
paste0(spec$base, "/manifest.json"), repo_type = "dataset",
local_files_only = TRUE))
all(vapply(m$shards, cached, logical(1)))
}

# Fetch a hosted NF4 artifact into output_dir. Returns TRUE when the
# artifact is complete there, FALSE when the model has no hosted
# artifact or the fetch failed (callers fall back to source +
# quantize). Files are hard-linked out of the hfhub cache when the
# filesystem allows it, copied otherwise.
.flux_fetch_prebuilt <- function(model, output_dir, verbose = TRUE) {
spec <- .prebuilt_nf4_spec[[model]]
if (is.null(spec)) {
return(FALSE)
}
# Every model download is consent-gated (see download_ltx2). A
# declined fetch returns FALSE so the caller falls through to the
# source + quantize path, which carries its own (larger) consent ask.
if (!.prebuilt_all_cached(spec) &&
!.ltx23_consent(sprintf("the prebuilt NF4 artifact (%s, %s)",
spec$size, spec$repo))) {
return(FALSE)
}
manifest_cache <- tryCatch(
hfhub::hub_download(spec$repo, paste0(spec$base, "/manifest.json"),
repo_type = "dataset"),
error = function(e) NULL)
if (is.null(manifest_cache)) {
if (verbose) {
message("No hosted NF4 artifact reachable for '", model,
"'; building locally instead.")
}
return(FALSE)
}
manifest <- jsonlite::fromJSON(manifest_cache)
if (verbose) {
message("Downloading the prebuilt NF4 artifact from ", spec$repo,
" (", length(manifest$shards), " shards)...")
}
shard_cache <- vapply(manifest$shards, function(s) {
tryCatch(hfhub::hub_download(spec$repo, paste0(spec$base, "/", s),
repo_type = "dataset"),
error = function(e) NA_character_)
}, character(1))
if (anyNA(shard_cache)) {
if (verbose) {
message("Prebuilt artifact fetch incomplete; ",
"building locally instead.")
}
return(FALSE)
}
dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
src <- c(manifest_cache, shard_cache)
dst <- file.path(output_dir, c("manifest.json", manifest$shards))
for (i in seq_along(src)) {
.link_or_copy(normalizePath(src[[i]]), dst[[i]])
}
ok <- file.exists(file.path(output_dir, "manifest.json"))
if (ok && verbose) {
message("NF4 artifact ready: ", output_dir)
}
ok
}

# Hard link (free on one filesystem), falling back to a copy. An
# existing destination is replaced.
.link_or_copy <- function(from, to) {
if (file.exists(to)) {
unlink(to)
}
ok <- suppressWarnings(file.link(from, to))
if (!ok) {
ok <- file.copy(from, to, overwrite = TRUE)
}
if (!ok) {
stop("Could not place ", basename(to), " in ", dirname(to),
call. = FALSE)
}
invisible(ok)
}
17 changes: 14 additions & 3 deletions R/download_zimage.R
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,15 @@ NULL
#' @param quantize Logical. Build the quantized artifact.
#' @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).
#' quality), or "nf4" (~3.6 GB, fetched prebuilt from the cornball-ai
#' HuggingFace dataset when available).
#' @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).
#' @param prebuilt Logical. When the resolved precision is nf4, fetch
#' the hosted prebuilt artifact (~3.6 GB) instead of downloading the
#' 24.6 GB float32 source and quantizing locally. \code{FALSE} forces
#' a local build.
#' @param verbose Logical.
#'
#' @return Invisibly, a list with \code{transformer_dir},
Expand All @@ -53,7 +58,7 @@ NULL
download_zimage_turbo <- function(quantize = TRUE,
precision = c("auto", "fp8", "nf4"),
output_dir = NULL, text_encoders = TRUE,
verbose = TRUE) {
prebuilt = TRUE, verbose = TRUE) {
precision <- match.arg(precision)
precision <- .flux_resolve_precision(precision,
file.path(tools::R_user_dir("diffuseR", "data"), "zimage-turbo-"))
Expand All @@ -75,6 +80,12 @@ download_zimage_turbo <- function(quantize = TRUE,
all(file.exists(file.path(output_dir, m$shards)))
}

fetched <- FALSE
if (prebuilt && quantize && !have_artifact && identical(precision, "nf4")) {
fetched <- .flux_fetch_prebuilt("zimage", output_dir, verbose)
have_artifact <- have_artifact || fetched
}

if (!have_artifact || !quantize) {
cached <- tryCatch(
hfhub::hub_download(.zimage_repo, .zimage_transformer_files[[3]],
Expand Down Expand Up @@ -121,7 +132,7 @@ download_zimage_turbo <- function(quantize = TRUE,
)
}
}
} else if (verbose) {
} else if (verbose && !fetched) {
message(toupper(precision), " artifact already present: ", output_dir)
}

Expand Down
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,13 @@ torch::cuda_empty_cache()
### FLUX and Z-Image

FLUX.1-schnell (12B), FLUX.2 Klein (4B), and Z-Image-Turbo (6B) are
step-distilled models: 4-8 denoising steps, no guidance. All are
quantized locally once at download time and fit comfortably on a 16GB
GPU (measured 1024x1024 on an RTX 5060 Ti: FLUX.1 ~55s at 9.6GB peak;
FLUX.2 Klein ~13s at 12.5GB; Z-Image-Turbo ~24s at 13.1GB).
step-distilled models: 4-8 denoising steps, no guidance. All fit
comfortably on a 16GB GPU (measured 1024x1024 on an RTX 5060 Ti:
FLUX.1 ~55s at 9.6GB peak; FLUX.2 Klein ~9s at fp8; Z-Image-Turbo
~24s at 13.1GB). Klein and Z-Image ship prebuilt NF4 artifacts (2.3
and 3.6 GB, hosted on the cornball-ai HuggingFace org), so a stock
CRAN install downloads a few GB and generates right away; FLUX.1
downloads its gated source and quantizes locally.

```r
library(diffuseR)
Expand All @@ -170,14 +173,16 @@ download_flux1()
txt2img_flux("An astronaut riding a horse on Mars, photorealistic",
seed = 7)

# FLUX.2 Klein 4B: ungated. ~16GB download, one-time fp8 quantize to
# a 3.9GB artifact.
# FLUX.2 Klein 4B: ungated. On stock safetensors this fetches the
# prebuilt NF4 artifact (~2.3GB); a float8-capable safetensors builds
# fp8 from the 7.8GB source instead.
download_flux2_klein()
txt2img_flux2("a red fox sitting in a snowy forest, digital art",
seed = 42)

# Z-Image-Turbo: ungated, strong at legible text in images (EN + CN).
# ~33GB download, one-time fp8 quantize to a 5.9GB artifact.
# Prebuilt NF4 (~3.6GB) on stock safetensors, or fp8 from the 24.6GB
# float32 source.
download_zimage_turbo()
txt2img_zimage(paste("A storefront with a large wooden sign that reads",
"\"DIFFUSER\" in bold carved letters"), seed = 42)
Expand Down Expand Up @@ -261,7 +266,7 @@ Same prompt, same seed, 1024x1024, measured on an RTX 5060 Ti 16GB:
| Model | Settings | Load | Warm generation | Peak VRAM |
|---|---|---|---|---|
| SDXL | 50 steps, CFG 7.5 | 45 s | 20 s | **12.7 GB** |
| FLUX.2 Klein 4B | 4 steps, guidance-free | 32 s | **13 s** | 12.5 GB |
| FLUX.2 Klein 4B | 4 steps, guidance-free, fp8 | 32 s | **9.1 s** | 12.5 GB |

FLUX.2 is now faster per image (since the allocator gc-gate fix), and
its prompt adherence and coherence are in a different class — SDXL
Expand Down
21 changes: 21 additions & 0 deletions inst/tinytest/test_download_prebuilt.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Hosted-artifact policy: exactly the redistributable pair (Apache-2.0,
# ungated), nothing else. flux1 (gated) and ltx (LTX-2 Community
# License) must never appear here.
expect_equal(sort(names(diffuseR:::.prebuilt_nf4_spec)),
c("flux2", "zimage"))

# Unhosted models refuse without touching the network.
expect_false(diffuseR:::.flux_fetch_prebuilt("flux1", tempfile(),
verbose = FALSE))
expect_false(diffuseR:::.flux_fetch_prebuilt("ltx", tempfile(),
verbose = FALSE))

# .link_or_copy places a file and replaces an existing destination.
src <- tempfile()
writeLines("x", src)
dst <- tempfile()
diffuseR:::.link_or_copy(normalizePath(src), dst)
expect_true(file.exists(dst))
diffuseR:::.link_or_copy(normalizePath(src), dst)
expect_equal(readLines(dst), "x")
unlink(c(src, dst))
3 changes: 2 additions & 1 deletion man/download_flux2.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
\description{
Downloads FLUX.2-klein-4B from HuggingFace (Apache-2.0, ungated) and
quantizes the 4B transformer to a local fp8 (~4 GB) or NF4 (~2.3 GB)
artifact.
artifact. For nf4 the prebuilt hosted artifact is fetched instead of
building, when reachable (see \link{download_prebuilt}).
}
9 changes: 8 additions & 1 deletion man/download_flux2_klein.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ download_flux2_klein(
precision = c("auto", "fp8", "nf4"),
output_dir = NULL,
text_encoders = TRUE,
prebuilt = TRUE,
verbose = TRUE
)
}
Expand All @@ -16,13 +17,19 @@ download_flux2_klein(

\item{precision}{"auto" (default: fp8 when safetensors supports
float8, else nf4), "fp8" (~4 GB, GPU-resident; near-bf16 quality),
or "nf4" (~2.3 GB).}
or "nf4" (~2.3 GB, fetched prebuilt from the cornball-ai
HuggingFace dataset when available).}

\item{output_dir}{Directory for the quantized artifact.}

\item{text_encoders}{Logical. Also fetch the Qwen3 text encoder,
tokenizer, VAE, and scheduler config (~8.3 GB).}

\item{prebuilt}{Logical. When the resolved precision is nf4, fetch
the hosted prebuilt artifact (~2.3 GB) instead of downloading the
7.8 GB bf16 source and quantizing locally. \code{FALSE} forces a
local build.}

\item{verbose}{Logical.}
}
\value{
Expand Down
15 changes: 15 additions & 0 deletions man/download_prebuilt.Rd
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
% tinyrox says don't edit this manually, but it can't stop you!
\name{download_prebuilt}
\alias{download_prebuilt}
\title{Hosted prebuilt NF4 artifacts}
\description{
diffuseR hosts prebuilt NF4 artifacts for the two models whose
licenses permit redistribution: FLUX.2-klein-4B and Z-Image-Turbo
(both Apache-2.0, ungated). The artifacts are the exact output of
\code{\link{flux_quantize}} (sub-2 GB shards, bf16 residents), so
stock CRAN safetensors reads them. Nothing else in the catalog is
hosted: FLUX.1-schnell sits behind a HuggingFace license gate and
LTX-2.3's license does not permit redistribution, so both still
download their sources and quantize locally.
}
\keyword{internal}
Loading
Loading