From 424c383d8674a82197b4c37e636b9709775d7378 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 24 Aug 2026 16:28:44 -0500 Subject: [PATCH 1/5] rformat: normalise a continuation line carried over from #64 --- R/resident.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/resident.R b/R/resident.R index 29e5eb2..7a61b38 100644 --- a/R/resident.R +++ b/R/resident.R @@ -386,7 +386,7 @@ resident_load <- function(model = c("flux2", "flux1", "zimage", "ltx", held <- tryCatch({ s <- torch::cuda_memory_stats(device = .cuda_index(device)) as.numeric(s$reserved_bytes$all$current) - - as.numeric(s$allocated_bytes$all$current) + as.numeric(s$allocated_bytes$all$current) }, error = function(e) 0) } if (!isTRUE(is.finite(held)) || held < 0) { From 57b9213954ae4bca5d82ced5b4df15d919cfd36d Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 24 Aug 2026 16:28:59 -0500 Subject: [PATCH 2/5] Point safetensors messaging at CRAN 0.3.0 instead of a GitHub build safetensors 0.3.0 reached CRAN on 2026-08-21 with all four fixes diffuseR had been routing users around: float8 (mlverse/safetensors#13), bfloat16 write (#11), the >2 GB offset overflow (#14), and empty tensor names (#10). Every message telling users to install a development build from GitHub was, as of that date, advising the harder path to a fix already sitting in `install.packages("safetensors")`. Five messages carried it: recommend()'s tier note and the graceful fp8/bf16 fallback (both via the shared helper), flux_quantize()'s two errors, and the >2 GB read breadcrumb. Plus README's tier table and the st_caps/reshard docs. THE PROBES ARE UNCHANGED, and that is the point. They were written as runtime capability probes rather than a version floor precisely so this day would need no code change, and they earned it: a 0.3.0 user got the higher tiers the moment they updated, with nothing in the package to adjust. They also still cover what a version test cannot -- the fixes existed for three weeks in builds reporting 0.2.1, so the version number never distinguished them. No floor added to Suggests for the same reason: nf4 works on older safetensors, so a stale install costs a tier, not the model. TESTS NOW PIN THE REMEDY, NOT JUST THE PHRASING. The existing assertions checked "best fit for your card", the absence of an em dash, and the file size in the breadcrumb -- everything except what the message tells a user to DO. That is how the advice went stale silently and would have stayed stale: nothing failed. Added guards that the messages name install.packages and do NOT mention a development version or GitHub, and checked the guards reject the old wording rather than merely passing on the new. .st_fork_note is renamed .st_update_note, since it no longer suggests a fork. Four call sites, all internal. recommend()'s returned `fork_suggested` field KEEPS its name. It is part of a documented return contract that memory_flux.R propagates and five tests read, so renaming it would break callers for a cosmetic gain. The docs now say what it means and that the name is historical. reshard_safetensors() is no longer required to make a large artifact readable. It stays useful for publishing: its shards load on every safetensors including older ones, which is what makes a hosted artifact safe to redistribute. NEWS records this as a new 0.2.2.7 entry rather than editing 0.2.2.3's rationale, which described the situation accurately when it was written. Suite 1220 assertions, 0 failures. --- DESCRIPTION | 2 +- NEWS.md | 30 +++++++++++++++++ R/quantize_flux.R | 19 +++++------ R/recommend.R | 12 ++++--- R/reshard.R | 15 ++++++--- R/st_caps.R | 61 +++++++++++++++++++--------------- README.md | 7 ++-- inst/tinytest/test_recommend.R | 21 ++++++++++-- man/recommend.Rd | 10 ++++-- man/reshard_safetensors.Rd | 17 +++++++--- man/st_caps.Rd | 27 +++++++++------ 11 files changed, 151 insertions(+), 70 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index d85c958..c047ea6 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: diffuseR Title: Functional Interface to Diffusion Models in R -Version: 0.2.2.6 +Version: 0.2.2.7 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 a7e85df..c52f4cf 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,33 @@ +# diffuseR 0.2.2.7 + +* safetensors 0.3.0 reached CRAN on 2026-08-21 with all four fixes + diffuseR had been routing users around: float8 support + (mlverse/safetensors#13), bfloat16 write (#11), the >2 GB offset + overflow (#14), and empty tensor names (#10). Every message that told + users to install a development build from GitHub now tells them to run + `install.packages("safetensors")`. That covers `recommend()`'s tier + note, the graceful fp8/bf16 fallback, both `flux_quantize()` errors, and + the >2 GB read breadcrumb. + + The capability probes are unchanged, and deliberately so. They were + written as runtime probes rather than a version floor precisely so this + day would need no code change, and they still cover what a version test + cannot: the fixes existed for three weeks in builds that reported 0.2.1. + No version floor has been added to `Suggests` for the same reason — nf4 + works on older safetensors, so a stale install costs a tier rather than + the model. + + `recommend()`'s returned `fork_suggested` field keeps its name, which is + now historical: it means the installed safetensors cannot read a tier + the card could otherwise run. Renaming it would break the returned + contract for a cosmetic gain. + +* `reshard_safetensors()` is no longer required to make a large artifact + readable, since 0.3.0 fixed the overflow it worked around. It stays + useful for publishing: the shards it writes load on every safetensors + including the older ones, which is what makes a hosted artifact safe to + redistribute. + # diffuseR 0.2.2.6 * Fixed an allocator pre-warm accumulation introduced in 0.2.2.4. diff --git a/R/quantize_flux.R b/R/quantize_flux.R index 7f36d64..cca193c 100644 --- a/R/quantize_flux.R +++ b/R/quantize_flux.R @@ -327,14 +327,14 @@ flux_quantize <- function(transformer_dir, output_dir = NULL, if (format == "fp8" && !.st_can_write("float8_e4m3fn")) { stop("The installed safetensors package cannot write float8 ", "tensors (needs the float8 support from ", - "mlverse/safetensors#13, which is merged upstream but not ", - "yet on CRAN; install the development version of ", - "safetensors from GitHub, or use format = \"nf4\").", - call. = FALSE) + "mlverse/safetensors#13, released in safetensors 0.3.0). ", + "Run install.packages(\"safetensors\") to update, or use ", + "format = \"nf4\".", call. = FALSE) } # Residents load into the compute dtype either way; bf16 halves the - # artifact but the CRAN build of safetensors 0.2.1 cannot write it - # (the fix is merged upstream; the probe decides, not the version) + # artifact but safetensors before 0.3.0 cannot write it. The probe + # decides, not the version: the fix existed for three weeks in builds + # still reporting 0.2.1. resident_dtype <- if (.st_can_write("bfloat16")) { torch::torch_bfloat16() } else { @@ -491,10 +491,9 @@ flux_load_transformer <- function(ckpt, device = "cuda", dtype = "bfloat16", format <- ckpt$format %||% "full" if (identical(format, "fp8") && !.st_can_write("float8_e4m3fn")) { stop("This fp8 artifact needs float8 support the installed ", - "safetensors lacks (mlverse/safetensors#13, merged upstream ", - "but not yet on CRAN). Install the development version of ", - "safetensors from GitHub, or rebuild the artifact as nf4.", - call. = FALSE) + "safetensors lacks (mlverse/safetensors#13, released in ", + "safetensors 0.3.0). Run install.packages(\"safetensors\") ", + "to update, or rebuild the artifact as nf4.", call. = FALSE) } hooks <- .flux_family_hooks(ckpt$config) diff --git a/R/recommend.R b/R/recommend.R index e91a051..52477d3 100644 --- a/R/recommend.R +++ b/R/recommend.R @@ -13,7 +13,7 @@ #' bf16) AND the installed safetensors can \emph{read} that dtype #' (\code{\link{.st_can_read}}), that tier is recommended instead. #' \item When the card has room but safetensors cannot read the tier, -#' nf4 is recommended and the fork suggestion is surfaced in +#' nf4 is recommended and the upgrade suggestion is surfaced in #' \code{note} (never an error). #' } #' @@ -53,8 +53,12 @@ #' logical), \code{max_pixels}, \code{text_device}, \code{attn_chunk}, #' \code{vram_gb}, \code{pin} (page-lock the phase-swapped host #' copies), \code{pinned_set_gb} (estimated pinned bytes), -#' \code{host_ram_gb}, \code{fork_suggested} (logical), and -#' \code{note} (the fork suggestion string, or NULL). +#' \code{host_ram_gb}, \code{fork_suggested} (logical: the installed +#' safetensors cannot read the tier the card could otherwise run), and +#' \code{note} (the suggestion string, or NULL). The field name is +#' historical -- the remedy used to be the cornball-ai fork and is now +#' \code{install.packages("safetensors")}, since the fixes reached CRAN +#' in 0.3.0 -- and is kept because it is part of the returned contract. #' #' @export #' @@ -137,7 +141,7 @@ recommend <- function(model = c("sd21", "sdxl", "flux1", "flux2", "zimage", host_ram_gb = host_ram_gb, fork_suggested = fork, note = if (fork) { - .st_fork_note(want$precision) + .st_update_note(want$precision) } else { .bf16_note(model, chosen$precision) } diff --git a/R/reshard.R b/R/reshard.R index c28afc5..017dfcd 100644 --- a/R/reshard.R +++ b/R/reshard.R @@ -2,13 +2,18 @@ #' #' Splits a single \code{.safetensors} file into diffusers-style shards #' (\code{-00001-of-000NN.safetensors} plus a -#' \code{.safetensors.index.json} weight map) so it loads on stock -#' CRAN safetensors, which overflows a 32-bit offset on any file at or -#' above 2^31 bytes. Reading the oversize source requires a -#' fork-patched safetensors (a build-machine step); the shards it writes -#' are fork-free to read. Used to host large fp16 diffusers weights +#' \code{.safetensors.index.json} weight map) so it loads on +#' safetensors older than 0.3.0, which overflows a 32-bit offset on any +#' file at or above 2^31 bytes. Used to host large fp16 diffusers weights #' (e.g. the 5 GB SDXL UNet) unchanged, without quantization. #' +#' safetensors 0.3.0 fixed the overflow (mlverse/safetensors#14), so +#' reading an oversize source no longer needs a patched build and this is +#' no longer required to produce a readable artifact. It stays useful for +#' two reasons: the shards it writes load on every safetensors including +#' the older ones, which is what makes a hosted artifact safe to publish, +#' and sharded weights stream rather than materialising one multi-GB file. +#' #' @param input Path to the source \code{.safetensors} file, or a #' directory containing \code{.safetensors}. #' @param output_dir Output directory for the shards + index. diff --git a/R/st_caps.R b/R/st_caps.R index f546b8b..619807b 100644 --- a/R/st_caps.R +++ b/R/st_caps.R @@ -1,11 +1,17 @@ -#' safetensors read-capability probes and fork messaging +#' safetensors read-capability probes and upgrade messaging #' -#' The CRAN build of safetensors 0.2.1 reads bfloat16 but cannot write -#' it, and has no float8 support at all. Both fixes merged upstream on -#' 2026-07-31 (mlverse/safetensors#11 for bfloat16 write, #13 for -#' float8) without a version bump, so the installed version number -#' cannot tell you which build you have. That is why every gate here is -#' a runtime probe: write a tiny tensor, read it back, cache the answer. +#' safetensors 0.2.1 reads bfloat16 but cannot write it, and has no +#' float8 support at all. Both fixes (mlverse/safetensors#11 for bfloat16 +#' write, #13 for float8) merged upstream on 2026-07-31 and reached CRAN +#' in safetensors 0.3.0 on 2026-08-21, along with #14 for the >2 GB +#' offset overflow and #10 for empty tensor names. +#' +#' The gates here stayed runtime probes rather than a version floor, and +#' that decision is what let the fork requirement retire itself the day +#' the release landed: nothing had to change for a 0.3.0 user to get the +#' higher tiers. It also still covers the case a version test cannot -- +#' the fixes existed for three weeks in builds that reported 0.2.1, so +#' the version number never distinguished them. Probe, do not pin. #' Two capabilities matter and they differ: #' #' \itemize{ @@ -20,8 +26,9 @@ #' load. #' } #' -#' Both are capability-probed, never version-pinned, so the fork -#' requirement self-heals the day the fixes reach CRAN. +#' Both are capability-probed, never version-pinned. That is why the fork +#' requirement healed itself when 0.3.0 shipped, and why it will do the +#' same for whatever lands next. #' #' @name st_caps NULL @@ -95,12 +102,12 @@ NULL ok } -# The standard "install the fork, or press on with nf4" message. Shared +# The standard "update safetensors, or press on with nf4" message. Shared # by the recommender (read side, fit = TRUE: "best fit for your card") # and the download graceful-fallback path (write side, fit = FALSE, since # the user asked for it outright) so the wording stays identical # everywhere. No em dashes (house style). -.st_fork_note <- function(precision, fit = TRUE) { +.st_update_note <- function(precision, fit = TRUE) { precision <- as.character(precision) detail <- switch(precision, fp8 = "float8 support (mlverse/safetensors#13)", @@ -113,16 +120,16 @@ NULL } else { sprintf("%s needs", precision) } - sprintf(paste0("%s a safetensors newer than the one on CRAN: %s is ", - "merged upstream but not yet released. Install the ", - "development version from the mlverse/safetensors ", - "repository on GitHub, or press on with nf4: same ", - "weights, slightly lower precision, and it just works."), + sprintf(paste0("%s a newer safetensors than the one installed: %s ", + "reached CRAN in safetensors 0.3.0. Run ", + "install.packages(\"safetensors\") to update, or press ", + "on with nf4: same weights, slightly lower precision, ", + "and it just works."), lead, detail) } # When a user explicitly asks for fp8/bf16 but the needed safetensors -# capability is missing, print the fork suggestion and fall back to nf4 +# capability is missing, print the upgrade suggestion and fall back to nf4 # instead of letting a downstream builder or loader fail. nf4, fp16, # fp32 and anything unrecognized pass through untouched. `mode` selects # the capability that matters: "write" when about to BUILD an artifact, @@ -144,7 +151,7 @@ NULL return(precision) } if (verbose) { - message(.st_fork_note(precision, fit = FALSE), + message(.st_update_note(precision, fit = FALSE), "\nFalling back to nf4 for now.") } "nf4" @@ -154,21 +161,21 @@ NULL # .st_read_or_breadcrumb so it can be unit-tested without a real 2 GB # file. .st_overflow_message <- function(file_path, size_bytes, underlying) { - sprintf(paste0("Could not read %s (%.1f GB). Stock CRAN safetensors ", - "overflows a 32-bit offset on files at or above 2^31 ", - "bytes (~2.15 GB). Rebuild the artifact with smaller ", - "shards (the quantizers now default to ", - "shard_bytes = 1.9e9), or install the development ", - "version of safetensors from the mlverse/safetensors ", - "repository on GitHub, where the fix is merged. ", + sprintf(paste0("Could not read %s (%.1f GB). safetensors before ", + "0.3.0 overflows a 32-bit offset on files at or above ", + "2^31 bytes (~2.15 GB). Run ", + "install.packages(\"safetensors\") to update, or ", + "rebuild the artifact with smaller shards (the ", + "quantizers default to shard_bytes = 1.9e9). ", "Underlying error: %s"), basename(file_path), size_bytes / 1e9, underlying) } # Run a safetensors read; if it fails AND the backing shard is at/above # the 2^31-byte ceiling, translate the cryptic overflow into the -# fork-or-smaller-shards breadcrumb. A read that succeeds (fork, or a -# sub-2 GB shard) is untouched; a failure on a small shard rethrows +# update-or-smaller-shards breadcrumb. A read that succeeds (safetensors +# 0.3.0+, or a sub-2 GB shard) is untouched; a failure on a small shard +# rethrows # verbatim. Reactive by design, so it never false-alarms on a machine # that can read large files. .st_read_or_breadcrumb <- function(read_fn, file_path = NULL) { diff --git a/README.md b/README.md index 7b2b4e7..36ade78 100644 --- a/README.md +++ b/README.md @@ -232,9 +232,10 @@ shard size, not the dtype, is what gates readability. Higher-quality tiers behave as follows: - **bf16** (24 GB+ cards) is also CRAN-readable. -- **fp8** (the 12-16 GB sweet spot) needs the - [`cornball-ai/safetensors`](https://github.com/cornball-ai/safetensors) - fork until float8 support lands on CRAN (mlverse/safetensors#13). +- **fp8** (the 12-16 GB sweet spot) needs safetensors 0.3.0 or newer, + which added float8 support (mlverse/safetensors#13). Older versions + still read nf4, so a stale safetensors costs you a tier rather than + the model. `recommend(model)` picks the right tier for your VRAM and the safetensors you have installed, and asking for a tier your safetensors diff --git a/inst/tinytest/test_recommend.R b/inst/tinytest/test_recommend.R index 41c2881..bf8376f 100644 --- a/inst/tinytest/test_recommend.R +++ b/inst/tinytest/test_recommend.R @@ -143,19 +143,36 @@ expect_message(rr2 <- grc("bf16", "read"), pattern = "safetensors#11") expect_equal(rr2, "nf4") options(diffuseR.st_read_caps = NULL) -# --- fork note fit parameter ------------------------------------------------------ +# --- upgrade note fit parameter --------------------------------------------------- -fn <- diffuseR:::.st_fork_note +fn <- diffuseR:::.st_update_note expect_true(grepl("best fit for your card", fn("fp8", fit = TRUE))) expect_false(grepl("best fit for your card", fn("fp8", fit = FALSE))) expect_false(grepl("—", fn("fp8"))) # no em dash, either variant +# The REMEDY, not just the phrasing. Nothing here asserted what the +# message tells a user to DO, which is how it went on recommending a +# GitHub development build for three days after safetensors 0.3.0 shipped +# the fix to CRAN. A message can be well-formed and still wrong. +for (v in list(fn("fp8"), fn("bf16", fit = FALSE))) { + expect_true(grepl("install.packages", v, fixed = TRUE)) + # Regression guard: if this ever points back at a development build, + # the advice has gone stale again. + expect_false(grepl("development version", v, fixed = TRUE)) + expect_false(grepl("GitHub", v, fixed = TRUE)) +} + # --- multi-GB read breadcrumb ----------------------------------------------------- msg <- diffuseR:::.st_overflow_message("shard-00001.safetensors", 3.4e9, "boom") expect_true(grepl("3.4 GB", msg)) expect_true(grepl("2\\^31", msg)) expect_true(grepl("shard-00001", msg)) +# Same guard as above: the overflow breadcrumb carried the same stale +# GitHub advice, and nothing caught it. +expect_true(grepl("install.packages", msg, fixed = TRUE)) +expect_false(grepl("development version", msg, fixed = TRUE)) +expect_false(grepl("GitHub", msg, fixed = TRUE)) brc <- diffuseR:::.st_read_or_breadcrumb # a read that succeeds is returned untouched diff --git a/man/recommend.Rd b/man/recommend.Rd index b00f9b1..6413036 100644 --- a/man/recommend.Rd +++ b/man/recommend.Rd @@ -30,8 +30,12 @@ A list with \code{model}, \code{precision}, \code{devices} logical), \code{max_pixels}, \code{text_device}, \code{attn_chunk}, \code{vram_gb}, \code{pin} (page-lock the phase-swapped host copies), \code{pinned_set_gb} (estimated pinned bytes), - \code{host_ram_gb}, \code{fork_suggested} (logical), and - \code{note} (the fork suggestion string, or NULL). + \code{host_ram_gb}, \code{fork_suggested} (logical: the installed + safetensors cannot read the tier the card could otherwise run), and + \code{note} (the suggestion string, or NULL). The field name is + historical -- the remedy used to be the cornball-ai fork and is now + \code{install.packages("safetensors")}, since the fixes reached CRAN + in 0.3.0 -- and is kept because it is part of the returned contract. } \description{ One VRAM-and-capability-aware recommendation for every diffuseR @@ -48,7 +52,7 @@ floor is fp16 with placement varying by VRAM. bf16) AND the installed safetensors can \emph{read} that dtype (\code{\link{.st_can_read}}), that tier is recommended instead. \item When the card has room but safetensors cannot read the tier, -nf4 is recommended and the fork suggestion is surfaced in +nf4 is recommended and the upgrade suggestion is surfaced in \code{note} (never an error). } diff --git a/man/reshard_safetensors.Rd b/man/reshard_safetensors.Rd index ef470d1..e6e399e 100644 --- a/man/reshard_safetensors.Rd +++ b/man/reshard_safetensors.Rd @@ -31,10 +31,17 @@ Invisibly, the path to the written index.json. \description{ Splits a single \code{.safetensors} file into diffusers-style shards (\code{-00001-of-000NN.safetensors} plus a -\code{.safetensors.index.json} weight map) so it loads on stock -CRAN safetensors, which overflows a 32-bit offset on any file at or -above 2^31 bytes. Reading the oversize source requires a -fork-patched safetensors (a build-machine step); the shards it writes -are fork-free to read. Used to host large fp16 diffusers weights +\code{.safetensors.index.json} weight map) so it loads on +safetensors older than 0.3.0, which overflows a 32-bit offset on any +file at or above 2^31 bytes. Used to host large fp16 diffusers weights (e.g. the 5 GB SDXL UNet) unchanged, without quantization. } +\details{ +safetensors 0.3.0 fixed the overflow (mlverse/safetensors#14), so +reading an oversize source no longer needs a patched build and this is +no longer required to produce a readable artifact. It stays useful for +two reasons: the shards it writes load on every safetensors including +the older ones, which is what makes a hosted artifact safe to publish, +and sharded weights stream rather than materialising one multi-GB file. + +} diff --git a/man/st_caps.Rd b/man/st_caps.Rd index d79ec34..d3fdd05 100644 --- a/man/st_caps.Rd +++ b/man/st_caps.Rd @@ -1,17 +1,23 @@ % tinyrox says don't edit this manually, but it can't stop you! \name{st_caps} \alias{st_caps} -\title{safetensors read-capability probes and fork messaging} +\title{safetensors read-capability probes and upgrade messaging} \description{ -The CRAN build of safetensors 0.2.1 reads bfloat16 but cannot write -it, and has no float8 support at all. Both fixes merged upstream on -2026-07-31 (mlverse/safetensors#11 for bfloat16 write, #13 for -float8) without a version bump, so the installed version number -cannot tell you which build you have. That is why every gate here is -a runtime probe: write a tiny tensor, read it back, cache the answer. -Two capabilities matter and they differ: +safetensors 0.2.1 reads bfloat16 but cannot write it, and has no +float8 support at all. Both fixes (mlverse/safetensors#11 for bfloat16 +write, #13 for float8) merged upstream on 2026-07-31 and reached CRAN +in safetensors 0.3.0 on 2026-08-21, along with #14 for the >2 GB +offset overflow and #10 for empty tensor names. } \details{ +The gates here stayed runtime probes rather than a version floor, and +that decision is what let the fork requirement retire itself the day +the release landed: nothing had to change for a 0.3.0 user to get the +higher tiers. It also still covers the case a version test cannot -- +the fixes existed for three weeks in builds that reported 0.2.1, so +the version number never distinguished them. Probe, do not pin. +Two capabilities matter and they differ: + \itemize{ \item \emph{write} (\code{\link{flux_quantize}}'s internal \code{.st_can_write}, in quantize_flux.R): needed to BUILD a @@ -24,7 +30,8 @@ probe is the wrong signal for whether a hosted bf16 artifact will load. } -Both are capability-probed, never version-pinned, so the fork -requirement self-heals the day the fixes reach CRAN. +Both are capability-probed, never version-pinned. That is why the fork +requirement healed itself when 0.3.0 shipped, and why it will do the +same for whatever lands next. } From f39b163fdf0a670205bb26e15f55045989e76bee Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 24 Aug 2026 18:00:42 -0500 Subject: [PATCH 3/5] Finish the safetensors 0.3.0 sweep: docs, and guards with teeth The first pass fixed the messages and left the documentation, which is where most of the stale advice actually lived. The shard_bytes help for flux_quantize(), ltx23_quantize_nf4(), ltx23_quantize_fp8() and gemma3_quantize_nf4() still described the 1.9e9 default as what "stock CRAN safetensors" can read, and told users to install a fork for anything larger. README, the performance-levers vignette, and the unet_safetensors / download_prebuilt / convert_sd21_pt_to_diffusers pages carried variants of the same. All now describe the floor as "older than 0.3.0" and name install.packages("safetensors") as the remedy. st_caps.R contradicted itself: the opening said safetensors 0.2.1 lacks the fixes, while the explanation below correctly noted that fixed development builds also reported 0.2.1. The opening now says the CRAN 0.2.1 release, which is what was meant, and the probe-don't-pin rationale survives intact. The two direct fp8 errors had their wording corrected last commit but nothing asserted it: both tests only matched "float8", so the old GitHub-development-build advice would have passed. Both gates now assert the exact CRAN remedy and reject "GitHub" and "development version". Checked that these guards are not vacuous by running all four pre-change messages through them: every one fails all three assertions. The load-path fixture needs the class. flux_load_transformer() runs stopifnot(inherits(ckpt, "ltx23_checkpoint")) before the fp8 gate, so a bare list(format = "fp8") errors on the stopifnot and never reaches the message under test. Suite 1229 assertions, 0 failures. man/ regenerates byte-identical from source. Historical NEWS entries left alone. --- CLAUDE.md | 3 ++- NEWS.md | 13 +++++++++- R/convert_sd_pt.R | 2 +- R/download_prebuilt.R | 4 +-- R/fp8_ltx23.R | 7 +++--- R/nf4_ltx23.R | 7 +++--- R/quantize_flux.R | 14 +++++------ R/quantize_gemma3.R | 2 +- R/recommend.R | 2 +- R/st_caps.R | 20 +++++++-------- R/unet_safetensors.R | 4 +-- README.md | 11 ++++---- inst/tinytest/test_recommend.R | 4 +-- inst/tinytest/test_st_caps.R | 39 +++++++++++++++++++++++------ man/convert_sd21_pt_to_diffusers.Rd | 2 +- man/download_prebuilt.Rd | 4 +-- man/flux_quantize.Rd | 8 +++--- man/gemma3_quantize_nf4.Rd | 2 +- man/ltx23_quantize_fp8.Rd | 7 +++--- man/ltx23_quantize_nf4.Rd | 7 +++--- man/recommend.Rd | 2 +- man/st_caps.Rd | 16 ++++++------ man/unet_safetensors.Rd | 4 +-- vignettes/performance-levers.md | 2 +- 24 files changed, 113 insertions(+), 73 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 24f72a3..50d36d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -299,7 +299,8 @@ Native torch modules will load weights directly from HuggingFace safetensors for model <- load_from_hf("stabilityai/stable-diffusion-xl-base-1.0") ``` -See cornyverse CLAUDE.md for safetensors package setup (use cornball-ai fork until PR merged). +See cornyverse CLAUDE.md for safetensors package setup. CRAN safetensors +0.3.0 contains the required fixes; keep downstream gates capability-probed. ## Roadmap diff --git a/NEWS.md b/NEWS.md index c52f4cf..fcf24c3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -9,11 +9,22 @@ note, the graceful fp8/bf16 fallback, both `flux_quantize()` errors, and the >2 GB read breadcrumb. + The same sweep reached the documentation, which is where most of the + stale advice actually was: the `shard_bytes` help for `flux_quantize()`, + `ltx23_quantize_nf4()`, `ltx23_quantize_fp8()` and + `gemma3_quantize_nf4()` described the 1.9e9 default as what "stock CRAN + safetensors" can read and pointed at a fork for anything larger. Those + now say the default keeps artifacts readable on safetensors older than + 0.3.0, and that larger shards need 0.3.0 or newer. `README.md`, + `vignette("performance-levers")`, and the `unet_safetensors`, + `download_prebuilt` and `convert_sd21_pt_to_diffusers` help pages got + the same treatment. + The capability probes are unchanged, and deliberately so. They were written as runtime probes rather than a version floor precisely so this day would need no code change, and they still cover what a version test cannot: the fixes existed for three weeks in builds that reported 0.2.1. - No version floor has been added to `Suggests` for the same reason — nf4 + No version floor has been added to `Suggests` for the same reason: nf4 works on older safetensors, so a stale install costs a tier rather than the model. diff --git a/R/convert_sd_pt.R b/R/convert_sd_pt.R index 181f058..5b614d4 100644 --- a/R/convert_sd_pt.R +++ b/R/convert_sd_pt.R @@ -13,7 +13,7 @@ #' CreativeML OpenRAIL++-M (redistributable), and cornball already hosts #' these weights as \code{.pt}. At \code{float16} the components are all #' sub-2 GB single files (unet ~1.7 GB, text_encoder ~0.65 GB, vae -#' ~0.16 GB), so they load on stock CRAN safetensors. +#' ~0.16 GB), so they also load on safetensors older than 0.3.0. #' #' @param pt_dir Directory holding \code{unet-cpu.pt}, #' \code{decoder-cpu.pt}, \code{text_encoder-cpu.pt} (default: the diff --git a/R/download_prebuilt.R b/R/download_prebuilt.R index 0bf8d09..a4e5d8f 100644 --- a/R/download_prebuilt.R +++ b/R/download_prebuilt.R @@ -3,8 +3,8 @@ #' 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 +#' \code{\link{flux_quantize}} (sub-2 GB shards, bf16 residents), so even +#' safetensors older than 0.3.0 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. diff --git a/R/fp8_ltx23.R b/R/fp8_ltx23.R index 71a5fdb..9241a36 100644 --- a/R/fp8_ltx23.R +++ b/R/fp8_ltx23.R @@ -113,9 +113,10 @@ ltx23_fp8_linear <- torch::nn_module( #' @param output_dir Output directory for shards + manifest; NULL (the #' default) resolves under tools::R_user_dir("diffuseR", "data"). #' @param shard_bytes Numeric. Target shard size in bytes. The default -#' 1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling that -#' stock CRAN safetensors can read. Pass a larger value (e.g. 4e9) only -#' for local builds you will read back with a fork-patched safetensors. +#' 1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling in +#' safetensors older than 0.3.0, so the artifact remains readable there. +#' Larger values (e.g. 4e9) require safetensors 0.3.0 or newer and are +#' best kept for local artifacts; use the default for redistribution. #' @param force Logical. Re-quantize even if a valid manifest exists. #' @param verbose Logical. #' diff --git a/R/nf4_ltx23.R b/R/nf4_ltx23.R index 5d1e25f..20cf1f8 100644 --- a/R/nf4_ltx23.R +++ b/R/nf4_ltx23.R @@ -259,9 +259,10 @@ ltx23_nf4_linear <- torch::nn_module( #' @param output_dir Output directory for shards + manifest; NULL (the #' default) resolves under tools::R_user_dir("diffuseR", "data"). #' @param shard_bytes Numeric. Target shard size in bytes. The default -#' 1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling that -#' stock CRAN safetensors can read. Pass a larger value (e.g. 4e9) only -#' for local builds you will read back with a fork-patched safetensors. +#' 1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling in +#' safetensors older than 0.3.0, so the artifact remains readable there. +#' Larger values (e.g. 4e9) require safetensors 0.3.0 or newer and are +#' best kept for local artifacts; use the default for redistribution. #' @param force Logical. Re-quantize even if a valid manifest exists. #' @param verbose Logical. #' diff --git a/R/quantize_flux.R b/R/quantize_flux.R index cca193c..9d68fa6 100644 --- a/R/quantize_flux.R +++ b/R/quantize_flux.R @@ -17,8 +17,8 @@ NULL } # 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; +# The CRAN safetensors 0.2.1 release can read bfloat16 but not write it, +# and has no float8 support; 0.3.0 includes both fixes. Cached per session; # options(diffuseR.st_caps = list(bfloat16 = FALSE, ...)) overrides for # tests. .st_caps <- new.env(parent = emptyenv()) @@ -49,7 +49,7 @@ NULL # 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 +# otherwise an fp8 artifact on a reader without float8 support # 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). @@ -310,10 +310,10 @@ NULL #' the per-format location under \code{tools::R_user_dir}). #' @param format "nf4" or "fp8". #' @param shard_bytes Numeric. Target shard size in bytes. The default -#' 1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling that -#' stock CRAN safetensors can read, so the artifact loads fork-free. -#' Pass a larger value (e.g. 4e9) only for local builds you will read -#' back with a fork-patched safetensors. +#' 1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling in +#' safetensors older than 0.3.0, so the artifact remains readable there. +#' Larger values (e.g. 4e9) require safetensors 0.3.0 or newer and are +#' best kept for local artifacts; use the default for redistribution. #' @param force Logical. Re-quantize even if a valid manifest exists. #' @param verbose Logical. #' diff --git a/R/quantize_gemma3.R b/R/quantize_gemma3.R index 25f6f98..8c30c5e 100644 --- a/R/quantize_gemma3.R +++ b/R/quantize_gemma3.R @@ -18,7 +18,7 @@ #' @param output_dir Output directory for shards + manifest (default: #' \code{gemma3-nf4} under \code{tools::R_user_dir}). #' @param shard_bytes Numeric. Target shard size in bytes; the 1.9e9 -#' default keeps shards readable by stock CRAN safetensors. +#' default keeps shards readable by safetensors older than 0.3.0. #' @param force Logical. Re-quantize even if a valid manifest exists. #' @param verbose Logical. #' diff --git a/R/recommend.R b/R/recommend.R index 52477d3..86d4af1 100644 --- a/R/recommend.R +++ b/R/recommend.R @@ -69,7 +69,7 @@ #' st_caps = list(bfloat16 = TRUE, float8_e4m3fn = FALSE)) #' r$precision # "nf4": fp8 fits the card, but cannot be read #' r$fork_suggested # TRUE -#' cat(r$note) # the fork-or-nf4 message +#' cat(r$note) # the update-or-nf4 message #' #' # Same card, once safetensors can read float8 #' recommend("flux1", vram_gb = 16, diff --git a/R/st_caps.R b/R/st_caps.R index 619807b..e05ba18 100644 --- a/R/st_caps.R +++ b/R/st_caps.R @@ -1,10 +1,10 @@ #' safetensors read-capability probes and upgrade messaging #' -#' safetensors 0.2.1 reads bfloat16 but cannot write it, and has no -#' float8 support at all. Both fixes (mlverse/safetensors#11 for bfloat16 -#' write, #13 for float8) merged upstream on 2026-07-31 and reached CRAN -#' in safetensors 0.3.0 on 2026-08-21, along with #14 for the >2 GB -#' offset overflow and #10 for empty tensor names. +#' The CRAN safetensors 0.2.1 release reads bfloat16 but cannot write it, +#' and has no float8 support at all. Both fixes (mlverse/safetensors#11 +#' for bfloat16 write, #13 for float8) merged upstream on 2026-07-31 and +#' reached CRAN in safetensors 0.3.0 on 2026-08-21, along with #14 for the +#' >2 GB offset overflow and #10 for empty tensor names. #' #' The gates here stayed runtime probes rather than a version floor, and #' that decision is what let the fork requirement retire itself the day @@ -21,9 +21,9 @@ #' \item \emph{read} (\code{.st_can_read}, here): needed to LOAD a #' hosted artifact in that dtype. This is the capability that gates #' user-facing recommendations. It is strictly weaker than write: -#' CRAN safetensors reads bfloat16 it cannot write, so the write -#' probe is the wrong signal for whether a hosted bf16 artifact will -#' load. +#' the CRAN 0.2.1 release reads bfloat16 it cannot write, so the +#' write probe is the wrong signal for whether a hosted bf16 artifact +#' will load. #' } #' #' Both are capability-probed, never version-pinned. That is why the fork @@ -35,13 +35,13 @@ NULL # Read-probe cache, keyed by dtype. Separate from quantize_flux.R's # `.st_caps` write cache: the same dtype can be readable but not -# writable (bfloat16 on CRAN), so the two must not share entries. +# writable (bfloat16 on CRAN 0.2.1), so the two must not share entries. .st_read_caps <- new.env(parent = emptyenv()) # Write a minimal 2-element safetensors file by hand: a u64 # little-endian header length, the JSON header, then the raw tensor # bytes. Deliberately does NOT go through safetensors::safe_save_file - -# that is the whole point, since a CRAN safetensors cannot WRITE +# that is the whole point, since CRAN safetensors 0.2.1 cannot WRITE # bfloat16 yet can READ it. Lets `.st_can_read` test read capability in # isolation from write capability. .st_write_min <- function(path, dtype_name, payload) { diff --git a/R/unet_safetensors.R b/R/unet_safetensors.R index be8a76d..860c10d 100644 --- a/R/unet_safetensors.R +++ b/R/unet_safetensors.R @@ -11,8 +11,8 @@ #' parameter is filled and no key or shape is left unmatched. #' #' Reads route through the shared sharded opener, so an oversize (>2 GB) -#' single-file checkpoint on stock CRAN safetensors surfaces the -#' actionable "rebuild with smaller shards or install the fork" message +#' single-file checkpoint on safetensors older than 0.3.0 surfaces the +#' actionable "update safetensors or rebuild with smaller shards" message #' rather than a raw 32-bit overflow. #' #' @name unet_safetensors diff --git a/README.md b/README.md index 36ade78..2159616 100644 --- a/README.md +++ b/README.md @@ -224,10 +224,11 @@ txt2img("a lighthouse at dusk", model_name = "flux2") ### Precision and safetensors The quantized transformers ship as **nf4** by default (packed uint8 + -float32 blocks). nf4 loads on **stock CRAN safetensors** because the -artifacts are written in sub-2 GB shards; R's safetensors overflows a -32-bit offset on any single file at or above 2^31 bytes (~2.15 GB), so -shard size, not the dtype, is what gates readability. +float32 blocks). nf4 loads on every safetensors release because the +artifacts are written in sub-2 GB shards. Releases before 0.3.0 overflow +a 32-bit offset on any single file at or above 2^31 bytes (~2.15 GB), so +the default shard size preserves backward compatibility; 0.3.0 and newer +can also read larger shards. Higher-quality tiers behave as follows: @@ -243,7 +244,7 @@ cannot read falls back to nf4 with a note rather than erroring: ```r recommend("flux2") # e.g. list(precision = "fp8", ...) on a 16 GB card -recommend("flux1")$note # the fork suggestion, when fp8/bf16 would fit but can't load +recommend("flux1")$note # the upgrade suggestion when fp8/bf16 would fit but cannot load ``` ## Supported Models diff --git a/inst/tinytest/test_recommend.R b/inst/tinytest/test_recommend.R index bf8376f..7b1540f 100644 --- a/inst/tinytest/test_recommend.R +++ b/inst/tinytest/test_recommend.R @@ -155,7 +155,7 @@ expect_false(grepl("—", fn("fp8"))) # no em dash, either variant # GitHub development build for three days after safetensors 0.3.0 shipped # the fix to CRAN. A message can be well-formed and still wrong. for (v in list(fn("fp8"), fn("bf16", fit = FALSE))) { - expect_true(grepl("install.packages", v, fixed = TRUE)) + expect_true(grepl('install.packages("safetensors")', v, fixed = TRUE)) # Regression guard: if this ever points back at a development build, # the advice has gone stale again. expect_false(grepl("development version", v, fixed = TRUE)) @@ -170,7 +170,7 @@ expect_true(grepl("2\\^31", msg)) expect_true(grepl("shard-00001", msg)) # Same guard as above: the overflow breadcrumb carried the same stale # GitHub advice, and nothing caught it. -expect_true(grepl("install.packages", msg, fixed = TRUE)) +expect_true(grepl('install.packages("safetensors")', msg, fixed = TRUE)) expect_false(grepl("development version", msg, fixed = TRUE)) expect_false(grepl("GitHub", msg, fixed = TRUE)) diff --git a/inst/tinytest/test_st_caps.R b/inst/tinytest/test_st_caps.R index ea3f7f9..987fab5 100644 --- a/inst/tinytest/test_st_caps.R +++ b/inst/tinytest/test_st_caps.R @@ -1,6 +1,6 @@ # safetensors capability gating: dtype write probes, precision "auto" -# resolution, and the quantizer's resident-dtype fallback for CRAN -# safetensors (no bfloat16 write, no float8). +# resolution, and the quantizer's resident-dtype fallback for the CRAN +# safetensors 0.2.1 release (no bfloat16 write, no float8). if (!requireNamespace("torch", quietly = TRUE) || !torch::torch_is_installed()) { exit_file("torch not fully installed") @@ -15,6 +15,12 @@ can_write <- diffuseR:::.st_can_write resolve_precision <- diffuseR:::.flux_resolve_precision # --- capability probe ------------------------------------------------------------- +expect_cran_remedy <- function(msg) { + expect_true(grepl('install.packages("safetensors")', msg, fixed = TRUE)) + expect_false(grepl("development version", msg, fixed = TRUE)) + expect_false(grepl("GitHub", msg, fixed = TRUE)) + invisible(msg) +} expect_true(is.logical(can_write("bfloat16"))) expect_true(is.logical(can_write("float8_e4m3fn"))) @@ -34,7 +40,7 @@ expect_equal(resolve_precision("nf4"), "nf4") expect_equal(resolve_precision("fp8"), "fp8") # An existing artifact wins, fp8 preferred. Pin the fp8 capability so -# the assertion is hermetic: on stock CRAN safetensors the ambient probe +# the assertion is hermetic: on CRAN safetensors 0.2.1 the ambient probe # is FALSE and resolve_precision correctly refuses the fp8 artifact. options(diffuseR.st_caps = list(float8_e4m3fn = TRUE)) prefix <- file.path(tempdir(), "stcaps-test-") @@ -56,8 +62,8 @@ 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. +# An fp8 artifact present but unreadable (CRAN safetensors 0.2.1) is NOT +# selected - it would fail at read time. Only nf4 is safe there. dir.create(fp8_dir, showWarnings = FALSE) writeLines("{}", file.path(fp8_dir, "manifest.json")) options(diffuseR.st_caps = list(float8_e4m3fn = FALSE)) @@ -68,7 +74,7 @@ expect_equal(resolve_precision("auto", prefix), "fp8") options(diffuseR.st_caps = NULL) unlink(fp8_dir, recursive = TRUE) -# --- quantizer gates (tiny checkpoint, CRAN-safetensors emulation) ------------------ +# --- quantizer gates (tiny checkpoint, CRAN 0.2.1 emulation) ------------------------- ckpt_dir <- system.file("tinytest", "fixtures", "zimage_tiny_ckpt", package = "diffuseR") @@ -78,11 +84,28 @@ 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( +fp8_err <- tryCatch( flux_quantize(ckpt_dir, file.path(tempdir(), "stcaps-fp8"), format = "fp8", verbose = FALSE), - pattern = "float8" + error = function(e) conditionMessage(e) +) +expect_true(is.character(fp8_err)) +expect_true(grepl("float8", fp8_err, fixed = TRUE)) +expect_cran_remedy(fp8_err) + +# The load-path gate has its own hand-written error and must carry the +# same remedy even when this installation cannot build a real fp8 artifact. +# The fixture must carry the class: flux_load_transformer() checks +# inherits(ckpt, "ltx23_checkpoint") BEFORE the fp8 gate, so a bare list +# errors on the stopifnot and never reaches the message under test. +load_ckpt <- structure(list(format = "fp8"), class = "ltx23_checkpoint") +load_err <- tryCatch( + flux_load_transformer(load_ckpt, device = "cpu", verbose = FALSE), + error = function(e) conditionMessage(e) ) +expect_true(is.character(load_err)) +expect_true(grepl("float8", load_err, fixed = TRUE)) +expect_cran_remedy(load_err) # NF4 quantization falls back to float32 residents nf4_out <- file.path(tempdir(), "stcaps-nf4") diff --git a/man/convert_sd21_pt_to_diffusers.Rd b/man/convert_sd21_pt_to_diffusers.Rd index 49606b2..fe9faa2 100644 --- a/man/convert_sd21_pt_to_diffusers.Rd +++ b/man/convert_sd21_pt_to_diffusers.Rd @@ -41,6 +41,6 @@ provenance-clean way to build the hosted artifact: the upstream CreativeML OpenRAIL++-M (redistributable), and cornball already hosts these weights as \code{.pt}. At \code{float16} the components are all sub-2 GB single files (unet ~1.7 GB, text_encoder ~0.65 GB, vae -~0.16 GB), so they load on stock CRAN safetensors. +~0.16 GB), so they also load on safetensors older than 0.3.0. } diff --git a/man/download_prebuilt.Rd b/man/download_prebuilt.Rd index aa5e105..b0f772f 100644 --- a/man/download_prebuilt.Rd +++ b/man/download_prebuilt.Rd @@ -6,8 +6,8 @@ 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 +\code{\link{flux_quantize}} (sub-2 GB shards, bf16 residents), so even +safetensors older than 0.3.0 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. diff --git a/man/flux_quantize.Rd b/man/flux_quantize.Rd index cfa3aac..6e767bf 100644 --- a/man/flux_quantize.Rd +++ b/man/flux_quantize.Rd @@ -21,10 +21,10 @@ the per-format location under \code{tools::R_user_dir}).} \item{format}{"nf4" or "fp8".} \item{shard_bytes}{Numeric. Target shard size in bytes. The default -1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling that -stock CRAN safetensors can read, so the artifact loads fork-free. -Pass a larger value (e.g. 4e9) only for local builds you will read -back with a fork-patched safetensors.} +1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling in +safetensors older than 0.3.0, so the artifact remains readable there. +Larger values (e.g. 4e9) require safetensors 0.3.0 or newer and are +best kept for local artifacts; use the default for redistribution.} \item{force}{Logical. Re-quantize even if a valid manifest exists.} diff --git a/man/gemma3_quantize_nf4.Rd b/man/gemma3_quantize_nf4.Rd index 0195f38..934ef62 100644 --- a/man/gemma3_quantize_nf4.Rd +++ b/man/gemma3_quantize_nf4.Rd @@ -19,7 +19,7 @@ model-*.safetensors).} \code{gemma3-nf4} under \code{tools::R_user_dir}).} \item{shard_bytes}{Numeric. Target shard size in bytes; the 1.9e9 -default keeps shards readable by stock CRAN safetensors.} +default keeps shards readable by safetensors older than 0.3.0.} \item{force}{Logical. Re-quantize even if a valid manifest exists.} diff --git a/man/ltx23_quantize_fp8.Rd b/man/ltx23_quantize_fp8.Rd index 21a2ad9..fa06826 100644 --- a/man/ltx23_quantize_fp8.Rd +++ b/man/ltx23_quantize_fp8.Rd @@ -18,9 +18,10 @@ ltx23_quantize_fp8( default) resolves under tools::R_user_dir("diffuseR", "data").} \item{shard_bytes}{Numeric. Target shard size in bytes. The default -1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling that -stock CRAN safetensors can read. Pass a larger value (e.g. 4e9) only -for local builds you will read back with a fork-patched safetensors.} +1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling in +safetensors older than 0.3.0, so the artifact remains readable there. +Larger values (e.g. 4e9) require safetensors 0.3.0 or newer and are +best kept for local artifacts; use the default for redistribution.} \item{force}{Logical. Re-quantize even if a valid manifest exists.} diff --git a/man/ltx23_quantize_nf4.Rd b/man/ltx23_quantize_nf4.Rd index 2406f22..64c7dd0 100644 --- a/man/ltx23_quantize_nf4.Rd +++ b/man/ltx23_quantize_nf4.Rd @@ -18,9 +18,10 @@ ltx23_quantize_nf4( default) resolves under tools::R_user_dir("diffuseR", "data").} \item{shard_bytes}{Numeric. Target shard size in bytes. The default -1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling that -stock CRAN safetensors can read. Pass a larger value (e.g. 4e9) only -for local builds you will read back with a fork-patched safetensors.} +1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling in +safetensors older than 0.3.0, so the artifact remains readable there. +Larger values (e.g. 4e9) require safetensors 0.3.0 or newer and are +best kept for local artifacts; use the default for redistribution.} \item{force}{Logical. Re-quantize even if a valid manifest exists.} diff --git a/man/recommend.Rd b/man/recommend.Rd index 6413036..95d08b4 100644 --- a/man/recommend.Rd +++ b/man/recommend.Rd @@ -85,7 +85,7 @@ r <- recommend("flux1", vram_gb = 16, st_caps = list(bfloat16 = TRUE, float8_e4m3fn = FALSE)) r$precision # "nf4": fp8 fits the card, but cannot be read r$fork_suggested # TRUE -cat(r$note) # the fork-or-nf4 message +cat(r$note) # the update-or-nf4 message # Same card, once safetensors can read float8 recommend("flux1", vram_gb = 16, diff --git a/man/st_caps.Rd b/man/st_caps.Rd index d3fdd05..b559141 100644 --- a/man/st_caps.Rd +++ b/man/st_caps.Rd @@ -3,11 +3,11 @@ \alias{st_caps} \title{safetensors read-capability probes and upgrade messaging} \description{ -safetensors 0.2.1 reads bfloat16 but cannot write it, and has no -float8 support at all. Both fixes (mlverse/safetensors#11 for bfloat16 -write, #13 for float8) merged upstream on 2026-07-31 and reached CRAN -in safetensors 0.3.0 on 2026-08-21, along with #14 for the >2 GB -offset overflow and #10 for empty tensor names. +The CRAN safetensors 0.2.1 release reads bfloat16 but cannot write it, +and has no float8 support at all. Both fixes (mlverse/safetensors#11 +for bfloat16 write, #13 for float8) merged upstream on 2026-07-31 and +reached CRAN in safetensors 0.3.0 on 2026-08-21, along with #14 for the +>2 GB offset overflow and #10 for empty tensor names. } \details{ The gates here stayed runtime probes rather than a version floor, and @@ -25,9 +25,9 @@ quantized artifact in that dtype. \item \emph{read} (\code{.st_can_read}, here): needed to LOAD a hosted artifact in that dtype. This is the capability that gates user-facing recommendations. It is strictly weaker than write: -CRAN safetensors reads bfloat16 it cannot write, so the write -probe is the wrong signal for whether a hosted bf16 artifact will -load. +the CRAN 0.2.1 release reads bfloat16 it cannot write, so the +write probe is the wrong signal for whether a hosted bf16 artifact +will load. } Both are capability-probed, never version-pinned. That is why the fork diff --git a/man/unet_safetensors.Rd b/man/unet_safetensors.Rd index 649ec21..f191dc0 100644 --- a/man/unet_safetensors.Rd +++ b/man/unet_safetensors.Rd @@ -15,8 +15,8 @@ parameter is filled and no key or shape is left unmatched. } \details{ Reads route through the shared sharded opener, so an oversize (>2 GB) -single-file checkpoint on stock CRAN safetensors surfaces the -actionable "rebuild with smaller shards or install the fork" message +single-file checkpoint on safetensors older than 0.3.0 surfaces the +actionable "update safetensors or rebuild with smaller shards" message rather than a raw 32-bit overflow. } diff --git a/vignettes/performance-levers.md b/vignettes/performance-levers.md index f9b1c8b..a9eac35 100644 --- a/vignettes/performance-levers.md +++ b/vignettes/performance-levers.md @@ -122,7 +122,7 @@ r <- recommend("ltx") # or "flux1", "flux2", "zimage", "sdxl", "sd21" r$precision # tier the card + safetensors support r$devices # per-component placement r$pin # page-lock the phase-swapped host copies? -r$note # fork suggestion when fp8 wanted but unreadable +r$note # upgrade suggestion when fp8 wanted but unreadable ``` Treat the result as the machine's advice: pass its fields to the From baabb5bb69de3a3ca86767167ad3708271a0ae94 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 24 Aug 2026 18:58:33 -0500 Subject: [PATCH 4/5] Diagnose the capability that actually failed in the bf16 upgrade note recommend() gates tiers on .st_can_read(), but the note it produced for a blocked bf16 tier cited mlverse/safetensors#11, which is the bfloat16 WRITE fix. bfloat16 read worked on CRAN 0.2.1, so a reader that lacks it is not waiting on #11: the message was well-formed, carried the right remedy, and pointed at an issue unrelated to the user's failure. .st_update_note() now takes mode, and both call sites pass the capability they gated on: recommend() passes "read", .st_graceful_precision() forwards its own mode. The read path for bf16 drops the issue reference entirely, because no release added bfloat16 read and there is no fix to point at. float8 is untouched in both modes, since 0.2.1 had neither read nor write for it. The suite was holding this in place rather than catching it: its read-mode assertion matched "safetensors#11", so the wrong reference was pinned by the test. That assertion now matches the read wording. The remedy-only guards from the previous commit could not have caught this class of defect, which is the point of the added assertions: checked that both new ones fail against the old output while the remedy guard still passes it. Also stopped making the version the requirement in prose. The shard_bytes docs for flux_quantize(), ltx23_quantize_nf4(), ltx23_quantize_fp8(), plus reshard_safetensors() and the README fp8 bullet, said "requires safetensors 0.3.0 or newer". That contradicts the probe-don't-pin rationale in the same PR, which turns on capable builds having existed while reporting 0.2.1. They now name the fix and note where it landed. Suite 1242 assertions, 0 failures, re-run after the rformat pass against a fresh install. man/ regenerated from source. --- NEWS.md | 22 +++++++++++++++++ R/fp8_ltx23.R | 9 +++---- R/nf4_ltx23.R | 9 +++---- R/quantize_flux.R | 9 +++---- R/recommend.R | 3 ++- R/reshard.R | 6 ++--- R/st_caps.R | 44 +++++++++++++++++++++++++--------- README.md | 8 +++---- inst/tinytest/test_recommend.R | 37 ++++++++++++++++++++++++++-- man/flux_quantize.Rd | 9 +++---- man/ltx23_quantize_fp8.Rd | 9 +++---- man/ltx23_quantize_nf4.Rd | 9 +++---- man/reshard_safetensors.Rd | 6 ++--- 13 files changed, 132 insertions(+), 48 deletions(-) diff --git a/NEWS.md b/NEWS.md index fcf24c3..db3756e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,20 @@ # diffuseR 0.2.2.7 +* `recommend()` diagnosed the wrong safetensors capability for bf16. The + tier gate consults `.st_can_read()`, but the note it produced cited + mlverse/safetensors#11, which is the bfloat16 *write* fix. bfloat16 + read worked on CRAN 0.2.1, so a reader that lacks it is not waiting on + #11, and the message sent users to an unrelated issue. `.st_update_note()` + now takes `mode`, and both call sites pass the capability they actually + gated on. float8 is unchanged: 0.2.1 had neither read nor write, so #13 + is correct from both sides. + + The suite had been holding this in place. Its read-mode assertion + matched `"safetensors#11"`, so the wrong reference was pinned by the + test rather than caught by it. The remedy-only guards added earlier in + this release could not catch it either: the message named the right + package to install while blaming the wrong missing feature. + * safetensors 0.3.0 reached CRAN on 2026-08-21 with all four fixes diffuseR had been routing users around: float8 support (mlverse/safetensors#13), bfloat16 write (#11), the >2 GB offset @@ -20,6 +35,13 @@ `download_prebuilt` and `convert_sd21_pt_to_diffusers` help pages got the same treatment. + That prose now names the fix rather than the version: "needs the + overflow fix (mlverse/safetensors#14, which reached CRAN in 0.3.0)" + instead of "requires safetensors 0.3.0 or newer". Making the version + the requirement contradicted the probe-don't-pin rationale documented + two paragraphs down, which turns on capable builds having existed while + still reporting 0.2.1. + The capability probes are unchanged, and deliberately so. They were written as runtime probes rather than a version floor precisely so this day would need no code change, and they still cover what a version test diff --git a/R/fp8_ltx23.R b/R/fp8_ltx23.R index 9241a36..4a7fece 100644 --- a/R/fp8_ltx23.R +++ b/R/fp8_ltx23.R @@ -113,10 +113,11 @@ ltx23_fp8_linear <- torch::nn_module( #' @param output_dir Output directory for shards + manifest; NULL (the #' default) resolves under tools::R_user_dir("diffuseR", "data"). #' @param shard_bytes Numeric. Target shard size in bytes. The default -#' 1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling in -#' safetensors older than 0.3.0, so the artifact remains readable there. -#' Larger values (e.g. 4e9) require safetensors 0.3.0 or newer and are -#' best kept for local artifacts; use the default for redistribution. +#' 1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) offset +#' ceiling, so the artifact reads on a safetensors that lacks the +#' overflow fix (mlverse/safetensors#14, which reached CRAN in 0.3.0). +#' Larger values (e.g. 4e9) need that fix present, so keep them for +#' local artifacts and use the default for anything redistributed. #' @param force Logical. Re-quantize even if a valid manifest exists. #' @param verbose Logical. #' diff --git a/R/nf4_ltx23.R b/R/nf4_ltx23.R index 20cf1f8..ff47354 100644 --- a/R/nf4_ltx23.R +++ b/R/nf4_ltx23.R @@ -259,10 +259,11 @@ ltx23_nf4_linear <- torch::nn_module( #' @param output_dir Output directory for shards + manifest; NULL (the #' default) resolves under tools::R_user_dir("diffuseR", "data"). #' @param shard_bytes Numeric. Target shard size in bytes. The default -#' 1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling in -#' safetensors older than 0.3.0, so the artifact remains readable there. -#' Larger values (e.g. 4e9) require safetensors 0.3.0 or newer and are -#' best kept for local artifacts; use the default for redistribution. +#' 1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) offset +#' ceiling, so the artifact reads on a safetensors that lacks the +#' overflow fix (mlverse/safetensors#14, which reached CRAN in 0.3.0). +#' Larger values (e.g. 4e9) need that fix present, so keep them for +#' local artifacts and use the default for anything redistributed. #' @param force Logical. Re-quantize even if a valid manifest exists. #' @param verbose Logical. #' diff --git a/R/quantize_flux.R b/R/quantize_flux.R index 9d68fa6..4668c75 100644 --- a/R/quantize_flux.R +++ b/R/quantize_flux.R @@ -310,10 +310,11 @@ NULL #' the per-format location under \code{tools::R_user_dir}). #' @param format "nf4" or "fp8". #' @param shard_bytes Numeric. Target shard size in bytes. The default -#' 1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling in -#' safetensors older than 0.3.0, so the artifact remains readable there. -#' Larger values (e.g. 4e9) require safetensors 0.3.0 or newer and are -#' best kept for local artifacts; use the default for redistribution. +#' 1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) offset +#' ceiling, so the artifact reads on a safetensors that lacks the +#' overflow fix (mlverse/safetensors#14, which reached CRAN in 0.3.0). +#' Larger values (e.g. 4e9) need that fix present, so keep them for +#' local artifacts and use the default for anything redistributed. #' @param force Logical. Re-quantize even if a valid manifest exists. #' @param verbose Logical. #' diff --git a/R/recommend.R b/R/recommend.R index 86d4af1..5005067 100644 --- a/R/recommend.R +++ b/R/recommend.R @@ -141,7 +141,8 @@ recommend <- function(model = c("sd21", "sdxl", "flux1", "flux2", "zimage", host_ram_gb = host_ram_gb, fork_suggested = fork, note = if (fork) { - .st_update_note(want$precision) + # The tier was blocked by .st_can_read, so diagnose read. + .st_update_note(want$precision, mode = "read") } else { .bf16_note(model, chosen$precision) } diff --git a/R/reshard.R b/R/reshard.R index 017dfcd..7d32a00 100644 --- a/R/reshard.R +++ b/R/reshard.R @@ -2,9 +2,9 @@ #' #' Splits a single \code{.safetensors} file into diffusers-style shards #' (\code{-00001-of-000NN.safetensors} plus a -#' \code{.safetensors.index.json} weight map) so it loads on -#' safetensors older than 0.3.0, which overflows a 32-bit offset on any -#' file at or above 2^31 bytes. Used to host large fp16 diffusers weights +#' \code{.safetensors.index.json} weight map) so it loads on a +#' safetensors without the overflow fix, which trips a 32-bit offset on +#' any file at or above 2^31 bytes. Used to host large fp16 diffusers weights #' (e.g. the 5 GB SDXL UNet) unchanged, without quantization. #' #' safetensors 0.3.0 fixed the overflow (mlverse/safetensors#14), so diff --git a/R/st_caps.R b/R/st_caps.R index e05ba18..32597cb 100644 --- a/R/st_caps.R +++ b/R/st_caps.R @@ -103,23 +103,45 @@ NULL } # The standard "update safetensors, or press on with nf4" message. Shared -# by the recommender (read side, fit = TRUE: "best fit for your card") -# and the download graceful-fallback path (write side, fit = FALSE, since -# the user asked for it outright) so the wording stays identical -# everywhere. No em dashes (house style). -.st_update_note <- function(precision, fit = TRUE) { +# by the recommender (fit = TRUE: "best fit for your card") and the +# download graceful-fallback path (fit = FALSE, since the user asked for +# it outright) so the wording stays identical everywhere. No em dashes +# (house style). +# +# `mode` names the capability actually being diagnosed, and it is not +# cosmetic. bfloat16 READ worked on CRAN 0.2.1, so a build that cannot +# read bf16 is not waiting on mlverse/safetensors#11: that is the WRITE +# fix, and citing it on the read path sends the user to an issue that +# has nothing to do with their failure. The recommender gates on read +# (.st_can_read), so it was doing exactly that. float8 is unaffected +# either way, since 0.2.1 had neither read nor write for it. +.st_update_note <- function(precision, fit = TRUE, mode = c("read", "write")) { + mode <- match.arg(mode) precision <- as.character(precision) + lead <- if (fit) { + sprintf("%s is the best fit for your card but needs", precision) + } else { + sprintf("%s needs", precision) + } + is_bf16 <- precision %in% c("bf16", "bfloat16") + # No release "added" bfloat16 read, so there is no fix to point at + # and no version that makes it appear: a reader lacking it predates + # the capability rather than trailing a patch. + if (is_bf16 && mode == "read") { + return(sprintf(paste0("%s a safetensors that can read bfloat16, ", + "which the installed one cannot. Run ", + "install.packages(\"safetensors\") to ", + "update, or press on with nf4: same weights, ", + "slightly lower precision, and it just ", + "works."), + lead)) + } detail <- switch(precision, fp8 = "float8 support (mlverse/safetensors#13)", float8_e4m3fn = "float8 support (mlverse/safetensors#13)", bf16 = "bfloat16 write support (mlverse/safetensors#11)", bfloat16 = "bfloat16 write support (mlverse/safetensors#11)", paste0(precision, " support (mlverse/safetensors)")) - lead <- if (fit) { - sprintf("%s is the best fit for your card but needs", precision) - } else { - sprintf("%s needs", precision) - } sprintf(paste0("%s a newer safetensors than the one installed: %s ", "reached CRAN in safetensors 0.3.0. Run ", "install.packages(\"safetensors\") to update, or press ", @@ -151,7 +173,7 @@ NULL return(precision) } if (verbose) { - message(.st_update_note(precision, fit = FALSE), + message(.st_update_note(precision, fit = FALSE, mode = mode), "\nFalling back to nf4 for now.") } "nf4" diff --git a/README.md b/README.md index 2159616..4b6566b 100644 --- a/README.md +++ b/README.md @@ -233,10 +233,10 @@ can also read larger shards. Higher-quality tiers behave as follows: - **bf16** (24 GB+ cards) is also CRAN-readable. -- **fp8** (the 12-16 GB sweet spot) needs safetensors 0.3.0 or newer, - which added float8 support (mlverse/safetensors#13). Older versions - still read nf4, so a stale safetensors costs you a tier rather than - the model. +- **fp8** (the 12-16 GB sweet spot) needs float8 support + (mlverse/safetensors#13), which reached CRAN in safetensors 0.3.0. A + safetensors without it still reads nf4, so a stale install costs you a + tier rather than the model. `recommend(model)` picks the right tier for your VRAM and the safetensors you have installed, and asking for a tier your safetensors diff --git a/inst/tinytest/test_recommend.R b/inst/tinytest/test_recommend.R index 7b1540f..1aa5eba 100644 --- a/inst/tinytest/test_recommend.R +++ b/inst/tinytest/test_recommend.R @@ -137,9 +137,11 @@ expect_equal(rr, "nf4") options(diffuseR.st_caps = list(float8_e4m3fn = TRUE)) expect_equal(grc("fp8", "write"), "fp8") # present -> passes through options(diffuseR.st_caps = NULL) -# bf16 gate goes through the READ probe in read mode +# bf16 gate goes through the READ probe in read mode. The marker is the +# read wording, not "safetensors#11": this assertion used to match the +# write fix and so held the wrong diagnosis in place. options(diffuseR.st_read_caps = list(bfloat16 = FALSE)) -expect_message(rr2 <- grc("bf16", "read"), pattern = "safetensors#11") +expect_message(rr2 <- grc("bf16", "read"), pattern = "read bfloat16") expect_equal(rr2, "nf4") options(diffuseR.st_read_caps = NULL) @@ -162,6 +164,37 @@ for (v in list(fn("fp8"), fn("bf16", fit = FALSE))) { expect_false(grepl("GitHub", v, fixed = TRUE)) } +# --- the note must diagnose the capability that actually failed ------------------- + +# Asserting the remedy is not enough: a message can name the right fix to +# install and still blame the wrong missing feature. bfloat16 READ worked +# on CRAN 0.2.1, so mlverse/safetensors#11 (the WRITE fix) cannot be what +# a failed read gate is waiting on. recommend() gates on .st_can_read and +# was citing #11 anyway, which is a correct-looking sentence pointing at +# an unrelated issue. +expect_true(grepl("safetensors#11", fn("bf16", mode = "write"), fixed = TRUE)) +expect_false(grepl("safetensors#11", fn("bf16", mode = "read"), fixed = TRUE)) +expect_true(grepl("read bfloat16", fn("bf16", mode = "read"), fixed = TRUE)) + +# float8 is unaffected: 0.2.1 had neither read nor write, so #13 is the +# right reference from both sides. +expect_true(grepl("safetensors#13", fn("fp8", mode = "read"), fixed = TRUE)) +expect_true(grepl("safetensors#13", fn("fp8", mode = "write"), fixed = TRUE)) + +# Both modes still carry the remedy and the house style. +for (v in list(fn("bf16", mode = "read"), fn("bf16", mode = "write"))) { + expect_true(grepl('install.packages("safetensors")', v, fixed = TRUE)) + expect_false(grepl("GitHub", v, fixed = TRUE)) + expect_false(grepl("—", v)) +} + +# End to end: the recommender's read-gated bf16 note must not send the +# user to the write fix. +nob_note <- recommend("flux1", 24, + list(bfloat16 = FALSE, float8_e4m3fn = FALSE))$note +expect_false(grepl("safetensors#11", nob_note, fixed = TRUE)) +expect_true(grepl("read bfloat16", nob_note, fixed = TRUE)) + # --- multi-GB read breadcrumb ----------------------------------------------------- msg <- diffuseR:::.st_overflow_message("shard-00001.safetensors", 3.4e9, "boom") diff --git a/man/flux_quantize.Rd b/man/flux_quantize.Rd index 6e767bf..0065d9f 100644 --- a/man/flux_quantize.Rd +++ b/man/flux_quantize.Rd @@ -21,10 +21,11 @@ the per-format location under \code{tools::R_user_dir}).} \item{format}{"nf4" or "fp8".} \item{shard_bytes}{Numeric. Target shard size in bytes. The default -1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling in -safetensors older than 0.3.0, so the artifact remains readable there. -Larger values (e.g. 4e9) require safetensors 0.3.0 or newer and are -best kept for local artifacts; use the default for redistribution.} +1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) offset +ceiling, so the artifact reads on a safetensors that lacks the +overflow fix (mlverse/safetensors#14, which reached CRAN in 0.3.0). +Larger values (e.g. 4e9) need that fix present, so keep them for +local artifacts and use the default for anything redistributed.} \item{force}{Logical. Re-quantize even if a valid manifest exists.} diff --git a/man/ltx23_quantize_fp8.Rd b/man/ltx23_quantize_fp8.Rd index fa06826..20cef8a 100644 --- a/man/ltx23_quantize_fp8.Rd +++ b/man/ltx23_quantize_fp8.Rd @@ -18,10 +18,11 @@ ltx23_quantize_fp8( default) resolves under tools::R_user_dir("diffuseR", "data").} \item{shard_bytes}{Numeric. Target shard size in bytes. The default -1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling in -safetensors older than 0.3.0, so the artifact remains readable there. -Larger values (e.g. 4e9) require safetensors 0.3.0 or newer and are -best kept for local artifacts; use the default for redistribution.} +1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) offset +ceiling, so the artifact reads on a safetensors that lacks the +overflow fix (mlverse/safetensors#14, which reached CRAN in 0.3.0). +Larger values (e.g. 4e9) need that fix present, so keep them for +local artifacts and use the default for anything redistributed.} \item{force}{Logical. Re-quantize even if a valid manifest exists.} diff --git a/man/ltx23_quantize_nf4.Rd b/man/ltx23_quantize_nf4.Rd index 64c7dd0..9b1566d 100644 --- a/man/ltx23_quantize_nf4.Rd +++ b/man/ltx23_quantize_nf4.Rd @@ -18,10 +18,11 @@ ltx23_quantize_nf4( default) resolves under tools::R_user_dir("diffuseR", "data").} \item{shard_bytes}{Numeric. Target shard size in bytes. The default -1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) ceiling in -safetensors older than 0.3.0, so the artifact remains readable there. -Larger values (e.g. 4e9) require safetensors 0.3.0 or newer and are -best kept for local artifacts; use the default for redistribution.} +1.9e9 keeps every shard under the 2^31-byte (~2.15 GB) offset +ceiling, so the artifact reads on a safetensors that lacks the +overflow fix (mlverse/safetensors#14, which reached CRAN in 0.3.0). +Larger values (e.g. 4e9) need that fix present, so keep them for +local artifacts and use the default for anything redistributed.} \item{force}{Logical. Re-quantize even if a valid manifest exists.} diff --git a/man/reshard_safetensors.Rd b/man/reshard_safetensors.Rd index e6e399e..c758fd3 100644 --- a/man/reshard_safetensors.Rd +++ b/man/reshard_safetensors.Rd @@ -31,9 +31,9 @@ Invisibly, the path to the written index.json. \description{ Splits a single \code{.safetensors} file into diffusers-style shards (\code{-00001-of-000NN.safetensors} plus a -\code{.safetensors.index.json} weight map) so it loads on -safetensors older than 0.3.0, which overflows a 32-bit offset on any -file at or above 2^31 bytes. Used to host large fp16 diffusers weights +\code{.safetensors.index.json} weight map) so it loads on a +safetensors without the overflow fix, which trips a 32-bit offset on +any file at or above 2^31 bytes. Used to host large fp16 diffusers weights (e.g. the 5 GB SDXL UNet) unchanged, without quantization. } \details{ From f6ae3e24e7c3705d6f1d51d43f6da3e3a1ff39d2 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 24 Aug 2026 19:47:01 -0500 Subject: [PATCH 5/5] Finish the version-to-capability vocabulary sweep Four sites still described the missing capability as a version. The worst contradicted itself in adjacent sentences: flux_quantize()'s resident-dtype comment said "safetensors before 0.3.0 cannot write it. The probe decides, not the version: the fix existed for three weeks in builds still reporting 0.2.1." Both halves cannot be true. The others: the >2 GB read breadcrumb told users "safetensors before 0.3.0 overflows", unet_safetensors said "on safetensors older than 0.3.0", and .st_read_or_breadcrumb's comment used "safetensors 0.3.0+" as shorthand for a reader with the fix. All now name the capability and say where it landed. NEWS was narrating the iteration rather than the result: one paragraph said the docs now require 0.3.0 or newer, the next said that wording was replaced because it was wrong. Collapsed to the end state. Left alone deliberately: convert_sd_pt, quantize_gemma3 and download_prebuilt say sub-2 GB artifacts load on readers older than 0.3.0, which is a compatibility claim that holds for every build, capable or not. README says "releases before 0.3.0 overflow", which is accurate as scoped, since no release before 0.3.0 carried the fix. Suite 1242 assertions, 0 failures. The overflow breadcrumb still carries 2^31, the size, the shard name and the CRAN remedy. --- NEWS.md | 24 ++++++++++++------------ R/quantize_flux.R | 7 ++++--- R/st_caps.R | 16 ++++++++-------- R/unet_safetensors.R | 3 ++- man/unet_safetensors.Rd | 3 ++- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/NEWS.md b/NEWS.md index db3756e..04a8ae9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -28,19 +28,19 @@ stale advice actually was: the `shard_bytes` help for `flux_quantize()`, `ltx23_quantize_nf4()`, `ltx23_quantize_fp8()` and `gemma3_quantize_nf4()` described the 1.9e9 default as what "stock CRAN - safetensors" can read and pointed at a fork for anything larger. Those - now say the default keeps artifacts readable on safetensors older than - 0.3.0, and that larger shards need 0.3.0 or newer. `README.md`, - `vignette("performance-levers")`, and the `unet_safetensors`, - `download_prebuilt` and `convert_sd21_pt_to_diffusers` help pages got - the same treatment. - - That prose now names the fix rather than the version: "needs the + safetensors" can read and pointed at a fork for anything larger. + `README.md`, `vignette("performance-levers")`, `reshard_safetensors()` + and the `unet_safetensors`, `download_prebuilt` and + `convert_sd21_pt_to_diffusers` help pages carried variants of the same. + + All of it now names the capability rather than a version: "needs the overflow fix (mlverse/safetensors#14, which reached CRAN in 0.3.0)" - instead of "requires safetensors 0.3.0 or newer". Making the version - the requirement contradicted the probe-don't-pin rationale documented - two paragraphs down, which turns on capable builds having existed while - still reporting 0.2.1. + rather than "requires safetensors 0.3.0 or newer". That distinction is + the package's whole stance here, documented below: a version number + never separated a capable build from an incapable one, so prose making + 0.3.0 the requirement contradicts the probes it sits beside. The >2 GB + read breadcrumb and the bf16 resident-dtype message use the same + vocabulary. The capability probes are unchanged, and deliberately so. They were written as runtime probes rather than a version floor precisely so this diff --git a/R/quantize_flux.R b/R/quantize_flux.R index 4668c75..5c8cc83 100644 --- a/R/quantize_flux.R +++ b/R/quantize_flux.R @@ -333,9 +333,10 @@ flux_quantize <- function(transformer_dir, output_dir = NULL, "format = \"nf4\".", call. = FALSE) } # Residents load into the compute dtype either way; bf16 halves the - # artifact but safetensors before 0.3.0 cannot write it. The probe - # decides, not the version: the fix existed for three weeks in builds - # still reporting 0.2.1. + # artifact but a safetensors without the bfloat16 write fix + # (mlverse/safetensors#11, which reached CRAN in 0.3.0) cannot write + # it. The probe decides, not the version: the fix existed for three + # weeks in builds still reporting 0.2.1. resident_dtype <- if (.st_can_write("bfloat16")) { torch::torch_bfloat16() } else { diff --git a/R/st_caps.R b/R/st_caps.R index 32597cb..e5558a6 100644 --- a/R/st_caps.R +++ b/R/st_caps.R @@ -183,9 +183,10 @@ NULL # .st_read_or_breadcrumb so it can be unit-tested without a real 2 GB # file. .st_overflow_message <- function(file_path, size_bytes, underlying) { - sprintf(paste0("Could not read %s (%.1f GB). safetensors before ", - "0.3.0 overflows a 32-bit offset on files at or above ", - "2^31 bytes (~2.15 GB). Run ", + sprintf(paste0("Could not read %s (%.1f GB). A safetensors without ", + "the >2 GB offset fix (mlverse/safetensors#14, which ", + "reached CRAN in 0.3.0) overflows a 32-bit offset on ", + "files at or above 2^31 bytes (~2.15 GB). Run ", "install.packages(\"safetensors\") to update, or ", "rebuild the artifact with smaller shards (the ", "quantizers default to shard_bytes = 1.9e9). ", @@ -195,11 +196,10 @@ NULL # Run a safetensors read; if it fails AND the backing shard is at/above # the 2^31-byte ceiling, translate the cryptic overflow into the -# update-or-smaller-shards breadcrumb. A read that succeeds (safetensors -# 0.3.0+, or a sub-2 GB shard) is untouched; a failure on a small shard -# rethrows -# verbatim. Reactive by design, so it never false-alarms on a machine -# that can read large files. +# update-or-smaller-shards breadcrumb. A read that succeeds (a reader +# with the overflow fix, or a sub-2 GB shard) is untouched; a failure on +# a small shard rethrows verbatim. Reactive by design, so it never +# false-alarms on a machine that can read large files. .st_read_or_breadcrumb <- function(read_fn, file_path = NULL) { tryCatch(read_fn(), error = function(e) { sz <- if (!is.null(file_path)) { diff --git a/R/unet_safetensors.R b/R/unet_safetensors.R index 860c10d..7fa18ed 100644 --- a/R/unet_safetensors.R +++ b/R/unet_safetensors.R @@ -11,7 +11,8 @@ #' parameter is filled and no key or shape is left unmatched. #' #' Reads route through the shared sharded opener, so an oversize (>2 GB) -#' single-file checkpoint on safetensors older than 0.3.0 surfaces the +#' single-file checkpoint read by a safetensors without the overflow fix +#' (mlverse/safetensors#14, which reached CRAN in 0.3.0) surfaces the #' actionable "update safetensors or rebuild with smaller shards" message #' rather than a raw 32-bit overflow. #' diff --git a/man/unet_safetensors.Rd b/man/unet_safetensors.Rd index f191dc0..ad07a1d 100644 --- a/man/unet_safetensors.Rd +++ b/man/unet_safetensors.Rd @@ -15,7 +15,8 @@ parameter is filled and no key or shape is left unmatched. } \details{ Reads route through the shared sharded opener, so an oversize (>2 GB) -single-file checkpoint on safetensors older than 0.3.0 surfaces the +single-file checkpoint read by a safetensors without the overflow fix +(mlverse/safetensors#14, which reached CRAN in 0.3.0) surfaces the actionable "update safetensors or rebuild with smaller shards" message rather than a raw 32-bit overflow.