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.
- modelopt/__init__.py +20 -0
- modelopt/deploy/__init__.py +16 -0
- modelopt/deploy/llm/__init__.py +30 -0
- modelopt/deploy/llm/generate.py +345 -0
- modelopt/onnx/__init__.py +34 -0
- modelopt/onnx/autocast/__init__.py +19 -0
- modelopt/onnx/autocast/__main__.py +187 -0
- modelopt/onnx/autocast/convert.py +202 -0
- modelopt/onnx/autocast/graphsanitizer.py +422 -0
- modelopt/onnx/autocast/logging_config.py +78 -0
- modelopt/onnx/autocast/nodeclassifier.py +416 -0
- modelopt/onnx/autocast/precisionconverter.py +1032 -0
- modelopt/onnx/autocast/referencerunner.py +159 -0
- modelopt/onnx/autocast/utils.py +117 -0
- modelopt/onnx/llm_export_utils/__init__.py +16 -0
- modelopt/onnx/llm_export_utils/export_utils.py +162 -0
- modelopt/onnx/llm_export_utils/quantization_utils.py +122 -0
- modelopt/onnx/llm_export_utils/surgeon_utils.py +120 -0
- modelopt/onnx/logging_config.py +77 -0
- modelopt/onnx/op_types.py +300 -0
- modelopt/onnx/quantization/__init__.py +20 -0
- modelopt/onnx/quantization/__main__.py +291 -0
- modelopt/onnx/quantization/calib_utils.py +178 -0
- modelopt/onnx/quantization/extensions.py +35 -0
- modelopt/onnx/quantization/fp8.py +375 -0
- modelopt/onnx/quantization/graph_utils.py +1479 -0
- modelopt/onnx/quantization/gs_patching.py +112 -0
- modelopt/onnx/quantization/int4.py +1347 -0
- modelopt/onnx/quantization/int8.py +283 -0
- modelopt/onnx/quantization/operators.py +121 -0
- modelopt/onnx/quantization/ort_patching.py +1766 -0
- modelopt/onnx/quantization/ort_utils.py +346 -0
- modelopt/onnx/quantization/partitioning.py +439 -0
- modelopt/onnx/quantization/qdq_utils.py +1349 -0
- modelopt/onnx/quantization/quant_utils.py +283 -0
- modelopt/onnx/quantization/quantize.py +528 -0
- modelopt/onnx/quantization/src/modelopt_round_and_pack_ext.cpp +127 -0
- modelopt/onnx/trt_utils.py +424 -0
- modelopt/onnx/utils.py +753 -0
- modelopt/torch/__init__.py +41 -0
- modelopt/torch/_deploy/__init__.py +23 -0
- modelopt/torch/_deploy/_runtime/__init__.py +29 -0
- modelopt/torch/_deploy/_runtime/common.py +72 -0
- modelopt/torch/_deploy/_runtime/ort_client.py +193 -0
- modelopt/torch/_deploy/_runtime/registry.py +83 -0
- modelopt/torch/_deploy/_runtime/runtime_client.py +154 -0
- modelopt/torch/_deploy/_runtime/tensorrt/constants.py +108 -0
- modelopt/torch/_deploy/_runtime/tensorrt/engine_builder.py +324 -0
- modelopt/torch/_deploy/_runtime/tensorrt/hw_param_config.py +51 -0
- modelopt/torch/_deploy/_runtime/tensorrt/layerwise_profiling.py +178 -0
- modelopt/torch/_deploy/_runtime/tensorrt/parse_trtexec_log.py +151 -0
- modelopt/torch/_deploy/_runtime/tensorrt/tensorrt_utils.py +200 -0
- modelopt/torch/_deploy/_runtime/trt_client.py +219 -0
- modelopt/torch/_deploy/compilation.py +142 -0
- modelopt/torch/_deploy/device_model.py +157 -0
- modelopt/torch/_deploy/profiling.py +193 -0
- modelopt/torch/_deploy/utils/__init__.py +18 -0
- modelopt/torch/_deploy/utils/onnx_optimizer.py +120 -0
- modelopt/torch/_deploy/utils/onnx_utils.py +58 -0
- modelopt/torch/_deploy/utils/torch_onnx.py +593 -0
- modelopt/torch/distill/__init__.py +28 -0
- modelopt/torch/distill/config.py +130 -0
- modelopt/torch/distill/distillation.py +84 -0
- modelopt/torch/distill/distillation_model.py +318 -0
- modelopt/torch/distill/loss_balancers.py +140 -0
- modelopt/torch/distill/losses.py +275 -0
- modelopt/torch/distill/mode.py +223 -0
- modelopt/torch/distill/plugins/__init__.py +24 -0
- modelopt/torch/distill/plugins/huggingface.py +110 -0
- modelopt/torch/distill/plugins/megatron.py +476 -0
- modelopt/torch/distill/registry.py +23 -0
- modelopt/torch/export/__init__.py +24 -0
- modelopt/torch/export/convert_hf_config.py +117 -0
- modelopt/torch/export/distribute.py +300 -0
- modelopt/torch/export/hf_config_map.py +84 -0
- modelopt/torch/export/layer_utils.py +1893 -0
- modelopt/torch/export/mcore_config_map.py +25 -0
- modelopt/torch/export/model_config.py +623 -0
- modelopt/torch/export/model_config_export.py +552 -0
- modelopt/torch/export/model_config_utils.py +392 -0
- modelopt/torch/export/model_utils.py +71 -0
- modelopt/torch/export/plugins/__init__.py +21 -0
- modelopt/torch/export/plugins/mcore_common.py +67 -0
- modelopt/torch/export/plugins/mcore_custom.py +530 -0
- modelopt/torch/export/plugins/mcore_deepseek.py +143 -0
- modelopt/torch/export/plugins/mcore_gptoss.py +71 -0
- modelopt/torch/export/plugins/mcore_llama.py +164 -0
- modelopt/torch/export/plugins/mcore_nemotron.py +90 -0
- modelopt/torch/export/plugins/mcore_qwen.py +99 -0
- modelopt/torch/export/plugins/megatron_importer.py +647 -0
- modelopt/torch/export/postprocess.py +847 -0
- modelopt/torch/export/quant_utils.py +1069 -0
- modelopt/torch/export/tensorrt_llm_type.py +42 -0
- modelopt/torch/export/tensorrt_llm_utils.py +405 -0
- modelopt/torch/export/transformer_engine.py +94 -0
- modelopt/torch/export/unified_export_hf.py +537 -0
- modelopt/torch/export/unified_export_megatron.py +1164 -0
- modelopt/torch/nas/__init__.py +27 -0
- modelopt/torch/nas/algorithms.py +856 -0
- modelopt/torch/nas/autonas.py +808 -0
- modelopt/torch/nas/conversion.py +119 -0
- modelopt/torch/nas/hparams/__init__.py +19 -0
- modelopt/torch/nas/hparams/concat.py +319 -0
- modelopt/torch/nas/hparams/container.py +48 -0
- modelopt/torch/nas/modules/__init__.py +21 -0
- modelopt/torch/nas/modules/container.py +99 -0
- modelopt/torch/nas/modules/conv.py +254 -0
- modelopt/torch/nas/modules/linear.py +76 -0
- modelopt/torch/nas/modules/norm.py +193 -0
- modelopt/torch/nas/modules/utils.py +61 -0
- modelopt/torch/nas/patch.py +261 -0
- modelopt/torch/nas/plugins/__init__.py +29 -0
- modelopt/torch/nas/plugins/megatron.py +1497 -0
- modelopt/torch/nas/plugins/torch.py +71 -0
- modelopt/torch/nas/plugins/transformer_engine.py +42 -0
- modelopt/torch/nas/plugins/transformers.py +210 -0
- modelopt/torch/nas/registry.py +23 -0
- modelopt/torch/nas/search_space.py +260 -0
- modelopt/torch/nas/traced_hp.py +149 -0
- modelopt/torch/nas/utils.py +408 -0
- modelopt/torch/opt/__init__.py +41 -0
- modelopt/torch/opt/_hooks.py +98 -0
- modelopt/torch/opt/config.py +383 -0
- modelopt/torch/opt/conversion.py +620 -0
- modelopt/torch/opt/dynamic.py +1367 -0
- modelopt/torch/opt/hparam.py +275 -0
- modelopt/torch/opt/mode.py +353 -0
- modelopt/torch/opt/plugins/__init__.py +38 -0
- modelopt/torch/opt/plugins/diffusers.py +40 -0
- modelopt/torch/opt/plugins/huggingface.py +162 -0
- modelopt/torch/opt/plugins/mcore_dist_checkpointing.py +211 -0
- modelopt/torch/opt/plugins/megatron.py +142 -0
- modelopt/torch/opt/plugins/peft.py +108 -0
- modelopt/torch/opt/plugins/transformers.py +160 -0
- modelopt/torch/opt/searcher.py +363 -0
- modelopt/torch/opt/utils.py +125 -0
- modelopt/torch/prune/__init__.py +26 -0
- modelopt/torch/prune/fastnas.py +376 -0
- modelopt/torch/prune/gradnas.py +349 -0
- modelopt/torch/prune/plugins/__init__.py +24 -0
- modelopt/torch/prune/plugins/mcore_minitron.py +263 -0
- modelopt/torch/prune/plugins/transformers.py +41 -0
- modelopt/torch/prune/pruning.py +211 -0
- modelopt/torch/quantization/__init__.py +26 -0
- modelopt/torch/quantization/algorithms.py +671 -0
- modelopt/torch/quantization/backends/__init__.py +20 -0
- modelopt/torch/quantization/backends/fp8_per_tensor_gemm.py +204 -0
- modelopt/torch/quantization/backends/gemm_registry.py +156 -0
- modelopt/torch/quantization/backends/nvfp4_gemm.py +201 -0
- modelopt/torch/quantization/backends/utils.py +28 -0
- modelopt/torch/quantization/calib/__init__.py +25 -0
- modelopt/torch/quantization/calib/bias.py +172 -0
- modelopt/torch/quantization/calib/calibrator.py +63 -0
- modelopt/torch/quantization/calib/histogram.py +434 -0
- modelopt/torch/quantization/calib/max.py +110 -0
- modelopt/torch/quantization/compress.py +227 -0
- modelopt/torch/quantization/config.py +1180 -0
- modelopt/torch/quantization/conversion.py +389 -0
- modelopt/torch/quantization/export_onnx.py +639 -0
- modelopt/torch/quantization/extensions.py +89 -0
- modelopt/torch/quantization/mode.py +428 -0
- modelopt/torch/quantization/model_calib.py +949 -0
- modelopt/torch/quantization/model_quant.py +477 -0
- modelopt/torch/quantization/nn/__init__.py +26 -0
- modelopt/torch/quantization/nn/functional.py +109 -0
- modelopt/torch/quantization/nn/modules/__init__.py +16 -0
- modelopt/torch/quantization/nn/modules/quant_activations.py +22 -0
- modelopt/torch/quantization/nn/modules/quant_batchnorm.py +24 -0
- modelopt/torch/quantization/nn/modules/quant_conv.py +123 -0
- modelopt/torch/quantization/nn/modules/quant_instancenorm.py +39 -0
- modelopt/torch/quantization/nn/modules/quant_linear.py +258 -0
- modelopt/torch/quantization/nn/modules/quant_module.py +211 -0
- modelopt/torch/quantization/nn/modules/quant_pooling.py +99 -0
- modelopt/torch/quantization/nn/modules/quant_rnn.py +527 -0
- modelopt/torch/quantization/nn/modules/tensor_quantizer.py +1293 -0
- modelopt/torch/quantization/plugins/__init__.py +73 -0
- modelopt/torch/quantization/plugins/accelerate.py +214 -0
- modelopt/torch/quantization/plugins/apex.py +52 -0
- modelopt/torch/quantization/plugins/attention.py +312 -0
- modelopt/torch/quantization/plugins/custom.py +192 -0
- modelopt/torch/quantization/plugins/diffusers.py +237 -0
- modelopt/torch/quantization/plugins/fairscale.py +45 -0
- modelopt/torch/quantization/plugins/huggingface.py +736 -0
- modelopt/torch/quantization/plugins/megatron.py +462 -0
- modelopt/torch/quantization/plugins/peft.py +148 -0
- modelopt/torch/quantization/plugins/transformer_engine.py +60 -0
- modelopt/torch/quantization/plugins/transformers.py +52 -0
- modelopt/torch/quantization/plugins/transformers_trainer.py +426 -0
- modelopt/torch/quantization/plugins/trl.py +27 -0
- modelopt/torch/quantization/plugins/vllm.py +199 -0
- modelopt/torch/quantization/qtensor/__init__.py +24 -0
- modelopt/torch/quantization/qtensor/base_qtensor.py +370 -0
- modelopt/torch/quantization/qtensor/fp8_tensor.py +151 -0
- modelopt/torch/quantization/qtensor/int4_tensor.py +130 -0
- modelopt/torch/quantization/qtensor/int8_tensor.py +124 -0
- modelopt/torch/quantization/qtensor/mxfp4_tensor.py +144 -0
- modelopt/torch/quantization/qtensor/nf4_tensor.py +199 -0
- modelopt/torch/quantization/qtensor/nvfp4_tensor.py +295 -0
- modelopt/torch/quantization/src/tensor_quant.cpp +77 -0
- modelopt/torch/quantization/src/tensor_quant.h +69 -0
- modelopt/torch/quantization/src/tensor_quant_fp8.cpp +48 -0
- modelopt/torch/quantization/src/tensor_quant_gpu.cu +361 -0
- modelopt/torch/quantization/src/tensor_quant_gpu_fp8.cu +122 -0
- modelopt/torch/quantization/src/tensor_quant_mx.cu +411 -0
- modelopt/torch/quantization/src/tensor_quant_mx.h +247 -0
- modelopt/torch/quantization/tensor_quant.py +816 -0
- modelopt/torch/quantization/triton/__init__.py +36 -0
- modelopt/torch/quantization/triton/fp4_kernel.py +303 -0
- modelopt/torch/quantization/utils.py +443 -0
- modelopt/torch/sparsity/__init__.py +19 -0
- modelopt/torch/sparsity/config.py +51 -0
- modelopt/torch/sparsity/magnitude.py +148 -0
- modelopt/torch/sparsity/mode.py +203 -0
- modelopt/torch/sparsity/module.py +87 -0
- modelopt/torch/sparsity/plugins/__init__.py +27 -0
- modelopt/torch/sparsity/plugins/megatron.py +93 -0
- modelopt/torch/sparsity/searcher.py +84 -0
- modelopt/torch/sparsity/sparsegpt.py +276 -0
- modelopt/torch/sparsity/sparsification.py +123 -0
- modelopt/torch/speculative/__init__.py +20 -0
- modelopt/torch/speculative/config.py +100 -0
- modelopt/torch/speculative/eagle/__init__.py +20 -0
- modelopt/torch/speculative/eagle/conversion.py +67 -0
- modelopt/torch/speculative/eagle/default_config.py +50 -0
- modelopt/torch/speculative/eagle/eagle_model.py +51 -0
- modelopt/torch/speculative/eagle/utils.py +91 -0
- modelopt/torch/speculative/medusa/__init__.py +19 -0
- modelopt/torch/speculative/medusa/conversion.py +59 -0
- modelopt/torch/speculative/medusa/medusa_model.py +32 -0
- modelopt/torch/speculative/mode.py +86 -0
- modelopt/torch/speculative/plugins/__init__.py +33 -0
- modelopt/torch/speculative/plugins/megatron_eagle.py +2119 -0
- modelopt/torch/speculative/plugins/megatron_medusa.py +312 -0
- modelopt/torch/speculative/plugins/transformers.py +1138 -0
- modelopt/torch/speculative/speculative_decoding.py +59 -0
- modelopt/torch/speculative/utils.py +364 -0
- modelopt/torch/trace/__init__.py +25 -0
- modelopt/torch/trace/analyzer.py +1368 -0
- modelopt/torch/trace/modules/__init__.py +19 -0
- modelopt/torch/trace/modules/concat.py +384 -0
- modelopt/torch/trace/modules/nn.py +153 -0
- modelopt/torch/trace/plugins/__init__.py +34 -0
- modelopt/torch/trace/plugins/megatron.py +36 -0
- modelopt/torch/trace/plugins/transformers.py +58 -0
- modelopt/torch/trace/symbols.py +545 -0
- modelopt/torch/trace/tracer.py +331 -0
- modelopt/torch/utils/__init__.py +28 -0
- modelopt/torch/utils/_pytree.py +133 -0
- modelopt/torch/utils/cpp_extension.py +91 -0
- modelopt/torch/utils/dataset_utils.py +484 -0
- modelopt/torch/utils/distributed.py +311 -0
- modelopt/torch/utils/graph.py +123 -0
- modelopt/torch/utils/image_processor.py +112 -0
- modelopt/torch/utils/import_utils.py +35 -0
- modelopt/torch/utils/list.py +54 -0
- modelopt/torch/utils/logging.py +127 -0
- modelopt/torch/utils/memory_monitor.py +146 -0
- modelopt/torch/utils/network.py +658 -0
- modelopt/torch/utils/perf.py +174 -0
- modelopt/torch/utils/plugins/__init__.py +27 -0
- modelopt/torch/utils/plugins/megatron_generate.py +299 -0
- modelopt/torch/utils/plugins/megatron_mmlu.py +152 -0
- modelopt/torch/utils/plugins/megatron_preprocess_data.py +200 -0
- modelopt/torch/utils/random.py +163 -0
- modelopt/torch/utils/speech_dataset_utils.py +134 -0
- modelopt/torch/utils/tensor.py +87 -0
- modelopt/torch/utils/vlm_dataset_utils.py +110 -0
- nvidia_modelopt-0.35.0.dist-info/METADATA +171 -0
- nvidia_modelopt-0.35.0.dist-info/RECORD +272 -0
- nvidia_modelopt-0.35.0.dist-info/WHEEL +5 -0
- nvidia_modelopt-0.35.0.dist-info/licenses/LICENSE +14 -0
- 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
|