nvidia-modelopt 0.35.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. modelopt/__init__.py +20 -0
  2. modelopt/deploy/__init__.py +16 -0
  3. modelopt/deploy/llm/__init__.py +30 -0
  4. modelopt/deploy/llm/generate.py +345 -0
  5. modelopt/onnx/__init__.py +34 -0
  6. modelopt/onnx/autocast/__init__.py +19 -0
  7. modelopt/onnx/autocast/__main__.py +187 -0
  8. modelopt/onnx/autocast/convert.py +202 -0
  9. modelopt/onnx/autocast/graphsanitizer.py +422 -0
  10. modelopt/onnx/autocast/logging_config.py +78 -0
  11. modelopt/onnx/autocast/nodeclassifier.py +416 -0
  12. modelopt/onnx/autocast/precisionconverter.py +1032 -0
  13. modelopt/onnx/autocast/referencerunner.py +159 -0
  14. modelopt/onnx/autocast/utils.py +117 -0
  15. modelopt/onnx/llm_export_utils/__init__.py +16 -0
  16. modelopt/onnx/llm_export_utils/export_utils.py +162 -0
  17. modelopt/onnx/llm_export_utils/quantization_utils.py +122 -0
  18. modelopt/onnx/llm_export_utils/surgeon_utils.py +120 -0
  19. modelopt/onnx/logging_config.py +77 -0
  20. modelopt/onnx/op_types.py +300 -0
  21. modelopt/onnx/quantization/__init__.py +20 -0
  22. modelopt/onnx/quantization/__main__.py +291 -0
  23. modelopt/onnx/quantization/calib_utils.py +178 -0
  24. modelopt/onnx/quantization/extensions.py +35 -0
  25. modelopt/onnx/quantization/fp8.py +375 -0
  26. modelopt/onnx/quantization/graph_utils.py +1479 -0
  27. modelopt/onnx/quantization/gs_patching.py +112 -0
  28. modelopt/onnx/quantization/int4.py +1347 -0
  29. modelopt/onnx/quantization/int8.py +283 -0
  30. modelopt/onnx/quantization/operators.py +121 -0
  31. modelopt/onnx/quantization/ort_patching.py +1766 -0
  32. modelopt/onnx/quantization/ort_utils.py +346 -0
  33. modelopt/onnx/quantization/partitioning.py +439 -0
  34. modelopt/onnx/quantization/qdq_utils.py +1349 -0
  35. modelopt/onnx/quantization/quant_utils.py +283 -0
  36. modelopt/onnx/quantization/quantize.py +528 -0
  37. modelopt/onnx/quantization/src/modelopt_round_and_pack_ext.cpp +127 -0
  38. modelopt/onnx/trt_utils.py +424 -0
  39. modelopt/onnx/utils.py +753 -0
  40. modelopt/torch/__init__.py +41 -0
  41. modelopt/torch/_deploy/__init__.py +23 -0
  42. modelopt/torch/_deploy/_runtime/__init__.py +29 -0
  43. modelopt/torch/_deploy/_runtime/common.py +72 -0
  44. modelopt/torch/_deploy/_runtime/ort_client.py +193 -0
  45. modelopt/torch/_deploy/_runtime/registry.py +83 -0
  46. modelopt/torch/_deploy/_runtime/runtime_client.py +154 -0
  47. modelopt/torch/_deploy/_runtime/tensorrt/constants.py +108 -0
  48. modelopt/torch/_deploy/_runtime/tensorrt/engine_builder.py +324 -0
  49. modelopt/torch/_deploy/_runtime/tensorrt/hw_param_config.py +51 -0
  50. modelopt/torch/_deploy/_runtime/tensorrt/layerwise_profiling.py +178 -0
  51. modelopt/torch/_deploy/_runtime/tensorrt/parse_trtexec_log.py +151 -0
  52. modelopt/torch/_deploy/_runtime/tensorrt/tensorrt_utils.py +200 -0
  53. modelopt/torch/_deploy/_runtime/trt_client.py +219 -0
  54. modelopt/torch/_deploy/compilation.py +142 -0
  55. modelopt/torch/_deploy/device_model.py +157 -0
  56. modelopt/torch/_deploy/profiling.py +193 -0
  57. modelopt/torch/_deploy/utils/__init__.py +18 -0
  58. modelopt/torch/_deploy/utils/onnx_optimizer.py +120 -0
  59. modelopt/torch/_deploy/utils/onnx_utils.py +58 -0
  60. modelopt/torch/_deploy/utils/torch_onnx.py +593 -0
  61. modelopt/torch/distill/__init__.py +28 -0
  62. modelopt/torch/distill/config.py +130 -0
  63. modelopt/torch/distill/distillation.py +84 -0
  64. modelopt/torch/distill/distillation_model.py +318 -0
  65. modelopt/torch/distill/loss_balancers.py +140 -0
  66. modelopt/torch/distill/losses.py +275 -0
  67. modelopt/torch/distill/mode.py +223 -0
  68. modelopt/torch/distill/plugins/__init__.py +24 -0
  69. modelopt/torch/distill/plugins/huggingface.py +110 -0
  70. modelopt/torch/distill/plugins/megatron.py +476 -0
  71. modelopt/torch/distill/registry.py +23 -0
  72. modelopt/torch/export/__init__.py +24 -0
  73. modelopt/torch/export/convert_hf_config.py +117 -0
  74. modelopt/torch/export/distribute.py +300 -0
  75. modelopt/torch/export/hf_config_map.py +84 -0
  76. modelopt/torch/export/layer_utils.py +1893 -0
  77. modelopt/torch/export/mcore_config_map.py +25 -0
  78. modelopt/torch/export/model_config.py +623 -0
  79. modelopt/torch/export/model_config_export.py +552 -0
  80. modelopt/torch/export/model_config_utils.py +392 -0
  81. modelopt/torch/export/model_utils.py +71 -0
  82. modelopt/torch/export/plugins/__init__.py +21 -0
  83. modelopt/torch/export/plugins/mcore_common.py +67 -0
  84. modelopt/torch/export/plugins/mcore_custom.py +530 -0
  85. modelopt/torch/export/plugins/mcore_deepseek.py +143 -0
  86. modelopt/torch/export/plugins/mcore_gptoss.py +71 -0
  87. modelopt/torch/export/plugins/mcore_llama.py +164 -0
  88. modelopt/torch/export/plugins/mcore_nemotron.py +90 -0
  89. modelopt/torch/export/plugins/mcore_qwen.py +99 -0
  90. modelopt/torch/export/plugins/megatron_importer.py +647 -0
  91. modelopt/torch/export/postprocess.py +847 -0
  92. modelopt/torch/export/quant_utils.py +1069 -0
  93. modelopt/torch/export/tensorrt_llm_type.py +42 -0
  94. modelopt/torch/export/tensorrt_llm_utils.py +405 -0
  95. modelopt/torch/export/transformer_engine.py +94 -0
  96. modelopt/torch/export/unified_export_hf.py +537 -0
  97. modelopt/torch/export/unified_export_megatron.py +1164 -0
  98. modelopt/torch/nas/__init__.py +27 -0
  99. modelopt/torch/nas/algorithms.py +856 -0
  100. modelopt/torch/nas/autonas.py +808 -0
  101. modelopt/torch/nas/conversion.py +119 -0
  102. modelopt/torch/nas/hparams/__init__.py +19 -0
  103. modelopt/torch/nas/hparams/concat.py +319 -0
  104. modelopt/torch/nas/hparams/container.py +48 -0
  105. modelopt/torch/nas/modules/__init__.py +21 -0
  106. modelopt/torch/nas/modules/container.py +99 -0
  107. modelopt/torch/nas/modules/conv.py +254 -0
  108. modelopt/torch/nas/modules/linear.py +76 -0
  109. modelopt/torch/nas/modules/norm.py +193 -0
  110. modelopt/torch/nas/modules/utils.py +61 -0
  111. modelopt/torch/nas/patch.py +261 -0
  112. modelopt/torch/nas/plugins/__init__.py +29 -0
  113. modelopt/torch/nas/plugins/megatron.py +1497 -0
  114. modelopt/torch/nas/plugins/torch.py +71 -0
  115. modelopt/torch/nas/plugins/transformer_engine.py +42 -0
  116. modelopt/torch/nas/plugins/transformers.py +210 -0
  117. modelopt/torch/nas/registry.py +23 -0
  118. modelopt/torch/nas/search_space.py +260 -0
  119. modelopt/torch/nas/traced_hp.py +149 -0
  120. modelopt/torch/nas/utils.py +408 -0
  121. modelopt/torch/opt/__init__.py +41 -0
  122. modelopt/torch/opt/_hooks.py +98 -0
  123. modelopt/torch/opt/config.py +383 -0
  124. modelopt/torch/opt/conversion.py +620 -0
  125. modelopt/torch/opt/dynamic.py +1367 -0
  126. modelopt/torch/opt/hparam.py +275 -0
  127. modelopt/torch/opt/mode.py +353 -0
  128. modelopt/torch/opt/plugins/__init__.py +38 -0
  129. modelopt/torch/opt/plugins/diffusers.py +40 -0
  130. modelopt/torch/opt/plugins/huggingface.py +162 -0
  131. modelopt/torch/opt/plugins/mcore_dist_checkpointing.py +211 -0
  132. modelopt/torch/opt/plugins/megatron.py +142 -0
  133. modelopt/torch/opt/plugins/peft.py +108 -0
  134. modelopt/torch/opt/plugins/transformers.py +160 -0
  135. modelopt/torch/opt/searcher.py +363 -0
  136. modelopt/torch/opt/utils.py +125 -0
  137. modelopt/torch/prune/__init__.py +26 -0
  138. modelopt/torch/prune/fastnas.py +376 -0
  139. modelopt/torch/prune/gradnas.py +349 -0
  140. modelopt/torch/prune/plugins/__init__.py +24 -0
  141. modelopt/torch/prune/plugins/mcore_minitron.py +263 -0
  142. modelopt/torch/prune/plugins/transformers.py +41 -0
  143. modelopt/torch/prune/pruning.py +211 -0
  144. modelopt/torch/quantization/__init__.py +26 -0
  145. modelopt/torch/quantization/algorithms.py +671 -0
  146. modelopt/torch/quantization/backends/__init__.py +20 -0
  147. modelopt/torch/quantization/backends/fp8_per_tensor_gemm.py +204 -0
  148. modelopt/torch/quantization/backends/gemm_registry.py +156 -0
  149. modelopt/torch/quantization/backends/nvfp4_gemm.py +201 -0
  150. modelopt/torch/quantization/backends/utils.py +28 -0
  151. modelopt/torch/quantization/calib/__init__.py +25 -0
  152. modelopt/torch/quantization/calib/bias.py +172 -0
  153. modelopt/torch/quantization/calib/calibrator.py +63 -0
  154. modelopt/torch/quantization/calib/histogram.py +434 -0
  155. modelopt/torch/quantization/calib/max.py +110 -0
  156. modelopt/torch/quantization/compress.py +227 -0
  157. modelopt/torch/quantization/config.py +1180 -0
  158. modelopt/torch/quantization/conversion.py +389 -0
  159. modelopt/torch/quantization/export_onnx.py +639 -0
  160. modelopt/torch/quantization/extensions.py +89 -0
  161. modelopt/torch/quantization/mode.py +428 -0
  162. modelopt/torch/quantization/model_calib.py +949 -0
  163. modelopt/torch/quantization/model_quant.py +477 -0
  164. modelopt/torch/quantization/nn/__init__.py +26 -0
  165. modelopt/torch/quantization/nn/functional.py +109 -0
  166. modelopt/torch/quantization/nn/modules/__init__.py +16 -0
  167. modelopt/torch/quantization/nn/modules/quant_activations.py +22 -0
  168. modelopt/torch/quantization/nn/modules/quant_batchnorm.py +24 -0
  169. modelopt/torch/quantization/nn/modules/quant_conv.py +123 -0
  170. modelopt/torch/quantization/nn/modules/quant_instancenorm.py +39 -0
  171. modelopt/torch/quantization/nn/modules/quant_linear.py +258 -0
  172. modelopt/torch/quantization/nn/modules/quant_module.py +211 -0
  173. modelopt/torch/quantization/nn/modules/quant_pooling.py +99 -0
  174. modelopt/torch/quantization/nn/modules/quant_rnn.py +527 -0
  175. modelopt/torch/quantization/nn/modules/tensor_quantizer.py +1293 -0
  176. modelopt/torch/quantization/plugins/__init__.py +73 -0
  177. modelopt/torch/quantization/plugins/accelerate.py +214 -0
  178. modelopt/torch/quantization/plugins/apex.py +52 -0
  179. modelopt/torch/quantization/plugins/attention.py +312 -0
  180. modelopt/torch/quantization/plugins/custom.py +192 -0
  181. modelopt/torch/quantization/plugins/diffusers.py +237 -0
  182. modelopt/torch/quantization/plugins/fairscale.py +45 -0
  183. modelopt/torch/quantization/plugins/huggingface.py +736 -0
  184. modelopt/torch/quantization/plugins/megatron.py +462 -0
  185. modelopt/torch/quantization/plugins/peft.py +148 -0
  186. modelopt/torch/quantization/plugins/transformer_engine.py +60 -0
  187. modelopt/torch/quantization/plugins/transformers.py +52 -0
  188. modelopt/torch/quantization/plugins/transformers_trainer.py +426 -0
  189. modelopt/torch/quantization/plugins/trl.py +27 -0
  190. modelopt/torch/quantization/plugins/vllm.py +199 -0
  191. modelopt/torch/quantization/qtensor/__init__.py +24 -0
  192. modelopt/torch/quantization/qtensor/base_qtensor.py +370 -0
  193. modelopt/torch/quantization/qtensor/fp8_tensor.py +151 -0
  194. modelopt/torch/quantization/qtensor/int4_tensor.py +130 -0
  195. modelopt/torch/quantization/qtensor/int8_tensor.py +124 -0
  196. modelopt/torch/quantization/qtensor/mxfp4_tensor.py +144 -0
  197. modelopt/torch/quantization/qtensor/nf4_tensor.py +199 -0
  198. modelopt/torch/quantization/qtensor/nvfp4_tensor.py +295 -0
  199. modelopt/torch/quantization/src/tensor_quant.cpp +77 -0
  200. modelopt/torch/quantization/src/tensor_quant.h +69 -0
  201. modelopt/torch/quantization/src/tensor_quant_fp8.cpp +48 -0
  202. modelopt/torch/quantization/src/tensor_quant_gpu.cu +361 -0
  203. modelopt/torch/quantization/src/tensor_quant_gpu_fp8.cu +122 -0
  204. modelopt/torch/quantization/src/tensor_quant_mx.cu +411 -0
  205. modelopt/torch/quantization/src/tensor_quant_mx.h +247 -0
  206. modelopt/torch/quantization/tensor_quant.py +816 -0
  207. modelopt/torch/quantization/triton/__init__.py +36 -0
  208. modelopt/torch/quantization/triton/fp4_kernel.py +303 -0
  209. modelopt/torch/quantization/utils.py +443 -0
  210. modelopt/torch/sparsity/__init__.py +19 -0
  211. modelopt/torch/sparsity/config.py +51 -0
  212. modelopt/torch/sparsity/magnitude.py +148 -0
  213. modelopt/torch/sparsity/mode.py +203 -0
  214. modelopt/torch/sparsity/module.py +87 -0
  215. modelopt/torch/sparsity/plugins/__init__.py +27 -0
  216. modelopt/torch/sparsity/plugins/megatron.py +93 -0
  217. modelopt/torch/sparsity/searcher.py +84 -0
  218. modelopt/torch/sparsity/sparsegpt.py +276 -0
  219. modelopt/torch/sparsity/sparsification.py +123 -0
  220. modelopt/torch/speculative/__init__.py +20 -0
  221. modelopt/torch/speculative/config.py +100 -0
  222. modelopt/torch/speculative/eagle/__init__.py +20 -0
  223. modelopt/torch/speculative/eagle/conversion.py +67 -0
  224. modelopt/torch/speculative/eagle/default_config.py +50 -0
  225. modelopt/torch/speculative/eagle/eagle_model.py +51 -0
  226. modelopt/torch/speculative/eagle/utils.py +91 -0
  227. modelopt/torch/speculative/medusa/__init__.py +19 -0
  228. modelopt/torch/speculative/medusa/conversion.py +59 -0
  229. modelopt/torch/speculative/medusa/medusa_model.py +32 -0
  230. modelopt/torch/speculative/mode.py +86 -0
  231. modelopt/torch/speculative/plugins/__init__.py +33 -0
  232. modelopt/torch/speculative/plugins/megatron_eagle.py +2119 -0
  233. modelopt/torch/speculative/plugins/megatron_medusa.py +312 -0
  234. modelopt/torch/speculative/plugins/transformers.py +1138 -0
  235. modelopt/torch/speculative/speculative_decoding.py +59 -0
  236. modelopt/torch/speculative/utils.py +364 -0
  237. modelopt/torch/trace/__init__.py +25 -0
  238. modelopt/torch/trace/analyzer.py +1368 -0
  239. modelopt/torch/trace/modules/__init__.py +19 -0
  240. modelopt/torch/trace/modules/concat.py +384 -0
  241. modelopt/torch/trace/modules/nn.py +153 -0
  242. modelopt/torch/trace/plugins/__init__.py +34 -0
  243. modelopt/torch/trace/plugins/megatron.py +36 -0
  244. modelopt/torch/trace/plugins/transformers.py +58 -0
  245. modelopt/torch/trace/symbols.py +545 -0
  246. modelopt/torch/trace/tracer.py +331 -0
  247. modelopt/torch/utils/__init__.py +28 -0
  248. modelopt/torch/utils/_pytree.py +133 -0
  249. modelopt/torch/utils/cpp_extension.py +91 -0
  250. modelopt/torch/utils/dataset_utils.py +484 -0
  251. modelopt/torch/utils/distributed.py +311 -0
  252. modelopt/torch/utils/graph.py +123 -0
  253. modelopt/torch/utils/image_processor.py +112 -0
  254. modelopt/torch/utils/import_utils.py +35 -0
  255. modelopt/torch/utils/list.py +54 -0
  256. modelopt/torch/utils/logging.py +127 -0
  257. modelopt/torch/utils/memory_monitor.py +146 -0
  258. modelopt/torch/utils/network.py +658 -0
  259. modelopt/torch/utils/perf.py +174 -0
  260. modelopt/torch/utils/plugins/__init__.py +27 -0
  261. modelopt/torch/utils/plugins/megatron_generate.py +299 -0
  262. modelopt/torch/utils/plugins/megatron_mmlu.py +152 -0
  263. modelopt/torch/utils/plugins/megatron_preprocess_data.py +200 -0
  264. modelopt/torch/utils/random.py +163 -0
  265. modelopt/torch/utils/speech_dataset_utils.py +134 -0
  266. modelopt/torch/utils/tensor.py +87 -0
  267. modelopt/torch/utils/vlm_dataset_utils.py +110 -0
  268. nvidia_modelopt-0.35.0.dist-info/METADATA +171 -0
  269. nvidia_modelopt-0.35.0.dist-info/RECORD +272 -0
  270. nvidia_modelopt-0.35.0.dist-info/WHEEL +5 -0
  271. nvidia_modelopt-0.35.0.dist-info/licenses/LICENSE +14 -0
  272. nvidia_modelopt-0.35.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1293 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """TensorQuantizer Module."""
