Skip to content

Add Nunchaku Lite single-file quantization#14100

Open
rootonchair wants to merge 27 commits into
huggingface:mainfrom
rootonchair:feature/nunchaku-lite-single-file
Open

Add Nunchaku Lite single-file quantization#14100
rootonchair wants to merge 27 commits into
huggingface:mainfrom
rootonchair:feature/nunchaku-lite-single-file

Conversation

@rootonchair

@rootonchair rootonchair commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds Nunchaku Lite single-file checkpoint loading for Diffusers models.

This introduces NunchakuLiteQuantizationConfig and a new Nunchaku Lite quantizer that can patch supported nn.Linear modules into runtime SVDQ/AWQ linear layers before strict checkpoint loading. The loader reads safetensors metadata during from_single_file so Nunchaku Lite checkpoints can use their embedded runtime manifest to decide which modules to replace.

Deprecated API

import torch
from diffusers import (
    ErnieImagePipeline,
    ErnieImageTransformer2DModel,
    NunchakuLiteQuantizationConfig,
)

checkpoint = hf_hub_download(
    repo_id="rootonchair/ERNIE-Image-Turbo-nunchaku-lite",
    filename="svdq-int4_r32-ernie-image-turbo-zero-svdq-fix-bias.safetensors",
)

transformer = ErnieImageTransformer2DModel.from_single_file(
    checkpoint,
    config="baidu/ERNIE-Image-Turbo",
    subfolder="transformer",
    quantization_config=NunchakuLiteQuantizationConfig(compute_dtype=torch.bfloat16),
    torch_dtype=torch.bfloat16,
)

pipe = ErnieImagePipeline.from_pretrained(
    "baidu/ERNIE-Image-Turbo",
    transformer=transformer,
    torch_dtype=torch.bfloat16,
)

pipe.to("cuda")

image = pipe(
    prompt="A modern red armchair in a quiet studio, soft window light, realistic product photography",
    height=512,
    width=512,
    num_inference_steps=8,
    guidance_scale=1.0,
    generator=torch.Generator(device="cuda").manual_seed(1234),
    use_pe=False,
).images[0]

image.save("ernie-image-turbo-nunchaku-lite.png")

New API for from_single_file use

import torch
from huggingface_hub import hf_hub_download

from diffusers import (
    ErnieImagePipeline,
    ErnieImageTransformer2DModel,
    NunchakuLiteQuantizationConfig,
)


dtype = torch.bfloat16
device = "cuda"

checkpoint = hf_hub_download(
    repo_id="rootonchair/ERNIE-Image-Turbo-nunchaku-lite",
    filename="svdq-int4_r32-ernie-image-turbo-zero-svdq-fix-bias.safetensors",
)

svdq_targets = []
for name in [
    "self_attention.to_q",
    "self_attention.to_k",
    "self_attention.to_v",
    "self_attention.to_out.0",
    "mlp.gate_proj",
    "mlp.up_proj",
    "mlp.linear_fc2",
]:
    svdq_targets.extend([f"layers.{i}.{name}" for i in range(36)])

quantization_config = NunchakuLiteQuantizationConfig(
    compute_dtype=dtype,
    svdq_w4a4={
        "precision": "int4",
        "group_size": 64,
        "rank": 32,
        "targets": svdq_targets,
    },
    awq_w4a16={
        "precision": "int4",
        "group_size": 64,
        "targets": [
            "text_proj",
            "time_embedding.linear_1",
            "time_embedding.linear_2",
            "adaLN_modulation.1",
            "final_norm.linear",
            "final_linear",
        ],
    },
)

transformer = ErnieImageTransformer2DModel.from_single_file(
    checkpoint,
    config="baidu/ERNIE-Image-Turbo",
    subfolder="transformer",
    quantization_config=quantization_config,
    torch_dtype=dtype,
)

pipe = ErnieImagePipeline.from_pretrained(
    "baidu/ERNIE-Image-Turbo",
    transformer=transformer,
    torch_dtype=dtype,
)

pipe.to(device)

image = pipe(
    prompt="A modern red armchair in a quiet studio, soft window light, realistic product photography",
    height=512,
    width=512,
    num_inference_steps=8,
    guidance_scale=1.0,
    generator=torch.Generator(device=device).manual_seed(1234),
    use_pe=False,
).images[0]

