diffuseR runs 20B-parameter models on 16 GB consumer GPUs, and most
models on no GPU at all. It does that with three independent levers:
weight precision, per-component device placement, and memory residency.
This vignette is the map. The machine-readable version of the same
policy is recommend(), which inspects your VRAM, host RAM,
and installed safetensors capabilities and returns a
configuration. Today flux_memory_profile() delegates to it
and serve() consults it to pick between built LTX
artifacts; the FLUX-family loaders resolve their pin (and
FLUX.1 its text_device) through it. For the rest of the
fields it is advisory — call it and pass them to the loaders
yourself.
The ladder that exists in code: fp32 → bf16/fp16 → fp8 (e4m3fn) → nf4. Quantization applies to the big diffusion transformers only — VAEs, vocoders, connectors, and modulation/embedding layers always stay at 16/32 bits (they are small and precision-sensitive; each quantizer carries an exact census of which weights it may touch).
| model | DiT / UNet | text encoder(s) | VAE |
|---|---|---|---|
| FLUX.1 (12B) | nf4, fp8 (streamed), bf16, fp32 | T5: bf16 (GPU, 14 GB+) or fp32 (CPU) · CLIP-L: fp16/fp32 | 16/32 |
| FLUX.2 klein (4B) | nf4, fp8 (resident), bf16, fp32 | Qwen3-4B: bf16/fp32 | 16/32 |
| Z-Image (6B) | nf4, fp8 (resident), bf16, fp32 | Qwen3-4B: bf16/fp32 | 16/32 |
| LTX-2.3 (22B video) | nf4, fp8 (streamed), bf16, fp32 | Gemma3-12B: nf4, bf16, fp32 | 16/32 |
| SD 2.1 / SDXL | fp16, fp32 | CLIP: fp16/fp32 | 16/32 |
Two readability rules govern the ladder:
safetensors build reads them. It is the default tier for
the quantized families.safetensors. The
float8 dtypes are not yet readable by the CRAN safetensors;
recommend() probes the installed build and, when a card
could run fp8 but the reader cannot, recommends nf4 and surfaces the
suggestion in $note (never an error).The SD-family models ship no quantized weights: their floor is fp16, and what varies across VRAM is placement, not precision.
Every component takes its own device. The SD family uses explicit
device maps (auto_devices() strategies:
full_gpu, unet_gpu, cpu_only);
the flux family and LTX use phase offloading, where each component holds
the GPU only for its own phase — text encoding, denoising, decoding —
and the denoiser is the sole GPU tenant during the loop. Text encoders
earn special placement: FLUX.1’s T5 phase-onloads in bf16 on 14 GB+
cards (its ~9.8 GB encode phase fits) and runs fp32 on the CPU below
that; the Qwen3 encoders phase-onload in bf16; the Gemma3 encoder can be
GPU-resident or CPU-resident with a staged swap (see below).
From most to least VRAM:
There is no disk tier at inference time: weights load from disk once,
and the closest thing to “swap to disk” is unpinned host memory
being paged out by the OS — which is exactly the trade
recommend() weighs.
Pinned pages are unswappable — they subtract from what the OS can
page out, so on small-RAM machines they convert memory pressure into
process kills rather than slowdowns. recommend() therefore
returns pin = TRUE only when available host RAM covers the
model’s estimated pinned set twice over, FALSE on the CPU
tier (nothing stages), and TRUE when RAM cannot be
detected, because page-locking already fails soft per component. The
global switch is options(diffuseR.pin_staging = FALSE) —
reach for it under host memory pressure, in containers with hard memory
caps, or for single-generation sessions where the one-time page-lock
never pays itself back. The LTX pipeline, the Gemma3 loaders, and the
FLUX-family image loaders consume the decision (their pin
argument defaults to it); the SD-family loaders place components
statically, so pinning is inert for them.
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 unreadableTreat the result as the machine’s advice: pass its fields to the
loaders and generators. The FLUX-family loaders’ pin (and
FLUX.1’s text_device), flux_memory_profile(),
and serve()’s LTX artifact selection consume it
automatically.