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.
Files changed (176) hide show
  1. onnxscript/__init__.py +131 -0
  2. onnxscript/_framework_apis/__init__.py +3 -0
  3. onnxscript/_framework_apis/torch_2_5.py +117 -0
  4. onnxscript/_framework_apis/torch_2_6.py +45 -0
  5. onnxscript/_internal/__init__.py +0 -0
  6. onnxscript/_internal/analysis.py +229 -0
  7. onnxscript/_internal/ast_utils.py +64 -0
  8. onnxscript/_internal/autocast.py +250 -0
  9. onnxscript/_internal/deprecation.py +78 -0
  10. onnxscript/_internal/param_manipulation.py +148 -0
  11. onnxscript/_internal/runtime_typing.py +43 -0
  12. onnxscript/_internal/utils.py +99 -0
  13. onnxscript/_internal/version_utils.py +118 -0
  14. onnxscript/_legacy_ir/__init__.py +341 -0
  15. onnxscript/_legacy_ir/visitor.py +937 -0
  16. onnxscript/_thirdparty/asciichartpy.py +313 -0
  17. onnxscript/backend/__init__.py +2 -0
  18. onnxscript/backend/onnx_backend.py +303 -0
  19. onnxscript/backend/onnx_export.py +885 -0
  20. onnxscript/converter.py +1470 -0
  21. onnxscript/evaluator.py +619 -0
  22. onnxscript/function_libs/tools/torch_lib/deduce_type_constraints.py +403 -0
  23. onnxscript/function_libs/tools/torch_lib/generate_aten_signatures.py +333 -0
  24. onnxscript/function_libs/tools/torch_lib/generate_prims_signatures.py +331 -0
  25. onnxscript/function_libs/torch_lib/__init__.py +12 -0
  26. onnxscript/function_libs/torch_lib/_constants.py +5 -0
  27. onnxscript/function_libs/torch_lib/_flags.py +58 -0
  28. onnxscript/function_libs/torch_lib/graph_building/__init__.py +56 -0
  29. onnxscript/function_libs/torch_lib/graph_building/_graph_building_ir.py +723 -0
  30. onnxscript/function_libs/torch_lib/graph_building/_graph_building_torch.py +1125 -0
  31. onnxscript/function_libs/torch_lib/ops/__init__.py +27 -0
  32. onnxscript/function_libs/torch_lib/ops/common.py +80 -0
  33. onnxscript/function_libs/torch_lib/ops/core.py +8935 -0
  34. onnxscript/function_libs/torch_lib/ops/fft.py +385 -0
  35. onnxscript/function_libs/torch_lib/ops/linalg.py +399 -0
  36. onnxscript/function_libs/torch_lib/ops/nested.py +25 -0
  37. onnxscript/function_libs/torch_lib/ops/nn.py +2713 -0
  38. onnxscript/function_libs/torch_lib/ops/prims.py +850 -0
  39. onnxscript/function_libs/torch_lib/ops/quantized_decomposed.py +63 -0
  40. onnxscript/function_libs/torch_lib/ops/sparse.py +23 -0
  41. onnxscript/function_libs/torch_lib/ops/special.py +387 -0
  42. onnxscript/function_libs/torch_lib/ops/vision.py +25 -0
  43. onnxscript/function_libs/torch_lib/registration.py +151 -0
  44. onnxscript/function_libs/torch_lib/tensor_typing.py +74 -0
  45. onnxscript/ir/__init__.py +153 -0
  46. onnxscript/ir/_convenience.py +447 -0
  47. onnxscript/ir/_core.py +3119 -0
  48. onnxscript/ir/_display.py +49 -0
  49. onnxscript/ir/_enums.py +163 -0
  50. onnxscript/ir/_graph_comparison.py +23 -0
  51. onnxscript/ir/_io.py +97 -0
  52. onnxscript/ir/_linked_list.py +276 -0
  53. onnxscript/ir/_metadata.py +44 -0
  54. onnxscript/ir/_name_authority.py +72 -0
  55. onnxscript/ir/_polyfill.py +25 -0
  56. onnxscript/ir/_protocols.py +598 -0
  57. onnxscript/ir/_schemas.py +548 -0
  58. onnxscript/ir/_tape.py +120 -0
  59. onnxscript/ir/_type_casting.py +106 -0
  60. onnxscript/ir/convenience.py +32 -0
  61. onnxscript/ir/external_data.py +396 -0
  62. onnxscript/ir/passes/__init__.py +33 -0
  63. onnxscript/ir/passes/_pass_infra.py +172 -0
  64. onnxscript/ir/serde.py +1620 -0
  65. onnxscript/ir/tensor_adapters.py +122 -0
  66. onnxscript/ir/traversal.py +82 -0
  67. onnxscript/irbuilder.py +542 -0
  68. onnxscript/main.py +167 -0
  69. onnxscript/onnx_opset/__init__.py +232 -0
  70. onnxscript/onnx_opset/_impl/opset1.py +4100 -0
  71. onnxscript/onnx_opset/_impl/opset10.py +1227 -0
  72. onnxscript/onnx_opset/_impl/opset11.py +4013 -0
  73. onnxscript/onnx_opset/_impl/opset12.py +1078 -0
  74. onnxscript/onnx_opset/_impl/opset13.py +3924 -0
  75. onnxscript/onnx_opset/_impl/opset14.py +999 -0
  76. onnxscript/onnx_opset/_impl/opset15.py +604 -0
  77. onnxscript/onnx_opset/_impl/opset16.py +1255 -0
  78. onnxscript/onnx_opset/_impl/opset17.py +561 -0
  79. onnxscript/onnx_opset/_impl/opset18.py +1803 -0
  80. onnxscript/onnx_opset/_impl/opset19.py +1942 -0
  81. onnxscript/onnx_opset/_impl/opset2.py +218 -0
  82. onnxscript/onnx_opset/_impl/opset20.py +675 -0
  83. onnxscript/onnx_opset/_impl/opset21.py +1976 -0
  84. onnxscript/onnx_opset/_impl/opset22.py +2588 -0
  85. onnxscript/onnx_opset/_impl/opset3.py +199 -0
  86. onnxscript/onnx_opset/_impl/opset4.py +77 -0
  87. onnxscript/onnx_opset/_impl/opset5.py +84 -0
  88. onnxscript/onnx_opset/_impl/opset6.py +944 -0
  89. onnxscript/onnx_opset/_impl/opset7.py +1243 -0
  90. onnxscript/onnx_opset/_impl/opset8.py +444 -0
  91. onnxscript/onnx_opset/_impl/opset9.py +1485 -0
  92. onnxscript/onnx_opset/_impl/opset_ai_onnx_ml1.py +974 -0
  93. onnxscript/onnx_opset/_impl/opset_ai_onnx_ml2.py +112 -0
  94. onnxscript/onnx_opset/_impl/opset_ai_onnx_ml3.py +308 -0
  95. onnxscript/onnx_opset/_impl/opset_ai_onnx_ml4.py +129 -0
  96. onnxscript/onnx_opset/_impl/opset_ai_onnx_ml5.py +158 -0
  97. onnxscript/onnx_opset/_impl/opset_ai_onnx_preview_training1.py +577 -0
  98. onnxscript/onnx_types.py +229 -0
  99. onnxscript/optimizer/__init__.py +39 -0
  100. onnxscript/optimizer/_constant_folding.py +1083 -0
  101. onnxscript/optimizer/_inliner.py +312 -0
  102. onnxscript/optimizer/_legacy/_optimizer.py +98 -0
  103. onnxscript/optimizer/_legacy/_remove_unused_proto.py +144 -0
  104. onnxscript/optimizer/_legacy/_simple_function_folding.py +243 -0
  105. onnxscript/optimizer/_legacy/constant_folding.py +293 -0
  106. onnxscript/optimizer/_legacy/evaluator.py +439 -0
  107. onnxscript/optimizer/_optimizer.py +61 -0
  108. onnxscript/optimizer/_remove_unused.py +106 -0
  109. onnxscript/optimizer/_remove_unused_function.py +72 -0
  110. onnxscript/py.typed +1 -0
  111. onnxscript/rewriter/__init__.py +56 -0
  112. onnxscript/rewriter/_ir_utils.py +111 -0
  113. onnxscript/rewriter/broadcast_to_matmul.py +180 -0
  114. onnxscript/rewriter/cast_constant_of_shape.py +50 -0
  115. onnxscript/rewriter/collapse_slices.py +141 -0
  116. onnxscript/rewriter/erfgelu.py +27 -0
  117. onnxscript/rewriter/function_rule.py +232 -0
  118. onnxscript/rewriter/gemm_to_matmul_add.py +21 -0
  119. onnxscript/rewriter/generic_pattern.py +700 -0
  120. onnxscript/rewriter/llama_rule_sets.py +287 -0
  121. onnxscript/rewriter/no_op.py +56 -0
  122. onnxscript/rewriter/onnxruntime/__init__.py +50 -0
  123. onnxscript/rewriter/onnxruntime/bfloat16_utils/bfloat16_converter.py +99 -0
  124. onnxscript/rewriter/onnxruntime/fused_matmul_rule_sets.py +177 -0
  125. onnxscript/rewriter/onnxruntime/group_normalization_merge_silu.py +64 -0
  126. onnxscript/rewriter/onnxruntime/instance_to_group_normalization.py +155 -0
  127. onnxscript/rewriter/onnxruntime/softmax.py +63 -0
  128. onnxscript/rewriter/onnxruntime/transformers/__init__.py +21 -0
  129. onnxscript/rewriter/onnxruntime/transformers/biassplitgelu.py +31 -0
  130. onnxscript/rewriter/onnxruntime/transformers/fastgelu.py +29 -0
  131. onnxscript/rewriter/onnxruntime/transformers/layernorm.py +47 -0
  132. onnxscript/rewriter/onnxruntime/transformers/multihead_attention.py +715 -0
  133. onnxscript/rewriter/ort_fusions/__init__.py +9 -0
  134. onnxscript/rewriter/ort_fusions/_core.py +28 -0
  135. onnxscript/rewriter/ort_fusions/_smollm_1.py +253 -0
  136. onnxscript/rewriter/ort_fusions/_smollm_2.py +467 -0
  137. onnxscript/rewriter/ort_fusions/_test_models.py +122 -0
  138. onnxscript/rewriter/ort_fusions/_test_utils.py +42 -0
  139. onnxscript/rewriter/ort_fusions/cos_sin_cache.py +154 -0
  140. onnxscript/rewriter/ort_fusions/gqa.py +156 -0
  141. onnxscript/rewriter/ort_fusions/mha.py +198 -0
  142. onnxscript/rewriter/ort_fusions/rms_normalization.py +95 -0
  143. onnxscript/rewriter/ort_fusions/rotary_embedding.py +64 -0
  144. onnxscript/rewriter/ort_fusions/sdpa.py +75 -0
  145. onnxscript/rewriter/ort_fusions/skip_normalization.py +46 -0
  146. onnxscript/rewriter/pattern.py +1714 -0
  147. onnxscript/rewriter/testing.py +77 -0
  148. onnxscript/sourceinfo.py +59 -0
  149. onnxscript/tensor.py +227 -0
  150. onnxscript/testing/__init__.py +482 -0
  151. onnxscript/tools/__init__.py +4 -0
  152. onnxscript/tools/benchmark/__init__.py +23 -0
  153. onnxscript/tools/benchmark/benchmark_helpers.py +783 -0
  154. onnxscript/tools/benchmark/benchmark_run.py +140 -0
  155. onnxscript/tools/benchmark/export_model.py +207 -0
  156. onnxscript/tools/benchmark/export_model_batch.py +146 -0
  157. onnxscript/tools/memory_peak.py +244 -0
  158. onnxscript/tools/training_helper.py +50 -0
  159. onnxscript/tools/transformers_models/__init__.py +190 -0
  160. onnxscript/tools/transformers_models/llama.py +168 -0
  161. onnxscript/tools/transformers_models/mistral.py +238 -0
  162. onnxscript/tools/transformers_models/phi.py +248 -0
  163. onnxscript/tools/transformers_models/phi3.py +259 -0
  164. onnxscript/type_annotation.py +281 -0
  165. onnxscript/utils/__init__.py +0 -0
  166. onnxscript/utils/evaluation_utils.py +56 -0
  167. onnxscript/utils/timing_utils.py +33 -0
  168. onnxscript/utils/utils.py +84 -0
  169. onnxscript/values.py +790 -0
  170. onnxscript/version_converter/__init__.py +21 -0
  171. onnxscript/version_converter/_version_converter.py +314 -0
  172. onnxscript-0.1.0.dist-info/LICENSE +21 -0
  173. onnxscript-0.1.0.dist-info/METADATA +370 -0
  174. onnxscript-0.1.0.dist-info/RECORD +176 -0
  175. onnxscript-0.1.0.dist-info/WHEEL +5 -0
  176. 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)