From 61356971e0d4988ff4c03f5dd417013099dded37 Mon Sep 17 00:00:00 2001 From: Shengliang Xu Date: Wed, 19 Aug 2026 20:48:42 +0000 Subject: [PATCH] Fix ModelOpt integration for the >=0.44 list quant_cfg format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ModelOpt 0.44 changed `quant_cfg` and `mtq.config._default_disabled_quantizer_cfg` from a `{pattern: cfg}` mapping to a list of `{"quantizer_name": ...}` entries. `NVIDIAModelOptConfig.get_config_from_quant_type` spread the default-disabled set into a dict literal, so on ModelOpt >=0.44 building the config raised `TypeError: 'list' object is not a mapping` (NVIDIA/Model-Optimizer#2001) — hitting the default `NVIDIAModelOptConfig(quant_type=...)` path. Emit `quant_cfg` in whichever shape the installed ModelOpt expects, keyed off the shape ModelOpt exposes for its default-disabled set: a mapping for <0.44 (unchanged behavior) and the native list of entries for >=0.44 (no deprecation warning). The `modules_to_not_convert` loop in the quantizer appends a list entry or assigns a mapping key accordingly. Add a CPU-only regression test that builds the config across quant types and checks the installed ModelOpt accepts it — the existing GPU tests are nightly/big-accelerator gated, so this config-construction crash slipped through CI. Signed-off-by: Shengliang Xu --- .../quantizers/modelopt/modelopt_quantizer.py | 7 +- .../quantizers/quantization_config.py | 23 ++++- tests/quantization/modelopt/__init__.py | 0 tests/quantization/modelopt/test_modelopt.py | 88 +++++++++++++++++++ 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 tests/quantization/modelopt/__init__.py create mode 100644 tests/quantization/modelopt/test_modelopt.py diff --git a/src/diffusers/quantizers/modelopt/modelopt_quantizer.py b/src/diffusers/quantizers/modelopt/modelopt_quantizer.py index b7cadb50e54b..53f657d54e7d 100644 --- a/src/diffusers/quantizers/modelopt/modelopt_quantizer.py +++ b/src/diffusers/quantizers/modelopt/modelopt_quantizer.py @@ -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 diff --git a/src/diffusers/quantizers/quantization_config.py b/src/diffusers/quantizers/quantization_config.py index ea78b5f7ff53..95bb1d62eacc 100644 --- a/src/diffusers/quantizers/quantization_config.py +++ b/src/diffusers/quantizers/quantization_config.py @@ -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, } @@ -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 diff --git a/tests/quantization/modelopt/__init__.py b/tests/quantization/modelopt/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/quantization/modelopt/test_modelopt.py b/tests/quantization/modelopt/test_modelopt.py new file mode 100644 index 000000000000..77b62c29acae --- /dev/null +++ b/tests/quantization/modelopt/test_modelopt.py @@ -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)