image.save("ernie-image-turbo-nunchaku-lite.png")

Fixes # (issue)

Before submitting

Who can review?

Anyone in the community is free to review the PR once the tests have passed. Feel free to tag
members/contributors who may be interested in your PR.

@github-actions github-actions Bot added size/L PR with diff > 200 LOC quantization tests single-file and removed size/L PR with diff > 200 LOC labels Jul 1, 2026
@rootonchair rootonchair marked this pull request as draft July 1, 2026 17:13

@sayakpaul sayakpaul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for getting started! Just did a first pass and left high-level reviews.

Comment thread src/diffusers/quantizers/nunchaku/nunchaku_quantizer.py Outdated
Comment thread src/diffusers/quantizers/nunchaku/nunchaku_quantizer.py Outdated
Comment thread src/diffusers/quantizers/quantization_config.py
@@ -0,0 +1,161 @@
import json

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For tests, WDYT of adding a mixin to https://github.com/huggingface/diffusers/blob/main/tests/models/testing_utils/quantization.py and then extending a popular model like Flux to use that mixin?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, let's do it that way

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this test then?

@rootonchair

rootonchair commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

I just did some benchmark on RTX PRO 6000, here is the visual result between bf16 and nvfp4 checkpoint for ERNIE-Image-Turbo

BF16 Nunchaku NVFP4
image image
Case Full mean Denoise mean Denoise peak alloc Full peak alloc Speedup
original 3.003s 2.862s 29.429GB 31.081GB 1.0x
nunchaku_lite NVFP4 2.271s 2.127s 18.926GB 20.578GB 1.35x
nunchaku_lite NVFP4 + compile 1.675s 1.525s 18.672GB 20.578GB 1.8x
nunchaku_lite NVFP4 + bnb text encoder 2.285s 2.132s 14.317GB 15.969GB 1.35x

By replacing Nunchaku Linear, we have reduced the latency of these linear operations by 2x with large shape

Target Op Rows Shape Normal ms Nunchaku ms Speedup
layers.0.self_attention.to_q svdq_w4a4 4096 4096 -> 4096 0.3660 0.1563 2.34x
layers.0.mlp.gate_proj svdq_w4a4 4096 4096 -> 12288 1.0646 0.4272 2.49x
layers.0.mlp.linear_fc2 svdq_w4a4 4096 12288 -> 4096 1.0269 0.4596 2.23x

One note here, AWQ only benefit with adaLN layer, so other modules like time embedding or final linear can stay as bf16

Target Op Rows Shape Normal ms Nunchaku ms Speedup
text_proj awq_w4a16 1 3072 -> 4096 0.0111 0.0201 0.55x
time_embedding.linear_1 awq_w4a16 1 4096 -> 4096 0.0129 0.0251 0.52x
time_embedding.linear_2 awq_w4a16 1 4096 -> 4096 0.0129 0.0247 0.52x
adaLN_modulation.1 awq_w4a16 1 4096 -> 24576 0.1389 0.0243 5.71x
final_norm.linear awq_w4a16 1 4096 -> 8192 0.0220 0.0245 0.90x
final_linear awq_w4a16 4114 4096 -> 128 0.0248 0.0635 0.39x

@github-actions github-actions Bot added the size/L PR with diff > 200 LOC label Jul 2, 2026
@rootonchair

Copy link
Copy Markdown
Contributor Author

I have just implemented the native loading feature, which now can load by from_pretrained with converted repo:

import torch
from diffusers import ErnieImagePipeline

pipe = ErnieImagePipeline.from_pretrained(
    "rootonchair/ERNIE-Image-Turbo-nunchaku-lite-int4",
    torch_dtype=torch.bfloat16,
).to("cuda")

image = pipe(
    prompt="A modern red armchair in a quiet studio, soft window light, realistic product photography",
    height=1024,
    width=1024,
    num_inference_steps=8,
    guidance_scale=1.0,
    use_pe=False,
).images[0]

image.save("ernie-image-turbo-nunchaku-lite-int4.png")

Quantization config now change to:

