Repository: huggingface/transformers
Pull request: [core] Fix quark

# What does this PR do?

Fix Quark after weight refactor, all tests passing locally

Changed files:
filename: src/transformers/core_model_loading.py
patch: @@ -462,9 +462,15 @@ def convert(
         full_name = layer_name
         if ".*." in layer_name:
             full_name = layer_name.replace(".*.", ".0.")
-        prefix, _, suffix = next(full_name.partition(k) for k in collected_tensors.keys() if k in full_name)
-        # Rename the tensors
-        collected_tensors = {prefix + k + suffix: v for k, v in collected_tensors.items()}
+
+        try:
+            prefix, _, suffix = next(full_name.partition(k) for k in collected_tensors.keys() if k in full_name)
+            # Rename the tensors
+            collected_tensors = {prefix + k + suffix: v for k, v in collected_tensors.items()}
+        # some quantizers need to already rename in `convert` as they cannot only rely on prefix and suffix
+        except StopIteration:
+            pass
+
         if hf_quantizer is not None and self.quantization_operation is not None:
             with log_to_misc(layer_name, misc, (collected_tensors, layer_name), self.quantization_operation):
                 collected_tensors = self.quantization_operation.convert(

filename: src/transformers/integrations/quark.py
patch: @@ -0,0 +1,55 @@
+# coding=utf-8
+# Copyright 2025 The HuggingFace Inc. 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.
+
+from typing import Optional
+
+from ..core_model_loading import ConversionOps
+from ..utils import is_torch_available
+
+
+if is_torch_available():
+    import torch
+
+
+class QuarkDeserialize(ConversionOps):
+    def __init__(self, hf_quantizer):
+        self.hf_quantizer = hf_quantizer
+
+    def convert(
+        self,
+        input_dict: torch.Tensor,
+        model: Optional[torch.nn.Module] = None,
+        missing_keys: Optional[list[str]] = None,
+        full_layer_name: str | None = None,
+        **kwargs,
+    ) -> dict[str, torch.Tensor]:
+        # target_key should be in the form of weight_scale, bias_scale, input_scale, output_scale, weight_zero_point, bias_zero_point, input_zero_point, output_zero_point
+        target_key, value = tuple(input_dict.items())[0]
+        value = value[0] if isinstance(value, list) else value
+        # this will get the param name : weight, input, bias or output
+        param = target_key.split("_", 1)[0]
+        # quant_state should be in the form of scale, or zero_point
+        quant_state = target_key.split("_", 1)[-1]
+
+        # here we change the name for example from the form of :
+        # model.layers.0.mlp.down_proj.weight_scale to model.layers.0.mlp.down_proj.weight_quantizer.scale to fit within
+        # the QParamsLinear module of quark
+        sub_module_state = full_layer_name.rsplit(".", 1)[0] + "." + param + "_quantizer" + "." + quant_state
+
+        # since quark module was expecting keys in the form of model.layers.0.mlp.down_proj.weight_scale
+        # we need to remove it from the missing_keys list
+        missing_keys.discard(full_layer_name)
+
+        return {sub_module_state: value}

filename: src/transformers/quantizers/quantizer_quark.py
patch: @@ -94,3 +94,38 @@ def is_serializable(self, safe_serialization=None):
     @property
     def is_trainable(self):
         return False
+
+    def get_weight_conversions(self):
+        from ..core_model_loading import WeightConverter
+        from ..integrations.quark import QuarkDeserialize
+        # In Quark, quantization is managed through a QParamsLinear module, which holds
+        # separate quantizers for the weights, inputs, and biases (e.g. weight_quantizer
+        # input_quantizer, bias_quantizer, etc.).
+        #
+        # When you call `module.state_dict()`, Quark automatically renames the quantizer
+        # parameters — for example, `input_quantizer.scale` becomes `input_scale` — and
+        # saves them directly at the parent module level.
+        #
+        # This means we cannot simply rename keys like `weight_scale` back to
+        # `weight_quantizer.scale` when loading the state_dict.
+        # Otherwise, the `missing_keys` list would still expect keys such as
+        # `weight_scale`, `bias_scale`, etc.
+        #
+        # To fix this, we keep the expected state_dict keys (like `weight_scale`,
+        # `bias_scale`, etc.) unchanged, and during the conversion step, we explicitly
+        # assign their values into the corresponding quantizer attributes
+        # (`weight_quantizer.scale`, `input_quantizer.scale`, and so on).
+
+        # You can notice here that in target_patterns we use the same key as the source_patterns,
+        # this is because we just want to collect the tensors, and we will rename them later in the convert function.
+        # We cannot rename directly or else the missing_keys list will not be able to find the tensors.
+        converters = []
+        for key in CHECKPOINT_KEYS.keys():
+            converters.append(
+                WeightConverter(
+                    source_patterns=[key],
+                    target_patterns=key,
+                    operations=[QuarkDeserialize(self)],
+                )
+            )
+        return converters

filename: tests/quantization/quark_integration/test_quark.py
patch: @@ -56,6 +56,7 @@ class QuarkTest(unittest.TestCase):
     EXPECTED_OUTPUTS.add("Today I am in Paris and I am enjoying my day off! The sun is shining, the birds are")
     EXPECTED_OUTPUTS.add("Today I am in Paris and I'm here to tell you about it. It's a beautiful day,")
     EXPECTED_OUTPUTS.add("Today I am in Paris and I am not in Paris at all! I am not in Paris, but")
+    EXPECTED_OUTPUTS.add("Today I am in Paris and I am in Paris, but I am not in Paris\nToday I am")
 
     EXPECTED_RELATIVE_DIFFERENCE = 1.66
     device_map = None