Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/source/en/quantization/nunchaku.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,30 @@ List each module you want to quantize under `svdq_w4a4` or `awq_w4a16`. A module
}
```

## Data-free quantization on load

Pass `pre_quantized=False` to quantize an *unquantized* checkpoint at load time with data-free SVDQuant — no calibration data is needed. Each targeted linear gets weight-span smoothing, a rank-`r` SVD low-rank branch, and int4 or nvfp4 group quantization of the residual, packed directly into the kernel layout. Only `svdq_w4a4` targets are supported in this mode, and each target's `in_features`/`out_features` must be multiples of 128 (with `rank` a multiple of 16, or 0 to disable the low-rank branch).

When `targets` is omitted, eligible targets are inferred automatically from the model's structure: quantization is restricted to the repeated transformer-block stacks (so embedders, final projections, and modulation heads outside the stacks stay unquantized), adaLN-style linears are skipped via the default `("norm", "modulation")` name patterns, and every remaining `nn.Linear` satisfying the packing constraints is selected. The model's `_keep_in_fp32_modules` is always honored. No configuration is needed for typical DiTs:

```python
import torch
from diffusers import Flux2Transformer2DModel, NunchakuLiteQuantizationConfig

transformer = Flux2Transformer2DModel.from_pretrained(
"black-forest-labs/FLUX.2-klein-9B",
subfolder="transformer",
quantization_config=NunchakuLiteQuantizationConfig(
svdq_w4a4={"precision": "nvfp4", "group_size": 16, "rank": 32},
pre_quantized=False,
),
torch_dtype=torch.bfloat16,
device_map="cuda",
)
```

Pass `exclude_targets` (substring match; it replaces the default name patterns) to exclude further modules, or an explicit `targets` list for full control. Quantization happens per weight as the checkpoint streams in, so peak memory stays near the quantized model size. The result is identical to loading a checkpoint produced offline by a data-free SVDQuant exporter.

## Fused kernels

The original [Nunchaku](https://github.com/nunchaku-ai/nunchaku) engine gets much of its speed from model-specific fused execution paths. It combines the Q, K, and V projections with RMSNorm and RoPE, and uses a fused GELU kernel for the MLP. Nunchaku Lite instead uses the standard Diffusers model with generic quantized linear layers, so it does not include these fusions.
Expand Down
81 changes: 79 additions & 2 deletions src/diffusers/quantizers/nunchaku/nunchaku_quantizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


if TYPE_CHECKING:
import torch

from ...models.modeling_utils import ModelMixin


Expand All @@ -19,7 +21,9 @@ class NunchakuLiteQuantizer(DiffusersQuantizer):
def __init__(self, quantization_config, **kwargs):
super().__init__(quantization_config, **kwargs)
self.compute_dtype = quantization_config.compute_dtype
self.pre_quantized = quantization_config.pre_quantized
# Quantize on load when either the loader inferred an unquantized
# checkpoint or the config explicitly requested `pre_quantized=False`.
self.pre_quantized = self.pre_quantized and quantization_config.pre_quantized

def validate_environment(self, *args, **kwargs):
if not is_kernels_available():
Expand Down Expand Up @@ -66,13 +70,86 @@ def _process_model_before_weight_loading(
):
from .utils import check_strict_state_dict_match, replace_with_nunchaku_linear

svdq_config = self.quantization_config.svdq_w4a4
if not self.pre_quantized and svdq_config is not None and svdq_config.get("targets") is None:
from .svdquant import infer_data_free_targets

svdq_config["targets"] = infer_data_free_targets(
model,
group_size=svdq_config["group_size"],
exclude_targets=self.quantization_config.exclude_targets or (),
)
logger.info(f"Inferred {len(svdq_config['targets'])} data-free quantization targets.")

quantization_config = self.quantization_config.to_dict()
num_replaced = replace_with_nunchaku_linear(model, quantization_config, self.compute_dtype)

if state_dict is not None:
if self.pre_quantized and state_dict is not None:
check_strict_state_dict_match(model, state_dict)
logger.info(f"Applied Nunchaku quantization config with {num_replaced} targets.")

def update_missing_keys(self, model, missing_keys: list[str], prefix: str) -> list[str]:
if self.pre_quantized:
return missing_keys
# In data-free mode the checkpoint holds `weight`/`bias` while the model
# expects the packed parameters; those are produced at load time.
from .svdquant import DATA_FREE_PARAMETER_NAMES

return [key for key in missing_keys if key.rpartition(".")[2] not in DATA_FREE_PARAMETER_NAMES]

def check_if_quantized_param(
self,
model: "ModelMixin",
param_value: "torch.Tensor",
param_name: str,
state_dict: dict[str, Any],
**kwargs,
) -> bool:
if self.pre_quantized:
return False
from .utils import SVDQW4A4Linear

module_name, _, tensor_name = param_name.rpartition(".")
if tensor_name not in ("weight", "bias") or not module_name:
return False
try:
module = model.get_submodule(module_name)
except AttributeError:
return False
return isinstance(module, SVDQW4A4Linear)

def create_quantized_param(
self,
model: "ModelMixin",
param_value: "torch.Tensor",
param_name: str,
target_device: "torch.device",
state_dict: dict[str, Any] | None = None,
unexpected_keys: list[str] | None = None,
**kwargs,
):
import torch

from .svdquant import pack_data_free_bias, quantize_linear_data_free

module_name, _, tensor_name = param_name.rpartition(".")
module = model.get_submodule(module_name)
if unexpected_keys is not None and param_name in unexpected_keys:
unexpected_keys.remove(param_name)
if tensor_name == "bias":
packed_bias = pack_data_free_bias(param_value.to(target_device), torch_dtype=self.compute_dtype)
module._parameters["bias"] = torch.nn.Parameter(packed_bias, requires_grad=False)
return
quantized = quantize_linear_data_free(
param_value.to(target_device),
precision=module.precision,
group_size=module.group_size,
rank=module.rank,
torch_dtype=self.compute_dtype,
)
for name, tensor in quantized.items():
module._parameters[name] = torch.nn.Parameter(tensor.to(target_device), requires_grad=False)

def _process_model_after_weight_loading(self, model: "ModelMixin", **kwargs):
return model

Expand Down
Loading
Loading