"quantization_config": {
    "awq_w4a16": {
      "group_size": 64,
      "precision": "int4",
      "targets": [
        "text_proj",
        "time_embedding.linear_1",
        "time_embedding.linear_2",
        "adaLN_modulation.1",
        "final_norm.linear",
        "final_linear"
      ]
    },
    "compute_dtype": "bfloat16",
    "quant_method": "nunchaku_lite",
    "svdq_w4a4": {
      "group_size": 16,
      "precision": "fp4",
      "rank": 32,
      "targets": [
        "layers.0.self_attention.to_q",
        "layers.1.self_attention.to_q",
        "layers.2.self_attention.to_q",
        "layers.3.self_attention.to_q",
         ...
      ]
    }

If we agree to use this schema, I will remove the old metadata/from_single_file approach

@sayakpaul sayakpaul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good. I think we can remove all metadata related code?

Comment thread src/diffusers/quantizers/nunchaku/nunchaku_quantizer.py
Comment thread src/diffusers/quantizers/quantization_config.py
Comment thread src/diffusers/quantizers/quantization_config.py
Comment thread src/diffusers/quantizers/quantization_config.py

@sayakpaul sayakpaul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No rush but let us know once you would like another round of review.

@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@github-actions github-actions Bot added documentation Improvements or additions to documentation and removed single-file labels Jul 3, 2026
@rootonchair

Copy link
Copy Markdown
Contributor Author

@sayakpaul I think this is ready for the next review

@sayakpaul sayakpaul requested review from SunMarc and asomoza July 3, 2026 15:03

@sergereview sergereview Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤗 Serge says:

This adds a new nunchaku_lite quantization backend (config, quantizer, runtime linear layers backed by the HF kernels package, docs, and tests). The overall structure follows existing quantizer integrations, but there are a few blocking issues.

Security

  • _get_ops() in src/diffusers/quantizers/nunchaku/utils.py calls get_kernel(..., trust_remote_code=True) against a personal user repo (rootonchair/nunchaku-lite-kernels). This silently enables remote code execution for anyone who loads a Nunchaku Lite checkpoint, with no user opt-in. No other kernel usage in the repo does this (gguf and attention_dispatch call get_kernel without trust_remote_code). This needs to be removed or made an explicit user decision, and the kernel repo should ideally live under an org namespace with pinned revisions.

Description vs. diff mismatch

  • The PR description claims "The loader reads safetensors metadata during from_single_file so Nunchaku Lite checkpoints can use their embedded runtime manifest" — but there are no changes to single_file_model.py or any loader in this diff. Correspondingly, the metadata parameter of NunchakuLiteQuantizer._process_model_before_weight_loading is never populated by any call site and is dead code.

Correctness

  • NunchakuLiteTesterMixin._test_quantized_layers is tautological: it sets expected_quantized_layers = num_quantized_layers and then asserts they're equal, so the count check can never fail. It should compare against the number of targets in the quantization config (like the base mixin compares against linear-layer count).
  • The docs and the NunchakuLiteQuantizationConfig docstring repeatedly reference model.json, but Diffusers model configs (and the test in this PR) use config.json. As written, users following the doc will put the quantization config in a file Diffusers never reads.

Style

  • nunchaku_quantizer.py has trailing whitespace (line 64) — make style was apparently not run.
  • New source files are missing the Apache license header used across src/diffusers.
  • NunchakuLiteQuantizationConfig is appended to _import_structure via a standalone statement instead of being listed in the dict literal like every other unconditional export.
  • import itertools is buried inside check_strict_state_dict_match; move it to module top level.
  • SVDQW4A4Linear re-validates precision/group_size that NunchakuLiteQuantizationConfig.post_init already enforces — per repo guidelines, drop the duplicated defensive checks (the forward path hard-codes group sizes 16/64 anyway, so the group_size argument is effectively unused at runtime).

serge v0.1.0 · model: claude-fable-5 · 10 LLM turns · 22 tool calls · 390.5s · 437374 in / 30106 out tokens

if _ops is None:
from kernels import get_kernel

_ops = get_kernel(_HF_KERNEL_REPO, version=_HF_KERNEL_VERSION, trust_remote_code=True).ops

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: trust_remote_code=True silently enables execution of arbitrary remote code from a personal user repo whenever a Nunchaku Lite checkpoint is loaded — the user never opts in. Neither of the existing get_kernel call sites in this repo (quantizers/gguf/utils.py, models/attention_dispatch.py) passes trust_remote_code. Please drop it (publish the kernel as a standard prebuilt kernels repo that doesn't require remote code), or at minimum surface this as an explicit user-facing opt-in. Hosting under a personal namespace (rootonchair/...) rather than an org also makes this a supply-chain risk for everyone using the backend.

self,
model: "ModelMixin",
state_dict: dict[str, Any] | None = None,
metadata: dict[str, str] | None = None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metadata is never passed by any caller — neither modeling_utils.py nor single_file_model.py forwards safetensors metadata to preprocess_model. The PR description claims the loader "reads safetensors metadata during from_single_file", but no loader changes exist in this diff. Per the repo guidelines (no unused parameters "for API consistency"), remove this parameter.

Comment thread src/diffusers/quantizers/nunchaku/nunchaku_quantizer.py Outdated
self._verify_if_layer_quantized(name, module, config_kwargs)
num_quantized_layers += 1

expected_quantized_layers = num_quantized_layers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion is tautological: expected_quantized_layers is set to num_quantized_layers, so the num_quantized_layers == expected_quantized_layers check below can never fail, and num_fp32_modules is always 0. The only effective check left is num_quantized_layers > 0. Compute the expected count from the config instead, e.g. the total number of entries in svdq_w4a4["targets"] + awq_w4a16["targets"], so the test actually verifies all targets were replaced.

Comment thread docs/source/en/quantization/nunchaku.md Outdated
The exported state dict must match the target Diffusers model architecture exactly. Checkpoints quantized with
fused QKV projections won't load into a model config that expects separate Q, K, and V projection modules.

Example compact `model.json` config:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the doc page: Diffusers reads the model config from config.json, not model.json. Please fix the filename here and in the "quantization_config stored in model.json" sentence above so users don't package their checkpoints incorrectly.

Comment thread src/diffusers/__init__.py Outdated
],
}

