Data-free 4-bit quantization for diffusion transformers

Quantize-on-load SVDQuant across four video and image models, and a Hadamard rotation that brings int4 up to nvfp4 quality

By Pham Hong Vinh

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.

Models validated
4
LTX-2.3 19B (distilled + base), FLUX.2-klein 9B/4B, MiniMax-H3 30.8B, Qwen-Image-2.1 7B
Zero-config targets
1344 / 362 / 224
Quantized linears auto-selected on LTX / MiniMax / Qwen with no per-model tuning; the same recipe on every architecture
nvfp4 over int4
+1.4 to +4 dB
Precision is the first lever on every model. nvfp4 needs no tuning; int4 drifts further from bf16
Memory
3.3 to 3.6×
Transformer bf16 → int4/nvfp4. MiniMax 61.7 → 17 GB makes the pipeline resident on 96 GB

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.

modelsetupnvfp4int4
LTX-2.3-Distilled 19Bvideo, 768×512, 121 frames, 8 steps17.33 / 0.6013.29 / 0.42
MiniMax-H3 30.8Bvideo + audio, 960×544, 124 frames15.87 / 0.5414.52 / 0.46
FLUX.2-klein-9Btext-to-image, 1024², 4 steps18.14 / 0.7016.40 / 0.61
Qwen-Image-2.1 7Btext-to-image, 2048², 40 steps22.74 / 0.8920.45 / 0.82
Qwen-Image-2.1 7Bimage edits, 12 instructions25.27 / 0.8921.84 / 0.84

Memory, speed and load time

modelbf16 weightsquantized ↓quantize-on-load ↓nvfp4 speed vs bf16 ↑int4 speed vs bf16, A100 ↑
MiniMax-H3 (RTX PRO 6000 96 GB / A100 80 GB)61.7 GB17.1–18.1 GB17–22 min1.67× faster1.24× faster
Qwen-Image-2.1 (RTX PRO 6000)13.25 GB3.9–4.1 GB10 min1.36× faster at 4k tokens, ≈1× at 2knot run on A100
FLUX.2-klein-9B (RTX PRO 6000)——3.5 min1.5× fasternot run on A100
LTX-2.3 19B (A100)35.4 GB10.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

FLUX.2 contact sheet
FLUX.2-klein-9B. nvfp4 is near-identical to bf16; int4 keeps the composition but changes details such as the saxophonist's face and the dog's pose.

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

Qwen-Image-2.1 sample 0 sheet
Qwen-Image-2.1: rows bf16 / nvfp4 / int4, columns CFG off / 2.5 / 4. nvfp4 stays on-prompt; int4 drifts further, more so at higher CFG.
Qwen-Image-2.1 edit 0 sheet
Qwen-Image-2.1 edit. The input image anchors the result, so both precisions stay close to bf16.

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)
sample0: A street musician playing saxophone under a lamppost, holding a cardboard sign that says OPEN MIC, warm evening light.
sample1: A misty mountain valley at sunrise, a winding river reflecting golden clouds, pine forests on the slopes.
sample2: A rainy neon-lit city street at night, reflections on wet asphalt, people with umbrellas, cinematic.
sample3: A golden retriever mid-leap catching a frisbee in a sunlit meadow, motion blur in the grass.
sample4: A still life of oranges, a ceramic teapot and lace cloth on a wooden table, dramatic window light, fine texture.
Edits, 12 instructions (own edits 0–4, identity edit 5, blog Fidelity pairs 6–11)
edit0 (T2I sample 0): Change the text on the cardboard sign to say JAZZ TONIGHT.
edit1 (T2I sample 1): Add a red hot-air balloon floating above the river.
edit2 (T2I sample 2): Turn the rain into a snowy winter night, with snow covering the street and the umbrellas.
edit3 (T2I sample 3): Replace the golden retriever with a black labrador, keeping the frisbee and the leaping pose.
edit4 (T2I sample 4): Change the oranges into green apples and make the teapot cobalt blue.
edit5 (T2I sample 0): Keep the man's face and identity exactly the same; put him in a white suit on a concert stage under spotlights.
edit6 (blog_example-25.png): Show this woman full-body, walking along a wooden boardwalk through a mangrove forest, wearing a white T-shirt and khaki cargo pants, holding a straw hat.
edit7 (blog_example-27.png): Change her hairstyle to long, loose wavy hair falling over her shoulders; keep everything else the same.
edit8 (blog_example-29.png): Show her sitting in a cozy living room, smiling while opening a cardboard package, wearing the same sweater and scarf.
edit9 (blog_example-31.png): Place this foundation bottle on a wooden table by a sunny window, next to a cup of tea and a plate of strawberries.
edit10 (blog_example-33.png): Show this dress worn by a woman standing full-body in a warm-toned studio.
edit11 (blog_example-35.png): Show this bracelet worn on a woman's wrist as she sits at a wooden table with a teapot, wearing a linen blouse.

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.250.5 (default)0.75
LTX-2.3-Distilled · int413.0612.5313.2913.00
FLUX.2-klein-9B · nvfp418.6217.9418.1418.58

