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/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..04a8ae9 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,3 +1,66 @@
+# 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
+ 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 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.
+ `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)"
+ 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
+ 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/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..4a7fece 100644
--- a/R/fp8_ltx23.R
+++ b/R/fp8_ltx23.R
@@ -113,9 +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 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) 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 5d1e25f..ff47354 100644
--- a/R/nf4_ltx23.R
+++ b/R/nf4_ltx23.R
@@ -259,9 +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 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) 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 7f36d64..5c8cc83 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,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 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) 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.
#'
@@ -327,14 +328,15 @@ 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 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 {
@@ -491,10 +493,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/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 e91a051..5005067 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
#'
@@ -65,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,
@@ -137,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_fork_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 c28afc5..7d32a00 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 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
+#' 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/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) {
diff --git a/R/st_caps.R b/R/st_caps.R
index f546b8b..e5558a6 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.
+#' 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
+#' 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{
@@ -15,26 +21,27 @@
#' \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, 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
# 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) {
@@ -95,34 +102,56 @@ NULL
ok
}
-# The standard "install the fork, 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) {
+# The standard "update safetensors, or press on with nf4" message. Shared
+# 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 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 +173,7 @@ NULL
return(precision)
}
if (verbose) {
- message(.st_fork_note(precision, fit = FALSE),
+ message(.st_update_note(precision, fit = FALSE, mode = mode),
"\nFalling back to nf4 for now.")
}
"nf4"
@@ -154,23 +183,23 @@ 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). 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). ",
"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
-# 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 be8a76d..7fa18ed 100644
--- a/R/unet_safetensors.R
+++ b/R/unet_safetensors.R
@@ -11,8 +11,9 @@
#' 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 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.
#'
#' @name unet_safetensors
diff --git a/README.md b/README.md
index 7b2b4e7..4b6566b 100644
--- a/README.md
+++ b/README.md
@@ -224,17 +224,19 @@ 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:
- **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 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
@@ -242,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 41c2881..1aa5eba 100644
--- a/inst/tinytest/test_recommend.R
+++ b/inst/tinytest/test_recommend.R
@@ -137,25 +137,75 @@ 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)
-# --- 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("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))
+ 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")
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("safetensors")', 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/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..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 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) 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/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..20cef8a 100644
--- a/man/ltx23_quantize_fp8.Rd
+++ b/man/ltx23_quantize_fp8.Rd
@@ -18,9 +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 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) 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 2406f22..9b1566d 100644
--- a/man/ltx23_quantize_nf4.Rd
+++ b/man/ltx23_quantize_nf4.Rd
@@ -18,9 +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 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) 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/recommend.Rd b/man/recommend.Rd
index b00f9b1..95d08b4 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).
}
@@ -81,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/reshard_safetensors.Rd b/man/reshard_safetensors.Rd
index ef470d1..c758fd3 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 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{
+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..b559141 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:
+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
+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
@@ -19,12 +25,13 @@ 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, 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.
}
diff --git a/man/unet_safetensors.Rd b/man/unet_safetensors.Rd
index 649ec21..ad07a1d 100644
--- a/man/unet_safetensors.Rd
+++ b/man/unet_safetensors.Rd
@@ -15,8 +15,9 @@ 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 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/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