Quantize-on-load with no calibration data, scored as seed-matched PSNR/SSIM against a bf16 reference generated on the same GPU. Four models, three precisions, one recipe.
↑ higher is better · ↓ lower is better · →1 closest to 1.0 is best (ratios vs bf16). Every quality metric is measured against a bf16 reference.
Verdict on the base method. nvfp4 with plain defaults is the safe precision on every model. int4 trails it by 1.4 to 4 dB at any rank or smoothing setting. The remaining defects are int4 trajectory drift and temporal flicker on video, both of which live in activation quantization and are not fixed by any recipe setting. Two follow-up sections below set this recipe beside calibrated checkpoints and present the Hadamard rotation that addresses the int4 gap.
Quality vs bf16
Each cell is PSNR in dB ↑ / SSIM ↑ against a bf16 reference from the same GPU, averaged over 5 seed-matched samples. Both precisions use the default recipe: rank 32, smoothing 0.5, no rotation. Per-sample spread is about ±3 dB, so treat differences under 0.5 dB as ties.
| model | setup | nvfp4 | int4 |
|---|---|---|---|
| LTX-2.3-Distilled 19B | video, 768×512, 121 frames, 8 steps | 17.33 / 0.60 | 13.29 / 0.42 |
| MiniMax-H3 30.8B | video + audio, 960×544, 124 frames | 15.87 / 0.54 | 14.52 / 0.46 |
| FLUX.2-klein-9B | text-to-image, 1024², 4 steps | 18.14 / 0.70 | 16.40 / 0.61 |
| Qwen-Image-2.1 7B | text-to-image, 2048², 40 steps | 22.74 / 0.89 | 20.45 / 0.82 |
| Qwen-Image-2.1 7B | image edits, 12 instructions | 25.27 / 0.89 | 21.84 / 0.84 |
Memory, speed and load time
| model | bf16 weights | quantized ↓ | quantize-on-load ↓ | nvfp4 speed vs bf16 ↑ | int4 speed vs bf16, A100 ↑ |
|---|---|---|---|---|---|
| MiniMax-H3 (RTX PRO 6000 96 GB / A100 80 GB) | 61.7 GB | 17.1–18.1 GB | 17–22 min | 1.67× faster | 1.24× faster |
| Qwen-Image-2.1 (RTX PRO 6000) | 13.25 GB | 3.9–4.1 GB | 10 min | 1.36× faster at 4k tokens, ≈1× at 2k | not run on A100 |
| FLUX.2-klein-9B (RTX PRO 6000) | — | — | 3.5 min | 1.5× faster | not run on A100 |
| LTX-2.3 19B (A100) | 35.4 GB | 10.6 GB (r32) / 12.3 GB (r128) | 22 min (pre-quantized export loads in 40 s) | — | ≈ bf16 |
int4 speed is shown only where it ran on its target hardware, the A100. Qwen-Image-2.1 and FLUX.2 int4 were run on Blackwell, which has no native int4 tensor-core path for this kernel, so their timings there (2.2 to 3× slower than bf16) reflect the GPU, not int4 itself. Pre-quantized export's real value is load time, not quality.
Visual comparison
Video comparisons show every configuration of one seed side by side; pick a scene, then play whichever video you want to watch. Set speed to 0.25× to see flicker. Image comparisons are contact sheets with the same seed down each column. The int4 rows are compared with the A100 bf16 row and the nvfp4 rows with the RTX bf16 row, because the same seed gives a different trajectory on a different GPU.
LTX-2.3-Distilled: int4 default vs rank 128 vs nvfp4
What to look for: plain int4 keeps the scene but shifts pose, lighting and composition (the drummer gains headphones, the car moves lanes, the fireworks change count). int4 at rank 128 tracks its reference more closely. nvfp4 against its own RTX reference is the closest match. Every video is clean and sharp; the loss is trajectory drift, not artifacts.
MiniMax-H3: joint audio-video, 30.8B
nvfp4 is compared with the RTX reference and int4 with the A100 reference. Both keep the scene and the camera move; int4 drifts further in the drummer's framing and the dog's pose. With sound on, the quantized soundtracks are a different rendition of the same scene (envelope correlation about 0.55 to 0.6), not a failure.
FLUX.2-klein-9B: 4-step image model

