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,1032 @@
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
+ """Precision conversion module for ONNX models.
17
+
18
+ This module provides functionality for converting ONNX models between different floating point
19
+ precisions, specifically handling conversions between FP32 and lower precisions like FP16 or BF16.
20
+ It handles the insertion of cast operations, conversion of initializers, and ensures model validity
21
+ through type checking and cleanup of redundant operations.
22
+ """
23
+
24
+ from collections import namedtuple
25
+ from copy import deepcopy
26
+
27
+ import ml_dtypes
28
+ import numpy as np
29
+ import onnx
30
+ import onnx_graphsurgeon as gs
31
+ from onnx import TensorProto, helper, numpy_helper
32
+
33
+ import modelopt.onnx.autocast.utils as utils
34
+ import modelopt.onnx.utils as onnx_utils
35
+ from modelopt.onnx.autocast.logging_config import configure_logging, logger
36
+
37
+ configure_logging()
38
+
39
+ PrecisionTypes = namedtuple("PrecisionTypes", ["onnx_type", "numpy_type", "str_short", "str_full"])
40
+
41
+ PRECISION_MAP = {
42
+ "fp32": PrecisionTypes(TensorProto.FLOAT, np.float32, "fp32", "float32"),
43
+ "fp16": PrecisionTypes(TensorProto.FLOAT16, np.float16, "fp16", "float16"),
44
+ "bf16": PrecisionTypes(TensorProto.BFLOAT16, None, "bf16", "bfloat16"),
45
+ }
46
+
47
+ ONNX_TYPES = [t.onnx_type for t in PRECISION_MAP.values()]
48
+
49
+ OP_TYPES_NOT_SUPPORTED_IN_LOW_PRECISION = ["Resize", "Upsample", "NonMaxSuppression", "Celu"]
50
+
51
+ # Temporarily block these ops in low precision, as they are not supported yet
52
+ OP_TYPES_NOT_SUPPORTED_IN_LOW_PRECISION.extend(["Scan", "If", "Loop", "LSTM"])
53
+
54
+
55
+ class PrecisionConverter:
56
+ """Precision conversion module for ONNX models.
57
+
58
+ This module provides functionality for converting ONNX models between different floating point
59
+ precisions, specifically handling conversions between FP32 and lower precisions like FP16 or BF16.
60
+ It handles the insertion of cast operations, conversion of initializers, and ensures model validity.
61
+
62
+ Public Methods:
63
+ convert: Convert specified nodes to FP16/BF16 precision while keeping others in FP32.
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ model: onnx.ModelProto,
69
+ value_info_map: dict[str, onnx.ValueInfoProto],
70
+ initializer_map: dict[str, onnx.TensorProto],
71
+ node_to_init_map: dict[str, list[str]],
72
+ keep_io_types: bool = False,
73
+ low_precision_type: str = "fp16",
74
+ init_conversion_max_bytes: int | None = None,
75
+ custom_ops: set[str] | None = None,
76
+ ) -> None:
77
+ """Initialize PrecisionConverter.
78
+
79
+ Args:
80
+ model: ONNX model to convert.
81
+ value_info_map: Map of tensor names to value info.
82
+ initializer_map: Map of tensor names to initializers.
83
+ node_to_init_map: Map of node names to lists of initializer names.
84
+ keep_io_types: Keep the input and output types of the model, otherwise they will be converted.
85
+ low_precision_type: Precision to convert to.
86
+ init_conversion_max_bytes: Maximum size in bytes for initializer conversion. Larger initializers will be
87
+ cast at runtime.
88
+ custom_ops: List of custom ops.
89
+ """
90
+ self.model = deepcopy(model)
91
+ self.value_info_map = value_info_map
92
+ self.initializer_map = initializer_map
93
+ self.node_to_init_map = node_to_init_map
94
+ self.keep_io_types = keep_io_types
95
+ self.init_conversion_max_bytes = (
96
+ np.inf if init_conversion_max_bytes is None else init_conversion_max_bytes
97
+ )
98
+ self.custom_ops = custom_ops
99
+ if low_precision_type not in ["fp16", "bf16"]:
100
+ raise ValueError(f"Unsupported precision type: {low_precision_type}")
101
+
102
+ self.low_precision_type = PRECISION_MAP[low_precision_type]
103
+ self.high_precision_type = PRECISION_MAP["fp32"]
104
+
105
+ # Preserve original network inputs and outputs for sanity checks
106
+ self.original_network_io = {
107
+ io.name: io.type.tensor_type.elem_type for io in self.model.graph.input
108
+ }
109
+ self.original_network_io.update(
110
+ {io.name: io.type.tensor_type.elem_type for io in self.model.graph.output}
111
+ )
112
+
113
+ def convert(
114
+ self,
115
+ high_precision_nodes: list[str],
116
+ low_precision_nodes: list[str],
117
+ ) -> onnx.ModelProto:
118
+ """Convert model to mixed precision.
119
+
120
+ Args:
121
+ high_precision_nodes: List of node names to keep in high precision.
122
+ low_precision_nodes: List of node names to convert to low precision.
123
+
124
+ Returns:
125
+ onnx.ModelProto: The converted mixed precision model.
126
+ """
127
+ try:
128
+ self.model = onnx_utils.check_model(self.model)
129
+ except onnx.checker.ValidationError as e:
130
+ logger.error(f"Internal error: onnx.checker failed on input model {e}")
131
+ raise Exception(
132
+ "AutoCast can only operate on valid ONNX models, but the input model is invalid. See log for details."
133
+ )
134
+
135
+ # Filter out nodes that are not allowed to be in low precision
136
+ # This is done here and not in NodeClassifier because it is required for the model to be valid
137
+ high_precision_nodes, low_precision_nodes = self._filter_unsupported_op_types(
138
+ high_precision_nodes, low_precision_nodes
139
+ )
140
+
141
+ # We remove any existing casts to FP16/BF16/FP32, as we will be adding our own
142
+ self._remove_preexisting_casts()
143
+
144
+ # Convert inputs to reduced precision type
145
+ if not self.keep_io_types:
146
+ for input in self.model.graph.input:
147
+ if input.type.tensor_type.elem_type == self.high_precision_type.onnx_type:
148
+ input.type.tensor_type.elem_type = self.low_precision_type.onnx_type
149
+
150
+ cast_down_tensors, cast_up_tensors = self._get_tensors_to_cast(low_precision_nodes)
151
+ logger.debug(f"cast down (to {self.low_precision_type.str_full}): {cast_down_tensors}")
152
+ logger.debug(f"cast up (to {self.high_precision_type.str_full}): {cast_up_tensors}")
153
+
154
+ # Add cast nodes for "cast_up" tensors
155
+ for tensor_name in cast_up_tensors:
156
+ self._add_cast(
157
+ tensor_name, self.high_precision_type, exclude_consumers=low_precision_nodes
158
+ )
159
+
160
+ # Add cast nodes for "cast_down" tensors
161
+ for tensor_name in cast_down_tensors:
162
+ self._add_cast(
163
+ tensor_name,
164
+ self.low_precision_type,
165
+ exclude_consumers=high_precision_nodes,
166
+ )
167
+
168
+ # Convert initializers to correct precision according to the consumer nodes
169
+ self._convert_initializers(
170
+ low_precision_nodes=low_precision_nodes, high_precision_nodes=high_precision_nodes
171
+ )
172
+
173
+ # Infer data types (and shapes), propagating the changes we made from graph inputs to outputs
174
+ if self.custom_ops:
175
+ # Populate type information with inferred types
176
+ self.model = self._propagate_types_shapes_custom_ops(self.model)
177
+ else:
178
+ # Clear type/shape information for intermediates and outputs
179
+ for vi in self.model.graph.value_info:
180
+ vi.type.tensor_type.elem_type = onnx.TensorProto.UNDEFINED
181
+ for idx, d in enumerate(vi.type.tensor_type.shape.dim):
182
+ vi.type.tensor_type.shape.dim[idx].dim_param = "unk"
183
+ for out in self.model.graph.output:
184
+ out.type.tensor_type.elem_type = onnx.TensorProto.UNDEFINED
185
+ for idx, d in enumerate(out.type.tensor_type.shape.dim):
186
+ out.type.tensor_type.shape.dim[idx].dim_param = "unk"
187
+ # Populate type information with inferred types
188
+ self.model = onnx_utils.infer_shapes(self.model, strict_mode=True, check_type=False)
189
+ # Sanity check: Verify type correctness
190
+ self.model = onnx_utils.infer_shapes(self.model, strict_mode=True, check_type=True)
191
+
192
+ # Update value_info_map and initializer_map with casts we added
193
+ self.value_info_map, self.initializer_map, self.node_to_init_map = utils.setup_mappings(
194
+ self.model
195
+ )
196
+
197
+ # Remove redundant casts
198
+ self._cleanup()
199
+
200
+ self._sanity_check()
201
+
202
+ return self.model
203
+
204
+ def _propagate_types_shapes_custom_ops(self, model):
205
+ """Propagate types and shapes after insertion of 'Cast' nodes or other graph modifications."""
206
+ logger.info("Propagating tensor shapes and types in model with custom ops.")
207
+ graph = gs.import_onnx(model)
208
+ traversed_tensors = []
209
+
210
+ def _get_np_type(node, inp, opset=onnx.defs.onnx_opset_version()):
211
+ if node.op == "Cast":
212
+ return helper.tensor_dtype_to_np_dtype(node.attrs["to"])
213
+ elif node.op == "DequantizeLinear":
214
+ return node.inputs[1].dtype # scale type
215
+ elif not inp.dtype or inp.dtype == onnx.TensorProto.UNDEFINED:
216
+ return None
217
+ elif node.op not in self.custom_ops:
218
+ op_schema = onnx.defs.get_schema(node.op, opset)
219
+ out_types = list(op_schema.outputs[0].types)
220
+ inp_type = f"tensor({'float' if inp.dtype == 'float32' else inp.dtype})"
221
+ return (
222
+ inp.dtype
223
+ if inp_type in out_types
224
+ else helper.tensor_dtype_to_np_dtype(
225
+ onnx_utils.onnx_type_str_to_enum(out_types[0])
226
+ )
227
+ )
228
+ return None
229
+
230
+ def _can_propagate_type(from_type, to_type):
231
+ try:
232
+ from_type_onnx = helper.np_dtype_to_tensor_dtype(from_type)
233
+ to_type_onnx = helper.np_dtype_to_tensor_dtype(to_type)
234
+ return (
235
+ from_type_onnx in [*ONNX_TYPES, onnx.TensorProto.UNDEFINED]
236
+ and to_type_onnx in ONNX_TYPES
237
+ )
238
+ except Exception as e:
239
+ logger.warning(f"Failed to check if type can be propagated: {e}")
240
+ return False
241
+
242
+ def _propagate_cast_type_through_nodes(node, np_type, iter=1):
243
+ # Return if node is of cast type (from iter=2)
244
+ indent = " " * iter
245
+ if iter > 1 and any(op in node.op.lower() for op in ["cast"]):
246
+ return
247
+
248
+ out = node.outputs[0]
249
+ # Return if there's no consumer node
250
+ is_graph_output_tensor = any(out.name == n.name for n in graph.outputs)
251
+ if is_graph_output_tensor or not out.outputs:
252
+ out.dtype = np_type
253
+ logger.debug(f"{indent}Updated type in {out.name} to {np_type}.")
254
+ return
255
+
256
+ # Search children nodes
257
+ for child_node in out.outputs:
258
+ for child_out in child_node.outputs:
259
+ # Continue if the type is already correct
260
+ if child_out.dtype and child_out.dtype == np_type:
261
+ logger.debug(
262
+ f"{indent}Type is already correct in {child_out.name}: {child_out.dtype}. Continue."
263
+ )
264
+ continue
265
+
266
+ # Continue if the tensor was already traversed
267
+ if child_out.name in traversed_tensors and all(
268
+ inp.name in traversed_tensors for inp in child_node.inputs
269
+ ):
270
+ logger.debug(
271
+ f"{indent}Tensor {child_out.name} of shape {child_out.shape} and type {child_out.dtype} "
272
+ f"was already traversed. Continue."
273
+ )
274
+ return
275
+ if child_out.dtype and child_out.dtype != onnx.TensorProto.UNDEFINED:
276
+ traversed_tensors.append(child_out.name)
277
+
278
+ # Update tensor type if the types are supported
279
+ if child_out.dtype:
280
+ if _can_propagate_type(child_out.dtype, np_type):
281
+ child_out.dtype = np_type
282
+ logger.debug(
283
+ f"{indent}Updated type in {child_out.name} from {child_out.dtype} to {np_type}."
284
+ )
285
+ elif helper.np_dtype_to_tensor_dtype(np_type) in ONNX_TYPES:
286
+ child_out.dtype = np_type
287
+ logger.debug(
288
+ f"{indent}Updated type in {child_out.name} from 'None' to {np_type}."
289
+ )
290
+
291
+ # Propagate types to the next node
292
+ if child_out.outputs:
293
+ _propagate_cast_type_through_nodes(child_node, np_type, iter=iter + 1)
294
+ return
295
+
296
+ # Propagate tensor types and shapes for all layers in the graph
297
+ for node in graph.nodes:
298
+ # Get input and type information
299
+ if not (inp := (node.inputs[0] if node.inputs else None)):
300
+ continue
301
+ if not (np_type := _get_np_type(node, inp)):
302
+ continue
303
+
304
+ # Propagate tensor types to outputs
305
+ for out in node.outputs:
306
+ # Update the output type if relevant
307
+ if not out.dtype or _can_propagate_type(out.dtype, np_type):
308
+ out.dtype = np_type
309
+
310
+ # Set the output shape
311
+ if not out.shape:
312
+ if isinstance(inp, gs.Constant):
313
+ out.shape = inp.values.shape
314
+ elif inp.inputs and inp.inputs[0].op == "Constant":
315
+ out.shape = inp.inputs[0].attrs["value"].values.shape
316
+ elif inp.shape:
317
+ out.shape = inp.shape
318
+
319
+ # Propagate tensor types to the children nodes (until another Cast or Q node is met)
320
+ _propagate_cast_type_through_nodes(node, np_type)
321
+
322
+ return gs.export_onnx(graph)
323
+
324
+ def _is_bf16(self, type: PrecisionTypes = None) -> bool:
325
+ if type is None:
326
+ type = self.low_precision_type
327
+ return type.onnx_type == onnx.TensorProto.BFLOAT16
328
+
329
+ def _is_fp16(self, type: PrecisionTypes = None) -> bool:
330
+ if type is None:
331
+ type = self.low_precision_type
332
+ return type.onnx_type == onnx.TensorProto.FLOAT16
333
+
334
+ def _is_fp32(self, type: PrecisionTypes = None) -> bool:
335
+ if type is None:
336
+ type = self.high_precision_type
337
+ return type.onnx_type == onnx.TensorProto.FLOAT
338
+
339
+ def _get_node_initializers_map(self) -> dict[str, list[str]]:
340
+ """Creates a mapping from node names to lists of initializer names used as inputs by that node.
341
+
342
+ Returns:
343
+ dict[str, list[str]]: Mapping from node names to lists of initializer names.
344
+ """
345
+ node_to_initializers = {}
346
+ for node in self.model.graph.node:
347
+ initializer_inputs = [
348
+ self.initializer_map[input_name]
349
+ for input_name in node.input
350
+ if input_name in self.initializer_map
351
+ ]
352
+ node_to_initializers[node.name] = initializer_inputs
353
+ return node_to_initializers
354
+
355
+ def _is_castable_tensor(self, tensor_name: str) -> bool:
356
+ if tensor_name in self.value_info_map:
357
+ return self.value_info_map[tensor_name].type.tensor_type.elem_type in ONNX_TYPES
358
+ elif tensor_name in self.initializer_map:
359
+ return self.initializer_map[tensor_name].data_type in ONNX_TYPES
360
+ else:
361
+ logger.warning(f"Did not find {tensor_name} in value info map! Assuming not castable")
362
+ return False
363
+
364
+ def _is_empty_tensor(self, tensor_name: str) -> bool:
365
+ if tensor_name in self.value_info_map:
366
+ tensor_info = self.value_info_map[tensor_name]
367
+ for dim in tensor_info.type.tensor_type.shape.dim:
368
+ if (dim.HasField("dim_value") and dim.dim_value == 0) or (
369
+ dim.HasField("dim_param") and dim.dim_param == "0"
370
+ ):
371
+ return True
372
+ return False
373
+
374
+ def _filter_unsupported_op_types(
375
+ self, high_precision_nodes: list[str], low_precision_nodes: list[str]
376
+ ) -> tuple[list[str], list[str]]:
377
+ # NonMaxSuppression and Celu require FP32 inputs per ONNX standard
378
+ # Resize and Upsample allow the data input (index 0) to be FP16/BF16 per ONNX standard, but require the scale
379
+ # input (index 1) to be FP32. However, AutoCast requires a binary classification for each node: high/low
380
+ # precision so we need to set Resize and Upsample to high precision
381
+ for node in self.model.graph.node:
382
+ if (
383
+ node.op_type in OP_TYPES_NOT_SUPPORTED_IN_LOW_PRECISION
384
+ and node.name in low_precision_nodes
385
+ ):
386
+ low_precision_nodes.remove(node.name)
387
+ high_precision_nodes.append(node.name)
388
+ logger.debug(
389
+ f"Node {node.name} (op type: {node.op_type}) is not supported in low precision, moving"
390
+ " to high precision"
391
+ )
392
+ return high_precision_nodes, low_precision_nodes
393
+
394
+ def _get_tensors_to_cast(self, low_precision_nodes: list[str]) -> tuple[list[str], list[str]]:
395
+ cast_to_fp16 = [] # Tensors to cast down to FP16
396
+ cast_to_fp32 = [] # Tensors to cast up to FP32
397
+
398
+ # Get tensors for FP16 nodes
399
+ for node in self.model.graph.node:
400
+ if node.name in low_precision_nodes:
401
+ # Cast inputs to FP16 nodes down to FP16
402
+ cast_to_fp16.extend(node.input)
403
+ # Cast outputs from FP16 nodes up to FP32
404
+ cast_to_fp32.extend(node.output)
405
+
406
+ # Handle consumers and producers of network inputs and outputs
407
+ high_precision_nodes = [
408
+ node for node in self.model.graph.node if node.name not in low_precision_nodes
409
+ ]
410
+ network_inputs = [input.name for input in self.model.graph.input]
411
+ network_outputs = [output.name for output in self.model.graph.output]
412
+ for node in high_precision_nodes:
413
+ # Add cast up for network inputs
414
+ cast_to_fp32.extend([input for input in node.input if input in network_inputs])
415
+ # Add cast down for network outputs
416
+ cast_to_fp16.extend([output for output in node.output if output in network_outputs])
417
+
418
+ # Remove initializers, they are handled separately
419
+ initializers = {init.name for init in self.model.graph.initializer}
420
+ cast_to_fp16 = list(set(cast_to_fp16) - initializers)
421
+ cast_to_fp32 = list(set(cast_to_fp32) - initializers)
422
+
423
+ # Filter out non-float tensors
424
+ cast_to_fp16 = [t for t in cast_to_fp16 if self._is_castable_tensor(t)]
425
+ cast_to_fp32 = [t for t in cast_to_fp32 if self._is_castable_tensor(t)]
426
+
427
+ logger.debug(f"tensors to cast to FP16: {cast_to_fp16}")
428
+ logger.debug(f"tensors to cast to FP32: {cast_to_fp32}")
429
+ return cast_to_fp16, cast_to_fp32
430
+
431
+ def _convert_initializers(
432
+ self, low_precision_nodes: list[str], high_precision_nodes: list[str]
433
+ ) -> onnx.ModelProto:
434
+ def convert_initializer(
435
+ init: onnx.TensorProto,
436
+ node: onnx.NodeProto,
437
+ from_type: PrecisionTypes,
438
+ to_type: PrecisionTypes,
439
+ ):
440
+ if init.data_type != from_type.onnx_type:
441
+ logger.debug(
442
+ f"Initializer {init.name} has data type {init.data_type}, and size {len(init.raw_data)},"
443
+ "skipping conversion"
444
+ )
445
+ return False
446
+
447
+ # If initializer is too large, skip conversion, perform cast instead
448
+ if init.raw_data and len(init.raw_data) > self.init_conversion_max_bytes:
449
+ logger.debug(
450
+ f"Initializer {init.name} is too large, skipping initializer conversion, cast in "
451
+ "runtime instead"
452
+ )
453
+ exclude_consumers = (
454
+ low_precision_nodes if self._is_fp32(to_type) else high_precision_nodes
455
+ )
456
+ self._add_cast(init.name, to_type, exclude_consumers=exclude_consumers)
457
+ return True
458
+ try:
459
+ np_array = numpy_helper.to_array(init)
460
+ assert from_type.str_short in PRECISION_MAP
461
+ assert to_type.str_short in PRECISION_MAP
462
+ assert from_type.str_short != to_type.str_short
463
+
464
+ if np_array.dtype == from_type.numpy_type:
465
+ consumers = [n.name for n in utils.get_consumer_nodes(self.model, init.name)]
466
+ should_duplicate = len(consumers) > 1 and set(consumers) & set(
467
+ high_precision_nodes
468
+ )
469
+
470
+ if should_duplicate:
471
+ # Create a new low precision copy with a different name
472
+ new_name = f"{init.name}_{to_type.str_short}"
473
+ logger.debug(
474
+ f"Initializer {init.name} is shared, creating {to_type.str_short} copy as {new_name} due "
475
+ f"to node {node.name}"
476
+ )
477
+
478
+ # Update the node to use the new initializer
479
+ for i, input_name in enumerate(node.input):
480
+ if input_name == init.name:
481
+ node.input[i] = new_name
482
+ break
483
+
484
+ if init.name in initializer_converted_dup:
485
+ return False
486
+ initializer_converted_dup.append(init.name)
487
+ else:
488
+ if init.name in initializer_converted:
489
+ return False
490
+ new_name = init.name
491
+ logger.debug(
492
+ f"Converting initializer {new_name} to {to_type.str_short} due to node {node.name}"
493
+ )
494
+ initializer_converted.append(init.name)
495
+ self.model.graph.initializer.remove(init)
496
+
497
+ # Numpy does not support bfloat16, use ml_dtypes to create the raw data instead
498
+ if self._is_bf16(to_type) and self._is_fp32(from_type):
499
+ new_init = onnx.TensorProto()
500
+ new_init.dims.extend(np_array.shape)
501
+ new_init.name = new_name
502
+ new_init.data_type = onnx.TensorProto.BFLOAT16
503
+ bf16_bytes = np_array.astype(ml_dtypes.bfloat16).view(np.uint16)
504
+ new_init.raw_data = bf16_bytes.tobytes()
505
+ else:
506
+ assert to_type.numpy_type is not None
507
+ data_max, data_lowest = (
508
+ np.finfo(to_type.numpy_type).max,
509
+ np.finfo(to_type.numpy_type).smallest_subnormal,
510
+ )
511
+ if np.any(np.abs(np_array) > data_max):
512
+ logger.warning(
513
+ f"Initializer {init.name} used by node {node.name} contains values larger than "
514
+ f"largest {to_type.str_short} value, values will be clamped to {data_max}."
515
+ )
516
+ np_array = np.clip(np_array, -1 * data_max, data_max)
517
+ if np.any((np_array != 0.0) & (np.abs(np_array) < data_lowest)):
518
+ logger.warning(
519
+ f"Initializer {init.name} used by node {node.name} contains values smaller than "
520
+ f"smallest {to_type.str_short} value, values will be replaced with {data_lowest:.1e}."
521
+ )
522
+ np_array = np.where(
523
+ (np_array != 0.0) & (np.abs(np_array) < data_lowest),
524
+ data_lowest,
525
+ np_array,
526
+ )
527
+ new_array = np_array.astype(to_type.numpy_type)
528
+ new_init = numpy_helper.from_array(new_array, new_name)
529
+ self.model.graph.initializer.extend([new_init])
530
+ return True
531
+ return False
532
+ except Exception as e:
533
+ logger.error(f"Error converting initializer {init.name}: {e}")
534
+ return False
535
+
536
+ initializer_converted = []
537
+ initializer_converted_dup = []
538
+ modified = False
539
+ for node in self.model.graph.node:
540
+ if node.name in low_precision_nodes:
541
+ for init in self.node_to_init_map[node.name]:
542
+ modified |= convert_initializer(
543
+ init,
544
+ node,
545
+ from_type=self.high_precision_type,
546
+ to_type=self.low_precision_type,
547
+ )
548
+ if modified:
549
+ _, _, self.node_to_init_map = utils.setup_mappings(self.model)
550
+
551
+ if node.name in high_precision_nodes:
552
+ for init in self.node_to_init_map[node.name]:
553
+ convert_initializer(
554
+ init,
555
+ node,
556
+ from_type=self.low_precision_type,
557
+ to_type=self.high_precision_type,
558
+ )
559
+
560
+ def _bypass_cast_node(self, node: onnx.NodeProto) -> None:
561
+ # handling only a single input and output, as we only remove cast nodes
562
+ assert len(node.input) == 1
563
+ assert len(node.output) == 1
564
+
565
+ input_tensor = node.input[0]
566
+ output_tensor = node.output[0]
567
+ is_output_producer = False
568
+
569
+ # If removed cast node is producing a network output, we need to update the node producing the cast
570
+ # Network output name should not be changed
571
+ for output in self.model.graph.output:
572
+ if output.name == output_tensor:
573
+ is_output_producer = True
574
+ producers = utils.get_producer_nodes(self.model, input_tensor)
575
+ for producer in producers:
576
+ for i, prod_out in enumerate(producer.output):
577
+ if prod_out == input_tensor:
578
+ producer.output[i] = output_tensor
579
+ if (
580
+ not is_output_producer
581
+ ): # Reconnect consumers of the cast output to use the cast input instead
582
+ consumers = utils.get_consumer_nodes(self.model, output_tensor)
583
+ for consumer in consumers:
584
+ for i, input_name in enumerate(consumer.input):
585
+ if input_name == output_tensor:
586
+ consumer.input[i] = input_tensor
587
+
588
+ def _remove_preexisting_casts(self) -> None:
589
+ nodes_to_remove = []
590
+ for node in self.model.graph.node:
591
+ if node.op_type == "Cast":
592
+ cast_from_type = self._get_tensor_type(node.input[0])
593
+ cast_to_type = utils.get_cast_to_type(node)
594
+ is_fp_cast = cast_to_type in [
595
+ onnx.TensorProto.FLOAT16,
596
+ onnx.TensorProto.FLOAT,
597
+ ] and cast_from_type in [
598
+ onnx.TensorProto.FLOAT16,
599
+ onnx.TensorProto.FLOAT,
600
+ onnx.TensorProto.BFLOAT16,
601
+ ]
602
+ # Check if input comes from an initializer - don't remove cast in that case
603
+ input_from_initializer = node.input[0] in {
604
+ init.name for init in self.model.graph.initializer
605
+ }
606
+ if is_fp_cast and not input_from_initializer:
607
+ # Keep cast nodes that are necessary producers of network outputs
608
+ if any(node.input[0] == out.name for out in self.model.graph.output) and any(
609
+ node.output[0] == out.name for out in self.model.graph.output
610
+ ):
611
+ continue
612
+ nodes_to_remove.append(node)
613
+ self._bypass_cast_node(node)
614
+ logger.debug(f"Removing {len(nodes_to_remove)} pre-existing casts")
615
+
616
+ for node in nodes_to_remove:
617
+ self.model.graph.node.remove(node)
618
+
619
+ def _add_cast(
620
+ self, tensor_name: str, cast_to: PrecisionTypes, exclude_consumers: list[str] = []
621
+ ) -> None:
622
+ """Adds a cast operation on a tensor and reconnects its consumers.
623
+
624
+ Args:
625
+ tensor_name: Name of the tensor to cast.
626
+ cast_to: Target precision type to cast to.
627
+ exclude_consumers: List of consumer nodes to exclude from reconnection.
628
+ """
629
+ # Empty tensors may have special handling in ONNX (e.g. for Resize scales) which can break when redundant casts
630
+ # are injected. Since there's no data, it's safe to only update the metadata.
631
+ if self._is_empty_tensor(tensor_name):
632
+ logger.debug(f"Fake-casting empty tensor: {tensor_name}")
633
+ if tensor_name in self.value_info_map:
634
+ tensor_info = self.value_info_map[tensor_name]
635
+ tensor_info.type.tensor_type.elem_type = cast_to.onnx_type
636
+ # Update the corresponding value_info in the model graph
637
+ for vi in self.model.graph.value_info:
638
+ if vi.name == tensor_name:
639
+ vi.type.tensor_type.elem_type = cast_to.onnx_type
640
+ break
641
+
642
+ # Also check if tensor is output of a Constant node and update its value attribute
643
+ for node in self.model.graph.node:
644
+ if node.op_type == "Constant" and tensor_name in node.output:
645
+ logger.debug(f"Found {tensor_name} as output of Constant node {node.name}")
646
+ for attr in node.attribute:
647
+ if attr.name == "value" and attr.type == onnx.AttributeProto.TENSOR:
648
+ attr.t.data_type = cast_to.onnx_type
649
+ break
650
+ break
651
+ else:
652
+ logger.error(f"Failed to fake-cast empty tensor: {tensor_name} not found.")
653
+ return
654
+
655
+ cast_output_name = f"{tensor_name}_cast_to_{cast_to.str_short}"
656
+
657
+ cast_node = helper.make_node(
658
+ "Cast",
659
+ inputs=[tensor_name],
660
+ outputs=[cast_output_name],
661
+ to=cast_to.onnx_type,
662
+ name=f"{tensor_name}_cast_to_{cast_to.str_short}",
663
+ )
664
+
665
+ consumer_nodes = utils.get_consumer_nodes(self.model, tensor_name)
666
+ consumer_nodes = [n for n in consumer_nodes if n.name not in exclude_consumers]
667
+ for node in consumer_nodes:
668
+ for i, input_name in enumerate(node.input):
669
+ if input_name == tensor_name:
670
+ node.input[i] = cast_output_name
671
+
672
+ # Update network output
673
+ for output in self.model.graph.output:
674
+ if output.name == tensor_name and (
675
+ (self.keep_io_types and cast_to.onnx_type == output.type.tensor_type.elem_type)
676
+ or (
677
+ not self.keep_io_types
678
+ and cast_to.onnx_type == self.low_precision_type.onnx_type
679
+ )
680
+ ):
681
+ output.name = cast_output_name
682
+ break
683
+
684
+ # Find producer node to insert cast after it
685
+ producer_nodes = utils.get_producer_nodes(self.model, tensor_name)
686
+ if producer_nodes:
687
+ # Insert after the producer node
688
+ # Find index by iterating since RepeatedCompositeContainer doesn't support index()
689
+ producer_idx = -1
690
+ for i, node in enumerate(self.model.graph.node):
691
+ if node == producer_nodes[0]:
692
+ producer_idx = i
693
+ break
694
+ self.model.graph.node.insert(producer_idx + 1, cast_node)
695
+ else:
696
+ # If no producer (e.g. network input), insert at beginning
697
+ self.model.graph.node.insert(0, cast_node)
698
+
699
+ logger.debug(f"Inject cast to {cast_to.str_full} on {tensor_name}")
700
+
701
+ def _cleanup(self):
702
+ # Cleanup dead-end cast nodes
703
+ self._cleanup_no_consumer_nodes()
704
+
705
+ # Cleanup double same-type cast nodes that produce network outputs before calling _fix_network_output_names
706
+ # This is necessary because fix_network_output_names only handles one level of cast nodes
707
+ self._cleanup_pre_output_same_type_cast()
708
+
709
+ # Restores the original output names, must execute before removing cast nodes, otherwise
710
+ # the nodes generating the outputs might be removed
711
+ self._fix_network_output_names()
712
+
713
+ # Remove redundant casts
714
+ self._remove_redundant_casts()
715
+
716
+ def _cleanup_no_consumer_nodes(self):
717
+ network_outputs = {o.name for o in self.model.graph.output}
718
+ nodes_to_remove = [
719
+ node
720
+ for node in self.model.graph.node
721
+ if not any(
722
+ out in network_outputs or utils.get_consumer_nodes(self.model, out)
723
+ for out in node.output
724
+ )
725
+ ]
726
+ for node in nodes_to_remove:
727
+ # We only add Cast nodes, other nodes with no consumers originate from the original model
728
+ if node.op_type != "Cast":
729
+ logger.debug(
730
+ f"Removing non-cast node with no consumers: {node.name} (type: {node.op_type})"
731
+ )
732
+ self.model.graph.node.remove(node)
733
+
734
+ def _cleanup_pre_output_same_type_cast(self):
735
+ if not self.keep_io_types:
736
+ return
737
+
738
+ for output in self.model.graph.output:
739
+ if "_cast_to_" in output.name:
740
+ out_producer_nodes = utils.get_producer_nodes(self.model, output.name)
741
+ if len(out_producer_nodes) == 1 and out_producer_nodes[0].op_type == "Cast":
742
+ second_cast_node = out_producer_nodes[0]
743
+ cast_producer_nodes = utils.get_producer_nodes(
744
+ self.model, second_cast_node.input[0]
745
+ )
746
+ if len(cast_producer_nodes) == 1 and cast_producer_nodes[0].op_type == "Cast":
747
+ first_cast_node = cast_producer_nodes[0]
748
+ if (
749
+ self._is_same_type_cast(first_cast_node)
750
+ and utils.get_cast_to_type(second_cast_node)
751
+ == self.high_precision_type.onnx_type
752
+ ):
753
+ logger.debug(f"Removing pre-output double cast: {first_cast_node.name}")
754
+ self._bypass_cast_node(first_cast_node)
755
+ self.model.graph.node.remove(first_cast_node)
756
+
757
+ def _is_same_type_cast(self, node: onnx.NodeProto) -> bool:
758
+ assert node.op_type == "Cast"
759
+ input_types = [self._get_tensor_type(inp) for inp in node.input]
760
+ output_type = utils.get_cast_to_type(node)
761
+ return all(inp_type == output_type for inp_type in input_types) and input_types is not None
762
+
763
+ def _is_sequential_cast(self, node: onnx.NodeProto) -> bool:
764
+ assert node.op_type == "Cast"
765
+ output_type = utils.get_cast_to_type(node)
766
+
767
+ # Cast to high precision -> cast to low precision, first cast has no impact and can be safely removed
768
+ # Cast to low precision -> cast to high precision affects precision and should not be removed
769
+ precision_order = [
770
+ TensorProto.DOUBLE,
771
+ TensorProto.FLOAT,
772
+ TensorProto.FLOAT16,
773
+ TensorProto.BFLOAT16,
774
+ ]
775
+ consumers = [
776
+ n for n in utils.get_consumer_nodes(self.model, node.output[0]) if n.op_type == "Cast"
777
+ ]
778
+
779
+ # If the first cast has additional consumers, we should not remove it
780
+ if len(consumers) != 1:
781
+ return False
782
+
783
+ next_node = consumers[0]
784
+ first_cast_type = output_type
785
+ second_cast_type = utils.get_cast_to_type(next_node)
786
+
787
+ return (
788
+ first_cast_type in precision_order
789
+ and second_cast_type in precision_order
790
+ and precision_order.index(first_cast_type) <= precision_order.index(second_cast_type)
791
+ )
792
+
793
+ def _remove_redundant_casts(self):
794
+ """Removes both sequential casts and casts that don't change precision.
795
+
796
+ This method optimizes the graph by removing unnecessary cast operations that either:
797
+ 1. Don't actually change the data type
798
+ 2. Could be replaced by a single cast operation
799
+ """
800
+ if self.custom_ops:
801
+ self.model = self._propagate_types_shapes_custom_ops(self.model)
802
+ else:
803
+ self.model = onnx_utils.infer_shapes(self.model, strict_mode=True)
804
+ self.model = onnx_utils.infer_shapes(self.model, strict_mode=True, check_type=True)
805
+
806
+ nodes_to_remove = []
807
+ for node in self.model.graph.node:
808
+ if node.op_type == "Cast":
809
+ # Find cast nodes that don't change precision
810
+ if self._is_same_type_cast(node):
811
+ nodes_to_remove.append(node)
812
+ self._bypass_cast_node(node)
813
+ logger.debug(f"Found redundant same-type cast: {node.name}")
814
+ continue
815
+
816
+ # Find sequential casts that don't change precision
817
+ if self._is_sequential_cast(node):
818
+ nodes_to_remove.append(node)
819
+ self._bypass_cast_node(node)
820
+ logger.debug(f"Found removable double-cast: {node.name}")
821
+
822
+ # Find foldable Constant -> Cast. Initializers are handled by _convert_initializers.
823
+ if self._is_foldable_constant_cast_pattern(node):
824
+ nodes_to_remove.append(node)
825
+ cast_producers = utils.get_producer_nodes(self.model, node.input[0])
826
+ assert len(cast_producers) == 1 and cast_producers[0].op_type == "Constant"
827
+ constant_producer = cast_producers[0]
828
+ self._convert_constant_values(constant_producer, node)
829
+ self._bypass_cast_node(node)
830
+ logger.debug(f"Found foldable Constant->Cast pattern, removing {node.name}")
831
+
832
+ logger.debug(f"Removing redundant casts: {[n.name for n in nodes_to_remove]}")
833
+ for node in nodes_to_remove:
834
+ self.model.graph.node.remove(node)
835
+
836
+ def _fix_network_output_names(self):
837
+ modified = False
838
+ for output in self.model.graph.output:
839
+ if "_cast_to_" in output.name:
840
+ post_cast_name = output.name
841
+ producer_nodes = utils.get_producer_nodes(self.model, output.name)
842
+ if (
843
+ len(producer_nodes) == 1
844
+ and producer_nodes[0].op_type == "Cast"
845
+ and producer_nodes[0].output[0] == output.name
846
+ ):
847
+ cast_node = producer_nodes[0]
848
+ assert cast_node.op_type == "Cast"
849
+ original_name = cast_node.input[0]
850
+ pre_cast_name = original_name + "_pre_cast"
851
+ output.name = original_name
852
+ # Update all consumers of the original (pre-cast) output to use the pre-cast name
853
+ for node in utils.get_consumer_nodes(self.model, original_name):
854
+ if node == cast_node:
855
+ continue
856
+ for i, input_name in enumerate(node.input):
857
+ if input_name == original_name:
858
+ node.input[i] = pre_cast_name
859
+ # do not break, can use the same tensor for multiple node inputs
860
+ # Update all consumers of the post-cast output to use the original name
861
+ for node in utils.get_consumer_nodes(self.model, post_cast_name):
862
+ for i, input_name in enumerate(node.input):
863
+ if input_name == post_cast_name:
864
+ node.input[i] = original_name
865
+ # do not break, can use the same tensor for multiple node inputs
866
+ # Update all producers of the original output to use the original name
867
+ cast_producer_nodes = utils.get_producer_nodes(self.model, cast_node.input[0])
868
+ for node in cast_producer_nodes:
869
+ for i, node_output in enumerate(node.output):
870
+ if node_output == original_name:
871
+ node.output[i] = pre_cast_name
872
+ break
873
+ cast_node.input[0] = pre_cast_name
874
+ cast_node.output[0] = original_name
875
+
876
+ modified = True
877
+ logger.debug(f"Fixed network output names: {post_cast_name} -> {output.name}")
878
+ if modified:
879
+ if self.custom_ops:
880
+ self.model = self._propagate_types_shapes_custom_ops(self.model)
881
+ else:
882
+ self.model = onnx_utils.infer_shapes(self.model, strict_mode=True, check_type=True)
883
+ self.value_info_map, self.initializer_map, self.node_to_init_map = utils.setup_mappings(
884
+ self.model
885
+ )
886
+
887
+ def _sanity_check(self):
888
+ sanity_ok = True
889
+ try:
890
+ onnx_utils.check_model(self.model)
891
+ except onnx.checker.ValidationError as e:
892
+ logger.error(f"Internal error: onnx.checker failed: {e}")
893
+ sanity_ok = False
894
+
895
+ network_inputs = list(self.model.graph.input)
896
+ network_outputs = list(self.model.graph.output)
897
+ disconnected_outputs = []
898
+
899
+ # Verify that the output tensors are not disconnected
900
+ for output in network_outputs:
901
+ producer_nodes = utils.get_producer_nodes(self.model, output.name)
902
+ if len(producer_nodes) == 0:
903
+ logger.warning(
904
+ f"Output tensor {output.name} is disconnected. This may be benign if it's part of a cast operation "
905
+ "chain (e.g., output1 -> cast -> output2)."
906
+ )
907
+ disconnected_outputs.append(output)
908
+
909
+ # Verify that the original and current network inputs/outputs match
910
+ current_io = {io.name for io in network_inputs + network_outputs}
911
+ original_io = set(self.original_network_io.keys())
912
+ if current_io != original_io:
913
+ logger.error(
914
+ f"Internal error: Sanity check failed: Network inputs/outputs do not match original inputs/outputs. "
915
+ f"Current: {current_io}, Original: {original_io}"
916
+ )
917
+ sanity_ok = False
918
+
919
+ # Verify that the original input and output types are handled according to keep_io_types
920
+ if sanity_ok:
921
+ for tensor in network_inputs + network_outputs:
922
+ if tensor in disconnected_outputs:
923
+ logger.debug(
924
+ f"Skipping validating type of disconnected output tensor {tensor.name}"
925
+ )
926
+ continue
927
+ original_type = self.original_network_io[tensor.name]
928
+ converted_type = tensor.type.tensor_type.elem_type
929
+
930
+ if converted_type != original_type:
931
+ # There's one allowed exception: FP32 I/O converted to the selected low precision type with
932
+ # keep_io_types=False
933
+ if (
934
+ original_type == onnx.TensorProto.FLOAT
935
+ and converted_type == self.low_precision_type.onnx_type
936
+ and not self.keep_io_types
937
+ ):
938
+ continue
939
+ else:
940
+ logger.error(
941
+ f"Internal error: Sanity check failed: Unexpected type in I/O tensor {tensor.name}, "
942
+ f"keep_io_types={self.keep_io_types}, original type: {original_type}, converted type: "
943
+ f"{converted_type}."
944
+ )
945
+ sanity_ok = False
946
+ if not sanity_ok:
947
+ raise Exception("Sanity Check Failed")
948
+
949
+ def _get_tensor_type(self, tensor_name):
950
+ if tensor_name in self.value_info_map:
951
+ return self.value_info_map[tensor_name].type.tensor_type.elem_type
952
+ if tensor_name in self.initializer_map:
953
+ return self.initializer_map[tensor_name].data_type
954
+ raise Exception(f"did not find tensor {tensor_name}")
955
+
956
+ def _convert_constant_values(self, const_node, cast_node: onnx.NodeProto) -> None:
957
+ original_tensor = const_node.attribute[0].t
958
+ if original_tensor.data_type == onnx.TensorProto.BFLOAT16:
959
+ original_data = onnx_utils.read_f16_tensor_as_fp32(original_tensor)
960
+ else:
961
+ original_data = onnx.numpy_helper.to_array(original_tensor)
962
+
963
+ # Precompute casted value
964
+ cast_to_type = utils.get_cast_to_type(cast_node)
965
+ cast_dtype = onnx.helper.tensor_dtype_to_np_dtype(cast_to_type)
966
+
967
+ # Handle bfloat16 conversion manually since numpy doesn't support it natively
968
+ if cast_to_type == onnx.TensorProto.BFLOAT16:
969
+ casted_data = original_data.astype(ml_dtypes.bfloat16)
970
+ else:
971
+ casted_data = original_data.astype(cast_dtype)
972
+
973
+ # Workaround for 0-dimensional tensors (scalars)
974
+ if casted_data.ndim == 0:
975
+ casted_data = casted_data.reshape(1)
976
+
977
+ # Create a new constant node with casted data
978
+ if cast_to_type == onnx.TensorProto.BFLOAT16:
979
+ # Create TensorProto manually for bfloat16
980
+ tensor_proto = onnx.TensorProto()
981
+ tensor_proto.name = const_node.output[0]
982
+ tensor_proto.data_type = onnx.TensorProto.BFLOAT16
983
+ tensor_proto.dims.extend(casted_data.shape)
984
+ # Convert bfloat16 to raw bytes
985
+ bf16_bytes = casted_data.astype(ml_dtypes.bfloat16).view(np.uint16)
986
+ tensor_proto.raw_data = bf16_bytes.tobytes()
987
+ else:
988
+ # Create tensor manually to ensure proper handling
989
+ tensor_proto = onnx.numpy_helper.from_array(casted_data)
990
+ tensor_proto.name = const_node.output[0]
991
+
992
+ new_const_node = onnx.helper.make_node(
993
+ "Constant",
994
+ inputs=[],
995
+ outputs=const_node.output,
996
+ value=tensor_proto,
997
+ name=const_node.name,
998
+ )
999
+
1000
+ # Replace the original constant node with the new constant node
1001
+ # The scope of this function is to convert the constant node data. Removing the cast is done later.
1002
+ for node in utils.get_consumer_nodes(self.model, const_node.name):
1003
+ for i, input_name in enumerate(node.input):
1004
+ if input_name == const_node.name:
1005
+ node.input[i] = new_const_node.output[0]
1006
+ break
1007
+
1008
+ const_idx = -1
1009
+ for i, node in enumerate(self.model.graph.node):
1010
+ if node == const_node:
1011
+ const_idx = i
1012
+ break
1013
+
1014
+ self.model.graph.node.remove(const_node)
1015
+ self.model.graph.node.insert(const_idx, new_const_node)
1016
+ # The Cast node is the sole consumer of the Constant node, guaranteed by _is_foldable_constant_cast_pattern
1017
+ cast_node.input[0] = new_const_node.output[0]
1018
+
1019
+ def _is_foldable_constant_cast_pattern(self, node: onnx.NodeProto) -> bool:
1020
+ """Constant -> Cast and Cast is the only consumer of the Constant node."""
1021
+ assert node.op_type == "Cast"
1022
+
1023
+ producer = utils.get_producer_nodes(self.model, node.input[0])
1024
+
1025
+ const_producer = (
1026
+ producer[0] if len(producer) == 1 and producer[0].op_type == "Constant" else None
1027
+ )
1028
+
1029
+ if const_producer:
1030
+ get_consumer_nodes = utils.get_consumer_nodes(self.model, const_producer.output[0])
1031
+ return len(get_consumer_nodes) == 1 and get_consumer_nodes[0] == node
1032
+ return False