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