onnxscript 0.1.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.
- onnxscript/__init__.py +131 -0
- onnxscript/_framework_apis/__init__.py +3 -0
- onnxscript/_framework_apis/torch_2_5.py +117 -0
- onnxscript/_framework_apis/torch_2_6.py +45 -0
- onnxscript/_internal/__init__.py +0 -0
- onnxscript/_internal/analysis.py +229 -0
- onnxscript/_internal/ast_utils.py +64 -0
- onnxscript/_internal/autocast.py +250 -0
- onnxscript/_internal/deprecation.py +78 -0
- onnxscript/_internal/param_manipulation.py +148 -0
- onnxscript/_internal/runtime_typing.py +43 -0
- onnxscript/_internal/utils.py +99 -0
- onnxscript/_internal/version_utils.py +118 -0
- onnxscript/_legacy_ir/__init__.py +341 -0
- onnxscript/_legacy_ir/visitor.py +937 -0
- onnxscript/_thirdparty/asciichartpy.py +313 -0
- onnxscript/backend/__init__.py +2 -0
- onnxscript/backend/onnx_backend.py +303 -0
- onnxscript/backend/onnx_export.py +885 -0
- onnxscript/converter.py +1470 -0
- onnxscript/evaluator.py +619 -0
- onnxscript/function_libs/tools/torch_lib/deduce_type_constraints.py +403 -0
- onnxscript/function_libs/tools/torch_lib/generate_aten_signatures.py +333 -0
- onnxscript/function_libs/tools/torch_lib/generate_prims_signatures.py +331 -0
- onnxscript/function_libs/torch_lib/__init__.py +12 -0
- onnxscript/function_libs/torch_lib/_constants.py +5 -0
- onnxscript/function_libs/torch_lib/_flags.py +58 -0
- onnxscript/function_libs/torch_lib/graph_building/__init__.py +56 -0
- onnxscript/function_libs/torch_lib/graph_building/_graph_building_ir.py +723 -0
- onnxscript/function_libs/torch_lib/graph_building/_graph_building_torch.py +1125 -0
- onnxscript/function_libs/torch_lib/ops/__init__.py +27 -0
- onnxscript/function_libs/torch_lib/ops/common.py +80 -0
- onnxscript/function_libs/torch_lib/ops/core.py +8935 -0
- onnxscript/function_libs/torch_lib/ops/fft.py +385 -0
- onnxscript/function_libs/torch_lib/ops/linalg.py +399 -0
- onnxscript/function_libs/torch_lib/ops/nested.py +25 -0
- onnxscript/function_libs/torch_lib/ops/nn.py +2713 -0
- onnxscript/function_libs/torch_lib/ops/prims.py +850 -0
- onnxscript/function_libs/torch_lib/ops/quantized_decomposed.py +63 -0
- onnxscript/function_libs/torch_lib/ops/sparse.py +23 -0
- onnxscript/function_libs/torch_lib/ops/special.py +387 -0
- onnxscript/function_libs/torch_lib/ops/vision.py +25 -0
- onnxscript/function_libs/torch_lib/registration.py +151 -0
- onnxscript/function_libs/torch_lib/tensor_typing.py +74 -0
- onnxscript/ir/__init__.py +153 -0
- onnxscript/ir/_convenience.py +447 -0
- onnxscript/ir/_core.py +3119 -0
- onnxscript/ir/_display.py +49 -0
- onnxscript/ir/_enums.py +163 -0
- onnxscript/ir/_graph_comparison.py +23 -0
- onnxscript/ir/_io.py +97 -0
- onnxscript/ir/_linked_list.py +276 -0
- onnxscript/ir/_metadata.py +44 -0
- onnxscript/ir/_name_authority.py +72 -0
- onnxscript/ir/_polyfill.py +25 -0
- onnxscript/ir/_protocols.py +598 -0
- onnxscript/ir/_schemas.py +548 -0
- onnxscript/ir/_tape.py +120 -0
- onnxscript/ir/_type_casting.py +106 -0
- onnxscript/ir/convenience.py +32 -0
- onnxscript/ir/external_data.py +396 -0
- onnxscript/ir/passes/__init__.py +33 -0
- onnxscript/ir/passes/_pass_infra.py +172 -0
- onnxscript/ir/serde.py +1620 -0
- onnxscript/ir/tensor_adapters.py +122 -0
- onnxscript/ir/traversal.py +82 -0
- onnxscript/irbuilder.py +542 -0
- onnxscript/main.py +167 -0
- onnxscript/onnx_opset/__init__.py +232 -0
- onnxscript/onnx_opset/_impl/opset1.py +4100 -0
- onnxscript/onnx_opset/_impl/opset10.py +1227 -0
- onnxscript/onnx_opset/_impl/opset11.py +4013 -0
- onnxscript/onnx_opset/_impl/opset12.py +1078 -0
- onnxscript/onnx_opset/_impl/opset13.py +3924 -0
- onnxscript/onnx_opset/_impl/opset14.py +999 -0
- onnxscript/onnx_opset/_impl/opset15.py +604 -0
- onnxscript/onnx_opset/_impl/opset16.py +1255 -0
- onnxscript/onnx_opset/_impl/opset17.py +561 -0
- onnxscript/onnx_opset/_impl/opset18.py +1803 -0
- onnxscript/onnx_opset/_impl/opset19.py +1942 -0
- onnxscript/onnx_opset/_impl/opset2.py +218 -0
- onnxscript/onnx_opset/_impl/opset20.py +675 -0
- onnxscript/onnx_opset/_impl/opset21.py +1976 -0
- onnxscript/onnx_opset/_impl/opset22.py +2588 -0
- onnxscript/onnx_opset/_impl/opset3.py +199 -0
- onnxscript/onnx_opset/_impl/opset4.py +77 -0
- onnxscript/onnx_opset/_impl/opset5.py +84 -0
- onnxscript/onnx_opset/_impl/opset6.py +944 -0
- onnxscript/onnx_opset/_impl/opset7.py +1243 -0
- onnxscript/onnx_opset/_impl/opset8.py +444 -0
- onnxscript/onnx_opset/_impl/opset9.py +1485 -0
- onnxscript/onnx_opset/_impl/opset_ai_onnx_ml1.py +974 -0
- onnxscript/onnx_opset/_impl/opset_ai_onnx_ml2.py +112 -0
- onnxscript/onnx_opset/_impl/opset_ai_onnx_ml3.py +308 -0
- onnxscript/onnx_opset/_impl/opset_ai_onnx_ml4.py +129 -0
- onnxscript/onnx_opset/_impl/opset_ai_onnx_ml5.py +158 -0
- onnxscript/onnx_opset/_impl/opset_ai_onnx_preview_training1.py +577 -0
- onnxscript/onnx_types.py +229 -0
- onnxscript/optimizer/__init__.py +39 -0
- onnxscript/optimizer/_constant_folding.py +1083 -0
- onnxscript/optimizer/_inliner.py +312 -0
- onnxscript/optimizer/_legacy/_optimizer.py +98 -0
- onnxscript/optimizer/_legacy/_remove_unused_proto.py +144 -0
- onnxscript/optimizer/_legacy/_simple_function_folding.py +243 -0
- onnxscript/optimizer/_legacy/constant_folding.py +293 -0
- onnxscript/optimizer/_legacy/evaluator.py +439 -0
- onnxscript/optimizer/_optimizer.py +61 -0
- onnxscript/optimizer/_remove_unused.py +106 -0
- onnxscript/optimizer/_remove_unused_function.py +72 -0
- onnxscript/py.typed +1 -0
- onnxscript/rewriter/__init__.py +56 -0
- onnxscript/rewriter/_ir_utils.py +111 -0
- onnxscript/rewriter/broadcast_to_matmul.py +180 -0
- onnxscript/rewriter/cast_constant_of_shape.py +50 -0
- onnxscript/rewriter/collapse_slices.py +141 -0
- onnxscript/rewriter/erfgelu.py +27 -0
- onnxscript/rewriter/function_rule.py +232 -0
- onnxscript/rewriter/gemm_to_matmul_add.py +21 -0
- onnxscript/rewriter/generic_pattern.py +700 -0
- onnxscript/rewriter/llama_rule_sets.py +287 -0
- onnxscript/rewriter/no_op.py +56 -0
- onnxscript/rewriter/onnxruntime/__init__.py +50 -0
- onnxscript/rewriter/onnxruntime/bfloat16_utils/bfloat16_converter.py +99 -0
- onnxscript/rewriter/onnxruntime/fused_matmul_rule_sets.py +177 -0
- onnxscript/rewriter/onnxruntime/group_normalization_merge_silu.py +64 -0
- onnxscript/rewriter/onnxruntime/instance_to_group_normalization.py +155 -0
- onnxscript/rewriter/onnxruntime/softmax.py +63 -0
- onnxscript/rewriter/onnxruntime/transformers/__init__.py +21 -0
- onnxscript/rewriter/onnxruntime/transformers/biassplitgelu.py +31 -0
- onnxscript/rewriter/onnxruntime/transformers/fastgelu.py +29 -0
- onnxscript/rewriter/onnxruntime/transformers/layernorm.py +47 -0
- onnxscript/rewriter/onnxruntime/transformers/multihead_attention.py +715 -0
- onnxscript/rewriter/ort_fusions/__init__.py +9 -0
- onnxscript/rewriter/ort_fusions/_core.py +28 -0
- onnxscript/rewriter/ort_fusions/_smollm_1.py +253 -0
- onnxscript/rewriter/ort_fusions/_smollm_2.py +467 -0
- onnxscript/rewriter/ort_fusions/_test_models.py +122 -0
- onnxscript/rewriter/ort_fusions/_test_utils.py +42 -0
- onnxscript/rewriter/ort_fusions/cos_sin_cache.py +154 -0
- onnxscript/rewriter/ort_fusions/gqa.py +156 -0
- onnxscript/rewriter/ort_fusions/mha.py +198 -0
- onnxscript/rewriter/ort_fusions/rms_normalization.py +95 -0
- onnxscript/rewriter/ort_fusions/rotary_embedding.py +64 -0
- onnxscript/rewriter/ort_fusions/sdpa.py +75 -0
- onnxscript/rewriter/ort_fusions/skip_normalization.py +46 -0
- onnxscript/rewriter/pattern.py +1714 -0
- onnxscript/rewriter/testing.py +77 -0
- onnxscript/sourceinfo.py +59 -0
- onnxscript/tensor.py +227 -0
- onnxscript/testing/__init__.py +482 -0
- onnxscript/tools/__init__.py +4 -0
- onnxscript/tools/benchmark/__init__.py +23 -0
- onnxscript/tools/benchmark/benchmark_helpers.py +783 -0
- onnxscript/tools/benchmark/benchmark_run.py +140 -0
- onnxscript/tools/benchmark/export_model.py +207 -0
- onnxscript/tools/benchmark/export_model_batch.py +146 -0
- onnxscript/tools/memory_peak.py +244 -0
- onnxscript/tools/training_helper.py +50 -0
- onnxscript/tools/transformers_models/__init__.py +190 -0
- onnxscript/tools/transformers_models/llama.py +168 -0
- onnxscript/tools/transformers_models/mistral.py +238 -0
- onnxscript/tools/transformers_models/phi.py +248 -0
- onnxscript/tools/transformers_models/phi3.py +259 -0
- onnxscript/type_annotation.py +281 -0
- onnxscript/utils/__init__.py +0 -0
- onnxscript/utils/evaluation_utils.py +56 -0
- onnxscript/utils/timing_utils.py +33 -0
- onnxscript/utils/utils.py +84 -0
- onnxscript/values.py +790 -0
- onnxscript/version_converter/__init__.py +21 -0
- onnxscript/version_converter/_version_converter.py +314 -0
- onnxscript-0.1.0.dist-info/LICENSE +21 -0
- onnxscript-0.1.0.dist-info/METADATA +370 -0
- onnxscript-0.1.0.dist-info/RECORD +176 -0
- onnxscript-0.1.0.dist-info/WHEEL +5 -0
- onnxscript-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Sequence
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import onnx
|
|
11
|
+
|
|
12
|
+
import onnxscript._legacy_ir as ir
|
|
13
|
+
from onnxscript.utils.utils import (
|
|
14
|
+
get_initializer_type,
|
|
15
|
+
is_control_flow_op,
|
|
16
|
+
normalize_domain,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _override_inferred_value_type_with_symbolic_value_type(
|
|
23
|
+
symbolic_value: ir.Value | None,
|
|
24
|
+
inferred_value: ir.Value | None,
|
|
25
|
+
) -> ir.Value | None:
|
|
26
|
+
if inferred_value is not None and symbolic_value is not None:
|
|
27
|
+
inferred_value.type = symbolic_value.type
|
|
28
|
+
if inferred_value is None:
|
|
29
|
+
inferred_value = symbolic_value
|
|
30
|
+
return inferred_value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_local_function_node(
|
|
34
|
+
node: onnx.NodeProto, functions: dict[ir.FunctionId, onnx.FunctionProto]
|
|
35
|
+
) -> bool:
|
|
36
|
+
return ir.get_function_id_from_node(node) in functions
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FunctionShapeEnv:
|
|
40
|
+
def __init__(self):
|
|
41
|
+
# Mapping from (domain, function_name, overload) to {value_name: ir_value}
|
|
42
|
+
self._function_values: dict[ir.FunctionId, dict[str, ir.Value]] = {}
|
|
43
|
+
|
|
44
|
+
def load_from_model_proto(self, model_proto: onnx.ModelProto) -> None:
|
|
45
|
+
for value_info in model_proto.graph.value_info:
|
|
46
|
+
self.load_from_value_info(value_info)
|
|
47
|
+
|
|
48
|
+
def save_to_model_proto(self, model_proto: onnx.ModelProto) -> None:
|
|
49
|
+
for (
|
|
50
|
+
domain,
|
|
51
|
+
function_name,
|
|
52
|
+
overload,
|
|
53
|
+
), named_ir_values in self._function_values.items():
|
|
54
|
+
for ir_value in named_ir_values.values():
|
|
55
|
+
if (
|
|
56
|
+
value_info := self.save_to_value_info(
|
|
57
|
+
ir_value, domain, function_name, overload
|
|
58
|
+
)
|
|
59
|
+
) is not None:
|
|
60
|
+
model_proto.graph.value_info.append(value_info)
|
|
61
|
+
|
|
62
|
+
def load_from_value_info(self, value_info: onnx.ValueInfoProto) -> None:
|
|
63
|
+
function_id, ir_value = self.process_value_info(value_info)
|
|
64
|
+
if function_id is not None:
|
|
65
|
+
logger.debug(
|
|
66
|
+
"Loads torch symbolic value info '%s'.",
|
|
67
|
+
value_info.name,
|
|
68
|
+
)
|
|
69
|
+
self._function_values.setdefault(function_id, {})[ir_value.name] = ir_value
|
|
70
|
+
|
|
71
|
+
def process_value_info(
|
|
72
|
+
self, value_info: onnx.ValueInfoProto
|
|
73
|
+
) -> tuple[ir.FunctionId | None, ir.Value]:
|
|
74
|
+
name = value_info.name
|
|
75
|
+
if len(splits := name.split("/")) == 2:
|
|
76
|
+
# Experimental function value info format.
|
|
77
|
+
# To be deprecated after ONNX 1.16, where value_info is introduced in FunctionProto.
|
|
78
|
+
function_id, value_name = splits
|
|
79
|
+
splits = function_id.split("::")
|
|
80
|
+
domain, function_name = splits[0], splits[1]
|
|
81
|
+
# 'overload' is introduced in ONNX 1.16, consider it as empty string prior to that.
|
|
82
|
+
# The code is for future proof, in case overload is encoded in this format.
|
|
83
|
+
overload = ""
|
|
84
|
+
if len(splits) == 3:
|
|
85
|
+
overload = splits[2]
|
|
86
|
+
function_id = (domain, function_name, overload)
|
|
87
|
+
else:
|
|
88
|
+
# Standard main graph value info format.
|
|
89
|
+
function_id = None
|
|
90
|
+
value_name = name
|
|
91
|
+
return function_id, ir.Value(name=value_name, type=value_info.type)
|
|
92
|
+
|
|
93
|
+
def save_to_value_info(
|
|
94
|
+
self, value: ir.Value, domain: str, function_name: str, overload: str
|
|
95
|
+
) -> onnx.ValueInfoProto | None:
|
|
96
|
+
if overload != "":
|
|
97
|
+
raise NotImplementedError("Overload is not supported yet.")
|
|
98
|
+
function_id = f"{domain}::{function_name}"
|
|
99
|
+
|
|
100
|
+
if value.type is not None:
|
|
101
|
+
return onnx.helper.make_value_info(f"{function_id}/{value.name}", value.type)
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def lookup(self, function: onnx.FunctionProto, value_name: str) -> ir.Value | None:
|
|
105
|
+
"""Lookup ir value of 'value_name' inside 'function'."""
|
|
106
|
+
function_id = ir.get_function_id(function)
|
|
107
|
+
function_values = self._function_values.get(function_id)
|
|
108
|
+
if function_values is None or (ir_value := function_values.get(value_name)) is None:
|
|
109
|
+
logger.debug(
|
|
110
|
+
"Lookup Missed %s torch symbolic value info in function %s::%s.",
|
|
111
|
+
value_name,
|
|
112
|
+
function.domain,
|
|
113
|
+
function.name,
|
|
114
|
+
)
|
|
115
|
+
return None
|
|
116
|
+
logger.debug(
|
|
117
|
+
"Lookup found %s torch symbolic value info in function %s::%s.",
|
|
118
|
+
value_name,
|
|
119
|
+
function.domain,
|
|
120
|
+
function.name,
|
|
121
|
+
)
|
|
122
|
+
return ir_value
|
|
123
|
+
|
|
124
|
+
def bind(self, value: ir.Value, domain: str, function_name: str, overload: str) -> None:
|
|
125
|
+
"""Bind ir value 'value' to 'value_name' inside 'function'."""
|
|
126
|
+
function_id = (domain, function_name, overload)
|
|
127
|
+
self._function_values.setdefault(function_id, {})[value.name] = value
|
|
128
|
+
|
|
129
|
+
def get_ir_values(self, function: onnx.FunctionProto) -> dict[str, ir.Value]:
|
|
130
|
+
"""Get all ir values inside 'function'."""
|
|
131
|
+
function_id = ir.get_function_id(function)
|
|
132
|
+
return self._function_values.get(function_id, {})
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class SubScope:
|
|
136
|
+
values: dict[str, ir.Value]
|
|
137
|
+
ref_attributes: dict[str, onnx.AttributeProto]
|
|
138
|
+
owner: onnx.GraphProto | onnx.FunctionProto
|
|
139
|
+
|
|
140
|
+
def __init__(self, owner: onnx.GraphProto | onnx.FunctionProto):
|
|
141
|
+
self.values = {}
|
|
142
|
+
self.ref_attributes = {}
|
|
143
|
+
self.owner = owner
|
|
144
|
+
|
|
145
|
+
def lookup(self, name: str) -> ir.Value | None:
|
|
146
|
+
return self.values.get(name)
|
|
147
|
+
|
|
148
|
+
def bind(self, name: str, value: ir.Value) -> None:
|
|
149
|
+
self.values[name] = value
|
|
150
|
+
|
|
151
|
+
def lookup_ref_attribute(self, ref_attr_name: str) -> onnx.AttributeProto | None:
|
|
152
|
+
return self.ref_attributes.get(ref_attr_name)
|
|
153
|
+
|
|
154
|
+
def bind_ref_attribute(self, ref_attr_name: str, attr: onnx.AttributeProto) -> None:
|
|
155
|
+
self.ref_attributes[ref_attr_name] = attr
|
|
156
|
+
|
|
157
|
+
def readable_strs(self, indent: int = 0) -> list[str]:
|
|
158
|
+
indent_str = " " * indent
|
|
159
|
+
strs = []
|
|
160
|
+
if isinstance(self.owner, onnx.GraphProto):
|
|
161
|
+
strs.append(f"Graph {self.owner.name}:")
|
|
162
|
+
else:
|
|
163
|
+
strs.append(f"Function {self.owner.name}:")
|
|
164
|
+
strs.append(" ir.Values:")
|
|
165
|
+
for name, value in self.values.items():
|
|
166
|
+
strs.append(f" {name}: {value}")
|
|
167
|
+
strs.append(" RefAttributes:")
|
|
168
|
+
for name, attr in self.ref_attributes.items():
|
|
169
|
+
strs.append(f" {name}: {attr}")
|
|
170
|
+
|
|
171
|
+
return [f"{indent_str}{s}" for s in strs]
|
|
172
|
+
|
|
173
|
+
def __str__(self) -> str:
|
|
174
|
+
return "\n".join(self.readable_strs())
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclasses.dataclass
|
|
178
|
+
class Scope:
|
|
179
|
+
_sub_scopes: list[SubScope] = dataclasses.field(default_factory=list)
|
|
180
|
+
|
|
181
|
+
def lookup(self, name: str) -> ir.Value | None:
|
|
182
|
+
"""Lookup value by name from all SubScopes."""
|
|
183
|
+
for sub_scope in reversed(self._sub_scopes):
|
|
184
|
+
if (result := sub_scope.lookup(name)) is not None:
|
|
185
|
+
return result
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
def bind(self, name: str, value: ir.Value) -> None:
|
|
189
|
+
"""Bind value to name in the most recent SubScope."""
|
|
190
|
+
if name == "":
|
|
191
|
+
raise ValueError("Cannot bind to empty name.")
|
|
192
|
+
if value is None:
|
|
193
|
+
raise ValueError(f"Cannot bind None to value {name}.")
|
|
194
|
+
self._sub_scopes[-1].bind(name, value)
|
|
195
|
+
|
|
196
|
+
def lookup_or_create(self, name: str) -> ir.Value:
|
|
197
|
+
"""Lookup value by name from all SubScopes. If not found, create a new one in most recent SubScope."""
|
|
198
|
+
if name == "":
|
|
199
|
+
raise ValueError("Cannot lookup or create empty name.")
|
|
200
|
+
for sub_scope in reversed(self._sub_scopes):
|
|
201
|
+
if (result := sub_scope.lookup(name)) is not None:
|
|
202
|
+
return result
|
|
203
|
+
value = ir.Value(name=name)
|
|
204
|
+
self.bind(name, value)
|
|
205
|
+
return value
|
|
206
|
+
|
|
207
|
+
def lookup_ref_attribute(self, ref_attr_name: str) -> onnx.AttributeProto | None:
|
|
208
|
+
for sub_scope in reversed(self._sub_scopes):
|
|
209
|
+
if (result := sub_scope.lookup_ref_attribute(ref_attr_name)) is not None:
|
|
210
|
+
return result
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def bind_ref_attribute(self, ref_attr_name: str, attr: onnx.AttributeProto) -> None:
|
|
214
|
+
self._sub_scopes[-1].bind_ref_attribute(ref_attr_name, attr)
|
|
215
|
+
|
|
216
|
+
def enter_sub_scope(self, owner: onnx.GraphProto) -> None:
|
|
217
|
+
self._sub_scopes.append(SubScope(owner))
|
|
218
|
+
|
|
219
|
+
def exit_sub_scope(self) -> SubScope:
|
|
220
|
+
return self._sub_scopes.pop()
|
|
221
|
+
|
|
222
|
+
def current_function_scope(self) -> SubScope | None:
|
|
223
|
+
if len(self._sub_scopes) == 0:
|
|
224
|
+
return None
|
|
225
|
+
if isinstance(self._sub_scopes[0].owner, onnx.FunctionProto):
|
|
226
|
+
return self._sub_scopes[0]
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def current_function(self) -> onnx.FunctionProto | None:
|
|
230
|
+
current_function_scope = self.current_function_scope()
|
|
231
|
+
if current_function_scope is not None:
|
|
232
|
+
return current_function_scope.owner
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
def current_graph(self) -> onnx.GraphProto | None:
|
|
236
|
+
for sub_scope in reversed(self._sub_scopes):
|
|
237
|
+
if isinstance(sub_scope.owner, onnx.GraphProto):
|
|
238
|
+
return sub_scope.owner
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
def readable_strs(self, indent: int = 0) -> list[str]:
|
|
242
|
+
indent_str = " " * indent
|
|
243
|
+
strs = []
|
|
244
|
+
for i, sub_scope in enumerate(self._sub_scopes):
|
|
245
|
+
strs.append(f"SubScope {i}:")
|
|
246
|
+
strs.extend(sub_scope.readable_strs(indent=indent + 2))
|
|
247
|
+
return [f"{indent_str}{s}" for s in strs]
|
|
248
|
+
|
|
249
|
+
def __str__(self) -> str:
|
|
250
|
+
return "\n".join(self.readable_strs())
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclasses.dataclass
|
|
254
|
+
class ScopeStack:
|
|
255
|
+
"""Stack of scopes.
|
|
256
|
+
|
|
257
|
+
Each Scope represents statically-nested SubScopes (where inner SubScopes can access names defined in outer SubScopes)
|
|
258
|
+
produced by subgraphs (occurring as attribute values), except for the first SubScope which could be produced by a function.
|
|
259
|
+
With a ScopeStack, there is no such possibility of referencing variables defined higher up in the stack by name.
|
|
260
|
+
Instead, it is meant to represent a sequence of (nested) function-calls. Each entry in the stack (except the outermost)
|
|
261
|
+
represents a call to a function.
|
|
262
|
+
|
|
263
|
+
Thus, we would use a ScopeStack for a context-sensitive analysis (where we recursively process a called function).
|
|
264
|
+
For a context-insensitive analysis, we would only need a Scope (where we recursively process subgraphs).
|
|
265
|
+
|
|
266
|
+
To debug, `print(scope_stack)` will print the scope structure as well as the info stored
|
|
267
|
+
in each scope.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
_scopes: list[Scope] = dataclasses.field(default_factory=lambda: [Scope()])
|
|
271
|
+
|
|
272
|
+
def current_scope(self) -> Scope:
|
|
273
|
+
return self._scopes[-1]
|
|
274
|
+
|
|
275
|
+
def lookup(self, name: str) -> ir.Value | None:
|
|
276
|
+
"""Lookup value by name from the current Scope."""
|
|
277
|
+
return self.current_scope().lookup(name)
|
|
278
|
+
|
|
279
|
+
def bind(self, name: str, value: ir.Value) -> None:
|
|
280
|
+
"""Bind value to name in the current Scope."""
|
|
281
|
+
self.current_scope().bind(name, value)
|
|
282
|
+
|
|
283
|
+
def lookup_or_create(self, name: str) -> ir.Value:
|
|
284
|
+
"""Lookup value by name from the current Scope. If not found, create a new one."""
|
|
285
|
+
return self.current_scope().lookup_or_create(name)
|
|
286
|
+
|
|
287
|
+
def lookup_ref_attribute(self, ref_attr_name: str) -> onnx.AttributeProto | None:
|
|
288
|
+
return self.current_scope().lookup_ref_attribute(ref_attr_name)
|
|
289
|
+
|
|
290
|
+
def bind_ref_attribute(self, ref_attr_name: str, attr: onnx.AttributeProto) -> None:
|
|
291
|
+
self.current_scope().bind_ref_attribute(ref_attr_name, attr)
|
|
292
|
+
|
|
293
|
+
def enter_graph_scope(self, graph: onnx.GraphProto) -> None:
|
|
294
|
+
self.current_scope().enter_sub_scope(graph)
|
|
295
|
+
|
|
296
|
+
def exit_graph_scope(self) -> SubScope:
|
|
297
|
+
sub_scope = self.current_scope().exit_sub_scope()
|
|
298
|
+
assert isinstance(sub_scope.owner, onnx.GraphProto), "Expected graph scope."
|
|
299
|
+
return sub_scope
|
|
300
|
+
|
|
301
|
+
def enter_function_scope(self, function: onnx.FunctionProto) -> None:
|
|
302
|
+
self._scopes.append(Scope())
|
|
303
|
+
self.current_scope().enter_sub_scope(function)
|
|
304
|
+
|
|
305
|
+
def exit_function_scope(self) -> SubScope:
|
|
306
|
+
sub_scope = self.current_scope().exit_sub_scope()
|
|
307
|
+
assert isinstance(sub_scope.owner, onnx.FunctionProto), "Expected function scope."
|
|
308
|
+
self._scopes.pop()
|
|
309
|
+
return sub_scope
|
|
310
|
+
|
|
311
|
+
def current_function(self) -> onnx.FunctionProto | None:
|
|
312
|
+
return self.current_scope().current_function()
|
|
313
|
+
|
|
314
|
+
def current_graph(self) -> onnx.GraphProto | None:
|
|
315
|
+
return self.current_scope().current_graph()
|
|
316
|
+
|
|
317
|
+
def __str__(self) -> str:
|
|
318
|
+
strs = ["ScopeStach:"]
|
|
319
|
+
for i, scope in enumerate(self._scopes):
|
|
320
|
+
strs.append(f" Scope {i}:")
|
|
321
|
+
strs.extend(scope.readable_strs(indent=2))
|
|
322
|
+
return "\n".join(strs)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class ProtoVisitorCore:
|
|
326
|
+
def visit_model(self, model: onnx.ModelProto):
|
|
327
|
+
self.process_model(model)
|
|
328
|
+
for opset in model.opset_import:
|
|
329
|
+
self.process_opset_import(opset)
|
|
330
|
+
self.visit_graph(model.graph)
|
|
331
|
+
for function in model.functions:
|
|
332
|
+
self.visit_function(function)
|
|
333
|
+
|
|
334
|
+
def process_model(self, model: onnx.ModelProto):
|
|
335
|
+
pass
|
|
336
|
+
|
|
337
|
+
def process_opset_import(self, opset: onnx.OperatorSetIdProto):
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
def visit_graph(self, graph: onnx.GraphProto):
|
|
341
|
+
self.enter_scope(graph)
|
|
342
|
+
self.process_graph(graph)
|
|
343
|
+
for input in graph.input:
|
|
344
|
+
self.process_graph_input(input)
|
|
345
|
+
for init in graph.initializer:
|
|
346
|
+
self.process_initializer(init)
|
|
347
|
+
for value_info in graph.value_info:
|
|
348
|
+
self.process_value_info(value_info)
|
|
349
|
+
for node in graph.node:
|
|
350
|
+
self.visit_node(node)
|
|
351
|
+
for output in graph.output:
|
|
352
|
+
self.process_graph_output(output)
|
|
353
|
+
self.exit_scope(graph)
|
|
354
|
+
|
|
355
|
+
def visit_function(self, function: onnx.FunctionProto):
|
|
356
|
+
self.enter_function_scope(function)
|
|
357
|
+
self.process_function(function)
|
|
358
|
+
for input in function.input:
|
|
359
|
+
self.process_function_input(input)
|
|
360
|
+
for node in function.node:
|
|
361
|
+
self.visit_node(node)
|
|
362
|
+
for output in function.output:
|
|
363
|
+
self.process_function_output(output)
|
|
364
|
+
self.exit_function_scope(function)
|
|
365
|
+
|
|
366
|
+
def process_function_input(self, input: str):
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
def process_function_output(self, output: str):
|
|
370
|
+
pass
|
|
371
|
+
|
|
372
|
+
def process_function(self, function: onnx.FunctionProto):
|
|
373
|
+
pass
|
|
374
|
+
|
|
375
|
+
def enter_function_scope(self, function: onnx.FunctionProto):
|
|
376
|
+
pass
|
|
377
|
+
|
|
378
|
+
def exit_function_scope(self, function: onnx.FunctionProto) -> SubScope:
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
def enter_scope(self, graph: onnx.GraphProto):
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
def process_graph(self, graph: onnx.GraphProto):
|
|
385
|
+
pass
|
|
386
|
+
|
|
387
|
+
def exit_scope(self, graph: onnx.GraphProto) -> SubScope:
|
|
388
|
+
pass
|
|
389
|
+
|
|
390
|
+
def process_graph_input(self, input: onnx.ValueInfoProto):
|
|
391
|
+
pass
|
|
392
|
+
|
|
393
|
+
def process_initializer(self, init: onnx.TensorProto):
|
|
394
|
+
pass
|
|
395
|
+
|
|
396
|
+
def process_value_info(self, value_info: onnx.ValueInfoProto):
|
|
397
|
+
pass
|
|
398
|
+
|
|
399
|
+
def visit_node(self, node: onnx.NodeProto):
|
|
400
|
+
self.process_node(node)
|
|
401
|
+
for attr in node.attribute:
|
|
402
|
+
self.visit_attribute(attr)
|
|
403
|
+
|
|
404
|
+
def process_node(self, node: onnx.NodeProto) -> Sequence[onnx.NodeProto] | None:
|
|
405
|
+
pass
|
|
406
|
+
|
|
407
|
+
def process_graph_output(self, output: onnx.ValueInfoProto):
|
|
408
|
+
pass
|
|
409
|
+
|
|
410
|
+
def visit_attribute(self, attr: onnx.AttributeProto):
|
|
411
|
+
self.process_attribute(attr)
|
|
412
|
+
if attr.HasField("g"):
|
|
413
|
+
self.visit_graph(attr.g)
|
|
414
|
+
elif len(attr.graphs) > 0:
|
|
415
|
+
for graph in attr.graphs:
|
|
416
|
+
self.visit_graph(graph)
|
|
417
|
+
|
|
418
|
+
def process_attribute(self, attr: onnx.AttributeProto):
|
|
419
|
+
pass
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class ProtoVisitor(ProtoVisitorCore):
|
|
423
|
+
def __init__(
|
|
424
|
+
self, external_data_folder: str = "", *, do_shape_inference: bool = False
|
|
425
|
+
) -> None:
|
|
426
|
+
super().__init__()
|
|
427
|
+
self.scopes = ScopeStack()
|
|
428
|
+
self.function_shape_env = FunctionShapeEnv()
|
|
429
|
+
self.version_map = {} # Map from domain to version
|
|
430
|
+
self.do_shape_inference = do_shape_inference
|
|
431
|
+
self.external_data_folder = external_data_folder
|
|
432
|
+
self.modified = False
|
|
433
|
+
|
|
434
|
+
def process_opset_import(self, opset: onnx.OperatorSetIdProto):
|
|
435
|
+
domain = normalize_domain(opset.domain)
|
|
436
|
+
self.version_map[domain] = opset.version
|
|
437
|
+
|
|
438
|
+
def lookup_version(self, domain: str) -> int:
|
|
439
|
+
domain = normalize_domain(domain)
|
|
440
|
+
return self.version_map.get(domain, 1) # TODO: handle missing domain
|
|
441
|
+
|
|
442
|
+
def lookup(self, name: str) -> ir.Value | None:
|
|
443
|
+
if name == "":
|
|
444
|
+
return None
|
|
445
|
+
if (result := self.scopes.lookup(name)) is None:
|
|
446
|
+
logger.debug("Lookup value %s unfound.", name)
|
|
447
|
+
raise ValueError(
|
|
448
|
+
f"Undefined variable {name}.\n"
|
|
449
|
+
f"Available variables: {self.scopes.current_scope()}"
|
|
450
|
+
)
|
|
451
|
+
logger.debug("Lookup value %s. Shape %s", name, result.tensor_shape_proto())
|
|
452
|
+
return result
|
|
453
|
+
|
|
454
|
+
def bind(self, name: str, value: ir.Value) -> None:
|
|
455
|
+
logger.debug("Binding value %s. Shape %s", name, value.tensor_shape_proto())
|
|
456
|
+
self.scopes.bind(name, value)
|
|
457
|
+
|
|
458
|
+
def lookup_or_create(self, name: str) -> ir.Value:
|
|
459
|
+
return self.scopes.lookup_or_create(name)
|
|
460
|
+
|
|
461
|
+
def has_input(self, node: onnx.NodeProto, index: int) -> bool:
|
|
462
|
+
return index < len(node.input) and node.input[index] != ""
|
|
463
|
+
|
|
464
|
+
# TODO: Cleanup handling of undefined variables. May fail in some of methods below.
|
|
465
|
+
|
|
466
|
+
def get_input(self, node: onnx.NodeProto, index: int) -> ir.Value | None:
|
|
467
|
+
if index < len(node.input):
|
|
468
|
+
return self.lookup(node.input[index])
|
|
469
|
+
return None
|
|
470
|
+
|
|
471
|
+
def input_type(self, node: onnx.NodeProto, index: int) -> onnx.TypeProto | None:
|
|
472
|
+
info = self.get_input(node, index)
|
|
473
|
+
return info.type if info is not None else None
|
|
474
|
+
|
|
475
|
+
def input_element_type(self, node: onnx.NodeProto, index: int) -> int | None:
|
|
476
|
+
info = self.get_input(node, index)
|
|
477
|
+
return info.element_type if info is not None else None
|
|
478
|
+
|
|
479
|
+
def input_shape(self, node: onnx.NodeProto, index: int) -> onnx.TensorShapeProto | None:
|
|
480
|
+
info = self.get_input(node, index)
|
|
481
|
+
return info.tensor_shape_proto() if info is not None else None
|
|
482
|
+
|
|
483
|
+
def input_const_value(self, node: onnx.NodeProto, index: int) -> Any:
|
|
484
|
+
if not self.has_input(node, index):
|
|
485
|
+
return None # This is treated as a known constant value "None"
|
|
486
|
+
info = self.get_input(node, index)
|
|
487
|
+
return info.value
|
|
488
|
+
|
|
489
|
+
def has_output(self, node: onnx.NodeProto, index: int) -> bool:
|
|
490
|
+
return index < len(node.output) and node.output[index] != ""
|
|
491
|
+
|
|
492
|
+
def get_output(self, node: onnx.NodeProto, index: int) -> ir.Value | None:
|
|
493
|
+
if index < len(node.output):
|
|
494
|
+
return self.lookup(node.output[index])
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
def get_input_value(
|
|
498
|
+
self, node: onnx.NodeProto, index: int, default: Any | None = None
|
|
499
|
+
) -> Any | None:
|
|
500
|
+
info = self.get_input(node, index)
|
|
501
|
+
if info is not None:
|
|
502
|
+
return info.value
|
|
503
|
+
return default
|
|
504
|
+
|
|
505
|
+
def get_input_type(
|
|
506
|
+
self, node: onnx.NodeProto, index: int, default: onnx.TypeProto | None = None
|
|
507
|
+
) -> onnx.TypeProto | None:
|
|
508
|
+
info = self.get_input(node, index)
|
|
509
|
+
if info is not None:
|
|
510
|
+
return info.type
|
|
511
|
+
return default
|
|
512
|
+
|
|
513
|
+
def enter_scope(self, graph: onnx.GraphProto):
|
|
514
|
+
logger.debug("enter_scope: graph %s", graph.name)
|
|
515
|
+
self.scopes.enter_graph_scope(graph)
|
|
516
|
+
|
|
517
|
+
def exit_scope(self, graph: onnx.GraphProto) -> SubScope:
|
|
518
|
+
logger.debug("exit_scope: graph %s", graph.name)
|
|
519
|
+
return self.scopes.exit_graph_scope()
|
|
520
|
+
|
|
521
|
+
def enter_function_scope(self, function: onnx.FunctionProto):
|
|
522
|
+
logger.debug("enter_function_scope: function %s", function.name)
|
|
523
|
+
self.scopes.enter_function_scope(function)
|
|
524
|
+
ir_values = self.function_shape_env.get_ir_values(function)
|
|
525
|
+
for name, ir_value in ir_values.items():
|
|
526
|
+
inferred_ir_value = self.lookup_or_create(name)
|
|
527
|
+
updated_ir_value = _override_inferred_value_type_with_symbolic_value_type(
|
|
528
|
+
ir_value, inferred_ir_value
|
|
529
|
+
)
|
|
530
|
+
self.bind(name, updated_ir_value)
|
|
531
|
+
|
|
532
|
+
def exit_function_scope(self, function: onnx.FunctionProto) -> SubScope:
|
|
533
|
+
logger.debug("exit_function_scope: function %s", function.name)
|
|
534
|
+
# Sync ir value back to function_shape_env
|
|
535
|
+
function_scope = self.scopes.exit_function_scope()
|
|
536
|
+
for ir_value in function_scope.values.values():
|
|
537
|
+
self.function_shape_env.bind(ir_value, *ir.get_function_id(function))
|
|
538
|
+
return function_scope
|
|
539
|
+
|
|
540
|
+
def process_initializer(self, init: onnx.TensorProto):
|
|
541
|
+
array = onnx.numpy_helper.to_array(init, self.external_data_folder)
|
|
542
|
+
self.bind(
|
|
543
|
+
init.name,
|
|
544
|
+
ir.Value(name=init.name, value=array, type=get_initializer_type(init)),
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
def process_graph_input(self, input: onnx.ValueInfoProto):
|
|
548
|
+
self.bind(input.name, ir.Value(name=input.name, type=input.type))
|
|
549
|
+
|
|
550
|
+
def process_value_info(self, value_info: onnx.ValueInfoProto):
|
|
551
|
+
logger.debug("process_value_info: %s", value_info)
|
|
552
|
+
value = self.lookup_or_create(value_info.name)
|
|
553
|
+
value.type = value_info.type
|
|
554
|
+
# Populate function shape environment
|
|
555
|
+
self.function_shape_env.load_from_value_info(value_info)
|
|
556
|
+
|
|
557
|
+
def process_node(self, node: onnx.NodeProto) -> Sequence[onnx.NodeProto] | None:
|
|
558
|
+
output_types = {}
|
|
559
|
+
if self.do_shape_inference and not is_control_flow_op(node):
|
|
560
|
+
# Control-flow ops are more complicated. Not supported here yet.
|
|
561
|
+
# TODO: handle optional inputs
|
|
562
|
+
def get_constant_value(i: int) -> onnx.TensorProto | None:
|
|
563
|
+
value = self.input_const_value(node, i)
|
|
564
|
+
if isinstance(value, np.ndarray) and value.size < 20:
|
|
565
|
+
return onnx.numpy_helper.from_array(value, node.input[i])
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
input_types = {x: self.input_type(node, i) for i, x in enumerate(node.input)}
|
|
569
|
+
input_data = {x: get_constant_value(i) for i, x in enumerate(node.input)}
|
|
570
|
+
input_data = {k: v for k, v in input_data.items() if v is not None}
|
|
571
|
+
if any(t is None for t in input_types.values()):
|
|
572
|
+
logger.debug(
|
|
573
|
+
"Skipping shape inference for node %s due to missing input type.",
|
|
574
|
+
node.name,
|
|
575
|
+
)
|
|
576
|
+
else:
|
|
577
|
+
# TODO: pass in constant values, ir_version
|
|
578
|
+
try:
|
|
579
|
+
schema = onnx.defs.get_schema(
|
|
580
|
+
node.op_type, self.lookup_version(node.domain), node.domain
|
|
581
|
+
)
|
|
582
|
+
output_types = onnx.shape_inference.infer_node_outputs(
|
|
583
|
+
schema, node, input_types, input_data
|
|
584
|
+
)
|
|
585
|
+
except Exception as e:
|
|
586
|
+
logger.debug(
|
|
587
|
+
"Skipping shape inference for node %s due to exception: %s",
|
|
588
|
+
node.name,
|
|
589
|
+
e,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
for output in node.output:
|
|
593
|
+
if output == "":
|
|
594
|
+
continue
|
|
595
|
+
info = self.lookup_or_create(output)
|
|
596
|
+
if output in output_types:
|
|
597
|
+
if info.type is not None:
|
|
598
|
+
if (
|
|
599
|
+
info.type.tensor_type.elem_type
|
|
600
|
+
!= output_types[output].tensor_type.elem_type
|
|
601
|
+
):
|
|
602
|
+
logger.warning(
|
|
603
|
+
"Overriding existing type %s with inferred type %s for %s",
|
|
604
|
+
info.type,
|
|
605
|
+
output_types[output],
|
|
606
|
+
output,
|
|
607
|
+
)
|
|
608
|
+
# TODO: merge types
|
|
609
|
+
info.type = output_types[output]
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
class ProtoTransformer(ProtoVisitor):
|
|
613
|
+
# TODO(lowpri) Practically this is useless.
|
|
614
|
+
# Subgraph only exist in 'if' nodes. 'if' nodes only exist in torchlib functions.
|
|
615
|
+
# There is no pre-existing value_info in torchlib functions.
|
|
616
|
+
# def exit_scope(self, graph: onnx.GraphProto) -> SubScope:
|
|
617
|
+
# # Also sync updated ir values back to value_info in graph.
|
|
618
|
+
# sub_scope = super().exit_scope(graph)
|
|
619
|
+
|
|
620
|
+
def visit_node(self, node: onnx.NodeProto) -> list[onnx.NodeProto] | None:
|
|
621
|
+
replacement = self.process_node(node)
|
|
622
|
+
logger.debug(
|
|
623
|
+
"visit_node: %s::%s %s replacement %s",
|
|
624
|
+
node.domain,
|
|
625
|
+
node.op_type,
|
|
626
|
+
node.name,
|
|
627
|
+
"found" if replacement is not None else "missed",
|
|
628
|
+
)
|
|
629
|
+
if replacement is None:
|
|
630
|
+
# No change. Process attributes.
|
|
631
|
+
for attr in node.attribute:
|
|
632
|
+
self.visit_attribute(attr)
|
|
633
|
+
return None
|
|
634
|
+
else:
|
|
635
|
+
self.modified = True
|
|
636
|
+
# We recursively visit the replacement nodes.
|
|
637
|
+
result = []
|
|
638
|
+
for newnode in replacement:
|
|
639
|
+
n = self.visit_node(newnode)
|
|
640
|
+
if n is not None:
|
|
641
|
+
result.extend(n)
|
|
642
|
+
else:
|
|
643
|
+
result.append(newnode)
|
|
644
|
+
return result
|
|
645
|
+
|
|
646
|
+
def visit_graph(self, graph: onnx.GraphProto) -> dict[str, ir.Value]:
|
|
647
|
+
self.enter_scope(graph)
|
|
648
|
+
self.process_graph(graph)
|
|
649
|
+
for input in graph.input:
|
|
650
|
+
self.process_graph_input(input)
|
|
651
|
+
for init in graph.initializer:
|
|
652
|
+
self.process_initializer(init)
|
|
653
|
+
for value_info in graph.value_info:
|
|
654
|
+
self.process_value_info(value_info)
|
|
655
|
+
updates = []
|
|
656
|
+
nodes = graph.node
|
|
657
|
+
for i, node in enumerate(nodes):
|
|
658
|
+
replacement = self.visit_node(node)
|
|
659
|
+
if replacement is not None:
|
|
660
|
+
updates.append((i, replacement))
|
|
661
|
+
for i, replacement in reversed(updates):
|
|
662
|
+
old_node_name = nodes[i].name
|
|
663
|
+
del nodes[i]
|
|
664
|
+
for newnode in reversed(replacement):
|
|
665
|
+
logger.debug(
|
|
666
|
+
"Replacement node %s for %s. Size %s",
|
|
667
|
+
newnode.name,
|
|
668
|
+
old_node_name,
|
|
669
|
+
newnode.ByteSize(),
|
|
670
|
+
)
|
|
671
|
+
nodes.insert(i, newnode)
|
|
672
|
+
for output in graph.output:
|
|
673
|
+
self.process_graph_output(output)
|
|
674
|
+
return self.exit_scope(graph)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class FunctionCallsiteAnalysis(ProtoVisitor):
|
|
678
|
+
"""Collects the callsites of each function."""
|
|
679
|
+
|
|
680
|
+
def __init__(self):
|
|
681
|
+
super().__init__()
|
|
682
|
+
self.functions: dict[ir.FunctionId, onnx.FunctionProto] = {}
|
|
683
|
+
self.function_calls: dict[ir.FunctionId, list[onnx.NodeProto]] = {}
|
|
684
|
+
|
|
685
|
+
def visit_function(self, function: onnx.FunctionProto):
|
|
686
|
+
# Do not visit function via model.functions.
|
|
687
|
+
# Only visit function at callsites.
|
|
688
|
+
# The purpose of this analysis is to collect the callsites of each function.
|
|
689
|
+
pass
|
|
690
|
+
|
|
691
|
+
def visit_node(self, node: onnx.NodeProto) -> None:
|
|
692
|
+
if is_local_function_node(node, self.functions):
|
|
693
|
+
function_id = ir.get_function_id_from_node(node)
|
|
694
|
+
self.function_calls.setdefault(function_id, []).append(node)
|
|
695
|
+
for subnode in self.functions[function_id].node:
|
|
696
|
+
self.visit_node(subnode)
|
|
697
|
+
|
|
698
|
+
def visit_model(self, model: onnx.ModelProto) -> None:
|
|
699
|
+
for function in model.functions:
|
|
700
|
+
self.functions[ir.get_function_id(function)] = function
|
|
701
|
+
|
|
702
|
+
super().visit_model(model)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
class FunctionRenamer:
|
|
706
|
+
_POSTFIX_FORMAT = "{name}|{postfix}_{count}"
|
|
707
|
+
|
|
708
|
+
def __init__(self, postfix="folded"):
|
|
709
|
+
self._function_key_to_instance_count = {}
|
|
710
|
+
self._postfix = postfix
|
|
711
|
+
|
|
712
|
+
def rename(self, function: onnx.FunctionProto) -> None:
|
|
713
|
+
domain = function.domain
|
|
714
|
+
name = function.name
|
|
715
|
+
key = (domain, name)
|
|
716
|
+
self._function_key_to_instance_count.setdefault(key, 0)
|
|
717
|
+
function.name = self._POSTFIX_FORMAT.format(
|
|
718
|
+
name=name,
|
|
719
|
+
postfix=self._postfix,
|
|
720
|
+
count=self._function_key_to_instance_count[key],
|
|
721
|
+
)
|
|
722
|
+
self._function_key_to_instance_count[key] += 1
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
class FunctionCallsiteProtoTransformer(ProtoTransformer):
|
|
726
|
+
"""Unlike other base visitors, this is a special visitor that visits functions at their callsite.
|
|
727
|
+
|
|
728
|
+
This allows transforming and constructing specialized functions based on callsite context.
|
|
729
|
+
"""
|
|
730
|
+
|
|
731
|
+
_functions: dict[ir.FunctionId, onnx.FunctionProto]
|
|
732
|
+
_function_callsites: dict[ir.FunctionId, list[onnx.NodeProto]]
|
|
733
|
+
_new_functions: list[onnx.FunctionProto]
|
|
734
|
+
_function_renamer: FunctionRenamer
|
|
735
|
+
|
|
736
|
+
def _gather_function_metadata(self, model: onnx.ModelProto):
|
|
737
|
+
analysis = FunctionCallsiteAnalysis()
|
|
738
|
+
analysis.visit_model(model)
|
|
739
|
+
self._functions = analysis.functions
|
|
740
|
+
self._function_callsites = analysis.function_calls
|
|
741
|
+
self._new_functions = []
|
|
742
|
+
self._function_renamer = FunctionRenamer()
|
|
743
|
+
|
|
744
|
+
def process_function_outputs(self, function: onnx.FunctionProto) -> bool:
|
|
745
|
+
"""Process function outputs.
|
|
746
|
+
|
|
747
|
+
This method is called when a function is visited at its callsite.
|
|
748
|
+
|
|
749
|
+
Returns:
|
|
750
|
+
True if the function outputs are modified.
|
|
751
|
+
"""
|
|
752
|
+
del function # Unused
|
|
753
|
+
return False
|
|
754
|
+
|
|
755
|
+
def process_function_node_outputs(
|
|
756
|
+
self,
|
|
757
|
+
node: onnx.NodeProto,
|
|
758
|
+
function_scope: SubScope,
|
|
759
|
+
) -> None:
|
|
760
|
+
"""Fetch value infos of function output to re-bind them for function node output."""
|
|
761
|
+
function = function_scope.owner
|
|
762
|
+
output_values = [function_scope.lookup(output) for output in function.output]
|
|
763
|
+
for actual_name, formal_value in zip(node.output, output_values):
|
|
764
|
+
if formal_value is None:
|
|
765
|
+
raise RuntimeError(
|
|
766
|
+
"Missing output %s in function-call to %s",
|
|
767
|
+
actual_name,
|
|
768
|
+
node.op_type,
|
|
769
|
+
)
|
|
770
|
+
actual_value = self.lookup_or_create(actual_name)
|
|
771
|
+
actual_value.identity_merge_from(formal_value)
|
|
772
|
+
if logger.level <= logging.INFO:
|
|
773
|
+
logger.info(
|
|
774
|
+
"Binding outputs for function %s. %s => %s",
|
|
775
|
+
function.name,
|
|
776
|
+
actual_value,
|
|
777
|
+
node.output,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
def lookup_ref_attribute(self, ref_attr_name: str) -> onnx.AttributeProto | None:
|
|
781
|
+
return self.scopes.lookup_ref_attribute(ref_attr_name)
|
|
782
|
+
|
|
783
|
+
def bind_ref_attribute(self, ref_attr_name: str, attr: onnx.AttributeProto) -> None:
|
|
784
|
+
self.scopes.bind_ref_attribute(ref_attr_name, attr)
|
|
785
|
+
|
|
786
|
+
def visit_model(self, model: onnx.ModelProto):
|
|
787
|
+
self._gather_function_metadata(model)
|
|
788
|
+
|
|
789
|
+
self.process_model(model)
|
|
790
|
+
for opset in model.opset_import:
|
|
791
|
+
self.process_opset_import(opset)
|
|
792
|
+
self.visit_graph(model.graph)
|
|
793
|
+
|
|
794
|
+
for new_function in self._new_functions:
|
|
795
|
+
model.functions.append(new_function)
|
|
796
|
+
|
|
797
|
+
self.function_shape_env.save_to_model_proto(model)
|
|
798
|
+
|
|
799
|
+
def visit_node(self, node: onnx.NodeProto) -> list[onnx.NodeProto] | None:
|
|
800
|
+
if is_local_function_node(node, self._functions):
|
|
801
|
+
function_id = ir.get_function_id_from_node(node)
|
|
802
|
+
if function_id not in self._functions:
|
|
803
|
+
# Do not recursively visit new functions.
|
|
804
|
+
return None
|
|
805
|
+
replacement, _ = self.process_function_node(node)
|
|
806
|
+
else:
|
|
807
|
+
replacement = self.process_node(node)
|
|
808
|
+
logger.debug(
|
|
809
|
+
"visit_node: %s::%s %s replacement %s",
|
|
810
|
+
node.domain,
|
|
811
|
+
node.op_type,
|
|
812
|
+
node.name,
|
|
813
|
+
"found" if replacement is not None else "missed",
|
|
814
|
+
)
|
|
815
|
+
if replacement is None:
|
|
816
|
+
# No change. Process attributes.
|
|
817
|
+
for attr in node.attribute:
|
|
818
|
+
self.visit_attribute(attr)
|
|
819
|
+
return None
|
|
820
|
+
else:
|
|
821
|
+
self.modified = True
|
|
822
|
+
# We recursively visit the replacement nodes.
|
|
823
|
+
result = []
|
|
824
|
+
for newnode in replacement:
|
|
825
|
+
n = self.visit_node(newnode)
|
|
826
|
+
if n is not None:
|
|
827
|
+
result.extend(n)
|
|
828
|
+
else:
|
|
829
|
+
result.append(newnode)
|
|
830
|
+
return result
|
|
831
|
+
|
|
832
|
+
def process_function_node(
|
|
833
|
+
self, node: onnx.NodeProto
|
|
834
|
+
) -> tuple[list[onnx.NodeProto] | None, onnx.FunctionProto | None]:
|
|
835
|
+
function_id = ir.get_function_id_from_node(node)
|
|
836
|
+
function = self._functions[function_id]
|
|
837
|
+
|
|
838
|
+
is_unique_callsite = len(self._function_callsites[function_id]) == 1
|
|
839
|
+
if not is_unique_callsite:
|
|
840
|
+
mutable_function = onnx.FunctionProto()
|
|
841
|
+
mutable_function.CopyFrom(function)
|
|
842
|
+
else:
|
|
843
|
+
mutable_function = function
|
|
844
|
+
|
|
845
|
+
logger.info("Visit function %s node %s", function_id, node.name)
|
|
846
|
+
actual_input_value_infos = [self.lookup(input) for input in node.input]
|
|
847
|
+
# Handle omitted inputs, these are considered optional inputs of the function.
|
|
848
|
+
actual_input_value_infos.extend(
|
|
849
|
+
[None] * (len(function.input) - len(actual_input_value_infos))
|
|
850
|
+
)
|
|
851
|
+
ref_attributes = {
|
|
852
|
+
attr_proto.name: self.lookup_ref_attribute(attr_proto.ref_attr_name)
|
|
853
|
+
for attr_proto in node.attribute
|
|
854
|
+
if attr_proto.ref_attr_name
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
self.enter_function_scope(mutable_function)
|
|
858
|
+
if logger.level <= logging.INFO:
|
|
859
|
+
printable_actual_input_value_infos = [str(x) for x in actual_input_value_infos]
|
|
860
|
+
logger.info(
|
|
861
|
+
"Actual input value infos: %s",
|
|
862
|
+
printable_actual_input_value_infos,
|
|
863
|
+
)
|
|
864
|
+
logger.info("Enter function scope: %s", self.scopes.current_scope())
|
|
865
|
+
|
|
866
|
+
logger.debug("Binding inputs for function %s", function.name)
|
|
867
|
+
for actual_input_value_info, formal_input in zip(
|
|
868
|
+
actual_input_value_infos, function.input
|
|
869
|
+
):
|
|
870
|
+
formal_info = ir.Value(formal_input)
|
|
871
|
+
if actual_input_value_info is not None:
|
|
872
|
+
formal_info.identity_merge_from(actual_input_value_info)
|
|
873
|
+
self.bind(formal_input, formal_info)
|
|
874
|
+
|
|
875
|
+
for attr_proto in function.attribute_proto:
|
|
876
|
+
# Default value of function attributes.
|
|
877
|
+
self.bind_ref_attribute(attr_proto.name, attr_proto)
|
|
878
|
+
|
|
879
|
+
for attr_proto in node.attribute:
|
|
880
|
+
if attr_proto.ref_attr_name:
|
|
881
|
+
concrete_attribute = ref_attributes.get(attr_proto.name)
|
|
882
|
+
if concrete_attribute is None:
|
|
883
|
+
continue
|
|
884
|
+
self.bind_ref_attribute(attr_proto.name, concrete_attribute)
|
|
885
|
+
else:
|
|
886
|
+
self.bind_ref_attribute(attr_proto.name, attr_proto)
|
|
887
|
+
|
|
888
|
+
# Visit inner function nodes.
|
|
889
|
+
node_updates: list[tuple[int, list[onnx.NodeProto]]] = []
|
|
890
|
+
nodes = mutable_function.node
|
|
891
|
+
for i, inner_node in enumerate(nodes):
|
|
892
|
+
replacement = self.visit_node(inner_node)
|
|
893
|
+
if replacement is not None:
|
|
894
|
+
node_updates.append((i, replacement))
|
|
895
|
+
for i, replacement in reversed(node_updates):
|
|
896
|
+
old_node_name = nodes[i].name
|
|
897
|
+
old_node_op_type = nodes[i].op_type
|
|
898
|
+
del nodes[i]
|
|
899
|
+
for newnode in reversed(replacement):
|
|
900
|
+
logger.debug(
|
|
901
|
+
"Replacement node inside function %s: %s for %s %s. Size %s",
|
|
902
|
+
node.name,
|
|
903
|
+
newnode.output,
|
|
904
|
+
old_node_name,
|
|
905
|
+
old_node_op_type,
|
|
906
|
+
newnode.ByteSize(),
|
|
907
|
+
)
|
|
908
|
+
nodes.insert(i, newnode)
|
|
909
|
+
added_domains = set()
|
|
910
|
+
del mutable_function.opset_import[:]
|
|
911
|
+
for inner_node in nodes:
|
|
912
|
+
# Update opset_import if needed.
|
|
913
|
+
if inner_node.domain not in added_domains:
|
|
914
|
+
version = self.lookup_version(inner_node.domain)
|
|
915
|
+
mutable_function.opset_import.append(
|
|
916
|
+
onnx.OperatorSetIdProto(domain=inner_node.domain, version=version)
|
|
917
|
+
)
|
|
918
|
+
added_domains.add(inner_node.domain)
|
|
919
|
+
|
|
920
|
+
output_updates = self.process_function_outputs(mutable_function)
|
|
921
|
+
|
|
922
|
+
is_new_function = not is_unique_callsite and (node_updates or output_updates)
|
|
923
|
+
if is_new_function:
|
|
924
|
+
self._new_functions.append(mutable_function)
|
|
925
|
+
self._function_renamer.rename(mutable_function)
|
|
926
|
+
node.op_type = mutable_function.name
|
|
927
|
+
|
|
928
|
+
function_scope = self.exit_function_scope(mutable_function)
|
|
929
|
+
|
|
930
|
+
self.process_function_node_outputs(node, function_scope)
|
|
931
|
+
|
|
932
|
+
logger.info("Exit function scope: %s", function_scope)
|
|
933
|
+
logger.info("Exit function %s node %s", function_id, node.name)
|
|
934
|
+
|
|
935
|
+
if is_new_function:
|
|
936
|
+
return [node], mutable_function
|
|
937
|
+
return None, None
|