Skip to content
Open
2 changes: 2 additions & 0 deletions docs/source/en/_toctree.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@
title: bitsandbytes
- local: quantization/gguf
title: gguf
- local: quantization/comfy_quant
title: Comfy Quant
- local: quantization/nunchaku
title: Nunchaku Lite
- local: quantization/torchao
Expand Down
77 changes: 77 additions & 0 deletions docs/source/en/quantization/comfy_quant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!--Copyright 2026 The HuggingFace Team. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.

-->

# Comfy Quant

The [Comfy Quant](https://github.com/Comfy-Org/comfy-quants) toolkit provides state-of-the-art quantization techniques. While `comfy-quants` is used for exporting and quantizing models, Diffusers natively supports running inference on these models using the [comfy-kitchen](https://github.com/Comfy-Org/comfy-kitchen) library.

`comfy-kitchen` provides highly optimized GPU kernels that allow you to seamlessly run quantized layers. By passing a `ComfyQuantConfig` to Diffusers, the library will dynamically intercept parameters and wrap them in a `QuantizedTensor` that maps directly to the optimized `comfy-kitchen` layouts.

Before starting, please install `comfy-kitchen` in your environment:

```shell
pip install comfy-kitchen
```

## Loading a Comfy Quant Model

To load a model prequantized with Comfy Quant, use the [`~FromSingleFileMixin.from_single_file`] method and pass in the [`ComfyQuantConfig`].

The configuration requires you to specify the `quant_format` that the model was quantized in, and the `compute_dtype` for active inference calculations.

The following example demonstrates how to load a quantized FLUX transformer:

```python
import torch
from diffusers import FluxPipeline, FluxTransformer2DModel, ComfyQuantConfig

ckpt_path = "path/to/comfy_quant_checkpoint.safetensors"

# Initialize the config with your desired format and compute dtype
quantization_config = ComfyQuantConfig(
quant_format="fp8",
compute_dtype=torch.bfloat16
)

# Load the transformer directly from the safetensors file
transformer = FluxTransformer2DModel.from_single_file(
ckpt_path,
quantization_config=quantization_config,
dtype=torch.bfloat16,
)

# Pass the quantized transformer into the pipeline
pipe = FluxPipeline.from_pretrained(
"black-forest-labs/FLUX.1-dev",
transformer=transformer,
dtype=torch.bfloat16,
)
pipe.enable_model_cpu_offload()

prompt = "A cat holding a sign that says hello world"
image = pipe(prompt, generator=torch.manual_seed(0)).images[0]
image.save("flux-comfy-quant.png")
```

## Supported Quantization Formats

Diffusers currently maps the following Comfy Quant formats to `comfy-kitchen` layouts:

- **FP8** (`fp8`): Maps to `TensorCoreFP8Layout` (E4M3/E5M2)
- **INT8** (`int8`): Maps to `TensorCoreInt8Layout` (W8A8, tensorwise)
- **MXFP8** (`mxfp8`): Maps to `TensorCoreMXFP8Layout`
- **NVFP4** (`nvfp4`): Maps to `TensorCoreNVFP4Layout`
- **INT4 SVD** (`int4_svd`): Maps to `SVDQuantW4A4Layout` (SVDQuant W4A4)
- **INT4 AWQ** (`int4_awq`): Maps to `AWQW4A16Layout` (AWQ W4A16)

When using optimized layouts, `comfy-kitchen` automatically dispatches the operations to the best available backend (HIP, CUDA, Triton, or Eager).
22 changes: 22 additions & 0 deletions src/diffusers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
is_accelerate_available,
is_auto_round_available,
is_bitsandbytes_available,
is_comfy_kitchen_available,
is_gguf_available,
is_librosa_available,
is_note_seq_available,
Expand Down Expand Up @@ -47,6 +48,7 @@
"schedulers": [],
"utils": [
"OptionalDependencyNotAvailable",
"is_comfy_kitchen_available",
"is_inflect_available",
"is_invisible_watermark_available",
"is_librosa_available",
Expand Down Expand Up @@ -158,6 +160,18 @@
else:
_import_structure["quantizers.quantization_config"].append("SDNQConfig")

try:
if not is_torch_available() and not is_accelerate_available() and not is_comfy_kitchen_available():
raise OptionalDependencyNotAvailable()
except OptionalDependencyNotAvailable:
from .utils import dummy_comfy_kitchen_objects

_import_structure["utils.dummy_comfy_kitchen_objects"] = [
name for name in dir(dummy_comfy_kitchen_objects) if not name.startswith("_")
]
else:
_import_structure["quantizers.quantization_config"].append("ComfyQuantConfig")

try:
if not is_onnx_available():
raise OptionalDependencyNotAvailable()
Expand Down Expand Up @@ -1048,6 +1062,14 @@
else:
from .quantizers.quantization_config import SDNQConfig

try:
if not is_comfy_kitchen_available():
raise OptionalDependencyNotAvailable()
except OptionalDependencyNotAvailable:
from .utils.dummy_comfy_kitchen_objects import *
else:
from .quantizers.quantization_config import Com

try:
if not is_onnx_available():
raise OptionalDependencyNotAvailable()
Expand Down
2 changes: 1 addition & 1 deletion src/diffusers/pipelines/ltx2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@
_import_structure["pipeline_ltx2_condition"] = ["LTX2ConditionPipeline", "LTX2VideoCondition"]
_import_structure["pipeline_ltx2_dfr"] = ["LTX2DFRPipeline"]
_import_structure["pipeline_ltx2_dfr_temporal_refine"] = ["LTX2DFRTemporalRefinePipeline"]
_import_structure["pipeline_output"] = ["LTX2DFRPipelineOutput", "LTX2PipelineOutput", "LTX2VideoDecodeOutput"]
_import_structure["pipeline_ltx2_diffusion_decode"] = ["LTX2VideoDiffusionDecodePipeline"]
_import_structure["pipeline_ltx2_hdr_lora"] = ["LTX2HDRPipeline", "LTX2HDRReferenceCondition"]
_import_structure["pipeline_ltx2_ic_lora"] = ["LTX2InContextPipeline", "LTX2ReferenceCondition"]
_import_structure["pipeline_ltx2_image2video"] = ["LTX2ImageToVideoPipeline"]
_import_structure["pipeline_ltx2_latent_upsample"] = ["LTX2LatentUpsamplePipeline"]
_import_structure["pipeline_output"] = ["LTX2DFRPipelineOutput", "LTX2PipelineOutput", "LTX2VideoDecodeOutput"]
_import_structure["vocoder"] = ["LTX2Vocoder", "LTX2VocoderWithBWE"]

if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
Expand Down
4 changes: 4 additions & 0 deletions src/diffusers/quantizers/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@

from .autoround import AutoRoundQuantizer
from .bitsandbytes import BnB4BitDiffusersQuantizer, BnB8BitDiffusersQuantizer
from .comfy_quant import ComfyQuantizer
from .gguf import GGUFQuantizer
from .modelopt import NVIDIAModelOptQuantizer
from .nunchaku import NunchakuLiteQuantizer
from .quantization_config import (
AutoRoundConfig,
BitsAndBytesConfig,
ComfyQuantConfig,
GGUFQuantizationConfig,
NunchakuLiteQuantizationConfig,
NVIDIAModelOptConfig,
Expand All @@ -43,6 +45,7 @@
AUTO_QUANTIZER_MAPPING = {
"bitsandbytes_4bit": BnB4BitDiffusersQuantizer,
"bitsandbytes_8bit": BnB8BitDiffusersQuantizer,
"comfy_quant": ComfyQuantizer,
"gguf": GGUFQuantizer,
"quanto": QuantoQuantizer,
"torchao": TorchAoHfQuantizer,
Expand All @@ -55,6 +58,7 @@
AUTO_QUANTIZATION_CONFIG_MAPPING = {
"bitsandbytes_4bit": BitsAndBytesConfig,
"bitsandbytes_8bit": BitsAndBytesConfig,
"comfy_quant": ComfyQuantConfig,
"gguf": GGUFQuantizationConfig,
"quanto": QuantoConfig,
"torchao": TorchAoConfig,
Expand Down
1 change: 1 addition & 0 deletions src/diffusers/quantizers/comfy_quant/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .comfy_quantizer import ComfyQuantizer
126 changes: 126 additions & 0 deletions src/diffusers/quantizers/comfy_quant/comfy_quantizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from typing import TYPE_CHECKING, Any

from ...utils import (
get_module_from_name,
is_torch_available,
logging,
)
from ..base import DiffusersQuantizer


if TYPE_CHECKING:
from ...models.modeling_utils import ModelMixin

if is_torch_available():
import torch


logger = logging.get_logger(__name__)


class ComfyQuantizer(DiffusersQuantizer):
"""
Quantizer for comfy-kitchen formats (FP8, INT8, etc.).
"""

use_keep_in_fp32_modules = True
requires_calibration = False
required_packages = ["comfy_kitchen"]

def __init__(self, quantization_config, **kwargs):
super().__init__(quantization_config, **kwargs)
self.quant_format = getattr(quantization_config, "quant_format", "fp8")
self.compute_dtype = quantization_config.compute_dtype
self.modules_to_not_convert = quantization_config.modules_to_not_convert or []
if not isinstance(self.modules_to_not_convert, list):
self.modules_to_not_convert = [self.modules_to_not_convert]

def validate_environment(self, *args, **kwargs):
from ...utils.import_utils import is_comfy_kitchen_available

if not is_comfy_kitchen_available():
raise ImportError(
"Loading Comfy Quant weights requires `comfy-kitchen`. "
"Please install it with: `pip install comfy-kitchen`."
)

def check_if_quantized_param(
self,
model: "ModelMixin",
param_value: "torch.Tensor",
param_name: str,
state_dict: dict[str, Any],
**kwargs,
) -> bool:
# Based on comfy_kitchen, we will likely wrap tensors based on some config layout.
# For now, we assume all linear weights that aren't excluded are quantized.
# This will be refined based on comfy-kitchen's actual detection logic.
if any(m in param_name.split(".") for m in self.modules_to_not_convert):
return False
return True

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,
):
module, tensor_name = get_module_from_name(model, param_name)

import comfy_kitchen.tensor as ck_tensor

layout_map = {
"fp8": getattr(ck_tensor, "TensorCoreFP8Layout", None),
"nvfp4": getattr(ck_tensor, "TensorCoreNVFP4Layout", None),
"mxfp8": getattr(ck_tensor, "TensorCoreMXFP8Layout", None),
"int8": getattr(ck_tensor, "Int8Layout", None),
"int4_svd": getattr(ck_tensor, "SVDQuantW4A4Layout", None),
"int4_awq": getattr(ck_tensor, "AWQW4A16Layout", None),
}

# Check if it's already a QuantizedTensor (e.g., if loaded directly from a custom loader)
if isinstance(param_value, ck_tensor.QuantizedTensor):
quantized_weight = param_value
else:
layout = layout_map.get(self.quant_format.lower())
if layout is None:
raise ValueError(f"The layout for '{self.quant_format}' was not found in `comfy_kitchen`.")

# comfy-kitchen natively handles wrapping standard float tensors via from_float
# If the tensor is pre-quantized raw bytes, comfy-kitchen exposes `.from_quantized(...)` or similar internally,
# but `.from_float` guarantees we intercept float weights (e.g. standard safetensors float weights).
quantized_weight = ck_tensor.QuantizedTensor.from_float(param_value.to(target_device), layout.__name__)

if tensor_name in module._parameters:
module._parameters[tensor_name] = quantized_weight.to(target_device)
if tensor_name in module._buffers:
module._buffers[tensor_name] = quantized_weight.to(target_device)

def update_torch_dtype(self, torch_dtype: "torch.dtype") -> "torch.dtype":
if torch_dtype is None:
torch_dtype = self.compute_dtype
return torch_dtype

def _process_model_before_weight_loading(
self,
model: "ModelMixin",
device_map,
keep_in_fp32_modules: list[str] = [],
**kwargs,
):
pass

def _process_model_after_weight_loading(self, model, **kwargs):
pass

@property
def is_serializable(self):
return False

@property
def is_trainable(self):
return False
30 changes: 30 additions & 0 deletions src/diffusers/quantizers/quantization_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class QuantizationMethod(str, Enum):
MODELOPT = "modelopt"
AUTOROUND = "auto-round"
SDNQ = "sdnq"
COMFY_QUANT = "comfy_quant"


@dataclass
Expand Down Expand Up @@ -994,3 +995,32 @@ def __new__(cls, *args, **kwargs):
from sdnq import SDNQConfig as SDNQLibConfig

return SDNQLibConfig(*args, **kwargs)


@dataclass
class ComfyQuantConfig(QuantizationConfigMixin):
"""
This is a wrapper class about all possible attributes and features that you can play with for a model that has been
quantized using comfy-kitchen.

Args:
quant_format (`str`, *optional*, defaults to `"fp8"`):
The quantization format. Supported values include `"fp8"`, `"int8"`, `"mxfp8"`, `"nvfp4"`, `"int4_svd"`,
and `"int4_awq"`.
compute_dtype (`torch.dtype`, *optional*):
The target dtype for the compute operations.
modules_to_not_convert (`list[str]`, *optional*, defaults to `None`):
The list of modules to skip during quantization.
"""

def __init__(
self,
quant_format: str = "fp8",
compute_dtype: Any = None,
modules_to_not_convert: list[str] | None = None,
**kwargs,
):
self.quant_method = QuantizationMethod.COMFY_QUANT
self.quant_format = quant_format
self.compute_dtype = compute_dtype
self.modules_to_not_convert = modules_to_not_convert
1 change: 1 addition & 0 deletions src/diffusers/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
is_bitsandbytes_available,
is_bitsandbytes_version,
is_bs4_available,
is_comfy_kitchen_available,
is_cosmos_guardrail_available,
is_flash_attn_3_available,
is_flash_attn_available,
Expand Down
17 changes: 17 additions & 0 deletions src/diffusers/utils/dummy_comfy_kitchen_objects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# This file is autogenerated by the command `make fix-copies`, do not edit.
from ..utils import DummyObject, requires_backends


class Com(metaclass=DummyObject):
_backends = ["comfy_kitchen"]

def __init__(self, *args, **kwargs):
requires_backends(self, ["comfy_kitchen"])

@classmethod
def from_config(cls, *args, **kwargs):
requires_backends(cls, ["comfy_kitchen"])

@classmethod
def from_pretrained(cls, *args, **kwargs):
requires_backends(cls, ["comfy_kitchen"])
Loading
Loading