17
+
18
+ import contextlib
19
+ import math
20
+ import warnings
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ import torch
24
+ import torch.distributed as dist
25
+
26
+ try:
27
+ from torch.distributed.tensor import DTensor
28
+ except ImportError:
29
+ DTensor = None
30
+
31
+ import torch.nn.functional as F
32
+ from packaging.version import Version
33
+ from torch import nn
34
+ from torch.onnx._globals import GLOBALS
35
+
36
+ from modelopt.torch.utils import standardize_constructor_args
37
+ from modelopt.torch.utils.distributed import DistributedProcessGroup
38
+
39
+ from ... import calib
40
+ from ... import utils as quant_utils
41
+ from ...config import QuantizerAttributeConfig
42
+ from ...qtensor import (
43
+ BaseQuantizedTensor,
44
+ FP8QTensor,
45
+ INT4QTensor,
46
+ INT8QTensor,
47
+ MXFP4QTensor,
48
+ NF4QTensor,
49
+ NVFP4QTensor,
50
+ QTensorWrapper,
51
+ )
52
+ from ...tensor_quant import dynamic_block_quant, fake_tensor_quant, scaled_e4m3
53
+ from ...utils import is_torch_export_mode
54
+ from ..functional import normalized_hadamard_transform
55
+
56
+ if TYPE_CHECKING:
57
+ from collections.abc import Callable
58
+
59
+ __all__ = ["SequentialQuantizer", "TensorQuantizer"]
60
+
61
+
62
+ class TensorQuantizer(nn.Module):
63
+ """Tensor quantizer module.
64
+
65
+ This module manages quantization and calibration of input tensor. It can perform fake (simulated quantization)
66
+ or real quantization for various precisions and formats such as FP8 per-tensor, INT8 per-channel,
67
+ INT4 per-block etc.
68
+
69
+ If quantization is enabled, it calls the appropriate quantization functional and
70
+ returns the quantized tensor. The quantized tensor data type will be same as the input tensor data type for
71
+ fake quantization. During calibration mode, the module collects the statistics using its calibrator.
72
+
73
+ The quantization parameters are as described in
74
+ :class:`QuantizerAttributeConfig <modelopt.torch.quantization.config.QuantizerAttributeConfig>`. They can be set
75
+ at initialization using ``quant_attribute_cfg`` or later by calling :meth:`set_from_attribute_config`.
76
+
77
+ Args:
78
+ quant_attribute_cfg: An instance of
79
+ :class:`QuantizerAttributeConfig <modelopt.torch.quantization.config.QuantizerAttributeConfig>` or None.
80
+ If None, default values are used.
81
+ if_quant: A boolean. If True, quantization is enabled in the forward path.
82
+ if_calib: A boolean. If True, calibration is enabled in the forward path.
83
+ amax: None or an array like object such as list, tuple, numpy array, scalar
84
+ which can be used to construct amax tensor.
85
+ """
86
+
87
+ _skip_properties_for_save_restore = {
88
+ "_calibrator",
89
+ "_bias_calibrator",
90
+ "_original_shape",
91
+ "_block_reshape_size",
92
+ "_padding",
93
+ # Extra flags added by huggingface
94
+ "_is_hf_initialized",
95
+ # Extra flags added by deepspeed
96
+ "ds_external_parameters",
97
+ "all_parameters",
98
+ "_external_params",
99
+ "_original_parameters",
100
+ "post_bwd_fn",
101
+ "ds_grads_remaining",
102
+ "ds_id",
103
+ "pre_bwd_fn",
104
+ }
105
+
106
+ def __init__(
107
+ self,
108
+ quant_attribute_cfg=None,
109
+ if_quant=True,
110
+ if_calib=False,
111
+ amax=None,
112
+ ):
113
+ """Initialize quantizer and set up required variables."""
114
+ super().__init__()
115
+ quant_attribute_cfg = (
116
+ quant_attribute_cfg if quant_attribute_cfg is not None else QuantizerAttributeConfig()
117
+ )
118
+ if amax is not None:
119
+ self.amax = amax
120
+
121
+ self.set_from_attribute_config(quant_attribute_cfg)
122
+
123
+ self._if_quant = if_quant
124
+ self._if_calib = if_calib
125
+ self._enable_pre_quant_scale = True
126
+ self._dequantize = False
127
+ self._input_dtype = None
128
+
129
+ # Lazy initialize the bias calibrator for KV cache quantization
130
+ self._bias_calibrator = None
131
+
132
+ def set_from_attribute_config(self, attribute_cfg: QuantizerAttributeConfig | dict):
133
+ """Set quantizer attributes from attribute_dict.
134
+
135
+ The attributes are defined in
136
+ :class:`QuantizerAttributeConfig <modelopt.torch.quantization.config.QuantizerAttributeConfig>`.
137
+ """
138
+
139
+ def _calibrator_setter(val):
140
+ if val in ["max", "histogram"]:
141
+ calib_cls = calib.MaxCalibrator if val == "max" else calib.HistogramCalibrator
142
+ args, kwargs = (self._num_bits, self._axis, self._unsigned), {}
143
+ else:
144
+ calib_cls, args, kwargs = standardize_constructor_args(val)
145
+ return calib_cls(*args, **kwargs)
146
+
147
+ # Some attributes need custom handling.
148
+ # By default, attributes from config are mapped to a name ``f"_{attribute}"``
149
+ _custom_setters: dict[str, tuple[str, Callable]] = {
150
+ "enable": ("_disabled", lambda val: val is False),
151
+ "type": ("_dynamic", lambda val: val == "dynamic"),
152
+ "calibrator": ("_calibrator", _calibrator_setter),
153
+ }
154
+
155
+ for attribute, val in attribute_cfg.items():
156
+ assert attribute in QuantizerAttributeConfig.model_fields, (
157
+ f"{attribute} is not a valid `TensorQuantizer` attribute"
158
+ )
159
+ _tq_attribute_name, _setter = _custom_setters.get(
160
+ attribute, (f"_{attribute}", lambda v: v)
161
+ )
162
+ setattr(self, _tq_attribute_name, _setter(val))
163
+
164
+ if self.is_mx_format:
165
+ self._pass_through_bwd = True
166
+
167
+ def dequantize(self, inputs: BaseQuantizedTensor | QTensorWrapper):
168
+ """De-quantize a real quantized tensor to a given dtype."""
169
+ qtensor = inputs.get_qtensor() if isinstance(inputs, QTensorWrapper) else inputs
170
+ assert isinstance(qtensor, BaseQuantizedTensor), "Expected input as real quantized tensors."
171
+ kwarg = {
172
+ "scale": self._scale,
173
+ "block_sizes": self.block_sizes,
174
+ "double_scale": getattr(self, "_double_scale", None),
175
+ "scale_zeros": getattr(self, "_scale_zeros", None),
176
+ }
177
+ return qtensor.dequantize(**kwarg)
178
+
179
+ @property
180
+ def num_bits(self):
181
+ """Return num_bits for quantization."""
182
+ return self._num_bits
183
+
184
+ @num_bits.setter
185
+ def num_bits(self, value):
186
+ self._num_bits = value
187
+ self._calibrator._num_bits = value
188
+
189
+ @property
190
+ def maxbound(self):
191
+ """Return maxbound for quantization."""
192
+ if self._num_bits == (4, 3):
193
+ return 448.0
194
+ if self._num_bits == (2, 1) and self._block_sizes.get("scale_bits") == (4, 3):
195
+ return 6.0
196
+ return (1 << (self._num_bits - 1 + int(self._unsigned))) - 1
197
+
198
+ @property
199
+ def unsigned(self):
200
+ """Return True if unsigned quantization is used."""
201
+ return self._unsigned
202
+
203
+ @unsigned.setter
204
+ def unsigned(self, value):
205
+ self._unsigned = value
206
+ self._calibrator._unsigned = value
207
+
208
+ @property
209
+ def pre_quant_scale(self):
210
+ """Return pre_quant_scale used for smoothquant."""
211
+ if not hasattr(self, "_pre_quant_scale") or not self._enable_pre_quant_scale:
212
+ return None
213
+ return self._pre_quant_scale
214
+
215
+ @pre_quant_scale.setter
216
+ def pre_quant_scale(self, value):
217
+ assert value is not None, "pre_quant_scale cannot be set to None."
218
+ assert self._enable_pre_quant_scale, (
219
+ "pre_quant_scale cannot be set when forward_with_pre_quant_scale is False."
220
+ )
221
+ if not isinstance(value, torch.Tensor):
222
+ value = torch.tensor(value)
223
+ if not hasattr(self, "_pre_quant_scale"):
224
+ self.register_buffer("_pre_quant_scale", value.clone().detach())
225
+ else:
226
+ if self._pre_quant_scale.shape != value.shape:
227
+ raise RuntimeError("Changing shape when setting pre_quant_scale is not allowed.")
228
+ self._pre_quant_scale.data.copy_(
229
+ value.clone().detach().to(self._pre_quant_scale.device)
230
+ )
231
+
232
+ @property
233
+ def amax(self):
234
+ """Return amax for quantization."""
235
+ if not hasattr(self, "_amax") or self.is_mx_format:
236
+ return None
237
+ assert not self._dynamic, "Dynamic quantization does not have fixed amax"
238
+ return self._amax
239
+
240
+ @amax.setter
241
+ def amax(self, value):
242
+ assert value is not None, "amax cannot be set to None."
243
+
244
+ if not isinstance(value, torch.Tensor):
245
+ value = torch.tensor(value)
246
+
247
+ if not hasattr(self, "_amax"):
248
+ self.register_buffer("_amax", value.clone().detach())
249
+ else:
250
+ if self._amax.shape != value.shape:
251
+ raise RuntimeError("Changing shape when setting amax is not allowed.")
252
+ self._amax.data.copy_(value.clone().detach().to(self._amax.device))
253
+
254
+ def reset_amax(self):
255
+ """Reset amax to None."""
256
+ if hasattr(self, "_amax"):
257
+ delattr(self, "_amax")
258
+ self._calibrator.reset()
259
+
260
+ def reset_bias(self):
261
+ """Reset bias to None."""
262
+ if hasattr(self, "_bias_value"):
263
+ delattr(self, "_bias_value")
264
+ if self._bias_calibrator is not None:
265
+ self._bias_calibrator.reset()
266
+
267
+ @property
268
+ def step_size(self):
269
+ """Return step size for integer quantization."""
270
+ if not hasattr(self, "_amax"):
271
+ warnings.warn("step_size is undefined under dynamic amax mode!")
272
+ return None
273
+ assert isinstance(self._num_bits, int), (
274
+ "Step size is not defined for non-integer quantization."
275
+ )
276
+ return self._amax / (2.0 ** (self._num_bits - 1 + int(self._unsigned)) - 1.0)
277
+
278
+ @property
279
+ def axis(self):
280
+ """Return axis for quantization."""
281
+ return self._axis
282
+
283
+ @axis.setter
284
+ def axis(self, value):
285
+ self._axis = value
286
+ self._calibrator._axis = value
287
+
288
+ @property
289
+ def block_sizes(self):
290
+ """Return block_sizes for quantization."""
291
+ return self._block_sizes
292
+
293
+ @block_sizes.setter
294
+ def block_sizes(self, value):
295
+ self._axis = None
296
+ self._block_sizes = value
297
+
298
+ @property
299
+ def bias(self):
300
+ """Return bias for quantization."""
301
+ if not hasattr(self, "_bias"):
302
+ return None
303
+ return self._bias
304
+
305
+ @property
306
+ def bias_axis(self):
307
+ """Return bias_axis for quantization."""
308
+ if not hasattr(self, "_bias_axis"):
309
+ return None
310
+ return self._bias_axis
311
+
312
+ @bias_axis.setter
313
+ def bias_axis(self, value):
314
+ assert value is not None, "bias_axis cannot be set to None."
315
+ assert isinstance(value, (tuple, list)), "bias_axis must be a tuple or a list."
316
+ self._bias_axis = value
317
+
318
+ @property
319
+ def bias_method(self):
320
+ """Return bias_method for quantization."""
321
+ if self._bias is None:
322
+ return None
323
+ return self._bias.get("method", "mean")
324
+
325
+ @property
326
+ def bias_type(self):
327
+ """Return bias_type for quantization."""
328
+ if self._bias is None:
329
+ return None
330
+ return self._bias.get("type", "static")
331
+
332
+ @bias_type.setter
333
+ def bias_type(self, value):
334
+ assert value in [
335
+ "static",
336
+ "dynamic",
337
+ ], "bias_type must be either 'static' or 'dynamic'."
338
+ self._bias["type"] = value
339
+
340
+ @property
341
+ def bias_value(self):
342
+ """Return bias for quantization."""
343
+ if not hasattr(self, "_bias_value"):
344
+ return None
345
+ return self._bias_value
346
+
347
+ @bias_value.setter
348
+ def bias_value(self, value):
349
+ assert value is not None, "bias cannot be set to None."
350
+
351
+ if not isinstance(value, torch.Tensor):
352
+ value = torch.tensor(value)
353
+
354
+ if not hasattr(self, "_bias_value"):
355
+ self.register_buffer("_bias_value", value.clone().detach())
356
+ else:
357
+ if self._bias_value.shape != value.shape:
358
+ raise RuntimeError("Changing shape when setting bias is not allowed.")
359
+ self._bias_value.data.copy_(value.clone().detach().to(self._bias_value.device))
360
+
361
+ @property
362
+ def bias_calibrator(self):
363
+ """Return bias_calibrator for quantization."""
364
+ # Get reduce_axis from bias config
365
+ # Bias calibration supports per-channel and per-token quantization
366
+ if self._bias_calibrator is None and self.bias is not None:
367
+ self.bias_axis = tuple(k for k in self.bias if isinstance(k, int))
368
+ if self._bias is not None:
369
+ self._bias_calibrator = calib.BiasCalibrator(
370
+ method=self.bias_method,
371
+ axis=self.bias_axis,
372
+ )
373
+
374
+ return self._bias_calibrator
375
+
376
+ @property
377
+ def fake_quant(self):
378
+ """Return True if fake quantization is used."""
379
+ return self._fake_quant
380
+
381
+ @property
382
+ def narrow_range(self):
383
+ """Return True if symmetric integer range for signed quantization is used."""
384
+ return self._narrow_range
385
+
386
+ @narrow_range.setter
387
+ def narrow_range(self, value):
388
+ self._narrow_range = value
389
+
390
+ @property
391
+ def is_enabled(self):
392
+ """Return true if the modules is not disabled."""
393
+ return not self._disabled
394
+
395
+ def disable(self):
396
+ """Bypass the module.
397
+
398
+ Neither of calibration, clipping and quantization will be performed if the module is disabled.
399
+ """
400
+ self._disabled = True
401
+
402
+ def enable(self):
403
+ """Enable the module."""
404
+ self._disabled = False
405
+
406
+ @property
407
+ def trt_high_precision_dtype(self):
408
+ """Return True if FP16 AMAX is used when exporting the model."""
409
+ return self._trt_high_precision_dtype
410
+
411
+ @trt_high_precision_dtype.setter
412
+ def trt_high_precision_dtype(self, value):
413
+ self._trt_high_precision_dtype = value
414
+
415
+ @property
416
+ def is_mx_format(self):
417
+ """Check if is MX formats."""
418
+ return (
419
+ self.block_sizes is not None
420
+ and self.block_sizes.get("type", None) == "dynamic"
421
+ and self.block_sizes.get("scale_bits", None) == (8, 0)
422
+ )
423
+
424
+ @property
425
+ def svdquant_lora_a(self):
426
+ """Lora a weights for svdquant."""
427
+ if not hasattr(self, "_svdquant_lora_a"):
428
+ return None
429
+ return self._svdquant_lora_a
430
+
431
+ @svdquant_lora_a.setter
432
+ def svdquant_lora_a(self, value):
433
+ """Lora a weights for svdquant."""
434
+ assert value is not None, "svdquant_lora_a cannot be set to None."
435
+
436
+ if not isinstance(value, torch.Tensor):
437
+ value = torch.tensor(value)
438
+
439
+ if not hasattr(self, "_svdquant_lora_a"):
440
+ self.register_buffer("_svdquant_lora_a", value.clone().detach())
441
+ else:
442
+ if self._svdquant_lora_a.shape != value.shape:
443
+ raise RuntimeError("Changing shape when setting svdquant_lora_a is not allowed.")
444
+ self._svdquant_lora_a.data.copy_(
445
+ value.clone().detach().to(self._svdquant_lora_a.device)
446
+ )
447
+
448
+ @property
449
+ def svdquant_lora_b(self):
450
+ """Lora b weights for svdquant."""
451
+ if not hasattr(self, "_svdquant_lora_b"):
452
+ return None
453
+ return self._svdquant_lora_b
454
+
455
+ @svdquant_lora_b.setter
456
+ def svdquant_lora_b(self, value):
457
+ """Lora b weights for svdquant."""
458
+ assert value is not None, "svdquant_lora_b cannot be set to None."
459
+
460
+ if not isinstance(value, torch.Tensor):
461
+ value = torch.tensor(value)
462
+
463
+ if not hasattr(self, "_svdquant_lora_b"):
464
+ self.register_buffer("_svdquant_lora_b", value.clone().detach())
465
+ else:
466
+ if self._svdquant_lora_b.shape != value.shape:
467
+ raise RuntimeError("Changing shape when setting svdquant_lora_b is not allowed.")
468
+ self._svdquant_lora_b.data.copy_(
469
+ value.clone().detach().to(self._svdquant_lora_b.device)
470
+ )
471
+
472
+ def disable_calib(self):
473
+ """Disable calibration."""
474
+ self._if_calib = False
475
+
476
+ def enable_calib(self):
477
+ """Enable calibration."""
478
+ if self._calibrator is None:
479
+ raise ValueError("Calibrator was not created, cannot enable calibration.")
480
+
481
+ # Dynamic quantization does not need calibration.
482
+ if self._dynamic:
483
+ return
484
+ self._if_calib = True
485
+
486
+ def disable_quant(self):
487
+ """Disable quantization."""
488
+ self._if_quant = False
489
+
490
+ def enable_quant(self):
491
+ """Enable quantization."""
492
+ self._if_quant = True
493
+
494
+ def load_calib_amax(self, *args, **kwargs):
495
+ """Load amax from calibrator.
496
+
497
+ Updates the amax buffer with value computed by the calibrator, creating it if necessary.
498
+ ``*args`` and ``**kwargs`` are directly passed to ``compute_amax``, except ``"strict"`` in
499
+ ``kwargs``. Refer to ``compute_amax`` for more details.
500
+ """
501
+ assert not self._dynamic, "Dynamic quantization does not need calibration."
502
+
503
+ strict = kwargs.pop("strict", True)
504
+ if getattr(self, "_calibrator", None) is None:
505
+ raise RuntimeError("Calibrator not created.")
506
+ calib_amax = self._calibrator.compute_amax(*args, **kwargs)
507
+ if calib_amax is None:
508
+ err_msg = (
509
+ "Calibrator returned None. This usually happens when calibrator hasn't seen any"
510
+ " tensor."
511
+ )
512
+ if not strict:
513
+ warnings.warn(err_msg)
514
+ warnings.warn("Set amax to NaN!")
515
+ calib_amax = torch.tensor(math.nan)
516
+ else:
517
+ raise RuntimeError(
518
+ err_msg
519
+ + " Passing 'strict=False' to `load_calib_amax()` will ignore the error."
520
+ )
521
+ if not hasattr(self, "_amax"):
522
+ self.register_buffer("_amax", calib_amax.clone().detach())
523
+ else:
524
+ self._amax.data.copy_(calib_amax.clone().detach())
525
+
526
+ def load_calib_bias(self, *args, **kwargs):
527
+ """Load affine bias for quantization."""
528
+ assert not self._dynamic, "Dynamic quantization does not need calibration."
529
+ calib_bias = self.bias_calibrator.compute_bias(*args, **kwargs)
530
+ if calib_bias is None:
531
+ raise RuntimeError(
532
+ "Calibrator returned None. This usually happens when calibrator hasn't seen any tensor."
533
+ )
534
+
535
+ if not hasattr(self, "_bias_value"):
536
+ self.register_buffer("_bias_value", calib_bias.clone().detach())
537
+ else:
538
+ self._bias_value.data.copy_(calib_bias.clone().detach())
539
+
540
+ def _get_amax(self, inputs):
541
+ """Get amax from buffer or compute it dynamically."""
542
+ if hasattr(self, "_amax"):
543
+ amax = self._amax
544
+ else:
545
+ reduce_axis = quant_utils.convert_quantization_axis_to_reduce_axis(inputs, self._axis)
546
+ amax = quant_utils.reduce_amax(inputs, axis=reduce_axis, keepdims=True).detach()
547
+
548
+ amax = amax.detach() if is_torch_export_mode() else amax.data
549
+ return amax
550
+
551
+ def _validate_amax(self, amax):
552
+ # Dynamic control flow is not supported by torch dynamo
553
+ if not is_torch_export_mode() and not torch._dynamo.is_compiling():
554
+ assert torch.all(amax >= 0) and not torch.any(torch.isinf(amax)), (
555
+ f"Got invalid amax: {amax}"
556
+ )
557
+
558
+ def _get_bias(self, inputs):
559
+ """Get bias from buffer or compute it dynamically."""
560
+ if self.bias_calibrator is None:
561
+ return None
562
+
563
+ if self.bias_type == "static":
564
+ bias = self._bias_value
565
+ elif self.bias_type == "dynamic":
566
+ bias = self.bias_calibrator.compute_dynamic_bias(inputs)
567
+ else:
568
+ raise ValueError(f"Unsupported bias type: {self.bias_type}")
569
+ return bias
570
+
571
+ def _is_real_quantize_support(self):
572
+ """Check if real quantization is supported for this quant config."""
573
+ return (
574
+ (self._num_bits == 4 and self._block_sizes)
575
+ or (self._num_bits == (2, 1) and self._block_sizes)
576
+ or self._num_bits in ((4, 3), 8) # Int8
577
+ )
578
+
579
+ def _real_quantize(self, inputs):
580
+ assert self._is_real_quantize_support(), "Real quantization not supported for this format."
581
+
582
+ buffer_to_register = {}
583
+ if self._num_bits == (4, 3):
584
+ # FP8 quantization
585
+ # For per-tensor/per-channel quantization, we might need amax which is synced across all ranks
586
+ outputs, _scale = FP8QTensor.quantize(
587
+ inputs,
588
+ axis=self._axis,
589
+ block_sizes=self._block_sizes,
590
+ scales=self.amax / 448.0 if self.amax is not None else None,
591
+ )
592
+ buffer_to_register["_scale"] = _scale
593
+ elif self._num_bits == 8:
594
+ outputs, _scale = INT8QTensor.quantize(
595
+ inputs, axis=self._axis, block_sizes=self._block_sizes
596
+ )
597
+ buffer_to_register["_scale"] = _scale
598
+ elif self._block_sizes.get("scale_bits", 0) == 8 and self._block_sizes.get(
599
+ "scale_block_sizes", None
600
+ ):
601
+ # NF4 double quantization
602
+ # Return real quantized tensor class and store scales inside the TensorQuantizer
603
+ outputs, scales = NF4QTensor.quantize(
604
+ inputs, self._block_sizes[-1], self._block_sizes["scale_block_sizes"][-1]
605
+ )
606
+
607
+ _scale, _double_scale, _scale_zeros = NF4QTensor.double_quantization(
608
+ scales,
609
+ self._block_sizes["scale_block_sizes"][-1],
610
+ self._block_sizes["scale_bits"],
611
+ )
612
+ buffer_to_register["_scale"] = _scale
613
+ buffer_to_register["_double_scale"] = _double_scale
614
+ buffer_to_register["_scale_zeros"] = _scale_zeros
615
+ elif (
616
+ self._block_sizes.get("scale_bits") == (8, 0)
617
+ and self._block_sizes.get("type") == "dynamic"
618
+ ):
619
+ # MX quantization
620
+ if self._num_bits == (2, 1):
621
+ outputs, scales = MXFP4QTensor.quantize(inputs, self._block_sizes[-1])
622
+ buffer_to_register["_scale"] = scales
623
+ else:
624
+ raise ValueError(
625
+ f"Real quantization for MX {self._num_bits} format is not supported."
626
+ )
627
+ elif self._block_sizes.get("scale_bits") == (4, 3):
628
+ # NVFP4 default quantization
629
+ # Return real quantized tensor and store scales inside TensorQuantizer
630
+ outputs, _weights_scaling_factor, _weights_scaling_factor_2 = NVFP4QTensor.quantize(
631
+ inputs,
632
+ self._block_sizes[-1],
633
+ weights_scaling_factor_2=self.amax.float() / (448.0 * 6.0)
634
+ if self.amax is not None
635
+ else None,
636
+ try_tensorrt=True,
637
+ )
638
+ buffer_to_register["_scale"] = _weights_scaling_factor
639
+ buffer_to_register["_double_scale"] = _weights_scaling_factor_2
640
+ else:
641
+ outputs, _scale = INT4QTensor.quantize(inputs, self._block_sizes[-1])
642
+ buffer_to_register["_scale"] = _scale
643
+ for k, v in buffer_to_register.items():
644
+ self._set_buffer(k, v)
645
+
646
+ # We assume _real_quantize is called when compress the model weights, and we set
647
+ # self._dequantize to True so that future forward call will only do dequantize.
648
+ self._dequantize = True
649
+ return outputs
650
+
651
+ def _fake_quantize(self, inputs):
652
+ """Fake quantization."""
653
+ amax = None
654
+ if not self.is_mx_format:
655
+ amax = self._get_amax(inputs)
656
+ self._validate_amax(amax)
657
+
658
+ if self.block_sizes is not None and self.block_sizes.get("type", "static") == "dynamic":
659
+ # Block quantization, including dynamic and static block quantization
660
+ block_size = self.block_sizes.get(-1, None) or self.block_sizes.get(
661
+ inputs.dim() - 1, None
662
+ )
663
+ assert block_size is not None, "block size for dynamic quantization not found."
664
+
665
+ outputs = dynamic_block_quant(
666
+ inputs,
667
+ block_size,
668
+ amax,
669
+ self._get_bias(inputs),
670
+ self._num_bits,
671
+ self.block_sizes.get("scale_bits", None),
672
+ getattr(self, "_trt_high_precision_dtype", None),
673
+ getattr(self, "_onnx_quantizer_type", None),
674
+ self._pass_through_bwd,
675
+ )
676
+ elif isinstance(self._num_bits, tuple):
677
+ # Float-point quantization, e.g., FP8
678
+ E, M = self._num_bits # noqa: N806
679
+
680
+ outputs = scaled_e4m3(
681
+ inputs,
682
+ amax,
683
+ self._get_bias(inputs),
684
+ E,
685
+ M,
686
+ self._trt_high_precision_dtype,
687
+ self._pass_through_bwd,
688
+ )
689
+
690
+ else:
691
+ # Integer quantization, e.g., INT8
692
+ outputs = fake_tensor_quant(
693
+ inputs,
694
+ amax,
695
+ self._get_bias(inputs),
696
+ self._num_bits,
697
+ self._unsigned,
698
+ self._narrow_range,
699
+ self._trt_high_precision_dtype,
700
+ self._pass_through_bwd,
701
+ self.block_sizes.get(-1) if self.block_sizes else None,
702
+ self.axis[0] if isinstance(self.axis, tuple) else self.axis,
703
+ )
704
+ return outputs
705
+
706
+ def _check_onnx_readiness(self, inputs):
707
+ """Check if quantizer is ready for ONNX export."""
708
+ if not self.block_sizes or self.block_sizes.get("scale_bits", None) != (8, 0):
709
+ assert hasattr(self, "_amax"), (
710
+ "Quantizer has not been calibrated. ONNX export requires the quantizer to be"
711
+ " calibrated. Calibrate and load amax before exporting to ONNX."
712
+ )
713
+
714
+ if self._if_calib:
715
+ warnings.warn(
716
+ "Quantizer is in calibration mode. "
717
+ "Please complete calibration before exporting to ONNX for correct results."
718
+ )
719
+
720
+ amax = self._get_amax(inputs)
721
+
722
+ # We only support scalar amax for E4M3 ONNX export
723
+ if isinstance(self.num_bits, tuple):
724
+ assert amax.numel() == 1, (
725
+ "E4M3 supports ONNX export only for per-tensor quantization."
726
+ " Per-tensor quantization requires scalar amax. "
727
+ f"Received non-scalar amax of shape: {amax.shape}"
728
+ )
729
+
730
+ def _setup_for_blockquant(self, inputs: torch.Tensor):
731
+ # Get reshape sizes and paddings for block-quantization
732
+ def get_axis_quant_params(ax):
733
+ ax = ax if ax in self.block_sizes else ax - inputs.dim()
734
+ bsize = self.block_sizes.get(ax, None)
735
+ padding, ax_slice = None, None
736
+ if bsize is not None and inputs.shape[ax] % bsize != 0:
737
+ padding = (bsize - (inputs.shape[ax] % bsize), 0)
738
+ ax_slice = slice(inputs.shape[ax])
739
+ return bsize, padding, ax_slice
740
+
741
+ def set_quant_params(axis, block_reshape_size, padding, slices, amax_shape=None):
742
+ self._axis = tuple(axis)
743
+ if hasattr(self, "_calibrator"):
744
+ self._calibrator._axis = self._axis
745
+ self._original_shape = inputs.shape
746
+ self._block_reshape_size = torch.Size(block_reshape_size)
747
+ if padding is not None:
748
+ self._padding = tuple(padding)
749
+ self._original_shape = F.pad(inputs, self._padding, "constant", 0).shape
750
+ if slices is not None:
751
+ self._slices = slices
752
+ if amax_shape:
753
+ self._amax_shape_for_export = amax_shape
754
+
755
+ # Reshape size have already been set
756
+ if hasattr(self, "_block_reshape_size"):
757
+ return
758
+
759
+ reshape_size, quantize_axis, paddings, slices = [], [], [], []
760
+
761
+ # special handling for block-quantization along the last axis:
762
+ # flatten the input for faster execution
763
+ if (self.block_sizes.get(inputs.dim() - 1, None) or self.block_sizes.get(-1, None)) and len(
764
+ QuantizerAttributeConfig._get_block_quant_axes_and_sizes(self.block_sizes)
765
+ ) == 1:
766
+ bsize, padding, ax_slice = get_axis_quant_params(inputs.dim() - 1)
767
+ slices = None if ax_slice is None else (*(slice(None),) * (inputs.dim() - 1), ax_slice)
768
+ padding = padding if not padding else tuple(reversed(padding))
769
+ amax_shape_for_export = (*(inputs.shape[:-1]), -1)
770
+ set_quant_params((0,), (-1, bsize), padding, slices, amax_shape_for_export)
771
+ return
772
+
773
+ for ax in range(inputs.dim()):
774
+ bsize, padding, ax_slice = get_axis_quant_params(ax)
775
+ paddings.append(padding)
776
+ slices.append(ax_slice)
777
+ if bsize is not None:
778
+ reshape_size.extend([math.ceil(inputs.shape[ax] / bsize), bsize])
779
+ quantize_axis.extend([True, False])
780
+ else:
781
+ reshape_size.append(inputs.shape[ax])
782
+ quantize_axis.append(True)
783
+
784
+ quant_axis = [i for i in range(len(quantize_axis)) if quantize_axis[i]]
785
+
786
+ slices = (
787
+ None if all(s is None for s in slices) else [s if s else slice(None) for s in slices]
788
+ )
789
+
790
+ if all(p is None for p in paddings):
791
+ paddings = None
792
+ else:
793
+ new_paddings = []
794
+ for padding in paddings:
795
+ if not (new_paddings or padding):
796
+ continue
797
+ new_paddings.extend(padding if padding else (0, 0))
798
+ paddings = tuple(reversed(new_paddings))
799
+
800
+ set_quant_params(quant_axis, reshape_size, paddings, slices)
801
+
802
+ def _process_for_blockquant(self, inputs: torch.Tensor):
803
+ if hasattr(self, "_padding"):
804
+ inputs = F.pad(inputs, self._padding, "constant", 0)
805
+ assert inputs.shape == self._original_shape, (
806
+ f"Input shape has changed from {self._original_shape} to {inputs.shape}."
807
+ " Block-quantization requires a fixed input shape."
808
+ )
809
+ inputs = inputs.reshape(self._block_reshape_size)
810
+ return inputs
811
+
812
+ def _reset_to_original_shape(self, outputs: torch.Tensor):
813
+ outputs = outputs.reshape(self._original_shape)
814
+ if hasattr(self, "_slices"):
815
+ outputs = outputs[self._slices]
816
+ return outputs
817
+
818
+ def _block_sizes_to_axis(self, x: torch.Tensor):
819
+ """Convert block_sizes to axis in per-channel/tensor quantization.
820
+
821
+ For example, for input tensor with shape (B, T, H),
822
+ {"block_sizes": {-1: None, -3: None}} equals to {axis: (-2)}, amax shape: (1, T, 1),
823
+ {"block_sizes": {-1: None, -2: None, -3: None}} equals to {axis: None}, amax shape: (1, T, 1)
824
+ """
825
+ block_sizes = self._block_sizes
826
+ if block_sizes is None:
827
+ return
828
+
829
+ def _check_per_channel_block_sizes(block_sizes):
830
+ # Check per-channel/block quant
831
+ return all(v is None for k, v in block_sizes.items() if isinstance(k, int))
832
+
833
+ if _check_per_channel_block_sizes(block_sizes):
834
+ # Convert block_sizes to axis
835
+ assert self.axis is None, "Axis and block_sizes are both set."
836
+ axis = tuple(k if k >= 0 else k + x.dim() for k in block_sizes if isinstance(k, int))
837
+ self.axis = tuple(i for i in range(x.dim()) if i not in axis) or None
838
+
839
+ # remove block_sizes
840
+ self._block_sizes = None
841
+
842
+ def export_amax(self) -> torch.Tensor | None:
843
+ """Export correctly formatted/shaped amax."""
844
+ if self.block_sizes is not None and self.block_sizes.get("type", None) == "dynamic":
845
+ return self.amax
846
+
847
+ if self.amax is None:
848
+ return None
849
+
850
+ if not hasattr(self, "_amax_shape_for_export"):
851
+ amax = self.amax
852
+ else:
853
+ amax = self.amax.reshape(self._amax_shape_for_export)
854
+ amax[amax == 0] = self.maxbound
855
+ amax = torch.nan_to_num(amax, nan=self.maxbound)
856
+ clamp_min, clamp_max = torch.finfo(amax.dtype).tiny, torch.finfo(amax.dtype).max
857
+ amax = amax.clamp(min=clamp_min, max=clamp_max)
858
+
859
+ self._validate_amax(amax)
860
+
861
+ if self.block_sizes is None:
862
+ # tensorrt_llm assumes the scaling_factor dim >= 1 for per-tensor.
863
+ if self.axis is None:
864
+ amax = amax.unsqueeze(0)
865
+
866
+ # If single-axis quantization, squeeze amax
867
+ elif isinstance(self.axis, int) or (
868
+ isinstance(self.axis, (list, tuple)) and len(self.axis) == 1
869
+ ):
870
+ amax = amax.squeeze()
871
+
872
+ return amax
873
+
874
+ def forward(self, inputs):
875
+ """Apply tensor_quant function to inputs.
876
+
877
+ Args:
878
+ inputs: A Tensor of type float32/float16/bfloat16.
879
+
880
+ Returns:
881
+ outputs: A Tensor of type output_dtype
882
+ """
883
+ if DTensor is not None and isinstance(inputs, DTensor):
884
+ # TensorQuantizer only handles regular non-DTensor inputs
885
+ device_mesh, placements = inputs.device_mesh, inputs.placements
886
+ outputs = self.forward(inputs.to_local())
887
+ return DTensor.from_local(outputs, device_mesh, placements)
888
+
889
+ if isinstance(inputs, (BaseQuantizedTensor, QTensorWrapper)):
890
+ assert self._dequantize, "No dequantization stats in the tensor quantizer."
891
+ return self.dequantize(inputs)
892
+
893
+ # Early return if nothing is collected during the forward (e.g. MoE)
894
+ if inputs.numel() == 0:
895
+ return inputs
896
+
897
+ # Activation scaling for smoothquant
898
+ if self.pre_quant_scale is not None:
899
+ inputs = inputs * self.pre_quant_scale
900
+
901
+ # Rotating the input
902
+ if self._rotate:
903
+ inputs = normalized_hadamard_transform(inputs)
904
+
905
+ if self._disabled:
906
+ # if quantizer is disabled, we still need to track the input dtype for saving the model
907
+ # TODO: This is a temporary solution and needs to be removed once megatron supports
908
+ # non-homogeneous layers
909
+ self._input_dtype = inputs.dtype if hasattr(inputs, "dtype") else None
910
+ return inputs
911
+
912
+ if (
913
+ not is_torch_export_mode()
914
+ and not torch._dynamo.is_compiling()
915
+ and GLOBALS.in_onnx_export
916
+ ):
917
+ # GLOBALS could break TorchDynamo for some Pytorch versions (i.e., 2.3.0)
918
+ self._check_onnx_readiness(inputs)
919
+
920
+ if self.block_sizes is not None and self._fake_quant:
921
+ # To support the new block_sizes representation for per-channel quantization,
922
+ # convert the dim dict in block_sizes to axis.
923
+ # The axis attribute is still preserved for backward compatibility.
924
+ self._block_sizes_to_axis(inputs)
925
+
926
+ if (
927
+ self.block_sizes is not None
928
+ and self.block_sizes.get("type", None) != "dynamic"
929
+ and self._fake_quant
930
+ ):
931
+ # Tensor reshaping is required for static block quantization
932
+ # Tensor shapes are handled separately by the quantization kernels for dynamic block quantization
933
+ self._setup_for_blockquant(inputs)
934
+ inputs = self._process_for_blockquant(inputs)
935
+
936
+ outputs = inputs
937
+
938
+ block_size = None
939
+ if self._if_calib and not self._dynamic:
940
+ if self._calibrator is None:
941
+ raise RuntimeError("Calibrator was not created.")
942
+ # Shape is only known when it sees the first tensor
943
+ if self.block_sizes is not None and self.block_sizes.get("type", None) == "dynamic":
944
+ block_size = self.block_sizes.get(-1, None) or self.block_sizes.get(
945
+ inputs.dim() - 1, None
946
+ )
947
+ assert block_size is not None, "block size for dynamic quantization not found."
948
+
949
+ # Collect calibration data for bias
950
+ self.collect(inputs)
951
+
952
+ if self._if_quant:
953
+ # Check if the input tensor is contiguous
954
+ # Non-contiguous tensors will generate incorrect FP4 quantization results
955
+ if hasattr(inputs, "is_contiguous") and not inputs.is_contiguous():
956
+ inputs.data = inputs.data.contiguous()
957
+ if self.fake_quant:
958
+ outputs = self._fake_quantize(inputs)
959
+ elif not self._dequantize:
960
+ outputs = self._real_quantize(inputs)
961
+ else:
962
+ raise ValueError(
963
+ "self._dequantize is True and self.fake_quant is False. "
964
+ "This case should have been handled."
965
+ )
966
+
967
+ if (
968
+ self.block_sizes is not None
969
+ and self.block_sizes.get("type", None) != "dynamic"
970
+ and self._fake_quant
971
+ ):
972
+ outputs = self._reset_to_original_shape(outputs)
973
+
974
+ return outputs
975
+
976
+ def _short_amax(self, fmt=".4f"):
977
+ """Short description of amax.
978
+
979
+ Returns:
980
+ 'dynamic': if _amax is not registered
981
+ 'amax': if _amax is per-tensor
982
+ '[min, max](size)': if _amax is per-channel
983
+ """
984
+ if self.is_mx_format:
985
+ return "None"
986
+ if not hasattr(self, "_amax"):
987
+ return "dynamic"
988
+ if self._amax is None:
989
+ return "None"
990
+ if self._amax.is_meta:
991
+ return "meta"
992
+ if self._amax.numel() == 1:
993
+ return f"{self._amax.item():{fmt}}"
994
+ return (
995
+ f"[{self._amax.min().item():{fmt}},"
996
+ f" {self._amax.max().item():{fmt}}]({self._amax.numel()})"
997
+ )
998
+
999
+ def extra_repr(self):
1000
+ """Set the extra information about this module."""
1001
+ if self._disabled:
1002
+ return "disabled"
1003
+ s = f"{'unsigned ' if self._unsigned else ''}{self._num_bits} bit"
1004
+ s += " narrow" if (self._narrow_range) else ""
1005
+ s += " fake" if (self._fake_quant) else ""
1006
+ if self.block_sizes is not None:
1007
+ s += f" block_sizes={self._block_sizes},"
1008
+ else:
1009
+ s += f" axis={self._axis}" if self._axis is not None else " per-tensor"
1010
+ s += f" amax={self._short_amax()}"
1011
+ s += " pre_quant_scale" if self.pre_quant_scale is not None else ""
1012
+ s += " svdquant" if self.svdquant_lora_a is not None else ""
1013
+ s += " rotated" if self._rotate else ""
1014
+ s += (
1015
+ f" calibrator={self._calibrator.__class__.__name__}"
1016
+ if (self._calibrator is not None)
1017
+ else ""
1018
+ )
1019
+ if self._bias:
1020
+ s += f" bias={self._bias}"
1021
+
1022
+ s += " quant" if (self._if_quant) else ""
1023
+ s += " calib" if (self._if_calib) else ""
1024
+ return s
1025
+
1026
+ @property
1027
+ def mopt_ckpt_versn(self):
1028
+ """Version of the checkpoint if it is restored from a checkpoint."""
1029
+ return getattr(self, "_mopt_ckpt_versn", None)
1030
+
1031
+ @mopt_ckpt_versn.setter
1032
+ def mopt_ckpt_versn(self, version: str):
1033
+ self._mopt_ckpt_versn = str(version)
1034
+
1035
+ def _load_from_state_dict(self, state_dict, prefix, *args, **kwargs):
1036
+ """Special handling for loading older checkpoints.
1037
+
1038
+ This implementation is for backward compatibility and can be deprecated in future versions.
1039
+
1040
+ Args:
1041
+ state_dict: A dict containing the state of the top level module
1042
+ prefix: A string that prefixes all of this modules state in state_dict, e.g. 'model.conv1.'
1043
+ """
1044
+ if self.mopt_ckpt_versn is None or Version(self.mopt_ckpt_versn) >= Version("0.29"):
1045
+ # Warnings below are raised if users use partial state dictionary intentionally (eg:- HF ckpts)
1046
+ # For ModelOpt >= 0.29, the buffers will be correctly created, So lets skip the warnings
1047
+ return super()._load_from_state_dict(state_dict, prefix, *args, **kwargs)
1048
+
1049
+ _attrs = ["_amax", "_pre_quant_scale", "_svdquant_lora_a", "_svdquant_lora_b"]
1050
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
1051
+
1052
+ for attr in _attrs:
1053
+ has_dst = attr in self._buffers
1054
+ has_src = prefix + attr in state_dict
1055
+
1056
+ if not has_src and has_dst:
1057
+ warnings.warn(f"{prefix[:-1]}: No {attr} in state_dict.")
1058
+ elif has_src and not has_dst:
1059
+ warnings.warn(
1060
+ f"{prefix[:-1]}: No '{attr}' buffer to load {attr} into."
1061
+ f" '{attr}` is created as a buffer for now. Please move the model to the correct device and "
1062
+ "dtype after this by calling `model.to(device, dtype)`."
1063
+ )
1064
+ self.register_buffer(attr, state_dict[prefix + attr].clone().detach().to(device))
1065
+
1066
+ super()._load_from_state_dict(state_dict, prefix, *args, **kwargs)
1067
+
1068
+ def _get_properties_for_modelopt_state(self):
1069
+ return (
1070
+ self.__dict__.keys()
1071
+ - nn.Module().__dict__.keys()
1072
+ - self._skip_properties_for_save_restore
1073
+ )
1074
+
1075
+ def _get_pytorch_state_metadata(self):
1076
+ """Get Pytorch state metadata.
1077
+
1078
+ Current we only store the shape of the state_dict values.
1079
+ """
1080
+ metadata = {"params": {}, "buffers": {}}
1081
+ for k, v in self._parameters.items():
1082
+ metadata["params"][k] = {"shape": v.shape, "dtype": v.dtype}
1083
+ for k, v in self._buffers.items():
1084
+ if k in self._non_persistent_buffers_set:
1085
+ continue
1086
+ metadata["buffers"][k] = {"shape": v.shape, "dtype": v.dtype}
1087
+ return metadata
1088
+
1089
+ def _del_pytorch_state(self):
1090
+ # Lets delete the parameters and buffers
1091
+ self._parameters.clear()
1092
+ self._buffers.clear()
1093
+ self._non_persistent_buffers_set.clear()
1094
+
1095
+ def _reset_pytorch_state_from_metadata(self, metadata: dict[str, Any]):
1096
+ # Lets delete existing parameters and buffers and create fresh ones
1097
+ self._del_pytorch_state()
1098
+ for k, v in metadata.get("params", {}).items():
1099
+ dtype = v.get("dtype", None)
1100
+ self.register_parameter(k, nn.Parameter(torch.empty(v["shape"], dtype=dtype)))
1101
+ for k, v in metadata.get("buffers", {}).items():
1102
+ dtype = v.get("dtype", None)
1103
+ self.register_buffer(k, torch.empty(v["shape"], dtype=dtype))
1104
+
1105
+ def get_modelopt_state(self, properties_only: bool = False) -> dict[str, Any]:
1106
+ """Get meta state to be saved in checkpoint.
1107
+
1108
+ If `properties_only` is True, only the quantizer properties such as `num_bits`, `axis` etc are included.
1109
+ For restoring the quantizer fully including the parameters and buffers, use `properties_only=False`.
1110
+ """
1111
+ modelopt_state = {}
1112
+ for k in self._get_properties_for_modelopt_state():
1113
+ modelopt_state[k] = getattr(self, k)
1114
+
1115
+ if properties_only:
1116
+ return modelopt_state
1117
+
1118
+ modelopt_state["_pytorch_state_metadata"] = self._get_pytorch_state_metadata()
1119
+
1120
+ return modelopt_state
1121
+
1122
+ def set_from_modelopt_state(self, modelopt_state, properties_only: bool = False):
1123
+ """Set meta state from checkpoint.
1124
+
1125
+ If `properties_only` is True, only the quantizer properties such as `num_bits`, `axis` etc are included.
1126
+ For restoring the quantizer fully including the parameters and buffers, use `properties_only=False`.
1127
+ """
1128
+ # Set all properties except the skip properties; this is done for backward compatibility
1129
+ for key in modelopt_state.keys() - self._skip_properties_for_save_restore:
1130
+ setattr(self, key, modelopt_state[key])
1131
+
1132
+ # Set the calibrator properties
1133
+ # TODO: This might not be sufficient for the custom calibrators - however there is no use-case for it yet
1134
+ for key in ["_num_bits", "_axis", "_unsigned"]:
1135
+ setattr(self._calibrator, key, getattr(self, key))
1136
+
1137
+ if not properties_only:
1138
+ self._reset_pytorch_state_from_metadata(
1139
+ modelopt_state.get("_pytorch_state_metadata", {})
1140
+ )
1141
+
1142
+ def sync_amax_across_distributed_group(self, parallel_group: DistributedProcessGroup):
1143
+ """Synchronize the amax across all ranks in the given group."""
1144
+ if parallel_group.is_initialized() and getattr(self, "_amax", None) is not None:
1145
+ try:
1146
+ dist.all_reduce(self._amax, op=dist.ReduceOp.MAX, group=parallel_group.group)
1147
+ except RuntimeError as e:
1148
+ # This error happens if the distributed backend is using GPU and
1149
+ # the tensor is not on GPU (or vice versa).
1150
+ warnings.warn(
1151
+ f"Failed to synchronize amax: {e}, probably because the tensor is on a device which is not"
1152
+ "supported by the current distributed backend. This warning can be ignored"
1153
+ "if happening during modelopt restore."
1154
+ )
1155
+
1156
+ @contextlib.contextmanager
1157
+ def disable_pre_quant_scale(self):
1158
+ """Context manager to turn off pre_quant_scale inside this quantizer."""
1159
+ was_enabled = self._enable_pre_quant_scale
1160
+ self._enable_pre_quant_scale = False
1161
+ try:
1162
+ yield
1163
+ finally:
1164
+ self._enable_pre_quant_scale = was_enabled
1165
+
1166
+ def collect(self, inputs) -> None:
1167
+ """Collect calibration data."""
1168
+ if not self._if_calib or self._dynamic:
1169
+ return
1170
+
1171
+ # Collect bias data if bias calibration is enabled
1172
+ if self.bias_calibrator is not None and self.bias_type == "static":
1173
+ self.bias_calibrator.collect(inputs)
1174
+ inputs = inputs - self.bias_calibrator.compute_bias()
1175
+
1176
+ self._calibrator.collect(inputs)
1177
+
1178
+ def _set_buffer(self, key, value):
1179
+ if hasattr(self, key):
1180
+ setattr(self, key, value)
1181
+ else:
1182
+ self.register_buffer(key, value)
1183
+
1184
+
1185
+ class SequentialQuantizer(nn.Sequential):
1186
+ """A sequential container for :class:`TensorQuantizer` modules.
1187
+
1188
+ This modules is used to quantize a tensor in multiple formats sequentially. It takes as input
1189
+ :class:`TensorQuantizer` modules and containerize them similar to :class:`torch.nn.Sequential`.
1190
+
1191
+ We delegate certain properties and methods to all contained quantizers.
1192
+ In the case of conflicts, the first quantizer's property or method takes priority.
1193
+
1194
+ `SequentialQuantizer` is useful in cases like INT4 weights, FP8 activations where weight quantization is not the
1195
+ same as the gemm quantization. It allows for applying multiple quantization formats to the same tensor in sequence.
1196
+
1197
+ Use `SequentialQuantizer` methods in lower level implementations for better code organization and readability.
1198
+
1199
+ Args:
1200
+ quantizers (TensorQuantizer): :class:`TensorQuantizer` modules to be added to the container.
1201
+
1202
+ """
1203
+
1204
+ _delegated_properties = ["fake_quant", "is_enabled"]
1205
+ _delegated_methods = [
1206
+ "reset_amax",
1207
+ "disable",
1208
+ "enable",
1209
+ "load_calib_amax",
1210
+ "load_calib_bias",
1211
+ ]
1212
+
1213
+ def __init__(self, *quantizers: TensorQuantizer):
1214
+ """Initialize SequentialQuantizer module."""
1215
+ super().__init__(*quantizers)
1216
+ assert all(isinstance(q, TensorQuantizer) for q in self), (
1217
+ "All quantizers must be a TensorQuantizer."
1218
+ )
1219
+
1220
+ def __getattr__(self, name):
1221
+ """Delegate properties and methods to all contained quantizers."""
1222
+ if name in self._delegated_properties:
1223
+ # Return the property of the first quantizer
1224
+ return getattr(self[0], name)
1225
+
1226
+ if name in self._delegated_methods:
1227
+
1228
+ def method_wrapper(*args, **kwargs):
1229
+ outputs = getattr(self[0], name)(*args, **kwargs)
1230
+ for quantizer in self[1:]:
1231
+ outputs = getattr(quantizer, name)(*args, **kwargs)
1232
+ return outputs
1233
+
1234
+ return method_wrapper
1235
+
1236
+ # Defer to super class for attributes not handled here
1237
+ return super().__getattr__(name)
1238
+
1239
+ def __setattr__(self, name, value):
1240
+ if name in self._delegated_properties:
1241
+ for quantizer in self:
1242
+ setattr(quantizer, name, value)
1243
+ else:
1244
+ super().__setattr__(name, value)
1245
+
1246
+ def get_modelopt_state(self) -> dict[str, Any]:
1247
+ """Get meta state to be saved in checkpoint."""
1248
+ return {"num_quantizers": len(self), "is_sequential_quantizer": True}
1249
+
1250
+ def set_from_attribute_config(
1251
+ self,
1252
+ attributes: list[dict[str, Any] | QuantizerAttributeConfig]
1253
+ | dict[str, Any]
1254
+ | QuantizerAttributeConfig,
1255
+ ):
1256
+ """Set the attributes of contained quantizers from a list of attribute_dicts."""
1257
+ if not isinstance(attributes, (list, tuple)):
1258
+ assert isinstance(attributes, (dict, QuantizerAttributeConfig)), (
1259
+ "attributes must be a list or a dict."
1260
+ )
1261
+ attributes = [attributes] * len(self)
1262
+
1263
+ for attribute, quantizer in zip(attributes, self):
1264
+ quantizer.set_from_attribute_config(attribute)
1265
+
1266
+ @staticmethod
1267
+ @contextlib.contextmanager
1268
+ def convert_to_single_quantizer(model, indx: int = 0):
1269
+ """Replace instances of :class:`SequentialQuantizer` in the model with single `TensorQuantizer` quantizer.
1270
+
1271
+ The quantizer indexed by ``indx`` from the sequential quantizer is used to replace it.
1272
+ This method is useful for individually calibrating the quantizers in a sequential quantizer.
1273
+ """
1274
+ original_sequential_quantizers: dict[nn.Module, list] = {}
1275
+ for name, module in list(model.named_modules()):
1276
+ if isinstance(module, SequentialQuantizer):
1277
+ assert len(module) > indx
1278
+ parent_module = model.get_submodule(name.rpartition(".")[0])
1279
+ if parent_module not in original_sequential_quantizers:
1280
+ original_sequential_quantizers[parent_module] = []
1281
+ original_sequential_quantizers[parent_module].append(
1282
+ (name.rpartition(".")[-1], module)
1283
+ )
1284
+ setattr(parent_module, name.rpartition(".")[-1], module[indx])
1285
+
1286
+ yield
1287
+
1288
+ for (
1289
+ parent_module,
1290
+ sequential_quantizers_list,
1291
+ ) in original_sequential_quantizers.items():
1292
+ for name, sequential_quantizer in sequential_quantizers_list:
1293
+ setattr(parent_module, name, sequential_quantizer)