Nunchaku Lite quantization in place - #14608
Conversation
Support `pre_quantized=False` in NunchakuLiteQuantizationConfig: targeted linears of an unquantized checkpoint are quantized at load time with data-free SVDQuant (weight-span smoothing, rank-r SVD low-rank branch, int4/nvfp4 group quantization) and packed directly into the kernel layout SVDQW4A4Linear consumes — no calibration data needed. The math lives in quantizers/nunchaku/data_free.py, which is pure torch and stays importable without the `kernels` package; quantization happens per weight in create_quantized_param so peak memory stays near the quantized model size. Packed outputs are tensor-for-tensor identical to DeepCompressor's Nunchaku W4A4 converter (verified against it on random weights; qweight byte-identical). `awq_w4a16` targets are not supported in this mode and raise. CPU tests validate shapes, round-trip reconstruction error, bias packing, and the quantizer flow; a gated GPU mixin test runs quantize-on-load end to end where kernels are available. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NAtDkGmAw2fzbcvjfC79w
When `pre_quantized=False` and `svdq_w4a4.targets` is omitted, the quantizer now infers targets from the model at load time: every nn.Linear whose dimensions satisfy the Nunchaku packing constraints is selected, minus modules matched by the new `modules_to_not_convert` config option or listed in the model's `_keep_in_fp32_modules`. Explicit target lists keep working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NAtDkGmAw2fzbcvjfC79w
Auto-inference no longer needs a hand-written modules_to_not_convert list:
targets are restricted to the model's repeated transformer-block stacks
(identical-class nn.ModuleLists), which structurally excludes embedders,
final projections, and modulation heads, and adaLN-style linears inside
blocks are skipped via default ("norm", "modulation") name patterns. An
explicit modules_to_not_convert replaces the default patterns. For
FLUX.2-klein-9B the zero-config inferred target set matches the curated
list exactly (144 targets).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NAtDkGmAw2fzbcvjfC79w
Clearer pairing with the svdq_w4a4 `targets` field, and avoids implying the bnb/torchao semantics of keeping modules in high precision at load: the option only filters data-free target inference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NAtDkGmAw2fzbcvjfC79w
Match the bitsandbytes loader contract: filter the load-time-produced packed parameter names out of missing_keys via update_missing_keys, and remove the consumed `weight`/`bias` checkpoint keys from unexpected_keys inside create_quantized_param. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NAtDkGmAw2fzbcvjfC79w
The module implements the SVDQuant math (smoothing, low-rank split, quantization, kernel packing) as opposed to utils.py's kernel runtime; name it after the algorithm. Data-free stays in the function names, where it describes the mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NAtDkGmAw2fzbcvjfC79w
Name the integration test after the loader mechanism (pre_quantized=False) rather than the algorithm mode, and rename the companion class attribute to quantize_on_load_config_dict to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NAtDkGmAw2fzbcvjfC79w
…he model load_model_dict_into_meta skipped any checkpoint key not present in the model's state dict before consulting the quantizer. In data-free mode the replaced SVDQW4A4Linear modules no longer have a `weight` parameter, so the checkpoint's `weight`/`bias` keys were silently dropped, the packed parameters stayed on the meta device, and dispatch_model failed with "Cannot copy out of meta tensor". Give a quantize-on-load quantizer (pre_quantized=False) the chance to claim such keys and materialize the module's packed parameters from them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X4NMaGamTgkHjL9EYqNZ7K
The weight-span smoothing exponent was hard-coded to 0.5. Make it an optional config field (default unchanged) so the smoothing strength can be tuned: 0 disables smoothing, 1 fully flattens per-channel weight spans. Only valid in data-free mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X4NMaGamTgkHjL9EYqNZ7K
Measured on a 19B video DiT: int4 at rank=128 recovers ~2.7 dB over rank=32 for ~16% more transformer memory, and the optimal smoothing strength decreases as rank grows - note the interaction so users sweep the exponent when raising rank instead of assuming the 0.5 default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X4NMaGamTgkHjL9EYqNZ7K
Experimental validation of data-free quantize-on-loadEvidence appendix for the claims in the PR description. All quality numbers use a seed-matched protocol: identical prompts, seeds, and GPU per comparison, scored as PSNR/SSIM against a bf16 reference — so they measure pure quantization-induced divergence. All measurements were taken with guidance genuinely disabled; note that on current 1. Data-free vs calibrated checkpoints (LTX-2.3-Distilled v1.1, 19B, t2v, 5 seeds)Calibrated = published checkpoints produced offline with per-layer grid-searched smoothing (scored on calibration activations) + GPTQ residual rounding, on identical targets/rank/group size. Same-GPU bf16 references.
Data-free matches or nominally exceeds calibrated on both precisions (consistent across three protocol variants). An ablation transplanting the calibrated checkpoints' per-layer smoothing vectors into the data-free pipeline changed nothing (13.18 vs 13.71 dB, within noise) — the calibrated pipeline's extra machinery does not help at the trajectory level on this model. The practical difference is load time only: ~40 s (pre-quantized) vs ~15–22 min of on-load SVDs (19B model). 2. Tuning: smooth_exponent and rank (int4, same protocol)Smoothing-exponent sweep (at the default rank 32) — flat within noise; the default 0.5 is fine and the knob alone is not a quality lever:
Rank sweep (at the default exponent 0.5) — rank moves quality substantially:
Full grid — the two knobs interact: the rank-128 optimum sits at a weaker exponent (0.25), plausibly because strong smoothing and a large SVD branch compete for the same outlier energy. Each row is the exponent sweep at a fixed rank; each column the rank sweep at a fixed exponent:
Best cell: rank 128 / exponent 0.25 — +2.7 dB over the defaults for ~16% more transformer memory and ~unchanged inference time (wins on 5/5 samples). Practical guidance documented in this PR's quantization docs: when raising Both knobs are set in the transformer = LTX2VideoTransformer3DModel.from_pretrained(
"rootonchair/LTX-2.3-Distilled-v1.1-Diffusers",
subfolder="transformer",
quantization_config=NunchakuLiteQuantizationConfig(
svdq_w4a4={
"precision": "int4",
"group_size": 64,
"rank": 128, # low-rank branch size (multiple of 16, or 0 to disable)
"smooth_exponent": 0.25, # optional; defaults to 0.5
},
pre_quantized=False,
),
torch_dtype=torch.bfloat16,
device_map="cuda",
)
3. Image-to-videoSame protocol, conditioning image + prompt + seed matched,
Conditioning collapses the divergence: quantized i2v is near-reference (~22 dB / 0.86 SSIM vs ~16 dB / 0.55 unconditioned), and the conditioning pathway itself is unaffected (frame-0 adherence equal to bf16; the ~34 dB ceiling is the model's own CRF-33 image recompression). 4. Second architecture: FLUX.2-klein-9B (image, 4-step distilled)Auto-inference selected all 144 eligible block linears with zero configuration (fused SwiGLU/QKV-MLP projections included; dims-based exclusions correct). nvfp4: 18.1 ± 3.3 dB / SSIM 0.70 vs bf16; int4: 16.4 ± 2.3 / 0.61; the smoothing exponent is quality-neutral on this architecture across 0.0–0.75. Hardware caveat (pre-existing, kernel-level — not introduced here): the int4 kernel path is ~3–4× slower than nvfp4 on sm_120 (reproduces with published pre-quantized checkpoints; e.g. 427 s vs 113 s per five 121-frame clips). On A100 (sm_80) int4 performs normally. Quantize-on-load cost for reference (one
Times are effectively independent of precision, Per-sample raw data (frames, metrics, seeds) for every table is retained and available on request. |
What does this PR do?
Adds data-free quantize-on-load to the Nunchaku Lite backend: pass
pre_quantized=Falseto quantize an unquantized checkpoint into the Nunchaku W4A4 packed format while its weights stream in — no calibration data, no offline conversion step.For each targeted linear: weight-span smoothing, a rank-
rSVD low-rank branch, and int4/nvfp4 group quantization of the residual, packed directly into the kernel layout (quantizers/nunchaku/svdquant.py, pure PyTorch). Whentargetsis omitted they are inferred structurally (repeated block stacks, adaLN/_keep_in_fp32_modulesexclusions, packing-constraint filter) — zero configuration on typical DiTs. Includes a fix inload_model_dict_into_metaletting a quantize-on-load quantizer claim checkpoint keys that no longer exist on the model (replaced modules haveqweight/… instead ofweight), and an optionalsmooth_exponentknob.Validation
rankis the effective quality lever (int4rank=128: +2.7 dB overrank=32for +16% transformer memory) and interacts withsmooth_exponent(larger ranks prefer weaker smoothing) — documented in the quantization docs.Hardware notes
nvfp4 requires Blackwell (sm_100+); int4 runs on Turing/Ampere/Ada and is the pre-Blackwell path. Known kernel-level issue, not introduced here: the int4 path is ~3-4x slower than nvfp4 on sm_120 (fine on A100), reproducible with existing pre-quantized checkpoints.
Self-review notes (AI-assisted development)
NunchakuLiteTesterMixin.test_nunchaku_lite_quantize_on_loadexists but no test class subclasses it yet, so quantize-on-load still lacks an e2e regression test against a real model — known gap, happy to wire a small-model test if reviewers want it in this PR.smooth_overridesmechanism (injecting precomputed per-layer smoothing vectors) was built for ablations and deliberately left out to keep the API surface minimal.