_import_structure["quantizers.quantization_config"].append("NunchakuLiteQuantizationConfig")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this export is unconditional, add "NunchakuLiteQuantizationConfig" directly to the "quantizers.quantization_config" list in the _import_structure dict literal above instead of appending via a standalone statement — that matches how every other unconditional export is declared. Note also that NunchakuLiteQuantizationConfig.__init__ references torch unconditionally, while all other quantization configs exported here are gated behind is_torch_available(); consider whether this one needs the same guard.

Comment thread src/diffusers/quantizers/nunchaku/utils.py Outdated
if device is None:
device = torch.device("cpu")

if precision not in {"int4", "nvfp4"}:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These precision/group_size checks duplicate validation that NunchakuLiteQuantizationConfig.post_init already enforces (and post_init is stricter: it pins group_size to 16 for fp4 / 64 for int4, while this accepts any positive value that forward then ignores — the activation-scale layout hard-codes 16/64). Per the repo's no-defensive-code guideline, drop the re-validation here and rely on the config.

rootonchair and others added 3 commits July 3, 2026 16:11
Co-authored-by: sergereview[bot] <283583894+sergereview[bot]@users.noreply.github.com>
Co-authored-by: sergereview[bot] <283583894+sergereview[bot]@users.noreply.github.com>
rootonchair and others added 2 commits July 3, 2026 23:14
Co-authored-by: sergereview[bot] <283583894+sergereview[bot]@users.noreply.github.com>
@github-actions github-actions Bot added the utils label Jul 3, 2026
@rootonchair rootonchair marked this pull request as ready for review July 6, 2026 06:35

@SunMarc SunMarc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks ! added some comments

if _ops is None:
from kernels import get_kernel

_ops = get_kernel(_HF_KERNEL_REPO, version=_HF_KERNEL_VERSION, trust_remote_code=True).ops

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make it a trusted organization @sayakpaul ? not sure if this is good to leave trust_remote_code=True. Also, I think it is fine to not pin the version unless we really need it no ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ALWAYS recommended to pin a version when using kernels. For now, trust_remote_code=True is fine. We can flip it once there's usage and more community trust.

Comment on lines +11 to +21
_HF_KERNEL_REPO = "rootonchair/nunchaku-lite-kernels"
_HF_KERNEL_VERSION = 1


_ops = None


def _get_ops():
global _ops
if _ops is None:
from kernels import get_kernel

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make this more specific since this function is just there to load the nunchaku_kernels. maybe just all it nunchaku_kernels ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, can we avoid using global?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This for loading the kernels lazily, I don't want the user to load the nunchaku kernels if they don't use it. Or am I just overthink it? 😅

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this has been resolved

