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,202 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025 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
+
17
+ """AutoCast module for converting ONNX models to mixed precision.
18
+
19
+ AutoCast is a tool for converting FP32 ONNX models to mixed precision FP32-FP16 or FP32-BF16 models.
20
+ While casting FP32 to FP6/BF16, some nodes might be more sensitive to effecting accuracy.
21
+ AutoCast intelligently selects nodes to keep in FP32 precision to maintain model accuracy while benefiting from
22
+ reduced precision on the rest of the nodes. AutoCast automatically injects cast operations around the selected
23
+ nodes.
24
+ """
25
+
26
+ import numpy as np
27
+ import onnx
28
+
29
+ import modelopt.onnx.autocast.utils as utils
30
+ import modelopt.onnx.utils as onnx_utils
31
+ from modelopt.onnx.autocast.graphsanitizer import GraphSanitizer
32
+ from modelopt.onnx.autocast.logging_config import logger
33
+ from modelopt.onnx.autocast.nodeclassifier import NodeClassifier, NodeRuleBase
34
+ from modelopt.onnx.autocast.precisionconverter import PrecisionConverter
35
+ from modelopt.onnx.autocast.referencerunner import ReferenceRunner
36
+
37
+ """
38
+ FP16 accuracy decreases in accordance with the data's magnitude.
39
+ For 512, the unit in last place (ULP) is 0.5, for 1024 it is 1.0, etc.
40
+ """
41
+ DEFAULT_DATA_MAX = 512
42
+ DEFAULT_INIT_MAX = np.finfo(np.float16).max
43
+ LATEST_IR_VERSION_SUPPORTED_BY_ORT = 10
44
+
45
+
46
+ def convert_to_mixed_precision(
47
+ onnx_path: str,
48
+ low_precision_type: str = "fp16",
49
+ nodes_to_exclude: list[str] | None = None,
50
+ op_types_to_exclude: list[str] | None = None,
51
+ data_max: float = DEFAULT_DATA_MAX,
52
+ init_max: float = DEFAULT_INIT_MAX,
53
+ keep_io_types: bool = False,
54
+ calibration_data: str | None = None,
55
+ custom_rule: NodeRuleBase | None = None,
56
+ init_conversion_max_bytes: int | None = None,
57
+ providers: list[str] = ["cpu"],
58
+ trt_plugins: list[str] = [],
59
+ max_depth_of_reduction: int | None = None,
60
+ ) -> onnx.ModelProto:
61
+ """Convert model to mixed precision.
62
+
63
+ Args:
64
+ onnx_path: Path to the input ONNX model.
65
+ low_precision_type: Target precision to reduce to ('fp16' or 'bf16').
66
+ nodes_to_exclude: List of regex patterns to match node names that should remain in FP32.
67
+ op_types_to_exclude: List of operation types that should remain in FP32.
68
+ data_max: Maximum absolute value for node input and output values.
69
+ init_max: Maximum absolute value for initializers.
70
+ keep_io_types: Whether to preserve input/output types.
71
+ calibration_data: Path to input data file for reference runner.
72
+ custom_rule: Optional custom rule for node classification (inherits from NodeRuleBase).
73
+ init_conversion_max_bytes: Maximum size in bytes for initializer conversion. Larger initializers will be cast at
74
+ runtime.
75
+ providers: List of ORT execution providers.
76
+ trt_plugins: List of TensorRT plugin library paths in .so format (compiled shared library).
77
+ max_depth_of_reduction: Maximum depth of reduction for node classification.
78
+
79
+ Returns:
80
+ onnx.ModelProto: The converted mixed precision model.
81
+ """
82
+ # Load and process model
83
+ model = onnx.load(onnx_path, load_external_data=True)
84
+ assert low_precision_type in ["fp16", "bf16"], "low_precision_type must be either fp16 or bf16"
85
+
86
+ # Apply graph sanitization and optimizations
87
+ # Opsets < 22 have a very limited support for bfloat16
88
+ # Otherwise, prefer to keep the original opset version unless it's very old
89
+ min_opset = 22 if low_precision_type == "bf16" else 13
90
+ graph_sanitizer = GraphSanitizer(
91
+ model, min_opset, trt_plugins=trt_plugins, max_ir_version=LATEST_IR_VERSION_SUPPORTED_BY_ORT
92
+ )
93
+ graph_sanitizer.sanitize()
94
+ model = graph_sanitizer.model
95
+
96
+ # Setup internal mappings
97
+ model = onnx_utils.infer_shapes(model)
98
+ value_info_map, initializer_map, node_to_init_map = utils.setup_mappings(model)
99
+
100
+ # Automatically add 'trt' to list of providers if custom ops are detected
101
+ if "trt" not in providers and graph_sanitizer.custom_ops:
102
+ providers.insert(0, "trt")
103
+
104
+ # Initialize classifiers and converters
105
+ node_classifier = NodeClassifier(
106
+ model,
107
+ node_to_init_map,
108
+ initializer_map,
109
+ nodes_to_exclude=nodes_to_exclude or [],
110
+ op_types_to_exclude=op_types_to_exclude or [],
111
+ data_max=data_max,
112
+ init_max=init_max,
113
+ custom_rule=custom_rule,
114
+ max_depth_of_reduction=max_depth_of_reduction,
115
+ )
116
+
117
+ precision_converter = PrecisionConverter(
118
+ model,
119
+ value_info_map,
120
+ initializer_map,
121
+ node_to_init_map,
122
+ keep_io_types=keep_io_types,
123
+ low_precision_type=low_precision_type,
124
+ init_conversion_max_bytes=init_conversion_max_bytes,
125
+ custom_ops=graph_sanitizer.custom_ops,
126
+ )
127
+
128
+ # Obtain reference data
129
+ ref_outputs_dict = None
130
+ if (data_max is not None and data_max != np.inf) or graph_sanitizer.custom_ops:
131
+ ref_runner = ReferenceRunner(model, providers, trt_plugins)
132
+ ref_outputs_dict = ref_runner.run(calibration_data)
133
+
134
+ # Run conversion
135
+ low_precision_nodes, high_precision_nodes = node_classifier.run(ref_outputs_dict)
136
+ model_mod = precision_converter.convert(high_precision_nodes, low_precision_nodes)
137
+
138
+ # Log results
139
+ total_nodes = len(low_precision_nodes) + len(high_precision_nodes)
140
+ low_precision_percentage = (
141
+ 100 * len(low_precision_nodes) / total_nodes if total_nodes > 0 else 0
142
+ )
143
+ logger.info(
144
+ f"Converted {len(low_precision_nodes)}/{total_nodes} nodes "
145
+ f"({low_precision_percentage:.2f}%) to {low_precision_type}"
146
+ )
147
+
148
+ return model_mod
149
+
150
+
151
+ def convert_to_f16(
152
+ model: onnx.ModelProto,
153
+ low_precision_type: str = "fp16",
154
+ keep_io_types: bool = True,
155
+ op_block_list: list[str] = [],
156
+ trt_plugins: list[str] | None = [],
157
+ ) -> onnx.ModelProto:
158
+ """Convert model to mixed precision, using PrecisionConverter.
159
+
160
+ This method bypasses NodeClassifier, and uses a simple op_block_list.
161
+
162
+ Args:
163
+ model: ONNX model to convert.
164
+ low_precision_type: Target precision to reduce to ('fp16' or 'bf16').
165
+ keep_io_types: Whether to preserve input/output types.
166
+ disable_shape_infer: Whether to disable shape inference.
167
+ op_block_list: List of operation types that should remain in FP32.
168
+ trt_plugins: List of TensorRT plugin library paths in .so format (compiled shared library).
169
+ """
170
+ assert low_precision_type in ["fp16", "bf16"], "low_precision_type must be either fp16 or bf16"
171
+
172
+ # Opset 21 is needed for NVFP4 quantization support (DQ with 'block_size' attribute)
173
+ sanitizer = GraphSanitizer(
174
+ model,
175
+ min_opset=21,
176
+ trt_plugins=trt_plugins,
177
+ max_ir_version=LATEST_IR_VERSION_SUPPORTED_BY_ORT,
178
+ )
179
+ sanitizer.find_custom_nodes()
180
+ sanitizer.convert_opset()
181
+ sanitizer.ensure_graph_name_exists()
182
+ model = sanitizer.model
183
+
184
+ # Setup internal mappings
185
+ model = onnx_utils.infer_shapes(model)
186
+ value_info_map, initializer_map, node_to_init_map = utils.setup_mappings(model)
187
+
188
+ precision_converter = PrecisionConverter(
189
+ model,
190
+ value_info_map,
191
+ initializer_map,
192
+ node_to_init_map,
193
+ keep_io_types=keep_io_types,
194
+ low_precision_type=low_precision_type,
195
+ custom_ops=sanitizer.custom_ops,
196
+ )
197
+ high_precision_nodes = [node.name for node in model.graph.node if node.op_type in op_block_list]
198
+ low_precision_nodes = [
199
+ node.name for node in model.graph.node if node.op_type not in op_block_list
200
+ ]
201
+ model_mod = precision_converter.convert(high_precision_nodes, low_precision_nodes)
202
+ return model_mod
@@ -0,0 +1,422 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025 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
+ """Graph sanitization and optimization for ONNX models."""
17
+
18
+ import numpy as np
19
+ import onnx
20
+ import onnx_graphsurgeon as gs
21
+ from onnx import helper, numpy_helper
22
+
23
+ import modelopt.onnx.autocast.utils as utils
24
+ import modelopt.onnx.utils as onnx_utils
25
+ from modelopt.onnx.autocast.logging_config import logger
26
+
27
+
28
+ class GraphSanitizer:
29
+ """A class for sanitizing ONNX model graphs, a part of the AutoCast tool."""
30
+
31
+ def __init__(
32
+ self,
33
+ model: onnx.ModelProto,
34
+ min_opset: int = 13,
35
+ max_ir_version: int | None = None,
36
+ trt_plugins: list[str] | None = [],
37
+ ) -> None:
38
+ """Initialize GraphSanitizer.
39
+
40
+ Args:
41
+ model: ONNX model to sanitize
42
+ min_opset: minimum opset version to use
43
+ max_ir_version: maximum IR version supported by ORT
44
+ trt_plugins: list of TensorRT plugin library paths in .so format (compiled shared library).
45
+ """
46
+ self.model = model
47
+ self.min_opset = min_opset
48
+ self.max_ir_version = max_ir_version
49
+ self.standard_ops = {schema.name for schema in onnx.defs.get_all_schemas()}
50
+ self.custom_ops = None
51
+ self.trt_plugins = trt_plugins
52
+
53
+ def sanitize(self) -> None:
54
+ """Sanitize the model graph.
55
+
56
+ Currently, this finds decomposed LayerNorm patterns and replaces them with a single LayerNormalization operator.
57
+ Additional functionality may be added in the future.
58
+ """
59
+ self.find_custom_nodes()
60
+ self.remove_disconnected_outputs()
61
+ self.convert_opset()
62
+ self.replace_layernorm_pattern()
63
+ self.ensure_graph_name_exists()
64
+ onnx_utils.name_onnx_nodes(self.model.graph)
65
+ self.replace_custom_domain_nodes()
66
+ self.cleanup_model()
67
+ self.set_ir_version(self.max_ir_version)
68
+
69
+ def find_custom_nodes(self) -> None:
70
+ """Find custom nodes in the model.
71
+
72
+ Scans through all nodes in the graph and logs any nodes that use custom operators
73
+ that are not part of the standard ONNX operator set.
74
+ """
75
+ self.custom_ops = {
76
+ node.op_type for node in self.model.graph.node if node.op_type not in self.standard_ops
77
+ }
78
+ if self.custom_ops:
79
+ from modelopt.onnx.trt_utils import infer_types_shapes_tensorrt, set_trt_plugin_domain
80
+
81
+ # Set TensorRT plugin domain info in the graph for ORT compatibility
82
+ self.model = set_trt_plugin_domain(self.model, self.custom_ops)
83
+
84
+ # Infer types and shapes in the graph for ORT compatibility
85
+ self.model = infer_types_shapes_tensorrt(self.model, self.trt_plugins)
86
+
87
+ def remove_disconnected_outputs(self) -> None:
88
+ """Remove disconnected outputs from the model."""
89
+ tensors_to_remove = []
90
+ for tensor in self.model.graph.output:
91
+ if not utils.get_producer_nodes(self.model, tensor.name):
92
+ tensors_to_remove.append(tensor)
93
+ logger.debug(f"Found disconnected output: {tensor.name}")
94
+
95
+ if tensors_to_remove:
96
+ logger.warning(
97
+ f"Found {len(tensors_to_remove)} disconnected outputs. Removing disconnected outputs from the graph."
98
+ )
99
+
100
+ for tensor in tensors_to_remove:
101
+ self.model.graph.output.remove(tensor)
102
+
103
+ def convert_opset(self) -> None:
104
+ """Convert the model to the given opset version.
105
+
106
+ Args:
107
+ min_opset: minimum opset version to use
108
+
109
+ The method checks all opset imports and converts the model if any are below the minimum version.
110
+ """
111
+ # Check all opset imports
112
+ default_opsets = list(self.model.opset_import)
113
+
114
+ # Check for quantization nodes and update min_opset if needed
115
+ # Before opset 19, QuantizeLinear and DequantizeLinear data and scale must be fp32
116
+ has_quant_nodes = any(
117
+ node.op_type in ["QuantizeLinear", "DequantizeLinear"] for node in self.model.graph.node
118
+ )
119
+ if has_quant_nodes and self.min_opset < 19:
120
+ logger.warning(
121
+ f"Found QuantizeLinear/DequantizeLinear nodes. Updating minimum opset from {self.min_opset} to 19."
122
+ )
123
+ self.min_opset = 19
124
+
125
+ # Convert if any default domain opset is below minimum
126
+ if any(op.version < self.min_opset for op in default_opsets):
127
+ invalid_opsets = [op.version for op in default_opsets if op.version < self.min_opset]
128
+ try:
129
+ self.model = onnx.version_converter.convert_version(self.model, self.min_opset)
130
+ except Exception as e:
131
+ logger.warning(f"Failed to convert model to opset {self.min_opset}: {e!s}")
132
+ logger.warning(f"Attempting to continue with the original opsets: {invalid_opsets}")
133
+
134
+ def set_ir_version(self, max_ir_version: int | None) -> None:
135
+ """Set the model's IR version to the maximum supported version.
136
+
137
+ Args:
138
+ max_ir_version: maximum IR version to use.
139
+
140
+ The method checks the IR version and cuts it off at the maximum supported version.
141
+ See https://onnxruntime.ai/docs/reference/compatibility.html#onnx-opset-support
142
+ """
143
+ if self.custom_ops:
144
+ # Set ir_version to 10, remove it once ORT supports ir_version 11
145
+ self.max_ir_version = 10
146
+ max_ir_version = max_ir_version or self.max_ir_version
147
+ if max_ir_version and self.model.ir_version > max_ir_version:
148
+ try:
149
+ self.model.ir_version = max_ir_version
150
+ except Exception as e:
151
+ logger.warning(f"Failed to set IR version to {max_ir_version}: {e!s}")
152
+ logger.warning(
153
+ f"Attempting to continue with the original IR version: {self.model.ir_version}"
154
+ )
155
+
156
+ def replace_layernorm_pattern(self) -> None:
157
+ """Detects and replaces LayerNorm operation patterns.
158
+
159
+ This method scans through the graph looking for sequences of operations that implement LayerNorm functionality
160
+ and replaces them with the more efficient LayerNormalization operator.
161
+ """
162
+ nodes_to_remove = []
163
+ modified = False
164
+
165
+ for node in self.model.graph.node:
166
+ if node.op_type != "ReduceMean":
167
+ continue
168
+
169
+ try:
170
+ pattern = self._match_layernorm_pattern(node)
171
+ if not pattern:
172
+ continue
173
+
174
+ ln_node = self._create_layernorm_node(pattern)
175
+ insert_idx = self._find_insertion_point(pattern["input_name"])
176
+
177
+ # Insert LayerNorm node and update graph
178
+ self.model.graph.node.insert(insert_idx, ln_node)
179
+ nodes_to_remove.extend(pattern["nodes_to_remove"])
180
+ modified = True
181
+
182
+ logger.info(f"Replaced LayerNorm pattern starting at {node.name}")
183
+
184
+ except Exception as e:
185
+ logger.debug(f"Failed to match LayerNorm pattern at {node.name}: {e!s}")
186
+ continue
187
+ self._remove_nodes(nodes_to_remove)
188
+
189
+ if modified:
190
+ self._update_opset_version()
191
+ self.model = onnx_utils.infer_shapes(self.model, strict_mode=True)
192
+
193
+ def ensure_graph_name_exists(self) -> None:
194
+ """Ensures that the model's name exists."""
195
+ if not self.model.graph.name:
196
+ self.model.graph.name = "model"
197
+
198
+ def _match_layernorm_pattern(self, mean_node: onnx.NodeProto) -> dict | None:
199
+ """Match the sequence of operations that constitute a LayerNorm.
200
+
201
+ Args:
202
+ mean_node: The ReduceMean node to start pattern matching from.
203
+
204
+ Returns:
205
+ Dict | None: Pattern information if matched, None otherwise.
206
+ """
207
+ try:
208
+ axis = mean_node.attribute[0].ints[0]
209
+ sub_node = utils.get_unique_consumer_node(self.model, mean_node.output[0])
210
+ if sub_node.op_type != "Sub":
211
+ return None
212
+
213
+ # Find variance computation branch
214
+ pow_nodes = [
215
+ n
216
+ for n in utils.get_consumer_nodes(self.model, sub_node.output[0])
217
+ if n.op_type == "Pow"
218
+ ]
219
+ if len(pow_nodes) != 1:
220
+ return None
221
+ pow_node = pow_nodes[0]
222
+ pow_of_value = self._get_initializer_value(pow_node.input[1])
223
+ if pow_of_value != 2:
224
+ return None
225
+
226
+ var_mean_node = utils.get_unique_consumer_node(self.model, pow_node.output[0])
227
+ if var_mean_node.op_type != "ReduceMean":
228
+ return None
229
+
230
+ add_eps_node = utils.get_unique_consumer_node(self.model, var_mean_node.output[0])
231
+ if add_eps_node.op_type != "Add":
232
+ return None
233
+
234
+ sqrt_node = utils.get_unique_consumer_node(self.model, add_eps_node.output[0])
235
+ if sqrt_node.op_type != "Sqrt":
236
+ return None
237
+
238
+ # Find Div node
239
+ # Find the Div node that consumes both sqrt and sub outputs
240
+ sqrt_consumers = utils.get_consumer_nodes(self.model, sqrt_node.output[0])
241
+ sub_consumers = utils.get_consumer_nodes(self.model, sub_node.output[0])
242
+
243
+ div_nodes = [n for n in sqrt_consumers if n in sub_consumers and n.op_type == "Div"]
244
+ if len(div_nodes) != 1:
245
+ div_node = None
246
+ else:
247
+ div_node = div_nodes[0]
248
+ if (
249
+ div_node.input[0] != sub_node.output[0]
250
+ or div_node.input[1] != sqrt_node.output[0]
251
+ ):
252
+ div_node = None
253
+ if not div_node:
254
+ return None
255
+
256
+ # Get epsilon value
257
+ epsilon = self._get_initializer_value(add_eps_node.input[1])
258
+ if epsilon is None:
259
+ logger.warning(
260
+ f"Could not find epsilon value for LayerNorm pattern starting at {mean_node.name}"
261
+ )
262
+ return None
263
+
264
+ # Check for scale/bias
265
+ # Find and extract scale and bias nodes if present
266
+ scale = None
267
+ bias = None
268
+ final_node = div_node
269
+ nodes_to_remove = [
270
+ mean_node,
271
+ sub_node,
272
+ pow_node,
273
+ var_mean_node,
274
+ add_eps_node,
275
+ sqrt_node,
276
+ div_node,
277
+ ]
278
+
279
+ consumers = utils.get_consumer_nodes(self.model, div_node.output[0])
280
+ if len(consumers) == 1 and consumers[0].op_type == "Mul":
281
+ mul_node = consumers[0]
282
+ scale = self._get_initializer_value(mul_node.input[1], return_array=True)
283
+ final_node = mul_node
284
+ nodes_to_remove.append(mul_node)
285
+
286
+ consumers = utils.get_consumer_nodes(self.model, mul_node.output[0])
287
+ if len(consumers) == 1 and consumers[0].op_type == "Add":
288
+ add_node = consumers[0]
289
+ bias = self._get_initializer_value(add_node.input[1], return_array=True)
290
+ final_node = add_node
291
+ nodes_to_remove.append(add_node)
292
+ elif len(consumers) == 1 and consumers[0].op_type == "Add":
293
+ # just bias, no scale
294
+ add_node = consumers[0]
295
+ bias = self._get_initializer_value(add_node.input[1], return_array=True)
296
+ final_node = add_node
297
+ nodes_to_remove.append(add_node)
298
+
299
+ if scale is not None:
300
+ scale_dimension = scale.shape
301
+
302
+ # Skip pattern if we can't determine the scale dimension
303
+ if scale_dimension is None:
304
+ logger.debug(
305
+ f"Could not determine scale dimension for LayerNorm pattern at {mean_node.name}"
306
+ )
307
+ return None
308
+
309
+ return {
310
+ "mean_node": mean_node,
311
+ "input_name": sub_node.input[0],
312
+ "scale_dimension": scale_dimension,
313
+ "output_name": final_node.output[0],
314
+ "epsilon": epsilon,
315
+ "scale": scale,
316
+ "bias": bias,
317
+ "axis": axis,
318
+ "nodes_to_remove": nodes_to_remove,
319
+ }
320
+
321
+ except Exception as e:
322
+ logger.debug(f"Failed to match LayerNorm pattern at {mean_node.name}: {e!s}")
323
+ return None
324
+
325
+ def _create_layernorm_node(self, pattern: dict) -> onnx.NodeProto:
326
+ """Create a LayerNormalization node with optional bias."""
327
+ ln_name = f"LayerNorm_{pattern['mean_node'].name}"
328
+ scale_name = f"{ln_name}_scale"
329
+ bias_name = f"{ln_name}_bias" if pattern["bias"] is not None else ""
330
+ axis = pattern["axis"]
331
+
332
+ # Always create scale tensor, default to ones if not provided
333
+ scale_tensor = (
334
+ pattern["scale"]
335
+ if pattern["scale"] is not None
336
+ else np.ones(pattern["scale_dimension"], dtype=np.float32)
337
+ )
338
+ self.model.graph.initializer.append(numpy_helper.from_array(scale_tensor, name=scale_name))
339
+
340
+ if pattern["bias"] is not None:
341
+ self.model.graph.initializer.append(
342
+ numpy_helper.from_array(pattern["bias"], name=bias_name)
343
+ )
344
+
345
+ inputs = [pattern["input_name"], scale_name]
346
+ if pattern["bias"] is not None:
347
+ inputs.append(bias_name)
348
+
349
+ return helper.make_node(
350
+ "LayerNormalization",
351
+ inputs=inputs,
352
+ outputs=[pattern["output_name"]],
353
+ name=ln_name,
354
+ epsilon=pattern["epsilon"],
355
+ axis=axis,
356
+ )
357
+
358
+ def _find_insertion_point(self, input_name: str) -> int:
359
+ """Find the correct insertion point for the new LayerNorm node."""
360
+ producer_nodes = utils.get_producer_nodes(self.model, input_name)
361
+ if not producer_nodes:
362
+ return 0
363
+
364
+ producer_indices = [i for i, n in enumerate(self.model.graph.node) if n in producer_nodes]
365
+ return max(producer_indices) + 1
366
+
367
+ def _remove_nodes(self, nodes_to_remove: list[onnx.NodeProto]) -> None:
368
+ """Remove replaced nodes and their corresponding value_info entries."""
369
+ tensors_to_remove = set()
370
+ for node in nodes_to_remove:
371
+ tensors_to_remove.update(node.output)
372
+ if node in self.model.graph.node:
373
+ self.model.graph.node.remove(node)
374
+
375
+ value_info_to_remove = [
376
+ vi for vi in self.model.graph.value_info if vi.name in tensors_to_remove
377
+ ]
378
+ for vi in value_info_to_remove:
379
+ self.model.graph.value_info.remove(vi)
380
+
381
+ def _update_opset_version(self) -> None:
382
+ """Update the model's opset version to support LayerNormalization."""
383
+ current_opset = None
384
+ for opset in self.model.opset_import:
385
+ if opset.domain in {"", "ai.onnx"}:
386
+ current_opset = opset.version
387
+ break
388
+
389
+ if current_opset is None or current_opset < 17:
390
+ # Remove existing default domain opsets
391
+ default_opsets = [op for op in self.model.opset_import if op.domain in ("", "ai.onnx")]
392
+ for op in default_opsets:
393
+ self.model.opset_import.remove(op)
394
+ # Add opset 17
395
+ self.model.opset_import.append(helper.make_opsetid("", 17))
396
+ logger.info(
397
+ f"Updated model opset to 17 for LayerNormalization support (was {current_opset})"
398
+ )
399
+
400
+ def _get_initializer_value(self, name: str, return_array: bool = False) -> np.ndarray | None:
401
+ """Get value from an initializer by name."""
402
+ for init in self.model.graph.initializer:
403
+ if init.name == name:
404
+ value = numpy_helper.to_array(init)
405
+ return value if return_array else value.item()
406
+ return None
407
+
408
+ def cleanup_model(self) -> None:
409
+ """Use GraphSurgeon to cleanup unused nodes, tensors and initializers."""
410
+ gs_graph = gs.import_onnx(self.model)
411
+ gs_graph.cleanup()
412
+ self.model = gs.export_onnx(gs_graph)
413
+
414
+ def replace_custom_domain_nodes(self):
415
+ """Replace custom domain nodes with standard ONNX nodes."""
416
+ for node in self.model.graph.node:
417
+ if node.domain.startswith("com.microsoft") and node.op_type in self.standard_ops:
418
+ logger.warning(
419
+ f"Replacing custom domain node {node.name}, domain {node.domain} with standard ONNX "
420
+ f"node {node.op_type}"
421
+ )
422
+ node.domain = ""