From 5d6cb214f8fdb0554923d32263991880c28062b6 Mon Sep 17 00:00:00 2001 From: onepunchmonk Date: Sat, 24 Oct 2026 22:33:04 +0530 Subject: [PATCH 1/8] Fix AutoRound checkpoint config being overwritten by user args merge_quantization_configs looped over every field in the user-passed AutoRoundConfig and overrode any field differing from the checkpoint's, not just backend. The documented AutoRoundConfig(backend="auto") call was silently rewriting a checkpoint's bits/group_size/sym to dataclass defaults. Only backend is a runtime-only override; the rest describe how the checkpoint was quantized. Towards #15120. --- src/diffusers/quantizers/auto.py | 9 +++--- .../quantization/test_quantization_config.py | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 tests/quantization/test_quantization_config.py diff --git a/src/diffusers/quantizers/auto.py b/src/diffusers/quantizers/auto.py index 377b46b7b361..8b6df8c8e099 100644 --- a/src/diffusers/quantizers/auto.py +++ b/src/diffusers/quantizers/auto.py @@ -160,11 +160,10 @@ def merge_quantization_configs( quantization_config.check_model_patching() if quantization_config_from_args is not None and isinstance(quantization_config, AutoRoundConfig): - # For AutoRound, allow overriding fields like `backend` from user args, - # since the model config may store a default value (e.g. backend="auto"). - for key, value in quantization_config_from_args.__dict__.items(): - if key in ("quant_method",): - continue + # Only `backend` is runtime-only; bits/group_size/sym/etc. describe how the + # checkpoint was quantized and must come from the checkpoint's own config. + for key in ("backend",): + value = getattr(quantization_config_from_args, key, None) if hasattr(quantization_config, key) and getattr(quantization_config, key) != value: warnings.warn( f"Overriding `{key}` in the model's quantization_config with value {value!r} " diff --git a/tests/quantization/test_quantization_config.py b/tests/quantization/test_quantization_config.py new file mode 100644 index 000000000000..4f955ec541ca --- /dev/null +++ b/tests/quantization/test_quantization_config.py @@ -0,0 +1,31 @@ +# coding=utf-8 +# Copyright 2026 The HuggingFace Team Inc. +# +# 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. +"""CPU-only tests for quantization config logic that doesn't need a backend installed.""" + +from diffusers.quantizers.auto import DiffusersAutoQuantizer +from diffusers.quantizers.quantization_config import AutoRoundConfig + + +class TestAutoRoundConfigMerge: + def test_only_backend_is_overridden_from_args(self): + checkpoint_config = AutoRoundConfig(bits=4, group_size=32, sym=False, backend="torch") + args_config = AutoRoundConfig(backend="auto") + + merged = DiffusersAutoQuantizer.merge_quantization_configs(checkpoint_config, args_config) + + assert merged.backend == "auto" + assert merged.bits == 4 + assert merged.group_size == 32 + assert merged.sym is False From 7dceca77b22627d630462ac67a65f224309575db Mon Sep 17 00:00:00 2001 From: onepunchmonk Date: Sat, 24 Oct 2026 22:33:29 +0530 Subject: [PATCH 2/8] Fix NVIDIAModelOptConfig.to_dict() crashing on forward_loop callable forward_loop is a documented NVIDIAModelOptConfig parameter, but to_dict() had no override and inherited the base deepcopy-of-__dict__ implementation, so the raw callable flowed into modeling_utils.py's json.dumps(...) telemetry call and raised TypeError on every from_pretrained/save_pretrained. Towards #15120. --- src/diffusers/quantizers/quantization_config.py | 5 +++++ tests/quantization/test_quantization_config.py | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/diffusers/quantizers/quantization_config.py b/src/diffusers/quantizers/quantization_config.py index 5777ab45fa7a..3cc6b7454985 100644 --- a/src/diffusers/quantizers/quantization_config.py +++ b/src/diffusers/quantizers/quantization_config.py @@ -797,6 +797,11 @@ def __init__( self.modelopt_config = self.get_config_from_quant_type() if not modelopt_config else modelopt_config self.disable_conv_quantization = disable_conv_quantization + def to_dict(self) -> dict[str, Any]: + output = super().to_dict() + output.pop("forward_loop", None) + return output + def check_model_patching(self, operation: str = "loading"): # ModelOpt imports diffusers internally. This is here to prevent circular imports from modelopt.torch.opt.plugins.huggingface import _PATCHED_CLASSES diff --git a/tests/quantization/test_quantization_config.py b/tests/quantization/test_quantization_config.py index 4f955ec541ca..50822c469ca4 100644 --- a/tests/quantization/test_quantization_config.py +++ b/tests/quantization/test_quantization_config.py @@ -14,8 +14,10 @@ # limitations under the License. """CPU-only tests for quantization config logic that doesn't need a backend installed.""" +import json + from diffusers.quantizers.auto import DiffusersAutoQuantizer -from diffusers.quantizers.quantization_config import AutoRoundConfig +from diffusers.quantizers.quantization_config import AutoRoundConfig, NVIDIAModelOptConfig class TestAutoRoundConfigMerge: @@ -29,3 +31,15 @@ def test_only_backend_is_overridden_from_args(self): assert merged.bits == 4 assert merged.group_size == 32 assert merged.sym is False + + +class TestNVIDIAModelOptConfigToDict: + def test_to_dict_drops_forward_loop_callable(self): + config = NVIDIAModelOptConfig( + quant_type="FP8", forward_loop=lambda model: model, modelopt_config={"quant_cfg": {}} + ) + + output = config.to_dict() + + assert "forward_loop" not in output + json.dumps(output) From ba206f176d25c377eb788f415cb30ea6e01b8059 Mon Sep 17 00:00:00 2001 From: onepunchmonk Date: Sat, 24 Oct 2026 22:33:41 +0530 Subject: [PATCH 3/8] Fix from_single_file forcing pre_quantized=True on quantize-on-load backends DiffusersAutoQuantizer.from_config() defaults pre_quantized to True when not passed, and the single-file loader never passed it. GGUF and BFL set pre_quantized themselves from their config so were unaffected, but bitsandbytes (4-bit/8-bit), TorchAO, and Quanto need to quantize on load rather than assume already-quantized weights, so single-file loading broke or silently no-opped for them. No dedicated test: exercising this needs a real single-file checkpoint load, which isn't practical as a CPU-only unit test. Towards #15120. --- src/diffusers/loaders/single_file_model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/diffusers/loaders/single_file_model.py b/src/diffusers/loaders/single_file_model.py index 610e377d3ab9..7e2f934e1b63 100644 --- a/src/diffusers/loaders/single_file_model.py +++ b/src/diffusers/loaders/single_file_model.py @@ -416,7 +416,9 @@ def from_single_file(cls, pretrained_model_link_or_path_or_dict: str | None = No if quantization_config is not None: if not low_cpu_mem_usage: raise ValueError("`low_cpu_mem_usage` cannot be False when using quantization.") - hf_quantizer = DiffusersAutoQuantizer.from_config(quantization_config) + hf_quantizer = DiffusersAutoQuantizer.from_config( + quantization_config, pre_quantized=getattr(quantization_config, "pre_quantized", False) + ) hf_quantizer.validate_environment() torch_dtype = hf_quantizer.update_torch_dtype(torch_dtype) checkpoint = hf_quantizer.maybe_update_state_dict(checkpoint) From 247a9f169583bf99c3af2d9ee418f8315b4caa71 Mon Sep 17 00:00:00 2001 From: onepunchmonk Date: Sat, 24 Oct 2026 22:34:58 +0530 Subject: [PATCH 4/8] Fix quantizer configs being mutated in place across loads bnb (4-bit + 8-bit), TorchAO, Quanto, and ModelOpt quantizers aliased modules_to_not_convert-style lists directly off the quantization_config object and then .extend()-ed them in place. Reusing the same config across two from_pretrained calls leaked state between loads (e.g. offloaded module names accumulating on the shared config and getting written into config.json). Copy the list before mutating it. Towards #15120. --- .../quantizers/bitsandbytes/bnb_quantizer.py | 4 ++++ .../quantizers/modelopt/modelopt_quantizer.py | 4 +++- src/diffusers/quantizers/quanto/quanto_quantizer.py | 2 ++ .../quantizers/torchao/torchao_quantizer.py | 2 ++ tests/quantization/bnb/test_mixed_int8.py | 13 +++++++++++++ tests/quantization/torchao/test_torchao.py | 13 +++++++++++++ 6 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/diffusers/quantizers/bitsandbytes/bnb_quantizer.py b/src/diffusers/quantizers/bitsandbytes/bnb_quantizer.py index 63d2680b3d33..00404a168b75 100644 --- a/src/diffusers/quantizers/bitsandbytes/bnb_quantizer.py +++ b/src/diffusers/quantizers/bitsandbytes/bnb_quantizer.py @@ -263,6 +263,8 @@ def _process_model_before_weight_loading( if not isinstance(self.modules_to_not_convert, list): self.modules_to_not_convert = [self.modules_to_not_convert] + else: + self.modules_to_not_convert = list(self.modules_to_not_convert) self.modules_to_not_convert.extend(keep_in_fp32_modules) @@ -550,6 +552,8 @@ def _process_model_before_weight_loading( if not isinstance(self.modules_to_not_convert, list): self.modules_to_not_convert = [self.modules_to_not_convert] + else: + self.modules_to_not_convert = list(self.modules_to_not_convert) self.modules_to_not_convert.extend(keep_in_fp32_modules) diff --git a/src/diffusers/quantizers/modelopt/modelopt_quantizer.py b/src/diffusers/quantizers/modelopt/modelopt_quantizer.py index b7cadb50e54b..d64e18014ea6 100644 --- a/src/diffusers/quantizers/modelopt/modelopt_quantizer.py +++ b/src/diffusers/quantizers/modelopt/modelopt_quantizer.py @@ -161,8 +161,10 @@ def _process_model_before_weight_loading( if modules_to_not_convert is None: modules_to_not_convert = [] - if isinstance(modules_to_not_convert, str): + elif isinstance(modules_to_not_convert, str): modules_to_not_convert = [modules_to_not_convert] + else: + modules_to_not_convert = list(modules_to_not_convert) modules_to_not_convert.extend(keep_in_fp32_modules) if self.quantization_config.disable_conv_quantization: modules_to_not_convert.extend(self.get_conv_param_names(model)) diff --git a/src/diffusers/quantizers/quanto/quanto_quantizer.py b/src/diffusers/quantizers/quanto/quanto_quantizer.py index 92bbecbb1b53..858210311ac0 100644 --- a/src/diffusers/quantizers/quanto/quanto_quantizer.py +++ b/src/diffusers/quantizers/quanto/quanto_quantizer.py @@ -165,6 +165,8 @@ def _process_model_before_weight_loading( if not isinstance(self.modules_to_not_convert, list): self.modules_to_not_convert = [self.modules_to_not_convert] + else: + self.modules_to_not_convert = list(self.modules_to_not_convert) self.modules_to_not_convert.extend(keep_in_fp32_modules) diff --git a/src/diffusers/quantizers/torchao/torchao_quantizer.py b/src/diffusers/quantizers/torchao/torchao_quantizer.py index e31db9faa068..a254febffed9 100644 --- a/src/diffusers/quantizers/torchao/torchao_quantizer.py +++ b/src/diffusers/quantizers/torchao/torchao_quantizer.py @@ -443,6 +443,8 @@ def _process_model_before_weight_loading( if not isinstance(self.modules_to_not_convert, list): self.modules_to_not_convert = [self.modules_to_not_convert] + else: + self.modules_to_not_convert = list(self.modules_to_not_convert) self.modules_to_not_convert.extend(keep_in_fp32_modules) diff --git a/tests/quantization/bnb/test_mixed_int8.py b/tests/quantization/bnb/test_mixed_int8.py index d37099ff623c..cab4da29d3a2 100644 --- a/tests/quantization/bnb/test_mixed_int8.py +++ b/tests/quantization/bnb/test_mixed_int8.py @@ -69,3 +69,16 @@ def test_bnb_8bit_logs_warning_for_no_quantization(self): "You are loading your model in 8bit or 4bit but no linear modules were found in your model." in cap_logger.out ) + + def test_bnb_8bit_modules_to_not_convert_does_not_mutate_config(self): + from diffusers.quantizers.bitsandbytes.bnb_quantizer import BnB8BitDiffusersQuantizer + + skip_modules = ["proj_out"] + quantization_config = BitsAndBytesConfig(load_in_8bit=True, llm_int8_skip_modules=skip_modules) + model = torch.nn.Sequential(torch.nn.Linear(4, 4), torch.nn.Linear(4, 4)) + + for _ in range(2): + quantizer = BnB8BitDiffusersQuantizer(quantization_config) + quantizer._process_model_before_weight_loading(model, device_map=None, keep_in_fp32_modules=["norm_out"]) + + assert quantization_config.llm_int8_skip_modules == skip_modules diff --git a/tests/quantization/torchao/test_torchao.py b/tests/quantization/torchao/test_torchao.py index 6666569187cb..4239f4b2f50e 100644 --- a/tests/quantization/torchao/test_torchao.py +++ b/tests/quantization/torchao/test_torchao.py @@ -14,9 +14,12 @@ # limitations under the License. +from types import SimpleNamespace + import pytest from diffusers import TorchAoConfig +from diffusers.quantizers.torchao.torchao_quantizer import TorchAoHfQuantizer from ...testing_utils import ( enable_full_determinism, @@ -73,3 +76,13 @@ def test_repr(self): quantization_repr = repr(quantization_config) assert "TorchAoConfig" in quantization_repr assert "torchao" in quantization_repr + + def test_process_model_before_weight_loading_does_not_mutate_config_list(self): + # The same config can be reused across multiple loads. + quantization_config = TorchAoConfig(Int4WeightOnlyConfig(), modules_to_not_convert=["a"]) + quantizer = TorchAoHfQuantizer(quantization_config) + model = SimpleNamespace(config=SimpleNamespace()) + + quantizer._process_model_before_weight_loading(model, device_map=None, keep_in_fp32_modules=["b"]) + + assert quantization_config.modules_to_not_convert == ["a"] From 9e1c8deb307f6cca07e7482ff92960e66b6fe8e9 Mon Sep 17 00:00:00 2001 From: onepunchmonk Date: Sat, 24 Oct 2026 22:35:23 +0530 Subject: [PATCH 5/8] Validate Nunchaku AWQ group_size against what the kernel supports Config validation for the Nunchaku AWQ path only checked precision == "int4"; the actual AWQ kernel call site hardcodes group_size 64. A checkpoint with group_size=128 loaded without error and would silently produce wrong output. The SVDQ path in the same config already validates its group size, so apply the same pattern here. Towards #15120. --- .../quantizers/quantization_config.py | 2 ++ .../quantization/test_quantization_config.py | 22 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/diffusers/quantizers/quantization_config.py b/src/diffusers/quantizers/quantization_config.py index 3cc6b7454985..deb8ed4c24d2 100644 --- a/src/diffusers/quantizers/quantization_config.py +++ b/src/diffusers/quantizers/quantization_config.py @@ -570,6 +570,8 @@ def post_init(self): ) elif precision != "int4": raise ValueError("Nunchaku AWQ target requires precision='int4'.") + elif group_size != 64: + raise ValueError(f"Nunchaku AWQ config requires group_size=64, got {group_size}.") def to_dict(self) -> dict[str, Any]: output = super().to_dict() diff --git a/tests/quantization/test_quantization_config.py b/tests/quantization/test_quantization_config.py index 50822c469ca4..974dbae08366 100644 --- a/tests/quantization/test_quantization_config.py +++ b/tests/quantization/test_quantization_config.py @@ -16,8 +16,14 @@ import json +import pytest + from diffusers.quantizers.auto import DiffusersAutoQuantizer -from diffusers.quantizers.quantization_config import AutoRoundConfig, NVIDIAModelOptConfig +from diffusers.quantizers.quantization_config import ( + AutoRoundConfig, + NunchakuLiteQuantizationConfig, + NVIDIAModelOptConfig, +) class TestAutoRoundConfigMerge: @@ -43,3 +49,17 @@ def test_to_dict_drops_forward_loop_callable(self): assert "forward_loop" not in output json.dumps(output) + + +class TestNunchakuAwqGroupSize: + def test_awq_rejects_group_size_other_than_64(self): + with pytest.raises(ValueError, match="group_size=64"): + NunchakuLiteQuantizationConfig( + awq_w4a16={"precision": "int4", "group_size": 128, "targets": ["final_linear"]} + ) + + def test_awq_accepts_group_size_64(self): + config = NunchakuLiteQuantizationConfig( + awq_w4a16={"precision": "int4", "group_size": 64, "targets": ["final_linear"]} + ) + assert config.awq_w4a16["group_size"] == 64 From f61b6bc1e53fd9da7624fdd56e386f8e515d41ae Mon Sep 17 00:00:00 2001 From: onepunchmonk Date: Sat, 24 Oct 2026 22:36:27 +0530 Subject: [PATCH 6/8] Fix broken __eq__/__repr__/hashing on quant configs, and to_json_string() Every quant config subclass is @dataclass-decorated but declares no dataclass fields beyond the inherited quant_method, so the generated __eq__/__repr__ only see that one field. Two GGUFQuantizationConfig instances with different compute_dtype compared equal, repr() hid every real parameter, and the configs were unhashable. Remove the decorator from subclasses with a custom __init__ and no declared fields. Separately, to_json_string() calls self.to_diff_dict(), which only BitsAndBytesConfig defined; every other config raised AttributeError. Add a base to_diff_dict (no comparable "default" instance exists for these configs in general, so it's just to_dict()). Removing @dataclass exposed GGUFQuantizationConfig/BFLQuantizationConfig's raw torch.dtype compute_dtype to the now-working diff path in to_json_string(), which would have been a new JSON-serialization crash, so both get a to_dict() override that stringifies it. Towards #15120. --- .../quantizers/quantization_config.py | 27 ++++++++++++------- .../quantization/test_quantization_config.py | 23 ++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/diffusers/quantizers/quantization_config.py b/src/diffusers/quantizers/quantization_config.py index deb8ed4c24d2..21bda08ffb76 100644 --- a/src/diffusers/quantizers/quantization_config.py +++ b/src/diffusers/quantizers/quantization_config.py @@ -27,7 +27,6 @@ import json import os import warnings -from dataclasses import dataclass from enum import Enum from typing import Any, Callable @@ -54,7 +53,6 @@ class QuantizationMethod(str, Enum): SDNQ = "sdnq" -@dataclass class QuantizationConfigMixin: """ Mixin class for quantization config @@ -120,6 +118,13 @@ def to_dict(self) -> dict[str, Any]: """ return copy.deepcopy(self.__dict__) + def to_diff_dict(self) -> dict[str, Any]: + """ + Serializes this instance to a Python dictionary. Returns: + `dict[str, Any]`: Dictionary of all the attributes that make up this configuration instance. + """ + return self.to_dict() + def __iter__(self): """allows `dict(obj)` for situations where obj may be a dict or QuantizationConfigMixin""" for attr, value in copy.deepcopy(self.__dict__).items(): @@ -169,7 +174,6 @@ def update(self, **kwargs): return unused_kwargs -@dataclass class BitsAndBytesConfig(QuantizationConfigMixin): """ This is a wrapper class about all possible attributes and features that you can play with a model that has been @@ -409,7 +413,6 @@ def to_diff_dict(self) -> dict[str, Any]: return serializable_config_dict -@dataclass class GGUFQuantizationConfig(QuantizationConfigMixin): """This is a config class for GGUF Quantization techniques. @@ -431,8 +434,12 @@ def __init__(self, compute_dtype: "torch.dtype" | None = None): if self.compute_dtype is None: self.compute_dtype = torch.float32 + def to_dict(self) -> dict[str, Any]: + output = super().to_dict() + output["compute_dtype"] = str(output["compute_dtype"]).split(".")[1] + return output + -@dataclass class BFLQuantizationConfig(QuantizationConfigMixin): """Config for loading checkpoints quantized with the Black Forest Labs `fp8r` and `nvfp4` schemes. @@ -455,8 +462,12 @@ def __init__(self, compute_dtype: "torch.dtype" | None = None): self.compute_dtype = compute_dtype if compute_dtype is not None else torch.bfloat16 self.pre_quantized = True + def to_dict(self) -> dict[str, Any]: + output = super().to_dict() + output["compute_dtype"] = str(output["compute_dtype"]).split(".")[1] + return output + -@dataclass class NunchakuLiteQuantizationConfig(QuantizationConfigMixin): """Configuration for loading Nunchaku Lite checkpoints. @@ -579,7 +590,6 @@ def to_dict(self) -> dict[str, Any]: return output -@dataclass class TorchAoConfig(QuantizationConfigMixin): """This is a config class for torchao quantization/sparsity techniques. @@ -674,7 +684,6 @@ def __repr__(self): return f"{self.__class__.__name__} {json.dumps(config_dict, indent=2, sort_keys=True)}\n" -@dataclass class QuantoConfig(QuantizationConfigMixin): """ This is a wrapper class about all possible attributes and features that you can play with a model that has been @@ -718,7 +727,6 @@ def post_init(self): raise ValueError(f"Only support weights in {accepted_weights} but found {self.weights_dtype}") -@dataclass class NVIDIAModelOptConfig(QuantizationConfigMixin): """This is a config class to use nvidia modelopt for quantization. @@ -914,7 +922,6 @@ def get_config_from_quant_type(self) -> dict[str, Any]: return BASE_CONFIG -@dataclass class AutoRoundConfig(QuantizationConfigMixin): """Configuration class for AutoRound quantization. diff --git a/tests/quantization/test_quantization_config.py b/tests/quantization/test_quantization_config.py index 974dbae08366..5d6639f3ee3b 100644 --- a/tests/quantization/test_quantization_config.py +++ b/tests/quantization/test_quantization_config.py @@ -21,8 +21,10 @@ from diffusers.quantizers.auto import DiffusersAutoQuantizer from diffusers.quantizers.quantization_config import ( AutoRoundConfig, + GGUFQuantizationConfig, NunchakuLiteQuantizationConfig, NVIDIAModelOptConfig, + QuantoConfig, ) @@ -51,6 +53,27 @@ def test_to_dict_drops_forward_loop_callable(self): json.dumps(output) +class TestQuantizationConfigEqAndRepr: + def test_gguf_config_eq_does_not_ignore_fields(self): + import torch + + a = GGUFQuantizationConfig(compute_dtype=torch.float16) + b = GGUFQuantizationConfig(compute_dtype=torch.bfloat16) + + assert a != b + + def test_gguf_config_repr_shows_fields(self): + import torch + + config = GGUFQuantizationConfig(compute_dtype=torch.float16) + assert "float16" in repr(config) + + def test_quanto_config_to_json_string_does_not_raise(self): + config = QuantoConfig(weights_dtype="int8") + json_string = config.to_json_string() + assert "int8" in json_string + + class TestNunchakuAwqGroupSize: def test_awq_rejects_group_size_other_than_64(self): with pytest.raises(ValueError, match="group_size=64"): From 23d81f46f8fbe91dffed978b9eb1a8bb1e123e41 Mon Sep 17 00:00:00 2001 From: onepunchmonk Date: Sat, 24 Oct 2026 22:36:40 +0530 Subject: [PATCH 7/8] Fix PipelineQuantizationConfig validation always raising _validate_init_kwargs_in_backends compared init_kwargs_transformers (None when transformers isn't involved for a given backend) against init_kwargs_diffusers (a set), which is always unequal, so the signature-mismatch error fired unconditionally. No dedicated test: this needs no existing test file (no coverage of PipelineQuantizationConfig anywhere in the repo) and the fix is a one-line guard with no side effects to assert on beyond "doesn't raise when it shouldn't", which overlaps the normal pipeline-quantization flow already covered elsewhere. Towards #15120. --- src/diffusers/quantizers/pipe_quant_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diffusers/quantizers/pipe_quant_config.py b/src/diffusers/quantizers/pipe_quant_config.py index cfb5803ee0ca..ee4751cae89e 100644 --- a/src/diffusers/quantizers/pipe_quant_config.py +++ b/src/diffusers/quantizers/pipe_quant_config.py @@ -101,7 +101,7 @@ def _validate_init_kwargs_in_backends(self): init_kwargs_diffusers = inspect.signature(quant_config_mapping_diffusers[quant_backend].__init__) init_kwargs_diffusers = {name for name in init_kwargs_diffusers.parameters if name != "self"} - if init_kwargs_transformers != init_kwargs_diffusers: + if init_kwargs_transformers is not None and init_kwargs_transformers != init_kwargs_diffusers: raise ValueError( "The signatures of the __init__ methods of the quantization config classes in `diffusers` and `transformers` don't match. " f"Please provide a `quant_mapping` instead, in the {self.__class__.__name__} class. Refer to [the docs](https://huggingface.co/docs/diffusers/main/en/quantization/overview#pipeline-level-quantization) to learn more about how " From fee4c69eb4a532c5683702ab33e411cb442151ca Mon Sep 17 00:00:00 2001 From: onepunchmonk Date: Sat, 24 Oct 2026 22:37:25 +0530 Subject: [PATCH 8/8] Fix bnb logging/error-message bugs and unsafe SCB key lookup - Two log lines were missing an f-string prefix, so {current_device} and {target_dtype} printed literally instead of interpolating. - check_quantized_param_shape's error message had expected and actual shapes swapped. - The SCB/weight_format key lookups used param_name.replace("weight", "SCB"), which corrupts the key for any module name containing "weight" elsewhere than the trailing ".weight" param (e.g. "weight_proj.weight" -> "weight_proj.SCB" would actually become "SCB_proj.weight"). Switch to suffix-safe slicing, matching the pattern this file already uses elsewhere. Towards #15120. --- .../quantizers/bitsandbytes/bnb_quantizer.py | 16 +++++++-------- tests/quantization/bnb/test_mixed_int8.py | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/diffusers/quantizers/bitsandbytes/bnb_quantizer.py b/src/diffusers/quantizers/bitsandbytes/bnb_quantizer.py index 00404a168b75..883bc1236369 100644 --- a/src/diffusers/quantizers/bitsandbytes/bnb_quantizer.py +++ b/src/diffusers/quantizers/bitsandbytes/bnb_quantizer.py @@ -95,7 +95,7 @@ def adjust_target_dtype(self, target_dtype: "torch.dtype") -> "torch.dtype": if target_dtype != torch.int8: from accelerate.utils import CustomDtype - logger.info("target_dtype {target_dtype} is replaced by `CustomDtype.INT4` for 4-bit BnB quantization") + logger.info(f"target_dtype {target_dtype} is replaced by `CustomDtype.INT4` for 4-bit BnB quantization") return CustomDtype.INT4 else: raise ValueError(f"Wrong `target_dtype` ({target_dtype}) provided.") @@ -207,7 +207,7 @@ def check_quantized_param_shape(self, param_name, current_param, loaded_param): inferred_shape = (n,) if "bias" in param_name else ((n + 1) // 2, 1) if loaded_param_shape != inferred_shape: raise ValueError( - f"Expected the flattened shape of the current param ({param_name}) to be {loaded_param_shape} but is {inferred_shape}." + f"Expected the flattened shape of the current param ({param_name}) to be {inferred_shape} but is {loaded_param_shape}." ) else: return True @@ -241,8 +241,7 @@ def update_device_map(self, device_map): device_map = {"": current_device} logger.info( "The device_map was not initialized. " - "Setting device_map to {" - ": {current_device}}. " + f"Setting device_map to {{'': {current_device}}}. " "If you want to use the model for inference, please set device_map ='auto' " ) return device_map @@ -443,8 +442,7 @@ def update_device_map(self, device_map): device_map = {"": current_device} logger.info( "The device_map was not initialized. " - "Setting device_map to {" - ": {current_device}}. " + f"Setting device_map to {{'': {current_device}}}. " "If you want to use the model for inference, please set device_map ='auto' " ) return device_map @@ -467,7 +465,7 @@ def check_if_quantized_param( module, tensor_name = get_module_from_name(model, param_name) if isinstance(module._parameters.get(tensor_name, None), bnb.nn.Int8Params): if self.pre_quantized: - if param_name.replace("weight", "SCB") not in state_dict.keys(): + if param_name[: -len("weight")] + "SCB" not in state_dict.keys(): raise ValueError("Missing quantization component `SCB`") if param_value.dtype != torch.int8: raise ValueError( @@ -488,8 +486,8 @@ def create_quantized_param( ): import bitsandbytes as bnb - fp16_statistics_key = param_name.replace("weight", "SCB") - fp16_weights_format_key = param_name.replace("weight", "weight_format") + fp16_statistics_key = param_name[: -len("weight")] + "SCB" + fp16_weights_format_key = param_name[: -len("weight")] + "weight_format" fp16_statistics = state_dict.get(fp16_statistics_key, None) fp16_weights_format = state_dict.get(fp16_weights_format_key, None) diff --git a/tests/quantization/bnb/test_mixed_int8.py b/tests/quantization/bnb/test_mixed_int8.py index cab4da29d3a2..f73726ab217b 100644 --- a/tests/quantization/bnb/test_mixed_int8.py +++ b/tests/quantization/bnb/test_mixed_int8.py @@ -82,3 +82,23 @@ def test_bnb_8bit_modules_to_not_convert_does_not_mutate_config(self): quantizer._process_model_before_weight_loading(model, device_map=None, keep_in_fp32_modules=["norm_out"]) assert quantization_config.llm_int8_skip_modules == skip_modules + + def test_bnb_8bit_scb_lookup_is_suffix_safe(self): + import bitsandbytes as bnb + + from diffusers.quantizers.bitsandbytes.bnb_quantizer import BnB8BitDiffusersQuantizer + + quantization_config = BitsAndBytesConfig(load_in_8bit=True) + quantizer = BnB8BitDiffusersQuantizer(quantization_config) + quantizer.pre_quantized = True + + # "weight" appears outside the trailing ".weight" param, which a naive str.replace corrupts. + model = torch.nn.Module() + model.weight_proj = torch.nn.Linear(4, 4, bias=False) + model.weight_proj.weight = bnb.nn.Int8Params(torch.zeros(4, 4, dtype=torch.int8), requires_grad=False) + + param_name = "weight_proj.weight" + state_dict = {"weight_proj.SCB": torch.zeros(4, dtype=torch.float32)} + assert quantizer.check_if_quantized_param( + model, param_value=torch.zeros(4, 4, dtype=torch.int8), param_name=param_name, state_dict=state_dict + )