Qwen-Image-2.1: text-to-image across CFG, and an edit


All Qwen-Image-2.1 sheets
Every sheet has rows bf16 / nvfp4 / int4 and columns CFG off / 2.5 / 4, with PSNR ↑ and LPIPS ↓ against bf16 on each quantized tile. Click any sheet to view it at full size.
Text-to-image, 5 prompts (2048², 40 steps)
Edits, 12 instructions (own edits 0–4, identity edit 5, blog Fidelity pairs 6–11)
Rank and smoothing
The data-free recipe has two settings: rank, the size of the low-rank branch that keeps the largest weight components in 16-bit, and smoothing strength, how far each input channel is rescaled before 4-bit rounding, from 0 (off) to 1 (fully flattened). The defaults are rank 32 and smoothing 0.5.
Smoothing barely matters at the default rank
| model · precision, rank 32 (PSNR dB ↑) | off (0) | 0.25 | 0.5 (default) | 0.75 |
|---|---|---|---|---|
| LTX-2.3-Distilled · int4 | 13.06 | 12.53 | 13.29 | 13.00 |
| FLUX.2-klein-9B · nvfp4 | 18.62 | 17.94 | 18.14 | 18.58 |
Rank helps only when smoothing is retuned with it
| LTX-2.3-Distilled · int4 (PSNR dB ↑ / SSIM ↑) | smoothing off | 0.1 | 0.25 | 0.5 (default) | transformer size |
|---|---|---|---|---|---|
| rank 32 (default) | 13.06 / 0.42 | 13.39 / 0.44 | 12.53 / 0.41 | 13.29 / 0.42 | 10.6 GB |
| rank 64 | 13.25 / 0.43 | 13.68 / 0.45 | 14.27 / 0.47 | 15.11 / 0.52 | 11.2 GB |
| rank 128 | 13.93 / 0.48 | 14.57 / 0.53 | 15.99 / 0.54 | 13.65 / 0.45 | 12.3 GB |
- Up to +2.7 dB, but only at the right smoothing. Rank 128 reaches 15.99 dB at smoothing 0.25 and gains nothing at the default 0.5. The best smoothing value falls as rank grows, because the low-rank branch and smoothing absorb the same outliers.
- Cost. Transformer memory grows about 16% from rank 32 to 128. Inference time and quantize-on-load time do not change.
- Not worth it with rotation. Hadamard rotation at rank 32 reaches the same quality (15.42 dB) at the default memory, and rank 128 with rotation is worse (14.28 dB). See the Hadamard section.
- Other models. On MiniMax-H3, rank 64 cuts weight error by only about 3%; no end-to-end comparison was scored. Qwen-Image-2.1 and FLUX.2 were run at rank 32 only.
Calibrated vs data-free
The stock recipe above uses no data at all. Here it is set beside checkpoints that were calibrated on real activations, on the two models where such checkpoints exist, with the same seeds and the same bf16 references.
The calibrated checkpoints were quantized with rootonchair/diffuse-compressor, which runs the model on calibration data and chooses the smoothing values that minimize each block's error.
Weights used:
- LTX-2.3-Distilled int4:
lite-infer/LTX-2.3-Distilled-v1.1-Diffusers-nunchaku-lite-int4-bnb4-text-encoder - LTX-2.3-Distilled nvfp4:
lite-infer/LTX-2.3-Distilled-v1.1-Diffusers-nunchaku-lite-nvfp4-bnb4-text-encoder - MiniMax-H3 int4:
rootonchair/MiniMax-H3-nunchaku-lite-int4/calibrated-8x20 - MiniMax-H3 nvfp4:
rootonchair/MiniMax-H3-nunchaku-lite-nvfp4/calibrated-8x20
Summary. Calibration lowers and evens out the error inside each layer, and on MiniMax-H3 it gives visibly cleaner frames on scenes where data-free shows streaks and grain.
Per-layer error
- Calibration trades weight precision for activation precision. Its smoothing scales are much larger, so the weights themselves are rounded less exactly: 12.2% vs 8.2% relative weight error (nvfp4) and 12.6% vs 9.8% (int4) on LTX. In exchange, activation error drops sharply.
- The full layer comes out ahead. Scored on 512 real activation rows per layer through the whole 4-bit path, calibrated error is lower on 946 of 1344 layers (nvfp4): 6.9% vs 7.6% (nvfp4) and 9.3% vs 11.4% (int4).
- And far more even. Data-free error is spiky, reaching 80% activation error on some attention output layers. Calibration brings those to 20% or less, including the layer with the largest weight error (6.2% vs 9.7% full error).
- Caveat: the activations came from a data-free nvfp4 run because a bf16 capture did not fit on that machine, so the comparison is fair between the two recipes but absolute levels depend on that run.
Image quality
Every 16th frame of each video was scored with MUSIQ, a learned image-quality model (↑, roughly 0 to 100). It judges each frame on its own, so it captures artifacts such as streaks and grain directly.
| model · precision (MUSIQ ↑) | bf16 | calibrated | data-free |
|---|---|---|---|
| MiniMax-H3 · int4, rainy street scene | 51.9 | 50.7 | 39.3 |
| MiniMax-H3 · int4, all scenes | 55.6 | 54.9 | 54.1 |
| MiniMax-H3 · nvfp4, all scenes | 55.6 | 53.7 | 51.5 |
| LTX-2.3-Distilled · int4, all scenes | 57.4 | 52.6 | 56.5 |
| LTX-2.3-Distilled · nvfp4, all scenes | 55.6 | 55.9 | 56.4 |
- MiniMax-H3. On the rainy street, data-free int4 shows vertical and horizontal streaks across the buildings and dark areas, and calibrated int4 stays clean, close to bf16. On the ocean scene, data-free int4 water and sky are grainier. Calibrated nvfp4 also scores higher across all scenes.
- LTX-2.3-Distilled. Side-by-side crops of two scenes show no visible artifact difference between the two recipes.
- Why. The spiky per-layer errors of data-free are a plausible source of streaks and grain; calibration's flatter error avoids them.
- Limits: 5 videos per setting and one learned metric.


