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
onnxscript/converter.py
ADDED
|
@@ -0,0 +1,1470 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import logging
|
|
7
|
+
from typing import (
|
|
8
|
+
TYPE_CHECKING,
|
|
9
|
+
Any,
|
|
10
|
+
Dict,
|
|
11
|
+
List,
|
|
12
|
+
NoReturn,
|
|
13
|
+
Optional,
|
|
14
|
+
Sequence,
|
|
15
|
+
Tuple,
|
|
16
|
+
Union,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
import onnx
|
|
20
|
+
|
|
21
|
+
import onnxscript
|
|
22
|
+
from onnxscript import irbuilder, onnx_types, sourceinfo, values
|
|
23
|
+
from onnxscript import type_annotation as ta
|
|
24
|
+
from onnxscript._internal import analysis, ast_utils, autocast, param_manipulation
|
|
25
|
+
|
|
26
|
+
PY_VERSION_GE_39 = ast_utils.PY_VERSION_GE_39
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("onnxscript")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Python-to-IR converter:
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def not_allowed(construct):
|
|
36
|
+
return f"{construct}not supported."
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TranslationError(Exception):
|
|
40
|
+
def __init__(self, *args: object) -> None:
|
|
41
|
+
super().__init__(*args)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def warn(msg):
|
|
45
|
+
logger.warning(msg)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def fail(msg) -> NoReturn:
|
|
49
|
+
raise TranslationError(msg)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def fail_if(cond, msg):
|
|
53
|
+
if cond:
|
|
54
|
+
raise TranslationError(msg)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ignore(cond, msg):
|
|
58
|
+
if cond:
|
|
59
|
+
warn(msg)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# map from python operators to ONNX ops
|
|
63
|
+
primop_map = {
|
|
64
|
+
ast.Add: "Add",
|
|
65
|
+
ast.And: "And",
|
|
66
|
+
ast.BitAnd: "And",
|
|
67
|
+
ast.BitOr: "Or",
|
|
68
|
+
ast.Div: "Div",
|
|
69
|
+
ast.Eq: "Equal",
|
|
70
|
+
ast.Gt: "Greater",
|
|
71
|
+
ast.GtE: "GreaterOrEqual",
|
|
72
|
+
ast.Lt: "Less",
|
|
73
|
+
ast.LtE: "LessOrEqual",
|
|
74
|
+
ast.MatMult: "MatMul",
|
|
75
|
+
ast.Mod: "Mod",
|
|
76
|
+
ast.Mult: "Mul",
|
|
77
|
+
ast.Not: "Not",
|
|
78
|
+
ast.NotEq: "NotEqual",
|
|
79
|
+
ast.Or: "Or",
|
|
80
|
+
ast.Pow: "Pow",
|
|
81
|
+
ast.Sub: "Sub",
|
|
82
|
+
ast.USub: "Neg",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Variable:
|
|
87
|
+
"""Represents an ONNX variable.
|
|
88
|
+
|
|
89
|
+
TODO(rama): Consider merging this with IRVar. However, "castable" is specific to this
|
|
90
|
+
converter.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(self, name: str, castable: bool = False):
|
|
94
|
+
"""Initialize the instance.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
name: Name of the ONNX variable
|
|
98
|
+
castable: Whether this variable is castable to a desired target type.
|
|
99
|
+
Used for ONNX variables representing constants created from python values
|
|
100
|
+
like 0 or 1 or 0.5 which are treated as polymorphic values castable to other
|
|
101
|
+
types as needed.
|
|
102
|
+
"""
|
|
103
|
+
self.name = name
|
|
104
|
+
self.is_castable = castable
|
|
105
|
+
|
|
106
|
+
def __str__(self) -> str:
|
|
107
|
+
return self.name
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if TYPE_CHECKING:
|
|
111
|
+
# The type-alias LocalSymValue represents the types of values that local names in a
|
|
112
|
+
# script-function may be bound to during translation, (ONNX IR values).
|
|
113
|
+
# TODO(rama): Rationalize this and values.SymbolValue
|
|
114
|
+
|
|
115
|
+
LocalSymValue = Union[values.SymbolValue, irbuilder.IRFunction]
|
|
116
|
+
|
|
117
|
+
# The type-alias PyValue is used to represent the types of python values that may be used
|
|
118
|
+
# in an ONNX Script function.
|
|
119
|
+
# TODO(rama): Flesh out the set of valid types here. These include values such as
|
|
120
|
+
# 1 (int), 1.0 (float), [2, 4], [1.0], etc. which will be converted to ONNX, for
|
|
121
|
+
# use as value-parameters or attribute-parameters in an ONNX call (Node).
|
|
122
|
+
|
|
123
|
+
PyValue = Any
|
|
124
|
+
|
|
125
|
+
# The type-alias SymValue denotes values that an identifier may be bound to during
|
|
126
|
+
# translation. A local name will be bound to a LocalSymValue, while a global name
|
|
127
|
+
# will be bound to a PyValue.
|
|
128
|
+
|
|
129
|
+
SymValue = Union[LocalSymValue, PyValue]
|
|
130
|
+
|
|
131
|
+
# PreferredName is a type-alias used to represent the preferred name used in the generated
|
|
132
|
+
# ONNX for a value returned by an expression. There is no guarantee that the specified
|
|
133
|
+
# name will be used exactly. The converter will modify the name (with a suffix),
|
|
134
|
+
# if necesssary, to ensure that it is unique (to ensure ONNX's SSA requirement).
|
|
135
|
+
|
|
136
|
+
PreferredName = str
|
|
137
|
+
|
|
138
|
+
# The type-alias OnnxVar indicates variable names used in the generated ONNX.
|
|
139
|
+
OnnxVarName = str
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Converter:
|
|
143
|
+
"""Main class to translate python code into ONNX operators.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
ir_builder: convert AST node into ONNX structures, if None,
|
|
147
|
+
class :class:`onnxscript.irbuilder.IRBuilder` is used
|
|
148
|
+
|
|
149
|
+
The class uses logger `onnxscript`. Logging can be enabled with the following code:
|
|
150
|
+
|
|
151
|
+
::
|
|
152
|
+
|
|
153
|
+
import logging
|
|
154
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
155
|
+
|
|
156
|
+
Or if you need to enable only the logger used by this module:
|
|
157
|
+
|
|
158
|
+
::
|
|
159
|
+
|
|
160
|
+
import logging
|
|
161
|
+
logger = logging.getLogger('onnxscript')
|
|
162
|
+
logger.setLevel(logging.DEBUG)
|
|
163
|
+
console = logging.StreamHandler()
|
|
164
|
+
logger.addHandler(console)
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
ir_builder: Optional[irbuilder.IRBuilder] = None,
|
|
170
|
+
opset: Optional[values.Opset] = None,
|
|
171
|
+
global_names: Optional[dict[str, Any]] = None,
|
|
172
|
+
source: Optional[str] = None,
|
|
173
|
+
default_opset: Optional[values.Opset] = None,
|
|
174
|
+
):
|
|
175
|
+
self.ir_builder = ir_builder or irbuilder.IRBuilder()
|
|
176
|
+
self.source = source
|
|
177
|
+
if global_names is not None:
|
|
178
|
+
# We make a copy in case function eval modifies it.
|
|
179
|
+
self.globals = global_names.copy()
|
|
180
|
+
self.this_module = opset
|
|
181
|
+
self.default_opset_ = default_opset
|
|
182
|
+
|
|
183
|
+
# States initialized by `_init_function_translation`
|
|
184
|
+
self._outer: List[irbuilder.IRFunction] = []
|
|
185
|
+
self._current_fn: irbuilder.IRFunction = None
|
|
186
|
+
self._nextvar: int = 0
|
|
187
|
+
self._used_vars: set[str] = set()
|
|
188
|
+
self._locals: List[Dict[str, LocalSymValue]] = [{}]
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def default_opset(self) -> values.Opset:
|
|
192
|
+
if self.default_opset_ is None:
|
|
193
|
+
raise RuntimeError(
|
|
194
|
+
"default_opset must be specified in script for functions "
|
|
195
|
+
"that do not contain any use of an ONNX opset."
|
|
196
|
+
)
|
|
197
|
+
return self.default_opset_
|
|
198
|
+
|
|
199
|
+
def _set_default_opset(self, opset: values.Opset, node: ast.AST) -> None:
|
|
200
|
+
if opset.domain != "":
|
|
201
|
+
return
|
|
202
|
+
if self.default_opset_ is not None:
|
|
203
|
+
if (
|
|
204
|
+
opset.domain != self.default_opset_.domain
|
|
205
|
+
or opset.version != self.default_opset_.version
|
|
206
|
+
):
|
|
207
|
+
self.fail(
|
|
208
|
+
node, f"Two distincts opset were used ({opset} != {self.default_opset_})."
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
self.default_opset_ = opset
|
|
212
|
+
|
|
213
|
+
def _find_onnx_opset(self, node: ast.AST) -> Optional[values.Opset]:
|
|
214
|
+
"""Find the (first) ONNX opset used in the function, if any."""
|
|
215
|
+
# Search for a Call expression of form "op.OpName(...)"
|
|
216
|
+
if isinstance(node, ast.Call):
|
|
217
|
+
if isinstance(node.func, ast.Attribute):
|
|
218
|
+
opset_expr = node.func.value
|
|
219
|
+
if isinstance(opset_expr, ast.Name):
|
|
220
|
+
if opset_expr.id in self.globals:
|
|
221
|
+
opset = self.globals[opset_expr.id]
|
|
222
|
+
if isinstance(opset, values.Opset) and opset.domain == "":
|
|
223
|
+
return opset
|
|
224
|
+
for child in ast.iter_child_nodes(node):
|
|
225
|
+
res = self._find_onnx_opset(child)
|
|
226
|
+
if res is not None:
|
|
227
|
+
return res
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
def _init_function_translation(self) -> None:
|
|
231
|
+
"""Initialize self for translating a new (top-level) function."""
|
|
232
|
+
self._outer = []
|
|
233
|
+
self._current_fn: Optional[irbuilder.IRFunction] = None
|
|
234
|
+
self._nextvar = 0
|
|
235
|
+
self._used_vars = set()
|
|
236
|
+
self._locals: List[Dict[str, LocalSymValue]] = [{}]
|
|
237
|
+
|
|
238
|
+
def _source_of(self, node: ast.AST) -> sourceinfo.SourceInfo:
|
|
239
|
+
return sourceinfo.SourceInfo(node, self.source, self._current_fn.name)
|
|
240
|
+
|
|
241
|
+
def _message(self, node: ast.AST, error_msg: str) -> str:
|
|
242
|
+
"""Constructs an error _message containing source information about an ast node."""
|
|
243
|
+
return self._source_of(node).msg(error_msg)
|
|
244
|
+
|
|
245
|
+
def warn(self, node: ast.AST, error_msg: str) -> None:
|
|
246
|
+
warn(self._message(node, error_msg))
|
|
247
|
+
|
|
248
|
+
def fail(self, node: ast.AST, error_msg: str) -> NoReturn:
|
|
249
|
+
fail(self._message(node, error_msg))
|
|
250
|
+
|
|
251
|
+
# Name resolution and namescopes: This component handles the following aspects:
|
|
252
|
+
# * Name-scopes are different in Python and the generated ONNX:
|
|
253
|
+
# - Control-flow blocks (a loop body or the then-or-else block of an if-stmt)
|
|
254
|
+
# form part of the same name-scope in Python, but will be mapped to a nested
|
|
255
|
+
# name-scope (as a sub-graph) in ONNX.
|
|
256
|
+
# * Script-time name-value tracking: Name _lookup during script-time returns
|
|
257
|
+
# statically-known information about the value the name will have at runtime.
|
|
258
|
+
def _enter_scope(self, name: str, parent_node: ast.AST):
|
|
259
|
+
"""Enter a control-flow block (a loop body or if-then-else branch).
|
|
260
|
+
The block is translated into a nested-scope in ONNX.
|
|
261
|
+
"""
|
|
262
|
+
self._outer.insert(0, self._current_fn)
|
|
263
|
+
self._current_fn = self.ir_builder.new_function(name)
|
|
264
|
+
self._locals.insert(0, {})
|
|
265
|
+
logger.debug("Converter:_enter_scope:%d:node:%s", len(self._locals), type(parent_node))
|
|
266
|
+
|
|
267
|
+
def _exit_scope(self) -> irbuilder.IRFunction:
|
|
268
|
+
"""Exit from a control-flow block (a loop body or if-then-else branch)."""
|
|
269
|
+
logger.debug("Converter:_exit_scope:%d", len(self._locals))
|
|
270
|
+
graph = self._current_fn
|
|
271
|
+
self._current_fn = self._outer.pop(0)
|
|
272
|
+
self._locals.pop(0)
|
|
273
|
+
return graph
|
|
274
|
+
|
|
275
|
+
def _current_scope(self) -> Dict[str, LocalSymValue]:
|
|
276
|
+
return self._locals[0]
|
|
277
|
+
|
|
278
|
+
def _bind(self, name: str, val: LocalSymValue) -> None:
|
|
279
|
+
logger.debug("Converter:_bind:%s", name)
|
|
280
|
+
self._locals[0][name] = val
|
|
281
|
+
|
|
282
|
+
def _lookup(
|
|
283
|
+
self, name: str, info: sourceinfo.SourceInfo, raise_exception: bool = True
|
|
284
|
+
) -> SymValue:
|
|
285
|
+
for scope in self._locals:
|
|
286
|
+
if name in scope:
|
|
287
|
+
return scope[name]
|
|
288
|
+
if name in self.globals:
|
|
289
|
+
return self.globals[name]
|
|
290
|
+
if raise_exception:
|
|
291
|
+
raise ValueError(info.msg(f"Unbound name: {name}."))
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
def generate_unique_name(self, candidate: str = "tmp") -> str:
|
|
295
|
+
# TODO(justinchuby): Can we reduce the O complexity of this function?
|
|
296
|
+
r = candidate
|
|
297
|
+
while r in self._used_vars:
|
|
298
|
+
r = f"{candidate}_{self._nextvar}"
|
|
299
|
+
self._nextvar = self._nextvar + 1
|
|
300
|
+
self._used_vars.add(r)
|
|
301
|
+
return r
|
|
302
|
+
|
|
303
|
+
def _make_onnx_attr(
|
|
304
|
+
self, attrname: str, attrval: Any, attrtype: Optional[int] = None
|
|
305
|
+
) -> irbuilder.IRAttributeValue:
|
|
306
|
+
def tensor_name_generator() -> str:
|
|
307
|
+
"""Return name to be used for tensor, if we need to create one."""
|
|
308
|
+
return self.generate_unique_name(f"attr_{attrname}")
|
|
309
|
+
|
|
310
|
+
proto = autocast.pyvalue_to_onnx_attribute(
|
|
311
|
+
attrname, attrval, tensor_name_generator, attrtype
|
|
312
|
+
)
|
|
313
|
+
return self.ir_builder.make_attr(proto)
|
|
314
|
+
|
|
315
|
+
def _to_onnx_attr_ref(
|
|
316
|
+
self, val: values.AttrRef, info: Optional[sourceinfo.SourceInfo]
|
|
317
|
+
) -> irbuilder.IRAttributeValue:
|
|
318
|
+
pytype = val.typeinfo
|
|
319
|
+
attrtype = ta.pytype_to_attrtype(pytype)
|
|
320
|
+
attrname = None
|
|
321
|
+
if attrtype is onnx.AttributeProto.FLOAT:
|
|
322
|
+
attrname = "value_float"
|
|
323
|
+
elif attrtype is onnx.AttributeProto.INT:
|
|
324
|
+
attrname = "value_int"
|
|
325
|
+
elif attrtype is onnx.AttributeProto.STRING:
|
|
326
|
+
attrname = "value_string"
|
|
327
|
+
elif attrtype is onnx.AttributeProto.INTS:
|
|
328
|
+
attrname = "value_ints"
|
|
329
|
+
else:
|
|
330
|
+
msg = f"Unsupported attribute type {pytype!r}."
|
|
331
|
+
fail(info.msg(msg) if info else msg)
|
|
332
|
+
return self.ir_builder.make_attr_ref(attrname, val.value, pytype)
|
|
333
|
+
|
|
334
|
+
def _to_onnx_var(
|
|
335
|
+
self,
|
|
336
|
+
val: values.SymbolValue | PyValue,
|
|
337
|
+
target: Optional[PreferredName] = None,
|
|
338
|
+
info: Optional[sourceinfo.SourceInfo] = None,
|
|
339
|
+
) -> Variable:
|
|
340
|
+
if isinstance(val, values.AttrRef):
|
|
341
|
+
# promote attribute to value
|
|
342
|
+
result = self.generate_unique_name(target or "tmp")
|
|
343
|
+
attr = self._to_onnx_attr_ref(val, info)
|
|
344
|
+
self.emit([result], values.Op(self.default_opset, "Constant"), [], [attr])
|
|
345
|
+
if ta.base_type_is_bool(val.typeinfo):
|
|
346
|
+
# ONNX attributes use an int-encoding for bools, but ONNX tensor types
|
|
347
|
+
# distinguish between int and bool. So we cast the int tensor to a bool tensor,
|
|
348
|
+
# to promote a (python) bool attribute to a ONNX bool tensor.
|
|
349
|
+
result_as_bool = self.generate_unique_name(result + "_as_bool")
|
|
350
|
+
cast_attr = self._make_onnx_attr("to", onnx_types.BOOL.dtype)
|
|
351
|
+
self.emit(
|
|
352
|
+
[result_as_bool],
|
|
353
|
+
values.Op(self.default_opset, "Cast"),
|
|
354
|
+
[result],
|
|
355
|
+
[cast_attr],
|
|
356
|
+
)
|
|
357
|
+
return Variable(result_as_bool, True)
|
|
358
|
+
return Variable(result, True)
|
|
359
|
+
if isinstance(val, values.Dynamic):
|
|
360
|
+
return Variable(val.value)
|
|
361
|
+
# Assume value is a python-value convertible to a tensor
|
|
362
|
+
# TODO: check if value is convertible to a TensorProto, so that we can
|
|
363
|
+
# produce a better error _message otherwise
|
|
364
|
+
return self._emit_const(val, target or "tmp", info)
|
|
365
|
+
|
|
366
|
+
def _py_var_to_onnx_var(self, py_var: str, info: sourceinfo.SourceInfo) -> Variable:
|
|
367
|
+
return self._to_onnx_var(self._lookup(py_var, info), target=py_var, info=info)
|
|
368
|
+
|
|
369
|
+
def emit(
|
|
370
|
+
self,
|
|
371
|
+
outputs: Sequence[str],
|
|
372
|
+
callee: values.Op | str,
|
|
373
|
+
inputs: Sequence[Optional[str]],
|
|
374
|
+
attrs: Optional[Sequence[irbuilder.IRAttributeValue]] = None,
|
|
375
|
+
sub_functions: Optional[dict[str, onnx.FunctionProto]] = None,
|
|
376
|
+
):
|
|
377
|
+
if not isinstance(callee, values.Op):
|
|
378
|
+
callee = values.Op(self.default_opset, callee)
|
|
379
|
+
if attrs is None:
|
|
380
|
+
attrs = []
|
|
381
|
+
if sub_functions is None:
|
|
382
|
+
sub_functions = {}
|
|
383
|
+
self.ir_builder.add_stmt(
|
|
384
|
+
self._current_fn,
|
|
385
|
+
outputs,
|
|
386
|
+
callee,
|
|
387
|
+
inputs,
|
|
388
|
+
attrs,
|
|
389
|
+
sub_functions,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def _emit_const(
|
|
393
|
+
self,
|
|
394
|
+
pyvalue: PyValue,
|
|
395
|
+
suggested_name: Optional[PreferredName],
|
|
396
|
+
info: sourceinfo.SourceInfo,
|
|
397
|
+
) -> Variable:
|
|
398
|
+
if suggested_name is None:
|
|
399
|
+
if isinstance(pyvalue, int):
|
|
400
|
+
if pyvalue >= 0:
|
|
401
|
+
suggested_name = f"int64_{pyvalue}"
|
|
402
|
+
else:
|
|
403
|
+
suggested_name = f"int64_m{abs(pyvalue)}"
|
|
404
|
+
elif (
|
|
405
|
+
isinstance(pyvalue, list) and len(pyvalue) == 1 and isinstance(pyvalue[0], int)
|
|
406
|
+
):
|
|
407
|
+
if pyvalue[0] >= 0:
|
|
408
|
+
suggested_name = f"int64_{pyvalue[0]}_1d"
|
|
409
|
+
else:
|
|
410
|
+
suggested_name = f"int64_m{abs(pyvalue[0])}_1d"
|
|
411
|
+
else:
|
|
412
|
+
suggested_name = "const"
|
|
413
|
+
ovar = self.generate_unique_name(suggested_name)
|
|
414
|
+
try:
|
|
415
|
+
tensor = autocast.pyvalue_to_onnx_tensor(ovar, pyvalue)
|
|
416
|
+
except ValueError as e:
|
|
417
|
+
fail(info.msg(str(e)))
|
|
418
|
+
attr = self._make_onnx_attr("value", tensor)
|
|
419
|
+
self.emit([ovar], values.Op(self.default_opset, "Constant"), [], [attr])
|
|
420
|
+
return Variable(ovar, True)
|
|
421
|
+
|
|
422
|
+
def _emit_copy(self, original_var: str, suggested_name: str) -> str:
|
|
423
|
+
"""Emits a copy statement, using the ONNX Identity operator."""
|
|
424
|
+
new_var = self.generate_unique_name(suggested_name)
|
|
425
|
+
self.emit([new_var], "Identity", [original_var])
|
|
426
|
+
return new_var
|
|
427
|
+
|
|
428
|
+
def _is_constant_expr(self, node: ast.AST) -> None:
|
|
429
|
+
if isinstance(node, ast.UnaryOp):
|
|
430
|
+
return self._is_constant_expr(node.operand)
|
|
431
|
+
if isinstance(
|
|
432
|
+
node,
|
|
433
|
+
(
|
|
434
|
+
ast.Call,
|
|
435
|
+
ast.BinOp,
|
|
436
|
+
ast.UnaryOp,
|
|
437
|
+
ast.Compare,
|
|
438
|
+
ast.Num,
|
|
439
|
+
ast.Str,
|
|
440
|
+
ast.Attribute,
|
|
441
|
+
ast.List,
|
|
442
|
+
ast.Load,
|
|
443
|
+
ast.NameConstant,
|
|
444
|
+
ast.Constant,
|
|
445
|
+
ast.Str,
|
|
446
|
+
),
|
|
447
|
+
):
|
|
448
|
+
return all(self._is_constant_expr(c) for c in ast.iter_child_nodes(node))
|
|
449
|
+
return False
|
|
450
|
+
|
|
451
|
+
def _eval_constant_expr(self, expr: ast.AST) -> PyValue:
|
|
452
|
+
"""Evaluates a sub-expression that is assumed to represent a constant value.
|
|
453
|
+
The expression can refer only to global names (inherited from the scope
|
|
454
|
+
where the script is evaluated) and cannot refer to local names defined
|
|
455
|
+
within the script.) Further, these expressions are assumed to be constants.
|
|
456
|
+
Thus, any subsequent mutation of any state/variables (used in computing
|
|
457
|
+
this constant value) will potentially lead to unexpected behavior (such
|
|
458
|
+
as divergence between eager-mode execution and evaluation of the ONNX
|
|
459
|
+
function.)
|
|
460
|
+
"""
|
|
461
|
+
# TODO: assert (self._is_constant_expr(expr))
|
|
462
|
+
# TODO: Refine types
|
|
463
|
+
locals: dict[Any, Any] = {}
|
|
464
|
+
expr = ast.Expression(expr, lineno=expr.lineno, col_offset=expr.col_offset)
|
|
465
|
+
cpl = compile(expr, filename="<ast>", mode="eval")
|
|
466
|
+
try:
|
|
467
|
+
return eval(cpl, self.globals, locals) # pylint: disable=eval-used
|
|
468
|
+
except NameError as e:
|
|
469
|
+
raise NameError(
|
|
470
|
+
self._message(
|
|
471
|
+
expr,
|
|
472
|
+
f"Missing names, globals contains {list(self.globals)!r}, "
|
|
473
|
+
f"locals {list(locals)!r}.",
|
|
474
|
+
)
|
|
475
|
+
) from e
|
|
476
|
+
|
|
477
|
+
def _translate_attr(
|
|
478
|
+
self,
|
|
479
|
+
attr_name: str,
|
|
480
|
+
expr: ast.AST,
|
|
481
|
+
attr_meta: Optional[onnx.defs.OpSchema.Attribute] = None,
|
|
482
|
+
) -> Optional[irbuilder.IRAttributeValue]:
|
|
483
|
+
"""Translate an attribute-value specification of the form `attr_name=<expr>`
|
|
484
|
+
in a call to an op. expr is an AST. The following cases are supported:
|
|
485
|
+
* Expr evaluates to a script-time constant (a python-value) that can be mapped
|
|
486
|
+
into an ONNX attribute value, or
|
|
487
|
+
* Expr evaluates to None, in which case None is returned, or
|
|
488
|
+
* Expr must be an attribute-reference, that is a name representing an
|
|
489
|
+
attribute-parameter of a containing function.
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
if isinstance(expr, ast.Name):
|
|
493
|
+
val = self._lookup(expr.id, self._source_of(expr))
|
|
494
|
+
if isinstance(val, values.AttrRef):
|
|
495
|
+
attr_ref = self.ir_builder.make_attr_ref(attr_name, val.value, val.typeinfo)
|
|
496
|
+
if attr_meta is not None and (attr_ref.type != attr_meta.type):
|
|
497
|
+
self.fail(
|
|
498
|
+
expr,
|
|
499
|
+
f"Attribute type '{attr_ref.type}' does not match expected type '{attr_meta.type}'",
|
|
500
|
+
)
|
|
501
|
+
return attr_ref
|
|
502
|
+
if isinstance(val, irbuilder.IRFunction):
|
|
503
|
+
# Check that outer-scope variables referenced by function have same value
|
|
504
|
+
# at function-definition site and use-as-attribute site, to avoid errors.
|
|
505
|
+
for pyvar, previous in val.outer_scope_variables:
|
|
506
|
+
current = self._lookup(pyvar, self._source_of(expr))
|
|
507
|
+
if current.value != previous.value:
|
|
508
|
+
self.fail(
|
|
509
|
+
expr,
|
|
510
|
+
f"Outer scope variable '{pyvar}' referenced by function "
|
|
511
|
+
f"'{expr.id!r}' modified.",
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# Create GraphProto attribute
|
|
515
|
+
val = val.to_graph_proto()
|
|
516
|
+
else:
|
|
517
|
+
val = self._eval_constant_expr(expr)
|
|
518
|
+
|
|
519
|
+
# In ONNX, there is no way to explicitly specify a None value for an attribute.
|
|
520
|
+
# Instead, the attribute must be omitted from the attribute list.
|
|
521
|
+
# Hence, we do not create an attribute-proto if the value is None.
|
|
522
|
+
# The caller is responsible for omitting such attribute-values from the list of attributes
|
|
523
|
+
# in a NodeProto.
|
|
524
|
+
if val is None:
|
|
525
|
+
if attr_meta and attr_meta.required:
|
|
526
|
+
self.fail(expr, f"Attribute '{attr_name}' is required.")
|
|
527
|
+
return None
|
|
528
|
+
attr_type = attr_meta.type if attr_meta else None
|
|
529
|
+
attr = self._make_onnx_attr(attr_name, val, attr_type)
|
|
530
|
+
if attr_meta and (attr.type != attr_meta.type):
|
|
531
|
+
self.fail(
|
|
532
|
+
expr,
|
|
533
|
+
f"Attribute type '{attr.type}' does not match expected type '{attr_meta.type}'",
|
|
534
|
+
)
|
|
535
|
+
return attr
|
|
536
|
+
|
|
537
|
+
def _translate_docstring(self, node: ast.Expr) -> None:
|
|
538
|
+
if hasattr(node.value, "value"):
|
|
539
|
+
# python 3.8+
|
|
540
|
+
return self.ir_builder.add_docstring(self._current_fn, node.value.value)
|
|
541
|
+
raise TypeError(
|
|
542
|
+
f"Unexpected type {type(node)!r} for node. Unsupoorted version of python."
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def _translate_expr(
|
|
546
|
+
self, node: ast.AST, target: Optional[PreferredName] = None
|
|
547
|
+
) -> Variable:
|
|
548
|
+
"""Expression-translation generates "IR statements/nodes" that compute the value of
|
|
549
|
+
the expression into a target-variable, and returns the variable that is
|
|
550
|
+
assigned this value.
|
|
551
|
+
"""
|
|
552
|
+
if isinstance(node, ast.Call):
|
|
553
|
+
r = self._translate_call_expr(node)
|
|
554
|
+
elif isinstance(node, (ast.BinOp, ast.BitAnd, ast.BitOr)):
|
|
555
|
+
r = self._translate_binary_op_expr(node)
|
|
556
|
+
elif isinstance(node, ast.UnaryOp):
|
|
557
|
+
r = self._translate_unary_op_expr(node)
|
|
558
|
+
elif isinstance(node, ast.Compare):
|
|
559
|
+
r = self._translate_compare_expr(node)
|
|
560
|
+
elif isinstance(node, ast.Name):
|
|
561
|
+
r = self._translate_name_expr(node)
|
|
562
|
+
elif isinstance(node, ast.Subscript):
|
|
563
|
+
r = self._translate_subscript_expr(node, target)
|
|
564
|
+
elif self._is_constant_expr(node):
|
|
565
|
+
r = self._emit_const(self._eval_constant_expr(node), target, self._source_of(node))
|
|
566
|
+
else:
|
|
567
|
+
raise ValueError(
|
|
568
|
+
self._message(node, f"Unsupported expression type {type(node)!r}.")
|
|
569
|
+
)
|
|
570
|
+
if isinstance(r, Variable):
|
|
571
|
+
return r
|
|
572
|
+
callee, args, attrs = r
|
|
573
|
+
target = "tmp" if target is None else target
|
|
574
|
+
assert isinstance(target, str)
|
|
575
|
+
result = self.generate_unique_name(target)
|
|
576
|
+
self.emit([result], callee, args, attrs)
|
|
577
|
+
return Variable(result)
|
|
578
|
+
|
|
579
|
+
def _translate_opt_expr(self, node: ast.expr) -> Optional[Variable]:
|
|
580
|
+
"""Translation of an expression where "None" is permitted (eg., for an optional argument).
|
|
581
|
+
None is represented as a NameConstant in Python 3.7 and Constant in Python 3.9.
|
|
582
|
+
"""
|
|
583
|
+
if isinstance(node, (ast.NameConstant, ast.Constant)) and (node.value is None):
|
|
584
|
+
return None
|
|
585
|
+
return self._translate_expr(node)
|
|
586
|
+
|
|
587
|
+
def _translate_subscript_expr(
|
|
588
|
+
self, node: ast.Subscript, target: Optional[PreferredName]
|
|
589
|
+
) -> Variable:
|
|
590
|
+
"""List of supported syntaxes is below.
|
|
591
|
+
`A` is a tensor or an expression equivalent to a tensor.
|
|
592
|
+
|
|
593
|
+
::
|
|
594
|
+
|
|
595
|
+
A[:, 1]
|
|
596
|
+
A[:2, 0]
|
|
597
|
+
A[:2, :1]
|
|
598
|
+
A[2:0:-1]
|
|
599
|
+
A[1:]
|
|
600
|
+
A[:2]
|
|
601
|
+
A[1:-1]
|
|
602
|
+
A[1:2]
|
|
603
|
+
A[-1]
|
|
604
|
+
A[0]
|
|
605
|
+
A[:0:-1]
|
|
606
|
+
|
|
607
|
+
*i* is a tensor holding one integer.
|
|
608
|
+
|
|
609
|
+
::
|
|
610
|
+
|
|
611
|
+
A[i]
|
|
612
|
+
A[i+1:i+2]
|
|
613
|
+
|
|
614
|
+
Fully supported for python 3.9+.
|
|
615
|
+
|
|
616
|
+
::
|
|
617
|
+
|
|
618
|
+
A[i:i+j, k]
|
|
619
|
+
|
|
620
|
+
Not supported:
|
|
621
|
+
|
|
622
|
+
::
|
|
623
|
+
|
|
624
|
+
A[::-1]
|
|
625
|
+
"""
|
|
626
|
+
var = self._translate_expr(node.value)
|
|
627
|
+
var_name = var.name
|
|
628
|
+
if target is None:
|
|
629
|
+
target = f"{var_name}_subscripted"
|
|
630
|
+
target = self.generate_unique_name(target)
|
|
631
|
+
indices = ast_utils.normalize_subscript_expr(node)
|
|
632
|
+
info = self._source_of(node.slice if PY_VERSION_GE_39 else node)
|
|
633
|
+
|
|
634
|
+
# Create cached int constants:
|
|
635
|
+
# TODO: Do this at a graph-scope level.
|
|
636
|
+
cached_int_consts = {}
|
|
637
|
+
|
|
638
|
+
def const_1d(value, name: Optional[str] = None):
|
|
639
|
+
nonlocal cached_int_consts
|
|
640
|
+
if value not in cached_int_consts:
|
|
641
|
+
cached_int_consts[value] = self._emit_const([value], name, info)
|
|
642
|
+
return cached_int_consts[value]
|
|
643
|
+
|
|
644
|
+
def one_1d():
|
|
645
|
+
return const_1d(1)
|
|
646
|
+
|
|
647
|
+
# Max/min 64-bit int values are used to represent default values for start/stop in Slice.
|
|
648
|
+
maxint = (1 << 63) - 1
|
|
649
|
+
minint = -(1 << 63)
|
|
650
|
+
|
|
651
|
+
def translate_slice_component(
|
|
652
|
+
node_arg, default_value: Optional[int] = None
|
|
653
|
+
) -> tuple[str, Optional[int]]:
|
|
654
|
+
"""Translate optional start/stop/step component of a Slice expression."""
|
|
655
|
+
if node_arg is None:
|
|
656
|
+
if default_value is None:
|
|
657
|
+
# TODO: Emit "Where(step > 0, pos_default, neg_default)"
|
|
658
|
+
raise RuntimeError(
|
|
659
|
+
"Default start/stop not supported when step direction is unknown."
|
|
660
|
+
)
|
|
661
|
+
return const_1d(default_value), default_value
|
|
662
|
+
|
|
663
|
+
if self._is_constant_expr(node_arg):
|
|
664
|
+
cst = self._eval_constant_expr(node_arg)
|
|
665
|
+
if isinstance(cst, int):
|
|
666
|
+
return const_1d(cst), cst
|
|
667
|
+
else:
|
|
668
|
+
raise RuntimeError(f"Slice component type must be int, not {type(cst)}")
|
|
669
|
+
else:
|
|
670
|
+
name = self._translate_expr(node_arg).name
|
|
671
|
+
reshaped = self.generate_unique_name(f"{name}_reshaped")
|
|
672
|
+
self.emit(
|
|
673
|
+
[reshaped],
|
|
674
|
+
values.Op(self.default_opset, "Reshape"),
|
|
675
|
+
[name, one_1d().name],
|
|
676
|
+
[],
|
|
677
|
+
)
|
|
678
|
+
return reshaped, None
|
|
679
|
+
|
|
680
|
+
def translate_slice(slice_expr: ast.Slice) -> tuple[str, str, str]:
|
|
681
|
+
"""Translate slice-expression of the form from:to:step."""
|
|
682
|
+
step_name, step = translate_slice_component(slice_expr.step, 1)
|
|
683
|
+
if step is None:
|
|
684
|
+
# Step direction unknown.
|
|
685
|
+
# TODO: Handle default-values using runtime check on sign of step.
|
|
686
|
+
lower_name, _ = translate_slice_component(slice_expr.lower, None)
|
|
687
|
+
upper_name, _ = translate_slice_component(slice_expr.upper, None)
|
|
688
|
+
elif step > 0:
|
|
689
|
+
lower_name, _ = translate_slice_component(slice_expr.lower, 0)
|
|
690
|
+
upper_name, _ = translate_slice_component(slice_expr.upper, maxint)
|
|
691
|
+
else:
|
|
692
|
+
lower_name, _ = translate_slice_component(slice_expr.lower, maxint)
|
|
693
|
+
upper_name, _ = translate_slice_component(slice_expr.upper, minint)
|
|
694
|
+
return (lower_name, upper_name, step_name)
|
|
695
|
+
|
|
696
|
+
# An input like X[2] is translated into a Gather op.
|
|
697
|
+
# An input like X[1:5:2] is translated into a Slice op.
|
|
698
|
+
# An input like X[2, 3] is translated into a Slice + Squeeze (instead of two Gathers),
|
|
699
|
+
# as an optimization.
|
|
700
|
+
# An input like X[I, J] is translated into two Gathers (which is correct whatever the
|
|
701
|
+
# rank of I and J)
|
|
702
|
+
# To replace multiple Gathers by the Slice we need to know that the index-values
|
|
703
|
+
# are scalars.
|
|
704
|
+
|
|
705
|
+
# As the first step, we partition the index elements into four kinds: Slice (eg., 1:5:2),
|
|
706
|
+
# known-to-be-scalar (eg., 2), other-tensor (eg., I), skip/no-op (that is, just ":")
|
|
707
|
+
sliced_indices: List[Tuple[int, ast.expr]] = []
|
|
708
|
+
scalar_indices: List[Tuple[int, ast.expr]] = []
|
|
709
|
+
non_scalar_indices: List[Tuple[int, ast.expr]] = []
|
|
710
|
+
for axis, elt in enumerate(indices):
|
|
711
|
+
if isinstance(elt, ast.Slice):
|
|
712
|
+
# Add to sliced_indices, unless it is "::", which is a no-op.
|
|
713
|
+
if not (elt.lower is None and elt.upper is None and elt.step is None):
|
|
714
|
+
sliced_indices.append((axis, elt))
|
|
715
|
+
elif self._is_constant_expr(elt) and isinstance(
|
|
716
|
+
self._eval_constant_expr(elt), int
|
|
717
|
+
):
|
|
718
|
+
scalar_indices.append((axis, elt))
|
|
719
|
+
else:
|
|
720
|
+
non_scalar_indices.append((axis, elt))
|
|
721
|
+
if not (sliced_indices or scalar_indices or non_scalar_indices):
|
|
722
|
+
# Edge case: no index specified. Eg. A[:, :]
|
|
723
|
+
self.emit([target], "Identity", [var_name])
|
|
724
|
+
return Variable(target)
|
|
725
|
+
if sliced_indices or len(scalar_indices) > 1:
|
|
726
|
+
# We emit a Slice operation if we have any indices like 1:5:2 or if the number of
|
|
727
|
+
# scalar indices (like 2) is more than 1.
|
|
728
|
+
starts = []
|
|
729
|
+
ends = []
|
|
730
|
+
axes = []
|
|
731
|
+
steps = []
|
|
732
|
+
squeezed_axes = []
|
|
733
|
+
for axis, expr in scalar_indices:
|
|
734
|
+
# Treat a scalar index i as slice "i:i+1:1", but squeeze the axis finally.
|
|
735
|
+
# TODO: handle negative i
|
|
736
|
+
index = self._eval_constant_expr(expr)
|
|
737
|
+
squeezed_axes.append(axis)
|
|
738
|
+
kwargs = dict(
|
|
739
|
+
lineno=getattr(expr, "lineno", node.lineno),
|
|
740
|
+
col_offset=getattr(expr, "col_offset", node.col_offset),
|
|
741
|
+
)
|
|
742
|
+
element = ast.Slice(
|
|
743
|
+
ast.Constant(index, **kwargs),
|
|
744
|
+
ast.Constant(index + 1, **kwargs),
|
|
745
|
+
ast.Constant(1, **kwargs),
|
|
746
|
+
)
|
|
747
|
+
sliced_indices.append((axis, element))
|
|
748
|
+
scalar_indices = []
|
|
749
|
+
for axis, element in sliced_indices:
|
|
750
|
+
axis_var = const_1d(axis)
|
|
751
|
+
inputs = translate_slice(element)
|
|
752
|
+
starts.append(inputs[0])
|
|
753
|
+
ends.append(inputs[1])
|
|
754
|
+
axes.append(axis_var.name)
|
|
755
|
+
steps.append(inputs[2])
|
|
756
|
+
|
|
757
|
+
if len(starts) > 1:
|
|
758
|
+
axis_0_attr = self._make_onnx_attr("axis", 0)
|
|
759
|
+
start_name = self.generate_unique_name(f"{var_name}_start")
|
|
760
|
+
self.emit([start_name], "Concat", starts, [axis_0_attr])
|
|
761
|
+
|
|
762
|
+
end_name = self.generate_unique_name(f"{var_name}_end")
|
|
763
|
+
self.emit([end_name], "Concat", ends, [axis_0_attr])
|
|
764
|
+
|
|
765
|
+
axes_name = self.generate_unique_name(f"{var_name}_axis")
|
|
766
|
+
self.emit([axes_name], "Concat", axes, [axis_0_attr])
|
|
767
|
+
|
|
768
|
+
steps_name = self.generate_unique_name(f"{var_name}_step")
|
|
769
|
+
self.emit([steps_name], "Concat", steps, [axis_0_attr])
|
|
770
|
+
else:
|
|
771
|
+
start_name = starts[0]
|
|
772
|
+
end_name = ends[0]
|
|
773
|
+
axes_name = axes[0]
|
|
774
|
+
steps_name = steps[0]
|
|
775
|
+
|
|
776
|
+
if squeezed_axes:
|
|
777
|
+
sliced_name = self.generate_unique_name(f"{var_name}_sliced")
|
|
778
|
+
self.emit(
|
|
779
|
+
[sliced_name],
|
|
780
|
+
"Slice",
|
|
781
|
+
[var_name, start_name, end_name, axes_name, steps_name],
|
|
782
|
+
)
|
|
783
|
+
squeezed_axes = self._emit_const(squeezed_axes, "squeezed_axes", info)
|
|
784
|
+
|
|
785
|
+
if non_scalar_indices: # use temporary to store result of squeeze
|
|
786
|
+
result = self.generate_unique_name(f"{var_name}_squeezed")
|
|
787
|
+
else: # store squeezed result in final target
|
|
788
|
+
result = target
|
|
789
|
+
|
|
790
|
+
self.emit([result], "Squeeze", [sliced_name, squeezed_axes])
|
|
791
|
+
else:
|
|
792
|
+
if non_scalar_indices: # use temporary to store result of Slice
|
|
793
|
+
result = self.generate_unique_name(f"{var_name}_sliced")
|
|
794
|
+
else: # store result of Slice in final target
|
|
795
|
+
result = target
|
|
796
|
+
slice_inputs = [var_name, start_name, end_name, axes_name, steps_name]
|
|
797
|
+
self.emit([result], "Slice", slice_inputs)
|
|
798
|
+
else:
|
|
799
|
+
result = var_name
|
|
800
|
+
non_scalar_indices.extend(scalar_indices)
|
|
801
|
+
if non_scalar_indices:
|
|
802
|
+
last_axis, _ = non_scalar_indices[-1]
|
|
803
|
+
else:
|
|
804
|
+
# TODO(justinchuby): Clarify what last_axis should be when non_scalar_indices is False
|
|
805
|
+
last_axis = None
|
|
806
|
+
for axis, index_expr in non_scalar_indices:
|
|
807
|
+
index_value = self._translate_expr(index_expr)
|
|
808
|
+
axis_attr = self._make_onnx_attr("axis", axis)
|
|
809
|
+
# use Gather to perform indexing
|
|
810
|
+
# Assign gathered value to either temporary or final target
|
|
811
|
+
if axis != last_axis: # use temporary to store result of Gather
|
|
812
|
+
gathered = self.generate_unique_name(f"{var_name}_axis_{axis}")
|
|
813
|
+
else: # store result of Gather in final target
|
|
814
|
+
gathered = target
|
|
815
|
+
self.emit([gathered], "Gather", [str(result), index_value], [axis_attr])
|
|
816
|
+
result = gathered
|
|
817
|
+
|
|
818
|
+
return Variable(result)
|
|
819
|
+
|
|
820
|
+
def _translate_call_expr(self, node: ast.Call):
|
|
821
|
+
"""Translates a call-expression."""
|
|
822
|
+
callee = self._translate_callee_expr(node.func)
|
|
823
|
+
param_schemas = callee.param_schemas()
|
|
824
|
+
# If the callee's schema is available, we use it to determine the inputs and attributes.
|
|
825
|
+
# Otherwise, we map named arguments to attributes and positional arguments to inputs.
|
|
826
|
+
if param_schemas:
|
|
827
|
+
kwargs = {x.arg: x.value for x in node.keywords}
|
|
828
|
+
args, attrs = param_manipulation.separate_input_attributes_from_arguments(
|
|
829
|
+
param_schemas, node.args, kwargs, fill_defaults=False
|
|
830
|
+
)
|
|
831
|
+
args = [self._translate_opt_expr(x) for x in args]
|
|
832
|
+
attrs = [
|
|
833
|
+
self._translate_attr(x, y, callee.op_schema.attributes[x])
|
|
834
|
+
for x, y in attrs.items()
|
|
835
|
+
]
|
|
836
|
+
else:
|
|
837
|
+
args = [self._translate_opt_expr(x) for x in node.args]
|
|
838
|
+
attrs = [self._translate_attr(x.arg, x.value) for x in node.keywords]
|
|
839
|
+
args = autocast.static_cast_inputs(self, callee.op_schema, args)
|
|
840
|
+
|
|
841
|
+
# In ONNX, there is no way to explicitly specify a None value for an attribute.
|
|
842
|
+
# Instead, the attribute must be omitted from the attribute list.
|
|
843
|
+
# Hence, we do not create an attribute-proto if the value is None.
|
|
844
|
+
attrs = [attr for attr in attrs if attr is not None]
|
|
845
|
+
return callee, args, attrs
|
|
846
|
+
|
|
847
|
+
def _cast_like_binary_expression(self, op, left, right):
|
|
848
|
+
schema = op.op_schema
|
|
849
|
+
return autocast.static_cast_inputs(self, schema, (left, right))
|
|
850
|
+
|
|
851
|
+
def _translate_binary_op_expr(self, node: ast.BinOp):
|
|
852
|
+
op = type(node.op)
|
|
853
|
+
if op not in primop_map:
|
|
854
|
+
raise ValueError(self._message(node, f"Unsupported operator {op!r}."))
|
|
855
|
+
|
|
856
|
+
attr = []
|
|
857
|
+
if isinstance(node.op, ast.Mod) and self._is_constant_expr(node.right):
|
|
858
|
+
# specific case X % f where f is a float.
|
|
859
|
+
# attribute fmod=1 is added in that case.
|
|
860
|
+
cst = self._eval_constant_expr(node.right)
|
|
861
|
+
if isinstance(cst, float):
|
|
862
|
+
attr = [self._make_onnx_attr("fmod", 1)]
|
|
863
|
+
|
|
864
|
+
op = values.Op(self.default_opset, primop_map[op])
|
|
865
|
+
left, right = self._cast_like_binary_expression(
|
|
866
|
+
op, self._translate_expr(node.left), self._translate_expr(node.right)
|
|
867
|
+
)
|
|
868
|
+
return op, [left, right], attr
|
|
869
|
+
|
|
870
|
+
def _translate_unary_op_expr(self, node):
|
|
871
|
+
op = type(node.op)
|
|
872
|
+
if op not in primop_map:
|
|
873
|
+
raise ValueError(self._message(node, self).msg(f"Unsupported operator {op!r}."))
|
|
874
|
+
if self._is_constant_expr(node.operand):
|
|
875
|
+
# This function changed the constant node.operand
|
|
876
|
+
# and returns it. The function calling this one
|
|
877
|
+
# should intercept this call and replace node
|
|
878
|
+
# by node.operand.
|
|
879
|
+
# This mechanism does not handle somthing like `(-(-5))`.
|
|
880
|
+
if hasattr(node.operand, "value"):
|
|
881
|
+
# python 3.8+
|
|
882
|
+
val = node.operand.value
|
|
883
|
+
else:
|
|
884
|
+
raise TypeError(
|
|
885
|
+
f"Unable to guess constant value from type {type(node.operand)!r} "
|
|
886
|
+
f"and attributes {dir(node.operand)!r}."
|
|
887
|
+
)
|
|
888
|
+
if op == ast.USub:
|
|
889
|
+
cst = ast.Constant(-val, lineno=node.lineno, col_offset=node.col_offset)
|
|
890
|
+
return self._translate_expr(cst)
|
|
891
|
+
if op == ast.UAdd:
|
|
892
|
+
return self._translate_expr(node.operand)
|
|
893
|
+
opname = primop_map[op]
|
|
894
|
+
operand = self._translate_expr(node.operand)
|
|
895
|
+
return values.Op(self.default_opset, opname), [operand], []
|
|
896
|
+
|
|
897
|
+
def _translate_compare_expr(self, node):
|
|
898
|
+
# TODO: handle multiple comparisons in one expression
|
|
899
|
+
assert len(node.ops) == 1
|
|
900
|
+
assert len(node.comparators) == 1
|
|
901
|
+
op = type(node.ops[0])
|
|
902
|
+
if op not in primop_map:
|
|
903
|
+
raise ValueError(self._message(node, f"Unsupported operator {op!r}."))
|
|
904
|
+
opname = primop_map[op]
|
|
905
|
+
left = self._translate_expr(node.left)
|
|
906
|
+
right = self._translate_expr(node.comparators[0])
|
|
907
|
+
|
|
908
|
+
# NotEqual is not a standard ONNX op, and needs to be translated into
|
|
909
|
+
# an Equal op/node followed by a Not op/node.
|
|
910
|
+
op = values.Op(self.default_opset, opname if opname != "NotEqual" else "Equal")
|
|
911
|
+
left, right = self._cast_like_binary_expression(op, left, right)
|
|
912
|
+
if opname == "NotEqual":
|
|
913
|
+
tmp = self.generate_unique_name()
|
|
914
|
+
self.emit([tmp], op, [left, right])
|
|
915
|
+
not_op = values.Op(self.default_opset, "Not")
|
|
916
|
+
return not_op, [tmp], []
|
|
917
|
+
|
|
918
|
+
return op, [left, right], []
|
|
919
|
+
|
|
920
|
+
def _translate_name_expr(self, node: ast.Name) -> Variable:
|
|
921
|
+
return self._py_var_to_onnx_var(node.id, self._source_of(node))
|
|
922
|
+
|
|
923
|
+
# pylint: disable=inconsistent-return-statements
|
|
924
|
+
def _translate_opset_expr(self, node: ast.Attribute) -> values.Opset:
|
|
925
|
+
"""Return an Opset"""
|
|
926
|
+
if isinstance(node, ast.Name):
|
|
927
|
+
val = self._lookup(node.id, self._source_of(node), raise_exception=False)
|
|
928
|
+
if isinstance(val, values.Opset):
|
|
929
|
+
return val
|
|
930
|
+
self.fail(node, f"'{node.id}' is not an instance of type Opset but {type(val)}.")
|
|
931
|
+
elif isinstance(node, ast.Attribute):
|
|
932
|
+
self.fail(node, "Nested module unimplemented.") # TODO
|
|
933
|
+
else:
|
|
934
|
+
self.fail(node, "Invalid opset expression.")
|
|
935
|
+
|
|
936
|
+
# pylint: enable=inconsistent-return-statements
|
|
937
|
+
def _translate_callee_expr(self, node: ast.AST) -> values.Op: # pylint: disable=R1710
|
|
938
|
+
"""Return an Op"""
|
|
939
|
+
if isinstance(node, ast.Attribute):
|
|
940
|
+
module = self._translate_opset_expr(node.value)
|
|
941
|
+
self._set_default_opset(module, node)
|
|
942
|
+
opname = node.attr
|
|
943
|
+
if opname in module:
|
|
944
|
+
return values.Op(module, node.attr)
|
|
945
|
+
warn(f"'{opname}' is not a known op in '{module}'")
|
|
946
|
+
return values.Op(module, node.attr)
|
|
947
|
+
if isinstance(node, ast.Name):
|
|
948
|
+
function_name = node.id
|
|
949
|
+
found = self._lookup(function_name, self._source_of(node), raise_exception=False)
|
|
950
|
+
if isinstance(found, onnxscript.OnnxFunction):
|
|
951
|
+
self._current_fn.add_called_function(found)
|
|
952
|
+
return found
|
|
953
|
+
if isinstance(found, values.Op):
|
|
954
|
+
return found
|
|
955
|
+
if not found:
|
|
956
|
+
if function_name not in self.default_opset:
|
|
957
|
+
warn(
|
|
958
|
+
f"Unknown function name {function_name!r}. "
|
|
959
|
+
f"The ONNX graph may not work."
|
|
960
|
+
)
|
|
961
|
+
return values.Op(self.default_opset, function_name)
|
|
962
|
+
self.fail(node, "Invalid callee")
|
|
963
|
+
|
|
964
|
+
def _translate_stmt(self, node: ast.stmt, index_of_stmt=None) -> None:
|
|
965
|
+
"""Statement translation: A single Python statement is mapped into a
|
|
966
|
+
sequence of IR statements.
|
|
967
|
+
"""
|
|
968
|
+
if isinstance(node, ast.Assign):
|
|
969
|
+
return self._translate_assign_stmt(node)
|
|
970
|
+
if isinstance(node, ast.AnnAssign):
|
|
971
|
+
return self._translate_assign_stmt(node)
|
|
972
|
+
if isinstance(node, ast.Return):
|
|
973
|
+
if index_of_stmt is not None:
|
|
974
|
+
return self._translate_return_stmt(node)
|
|
975
|
+
raise ValueError(
|
|
976
|
+
self._message(
|
|
977
|
+
node, "Return statements are not permitted inside control-flow statements."
|
|
978
|
+
)
|
|
979
|
+
)
|
|
980
|
+
if isinstance(node, ast.If):
|
|
981
|
+
return self._translate_if_stmt(node)
|
|
982
|
+
if isinstance(node, (ast.For, ast.While)):
|
|
983
|
+
return self._translate_loop_stmt(node)
|
|
984
|
+
if ast_utils.is_doc_string(node):
|
|
985
|
+
if index_of_stmt == 0:
|
|
986
|
+
return self._translate_docstring(node)
|
|
987
|
+
return None
|
|
988
|
+
if isinstance(node, ast.FunctionDef):
|
|
989
|
+
return self._translate_nested_function_def(node)
|
|
990
|
+
if ast_utils.is_print_call(node):
|
|
991
|
+
return None
|
|
992
|
+
raise ValueError(self._message(node, f"Unsupported statement type '{type(node)!r}'."))
|
|
993
|
+
|
|
994
|
+
def _translate_assign_stmt(self, stmt: Union[ast.Assign, ast.AnnAssign]) -> None:
|
|
995
|
+
def assign(lhs: ast.AST, rhs: ast.AST) -> None:
|
|
996
|
+
if isinstance(lhs, ast.Name):
|
|
997
|
+
# Assignments of the form "x = SomeExpression"
|
|
998
|
+
info = self._source_of(lhs)
|
|
999
|
+
lhs = lhs.id
|
|
1000
|
+
t = self._translate_expr(rhs, lhs).name
|
|
1001
|
+
if isinstance(stmt, ast.AnnAssign):
|
|
1002
|
+
typeinfo = self._eval_constant_expr(stmt.annotation)
|
|
1003
|
+
else:
|
|
1004
|
+
typeinfo = None
|
|
1005
|
+
var = values.Dynamic(t, values.DynamicKind.Intermediate, info, typeinfo)
|
|
1006
|
+
self._bind(lhs, var)
|
|
1007
|
+
elif isinstance(lhs, ast.Tuple):
|
|
1008
|
+
# Assignments of the form "x, y, z = op.SomeOp(...)"
|
|
1009
|
+
if not isinstance(rhs, ast.Call):
|
|
1010
|
+
self.fail(
|
|
1011
|
+
rhs,
|
|
1012
|
+
f"RHS must be a Call expression for unpacking, found: '{type(rhs)!r}'",
|
|
1013
|
+
)
|
|
1014
|
+
callee, inputs, attrs = self._translate_call_expr(rhs)
|
|
1015
|
+
|
|
1016
|
+
def generate_onnx_name(x: ast.AST):
|
|
1017
|
+
if not isinstance(x, ast.Name):
|
|
1018
|
+
self.fail(x, f"LHS must be a Name for unpacking, found: '{type(x)!r}'")
|
|
1019
|
+
onnx_name = self.generate_unique_name(x.id)
|
|
1020
|
+
self._bind(
|
|
1021
|
+
x.id,
|
|
1022
|
+
values.Dynamic(
|
|
1023
|
+
onnx_name, values.DynamicKind.Intermediate, self._source_of(x)
|
|
1024
|
+
),
|
|
1025
|
+
)
|
|
1026
|
+
return onnx_name
|
|
1027
|
+
|
|
1028
|
+
outputs = [generate_onnx_name(x) for x in lhs.elts]
|
|
1029
|
+
self.emit(outputs, callee, inputs, attrs)
|
|
1030
|
+
else:
|
|
1031
|
+
self.fail(lhs, f"Unsupported construct in LHS of assignment: '{type(lhs)!r}'")
|
|
1032
|
+
|
|
1033
|
+
if isinstance(stmt, ast.Assign):
|
|
1034
|
+
targets = stmt.targets
|
|
1035
|
+
else:
|
|
1036
|
+
targets = [stmt.target]
|
|
1037
|
+
if len(targets) != 1:
|
|
1038
|
+
# Assignments of the form "x = y = SomeExpression"
|
|
1039
|
+
self.fail(stmt, "Multi-assignment not supported.")
|
|
1040
|
+
lhs = targets[0]
|
|
1041
|
+
rhs = stmt.value
|
|
1042
|
+
if isinstance(rhs, ast.Tuple):
|
|
1043
|
+
# Assignments of the form "... = Expression1, Expression2"
|
|
1044
|
+
if not isinstance(lhs, ast.Tuple):
|
|
1045
|
+
# Assignments of the form "single_var = Expression1, Expression2".
|
|
1046
|
+
# We do not support tuple-typed variables.
|
|
1047
|
+
self.fail(lhs, f"Left term must be a tuple not '{type(lhs)!r}'.")
|
|
1048
|
+
# Parallel assignments of the form "x, y = Expression1, Expression2"
|
|
1049
|
+
if len(lhs.elts) != len(rhs.elts):
|
|
1050
|
+
self.fail(
|
|
1051
|
+
stmt, "Expected same number of elements on lhs and rhs of assignments."
|
|
1052
|
+
)
|
|
1053
|
+
for p, r in zip(lhs.elts, rhs.elts):
|
|
1054
|
+
assign(p, r)
|
|
1055
|
+
else:
|
|
1056
|
+
assign(lhs, rhs)
|
|
1057
|
+
|
|
1058
|
+
def _translate_return_stmt(self, stmt: ast.Return) -> None:
|
|
1059
|
+
def check_num_outputs(n):
|
|
1060
|
+
if self.returntype is not None:
|
|
1061
|
+
if n != len(self.returntype):
|
|
1062
|
+
raise SyntaxError(
|
|
1063
|
+
self._message(
|
|
1064
|
+
stmt,
|
|
1065
|
+
f"Mismatch in number of return values and types. Keyword "
|
|
1066
|
+
f"'return' cannot be used in a subgraph (test, loop). "
|
|
1067
|
+
f"returntype is {self.returntype!r}, num_outputs={n!r}.",
|
|
1068
|
+
)
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
def ret(exp, i, suffix):
|
|
1072
|
+
preferred_name = f"return_val{suffix}"
|
|
1073
|
+
return_var = self._translate_expr(exp, preferred_name).name
|
|
1074
|
+
val = self._lookup(return_var, self._source_of(exp), False)
|
|
1075
|
+
if val and val.kind == values.DynamicKind.Input:
|
|
1076
|
+
# In ONNX, a graph-input cannot be an output of the graph.
|
|
1077
|
+
# We need to insert a copy.
|
|
1078
|
+
return_var = self._emit_copy(return_var, preferred_name)
|
|
1079
|
+
for prev_output in self._current_fn.outputs:
|
|
1080
|
+
if prev_output.name == return_var:
|
|
1081
|
+
# ONNX does not allow duplicate output names.
|
|
1082
|
+
return_var = self._emit_copy(return_var, f"{return_var}_copy")
|
|
1083
|
+
break
|
|
1084
|
+
if self.returntype is None:
|
|
1085
|
+
t = None
|
|
1086
|
+
else:
|
|
1087
|
+
t = self.returntype[i]
|
|
1088
|
+
self.ir_builder.add_output(self._current_fn, return_var, t, self._source_of(stmt))
|
|
1089
|
+
return return_var
|
|
1090
|
+
|
|
1091
|
+
val = stmt.value
|
|
1092
|
+
assert val is not None, "Return statement without return-value not supported."
|
|
1093
|
+
if isinstance(val, ast.Tuple):
|
|
1094
|
+
check_num_outputs(len(val.elts))
|
|
1095
|
+
return [ret(exp, i, str(i)) for i, exp in enumerate(val.elts)]
|
|
1096
|
+
check_num_outputs(1)
|
|
1097
|
+
return ret(val, 0, "")
|
|
1098
|
+
|
|
1099
|
+
def _translate_if_stmt(self, stmt: ast.If) -> None:
|
|
1100
|
+
if hasattr(stmt, "live_out"):
|
|
1101
|
+
live_defs = list(
|
|
1102
|
+
stmt.live_out.intersection(analysis.assigned_vars(stmt, self._message))
|
|
1103
|
+
)
|
|
1104
|
+
else:
|
|
1105
|
+
live_defs = list(analysis.assigned_vars(stmt, self._message))
|
|
1106
|
+
test = self._translate_expr(stmt.test, "cond").name
|
|
1107
|
+
lineno = self._source_of(stmt).lineno
|
|
1108
|
+
thenGraph, sub_fct_then = self._translate_block(
|
|
1109
|
+
stmt.body, f"thenGraph_{lineno}", live_defs, parent_stmt=stmt
|
|
1110
|
+
)
|
|
1111
|
+
thenAttr = self._make_onnx_attr("then_branch", thenGraph)
|
|
1112
|
+
elseGraph, sub_fct_else = self._translate_block(
|
|
1113
|
+
stmt.orelse, f"elseGraph_{lineno}", live_defs, parent_stmt=stmt
|
|
1114
|
+
)
|
|
1115
|
+
elseAttr = self._make_onnx_attr("else_branch", elseGraph)
|
|
1116
|
+
|
|
1117
|
+
def rename(x):
|
|
1118
|
+
r = self.generate_unique_name(x)
|
|
1119
|
+
self._bind(
|
|
1120
|
+
x,
|
|
1121
|
+
values.Dynamic(r, values.DynamicKind.Intermediate, self._source_of(stmt)),
|
|
1122
|
+
)
|
|
1123
|
+
return r
|
|
1124
|
+
|
|
1125
|
+
# no break condition
|
|
1126
|
+
renamed = [rename(x) for x in live_defs]
|
|
1127
|
+
if not renamed:
|
|
1128
|
+
self.fail(stmt, "A subgraph for a test do not have any output variable.")
|
|
1129
|
+
|
|
1130
|
+
sub_functions = {}
|
|
1131
|
+
sub_functions.update(sub_fct_then)
|
|
1132
|
+
sub_functions.update(sub_fct_else)
|
|
1133
|
+
if renamed == [test]:
|
|
1134
|
+
self.fail(stmt, f"Input and output cannot be the same {renamed!r}.")
|
|
1135
|
+
self.emit(
|
|
1136
|
+
renamed,
|
|
1137
|
+
values.Op(self.default_opset, "If"),
|
|
1138
|
+
[test],
|
|
1139
|
+
[thenAttr, elseAttr],
|
|
1140
|
+
sub_functions=sub_functions,
|
|
1141
|
+
)
|
|
1142
|
+
|
|
1143
|
+
def _translate_loop_stmt(self, loop_stmt: Union[ast.For, ast.While]) -> None:
|
|
1144
|
+
# loop-variable
|
|
1145
|
+
if isinstance(loop_stmt, ast.For):
|
|
1146
|
+
if not isinstance(loop_stmt.target, ast.Name):
|
|
1147
|
+
self.fail(loop_stmt, "For loop target must be a single variable.")
|
|
1148
|
+
p_loop_var = loop_stmt.target.id
|
|
1149
|
+
# iter
|
|
1150
|
+
iter = loop_stmt.iter
|
|
1151
|
+
assert isinstance(iter, ast.Call), "Loop bound not a call."
|
|
1152
|
+
if not isinstance(iter.func, ast.Name):
|
|
1153
|
+
self.fail(loop_stmt, f"Unsupported loop bound {iter.func!r}.")
|
|
1154
|
+
if iter.func.id != "range":
|
|
1155
|
+
self.fail(
|
|
1156
|
+
loop_stmt, "Unsupported loop bound, only function 'range' is allowed."
|
|
1157
|
+
)
|
|
1158
|
+
if not iter.args or len(iter.args) != 1:
|
|
1159
|
+
self.fail(loop_stmt, "Unsupported loop bound, it should be 'range(?)'.")
|
|
1160
|
+
assert not iter.keywords, "Unsupported loop bound."
|
|
1161
|
+
o_loop_bound = self._translate_expr(iter.args[0], "loop_bound").name
|
|
1162
|
+
o_cond_var = self.generate_unique_name("cond_in")
|
|
1163
|
+
i_cond_var = o_cond_var
|
|
1164
|
+
cond_while = None
|
|
1165
|
+
o_loop_condition = "" # No condition for a for loop.
|
|
1166
|
+
elif isinstance(loop_stmt, ast.While):
|
|
1167
|
+
test = loop_stmt.test
|
|
1168
|
+
if not isinstance(test, ast.Name):
|
|
1169
|
+
self.fail(
|
|
1170
|
+
loop_stmt,
|
|
1171
|
+
"Unexpected condition type {type(loop_stmt)!r} for a while loop, "
|
|
1172
|
+
"it should be 'while <condition_name>:'.",
|
|
1173
|
+
)
|
|
1174
|
+
p_loop_var = "infinite_loop"
|
|
1175
|
+
o_loop_bound = ""
|
|
1176
|
+
i_cond_var = test.id
|
|
1177
|
+
cond_while = test.id
|
|
1178
|
+
o_cond_var = None
|
|
1179
|
+
o_loop_condition = self._translate_name_expr(test)
|
|
1180
|
+
# we need to go through all the instructions to see
|
|
1181
|
+
# which instruction defines the condition test.id
|
|
1182
|
+
else:
|
|
1183
|
+
self.fail(loop_stmt, f"Unexpected loop type {type(loop_stmt)!r}.")
|
|
1184
|
+
# analyze loop body
|
|
1185
|
+
exposed_uses = analysis.exposed_uses(loop_stmt.body, self._message)
|
|
1186
|
+
vars_def_in_loop = analysis.assigned_vars(loop_stmt.body, self._message)
|
|
1187
|
+
loop_state_vars = vars_def_in_loop.intersection(exposed_uses | loop_stmt.live_out)
|
|
1188
|
+
scan_outputs = set() # TODO
|
|
1189
|
+
outputs = list(loop_state_vars | scan_outputs)
|
|
1190
|
+
|
|
1191
|
+
# loop-condition:
|
|
1192
|
+
# o_loop_condition = self._emit_const(True, "true", self._source_of(loop_stmt))
|
|
1193
|
+
|
|
1194
|
+
# build loop_body
|
|
1195
|
+
self._enter_scope("loop_body", loop_stmt)
|
|
1196
|
+
o_loop_var = self.generate_unique_name(p_loop_var)
|
|
1197
|
+
self.ir_builder.add_input(
|
|
1198
|
+
self._current_fn,
|
|
1199
|
+
o_loop_var,
|
|
1200
|
+
onnx_types.INT64,
|
|
1201
|
+
self._source_of(loop_stmt),
|
|
1202
|
+
)
|
|
1203
|
+
self._bind(
|
|
1204
|
+
p_loop_var,
|
|
1205
|
+
values.Dynamic(o_loop_var, values.DynamicKind.Loop, self._source_of(loop_stmt)),
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
self.ir_builder.add_input(
|
|
1209
|
+
self._current_fn,
|
|
1210
|
+
i_cond_var,
|
|
1211
|
+
onnx_types.BOOL,
|
|
1212
|
+
self._source_of(loop_stmt),
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
for pv in loop_state_vars:
|
|
1216
|
+
ov = self.generate_unique_name(pv)
|
|
1217
|
+
# TODO: retrieve the annotation for variable pv is any is specified.
|
|
1218
|
+
# typeinfo = self._eval_constant_expr(pv.annotation)
|
|
1219
|
+
typeinfo = None
|
|
1220
|
+
self.ir_builder.add_input(
|
|
1221
|
+
self._current_fn, ov, typeinfo, self._source_of(loop_stmt)
|
|
1222
|
+
)
|
|
1223
|
+
self._bind(
|
|
1224
|
+
pv,
|
|
1225
|
+
values.Dynamic(ov, values.DynamicKind.Loop, self._source_of(loop_stmt)),
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
condition_name = None
|
|
1229
|
+
operator_name = "Identity"
|
|
1230
|
+
for i, s in enumerate(loop_stmt.body):
|
|
1231
|
+
# We first need to intercept a break instruction in test block.
|
|
1232
|
+
# It must be something like `if <condition_name>: break`.
|
|
1233
|
+
# This instruction must be the last of the loop body.
|
|
1234
|
+
if isinstance(s, ast.If) and len(s.body) == 1 and isinstance(s.body[0], ast.Break):
|
|
1235
|
+
if not isinstance(s.test, ast.Name):
|
|
1236
|
+
self.fail(
|
|
1237
|
+
s,
|
|
1238
|
+
f"Instruction break can be introduced with test but it must be "
|
|
1239
|
+
f"if <condition>: break. However condition is of type "
|
|
1240
|
+
f"{type(s.test)!r}.",
|
|
1241
|
+
)
|
|
1242
|
+
if i != len(loop_stmt.body) - 1:
|
|
1243
|
+
self.fail(s, "Instruction break must be the last one of the loop.")
|
|
1244
|
+
|
|
1245
|
+
current_scope = self._current_scope()
|
|
1246
|
+
if s.test.id not in current_scope:
|
|
1247
|
+
self.fail(
|
|
1248
|
+
loop_stmt,
|
|
1249
|
+
f"Unable to find condition variable {s.test.id!r} in known "
|
|
1250
|
+
f"variables {list(current_scope)!r}.",
|
|
1251
|
+
)
|
|
1252
|
+
condition_name = current_scope[s.test.id].value
|
|
1253
|
+
operator_name = "Not"
|
|
1254
|
+
continue
|
|
1255
|
+
self._translate_stmt(s)
|
|
1256
|
+
|
|
1257
|
+
o_cond_out = self.generate_unique_name("cond_out")
|
|
1258
|
+
|
|
1259
|
+
if cond_while is not None:
|
|
1260
|
+
# Loop while
|
|
1261
|
+
current_scope = self._current_scope()
|
|
1262
|
+
if cond_while not in current_scope:
|
|
1263
|
+
self.fail(
|
|
1264
|
+
loop_stmt,
|
|
1265
|
+
f"Unable to find condition variable {cond_while!r} in known "
|
|
1266
|
+
f"variables {list(current_scope)!r}.",
|
|
1267
|
+
)
|
|
1268
|
+
o_cond_var = current_scope[cond_while].value
|
|
1269
|
+
|
|
1270
|
+
self.emit(
|
|
1271
|
+
[o_cond_out],
|
|
1272
|
+
values.Op(self.default_opset, operator_name),
|
|
1273
|
+
[condition_name or o_cond_var],
|
|
1274
|
+
[],
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
self.ir_builder.add_output(
|
|
1278
|
+
self._current_fn,
|
|
1279
|
+
o_cond_out,
|
|
1280
|
+
onnx_types.BOOL,
|
|
1281
|
+
self._source_of(loop_stmt),
|
|
1282
|
+
)
|
|
1283
|
+
for pv in loop_state_vars:
|
|
1284
|
+
ov = self._py_var_to_onnx_var(pv, self._source_of(loop_stmt)).name
|
|
1285
|
+
if ov not in self._current_fn.assigned_names:
|
|
1286
|
+
# When converting the loop-body into a graph, we need to handle
|
|
1287
|
+
# identity assignments of the form "x = y" inside the loop body
|
|
1288
|
+
# specially if y represents a value computed outside the loop body.
|
|
1289
|
+
# In this case, we create a copy of y, treating the statement as
|
|
1290
|
+
# shorthand for "x = op.Identity(y)".
|
|
1291
|
+
ov = self._emit_copy(ov, pv)
|
|
1292
|
+
# TODO: retrieve variable type for the annotation if any.
|
|
1293
|
+
typeinfo = None
|
|
1294
|
+
self.ir_builder.add_output(
|
|
1295
|
+
self._current_fn, ov, typeinfo, self._source_of(loop_stmt)
|
|
1296
|
+
)
|
|
1297
|
+
body = self._exit_scope()
|
|
1298
|
+
inputs = [o_loop_bound, o_loop_condition] + [
|
|
1299
|
+
self._py_var_to_onnx_var(pv, self._source_of(loop_stmt)).name
|
|
1300
|
+
for pv in loop_state_vars
|
|
1301
|
+
]
|
|
1302
|
+
graph, sub_functions = body.to_graph_and_functions()
|
|
1303
|
+
attrs = [self._make_onnx_attr("body", graph)]
|
|
1304
|
+
info = self._source_of(loop_stmt)
|
|
1305
|
+
|
|
1306
|
+
def rename(x):
|
|
1307
|
+
r = self.generate_unique_name(x)
|
|
1308
|
+
self._bind(x, values.Dynamic(r, values.DynamicKind.Output, info))
|
|
1309
|
+
return r
|
|
1310
|
+
|
|
1311
|
+
onnx_outputs = [rename(x) for x in outputs]
|
|
1312
|
+
self.emit(
|
|
1313
|
+
onnx_outputs,
|
|
1314
|
+
"Loop",
|
|
1315
|
+
inputs,
|
|
1316
|
+
attrs,
|
|
1317
|
+
sub_functions=sub_functions,
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
def _translate_block(
|
|
1321
|
+
self,
|
|
1322
|
+
stmts: Sequence[ast.stmt],
|
|
1323
|
+
name: str,
|
|
1324
|
+
live_defs: Sequence[str],
|
|
1325
|
+
parent_stmt: ast.stmt,
|
|
1326
|
+
):
|
|
1327
|
+
"""Translation of a statement-block to GraphProto attribute."""
|
|
1328
|
+
info_stmt = stmts[0] if len(stmts) > 0 else parent_stmt
|
|
1329
|
+
source = self._source_of(info_stmt)
|
|
1330
|
+
self._enter_scope(name, None)
|
|
1331
|
+
for s in stmts:
|
|
1332
|
+
self._translate_stmt(s)
|
|
1333
|
+
for pvar in live_defs:
|
|
1334
|
+
if pvar in self._current_scope():
|
|
1335
|
+
pv_val = self._current_scope()[pvar]
|
|
1336
|
+
output = self._to_onnx_var(pv_val, pvar).name
|
|
1337
|
+
if output not in self._current_fn.assigned_names:
|
|
1338
|
+
# To return an outer-scope variable, an ONNX Graph has to
|
|
1339
|
+
# use an explicit copy via Identity.
|
|
1340
|
+
output = self._emit_copy(output, pvar)
|
|
1341
|
+
self.ir_builder.add_output(
|
|
1342
|
+
self._current_fn,
|
|
1343
|
+
output,
|
|
1344
|
+
pv_val.typeinfo,
|
|
1345
|
+
source,
|
|
1346
|
+
)
|
|
1347
|
+
else:
|
|
1348
|
+
pv_val = None
|
|
1349
|
+
for scope in self._locals: # TODO: skip _current_scope
|
|
1350
|
+
if pvar in scope:
|
|
1351
|
+
pv_val = scope[pvar]
|
|
1352
|
+
break
|
|
1353
|
+
if pv_val is None:
|
|
1354
|
+
self.fail(
|
|
1355
|
+
stmts[0],
|
|
1356
|
+
f"Variable {pvar} is not assigned a value along a conditional "
|
|
1357
|
+
f"branch, known variables: {list(self._locals)}.",
|
|
1358
|
+
)
|
|
1359
|
+
# introduce a copy
|
|
1360
|
+
ovar = self._emit_copy(self._to_onnx_var(pv_val, pvar).name, pvar)
|
|
1361
|
+
|
|
1362
|
+
# TODO: retrieve the annotation if any.
|
|
1363
|
+
typeinfo = None
|
|
1364
|
+
self.ir_builder.add_output(self._current_fn, ovar, typeinfo, source)
|
|
1365
|
+
graph = self._exit_scope()
|
|
1366
|
+
return graph.to_graph_and_functions()
|
|
1367
|
+
|
|
1368
|
+
def _translate_nested_function_def(self, fn: ast.FunctionDef) -> None:
|
|
1369
|
+
"""Translate a nested function definition."""
|
|
1370
|
+
self._enter_scope(fn.name, fn)
|
|
1371
|
+
self._translate_function_def_common(fn)
|
|
1372
|
+
function_ir = self._exit_scope()
|
|
1373
|
+
outer_scope_vars = analysis.outer_scope_variables(fn, self._message)
|
|
1374
|
+
function_ir.outer_scope_variables = [
|
|
1375
|
+
(var, self._lookup(var, self._source_of(fn))) for var in outer_scope_vars
|
|
1376
|
+
]
|
|
1377
|
+
self._bind(fn.name, function_ir)
|
|
1378
|
+
# TODO: Does not yet handle nested functions within nested functions.
|
|
1379
|
+
self._current_fn.add_nested_function(function_ir)
|
|
1380
|
+
|
|
1381
|
+
def _translate_function_signature_common(
|
|
1382
|
+
self, fn: ast.FunctionDef
|
|
1383
|
+
) -> irbuilder.IRFunction:
|
|
1384
|
+
"""Translate a function signature (top-level or nested)."""
|
|
1385
|
+
args = fn.args
|
|
1386
|
+
if args.vararg or args.kwonlyargs or args.kw_defaults or args.kwarg:
|
|
1387
|
+
warn(f"{fn.name}: Unsupported feature in function signature.")
|
|
1388
|
+
for i, x in enumerate(args.args):
|
|
1389
|
+
arg_with_default_start_index = len(args.args) - len(args.defaults)
|
|
1390
|
+
if args.defaults and i >= arg_with_default_start_index:
|
|
1391
|
+
default_value = self._eval_constant_expr(
|
|
1392
|
+
args.defaults[i - arg_with_default_start_index]
|
|
1393
|
+
)
|
|
1394
|
+
else:
|
|
1395
|
+
default_value = None
|
|
1396
|
+
if x.annotation:
|
|
1397
|
+
typeinfo = self._eval_constant_expr(x.annotation)
|
|
1398
|
+
if not ta.is_valid_type(typeinfo):
|
|
1399
|
+
self.warn(
|
|
1400
|
+
x.annotation,
|
|
1401
|
+
f"Unsupported type annotation for argument {x.arg}.",
|
|
1402
|
+
)
|
|
1403
|
+
typeinfo = None
|
|
1404
|
+
else:
|
|
1405
|
+
# The code can only be exported as a function.
|
|
1406
|
+
typeinfo = None
|
|
1407
|
+
if typeinfo and ta.is_attr_type(typeinfo):
|
|
1408
|
+
self.ir_builder.add_attr_parameter(
|
|
1409
|
+
self._current_fn,
|
|
1410
|
+
x.arg,
|
|
1411
|
+
ta.pytype_to_attrtype(typeinfo),
|
|
1412
|
+
default_value,
|
|
1413
|
+
)
|
|
1414
|
+
self._bind(x.arg, values.AttrRef(x.arg, typeinfo, self._source_of(x)))
|
|
1415
|
+
else:
|
|
1416
|
+
self.ir_builder.add_input(
|
|
1417
|
+
self._current_fn, x.arg, typeinfo, self._source_of(x)
|
|
1418
|
+
)
|
|
1419
|
+
self._used_vars.add(x.arg)
|
|
1420
|
+
self._bind(
|
|
1421
|
+
x.arg,
|
|
1422
|
+
values.Dynamic(x.arg, values.DynamicKind.Input, self._source_of(x)),
|
|
1423
|
+
)
|
|
1424
|
+
if fn.returns:
|
|
1425
|
+
type_annotation = self._eval_constant_expr(fn.returns)
|
|
1426
|
+
self.returntype = ta.get_return_types(type_annotation)
|
|
1427
|
+
invalid = False
|
|
1428
|
+
for t in self.returntype:
|
|
1429
|
+
if not ta.is_valid_type(t):
|
|
1430
|
+
self.warn(
|
|
1431
|
+
fn.returns,
|
|
1432
|
+
f"Unsupported type annotation for return value {t}.",
|
|
1433
|
+
)
|
|
1434
|
+
invalid = True
|
|
1435
|
+
if invalid:
|
|
1436
|
+
self.returntype = None
|
|
1437
|
+
else:
|
|
1438
|
+
self.returntype = None
|
|
1439
|
+
|
|
1440
|
+
return self._current_fn
|
|
1441
|
+
|
|
1442
|
+
def _translate_function_def_common(self, fn: ast.FunctionDef) -> irbuilder.IRFunction:
|
|
1443
|
+
"""Translate a function definition, including the signature and its body."""
|
|
1444
|
+
logger.debug("Converter:_translate_function_def_common:%s", fn.name)
|
|
1445
|
+
_ = self._translate_function_signature_common(fn)
|
|
1446
|
+
for i, s in enumerate(fn.body):
|
|
1447
|
+
self._translate_stmt(s, index_of_stmt=i)
|
|
1448
|
+
return self._current_fn
|
|
1449
|
+
|
|
1450
|
+
def translate_function_def(self, stmt: ast.FunctionDef) -> irbuilder.IRFunction:
|
|
1451
|
+
if isinstance(stmt, ast.FunctionDef):
|
|
1452
|
+
self._init_function_translation()
|
|
1453
|
+
if self.default_opset_ is None:
|
|
1454
|
+
opset = self._find_onnx_opset(stmt)
|
|
1455
|
+
if opset:
|
|
1456
|
+
self._set_default_opset(opset, stmt)
|
|
1457
|
+
domain = self.this_module.domain
|
|
1458
|
+
self._current_fn = self.ir_builder.new_function(stmt.name, domain, True)
|
|
1459
|
+
analysis.do_liveness_analysis(stmt, self._message)
|
|
1460
|
+
fn_ir = self._translate_function_def_common(stmt)
|
|
1461
|
+
fn_ir.debug_print()
|
|
1462
|
+
self.this_module.add_function_def(fn_ir)
|
|
1463
|
+
return fn_ir
|
|
1464
|
+
raise ValueError(f"Unsupported top-level statement type {type(stmt)!r}.")
|
|
1465
|
+
|
|
1466
|
+
def translate_function_signature(self, fn: ast.FunctionDef) -> irbuilder.IRFunction:
|
|
1467
|
+
"""Translate a (top-level) function signature."""
|
|
1468
|
+
domain = self.this_module.domain
|
|
1469
|
+
self._current_fn = self.ir_builder.new_function(fn.name, domain, True)
|
|
1470
|
+
return self._translate_function_signature_common(fn)
|