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,885 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional, Sequence
|
|
6
|
+
|
|
7
|
+
import numpy
|
|
8
|
+
import onnx
|
|
9
|
+
from onnx import FunctionProto, GraphProto, ModelProto, TensorProto, ValueInfoProto
|
|
10
|
+
from onnx.helper import make_node
|
|
11
|
+
|
|
12
|
+
import onnxscript.onnx_types
|
|
13
|
+
import onnxscript.type_annotation
|
|
14
|
+
|
|
15
|
+
_SINGLE_INDENT = " "
|
|
16
|
+
|
|
17
|
+
kwlist = {
|
|
18
|
+
"False",
|
|
19
|
+
"None",
|
|
20
|
+
"True",
|
|
21
|
+
"and",
|
|
22
|
+
"as",
|
|
23
|
+
"assert",
|
|
24
|
+
"async",
|
|
25
|
+
"await",
|
|
26
|
+
"break",
|
|
27
|
+
"class",
|
|
28
|
+
"continue",
|
|
29
|
+
"def",
|
|
30
|
+
"del",
|
|
31
|
+
"elif",
|
|
32
|
+
"else",
|
|
33
|
+
"except",
|
|
34
|
+
"finally",
|
|
35
|
+
"for",
|
|
36
|
+
"from",
|
|
37
|
+
"global",
|
|
38
|
+
"if",
|
|
39
|
+
"import",
|
|
40
|
+
"in",
|
|
41
|
+
"is",
|
|
42
|
+
"lambda",
|
|
43
|
+
"nonlocal",
|
|
44
|
+
"not",
|
|
45
|
+
"or",
|
|
46
|
+
"pass",
|
|
47
|
+
"raise",
|
|
48
|
+
"return",
|
|
49
|
+
"try",
|
|
50
|
+
"while",
|
|
51
|
+
"with",
|
|
52
|
+
"yield",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_const_repr(const_node):
|
|
57
|
+
"""Given an ONNX Constant-op node, returns a string representation of
|
|
58
|
+
the constant-value in ONNXScript, if a compact representation is possible.
|
|
59
|
+
Returns None otherwise.
|
|
60
|
+
Supports only FLOAT/INT64 values and scalars and small rank-1 tensors.
|
|
61
|
+
This needs to be reconciled with the ONNXScript converter.
|
|
62
|
+
"""
|
|
63
|
+
assert const_node.op_type == "Constant", "Expected a constant node"
|
|
64
|
+
attr = const_node.attribute[0]
|
|
65
|
+
if not attr.HasField("t"):
|
|
66
|
+
return None
|
|
67
|
+
tensor_proto = attr.t
|
|
68
|
+
if tensor_proto.data_type in {TensorProto.FLOAT, TensorProto.INT64}:
|
|
69
|
+
rank = len(tensor_proto.dims)
|
|
70
|
+
if rank == 0:
|
|
71
|
+
array = onnx.numpy_helper.to_array(tensor_proto).reshape(1)
|
|
72
|
+
return repr(array[0])
|
|
73
|
+
if rank == 1 and tensor_proto.dims[0] < 5:
|
|
74
|
+
return repr(list(onnx.numpy_helper.to_array(tensor_proto)))
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _cleanup_variable_name(name: ValueInfoProto | str) -> str:
|
|
79
|
+
"""Converts given name into a valid python variable names.
|
|
80
|
+
Handles names that clash with python keywords and common issues seen in ONNX models:
|
|
81
|
+
* Identifiers like "5" (that do not start with an alpha character)
|
|
82
|
+
* Identifiers that contain a dot like "layers.0.foo"
|
|
83
|
+
This is a simple heuristic, and doesn't guarantee it avoids name-clashes.
|
|
84
|
+
"""
|
|
85
|
+
if isinstance(name, ValueInfoProto):
|
|
86
|
+
# Handle graph/function input/output uniformly
|
|
87
|
+
name = name.name
|
|
88
|
+
assert isinstance(name, str)
|
|
89
|
+
assert name != ""
|
|
90
|
+
if name in kwlist:
|
|
91
|
+
return f"r_{name}"
|
|
92
|
+
first = name[0]
|
|
93
|
+
if not (first.isalpha() or (first == "_")):
|
|
94
|
+
name = f"__{name}"
|
|
95
|
+
|
|
96
|
+
def rename_char(char):
|
|
97
|
+
"""Replace invalid character by underscore."""
|
|
98
|
+
return char if (char.isalnum() or (char == "_")) else "_"
|
|
99
|
+
|
|
100
|
+
return "".join([rename_char(c) for c in name])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _make_short_name_mapper():
|
|
104
|
+
"""Returns a renamer used to create short new names (like v0, v1, ...) for variables."""
|
|
105
|
+
variable_names: dict[str, str] = {}
|
|
106
|
+
|
|
107
|
+
def renamer(name):
|
|
108
|
+
# TODO: simplify this. No need to use _cleanup_variable_name?
|
|
109
|
+
var_name = _cleanup_variable_name(name)
|
|
110
|
+
if var_name in variable_names:
|
|
111
|
+
return variable_names[var_name]
|
|
112
|
+
new_name = f"v{len(variable_names) + 1}"
|
|
113
|
+
assert var_name is not None # TODO(rama): This looks suspect.
|
|
114
|
+
variable_names[var_name] = new_name
|
|
115
|
+
return new_name
|
|
116
|
+
|
|
117
|
+
return renamer
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _translate_type(onnx_type):
|
|
121
|
+
"""Converts a onnx type into a type defined by *onnxscript*."""
|
|
122
|
+
return onnxscript.onnx_types.onnx_type_to_onnxscript_repr(onnx_type)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _translate_signature(inputs, outputs):
|
|
126
|
+
"""Produce the script-functions signature."""
|
|
127
|
+
|
|
128
|
+
def input_sig(inp: ValueInfoProto | str):
|
|
129
|
+
if isinstance(inp, ValueInfoProto):
|
|
130
|
+
# GraphProto inputs/outputs are ValueInfoProto
|
|
131
|
+
return f"{_cleanup_variable_name(inp.name)}: {_translate_type(inp.type)}"
|
|
132
|
+
|
|
133
|
+
# FunctionProto inputs/outputs are just strings
|
|
134
|
+
return _cleanup_variable_name(inp)
|
|
135
|
+
|
|
136
|
+
result = f"({', '.join([input_sig(x) for x in inputs])})"
|
|
137
|
+
if outputs and isinstance(outputs[0], ValueInfoProto):
|
|
138
|
+
result += f" -> ({', '.join([_translate_type(x.type) for x in outputs])})"
|
|
139
|
+
return f"{result}:"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _to_str(s):
|
|
143
|
+
if isinstance(s, bytes):
|
|
144
|
+
return s.decode("utf-8")
|
|
145
|
+
return s
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _is_attribute_ref(attr: onnx.AttributeProto) -> bool:
|
|
149
|
+
return attr.HasField("ref_attr_name") and attr.ref_attr_name != ""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _attribute_value(attr: onnx.AttributeProto):
|
|
153
|
+
if attr.type == onnx.AttributeProto.FLOAT:
|
|
154
|
+
return attr.f
|
|
155
|
+
if attr.type == onnx.AttributeProto.INT:
|
|
156
|
+
return attr.i
|
|
157
|
+
if attr.type == onnx.AttributeProto.STRING:
|
|
158
|
+
return _to_str(attr.s)
|
|
159
|
+
if attr.type == onnx.AttributeProto.TENSOR:
|
|
160
|
+
tensor_proto = attr.t
|
|
161
|
+
if onnx.external_data_helper.uses_external_data(tensor_proto):
|
|
162
|
+
return tensor_proto
|
|
163
|
+
else:
|
|
164
|
+
return onnx.numpy_helper.to_array(tensor_proto)
|
|
165
|
+
# TODO:
|
|
166
|
+
# - onnx.AttributeProto.GRAPH
|
|
167
|
+
# - onnx.AttributeProto.SPARSE_TENSOR
|
|
168
|
+
# - onnx.AttributeProto.TYPE_PROTO
|
|
169
|
+
if attr.type == onnx.AttributeProto.FLOATS:
|
|
170
|
+
return list(attr.floats)
|
|
171
|
+
if attr.type == onnx.AttributeProto.INTS:
|
|
172
|
+
return list(attr.ints)
|
|
173
|
+
if attr.type == onnx.AttributeProto.STRINGS:
|
|
174
|
+
return list(map(_to_str, attr.strings))
|
|
175
|
+
# TODO:
|
|
176
|
+
# - onnx.AttributeProto.TENSORS
|
|
177
|
+
# - onnx.AttributeProto.GRAPHS
|
|
178
|
+
# - onnx.AttributeProto.SPARSE_TENSORS
|
|
179
|
+
# - onnx.AttributeProto.TYPE_PROTOS
|
|
180
|
+
raise NotImplementedError(f"Unable to return a value for attribute {attr!r}.")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _update_names_used_in_graph(names: set[str], graph: GraphProto) -> None:
|
|
184
|
+
"""Adds the names used in a graph to given set."""
|
|
185
|
+
names.update(x.name for x in graph.input)
|
|
186
|
+
names.update(x.name for x in graph.output)
|
|
187
|
+
names.update(x.name for x in graph.initializer)
|
|
188
|
+
for node in graph.node:
|
|
189
|
+
_update_names_used_in_node(names, node)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _update_names_used_in_node(names: set[str], node: onnx.NodeProto) -> None:
|
|
193
|
+
names.update(node.input)
|
|
194
|
+
names.update(node.output)
|
|
195
|
+
for attr in node.attribute:
|
|
196
|
+
if attr.HasField("g"):
|
|
197
|
+
_update_names_used_in_graph(names, attr.g)
|
|
198
|
+
for g in attr.graphs:
|
|
199
|
+
_update_names_used_in_graph(names, g)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _update_names_used_in_function(names: set[str], fun: FunctionProto) -> None:
|
|
203
|
+
names.update(fun.input)
|
|
204
|
+
names.update(fun.output)
|
|
205
|
+
for node in fun.node:
|
|
206
|
+
_update_names_used_in_node(names, node)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _names_used_in_function(fun: FunctionProto) -> set[str]:
|
|
210
|
+
names: set[str] = set()
|
|
211
|
+
_update_names_used_in_function(names, fun)
|
|
212
|
+
return names
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def has_input(node: onnx.NodeProto, index: int) -> bool:
|
|
216
|
+
"""Returns True iff the node has an input at given index."""
|
|
217
|
+
return index < len(node.input) and node.input[index] != ""
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def is_onnx_op(node: onnx.NodeProto, op_type: str) -> bool:
|
|
221
|
+
return node.op_type == op_type and node.domain in {"", "ai.onnx"}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _is_used_in_graph_body(name: str, graph: GraphProto) -> bool:
|
|
225
|
+
"""Returns True iff the given name is used in the graph body."""
|
|
226
|
+
# TODO: This is an approximation.
|
|
227
|
+
names: set[str] = set()
|
|
228
|
+
for node in graph.node:
|
|
229
|
+
_update_names_used_in_node(names, node)
|
|
230
|
+
return name in names
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _cond_is_used_in_loop_body(graph: GraphProto) -> bool:
|
|
234
|
+
"""Returns True iff loop requires a condition."""
|
|
235
|
+
cond_in = graph.input[1].name
|
|
236
|
+
cond_out = graph.output[0].name
|
|
237
|
+
for node in graph.node:
|
|
238
|
+
# Ignore "cond_out = Identity(cond_in)" node
|
|
239
|
+
if is_onnx_op(node, "Identity") and len(node.input) == 1 and len(node.output) == 1:
|
|
240
|
+
if node.input[0] == cond_in and node.output[0] == cond_out:
|
|
241
|
+
continue
|
|
242
|
+
names: set[str] = set()
|
|
243
|
+
# TODO: The following is an approximation-based check
|
|
244
|
+
_update_names_used_in_node(names, node)
|
|
245
|
+
if (cond_in in names) or (cond_out in names):
|
|
246
|
+
return True
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class _Exporter:
|
|
251
|
+
"""Class used for recursive traversal of Proto structures."""
|
|
252
|
+
|
|
253
|
+
def __init__(
|
|
254
|
+
self, *, rename: bool, use_operators: bool, inline_const: bool, skip_initializers: bool
|
|
255
|
+
) -> None:
|
|
256
|
+
self.use_operators = use_operators
|
|
257
|
+
if rename:
|
|
258
|
+
rename_function = _make_short_name_mapper()
|
|
259
|
+
else:
|
|
260
|
+
rename_function = _cleanup_variable_name
|
|
261
|
+
self._rename_variable = self._handle_attrname_conflict(rename_function)
|
|
262
|
+
self.inline_const = inline_const
|
|
263
|
+
self.constants: dict[str, str] = {}
|
|
264
|
+
self._attr_renaming: dict[str, str | None] = {} # For current function.
|
|
265
|
+
self._names_used: set[str] = set() # For current function.
|
|
266
|
+
# _name_remappings: used to undo the SSA-renaming in ONNX control-flow ops.
|
|
267
|
+
# We map the multiple SSA-variants back to the same Python variable name.
|
|
268
|
+
self._name_remappings: list[dict[str, str]] = []
|
|
269
|
+
self.skip_initializers = skip_initializers
|
|
270
|
+
self.skipped_initializers: dict[str, onnx.TensorProto] = {}
|
|
271
|
+
|
|
272
|
+
def _handle_attrname_conflict(self, renamer):
|
|
273
|
+
"""Add ref-attr-name-conflict handling logic to renaming function."""
|
|
274
|
+
|
|
275
|
+
def new_renamer(name):
|
|
276
|
+
new_name = renamer(name)
|
|
277
|
+
if new_name not in self._attr_renaming:
|
|
278
|
+
return new_name
|
|
279
|
+
# Name conflicts with attribute parameter name.
|
|
280
|
+
alternate = self._attr_renaming[new_name]
|
|
281
|
+
if alternate is not None:
|
|
282
|
+
return alternate
|
|
283
|
+
counter = 0
|
|
284
|
+
candidate = new_name
|
|
285
|
+
while candidate in self._names_used:
|
|
286
|
+
candidate = f"{new_name}_{counter}"
|
|
287
|
+
counter += 1
|
|
288
|
+
self._attr_renaming[new_name] = candidate
|
|
289
|
+
self._names_used.add(candidate)
|
|
290
|
+
return candidate
|
|
291
|
+
|
|
292
|
+
return new_renamer
|
|
293
|
+
|
|
294
|
+
def _translate_onnx_var(self, var):
|
|
295
|
+
"""Converts an ONNX variable name to a python variable name."""
|
|
296
|
+
if isinstance(var, ValueInfoProto):
|
|
297
|
+
var = var.name
|
|
298
|
+
if var == "":
|
|
299
|
+
return "None"
|
|
300
|
+
for scope in reversed(self._name_remappings):
|
|
301
|
+
if var in scope:
|
|
302
|
+
return scope[var]
|
|
303
|
+
return self._rename_variable(var)
|
|
304
|
+
|
|
305
|
+
def _translate_onnx_var_ref(self, var):
|
|
306
|
+
"""Translates a reference to an ONNX variable (a r-value)"""
|
|
307
|
+
if var in self.constants:
|
|
308
|
+
return self.constants[var]
|
|
309
|
+
|
|
310
|
+
return self._translate_onnx_var(var)
|
|
311
|
+
|
|
312
|
+
def _rename_domain(self, domain: str) -> str:
|
|
313
|
+
if domain in {"", "ai.onnx"}:
|
|
314
|
+
return "opset" # TODO: Need checks to avoid name conflicts.
|
|
315
|
+
return _cleanup_variable_name(domain) # type: ignore[return-value]
|
|
316
|
+
|
|
317
|
+
def _make_opset_name(self, domain, version):
|
|
318
|
+
return f"{self._rename_domain(domain)}{version}"
|
|
319
|
+
|
|
320
|
+
def _make_callee_name(self, domain, version, name, node=False):
|
|
321
|
+
"""Generate name to be used for called op/function in a node or for a generated script function."""
|
|
322
|
+
# TODO: Avoid name-conflict between function-names and value-names
|
|
323
|
+
name = _cleanup_variable_name(name)
|
|
324
|
+
if node:
|
|
325
|
+
if version is None:
|
|
326
|
+
version = 1
|
|
327
|
+
if not isinstance(version, int):
|
|
328
|
+
raise TypeError(
|
|
329
|
+
f"version must be an integer not {version!r} for domain={domain!r} "
|
|
330
|
+
f"and name={name!r}."
|
|
331
|
+
)
|
|
332
|
+
opset = self._make_opset_name(domain, version)
|
|
333
|
+
return f"{opset}.{name}"
|
|
334
|
+
return name
|
|
335
|
+
|
|
336
|
+
def _translate_graph_body(self, graph, opsets, indent=0):
|
|
337
|
+
"""Translates a graph body into python.
|
|
338
|
+
The graph may be the main graph (of a model) or a subgraph (of a Loop or If node).
|
|
339
|
+
"""
|
|
340
|
+
code = []
|
|
341
|
+
if hasattr(graph, "initializer"):
|
|
342
|
+
for init in graph.initializer:
|
|
343
|
+
if self.skip_initializers:
|
|
344
|
+
init_py_name = self._translate_onnx_var(init.name)
|
|
345
|
+
if init_py_name in self.skipped_initializers:
|
|
346
|
+
raise RuntimeError(
|
|
347
|
+
f"Initializer {init.name!r} is already present in skipped_initializers."
|
|
348
|
+
)
|
|
349
|
+
self.skipped_initializers[init_py_name] = init
|
|
350
|
+
continue
|
|
351
|
+
node = make_node(
|
|
352
|
+
"Constant",
|
|
353
|
+
[],
|
|
354
|
+
[self._translate_onnx_var(init.name)], # type: ignore[list-item]
|
|
355
|
+
value=init,
|
|
356
|
+
)
|
|
357
|
+
code.append(self._translate_node(node, opsets, indent=indent))
|
|
358
|
+
if hasattr(graph, "sparse_initializer") and len(graph.sparse_initializer) > 0:
|
|
359
|
+
raise NotImplementedError("Unable to convert sparse_initilizer into python.")
|
|
360
|
+
for node in graph.node:
|
|
361
|
+
pynode = self._translate_node(node, opsets, indent=indent)
|
|
362
|
+
if pynode:
|
|
363
|
+
code.append(pynode)
|
|
364
|
+
|
|
365
|
+
final = "\n".join(code)
|
|
366
|
+
return final
|
|
367
|
+
|
|
368
|
+
def _translate_attributes(self, node):
|
|
369
|
+
attributes = []
|
|
370
|
+
for at in node.attribute:
|
|
371
|
+
if _is_attribute_ref(at):
|
|
372
|
+
attributes.append((at.name, at.ref_attr_name))
|
|
373
|
+
continue
|
|
374
|
+
value = _attribute_value(at)
|
|
375
|
+
if isinstance(value, str):
|
|
376
|
+
attributes.append((at.name, f"{value!r}"))
|
|
377
|
+
continue
|
|
378
|
+
if isinstance(value, numpy.ndarray):
|
|
379
|
+
onnx_dtype = at.t.data_type
|
|
380
|
+
if len(value.shape) == 0:
|
|
381
|
+
text = (
|
|
382
|
+
f'make_tensor("value", {onnx_dtype}, dims=[], '
|
|
383
|
+
f"vals=[{value.tolist()!r}])"
|
|
384
|
+
)
|
|
385
|
+
else:
|
|
386
|
+
text = (
|
|
387
|
+
f'make_tensor("value", {onnx_dtype}, dims={list(value.shape)!r}, '
|
|
388
|
+
f"vals={value.ravel().tolist()!r})"
|
|
389
|
+
)
|
|
390
|
+
attributes.append((at.name, text))
|
|
391
|
+
continue
|
|
392
|
+
if isinstance(value, TensorProto):
|
|
393
|
+
metadata = onnx.external_data_helper.ExternalDataInfo(value)
|
|
394
|
+
name = value.name or "value"
|
|
395
|
+
text = "external_tensor("
|
|
396
|
+
text += f"{name!r}, {value.data_type}, {list(value.dims)!r}"
|
|
397
|
+
text += f", {metadata.location!r}"
|
|
398
|
+
if metadata.offset:
|
|
399
|
+
text += f", offset={metadata.offset!r}"
|
|
400
|
+
if metadata.length:
|
|
401
|
+
text += f", length={metadata.length!r}"
|
|
402
|
+
text += ")"
|
|
403
|
+
attributes.append((at.name, text))
|
|
404
|
+
continue
|
|
405
|
+
attributes.append((at.name, repr(value)))
|
|
406
|
+
|
|
407
|
+
return ", ".join(f"{k}={v}" for k, v in attributes)
|
|
408
|
+
|
|
409
|
+
def _translate_if(self, node, opsets, indent=0):
|
|
410
|
+
"""Translates a node If into python."""
|
|
411
|
+
sindent = _SINGLE_INDENT * indent
|
|
412
|
+
code = [f"{sindent}if {node.input[0]}:"]
|
|
413
|
+
if len(node.attribute) != 2:
|
|
414
|
+
raise RuntimeError(
|
|
415
|
+
f"Node {node.op_type!r} expected two attributes not {len(node.attribute)}."
|
|
416
|
+
)
|
|
417
|
+
atts = node.attribute
|
|
418
|
+
if atts[0].name == "else_branch":
|
|
419
|
+
else_branch, then_branch = atts[0].g, atts[1].g
|
|
420
|
+
else:
|
|
421
|
+
else_branch, then_branch = atts[1].g, atts[0].g
|
|
422
|
+
|
|
423
|
+
code.append(
|
|
424
|
+
self._translate_graph_body(
|
|
425
|
+
then_branch,
|
|
426
|
+
opsets,
|
|
427
|
+
indent=indent + 1,
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
code.extend(self._emit_assign(node.output, then_branch.output, indent + 1))
|
|
431
|
+
|
|
432
|
+
code.append(f"{sindent}else:")
|
|
433
|
+
code.append(
|
|
434
|
+
self._translate_graph_body(
|
|
435
|
+
else_branch,
|
|
436
|
+
opsets,
|
|
437
|
+
indent=indent + 1,
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
code.extend(self._emit_assign(node.output, else_branch.output, indent + 1))
|
|
441
|
+
return "\n".join(code)
|
|
442
|
+
|
|
443
|
+
def _emit_assign(self, lhs, rhs, indent):
|
|
444
|
+
def to_var(x):
|
|
445
|
+
if isinstance(x, ValueInfoProto):
|
|
446
|
+
x = x.name
|
|
447
|
+
return self._translate_onnx_var(x)
|
|
448
|
+
|
|
449
|
+
sindent = _SINGLE_INDENT * indent
|
|
450
|
+
|
|
451
|
+
def assign(lhs_var: str, rhs_var: str):
|
|
452
|
+
return f"{sindent}{to_var(lhs_var)} = {to_var(rhs_var)}"
|
|
453
|
+
|
|
454
|
+
if isinstance(lhs, (str, ValueInfoProto)):
|
|
455
|
+
return [assign(lhs, rhs)]
|
|
456
|
+
return [assign(x, y) for x, y in zip(lhs, rhs)]
|
|
457
|
+
|
|
458
|
+
def _translate_loop(self, node, opsets, indent=0):
|
|
459
|
+
"""Translates a node Loop into python."""
|
|
460
|
+
body = node.attribute[0].g
|
|
461
|
+
sindent = _SINGLE_INDENT * indent
|
|
462
|
+
rows = []
|
|
463
|
+
|
|
464
|
+
# Node inputs: optional max-trip-count, optional condition, initial values (of dependencies)
|
|
465
|
+
# Node outputs: final values (of dependencies), scan-outputs
|
|
466
|
+
# Body inputs: iteration-count, condition, input values (of dependencies)
|
|
467
|
+
# Body outputs: condition, output values (of dependencies), scan-outputs
|
|
468
|
+
|
|
469
|
+
onnx_iter_var = body.input[0].name
|
|
470
|
+
if has_input(node, 0):
|
|
471
|
+
use_iter_var = True
|
|
472
|
+
n_iter = self._translate_onnx_var(node.input[0])
|
|
473
|
+
else:
|
|
474
|
+
use_iter_var = _is_used_in_graph_body(onnx_iter_var, body)
|
|
475
|
+
n_iter = None
|
|
476
|
+
iter_var = self._translate_onnx_var(onnx_iter_var)
|
|
477
|
+
|
|
478
|
+
cond_in = body.input[1].name
|
|
479
|
+
cond_out = body.output[0].name
|
|
480
|
+
py_cond = self._translate_onnx_var(cond_in)
|
|
481
|
+
use_loop_cond = True # TODO
|
|
482
|
+
if has_input(node, 1):
|
|
483
|
+
rows.extend(self._emit_assign(cond_in, node.input[1], indent))
|
|
484
|
+
else:
|
|
485
|
+
use_loop_cond = _cond_is_used_in_loop_body(body)
|
|
486
|
+
# rows.append(f"{sindent}{py_cond} = True")
|
|
487
|
+
|
|
488
|
+
num_state_vars = max(len(node.input) - 2, 0)
|
|
489
|
+
actual_ins = node.input[2:]
|
|
490
|
+
formal_ins = body.input[2:]
|
|
491
|
+
formal_outs = body.output[1 : num_state_vars + 1]
|
|
492
|
+
actual_outs = node.output[0:num_state_vars]
|
|
493
|
+
|
|
494
|
+
rows.extend(self._emit_assign(formal_ins, actual_ins, indent))
|
|
495
|
+
|
|
496
|
+
if use_iter_var and not use_loop_cond:
|
|
497
|
+
rows.append(f"{sindent}for {iter_var} in range({n_iter}):")
|
|
498
|
+
# The following is a hacky way to suppress the generation of
|
|
499
|
+
# "cond_out = cond_in", which ONNX forces for a FOR loop.
|
|
500
|
+
# TODO: a cleaner solution for this.
|
|
501
|
+
self._name_remappings[-1][cond_out] = self._translate_onnx_var(cond_in)
|
|
502
|
+
elif not use_iter_var and use_loop_cond:
|
|
503
|
+
rows.append(f"{sindent}while {py_cond}:")
|
|
504
|
+
elif use_iter_var and use_loop_cond:
|
|
505
|
+
# TODO: This needs fixing
|
|
506
|
+
rows.append(f"{sindent}for {iter_var} in range({n_iter}):")
|
|
507
|
+
rows.append(f"{sindent}{_SINGLE_INDENT}if not {py_cond}:")
|
|
508
|
+
rows.append(f"{sindent}{_SINGLE_INDENT * 2}break")
|
|
509
|
+
else:
|
|
510
|
+
raise RuntimeError(
|
|
511
|
+
f"Unable to export loop type {node.op_type!r} into python because "
|
|
512
|
+
"there is no stop condition."
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
rows.append(
|
|
516
|
+
self._translate_graph_body(
|
|
517
|
+
body,
|
|
518
|
+
opsets,
|
|
519
|
+
indent=indent + 1,
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
if use_loop_cond:
|
|
523
|
+
rows.extend(self._emit_assign(cond_in, cond_out, indent + 1))
|
|
524
|
+
rows.extend(self._emit_assign(formal_ins, formal_outs, indent + 1))
|
|
525
|
+
rows.extend(self._emit_assign(actual_outs, formal_ins, indent))
|
|
526
|
+
|
|
527
|
+
# TODO: This doesn't handle scan-outputs yet.
|
|
528
|
+
|
|
529
|
+
return "\n".join(rows)
|
|
530
|
+
|
|
531
|
+
def _translate_scan(self, node, opsets, indent=0):
|
|
532
|
+
"""Translates a node Scan into python."""
|
|
533
|
+
raise NotImplementedError()
|
|
534
|
+
|
|
535
|
+
def _translate_node(self, onnx_node, opsets, indent=0):
|
|
536
|
+
if isinstance(onnx_node, dict):
|
|
537
|
+
node = onnx_node["onnx_node"]
|
|
538
|
+
else:
|
|
539
|
+
node = onnx_node
|
|
540
|
+
if self.inline_const and node.op_type == "Constant":
|
|
541
|
+
val = _get_const_repr(node)
|
|
542
|
+
if val is not None:
|
|
543
|
+
self.constants[node.output[0]] = str(val)
|
|
544
|
+
return ""
|
|
545
|
+
if node.op_type in {"If", "Loop", "Scan"}:
|
|
546
|
+
# If, Loop, Scan
|
|
547
|
+
if node.op_type == "If":
|
|
548
|
+
return self._translate_if(node, opsets, indent=indent)
|
|
549
|
+
if node.op_type == "Loop":
|
|
550
|
+
return self._translate_loop(node, opsets, indent=indent)
|
|
551
|
+
if node.op_type == "Scan":
|
|
552
|
+
return self._translate_scan(node, opsets, indent=indent)
|
|
553
|
+
raise RuntimeError(f"Unable to export node type {node.op_type!r} into python.")
|
|
554
|
+
if any(hasattr(att, "g") and att.g and att.g.ByteSize() > 0 for att in node.attribute):
|
|
555
|
+
raise RuntimeError(f"Unable to export node type {node.op_type!r} into python.")
|
|
556
|
+
ops = {
|
|
557
|
+
"Add": "+",
|
|
558
|
+
"Sub": "-",
|
|
559
|
+
"Mul": "*",
|
|
560
|
+
"MatMul": "@",
|
|
561
|
+
"Div": "/",
|
|
562
|
+
"Pow": "**",
|
|
563
|
+
"And": "&",
|
|
564
|
+
"Or": "|",
|
|
565
|
+
"Greater": ">",
|
|
566
|
+
"Equal": "==",
|
|
567
|
+
"Lesser": "<",
|
|
568
|
+
"GreaterOrEqual": ">=",
|
|
569
|
+
"LessOrEqual": "<=",
|
|
570
|
+
}
|
|
571
|
+
sindent = _SINGLE_INDENT * indent
|
|
572
|
+
if self.use_operators and node.op_type in ops:
|
|
573
|
+
return (
|
|
574
|
+
f"{sindent}{self._translate_onnx_var(node.output[0])} = "
|
|
575
|
+
f"{(f' {ops[node.op_type]} ').join(map(self._translate_onnx_var_ref, node.input))}"
|
|
576
|
+
)
|
|
577
|
+
callee_name = self._make_callee_name(
|
|
578
|
+
node.domain, opsets[node.domain], node.op_type, node=True
|
|
579
|
+
)
|
|
580
|
+
attributes_str = self._translate_attributes(node)
|
|
581
|
+
if len(node.input) > 0 and len(attributes_str) > 0:
|
|
582
|
+
attributes_str = f", {attributes_str}"
|
|
583
|
+
output_names: list[Any] = []
|
|
584
|
+
for i, o in enumerate(node.output):
|
|
585
|
+
if o in ("", None):
|
|
586
|
+
output_names.append(f"_{i}")
|
|
587
|
+
else:
|
|
588
|
+
output_names.append(self._translate_onnx_var(o))
|
|
589
|
+
|
|
590
|
+
input_names = [self._translate_onnx_var_ref(x) for x in node.input]
|
|
591
|
+
|
|
592
|
+
# Suppress generation of redundant copy: used to suppress "cond_out = cond_in"
|
|
593
|
+
# from an ONNX FOR loop, which can cause problems in python.
|
|
594
|
+
if node.op_type == "Identity" and len(node.input) == 1 and len(node.output) == 1:
|
|
595
|
+
if output_names[0] == input_names[0]:
|
|
596
|
+
return ""
|
|
597
|
+
text = [
|
|
598
|
+
sindent,
|
|
599
|
+
", ".join(output_names),
|
|
600
|
+
" = ",
|
|
601
|
+
callee_name,
|
|
602
|
+
"(",
|
|
603
|
+
", ".join(input_names),
|
|
604
|
+
attributes_str,
|
|
605
|
+
")",
|
|
606
|
+
]
|
|
607
|
+
return "".join(text)
|
|
608
|
+
|
|
609
|
+
def _translate_opset_import(self, domain: str, version: int) -> str:
|
|
610
|
+
if domain in {"", "ai.onnx"}:
|
|
611
|
+
return f"from onnxscript.onnx_opset import opset{version}\n"
|
|
612
|
+
else:
|
|
613
|
+
varname = self._make_opset_name(domain, version)
|
|
614
|
+
return f"{varname} = Opset('{domain}', {version})\n"
|
|
615
|
+
|
|
616
|
+
def _translate_opset_imports(
|
|
617
|
+
self, opset_imports: Sequence[onnx.OperatorSetIdProto]
|
|
618
|
+
) -> str:
|
|
619
|
+
return "".join(
|
|
620
|
+
[self._translate_opset_import(x.domain, x.version) for x in opset_imports]
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
def _translate_opset_imports_of(
|
|
624
|
+
self, proto: ModelProto | FunctionProto | GraphProto
|
|
625
|
+
) -> str:
|
|
626
|
+
if hasattr(proto, "opset_import"):
|
|
627
|
+
text = self._translate_opset_imports(proto.opset_import)
|
|
628
|
+
if isinstance(proto, FunctionProto):
|
|
629
|
+
if not any(x.domain == proto.domain for x in proto.opset_import):
|
|
630
|
+
text += self._translate_opset_import(proto.domain, 1)
|
|
631
|
+
return text
|
|
632
|
+
return ""
|
|
633
|
+
|
|
634
|
+
def _translate_function_signature(self, funproto: onnx.FunctionProto) -> str:
|
|
635
|
+
"""Generate signature for FunctionProto."""
|
|
636
|
+
type_map = _attribute_param_types(funproto)
|
|
637
|
+
|
|
638
|
+
def attr_sig(attr_name: str) -> str:
|
|
639
|
+
self._attr_renaming[attr_name] = None
|
|
640
|
+
self._names_used.add(attr_name)
|
|
641
|
+
# A default type of INT is used for attribute parameters that are never used.
|
|
642
|
+
type = type_map.get(attr_name, onnx.AttributeProto.INT)
|
|
643
|
+
typerep = onnxscript.type_annotation.onnx_attr_type_to_onnxscript_repr(type)
|
|
644
|
+
return f"{attr_name}: {typerep}"
|
|
645
|
+
|
|
646
|
+
inputs = [self._translate_onnx_var(x) for x in funproto.input]
|
|
647
|
+
attrs = [attr_sig(x) for x in funproto.attribute]
|
|
648
|
+
input_and_attrs = ", ".join(inputs + attrs) # type: ignore[arg-type]
|
|
649
|
+
if len(funproto.attribute_proto) > 0:
|
|
650
|
+
message = "\n # Attribute parameters default-values not handled yet."
|
|
651
|
+
else:
|
|
652
|
+
message = ""
|
|
653
|
+
return f"({input_and_attrs}):{message}"
|
|
654
|
+
|
|
655
|
+
def _translate_function(self, funproto: onnx.FunctionProto) -> str:
|
|
656
|
+
"""Generate python code for FunctionProto."""
|
|
657
|
+
opsets = {}
|
|
658
|
+
for imported in funproto.opset_import:
|
|
659
|
+
opsets[imported.domain] = imported.version
|
|
660
|
+
self._attr_renaming = {}
|
|
661
|
+
used_proto_names = _names_used_in_function(funproto)
|
|
662
|
+
renamed_names_used = [self._translate_onnx_var(x) for x in used_proto_names]
|
|
663
|
+
self._names_used = set(renamed_names_used)
|
|
664
|
+
result = []
|
|
665
|
+
|
|
666
|
+
def add_line(line: str) -> None:
|
|
667
|
+
result.append(line)
|
|
668
|
+
|
|
669
|
+
opset_name = self._make_opset_name(funproto.domain, 1)
|
|
670
|
+
add_line(f"@script({opset_name})")
|
|
671
|
+
fun_name = self._make_callee_name(funproto.domain, 1, funproto.name)
|
|
672
|
+
fun_sig = self._translate_function_signature(funproto)
|
|
673
|
+
add_line(f"def {fun_name}{fun_sig}")
|
|
674
|
+
if funproto.doc_string:
|
|
675
|
+
add_line(f' """{funproto.doc_string}"""')
|
|
676
|
+
self._name_remappings.append({})
|
|
677
|
+
for node in funproto.node:
|
|
678
|
+
add_line(self._translate_node(node, opsets, indent=1))
|
|
679
|
+
return_values = ", ".join(self._translate_onnx_var(x) for x in funproto.output)
|
|
680
|
+
add_line(f" return {return_values}")
|
|
681
|
+
self._name_remappings.pop()
|
|
682
|
+
return "\n".join(result)
|
|
683
|
+
|
|
684
|
+
def _translate_graph(self, model: onnx.ModelProto, function_name: Optional[str]) -> str:
|
|
685
|
+
graph = model.graph
|
|
686
|
+
opsets = {}
|
|
687
|
+
for imported in model.opset_import:
|
|
688
|
+
opsets[imported.domain] = imported.version
|
|
689
|
+
if function_name is None:
|
|
690
|
+
function_name = _cleanup_variable_name(graph.name)
|
|
691
|
+
|
|
692
|
+
result: list[str] = []
|
|
693
|
+
|
|
694
|
+
def add(line: str) -> None:
|
|
695
|
+
result.append(line)
|
|
696
|
+
|
|
697
|
+
if self.skip_initializers:
|
|
698
|
+
indent_level = 2
|
|
699
|
+
indent = _SINGLE_INDENT
|
|
700
|
+
else:
|
|
701
|
+
indent_level = 1
|
|
702
|
+
indent = ""
|
|
703
|
+
add(f"{indent}@script()")
|
|
704
|
+
add(f"{indent}def {function_name}{_translate_signature(graph.input, graph.output)}")
|
|
705
|
+
indent = indent + _SINGLE_INDENT
|
|
706
|
+
doc = graph.doc_string
|
|
707
|
+
if doc:
|
|
708
|
+
add(f'{indent}"""{doc}"""')
|
|
709
|
+
add(self._translate_graph_body(graph, opsets, indent=indent_level))
|
|
710
|
+
return_values = ", ".join(self._translate_onnx_var(x) for x in graph.output)
|
|
711
|
+
add(f"{indent}return {return_values}")
|
|
712
|
+
script = "\n".join(result)
|
|
713
|
+
if self.skipped_initializers:
|
|
714
|
+
return self._substitute_initializers(script, function_name)
|
|
715
|
+
return script
|
|
716
|
+
|
|
717
|
+
def _substitute_initializers(self, script: str, script_function_name: str) -> str:
|
|
718
|
+
init_names = self.skipped_initializers.keys()
|
|
719
|
+
# Formal parameters representing initializers (single level indentation)
|
|
720
|
+
__ = _SINGLE_INDENT
|
|
721
|
+
initializers_as_params = "\n".join(f"{__}{x}," for x in init_names)
|
|
722
|
+
|
|
723
|
+
def generate_rand(name: str, value: TensorProto) -> str:
|
|
724
|
+
shape = ",".join(str(d) for d in value.dims)
|
|
725
|
+
if value.data_type != TensorProto.FLOAT:
|
|
726
|
+
raise NotImplementedError(
|
|
727
|
+
f"Unable to generate random initializer for data type {value.data_type}."
|
|
728
|
+
)
|
|
729
|
+
return f"{__}{name} = numpy.random.rand({shape}).astype(numpy.float32)"
|
|
730
|
+
|
|
731
|
+
random_initializer_values = "\n".join(
|
|
732
|
+
generate_rand(key, value) for key, value in self.skipped_initializers.items()
|
|
733
|
+
)
|
|
734
|
+
# Actual parameter values for initializers (double level indentation)
|
|
735
|
+
indented_initializers_as_params = "\n".join(f"{__}{__}{x}," for x in init_names)
|
|
736
|
+
return f"""
|
|
737
|
+
def make_model(
|
|
738
|
+
{initializers_as_params}
|
|
739
|
+
):
|
|
740
|
+
{script}
|
|
741
|
+
|
|
742
|
+
{__}model = {script_function_name}.to_model_proto()
|
|
743
|
+
{__}return model
|
|
744
|
+
|
|
745
|
+
def make_model_with_random_weights():
|
|
746
|
+
{random_initializer_values}
|
|
747
|
+
{__}model = make_model(
|
|
748
|
+
{indented_initializers_as_params}
|
|
749
|
+
{__})
|
|
750
|
+
{__}return model
|
|
751
|
+
"""
|
|
752
|
+
|
|
753
|
+
def _import_onnx_types(
|
|
754
|
+
self, proto: onnx.ModelProto | onnx.GraphProto | onnx.FunctionProto
|
|
755
|
+
) -> str:
|
|
756
|
+
"""Generate import statements for types used in the graph."""
|
|
757
|
+
if isinstance(proto, ModelProto):
|
|
758
|
+
graph_or_function = proto.graph
|
|
759
|
+
else:
|
|
760
|
+
graph_or_function = proto
|
|
761
|
+
used_types: set[str] = set()
|
|
762
|
+
for t in list(graph_or_function.input) + list(graph_or_function.output):
|
|
763
|
+
if hasattr(t, "type"):
|
|
764
|
+
ts = _translate_type(t.type)
|
|
765
|
+
its = ts.split("[", maxsplit=1)[0]
|
|
766
|
+
used_types.add(its)
|
|
767
|
+
# TODO: handle types in nested graphs.
|
|
768
|
+
sorted_types = sorted(used_types)
|
|
769
|
+
if sorted_types:
|
|
770
|
+
return "from onnxscript.onnx_types import " + ", ".join(sorted_types)
|
|
771
|
+
return ""
|
|
772
|
+
|
|
773
|
+
def export(
|
|
774
|
+
self, proto: onnx.ModelProto | onnx.FunctionProto, function_name: Optional[str]
|
|
775
|
+
) -> str:
|
|
776
|
+
result: list[str] = []
|
|
777
|
+
|
|
778
|
+
def add(line: str) -> None:
|
|
779
|
+
result.append(line)
|
|
780
|
+
|
|
781
|
+
# Generic imports.
|
|
782
|
+
add("import numpy")
|
|
783
|
+
add("from onnx import TensorProto")
|
|
784
|
+
add("from onnx.helper import make_tensor")
|
|
785
|
+
add("from onnxscript import script, external_tensor")
|
|
786
|
+
add("from onnxscript.values import Opset")
|
|
787
|
+
add(self._import_onnx_types(proto))
|
|
788
|
+
|
|
789
|
+
if isinstance(proto, ModelProto):
|
|
790
|
+
translated_functions = [self._translate_function(f) for f in proto.functions]
|
|
791
|
+
translated_functions.append(self._translate_graph(proto, function_name))
|
|
792
|
+
else:
|
|
793
|
+
assert isinstance(proto, FunctionProto)
|
|
794
|
+
translated_functions = [self._translate_function(proto)]
|
|
795
|
+
|
|
796
|
+
# TODO: unique_function_domain_version.add((f.domain, 1))
|
|
797
|
+
add(self._translate_opset_imports_of(proto))
|
|
798
|
+
result.extend(translated_functions)
|
|
799
|
+
|
|
800
|
+
add("")
|
|
801
|
+
final = "\n".join(result)
|
|
802
|
+
|
|
803
|
+
if "\nreturn" in final:
|
|
804
|
+
raise SyntaxError(f"The produced code is wrong.\n{final}")
|
|
805
|
+
|
|
806
|
+
return final
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def _attribute_param_types(
|
|
810
|
+
funproto: onnx.FunctionProto,
|
|
811
|
+
) -> dict[str, onnx.AttributeProto.AttributeType]:
|
|
812
|
+
"""Compute mapping from (names of) attribute parameters of function to their types."""
|
|
813
|
+
type_map = {}
|
|
814
|
+
|
|
815
|
+
def visit_node(node: onnx.NodeProto) -> None:
|
|
816
|
+
for attr in node.attribute:
|
|
817
|
+
if _is_attribute_ref(attr):
|
|
818
|
+
type_map[attr.ref_attr_name] = attr.type
|
|
819
|
+
elif attr.type == onnx.AttributeProto.GRAPH:
|
|
820
|
+
visit_graph(attr.g)
|
|
821
|
+
elif attr.type == onnx.AttributeProto.GRAPHS:
|
|
822
|
+
for graph in attr.graphs:
|
|
823
|
+
visit_graph(graph)
|
|
824
|
+
|
|
825
|
+
def visit_graph(graph: onnx.GraphProto) -> None:
|
|
826
|
+
for node in graph.node:
|
|
827
|
+
visit_node(node)
|
|
828
|
+
|
|
829
|
+
for node in funproto.node:
|
|
830
|
+
visit_node(node)
|
|
831
|
+
return type_map
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def export2python(
|
|
835
|
+
model_onnx,
|
|
836
|
+
function_name: Optional[str] = None,
|
|
837
|
+
*,
|
|
838
|
+
rename: bool = False,
|
|
839
|
+
use_operators: bool = False,
|
|
840
|
+
inline_const: bool = False,
|
|
841
|
+
skip_initializers: bool = False,
|
|
842
|
+
):
|
|
843
|
+
"""Exports an ONNX model to the *python* syntax.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
model_onnx: string or ONNX graph
|
|
847
|
+
rename: rename the names to get shorter names
|
|
848
|
+
function_name: main function name
|
|
849
|
+
use_operators: use Python operators.
|
|
850
|
+
inline_const: replace ONNX constants inline if compact
|
|
851
|
+
skip_initializers: generated script will not include initializers.
|
|
852
|
+
Instead, a function that generates the model, given initializer values, is generated,
|
|
853
|
+
along with one that generates random values for the initializers.
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
python code
|
|
857
|
+
The following example shows what a python code creating a graph
|
|
858
|
+
implementing the KMeans would look like.
|
|
859
|
+
.. runpython::
|
|
860
|
+
:showcode:
|
|
861
|
+
:process:
|
|
862
|
+
import numpy
|
|
863
|
+
from sklearn.cluster import KMeans
|
|
864
|
+
from mlprodict.onnx_conv import to_onnx
|
|
865
|
+
from mlprodict.onnx_tools.onnx_export import export2python
|
|
866
|
+
X = numpy.arange(20).reshape(10, 2).astype(numpy.float32)
|
|
867
|
+
tr = KMeans(n_clusters=2)
|
|
868
|
+
tr.fit(X)
|
|
869
|
+
onx = to_onnx(tr, X, target_opset=14)
|
|
870
|
+
code = export2python(onx)
|
|
871
|
+
print(code)
|
|
872
|
+
"""
|
|
873
|
+
if isinstance(model_onnx, str):
|
|
874
|
+
model_onnx = onnx.load(model_onnx)
|
|
875
|
+
|
|
876
|
+
if not isinstance(model_onnx, (ModelProto, FunctionProto)):
|
|
877
|
+
raise TypeError(f"The function expects a ModelProto not {type(model_onnx)!r}.")
|
|
878
|
+
|
|
879
|
+
exporter = _Exporter(
|
|
880
|
+
rename=rename,
|
|
881
|
+
use_operators=use_operators,
|
|
882
|
+
inline_const=inline_const,
|
|
883
|
+
skip_initializers=skip_initializers,
|
|
884
|
+
)
|
|
885
|
+
return exporter.export(model_onnx, function_name)
|