Closeness to bf16
| model · precision (PSNR ↑ / SSIM ↑) | calibrated | data-free |
|---|---|---|
| LTX-2.3-Distilled · int4 (A100) | 12.62 / 0.39 | 13.29 / 0.42 |
| LTX-2.3-Distilled · nvfp4 (RTX PRO 6000) | 15.34 / 0.52 | 17.33 / 0.60 |
| MiniMax-H3 · nvfp4 | 15.23 | 15.66 |
| MiniMax-H3 · int4 | 13.87 | 14.48 |
PSNR and SSIM against a same-GPU bf16 reference, 5 seed-matched samples. These measure how closely each video follows the bf16 one, not how clean its frames look; quantized videos drift to slightly different compositions, which dominates these scores.
Visual comparison: LTX-2.3-Distilled
Both recipes keep each scene and drift from their reference in their own way: the drummer's pose, the car's position and the fireworks layout change from run to run.
Visual comparison: MiniMax-H3
Everything is compared with the RTX reference. On the rainy street, data-free int4 shows streak artifacts that calibrated int4 avoids; set speed to 0.25× and play each to compare. Calibrated int4 renders the drummer and fireworks with stronger contrast. Each video has its own volume control; both soundtracks are renditions of the same scene.
Hadamard rotation: the int4 improvement
Everything above uses the stock recipe. This section is the one change that moved int4 by more than trajectory noise: a data-free blockwise Hadamard rotation of the quantized residual, with the matching Walsh–Hadamard transform fused into the activation-quantization kernel. Validated on two video models and, as of 2026-10-09, on Qwen-Image-2.1.
Design
The problem it solves. 4-bit quantization gives each group of 64 input channels one shared scale. If one channel in a group carries a value ten times larger than its neighbours, the scale has to stretch to fit it, and every other channel in that group is rounded on a coarse grid. Diffusion transformers have exactly this: a handful of activation channels are consistently much larger than the rest, in a different place for every token. The low-rank branch of SVDQuant[1] absorbs the largest weight outliers, and smoothing[2] moves some activation range into the weights. But smoothing scales are computed from the weights, so they cannot follow outliers that change from token to token, and those remain the main error source.
The idea: rotate before rounding. Multiplying a vector by an orthonormal matrix changes its coordinates but not its length, and multiplying both sides of a matrix product by the same rotation leaves the result unchanged. A Hadamard matrix is a particularly convenient rotation: every entry is ±1/√n, so each output coordinate is an even mix of all inputs. One large channel gets spread across the whole block, and the rotated values look close to Gaussian, which a uniform 4-bit grid represents far better. This "incoherence processing" was introduced for weight quantization by QuIP[3] with random orthogonal matrices. QuIP#[4] switched to randomized Hadamard transforms because they can be applied fast. QuaRot[5] extended it to activations to run LLMs fully in 4 bits, and SpinQuant[6] learns the rotation instead of fixing it.
The math, step by step.
A linear layer maps an input vector to an output vector:
\[ y = W x, \qquad W \in \mathbb{R}^{m \times n},\; x \in \mathbb{R}^{n} \]Smoothing moves range from the activations into the weights with one scale per input channel. The product is unchanged:
\[ y = \underbrace{\big(W\,\mathrm{diag}(s)\big)}_{\hat W}\;\underbrace{\big(\mathrm{diag}(s)^{-1} x\big)}_{\hat x} \]SVDQuant splits the smoothed weight into a rank-32 part, kept in 16-bit, and a residual \(R\):
\[ \hat W = L_2 L_1 + R, \qquad L_1 \in \mathbb{R}^{32 \times n}\ \text{(LoRA down)},\quad L_2 \in \mathbb{R}^{m \times 32}\ \text{(LoRA up)} \]Only the residual path is quantized, both its weights and its activations, to 4 bits:
\[ y \approx \underbrace{L_2\,(L_1 \hat x)}_{\text{16-bit LoRA}} \;+\; \underbrace{Q(R)\;Q(\hat x)}_{\text{W4A4 GEMM}} \]Insert an orthonormal matrix \(H\) (so \(H H^\top = I\)) between the residual and its input. This is exact:
\[ R\,\hat x \;=\; R\,H H^\top \hat x \;=\; (R H)\,(H^\top \hat x) \]Now quantize the rotated operands instead. \(Q(RH)\) is computed once at load time, and \(H^\top \hat x\) per token inside the kernel; a Hadamard matrix is symmetric, so \(H^\top = H\):
\[ y \approx L_2\,(L_1 \hat x) \;+\; Q(R H)\;Q(H \hat x) \]\(H\) is block-diagonal, one \(64 \times 64\) Hadamard block per 4-bit quantization group, so every group keeps its own scale. Each block is applied with a fast Walsh–Hadamard transform in \(\log_2 64 = 6\) add/subtract stages:
\[ H = \mathrm{diag}\big(H_{64}, \dots, H_{64}\big), \qquad H_{64} = \tfrac{1}{8} \begin{bmatrix} 1 & 1 \\ 1 & -1 \end{bmatrix}^{\otimes 6} \]
How it is applied here.
- Weights, at load time. The residual is rotated before rounding, as
Q(R · H). H is block-diagonal with one 64×64 Hadamard block per quantization group, so each group's scale still covers only its own 64 channels. H is a fixed matrix, so no data is needed and the method stays data-free. - Activations, at run time. The matching rotation is applied to each token inside the kernel that quantizes activations, as a fast Walsh–Hadamard transform in registers (64 inputs in log₂64 = 6 add/subtract stages). It runs after the smoothing division and after the low-rank projection, so the low-rank branch still sees the original input. This is the same fused online-rotation pattern QuaRot[5] uses for LLMs.
- Why it beats smoothing here. Smoothing has to choose one scale per channel ahead of time. The rotation needs no statistics: it spreads whatever outlier a token has at run time, which is what per-token outliers require.
Why it does not help nvfp4. nvfp4 already gives every 16 channels their own fine-grained scale, and its 4-bit floating-point grid is built for heavy-tailed values. A 64-wide rotation mixes four of those small groups together and removes the tails the format handles well. Egiazarian et al.[7] show that nvfp4's small groups neutralize classic outlier treatments, and they use smaller block-wise rotations tuned to the format instead. That matches the −2.2 dB measured here.
End-to-end numbers
| run | PSNR ↑ | SSIM ↑ | flicker ↓ · LPIPS ↓ · identity ↑ | inference ↓ | transformer ↓ |
|---|---|---|---|---|---|
| LTX-2.3-Distilled · int4 r32 + H64, smoothing off (A100) | 15.42 ± 3.76 | 0.539 | 7.96 | 57.6 s / 5 clips | 10.6 GB |
| LTX-2.3-Distilled · int4 r32, rotation off, same kernel (control) | 13.70 ± 3.14 | 0.434 | 9.03 | 57.2 s | 10.6 GB |
| LTX-2.3-Distilled · int4 r128 + H64, smoothing off | 14.28 ± 4.40 | 0.481 | 7.78 | 59.3 s | 12.3 GB |
| LTX-2.3-Distilled · previous best, int4 r128 e0.25, no rotation | 15.99 ± 4.87 | 0.54 | — | ≈59 s | 12.3 GB |
| LTX-2.3-Distilled · bf16 reference flicker | — | — | 6.39 | — | 35.4 GB |
| MiniMax-H3 · int4 r32 + H64 (A100) | 15.64 ± 1.85 | 0.529 | 0.185 / 0.56 | 2005 s / 5 clips | 17.1 GB |
| MiniMax-H3 · int4 r32 plain | 14.52 ± 1.14 | 0.457 | 0.214 / 0.58 | 2021 s | 17.1 GB |
| MiniMax-H3 · nvfp4 r32 + H64 (RTX) | 13.66 ± 1.49 | 0.469 | 0.209 / 0.46 | — | 18.1 GB |
| MiniMax-H3 · nvfp4 r32 plain | 15.87 ± 2.01 | 0.542 | 0.107 / 0.17 | 924 s | 18.1 GB |
| MiniMax-H3 · bf16 reference flicker | — | — | 0.12 / 0.26–0.29 | — | 61.7 GB |
| Qwen-Image-2.1 · int4 r32 + H64, CFG off / 2.5 / 4 (RTX) | 22.81 / 20.75 / 20.17 | 0.886 / 0.800 / 0.790 | LPIPS 0.18 / 0.22 / 0.22 | 134 s / 269 s per image | 3.86 GB |
| Qwen-Image-2.1 · int4 r32 plain (earlier run, shared GPU) | 20.45 / 18.44 / 16.93 | 0.820 / 0.726 / 0.710 | LPIPS 0.26 / 0.36 / 0.36 | 273 s / ~570 s | 3.86 GB |
| Qwen-Image-2.1 · nvfp4 r32 plain | 22.74 / 21.56 / 20.97 | 0.894 / 0.834 / 0.841 | LPIPS 0.17 / 0.21 / 0.18 | 92 s / ~165 s | 4.07 GB |
| Qwen-Image-2.1 edits (12) · int4 r32 + H64, CFG off / 2.5 / 4 | 23.43 / 23.60 / 22.22 | 0.867 / 0.862 / 0.820 | LPIPS 0.14 / 0.14 / 0.18 · identity 0.89 / 0.78 / 0.76 | 34 s / 68 s per edit | 3.86 GB |
| Qwen-Image-2.1 edits · int4 r32 plain | 21.84 / 21.84 / 21.38 | 0.836 / 0.827 / 0.783 | LPIPS 0.19 / 0.18 / 0.23 · identity 0.79 / 0.72 / 0.70 | 81 s / ~190 s | 3.86 GB |
| Qwen-Image-2.1 edits · nvfp4 r32 plain | 25.27 / 23.39 / 23.00 | 0.889 / 0.856 / 0.843 | LPIPS 0.11 / 0.15 / 0.16 · identity 0.89 / 0.85 / 0.77 | 39 s / ~75 s | 4.07 GB |
Flicker ↓: on LTX it is mean |Laplacian| of frame differences (bf16 itself scores 6.39). MiniMax flicker is the period-4 power fraction, shown as mean / worst scene. Rotated int4 at rank 32 matches the old rank-128 best within noise at the default memory footprint. Qwen timings are not comparable across rows: the rotated run had the GPU to itself, the others shared it with other jobs.
Visual comparison: LTX-2.3-Distilled, same kernel, rotation off vs on
The control uses the same kernel build and seeds with the rotation off; H64 changes nothing else. With rotation the drummer loses the headphones and regains bf16's framing, the car stays on the crossing with pedestrians instead of drifting into traffic, the dog's pose and the tree line move back toward bf16, and the fireworks count matches. The second row repeats H64 next to rank 128 + H64: adding rank 128 on top drifts again, because the two treatments compete for the same outlier energy.