Comment on lines +339 to +356
def check_strict_state_dict_match(model: nn.Module, state_dict: dict[str, Any]) -> None:
expected_keys = {n for n, _ in itertools.chain(model.named_parameters(), model.named_buffers())}
loaded_keys = set(state_dict.keys())
missing_keys = sorted(expected_keys - loaded_keys)
unexpected_keys = sorted(loaded_keys - expected_keys)
if missing_keys or unexpected_keys:
message = "Nunchaku checkpoint keys must exactly match the patched model state dict."
if missing_keys:
message += f" Missing keys: {missing_keys[:10]}"
if len(missing_keys) > 10:
message += f" and {len(missing_keys) - 10} more"
message += "."
if unexpected_keys:
message += f" Unexpected keys: {unexpected_keys[:10]}"
if len(unexpected_keys) > 10:
message += f" and {len(unexpected_keys) - 10} more"
message += "."
raise ValueError(message)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need this no ? we already print some logs when there are missing or unexpected keys.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want a hard fail on this as partial loaded model will make inference incorrect and produce noisy results

precision="nvfp4" if precision == "fp4" else precision,
group_size=group_size,
torch_dtype=compute_dtype,
device=_module_device(module),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the module is practically always on meta device when loading with low_cpu_mem_usage. I think it should be fine to leave device to None no ? Also, if you want to be sure that the modules are loaded on meta device, we usually use

ctx = init_empty_weights if is_accelerate_available() else nullcontext
            with ctx():

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I think using ctx would be more correct

Comment on lines +35 to +38
def update_torch_dtype(self, torch_dtype):
if torch_dtype is None:
torch_dtype = self.compute_dtype
return torch_dtype

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the torch_dtype passed in from_pretrained don't match the compute_dtype specified in the quantization_config ? there will be an issue no ? Should be overwrite compute_dtype in that case ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes we should

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is addressed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep

Comment on lines +297 to +298
if op == "svdq_w4a4":
replacement = SVDQW4A4Linear(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this only works on specific machines, should we catch this beforehand or just let the user get an error when trying to get/run the kernels ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should let the kernels library handling it when loading. @sayakpaul WDYT

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added the validation to validate_environment for this

out_features,
rank=rank,
bias=bias is not None,
precision="nvfp4" if precision == "fp4" else precision,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just use nvfp4 instead of fp4 when naming the precision ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be a good idea to make the precision name shorter. But I will change it back

Comment thread docs/source/en/quantization/nunchaku.md Outdated
Comment on lines +73 to +76
| `svdq_w4a4` | `fp4` | 16 | Uses NVFP4 runtime kernels with SVDQ low-rank correction. |
| `svdq_w4a4` | `int4` | 64 | Uses INT4 W4A4 kernels with SVDQ low-rank correction. |
| `awq_w4a16` | `int4` | 64 | Uses INT4 weight-only AWQ-style kernels. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should add some recommendation of hardware to be used with ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I will be a good addition

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was not addressed? I think should mention a matrix of the quant schemes and their supported hardware / CUDA capabilities. Also, the note on hopper since we cannot support it.

@asomoza

asomoza commented Jul 8, 2026

Copy link
Copy Markdown
Member

nice, I tested it and made a krea model, it works pretty good!

comparison_bf16_vs_nvfp4
import torch
from diffusers import DiffusionPipeline

pipe = DiffusionPipeline.from_pretrained("OzzyGT/Krea_2_Turbo_nunchaku_lite_nvfp4", torch_dtype=torch.bfloat16)
pipe.to("cuda")

prompt = (
    "A cozy corner bookstore-cafe on a rainy evening, cinematic wide shot. "
    'A large hand-lettered chalkboard sign in the window reads "FRESH COFFEE & OLD BOOKS" '
    "and below it in smaller chalk letters \"open 'til late\". "
    "Warm golden light spills onto wet cobblestones that mirror pink and blue neon reflections. "
    "Inside, tall mahogany shelves are packed with hundreds of colorful book spines with tiny legible titles, "
    "a barista in a striped apron pours delicate latte art, steam curling upward, "
    "a tabby cat sleeps on a windowsill beside a stack of paperbacks. "
    "Intricate detail, sharp focus, shallow depth of field, photorealistic, rich color grading."
)

image = pipe(
    prompt,
    num_inference_steps=8,
    guidance_scale=0.0,
    height=1024,
    width=1024,
    generator=torch.Generator("cuda").manual_seed(7),
).images[0]
image.save("sample.png")

@sayakpaul sayakpaul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks a lot for the further updates. I will post about the benchmarking I conducted through HF Jobs and Claude Code below.

Comment thread docs/source/en/quantization/nunchaku.md Outdated
Compile the quantized transformer after loading the pipeline for faster inference.

```python
pipe.transformer = torch.compile(pipe.transformer, mode="default", fullgraph=False)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we do it fullgraph=True? Can you open an issue on the nunchaku-lite repo with the trace?


# Nunchaku Lite

Nunchaku Lite is a quantization backend for loading prequantized checkpoints in Diffusers. Use

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should provide some citations to the original Nunchaku repo since Nunchaku Lite is based off of it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not only the repo, I will reference their paper too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is resolved

Comment thread docs/source/en/quantization/nunchaku.md Outdated
use_pe=False,
).images[0]
image.save("ernie-image-turbo-nunchaku-lite.png")
```

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it compatible with offloading, too?

Comment thread docs/source/en/quantization/nunchaku.md Outdated
Comment on lines +73 to +76
| `svdq_w4a4` | `fp4` | 16 | Uses NVFP4 runtime kernels with SVDQ low-rank correction. |
| `svdq_w4a4` | `int4` | 64 | Uses INT4 W4A4 kernels with SVDQ low-rank correction. |
| `awq_w4a16` | `int4` | 64 | Uses INT4 weight-only AWQ-style kernels. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was not addressed? I think should mention a matrix of the quant schemes and their supported hardware / CUDA capabilities. Also, the note on hopper since we cannot support it.

self.compute_dtype = quantization_config.compute_dtype
self.pre_quantized = quantization_config.pre_quantized

def validate_environment(self, *args, **kwargs):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Hopper validation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopper validation added

Comment on lines +35 to +38
def update_torch_dtype(self, torch_dtype):
if torch_dtype is None:
torch_dtype = self.compute_dtype
return torch_dtype

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is addressed?

Comment thread src/diffusers/quantizers/nunchaku/nunchaku_quantizer.py Outdated

self.post_init()

def post_init(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice validation!

@@ -0,0 +1,161 @@
import json

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this test then?

@sayakpaul

Copy link
Copy Markdown
Member

Even though the below is generated with Claude Code (I have manually verified them).

Nunchaku Lite (PR #14100) — testing notes

I ran the snippet from docs/source/en/quantization/nunchaku.md end-to-end. TL;DR: the compiled NVFP4 benchmark in the doc reproduces cleanly on the same class of hardware, but I hit two rough edges (hardware/torch-version compatibility) that are worth documenting or guarding.

Reproduced the doc benchmark ✅

Ran the exact doc snippet (rootonchair/ERNIE-Image-Turbo-nunchaku-lite-nvfp4, 1024×1024, 8 steps, use_pe=False) on an RTX PRO 6000 Blackwell (via HF Jobs, --flavor rtx-pro-6000) — the same GPU the doc benchmarks on.

Variant Latency (mean of 3)
No compile 2.279 s
torch.compile(mode="default", fullgraph=False) 1.643 s
Speedup (compile vs eager) 1.39×
  • First compiled call (compilation): ~51 s
  • Peak memory: 22.1 GB

These match the doc's reported 2.271 s → 1.675 s almost exactly, so the NVFP4 + torch.compile claim holds up. 👍

Rough edge 1: capability check is more permissive than the shipped kernels

NunchakuLiteQuantizer.validate_environment allows INT4 on any GPU with capability >= (7, 5) (Turing+). But the kernel build actually fetched for INT4 (rootonchair/nunchaku-lite-kernels, torch212-cxx11-cu126) only contains SM images for sm_75 / 80 / 86 / 89. Verified with cuobjdump -lelf on the downloaded .so.

Consequence: on an H100 (sm_90) the INT4 path passes validate_environment and then dies at inference with:

CUDA error: no kernel image is available for execution on the device

which is a confusing failure mode (looks like a runtime bug, not an unsupported-hardware message). So Hopper is effectively unsupported for INT4 today even though the check green-lights it.

Suggestions (pick whatever fits):

  • Add sm_90 to the INT4 kernel build, or
  • Tighten/clarify the check so unsupported archs fail early with a clear message (e.g. "no INT4 kernel image for sm_90"), or
  • Note the actually-supported archs in the docs.

Rough edge 2: kernels only ship for torch 2.11 / 2.12

kernels.get_kernel matches builds by torch major.minor. The repo currently only has builds for torch 2.11 and 2.12 (cu126/cu128/cu130/cu132). Installing any other torch (I first tried 2.8) fails hard:

FileNotFoundError: Cannot find a build variant for this system ...
  torch211-cxx11-cu128-x86_64-linux: Torch version (2.11) does not match environment Torch version (2.8)
  ...

For Blackwell specifically, the working combo is torch 2.11 + cu128 (torch211-cxx11-cu128).

Suggestion: document the supported torch versions in nunchaku.md (and ideally surface a friendlier error / requirement up front), since this isn't obvious and the failure only appears at load time.

Environment

@sayakpaul

Copy link
Copy Markdown
Member

Now running the tests.

@sayakpaul

Copy link
Copy Markdown
Member

Testing report (again manually verified).

Nunchaku Lite (PR #14100) — test run on HF Jobs

Ran the PR's test suite tests/quantization/nunchaku/test_nunchaku.py on Hugging Face Jobs.

Setup

  • Hardware: HF Jobs rtx-pro-6000 — RTX PRO 6000 Blackwell, capability (12, 0)
  • Stack: torch 2.11.0+cu128, kernels 0.16.0, diffusers @ PR Add Nunchaku Lite single-file quantization #14100 head
  • Env: RUN_NIGHTLY=1, DIFFUSERS_TEST_DEVICE=cuda (so nothing is skipped, including the @nightly CUDA kernel test)

Result

9 passed in 4.66s ✅ (pytest exit code 0), including NunchakuLiteCudaKernelsTests::test_awq_cuda_kernels, which executes the real AWQ W4A16 kernel on GPU.

NunchakuLiteBasicTests::test_compact_config_round_trips_dtype_and_targets           PASSED
NunchakuLiteBasicTests::test_environment_requires_cuda                              PASSED
NunchakuLiteBasicTests::test_int4_environment_allows_turing_cuda                    PASSED
NunchakuLiteBasicTests::test_int4_environment_requires_turing_cuda                  PASSED
NunchakuLiteBasicTests::test_nvfp4_environment_allows_blackwell_cuda                PASSED
NunchakuLiteBasicTests::test_nvfp4_environment_requires_blackwell_cuda              PASSED
NunchakuLiteBasicTests::test_compact_config_replaces_svdq_and_awq_without_state_dict PASSED
NunchakuLiteBasicTests::test_nunchaku_lite_loads_with_from_pretrained               PASSED
NunchakuLiteCudaKernelsTests::test_awq_cuda_kernels                                 PASSED

One caveat (not a PR bug)

The 3 tests that download the kernel initially failed due to an external dependency bug: kernels passes an empty user_agent to huggingface_hub, producing a User-Agent header with a trailing space:

httpx.LocalProtocolError: Illegal header value b'kernels/0.16.0; hf_hub/1.22.0; python/3.12.12; '

huggingface_hub 1.x uses httpx, which rejects trailing whitespace. It only triggers on a cold first get_kernel call. Worked around by stripping the UA (a small sitecustomize.py patching huggingface_hub.utils._headers). Pinning huggingface_hub<1.0 does not help — it drags kernels down to 0.12.3, which lacks the trust_remote_code= argument the PR uses.

Suggestion: report to kernels upstream, and consider bumping the PR's require_kernels_version_greater_or_equal("0.9.0") to a version that actually provides trust_remote_code.

Co-authored-by: Sayak Paul <spsayakpaul@gmail.com>
@rootonchair rootonchair force-pushed the feature/nunchaku-lite-single-file branch from 03399a0 to 5522954 Compare July 9, 2026 16:15
@rootonchair rootonchair force-pushed the feature/nunchaku-lite-single-file branch from 5522954 to 86cd7c8 Compare July 9, 2026 16:17
Comment thread docs/source/en/quantization/overview.md Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation quantization size/L PR with diff > 200 LOC tests utils

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants