Skip to content
Open
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
7 changes: 6 additions & 1 deletion src/diffusers/quantizers/modelopt/modelopt_quantizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,13 @@ def _process_model_before_weight_loading(
if self.quantization_config.disable_conv_quantization:
modules_to_not_convert.extend(self.get_conv_param_names(model))

# ModelOpt >= 0.44 uses a list-of-entries `quant_cfg`; older versions use a mapping.
quant_cfg = self.quantization_config.modelopt_config["quant_cfg"]
for module in modules_to_not_convert:
self.quantization_config.modelopt_config["quant_cfg"]["*" + module + "*"] = {"enable": False}
if isinstance(quant_cfg, list):
quant_cfg.append({"quantizer_name": "*" + module + "*", "enable": False})
else:
quant_cfg["*" + module + "*"] = {"enable": False}
self.quantization_config.modules_to_not_convert = modules_to_not_convert
mto.apply_mode(model, mode=[("quantize", self.quantization_config.modelopt_config)])
model.config.quantization_config = self.quantization_config
Expand Down
23 changes: 22 additions & 1 deletion src/diffusers/quantizers/quantization_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,6 @@ def get_config_from_quant_type(self) -> dict[str, Any]:
"*k_bmm_quantizer": {},
"*v_bmm_quantizer": {},
"*softmax_quantizer": {},
**mtq.config._default_disabled_quantizer_cfg,
},
"algorithm": self.calib_cfg,
}
Expand Down Expand Up @@ -872,6 +871,28 @@ def get_config_from_quant_type(self) -> dict[str, Any]:
}
)

# Splice in ModelOpt's default-disabled quantizers and emit the `quant_cfg` shape the
# installed ModelOpt expects. ModelOpt < 0.44 consumes a `{pattern: cfg}` mapping (and
# exposes its default-disabled set as one); >= 0.44 consumes a list of `{"quantizer_name":
# ...}` entries (and exposes the default set as a list). Key the choice off that shape. See
# https://nvidia.github.io/Model-Optimizer/guides/_quant_cfg.html
default_disabled_quantizer_cfg = mtq.config._default_disabled_quantizer_cfg
if isinstance(default_disabled_quantizer_cfg, dict):
quant_cfg.update(default_disabled_quantizer_cfg)
return BASE_CONFIG

entries = []
for k in quant_cfg:
entry = {"quantizer_name": k}
if "enable" in quant_cfg[k]:
entry["enable"] = quant_cfg[k].pop("enable")
if quant_cfg[k]:
entry["cfg"] = quant_cfg[k]
elif "enable" not in entry:
entry["enable"] = True
entries.append(entry)
entries.extend(default_disabled_quantizer_cfg)
BASE_CONFIG["quant_cfg"] = entries
return BASE_CONFIG


Expand Down
Empty file.
88 changes: 88 additions & 0 deletions tests/quantization/modelopt/test_modelopt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# coding=utf-8
# 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.


import pytest

from diffusers import NVIDIAModelOptConfig
from diffusers.utils import is_nvidia_modelopt_available

from ...testing_utils import (
is_modelopt,
is_quantization,
require_modelopt_version_greater_or_equal,
)


if is_nvidia_modelopt_available():
import modelopt.torch.quantization as mtq


@is_quantization
@is_modelopt
@require_modelopt_version_greater_or_equal("0.33.1")
class TestNVIDIAModelOptConfigQuantCfg:
"""CPU-only regression tests for `NVIDIAModelOptConfig.get_config_from_quant_type`.

These only build the config (no model, no accelerator). Diffusers' contract is simply to build a
`modelopt_config` the installed ModelOpt accepts; the `quant_cfg` container shape (a mapping vs a
list of entries, which changed in ModelOpt 0.44) is ModelOpt's concern, so it is not asserted
here. This guards the regression where building the config raised `TypeError: 'list' object is
not a mapping` on `NVIDIAModelOptConfig(quant_type=...)`.
"""

def _weight_quantizer_num_bits(self, quant_cfg):
# `quant_cfg` is a `{pattern: cfg}` mapping or a list of `{"quantizer_name": ...}` entries.
if isinstance(quant_cfg, dict):
return quant_cfg["*weight_quantizer"].get("num_bits")
entry = next(e for e in quant_cfg if e["quantizer_name"] == "*weight_quantizer")
return entry.get("cfg", {}).get("num_bits")

@pytest.mark.parametrize(
"init_kwargs, weight_num_bits",
[
({"quant_type": "FP8"}, (4, 3)),
({"quant_type": "INT8"}, 8),
({"quant_type": "FP8_FP8"}, (4, 3)),
({"quant_type": "FP8", "weight_only": False}, (4, 3)),
(
{
"quant_type": "NVFP4",
"block_quantize": 128,
"channel_quantize": -1,
"scale_block_quantize": 8,
"scale_channel_quantize": -1,
"modules_to_not_convert": ["conv"],
},
(2, 1),
),
(
{
"quant_type": "INT4",
"block_quantize": 128,
"channel_quantize": -1,
"disable_conv_quantization": True,
},
4,
),
],
)
def test_quant_cfg_is_accepted_by_modelopt(self, init_kwargs, weight_num_bits):
modelopt_config = NVIDIAModelOptConfig(**init_kwargs).modelopt_config
# Diffusers builds the weight quantizer at the requested bit-width ...
assert self._weight_quantizer_num_bits(modelopt_config["quant_cfg"]) == weight_num_bits
# ... and the whole config must be consumable by the installed ModelOpt (what `mto.apply_mode`
# relies on), whatever `quant_cfg` container shape that ModelOpt uses.
mtq.config.QuantizeConfig(**modelopt_config)
Loading