Visual comparison: MiniMax-H3
Rotation pulls int4's drummer framing, dog pose and fireworks shape back toward the A100 reference. The second row is plain nvfp4, without rotation, against its RTX reference: rotated int4 (15.6 dB) lands at about the same distance from its reference as nvfp4 does (15.9 dB). On the ocean scene at 0.25×, int4's frame-to-frame pulsing is the easiest thing to see; plain nvfp4 is already close to bf16 there.
Visual comparison: Qwen-Image-2.1, text-to-image across CFG



All Qwen-Image-2.1 sheets with the rotation row
Every sheet has rows bf16 / nvfp4 / int4 / int4 + H64 and columns CFG off / 2.5 / 4, with nvfp4 in its plain form (no rotation), with PSNR ↑ and LPIPS ↓ against bf16 on each quantized tile. Click any sheet on this page to view it at full size; click again or press Escape to close.
Text-to-image, 5 prompts (2048², 40 steps)
Edits, 12 instructions (own edits 0–4, identity edit 5, blog Fidelity pairs 6–11)
Rules learned
- int4 only. On nvfp4 it costs 2.2 dB and doubles flicker. The kernel option is validated for int4 and must stay off for nvfp4.
- Rank 32, smoothing off. Rotation and rank do not stack (r128 + H64 is 1.1 dB below r32 + H64), and smoothing on top of rotation lowers activation SNR. Recommended config:
rank=32, hadamard=True, smooth_exponent=0. - Pre-Blackwell hardware reaches nvfp4-class quality. A100 int4 + H64 (15.64) ≈ RTX nvfp4 (15.87) on MiniMax, at the same runtime as plain int4. On Qwen-Image-2.1 rotated int4 ties nvfp4 at CFG off (22.8 vs 22.7) and sits 0.8 dB below it under CFG, within 0.04 LPIPS.
- It removes most of int4's CFG sensitivity on Qwen. Plain int4 lost 3.5 dB from CFG off to CFG 4; rotated int4 loses 2.6, about what nvfp4 loses. The compositions int4 forked come back.
- Not a flicker cure. Mean flicker improves; the worst scene does not.
- Variants already tested with no extra gain: sign-flipped H64, H128. Random permutation is the only untested piece of the OrbitQuant family.
References
- Muyang Li et al., 2024. SVDQuant: Absorbing Outliers by Low-Rank Components for 4-Bit Diffusion Models. arXiv:2411.05007
- Guangxuan Xiao et al., 2022. SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. arXiv:2211.10438
- Jerry Chee et al., 2023. QuIP: 2-Bit Quantization of Large Language Models With Guarantees. arXiv:2307.13304
- Albert Tseng et al., 2024. QuIP#: Even Better LLM Quantization with Hadamard Incoherence and Lattice Codebooks. arXiv:2402.04396
- Saleh Ashkboos et al., 2024. QuaRot: Outlier-Free 4-Bit Inference in Rotated LLMs. arXiv:2404.00456
- Zechun Liu et al., 2024. SpinQuant: LLM quantization with learned rotations. arXiv:2405.16406
- Vage Egiazarian et al., 2025. Bridging the Gap Between Promise and Performance for Microscaling FP4 Quantization. arXiv:2509.23202
Tested and negative
- LoftQ alternating SVD/quantize iteration: 4.6 to 11.1% reconstruction gain, below the pre-registered gate.
- Excluding the 288 worst layers (+3.5 GB, no effect; error is systemic). Decode-timestep dither (does not mask flicker).
- Activation scales derived from AdaLN modulation weights (helps only cross-modal to_q, hurts attn1).
- Frame-shared activation scales (+2.5 to 5 pts error, +75% jitter). DCT on all linears (harmful for nvfp4).
- AdaLN is not an error source on MiniMax (see the calibrated section).
Recommended configs
| target | config | note |
|---|---|---|
| Blackwell | nvfp4, rank 32, smooth_exponent 0.5 | Never add hadamard. Fastest and best quality everywhere. |
| A100 and older (int4) | int4, rank 32, hadamard=True, smooth_exponent 0 | Reaches nvfp4-class fidelity on LTX, MiniMax and Qwen-Image-2.1. Needs the hadamard kernel branch published. |
Code samples
Each sample is a complete script: it quantizes the transformer while it loads, with no calibration data, then generates one image. Quantizing takes roughly 5 to 20 minutes depending on model size. The examples use Qwen-Image-2.1; other models work the same way with their own transformer and pipeline classes.
Setup
# Data-free quantize-on-load lives on branch feature/data-free-quantization of rootonchair/diffusers.
# Qwen-Image-2.1 also needs upstream main merged in.
pip install "git+https://github.com/rootonchair/diffusers@feature/data-free-quantization" kernels
export DIFFUSERS_TRUST_REMOTE_KERNELS=true # loads rootonchair/nunchaku-lite-kernels from the Hubnvfp4
# nvfp4: Blackwell GPUs only. Best quality, no tuning needed.
import torch
from diffusers import NunchakuLiteQuantizationConfig, QwenImage21Pipeline, QwenImage21Transformer2DModel
model_id = "Qwen/Qwen-Image-2.1"
# Quantize the transformer while it loads. pre_quantized=False means data-free:
# no calibration data, the 4-bit weights are computed from the bf16 checkpoint.
quantization_config = NunchakuLiteQuantizationConfig(
svdq_w4a4={
"precision": "nvfp4",
"group_size": 16, # fixed for nvfp4
"rank": 32, # size of the 16-bit low-rank branch
},
pre_quantized=False,
)
transformer = QwenImage21Transformer2DModel.from_pretrained(
model_id,
subfolder="transformer",
quantization_config=quantization_config,
dtype=torch.bfloat16,
device_map="cuda",
)
# The rest of the pipeline (text encoder, VAE) stays in bf16.
pipe = QwenImage21Pipeline.from_pretrained(model_id, transformer=transformer, dtype=torch.bfloat16)
pipe.to("cuda")
image = pipe(
prompt="A street musician playing saxophone under a lamppost, holding a cardboard sign that says OPEN MIC, warm evening light.",
num_inference_steps=40,
generator=torch.Generator("cuda").manual_seed(42),
).images[0]
image.save("qwen_nvfp4.png")int4
# int4: runs on A100 and older GPUs.
import torch
from diffusers import NunchakuLiteQuantizationConfig, QwenImage21Pipeline, QwenImage21Transformer2DModel
model_id = "Qwen/Qwen-Image-2.1"
# Quantize the transformer while it loads. pre_quantized=False means data-free:
# no calibration data, the 4-bit weights are computed from the bf16 checkpoint.
quantization_config = NunchakuLiteQuantizationConfig(
svdq_w4a4={
"precision": "int4",
"group_size": 64, # fixed for int4
"rank": 32, # size of the 16-bit low-rank branch
},
pre_quantized=False,
)
transformer = QwenImage21Transformer2DModel.from_pretrained(
model_id,
subfolder="transformer",
quantization_config=quantization_config,
dtype=torch.bfloat16,
device_map="cuda",
)
# The rest of the pipeline (text encoder, VAE) stays in bf16.
pipe = QwenImage21Pipeline.from_pretrained(model_id, transformer=transformer, dtype=torch.bfloat16)
pipe.to("cuda")
image = pipe(
prompt="A street musician playing saxophone under a lamppost, holding a cardboard sign that says OPEN MIC, warm evening light.",
num_inference_steps=40,
generator=torch.Generator("cuda").manual_seed(42),
).images[0]
image.save("qwen_int4.png")int4 with Hadamard rotation
# Hadamard rotation needs branch feature/hadamard-rotation and a kernel build with the online transform
# (kernel branch "hadamard" of rootonchair/nunchaku_lite; builds exist for A100 and Blackwell).
pip install "git+https://github.com/rootonchair/diffusers@feature/hadamard-rotation"
pip install nunchaku_lite_kernels-0.1.1+cu12.8torch2.11-cp312-cp312-linux_x86_64.whl
export NUNCHAKU_LITE_LOCAL_KERNELS=1 # use the installed kernel instead of the Hub build# int4 + Hadamard rotation: the recommended int4 setting. Never use rotation with nvfp4.
import torch
from diffusers import NunchakuLiteQuantizationConfig, QwenImage21Pipeline, QwenImage21Transformer2DModel
model_id = "Qwen/Qwen-Image-2.1"
# Quantize the transformer while it loads. pre_quantized=False means data-free:
# no calibration data, the 4-bit weights are computed from the bf16 checkpoint.
quantization_config = NunchakuLiteQuantizationConfig(
svdq_w4a4={
"precision": "int4",
"group_size": 64,
"rank": 32,
"hadamard": True, # rotate the residual and the activations in 64-channel blocks
"smooth_exponent": 0.0, # smoothing off; rotation replaces it
},
pre_quantized=False,
)
transformer = QwenImage21Transformer2DModel.from_pretrained(
model_id,
subfolder="transformer",
quantization_config=quantization_config,
dtype=torch.bfloat16,
device_map="cuda",
)
# The rest of the pipeline (text encoder, VAE) stays in bf16.
pipe = QwenImage21Pipeline.from_pretrained(model_id, transformer=transformer, dtype=torch.bfloat16)
pipe.to("cuda")
image = pipe(
prompt="A street musician playing saxophone under a lamppost, holding a cardboard sign that says OPEN MIC, warm evening light.",
num_inference_steps=40,
generator=torch.Generator("cuda").manual_seed(42),
).images[0]
image.save("qwen_int4_hadamard.png")Changing rank and smoothing
# int4 with a larger rank and retuned smoothing (best LTX int4 setting without rotation: +2.7 dB, +16% memory).
import torch
from diffusers import NunchakuLiteQuantizationConfig, QwenImage21Pipeline, QwenImage21Transformer2DModel
model_id = "Qwen/Qwen-Image-2.1"
# Quantize the transformer while it loads. pre_quantized=False means data-free:
# no calibration data, the 4-bit weights are computed from the bf16 checkpoint.
quantization_config = NunchakuLiteQuantizationConfig(
svdq_w4a4={
"precision": "int4",
"group_size": 64,
"rank": 128, # default 32; larger = more memory, same speed
"smooth_exponent": 0.25, # default 0.5; 0 = off, 1 = fully flattened
},
pre_quantized=False,
)
transformer = QwenImage21Transformer2DModel.from_pretrained(
model_id,
subfolder="transformer",
quantization_config=quantization_config,
dtype=torch.bfloat16,
device_map="cuda",
)
# The rest of the pipeline (text encoder, VAE) stays in bf16.
pipe = QwenImage21Pipeline.from_pretrained(model_id, transformer=transformer, dtype=torch.bfloat16)
pipe.to("cuda")
image = pipe(
prompt="A street musician playing saxophone under a lamppost, holding a cardboard sign that says OPEN MIC, warm evening light.",
num_inference_steps=40,
generator=torch.Generator("cuda").manual_seed(42),
).images[0]
image.save("qwen_int4_rank128.png")group_sizeis fixed by precision: 16 for nvfp4, 64 for int4. Any other value is rejected.rankis required.smooth_exponent(0 to 1, default 0.5) andhadamardare optional and apply only whenpre_quantized=False.- At rank 32, smoothing makes no measurable difference, so
"smooth_exponent": 0.0or leaving it out are equivalent in quality. - Quantized layers are chosen automatically. To keep specific layers in bf16, pass
exclude_targets=[...]to the config.