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,1479 @@
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
+ """Provides ONNX graph related utils for QDQ placement."""
17
+
18
+ import re
19
+ from collections import defaultdict
20
+
21
+ import numpy as np
22
+ import onnx
23
+ import onnx_graphsurgeon as gs
24
+ from onnx_graphsurgeon.ir.graph import Graph
25
+ from onnx_graphsurgeon.ir.node import Node
26
+ from onnx_graphsurgeon.ir.tensor import Constant, Tensor, Variable
27
+ from onnxruntime.quantization.calibrate import CalibrationDataReader
28
+ from onnxruntime.tools.symbolic_shape_infer import SymbolicShapeInference
29
+
30
+ from modelopt.onnx.logging_config import logger
31
+ from modelopt.onnx.op_types import is_copy_op, is_linear_op
32
+ from modelopt.onnx.quantization.ort_utils import create_inference_session
33
+ from modelopt.onnx.utils import (
34
+ find_lowest_common_ancestor,
35
+ get_child_nodes,
36
+ get_parent_nodes,
37
+ parse_shapes_spec,
38
+ save_onnx,
39
+ )
40
+
41
+
42
+ def is_const_input(tensor: Tensor) -> bool:
43
+ """Returns whether the given tensor is an initializer or produced by const-foldable nodes."""
44
+ if isinstance(tensor, Constant):
45
+ return True
46
+
47
+ # Tensor is a graph input variable
48
+ if len(tensor.inputs) == 0:
49
+ return False
50
+
51
+ producer_node = tensor.inputs[0] # Generally tensors has single producer
52
+ if producer_node.op in ["Constant", "Identity"]:
53
+ return True
54
+
55
+ # Second axes input to Squeeze/Unsqueeze is a constant, we need to check the first input
56
+ if producer_node.op in ["Squeeze", "Unsqueeze"] and is_const_input(producer_node.inputs[0]):
57
+ return True
58
+
59
+ # Const -> Clip -> Exp -> Mul pattern matching for swin_v2
60
+ if producer_node.op == "Exp":
61
+ clip_node = producer_node.i()
62
+ if clip_node.op == "Clip" and has_const_input(clip_node):
63
+ return True
64
+
65
+ return False
66
+
67
+
68
+ def has_const_input(node: Node) -> bool:
69
+ """Returns whether the given node has any constant input."""
70
+ return any(is_const_input(tensor) for tensor in node.inputs)
71
+
72
+
73
+ def has_path_type(
74
+ node: Node,
75
+ graph: Graph,
76
+ path_type: list[str],
77
+ is_forward: bool,
78
+ wild_card_types: list[str] = [],
79
+ path_nodes: list[Node] = [],
80
+ ) -> bool:
81
+ """Checks if the given node is start/end of a given forward/backward path type.
82
+
83
+ Note, Path can be forward or backward wrt a node depending on the next level nodes.
84
+ Additionally, this method can work with optional nodes and collect the traversed path.
85
+
86
+ Args:
87
+ node: Start node of the path.
88
+ graph: ONNX model graph.
89
+ path_type: Path types to match from the given node.
90
+ is_forward: Whether to match forward or backward path.
91
+ wild_card_types: Wild card types, these type of nodes are skipped and not matched with the path_type.
92
+ path_nodes: Accumulated nodes in the matched path.
93
+
94
+ Returns:
95
+ Bool, whether the given node is start/end of the given forward/backward path type.
96
+ """
97
+ optional_path_types = ["BiasAdd", "ConstMul"]
98
+ if not path_type:
99
+ # All types matched
100
+ return True
101
+
102
+ # Current node type and special type conversion for optional BiasAdd and ConstMul
103
+ # Note, matching path with Add/Mul type nodes with const input will fail
104
+ node_type = node.op
105
+ if node_type == "Add" and has_const_input(node):
106
+ node_type = "BiasAdd"
107
+ elif node_type == "Mul" and has_const_input(node):
108
+ node_type = "ConstMul"
109
+
110
+ # Check if current non-wild node type does not match the expected path type
111
+ # And if path type is not optional (ex. BiasAdd)
112
+ is_match = (node_type == path_type[0]) or (node.op == path_type[0])
113
+ is_wild_match = node_type in wild_card_types
114
+ if not is_match and not is_wild_match and (path_type[0] not in optional_path_types):
115
+ return False
116
+
117
+ # Add current node name in the path
118
+ if is_match:
119
+ path_nodes.append(node)
120
+
121
+ # If current node type matches the expected path type or path type is optional (ex. BiasAdd), we have a type match
122
+ # Update the remaining path types to match
123
+ next_path_type = path_type[:]
124
+
125
+ # Non-repeatable optional types should be consumed
126
+ if is_match or (path_type[0] in ["BiasAdd", "ConstMul"]):
127
+ next_path_type = path_type[1:]
128
+
129
+ # If current node is not wild card and didn't match, go ahead and match with the
130
+ # remaining path types starting with the current node
131
+ if not is_match and not is_wild_match:
132
+ assert path_type[0] in optional_path_types
133
+ return has_path_type(
134
+ node,
135
+ graph,
136
+ next_path_type,
137
+ is_forward,
138
+ wild_card_types,
139
+ path_nodes,
140
+ )
141
+
142
+ next_level_nodes = get_child_nodes(node) if is_forward else get_parent_nodes(node)
143
+
144
+ # Check if any child (forward path) or parent (backward path) can match the remaining path types
145
+ for next_node in next_level_nodes:
146
+ sub_path = []
147
+ if has_path_type(next_node, graph, next_path_type, is_forward, wild_card_types, sub_path):
148
+ path_nodes.extend(sub_path)
149
+ return True
150
+
151
+ # Path type matches if there is no remaining types to match
152
+ return not next_path_type
153
+
154
+
155
+ def get_fusible_backbone(node: Node, graph: Graph) -> Node | None:
156
+ """Returns the linear backbone node for a given node if it matches the pattern.
157
+
158
+ TensorRT fuses convolution with BN, Relu etc. when in some specific pattern.
159
+ This rule tries to match some of those patterns.
160
+ Note. BiasAdd and ConstMul are optional in path types.
161
+
162
+ Args:
163
+ node: Start node of the pattern.
164
+ graph: ONNX model graph.
165
+
166
+ Returns:
167
+ Backbone node of the given node, None if not found.
168
+ """
169
+
170
+ def _get_backbone(root: Node):
171
+ if root.op in ["Conv", "ConvTranspose"]:
172
+ return root
173
+
174
+ for tensor in root.inputs:
175
+ if not isinstance(tensor, Constant):
176
+ parent_node = tensor.inputs[0]
177
+ bb = _get_backbone(parent_node)
178
+ if bb:
179
+ return bb
180
+
181
+ fusible_linear_path_types = []
182
+ for conv_type in ["Conv", "ConvTranspose"]:
183
+ fusible_linear_path_types += [
184
+ ["BiasAdd", "ConstMul", conv_type],
185
+ ["Relu", "BiasAdd", "ConstMul", conv_type],
186
+ ["BatchNormalization", "BiasAdd", conv_type],
187
+ ["Relu", "BatchNormalization", "BiasAdd", conv_type],
188
+ ["MaxPool", "Relu", "BatchNormalization", "BiasAdd", conv_type],
189
+ ]
190
+ for idx, path_type in enumerate(fusible_linear_path_types):
191
+ if has_path_type(node, graph, path_type, is_forward=False, wild_card_types=[]):
192
+ return _get_backbone(node)
193
+
194
+ return None
195
+
196
+
197
+ def get_tensor_from_name(graph: onnx.GraphProto, tensor_name: str) -> onnx.ValueInfoProto | None:
198
+ """Returns a ValueInfoProto given a tensor name.
199
+
200
+ Args:
201
+ graph: ONNX model graph
202
+ tensor_name: String with tensor name.
203
+
204
+ Returns:
205
+ onnx.ValueInfoProto: actual graph tensor.
206
+ """
207
+ # Search in inputs
208
+ vi = next((vi for vi in graph.input if vi.name == tensor_name), None)
209
+ # If not found, search in outputs
210
+ if vi is None:
211
+ vi = next((vi for vi in graph.output if vi.name == tensor_name), None)
212
+ # If not found, search in value_info (intermediate tensors)
213
+ if vi is None:
214
+ vi = next((vi for vi in graph.value_info if vi.name == tensor_name), None)
215
+ return vi
216
+
217
+
218
+ def get_tensor_producer_nodes(
219
+ graph: onnx.GraphProto,
220
+ ) -> dict[str, onnx.NodeProto]:
221
+ """Returns a dictionary of tensor name and their producer node object mapping.
222
+
223
+ Note. we create a special Root type node as external inputs producer for ease of implementation.
224
+
225
+ Args:
226
+ graph: ONNX model graph.
227
+
228
+ Returns:
229
+ Dictionary, key is tensor name and value is their producer node object
230
+ """
231
+ # Create a dictionary to store tensor producer nodes
232
+ tensor_producers = defaultdict(None)
233
+
234
+ # Special Root type producer node
235
+ root_node = onnx.helper.make_node(
236
+ op_type="Root",
237
+ inputs=[],
238
+ outputs=[i.name for i in graph.input],
239
+ name="root_0",
240
+ )
241
+
242
+ input_names = [graph_input.name for graph_input in graph.input]
243
+ initializer_names = [initializer.name for initializer in graph.initializer]
244
+ external_input_names = list(np.setdiff1d(input_names, initializer_names))
245
+
246
+ # Note. We are marking external inputs as non-constant by adding a parent,
247
+ # so that we can quantize the first node of the graph if appropriate
248
+ for graph_input in external_input_names:
249
+ tensor_producers[graph_input] = root_node
250
+
251
+ # Traverse the graph to find producer nodes for each tensor
252
+ for node in graph.node:
253
+ for output_name in node.output:
254
+ tensor_producers[output_name] = node
255
+
256
+ return tensor_producers
257
+
258
+
259
+ def get_tensor_consumer_nodes(
260
+ graph: onnx.GraphProto,
261
+ ) -> dict[str, list[onnx.NodeProto]]:
262
+ """Returns a dictionary of tensor name and their consumer node object mapping.
263
+
264
+ Args:
265
+ graph: ONNX model graph.
266
+
267
+ Returns:
268
+ Dictionary, key is tensor name and value is their consumer node object
269
+ """
270
+ # Create a dictionary to store tensor consumer nodes
271
+ tensor_consumers = defaultdict(list)
272
+
273
+ # Traverse the graph to find consumer nodes for each tensor
274
+ for node in graph.node:
275
+ for input_name in node.input:
276
+ tensor_consumers[input_name].append(node)
277
+
278
+ return tensor_consumers
279
+
280
+
281
+ def filter_quantizable_kgen_heads(
282
+ cask_fusible_partitions: list[list[Node]],
283
+ kgen_partitions: list[list[Node]],
284
+ quantizable_op_types: list[str],
285
+ graph: Graph,
286
+ ) -> tuple[list[Node], list[tuple[Node, Node, str]]]:
287
+ """Returns the list of kgen head names if it follows a CASK partition."""
288
+ cask_partition_nodes = set()
289
+ for partition in cask_fusible_partitions:
290
+ cask_partition_nodes.update([node.name for node in partition])
291
+
292
+ cask_partition_heads = [partition[0] for partition in cask_fusible_partitions]
293
+
294
+ def _is_following_cask_partition(node: Node):
295
+ # Checking if cask fusible partition can be reached backward
296
+ # ignoring the copy ops
297
+ if node.name in cask_partition_nodes:
298
+ return True
299
+
300
+ if not is_copy_op(node.op):
301
+ return False
302
+
303
+ parent_nodes = get_parent_nodes(node)
304
+ if len(parent_nodes) == 0:
305
+ return False
306
+
307
+ return all(_is_following_cask_partition(parent) for parent in parent_nodes)
308
+
309
+ def _is_mha_epilogue_pattern(node: Node, graph: Graph):
310
+ if head_node.op != "Add":
311
+ return False
312
+
313
+ # Below are valid patterns:
314
+ # (1)
315
+ # Add -> Softmax -> MatMul
316
+ #
317
+ # (2)
318
+ # Add -> Flatten -> Softmax -> Reshape -> MatMul
319
+ # \----------Shape-----/
320
+ #
321
+ mha_epilogue_path = ["Softmax", "MatMul"]
322
+ wild_card_types = ["Flatten", "Reshape"]
323
+ add_children = get_child_nodes(node)
324
+
325
+ for child in add_children:
326
+ if has_path_type(
327
+ child,
328
+ graph,
329
+ mha_epilogue_path,
330
+ is_forward=True,
331
+ wild_card_types=wild_card_types,
332
+ ):
333
+ return True
334
+
335
+ return False
336
+
337
+ def _has_other_quantizable_consumer(
338
+ tensor: Tensor, quantizable_kgen_heads: list[Node], head_name: str
339
+ ):
340
+ # Note. this is kinda approximate analysis,
341
+ # all quantizable kgen heads may haven't got discovered yet
342
+ quantizable_ops = [node.name for node in cask_partition_heads + quantizable_kgen_heads]
343
+
344
+ # Look for other quantizable consumer than the current kgen head
345
+ if head_name in quantizable_ops:
346
+ quantizable_ops.remove(head_name)
347
+
348
+ return any(consumer.name in quantizable_ops for consumer in tensor.outputs)
349
+
350
+ quantizable_kgen_heads = []
351
+ no_quantize_inputs = [] # list of tuple [(src_node_name, dst_node_name, input_name), ...]
352
+ output_quantization_candidates = [
353
+ "AveragePool",
354
+ "BatchNormalization",
355
+ "GlobalAveragePool",
356
+ "MaxPool",
357
+ "Mul", # Example: VoVNet
358
+ ]
359
+
360
+ for partition in kgen_partitions:
361
+ head_node = partition[0]
362
+ # Check if partition head is of default quantizable type
363
+ if head_node.op not in quantizable_op_types:
364
+ continue
365
+
366
+ # If the node has cost input, do not quantize
367
+ if has_const_input(head_node):
368
+ continue
369
+
370
+ head_parents = get_parent_nodes(head_node)
371
+ no_quantize_inputs_of_head = []
372
+ has_quantizable_input = False
373
+
374
+ # Check each of the parent (input producer for partition head)
375
+ # or predecessor nodes and see if output quantization is needed for them
376
+ # and decide which input of kgen head needs quantization
377
+ for parent in head_parents:
378
+ # If the head is consuming output of any quantizable op, then it is quantizable
379
+ if _is_following_cask_partition(parent) or parent.op in output_quantization_candidates:
380
+ # The mask add of MHA should not be quantized
381
+ if _is_mha_epilogue_pattern(head_node, graph):
382
+ no_quantize_inputs_of_head.append(
383
+ (parent, partition[0], parent.outputs[0].name)
384
+ )
385
+ else:
386
+ quantizable_kgen_heads.append(partition[0])
387
+ has_quantizable_input = True
388
+ # If the input from the current parent has no other quantizable consumer, do not quantize that input
389
+ elif not _has_other_quantizable_consumer(
390
+ parent.outputs[0], quantizable_kgen_heads, head_node.name
391
+ ):
392
+ no_quantize_inputs_of_head.append((parent, partition[0], parent.outputs[0].name))
393
+
394
+ # If at least one input of Add is quantizable, collect if there is any non-quantizable inputs
395
+ if head_node.op == "Add" and has_quantizable_input:
396
+ no_quantize_inputs.extend(no_quantize_inputs_of_head)
397
+
398
+ return quantizable_kgen_heads, no_quantize_inputs
399
+
400
+
401
+ def classify_partition_nodes(
402
+ partitions: list[list[Node]],
403
+ ) -> tuple[list[Node], list[Node], list[tuple[Node, Node, str]]]:
404
+ """We should partially quantize the partition nodes with inputs outside of the partition.
405
+
406
+ Args:
407
+ partitions: Partitions created by modelopt ptq algo.
408
+
409
+ Returns:
410
+ List of non-quantizable nodes.
411
+ List of quantizable nodes.
412
+ List of partially-quantizable inputs with non-quantizable input info as (src, dst, input_name)
413
+ """
414
+ non_quantizable_partition_nodes = [] # list of Node [node1, ...]
415
+ quantizable_partition_nodes = [] # list of Node [node1, ...]
416
+ no_quantize_inputs = [] # list of tuple [(src_node, dst_node, input_name), ...]
417
+
418
+ for partition in partitions:
419
+ partition_root_type = partition[0].op
420
+ assert is_linear_op(partition_root_type)
421
+
422
+ # Collect tensor names produced by partition nodes
423
+ partition_node_outputs = []
424
+ for node in partition:
425
+ partition_node_outputs.extend([output.name for output in node.outputs])
426
+
427
+ for node in partition:
428
+ has_external_inputs = False
429
+ internal_inputs = [] # Keeps (producer, consumer, tensor)
430
+ for tensor in node.inputs:
431
+ if is_const_input(tensor):
432
+ continue
433
+
434
+ # If a KGEN op has external non-constant input, it is considered partially quantizable
435
+ if tensor.name not in partition_node_outputs:
436
+ # partition heads will be fully quantizable and added
437
+ has_external_inputs = True
438
+ else:
439
+ producer_node = tensor.inputs[0]
440
+ # format: source, target, input
441
+ # Note. it might happen that this node was not quantized
442
+ # We just ignore it from no_quantize_inputs list in post-processing
443
+ internal_inputs.append((producer_node, node, tensor.name))
444
+
445
+ if not has_external_inputs:
446
+ non_quantizable_partition_nodes.append(node)
447
+ elif has_external_inputs and internal_inputs:
448
+ no_quantize_inputs.extend(internal_inputs)
449
+ else:
450
+ # partition head is quantizable
451
+ quantizable_partition_nodes.append(node)
452
+
453
+ return non_quantizable_partition_nodes, quantizable_partition_nodes, no_quantize_inputs
454
+
455
+
456
+ def build_non_residual_input_map(
457
+ graph: Graph,
458
+ ) -> tuple[dict[str, str], list[tuple[Node, Node, str]]]:
459
+ """Builds a map of non-residual Add input name to the Add node name from the given graph.
460
+
461
+ This assumes that the Add layer only has 2 inputs.
462
+
463
+ We will refer to a subgraph which has a Convolution node with a single output that is summed (element-wise)
464
+ with another non-constant input-tensor as a "residual-add" subgraph, because it occurs in modern
465
+ convnets that use residual connections.
466
+
467
+ Args:
468
+ graph: Onnx model graph.
469
+
470
+ Returns:
471
+ Dictionary of Add node names vs their non-residual input name.
472
+ List of partially-quantizable inputs with non-quantizable input info as (src, dst, input_name)
473
+ """
474
+ non_residual_inputs = {}
475
+ no_quantize_inputs = []
476
+ for node in graph.nodes:
477
+ if node.op in ["Add"]:
478
+ # Add nodes with constant or graph input does not have non-residual input
479
+ # Here, A = node.inputs[0], B = node.inputs[1] and A.inputs means producer nodes of A
480
+ # TODO: make this check a util?
481
+ if (
482
+ has_const_input(node)
483
+ or len(node.inputs[0].inputs) == 0
484
+ or len(node.inputs[1].inputs) == 0
485
+ ):
486
+ non_residual_inputs[node.name] = None
487
+ continue
488
+
489
+ input1_producer = node.i(0, 0)
490
+ input2_producer = node.i(1, 0)
491
+
492
+ backbone1 = get_fusible_backbone(input1_producer, graph)
493
+ backbone2 = get_fusible_backbone(input2_producer, graph)
494
+
495
+ # Input in the longest path to LCA is the non-residual input
496
+ lca, d1, d2 = find_lowest_common_ancestor(input1_producer, input2_producer)
497
+
498
+ # Generally if both the inputs have a backbone then both backbones are of the same type
499
+ if backbone1 and backbone2:
500
+ if backbone1 == backbone2 or backbone1.op != backbone2.op:
501
+ non_residual_inputs[node.name] = None
502
+ continue
503
+
504
+ if d1 > d2:
505
+ non_residual_inputs[node.name] = node.inputs[0].name
506
+ no_quantize_inputs.append((input1_producer, node, node.inputs[0].name))
507
+ else:
508
+ non_residual_inputs[node.name] = node.inputs[1].name
509
+ no_quantize_inputs.append((input2_producer, node, node.inputs[1].name))
510
+ elif backbone1:
511
+ # ConvNext pattern
512
+ # Conv ---------------------- add
513
+ # \---- non backbone---/
514
+ # This case LCA being backbone itself is not residual Add case.
515
+ if lca and lca == backbone1.name:
516
+ # Not a residual Add node
517
+ non_residual_inputs[node.name] = None
518
+ else:
519
+ non_residual_inputs[node.name] = node.inputs[0].name
520
+ no_quantize_inputs.append((input1_producer, node, node.inputs[0].name))
521
+ elif backbone2:
522
+ if lca and lca == backbone2.name:
523
+ # Not a residual Add node
524
+ non_residual_inputs[node.name] = None
525
+ else:
526
+ non_residual_inputs[node.name] = node.inputs[1].name
527
+ no_quantize_inputs.append((input2_producer, node, node.inputs[1].name))
528
+ else:
529
+ # Not a residual Add node
530
+ non_residual_inputs[node.name] = None
531
+
532
+ return non_residual_inputs, no_quantize_inputs
533
+
534
+
535
+ def remove_partial_input_qdq(
536
+ graph: Graph,
537
+ no_quantize_inputs: list[tuple[Node, Node, str]],
538
+ ) -> None:
539
+ """Modifies the onnx model by removing QDQ nodes from the marked inputs, ex. non-residual inputs etc.
540
+
541
+ Args:
542
+ graph: Onnx model graph.
543
+ no_quantize_inputs: List non-quantizable input info as (src, dst, input_name)
544
+ """
545
+ logger.info("Deleting QDQ nodes from marked inputs to make certain operations fusible")
546
+ graph_nodes = {node.name: node for node in graph.nodes}
547
+ for source, target, non_qdq_input_name in no_quantize_inputs:
548
+ # Note. no_quantize_inputs objects are from non-quantized input graph
549
+ # we are deleting some QDQ from the new quantized output graph
550
+ source_node = graph_nodes[source.name]
551
+ try:
552
+ dq_node = source_node.o().o()
553
+ except Exception:
554
+ # Reached end of the graph
555
+ continue
556
+ if dq_node.op == "DequantizeLinear":
557
+ dq_node = dq_node.outputs[0] # source_node->Q->DQ->target_node
558
+ while len(dq_node.outputs):
559
+ # Find the input index in the target connecting with source_node
560
+ target_input_idx_arr = [
561
+ idx
562
+ for idx, inp in enumerate(dq_node.outputs[0].inputs)
563
+ if inp.name == dq_node.name
564
+ ]
565
+ target_input_idx = target_input_idx_arr[0] if target_input_idx_arr else 0
566
+
567
+ # Connect the output of source_node with the outputs of DQ until DQ is not connected to any other
568
+ # layers. Note that when a connection is removed, this is also deleted from dq_node.outputs, thus
569
+ # why we keep iterating over the same idx=0 in dq_node.outputs[0].
570
+ dq_node.outputs[0].inputs[target_input_idx] = source_node.outputs[0]
571
+
572
+ graph.cleanup()
573
+ graph.toposort()
574
+
575
+
576
+ def _find_nodes_from_op_types_to_exclude(graph: Graph, op_types_to_exclude=None) -> list[str]:
577
+ nodes_to_exclude = []
578
+ if op_types_to_exclude:
579
+ nodes_to_exclude = [node.name for node in graph.nodes if node.op in op_types_to_exclude]
580
+ return nodes_to_exclude
581
+
582
+
583
+ def expand_node_names_from_patterns(
584
+ graph: onnx.GraphProto | Graph, name_patterns: list[str] | None = None
585
+ ) -> list[str]:
586
+ """Expand the node names from the given patterns."""
587
+ if not name_patterns:
588
+ return []
589
+ node_list = getattr(graph, "nodes", None) or getattr(graph, "node", None) or []
590
+
591
+ matched_node_names = []
592
+ for pattern in name_patterns:
593
+ matched_node_names.extend([node.name for node in node_list if re.match(pattern, node.name)])
594
+ return matched_node_names
595
+
596
+
597
+ def find_nodes_to_exclude(
598
+ graph: Graph, nodes_to_exclude: list[str], op_types_to_exclude: list[str]
599
+ ):
600
+ """Find the node names from the ONNX graph which matches user's exclusion patterns."""
601
+ nodes_to_exclude = nodes_to_exclude or []
602
+ nodes_to_exclude = expand_node_names_from_patterns(graph, nodes_to_exclude)
603
+ nodes_to_exclude.extend(_find_nodes_from_op_types_to_exclude(graph, op_types_to_exclude))
604
+
605
+ # Remove duplicates from the exclusion list
606
+ return [*set(nodes_to_exclude)]
607
+
608
+
609
+ def get_extended_model_outputs(
610
+ onnx_path: str,
611
+ extended_model: onnx.ModelProto,
612
+ use_external_data_format: bool,
613
+ intermediate_generated_files: list[str],
614
+ calibration_data_reader: CalibrationDataReader,
615
+ calibration_eps: list[str],
616
+ ) -> dict[str, np.ndarray]:
617
+ """Run one inference step on an onnx model which has some intermediate tensor marked as model outputs.
618
+
619
+ The first calibration data is used for the dummy inference. This is useful when we want to know the shape of an
620
+ intermediate tensor given the calibration data.
621
+
622
+ Args:
623
+ onnx_path:
624
+ Path to the original onnx model, used for saving the extended model nearby if it is larger than 2GB.
625
+ extended_model:
626
+ The onnx model with some intermediate tensors marked as model outputs.
627
+ use_external_data_format:
628
+ If True, external data path will be used to store the weights of the intermediate model.
629
+ intermediate_generated_files:
630
+ List of intermediate generated files that will be deleted after quantization.
631
+ calibration_data_reader:
632
+ Calibration data reader for running inference.
633
+ calibration_eps:
634
+ Priority order for the execution providers (EP) to calibrate the model.
635
+ Any subset of ['cuda:x', 'cpu', 'trt'], where 'x' is the device id.
636
+
637
+ Returns: a map with each output name pointed to the corresponding output numpy ndarray.
638
+ """
639
+ # Get the first calibration input data.
640
+ inputs = calibration_data_reader.get_first()
641
+
642
+ # Initialize ORT session.
643
+ if use_external_data_format:
644
+ extended_onnx_path = onnx_path.replace(".onnx", "_extended.onnx")
645
+ save_onnx(extended_model, extended_onnx_path, save_as_external_data=True)
646
+ intermediate_generated_files.append(extended_onnx_path)
647
+ session = create_inference_session(extended_onnx_path, calibration_eps)
648
+ else:
649
+ session = create_inference_session(extended_model.SerializeToString(), calibration_eps)
650
+
651
+ # Run extended model's inference.
652
+ extended_model_output_names = [output.name for output in session.get_outputs()]
653
+ outputs = session.run(extended_model_output_names, inputs)
654
+ output_map = dict(zip(extended_model_output_names, outputs))
655
+
656
+ return output_map
657
+
658
+
659
+ def find_nodes_from_matmul_to_exclude(
660
+ onnx_path: str,
661
+ use_external_data_format: bool = False,
662
+ intermediate_generated_files: list[str] | None = None,
663
+ calibration_data_reader: CalibrationDataReader = None,
664
+ calibration_eps: list[str] = ["cpu", "cuda:0", "trt"],
665
+ calibration_shapes: str | None = None,
666
+ ) -> list[str]:
667
+ """Find MatMul nodes that meets gemv condition to exclude.
668
+
669
+ Either of m or n in matmul is 1, this matmul cannot utilize
670
+ TensorCores. The perf of adding Q/DQ layers is not good in
671
+ TRT. Thus, in this case, do not add Q/DQ layers to this matmul.
672
+
673
+ Args:
674
+ onnx_path: Path to the onnx model.
675
+ use_external_data_format: If True, external data path will be used to store the
676
+ weights of the intermediate model.
677
+ intermediate_generated_files: List of intermediate generated files that will be deleted after quantization.
678
+ calibration_data_reader: Calibration data reader for running inference.
679
+ calibration_shapes: Model input shapes for inference. If provided, symbolic shape inference will be used
680
+ instead of calibration_data_reader.
681
+ calibration_eps: Priority order for the execution providers (EP) to calibrate the model.
682
+ Any subset of ['cuda:x', 'cpu', 'trt'], where 'x' is the device id.
683
+
684
+ Returns:
685
+ List of Nodes to exclude from quantization.
686
+ """
687
+ model = onnx.load(onnx_path, load_external_data=True)
688
+ graph = gs.import_onnx(model)
689
+
690
+ matmul_nodes = [node for node in graph.nodes if node.op in {"MatMul", "Gemm"}]
691
+ if not matmul_nodes:
692
+ logger.debug("No MatMul nodes found in the model")
693
+ return []
694
+
695
+ nodes_to_exclude = []
696
+ logger.debug(f"Found {len(matmul_nodes)} MatMul nodes to analyze")
697
+
698
+ if calibration_shapes:
699
+ nodes_to_exclude = _exclude_matmuls_by_symbolic_inference(
700
+ model, matmul_nodes, calibration_shapes
701
+ )
702
+ else:
703
+ if calibration_data_reader is None:
704
+ raise ValueError(
705
+ "Either calibration_shapes or calibration_data_reader must be provided"
706
+ )
707
+ nodes_to_exclude = _exclude_matmuls_by_inference(
708
+ onnx_path,
709
+ model,
710
+ matmul_nodes,
711
+ use_external_data_format,
712
+ intermediate_generated_files or [],
713
+ calibration_data_reader,
714
+ calibration_eps,
715
+ )
716
+
717
+ logger.debug(f"Matmul nodes to exclude: {nodes_to_exclude}")
718
+ return [*set(nodes_to_exclude)]
719
+
720
+
721
+ def _exclude_matmuls_by_symbolic_inference(
722
+ model: onnx.ModelProto, matmul_nodes: list, calibration_shapes: str | None = None
723
+ ) -> list[str]:
724
+ """Use symbolic shape inference to find MatMuls with dimension 1."""
725
+ # Prepare model for symbolic inference
726
+ for graph_input in model.graph.input:
727
+ for dim in graph_input.type.tensor_type.shape.dim:
728
+ if dim.HasField("dim_param"):
729
+ dim.Clear()
730
+ dim.dim_value = 1
731
+
732
+ # Apply calibration shapes if provided
733
+ input_shapes = parse_shapes_spec(calibration_shapes) if calibration_shapes else {}
734
+ for graph_input in model.graph.input:
735
+ if graph_input.name in input_shapes:
736
+ input_shape = input_shapes[graph_input.name]
737
+ tensor_shape = graph_input.type.tensor_type.shape.dim
738
+ if len(tensor_shape) != len(input_shape):
739
+ raise ValueError(
740
+ f"{graph_input.name} expects shape of rank {len(tensor_shape)}, "
741
+ f"but calibration shape of rank {len(input_shape)} was passed."
742
+ )
743
+ for dim, new_dim_value in zip(tensor_shape, input_shape):
744
+ dim.dim_value = new_dim_value
745
+
746
+ model.graph.ClearField("value_info")
747
+ model = SymbolicShapeInference.infer_shapes(model)
748
+ value_info_map = {vi.name: vi for vi in model.graph.value_info}
749
+
750
+ nodes_to_exclude = []
751
+ for matmul_node in matmul_nodes:
752
+ output_name = matmul_node.outputs[0].name
753
+ value_info = value_info_map.get(output_name)
754
+ if not value_info:
755
+ raise RuntimeError(f"Shape inference did not find shape for {output_name}.")
756
+
757
+ dims = value_info.type.tensor_type.shape.dim
758
+ if len(dims) < 2:
759
+ raise RuntimeError(f"Shape for {output_name} is incorrect.")
760
+
761
+ if dims[-1].dim_value == 1 or dims[-2].dim_value == 1:
762
+ nodes_to_exclude.append(matmul_node.name)
763
+
764
+ return nodes_to_exclude
765
+
766
+
767
+ def _exclude_matmuls_by_inference(
768
+ onnx_path: str,
769
+ model: onnx.ModelProto,
770
+ matmul_nodes: list,
771
+ use_external_data_format: bool,
772
+ intermediate_generated_files: list[str],
773
+ calibration_data_reader: CalibrationDataReader,
774
+ calibration_eps: list[str],
775
+ ) -> list[str]:
776
+ """Use actual inference to find MatMuls with dimension 1."""
777
+ # Add matmul outputs to model outputs
778
+ for matmul_node in matmul_nodes:
779
+ model.graph.output.extend([onnx.ValueInfoProto(name=matmul_node.outputs[0].name)])
780
+
781
+ output_map = get_extended_model_outputs(
782
+ onnx_path,
783
+ model,
784
+ use_external_data_format,
785
+ intermediate_generated_files,
786
+ calibration_data_reader,
787
+ calibration_eps,
788
+ )
789
+
790
+ nodes_to_exclude = []
791
+ for matmul_node in matmul_nodes:
792
+ matmul_output = output_map[matmul_node.outputs[0].name]
793
+ if (
794
+ len(matmul_output.shape) < 2
795
+ or matmul_output.shape[-1] == 1
796
+ or matmul_output.shape[-2] == 1
797
+ ):
798
+ nodes_to_exclude.append(matmul_node.name)
799
+
800
+ return nodes_to_exclude
801
+
802
+
803
+ def find_nodes_from_mha_to_exclude(
804
+ onnx_path: str,
805
+ use_external_data_format: bool = False,
806
+ nodes_to_exclude: list[str] | None = None,
807
+ disable_mha_qdq: bool = False,
808
+ quantize_mode: str = "int8",
809
+ intermediate_generated_files: list[str] | None = None,
810
+ calibration_data_reader: CalibrationDataReader = None,
811
+ calibration_eps: list[str] = ["cpu", "cuda:0", "trt"],
812
+ ) -> list[str]:
813
+ """Find MatMul nodes in MHA pattern to exclude.
814
+
815
+ If disable_mha_qdq is set, don't add Q/DQ layers to MatMuls in MHA pattern.
816
+ else when quantize_mode == "fp8", if head_size > 256 or head_size <= 8 or
817
+ mha doesn't meet fp8 fMHA v2 pattern, don't add Q/DQ layers to MatMuls in MHA pattern.
818
+ else when quantize_mode == "int8", if seq_len > 512, don't add Q/DQ layers
819
+ to MatMuls in MHA pattern.
820
+
821
+ Args:
822
+ onnx_path:
823
+ Path to the onnx model.
824
+ use_external_data_format:
825
+ If True, external data path will be used to store the weights of the intermediate model.
826
+ nodes_to_exclude:
827
+ List of Nodes to exclude from quantization.
828
+ disable_mha_qdq:
829
+ If True, all MHA's BMM1 and BMM2 will be added to nodes_to_exclude.
830
+ Else, each MHA will be checked whether to enable QDQ or not when is_fp8fp16 is True.
831
+ quantize_mode:
832
+ Quantization mode. One of 'int8' (default), 'int4' and 'fp8'.
833
+ intermediate_generated_files:
834
+ List of intermediate generated files that will be deleted after quantization.
835
+ calibration_data_reader:
836
+ Calibration data reader for running inference.
837
+ calibration_eps:
838
+ Priority list of execution providers (EP) for calibration.
839
+
840
+ Returns:
841
+ List of Nodes to exclude from quantization.
842
+ """
843
+ logger.info(f"Analyzing MHA nodes for {quantize_mode} quantization")
844
+ model = onnx.load(onnx_path, load_external_data=True)
845
+ graph = gs.import_onnx(model)
846
+
847
+ mha_partitions = find_mha_partitions(graph)
848
+ if len(mha_partitions) == 0:
849
+ logger.info("No MHA partitions found in the model")
850
+ return nodes_to_exclude # type: ignore[return-value]
851
+
852
+ matmul_nodes_to_exclude = []
853
+ if disable_mha_qdq:
854
+ logger.info("Disabling QDQ for all MHA nodes")
855
+ for mha_partition in mha_partitions:
856
+ matmul_nodes_to_exclude.append(mha_partition[0].name)
857
+ matmul_nodes_to_exclude.append(mha_partition[2].name)
858
+ elif quantize_mode in {"fp8", "int8"}:
859
+ # Add each BMM1's second input as BS1 model's extended outputs.
860
+ for mha_partition in mha_partitions:
861
+ bmm1_node = mha_partition[0]
862
+ model.graph.output.extend([onnx.ValueInfoProto(name=bmm1_node.inputs[1].name)])
863
+
864
+ # To get head_size and seq_len of MHA of the model, we run extended model inference once to get the shape info.
865
+ output_map = get_extended_model_outputs(
866
+ onnx_path,
867
+ model,
868
+ use_external_data_format,
869
+ intermediate_generated_files, # type: ignore[arg-type]
870
+ calibration_data_reader,
871
+ calibration_eps,
872
+ )
873
+
874
+ # For each MHA block,
875
+ # In quantize_mode == int8, if seq_len > 512, add bmm to nodes_to_exclude.
876
+ # In quantize_mode == fp8, if head_size > 256 or head_size <= 8, add its bmm to nodes_to_exclude.
877
+ for mha_partition in mha_partitions:
878
+ bmm1_node = mha_partition[0]
879
+ softmax_node = mha_partition[1]
880
+ bmm1_input_name = bmm1_node.inputs[1].name
881
+ bmm1_input = output_map[bmm1_input_name]
882
+ seq_len = bmm1_input.shape[-1]
883
+ head_size = bmm1_input.shape[-2]
884
+ enable_mha_qdq = True
885
+ if quantize_mode == "int8":
886
+ if seq_len > 512:
887
+ enable_mha_qdq = False
888
+ logger.debug(
889
+ f"Disabling QDQ for MHA node {bmm1_node.name} due to seq_len {seq_len} > 512"
890
+ )
891
+ elif quantize_mode == "fp8":
892
+ if head_size > 256 or head_size <= 8:
893
+ enable_mha_qdq = False
894
+ logger.debug(
895
+ f"Disabling QDQ for MHA node {bmm1_node.name} due to head_size {head_size}"
896
+ )
897
+ else:
898
+ fp8_fmha_v2_pattern = match_fp8_mha_pattern(graph, softmax_node, False)
899
+ if len(fp8_fmha_v2_pattern) == 0:
900
+ enable_mha_qdq = False
901
+ logger.debug(
902
+ f"Disabling QDQ for MHA node {bmm1_node.name} due to non-matching FP8 pattern"
903
+ )
904
+ if not enable_mha_qdq:
905
+ matmul_nodes_to_exclude.append(mha_partition[0].name)
906
+ matmul_nodes_to_exclude.append(mha_partition[2].name)
907
+
908
+ logger.debug(f"Matmul nodes From MHA to exclude: {matmul_nodes_to_exclude}")
909
+
910
+ nodes_to_exclude.extend(matmul_nodes_to_exclude) # type: ignore[union-attr]
911
+ # Remove duplicates from the exclusion list
912
+ return [*set(nodes_to_exclude)] # type: ignore[arg-type]
913
+
914
+
915
+ def validate_op_types_spelling(onnx_path, op_types_to_quantize, op_types_to_exclude) -> None:
916
+ """Validate spelling in op types."""
917
+
918
+ def find_item_ignore_case(target, arr):
919
+ target_lower = target.lower()
920
+ for item in arr:
921
+ if item.lower() == target_lower:
922
+ return item
923
+ return None
924
+
925
+ model = onnx.load(onnx_path, load_external_data=True)
926
+ op_types = {node.op_type for node in model.graph.node}
927
+ for op_type in op_types:
928
+ if op_types_to_quantize:
929
+ op_to_quant = find_item_ignore_case(op_type, op_types_to_quantize)
930
+ if op_type not in op_types_to_quantize and op_to_quant is not None:
931
+ logger.warning(
932
+ f"Model contains '{op_type}' ops, but you're requesting '{op_to_quant}' "
933
+ f"to be quantized, which is not a match. Please ensure that the lower/uppercasing is correct."
934
+ )
935
+ if op_types_to_exclude:
936
+ op_to_exclude = find_item_ignore_case(op_type, op_types_to_exclude)
937
+ if op_type not in op_types_to_exclude and op_to_exclude is not None:
938
+ logger.warning(
939
+ f"Model contains '{op_type}' ops, but you're requesting '{op_to_exclude}' "
940
+ f"to be excluded from quantization, which is not a match. "
941
+ f"Please ensure that the lower/uppercasing is correct."
942
+ )
943
+
944
+
945
+ def cast_custom_ops(onnx_model: onnx.ModelProto, ops_to_cast: dict) -> onnx.ModelProto:
946
+ """Adds cast_to_fp16 nodes to the inputs and cast_to_fp32 to the outputs of a layer in the requested indices."""
947
+ logger.info("Casting custom ops in the requested inputs and outputs")
948
+ name_dict = {}
949
+
950
+ def _get_unique_name(old_name):
951
+ if old_name not in name_dict:
952
+ name_dict[old_name] = 0
953
+ return old_name
954
+ name_dict[old_name] = name_dict[old_name] + 1
955
+ return old_name + "_" + str(name_dict[old_name])
956
+
957
+ def _is_castable_tensor(tensor) -> bool:
958
+ castable_types = ["float16", "float32", "double"]
959
+ return tensor.dtype and tensor.dtype in castable_types
960
+
961
+ def _add_cast_node_inp(tensor, precision="fp16", suffix=""):
962
+ if precision == "fp16":
963
+ onnx_precision = int(onnx.TensorProto.FLOAT16)
964
+ np_precision = "float16"
965
+ else:
966
+ onnx_precision = int(onnx.TensorProto.FLOAT)
967
+ np_precision = "float32"
968
+
969
+ cast_out = Variable(
970
+ name=_get_unique_name(tensor.name + f"_{precision}{suffix}"),
971
+ dtype=np_precision,
972
+ shape=tensor.shape,
973
+ )
974
+ cast_node = Node(
975
+ op="Cast",
976
+ name=_get_unique_name(tensor.name + f"_cast_to_{precision}{suffix}"),
977
+ attrs={"to": onnx_precision},
978
+ inputs=[tensor],
979
+ outputs=[cast_out],
980
+ )
981
+ graph.nodes.append(cast_node)
982
+ return cast_out
983
+
984
+ def _add_cast_node_out(tensor, inp_precision="fp16", out_precision="fp32", suffix=""):
985
+ cast_precision = (
986
+ int(onnx.TensorProto.FLOAT16)
987
+ if out_precision == "fp16"
988
+ else int(onnx.TensorProto.FLOAT)
989
+ )
990
+ np_precision = "float16" if inp_precision == "fp16" else "float32"
991
+
992
+ cast_inp = gs.Variable(
993
+ name=_get_unique_name(tensor.name + f"_{inp_precision}{suffix}"),
994
+ dtype=np_precision,
995
+ shape=tensor.shape,
996
+ )
997
+ cast_node = gs.Node(
998
+ op="Cast",
999
+ name=_get_unique_name(tensor.name + f"_cast_to_{out_precision}{suffix}"),
1000
+ attrs={"to": cast_precision},
1001
+ inputs=[cast_inp],
1002
+ outputs=[tensor],
1003
+ )
1004
+ graph.nodes.append(cast_node)
1005
+ return cast_inp
1006
+
1007
+ graph = gs.import_onnx(onnx_model)
1008
+ castable_nodes = [n for n in graph.nodes if n.op in ops_to_cast]
1009
+
1010
+ for node in castable_nodes:
1011
+ inp_idxs = ops_to_cast[node.op]["inp"]
1012
+ out_idxs = ops_to_cast[node.op]["out"]
1013
+
1014
+ # Cast relevant inputs to FP16
1015
+ for inp_idx, inp in enumerate(node.inputs):
1016
+ if inp_idx in inp_idxs and _is_castable_tensor(inp):
1017
+ cast_out = _add_cast_node_inp(inp)
1018
+ node.inputs[inp_idx] = cast_out
1019
+
1020
+ # Cast relevant outputs from FP16 back to FP32
1021
+ for out_idx, out in enumerate(node.outputs):
1022
+ if out_idx in out_idxs and _is_castable_tensor(out):
1023
+ cast_inp = _add_cast_node_out(out)
1024
+ node.outputs[out_idx] = cast_inp
1025
+
1026
+ graph.cleanup().toposort()
1027
+
1028
+ onnx_model = gs.export_onnx(graph)
1029
+ # TODO: remove manual ir_version change once ORT supports ir_version 11
1030
+ onnx_model.ir_version = 10
1031
+
1032
+ return onnx_model
1033
+
1034
+
1035
+ def print_stat(graph: Graph) -> None:
1036
+ """Collect and print stats of the quantized model."""
1037
+ count = 0
1038
+ quantized_type_counts = {}
1039
+ quantized_nodes = []
1040
+ output_names = [output_node.name for output_node in graph.outputs]
1041
+ for node in graph.nodes:
1042
+ for tensor in node.inputs:
1043
+ if len(tensor.inputs) == 0:
1044
+ continue
1045
+
1046
+ producer_node = tensor.inputs[0]
1047
+ if producer_node.op == "DequantizeLinear":
1048
+ quantized_type_counts[node.op] = quantized_type_counts.get(node.op, 0) + 1
1049
+ quantized_nodes.append(node.name)
1050
+ count += 1
1051
+ break
1052
+ else:
1053
+ # Sometimes "_DequantizeLinear_Output" is not suffix of the "DequantizeLinear" typed node,
1054
+ # if that node is also in final model output. Ex. CLIP-ViT-L-14-opset16.onnx
1055
+ assert tensor.name in output_names or producer_node.op != "DequantizeLinear"
1056
+
1057
+ logger.info(f"Total number of nodes: {len(graph.nodes)}")
1058
+ logger.info(f"Total number of quantized nodes: {count}")
1059
+ logger.debug(f"Quantized type counts: {quantized_type_counts}")
1060
+ logger.debug(f"Quantized nodes: {quantized_nodes}")
1061
+
1062
+
1063
+ def find_mha_partitions(graph):
1064
+ """Match MHA: BMM1 -> ... -> Softmax -> ... -> BMM2."""
1065
+ mha_chain_type = ["MatMul", "Softmax", "MatMul"]
1066
+ wild_card_types = [
1067
+ "Div",
1068
+ "Mul",
1069
+ "ConstMul",
1070
+ "Add",
1071
+ "BiasAdd",
1072
+ "Reshape",
1073
+ "Transpose",
1074
+ "Flatten",
1075
+ "Cast",
1076
+ ]
1077
+ mha_partitions = []
1078
+ for node in graph.nodes:
1079
+ if node.op == "MatMul":
1080
+ mha_partition = []
1081
+ if has_path_type(
1082
+ node, graph, mha_chain_type, True, wild_card_types, mha_partition
1083
+ ) and (
1084
+ len(mha_partition) == 3
1085
+ and mha_partition[0].op == "MatMul"
1086
+ and mha_partition[2].op == "MatMul"
1087
+ ):
1088
+ mha_partitions.append(mha_partition)
1089
+
1090
+ return mha_partitions
1091
+
1092
+
1093
+ def match_fp8_mha_pattern(graph: Graph, softmax_op: Node, has_fp8_qdq: bool) -> list[Node]:
1094
+ """Match FP8 fMHA v2 with the given softmax_op.
1095
+
1096
+ If has_fp8_qdq == True, we match this FP8 fMHA v2 pattern:
1097
+ Q -> DQ -> BMM1 -> (Mul/Div) -> (Add) -> Softmax -> (Cast) -> Q -> DQ -> BMM2 -> Q -> DQ.
1098
+ If has_fp8_qdq == False, we match this FP8 fMHA v2 pattern:
1099
+ BMM1 -> (Mul/Div) -> (Add) -> Softmax -> (Cast) -> BMM2.
1100
+
1101
+ Args:
1102
+ graph:
1103
+ The graph to match FP8 MHA pattern.
1104
+ softmax_op:
1105
+ The softmax op of FP8 MHA we want to match.
1106
+ nodes_to_exclude:
1107
+ List of Nodes to exclude from quantization.
1108
+ has_fp8_qdq:
1109
+ If True, match the FP8 MHA with Q/DQs.
1110
+ Else, match the FP8 MHA without Q/DQs.
1111
+
1112
+ Returns:
1113
+ List of BMM1 node, Softmax node and BMM2 node.
1114
+ """
1115
+ if has_fp8_qdq:
1116
+ softmax_bmm1_chain_types = [
1117
+ ["Softmax", "MatMul", "DequantizeLinear", "QuantizeLinear"],
1118
+ ["Softmax", "Add", "MatMul", "DequantizeLinear", "QuantizeLinear"],
1119
+ ["Softmax", "Div", "MatMul", "DequantizeLinear", "QuantizeLinear"],
1120
+ ["Softmax", "Mul", "MatMul", "DequantizeLinear", "QuantizeLinear"],
1121
+ ["Softmax", "Add", "Div", "MatMul", "DequantizeLinear", "QuantizeLinear"],
1122
+ ["Softmax", "Add", "Mul", "MatMul", "DequantizeLinear", "QuantizeLinear"],
1123
+ ]
1124
+ softmax_bmm2_chain_type = [
1125
+ "Softmax",
1126
+ "QuantizeLinear",
1127
+ "DequantizeLinear",
1128
+ "MatMul",
1129
+ "QuantizeLinear",
1130
+ "DequantizeLinear",
1131
+ ]
1132
+ else:
1133
+ softmax_bmm1_chain_types = [
1134
+ ["Softmax", "MatMul"],
1135
+ ["Softmax", "Add", "MatMul"],
1136
+ ["Softmax", "Div", "MatMul"],
1137
+ ["Softmax", "Mul", "MatMul"],
1138
+ ["Softmax", "Add", "Div", "MatMul"],
1139
+ ["Softmax", "Add", "Mul", "MatMul"],
1140
+ ]
1141
+ softmax_bmm2_chain_type = [
1142
+ "Softmax",
1143
+ "MatMul",
1144
+ ]
1145
+ wild_card_types = [
1146
+ "Reshape",
1147
+ "Transpose",
1148
+ "Flatten",
1149
+ "Cast",
1150
+ ]
1151
+
1152
+ bmm1_index = 1
1153
+ bmm2_index = 7 if has_fp8_qdq else 3
1154
+ # Maps chain_idx to bmm position offsets for indexing the fp8_mha_partition
1155
+ # chain_idx=0 -> offset=0
1156
+ # chain_idx=1,2,3 -> offset=1
1157
+ # chain_idx=4,5 -> offset=2
1158
+ bmm_offset_map = {0: 0, 1: 1, 2: 1, 3: 1, 4: 2, 5: 2}
1159
+ for chain_idx, softmax_bmm1_chain_type in enumerate(softmax_bmm1_chain_types):
1160
+ fp8_mha_partition = []
1161
+ if has_path_type(
1162
+ softmax_op, graph, softmax_bmm1_chain_type, False, wild_card_types, fp8_mha_partition
1163
+ ) and has_path_type(
1164
+ softmax_op, graph, softmax_bmm2_chain_type, True, wild_card_types, fp8_mha_partition
1165
+ ):
1166
+ offset = bmm_offset_map.get(chain_idx)
1167
+ bmm1_node = fp8_mha_partition[bmm1_index + offset]
1168
+ bmm2_node = fp8_mha_partition[bmm2_index + offset]
1169
+ assert bmm1_node.op == "MatMul" and bmm2_node.op == "MatMul"
1170
+ return [bmm1_node, softmax_op, bmm2_node]
1171
+ return []
1172
+
1173
+
1174
+ def insert_matmul_casts(graph, matmul_node):
1175
+ """Insert three cast nodes for MatMul's two inputs and output."""
1176
+ matmul_input0 = matmul_node.inputs[0]
1177
+ matmul_input0_cast_output = gs.Variable(
1178
+ name=f"{matmul_input0.name}/Cast_output", dtype=np.float32
1179
+ )
1180
+ graph.layer(
1181
+ op="Cast",
1182
+ name=f"{matmul_input0.name}/Cast",
1183
+ inputs=[matmul_input0],
1184
+ outputs=[matmul_input0_cast_output],
1185
+ attrs={"to": np.float32},
1186
+ )
1187
+ matmul_node.inputs[0] = matmul_input0_cast_output
1188
+
1189
+ matmul_input1 = matmul_node.inputs[1]
1190
+ matmul_input1_cast_output = gs.Variable(
1191
+ name=f"{matmul_input1.name}/Cast_output", dtype=np.float32
1192
+ )
1193
+ graph.layer(
1194
+ op="Cast",
1195
+ name=f"{matmul_input1.name}/Cast",
1196
+ inputs=[matmul_input1],
1197
+ outputs=[matmul_input1_cast_output],
1198
+ attrs={"to": np.float32},
1199
+ )
1200
+ matmul_node.inputs[1] = matmul_input1_cast_output
1201
+
1202
+ matmul_output = matmul_node.outputs[0]
1203
+ matmul_output_cast_input = gs.Variable(
1204
+ name=f"{matmul_output.name}/Cast_output", dtype=np.float32
1205
+ )
1206
+ graph.layer(
1207
+ op="Cast",
1208
+ name=f"{matmul_output.name}/Cast",
1209
+ inputs=[matmul_output_cast_input],
1210
+ outputs=[matmul_output],
1211
+ attrs={"to": np.float16},
1212
+ )
1213
+ matmul_node.outputs[0] = matmul_output_cast_input
1214
+
1215
+
1216
+ def insert_fp8_mha_casts(onnx_model):
1217
+ r"""Insert three cast ops.
1218
+
1219
+ The first cast will be added before the input0 of MatMul to cast fp16 to fp32.
1220
+ The second cast will be added before the input1 of MatMul to cast fp16 to fp32.
1221
+ The third cast will be added after the output of MatMul to cast fp32 back to fp16.
1222
+ The insertion of Cast ops in the FP8 MHA part actually forbids the MHAs to run
1223
+ with FP16 accumulation because the compiler only has FP32 accumulation kernels for FP8 MHAs.
1224
+ """
1225
+ graph = gs.import_onnx(onnx_model)
1226
+ graph.cleanup().toposort()
1227
+
1228
+ # Match FP8 MHA: Q -> DQ -> BMM1 -> (Mul/Div) -> (Add) -> Softmax -> (Cast) -> Q -> DQ -> BMM2 -> Q -> DQ
1229
+ for node in graph.nodes:
1230
+ if node.op == "Softmax":
1231
+ fp8_fmha_v2_pattern = match_fp8_mha_pattern(graph, node, True)
1232
+ # Insert cast nodes on BMM2's input and output tensors.
1233
+ if len(fp8_fmha_v2_pattern) == 3:
1234
+ insert_matmul_casts(graph, fp8_fmha_v2_pattern[0])
1235
+ insert_matmul_casts(graph, fp8_fmha_v2_pattern[2])
1236
+
1237
+ graph.cleanup().toposort()
1238
+
1239
+ return gs.export_onnx(graph)
1240
+
1241
+
1242
+ def convert_fp16_io(graph):
1243
+ """Convert graph I/O to FP16."""
1244
+ convertible_dtypes = [
1245
+ onnx.TensorProto.FLOAT,
1246
+ onnx.TensorProto.DOUBLE,
1247
+ onnx.TensorProto.BFLOAT16,
1248
+ ]
1249
+ for input_tensor in graph.inputs:
1250
+ input_tensor.dtype = (
1251
+ onnx.TensorProto.FLOAT16
1252
+ if input_tensor.dtype in convertible_dtypes
1253
+ else input_tensor.dtype
1254
+ )
1255
+ for output_tensor in graph.outputs:
1256
+ output_tensor.dtype = (
1257
+ onnx.TensorProto.FLOAT16
1258
+ if output_tensor.dtype in convertible_dtypes
1259
+ else output_tensor.dtype
1260
+ )
1261
+
1262
+
1263
+ def remove_output_initializers(graph: gs.Graph, graph_initializers: list):
1264
+ """Remove initializers that are also listed as graph outputs.
1265
+
1266
+ Having initializers (constant tensors) that are also marked as outputs can lead to ONNX Runtime
1267
+ or conversion tool errors, particularly related to ambiguous 'dtype' or shape inference.
1268
+ This step ensures compatibility by detaching such initializers from the graph's outputs.
1269
+ """
1270
+ init_names = [init.name for init in graph_initializers]
1271
+ init_names_removed = []
1272
+ for output_tensor in graph.outputs:
1273
+ if output_tensor.name in init_names:
1274
+ graph.outputs.remove(output_tensor)
1275
+ init_names_removed.append(output_tensor.name)
1276
+ if init_names_removed:
1277
+ logger.info(f"Removed output initializers: {init_names_removed}")
1278
+
1279
+
1280
+ def get_resize_scales(onnx_model):
1281
+ r"""Record Resize op's old scale value before converting to fp16.
1282
+
1283
+ Because low precision scale will lead to wrong shape. For example, if 7 is
1284
+ resized to 6, fp32 scale should be 6/7 = 0.85714. After converting to fp16,
1285
+ it becomes 0.85693 but 7 * 0.85693 = 5.9985 < 6.
1286
+ """
1287
+ resize_scale_inits = {}
1288
+ for node in onnx_model.graph.node:
1289
+ if node.op_type == "Resize" and len(node.input) > 2 and node.input[2] is not None:
1290
+ for init in onnx_model.graph.initializer:
1291
+ if init.name == node.input[2] and init.data_type == onnx.TensorProto.FLOAT:
1292
+ resize_scale_inits[node.name] = (init.data_type, init.raw_data)
1293
+ break
1294
+ return resize_scale_inits
1295
+
1296
+
1297
+ def get_concat_eliminated_tensors(
1298
+ onnx_model: onnx.ModelProto,
1299
+ nodes_to_quantize: list[str],
1300
+ ) -> dict[str, set[str]]:
1301
+ """Find the input tensors and output tensor of concat that will be quantized.
1302
+
1303
+ We can do some perf optimization for TRT.
1304
+
1305
+ For example, like the below pattern:
1306
+ (t1) q1 -> dq1 \
1307
+ (t2) q2 -> dq2 -> concat -> q4 -> dq4 (t4)
1308
+ (t3) q3 -> dq3 /
1309
+
1310
+ In TRT, q4 will be propagated forward concat. It will be like:
1311
+ (t1) q1 -> dq1 -> q4 \
1312
+ (t2) q2 -> dq2 -> q4 -> concat -> dq4 (t4)
1313
+ (t3) q3 -> dq3 -> q4 /
1314
+
1315
+ If the scaling factor of dq1 and q4 are different, it will cause the dq-q compute latency. If
1316
+ they are the same, then the dq-q pairs can be eliminated in TRT, and no extra dq-q compute
1317
+ latency. However, it will sacrifice the accuracy.
1318
+
1319
+ Thus, this function will collect which tensors should have the same scaling factors. For the
1320
+ above example, we want the scaling factor of dq1, dq2, dq3, q4 be the same. This function will
1321
+ return like
1322
+ {
1323
+ t1: {t1,t2,t3,t4},
1324
+ t2: {t1,t2,t3,t4},
1325
+ t3: {t1,t2,t3,t4},
1326
+ t4: {t1,t2,t3,t4},
1327
+ }
1328
+ This format is convenient for calibrator to assign the same scaling factor.
1329
+
1330
+ Returns:
1331
+ {current tensor name: set of tensors that should share the same scaling factor}
1332
+ """
1333
+ logger.info("Finding concat eliminated tensors")
1334
+ input_name_to_nodes = get_tensor_consumer_nodes(onnx_model.graph)
1335
+ graph = gs.import_onnx(onnx_model)
1336
+
1337
+ # We'll use a Union-Find data structure to track tensor groups
1338
+ parent = {}
1339
+
1340
+ def find(x):
1341
+ if x not in parent:
1342
+ parent[x] = x
1343
+ if parent[x] != x:
1344
+ parent[x] = find(parent[x])
1345
+ return parent[x]
1346
+
1347
+ def union(x, y):
1348
+ parent[find(x)] = find(y)
1349
+
1350
+ # First, identify concat ops where output is quantized
1351
+ for node in graph.nodes:
1352
+ if node.op == "Concat":
1353
+ # Check if concat output is quantized
1354
+ concat_output_name = node.outputs[0].name
1355
+ concat_consumers = input_name_to_nodes[concat_output_name]
1356
+ concat_has_qdq = any(
1357
+ consumer.name in nodes_to_quantize for consumer in concat_consumers
1358
+ )
1359
+
1360
+ if concat_has_qdq:
1361
+ # Find quantized inputs to concat
1362
+ quantized_inputs = []
1363
+ for input_tensor in node.inputs:
1364
+ input_name = input_tensor.name
1365
+ input_consumers = input_name_to_nodes[input_name]
1366
+ if any(consumer.name in nodes_to_quantize for consumer in input_consumers):
1367
+ quantized_inputs.append(input_name)
1368
+
1369
+ # If we have quantized inputs, merge them with the output
1370
+ if quantized_inputs:
1371
+ # Add the concat output
1372
+ quantized_inputs.append(concat_output_name)
1373
+
1374
+ # Use union-find to merge all related tensors
1375
+ for i in range(1, len(quantized_inputs)):
1376
+ union(quantized_inputs[i], quantized_inputs[0])
1377
+
1378
+ # Build the final result dictionary
1379
+ result = {}
1380
+ all_tensors = set(parent.keys())
1381
+
1382
+ # Group by the root parent
1383
+ groups = {}
1384
+ for tensor in all_tensors:
1385
+ root = find(tensor)
1386
+ if root not in groups:
1387
+ groups[root] = set()
1388
+ groups[root].add(tensor)
1389
+
1390
+ # Build the final mapping
1391
+ for tensor in all_tensors:
1392
+ root = find(tensor)
1393
+ result[tensor] = groups[root]
1394
+ return result
1395
+
1396
+
1397
+ def remove_redundant_cast_nodes(graph: onnx.GraphProto) -> None:
1398
+ """Remove redundant Cast nodes from the ONNX graph to optimize model performance.
1399
+
1400
+ This function identifies and removes two types of redundant Cast nodes:
1401
+
1402
+ 1. Cast nodes where input and output types are identical
1403
+ - Before: t1 (dtype=fp16) -> cast (to=fp16) -> t2 -> Op
1404
+ - After: t1 (dtype=fp16) -> Op
1405
+
1406
+ 2. Cast nodes that can be fused with initializers
1407
+ - Before: (initializer) t1 (dtype=fp32) -> cast (to=fp16) -> t2 -> Op
1408
+ - After: (initializer) t1 (dtype=fp16) -> Op
1409
+
1410
+ The function preserves Cast nodes that:
1411
+ - Have outputs that are graph outputs
1412
+ - Are necessary for type conversion
1413
+ - Have dynamic inputs (not initializers)
1414
+
1415
+ Args:
1416
+ graph: ONNX graph to optimize. The graph will be modified in-place.
1417
+
1418
+ Note:
1419
+ - This optimization is particularly useful for models with many Cast operations
1420
+ - The function modifies the graph in-place
1421
+ - All tensor consumers are updated to maintain graph connectivity
1422
+ - Initializer data types are converted when possible to eliminate Cast nodes
1423
+ """
1424
+ initializers = {init.name: init for init in graph.initializer}
1425
+ tensor_consumers = get_tensor_consumer_nodes(graph)
1426
+ value_info_map = {info.name: info for info in graph.value_info}
1427
+ cast_indices = []
1428
+ output_names = {output.name for output in graph.output}
1429
+
1430
+ def _get_tensor_type(tensor_name: str) -> int | None:
1431
+ """Get the tensor type for a given tensor name."""
1432
+ if tensor_name in value_info_map:
1433
+ return value_info_map[tensor_name].type.tensor_type.elem_type
1434
+ if tensor_name in initializers:
1435
+ return initializers[tensor_name].data_type
1436
+ return None
1437
+
1438
+ for node_idx, node in enumerate(graph.node):
1439
+ if node.op_type != "Cast":
1440
+ continue
1441
+
1442
+ # Skip if output is a graph output
1443
+ if any(out_name in output_names for out_name in node.output):
1444
+ continue
1445
+
1446
+ input_name = node.input[0]
1447
+ input_type = _get_tensor_type(input_name)
1448
+ if input_type is None:
1449
+ continue
1450
+
1451
+ # Get target type from Cast node attributes
1452
+ attr = next((attr for attr in node.attribute if attr.name == "to"), None)
1453
+ if attr is None:
1454
+ continue
1455
+
1456
+ # Pattern 1: Input and output types are the same
1457
+ if input_type == attr.i:
1458
+ cast_indices.append(node_idx)
1459
+ # Pattern 2: Convert and fuse Cast node for initializers
1460
+ elif input_name in initializers:
1461
+ cast_indices.append(node_idx)
1462
+ cast_input = onnx.numpy_helper.to_array(initializers[input_name])
1463
+ dtype = onnx.helper.tensor_dtype_to_np_dtype(attr.i)
1464
+ converted_tensor = onnx.numpy_helper.from_array(cast_input.astype(dtype), input_name)
1465
+ initializers[input_name].CopyFrom(converted_tensor)
1466
+ else:
1467
+ continue
1468
+
1469
+ # Update consumer nodes
1470
+ for consumer in tensor_consumers.get(node.output[0], []):
1471
+ for i, input_tensor in enumerate(consumer.input):
1472
+ if input_tensor == node.output[0]:
1473
+ consumer.input[i] = input_name
1474
+ break
1475
+
1476
+ # Remove Cast nodes in reverse order
1477
+ logger.info(f"Removing {len(cast_indices)} redundant Cast nodes")
1478
+ for node_idx in sorted(cast_indices, reverse=True):
1479
+ del graph.node[node_idx]