Rank helps only when smoothing is retuned with it

LTX-2.3-Distilled · int4 (PSNR dB ↑ / SSIM ↑)smoothing off0.10.250.5 (default)transformer size
rank 32 (default)13.06 / 0.4213.39 / 0.4412.53 / 0.4113.29 / 0.4210.6 GB
rank 6413.25 / 0.4313.68 / 0.4514.27 / 0.4715.11 / 0.5211.2 GB
rank 12813.93 / 0.4814.57 / 0.5315.99 / 0.5413.65 / 0.4512.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:

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, nvfp4 (LTX)
6.9% vs 7.6%
Calibrated vs data-free, full 4-bit layer error ↓, measured on real activations
Per-layer error, int4 (LTX)
9.3% vs 11.4%
Calibrated vs data-free, same measurement
Worst layers (LTX)
≤20% vs 80%
Calibration flattens the spiky layers where data-free error peaks
MiniMax-H3 int4, rainy street
50.7 vs 39.3
MUSIQ image quality ↑, calibrated vs data-free (bf16 51.9): data-free shows streak artifacts

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 ↑)bf16calibrateddata-free
MiniMax-H3 · int4, rainy street scene51.950.739.3
MiniMax-H3 · int4, all scenes55.654.954.1
MiniMax-H3 · nvfp4, all scenes55.653.751.5
LTX-2.3-Distilled · int4, all scenes57.452.656.5
LTX-2.3-Distilled · nvfp4, all scenes55.655.956.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.
MiniMax int4 rainy street crops: bf16, calibrated, data-free
MiniMax-H3 int4, rainy street, 2× crop: bf16, calibrated, data-free. Data-free shows streaks over the buildings and dark areas.
MiniMax int4 ocean crops: bf16, calibrated, data-free
MiniMax-H3 int4, ocean, 2× crop: bf16, calibrated, data-free. Data-free water and sky are grainier.

Closeness to bf16

model · precision (PSNR ↑ / SSIM ↑)calibrateddata-free
LTX-2.3-Distilled · int4 (A100)12.62 / 0.3913.29 / 0.42
LTX-2.3-Distilled · nvfp4 (RTX PRO 6000)15.34 / 0.5217.33 / 0.60
MiniMax-H3 · nvfp415.2315.66
MiniMax-H3 · int413.8714.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.

LTX-2.3-Distilled, int4 r32
+1.7 dB
13.70 → 15.42 PSNR, SSIM 0.43 → 0.54, vs the same kernel with rotation off
MiniMax-H3, int4 r32
+1.1 dB
14.52 → 15.64, SSIM 0.46 → 0.53. Rotated int4 ≈ plain nvfp4 (15.87)
Qwen-Image-2.1, int4 r32
+2.3 to +3.2 dB
20.5 / 18.4 / 16.9 → 22.8 / 20.8 / 20.2 at CFG off / 2.5 / 4. Ties nvfp4 (22.7 / 21.6 / 21.0)
Cost and limits
+0.7% time
Zero extra memory. Never combine with nvfp4 (−2.2 dB) or rank 128

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.

SVDQuant: round the group as is +7 0 −8 0 0 0 outlier +7 0 0 0 0 code the outlier stretches the step; 7 of 8 channels round to 0 + Hadamard: round H·x instead +7 0 −8 +7 -7 -7 +7 +7 -5 -7 +7 code values spread evenly; all 8 channels use the grid
One group rounded to 4 bits (8 channels shown instead of 64). Without rotation the outlier sets the step and 7 channels round to 0; with rotation all 8 keep a code and the error drops from 7.4% to 3.8%.

The math, step by step.

  1. 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} \]
  2. 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} \]
  3. 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)} \]
  4. 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}} \]
  5. 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) \]
  6. 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) \]
  7. \(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.
SVDQuant kernel 1 · quantize activations, LoRA down kernel 2 · W4A4 GEMM, LoRA up x ÷ ssmoothing LoRA down · L₁16-bit, to rank 32 rank 32 LoRA up · L₂16-bit round to 4-bitper 64 channels W4A4 GEMM4-bit act × 4-bit weight + y Q(R)residual, rounded prepared once at load time SVDQuant + Hadamard rotation added by the Hadamard rotation kernel 1 · quantize activations, LoRA down kernel 2 · W4A4 GEMM, LoRA up x ÷ ssmoothing LoRA down · L₁16-bit, to rank 32 rank 32 LoRA up · L₂16-bit × Hper token round to 4-bitper 64 channels W4A4 GEMM4-bit act × 4-bit weight + y Q(R·H)residual, rotated then rounded prepared once at load time
One layer as two fused kernels. Rotation adds only the highlighted parts: × H on each token before rounding, and weights stored as Q(R·H). The LoRA branch is unchanged.

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

runPSNR ↑SSIM ↑flicker ↓ · LPIPS ↓ · identity ↑inference ↓transformer ↓
LTX-2.3-Distilled · int4 r32 + H64, smoothing off (A100)15.42 ± 3.760.5397.9657.6 s / 5 clips10.6 GB
LTX-2.3-Distilled · int4 r32, rotation off, same kernel (control)13.70 ± 3.140.4349.0357.2 s10.6 GB
LTX-2.3-Distilled · int4 r128 + H64, smoothing off14.28 ± 4.400.4817.7859.3 s12.3 GB
LTX-2.3-Distilled · previous best, int4 r128 e0.25, no rotation15.99 ± 4.870.54—≈59 s12.3 GB
LTX-2.3-Distilled · bf16 reference flicker——6.39—35.4 GB
MiniMax-H3 · int4 r32 + H64 (A100)15.64 ± 1.850.5290.185 / 0.562005 s / 5 clips17.1 GB
MiniMax-H3 · int4 r32 plain14.52 ± 1.140.4570.214 / 0.582021 s17.1 GB
MiniMax-H3 · nvfp4 r32 + H64 (RTX)13.66 ± 1.490.4690.209 / 0.46—18.1 GB
MiniMax-H3 · nvfp4 r32 plain15.87 ± 2.010.5420.107 / 0.17924 s18.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.170.886 / 0.800 / 0.790LPIPS 0.18 / 0.22 / 0.22134 s / 269 s per image3.86 GB
Qwen-Image-2.1 · int4 r32 plain (earlier run, shared GPU)20.45 / 18.44 / 16.930.820 / 0.726 / 0.710LPIPS 0.26 / 0.36 / 0.36273 s / ~570 s3.86 GB
Qwen-Image-2.1 · nvfp4 r32 plain22.74 / 21.56 / 20.970.894 / 0.834 / 0.841LPIPS 0.17 / 0.21 / 0.1892 s / ~165 s4.07 GB
Qwen-Image-2.1 edits (12) · int4 r32 + H64, CFG off / 2.5 / 423.43 / 23.60 / 22.220.867 / 0.862 / 0.820LPIPS 0.14 / 0.14 / 0.18 · identity 0.89 / 0.78 / 0.7634 s / 68 s per edit3.86 GB
Qwen-Image-2.1 edits · int4 r32 plain21.84 / 21.84 / 21.380.836 / 0.827 / 0.783LPIPS 0.19 / 0.18 / 0.23 · identity 0.79 / 0.72 / 0.7081 s / ~190 s3.86 GB
Qwen-Image-2.1 edits · nvfp4 r32 plain25.27 / 23.39 / 23.000.889 / 0.856 / 0.843LPIPS 0.11 / 0.15 / 0.16 · identity 0.89 / 0.85 / 0.7739 s / ~75 s4.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.

LTX rain scene crop
LTX-2.3-Distilled rain scene, 2× crop: bf16, int4 without rotation, int4 with rotation. Rotation brings back the pedestrians, umbrella and storefronts.

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

Qwen sample 0 with rotation row
Qwen-Image-2.1: rows bf16 / nvfp4 / int4 / int4 + H64. Rotation restores the beanie and sign placement that plain int4 loses.
Qwen sample 3 with rotation row
Plain int4 leaves the frisbee floating; rotated int4 puts it back in the dog's mouth, as in bf16.
Qwen edit 3 with rotation row
Qwen-Image-2.1 edit. Rotated int4 keeps the leaping pose that plain int4 shifts.

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)
sample0: A street musician playing saxophone under a lamppost, holding a cardboard sign that says OPEN MIC, warm evening light.
sample1: A misty mountain valley at sunrise, a winding river reflecting golden clouds, pine forests on the slopes.
sample2: A rainy neon-lit city street at night, reflections on wet asphalt, people with umbrellas, cinematic.
sample3: A golden retriever mid-leap catching a frisbee in a sunlit meadow, motion blur in the grass.
sample4: A still life of oranges, a ceramic teapot and lace cloth on a wooden table, dramatic window light, fine texture.
Edits, 12 instructions (own edits 0–4, identity edit 5, blog Fidelity pairs 6–11)
edit0 (T2I sample 0): Change the text on the cardboard sign to say JAZZ TONIGHT.
edit1 (T2I sample 1): Add a red hot-air balloon floating above the river.
edit2 (T2I sample 2): Turn the rain into a snowy winter night, with snow covering the street and the umbrellas.
edit3 (T2I sample 3): Replace the golden retriever with a black labrador, keeping the frisbee and the leaping pose.
edit4 (T2I sample 4): Change the oranges into green apples and make the teapot cobalt blue.
edit5 (T2I sample 0): Keep the man's face and identity exactly the same; put him in a white suit on a concert stage under spotlights.
edit6 (blog_example-25.png): Show this woman full-body, walking along a wooden boardwalk through a mangrove forest, wearing a white T-shirt and khaki cargo pants, holding a straw hat.
edit7 (blog_example-27.png): Change her hairstyle to long, loose wavy hair falling over her shoulders; keep everything else the same.
edit8 (blog_example-29.png): Show her sitting in a cozy living room, smiling while opening a cardboard package, wearing the same sweater and scarf.
edit9 (blog_example-31.png): Place this foundation bottle on a wooden table by a sunny window, next to a cup of tea and a plate of strawberries.
edit10 (blog_example-33.png): Show this dress worn by a woman standing full-body in a warm-toned studio.
edit11 (blog_example-35.png): Show this bracelet worn on a woman's wrist as she sits at a wooden table with a teapot, wearing a linen blouse.

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

  1. Muyang Li et al., 2024. SVDQuant: Absorbing Outliers by Low-Rank Components for 4-Bit Diffusion Models. arXiv:2411.05007
  2. Guangxuan Xiao et al., 2022. SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. arXiv:2211.10438
  3. Jerry Chee et al., 2023. QuIP: 2-Bit Quantization of Large Language Models With Guarantees. arXiv:2307.13304
  4. Albert Tseng et al., 2024. QuIP#: Even Better LLM Quantization with Hadamard Incoherence and Lattice Codebooks. arXiv:2402.04396
  5. Saleh Ashkboos et al., 2024. QuaRot: Outlier-Free 4-Bit Inference in Rotated LLMs. arXiv:2404.00456
  6. Zechun Liu et al., 2024. SpinQuant: LLM quantization with learned rotations. arXiv:2405.16406
  7. 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).
targetconfignote
Blackwellnvfp4, rank 32, smooth_exponent 0.5Never add hadamard. Fastest and best quality everywhere.
A100 and older (int4)int4, rank 32, hadamard=True, smooth_exponent 0Reaches 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 Hub

nvfp4

# 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_size is fixed by precision: 16 for nvfp4, 64 for int4. Any other value is rejected.
  • rank is required. smooth_exponent (0 to 1, default 0.5) and hadamard are optional and apply only when pre_quantized=False.
  • At rank 32, smoothing makes no measurable difference, so "smooth_exponent": 0.0 or leaving it out are equivalent in quality.
  • Quantized layers are chosen automatically. To keep specific layers in bf16, pass exclude_targets=[...] to the config.
Share: X (Twitter) Facebook LinkedIn