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,1714 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ from __future__ import annotations
4
+
5
+ import abc
6
+ import contextlib
7
+ import dataclasses
8
+ import enum
9
+ import inspect
10
+ import itertools
11
+ import math
12
+ from collections import defaultdict
13
+ from typing import (
14
+ Any,
15
+ Callable,
16
+ Iterable,
17
+ Iterator,
18
+ MutableSequence,
19
+ Protocol,
20
+ Sequence,
21
+ Tuple,
22
+ TypeVar,
23
+ Union,
24
+ )
25
+
26
+ import onnxscript.optimizer
27
+ from onnxscript import ir
28
+ from onnxscript.ir import _convenience, _tape
29
+
30
+ T = TypeVar("T")
31
+
32
+
33
+ class Pattern(Protocol[T]): # type: ignore[misc]
34
+ """This is essentially a Predicate[T], that is, a Callable[[T], bool] bound to the name "matches"."""
35
+
36
+ def matches(self, item: T) -> bool: ...
37
+
38
+
39
+ class StringPattern(abc.ABC, Pattern[str]):
40
+ """Abstract base class for string patterns."""
41
+
42
+ @abc.abstractmethod
43
+ def matches(self, item: str) -> bool:
44
+ pass
45
+
46
+ @abc.abstractmethod
47
+ def __str__(self) -> str:
48
+ pass
49
+
50
+
51
+ class StringConstantPattern(StringPattern):
52
+ """Matches strings with given value."""
53
+
54
+ def __init__(self, value: str):
55
+ self._value = value
56
+
57
+ def matches(self, item: str) -> bool:
58
+ return item == self._value
59
+
60
+ def __str__(self) -> str:
61
+ return self._value
62
+
63
+ def value(self) -> str:
64
+ return self._value
65
+
66
+
67
+ class PrefixPattern(StringPattern):
68
+ """Matches strings with a given prefix."""
69
+
70
+ def __init__(self, value: str) -> None:
71
+ self._value = value
72
+
73
+ def matches(self, value: str) -> bool:
74
+ return value.startswith(self._value)
75
+
76
+ def __str__(self) -> str:
77
+ return f"{self._value}*"
78
+
79
+
80
+ class AttrPattern(Pattern[Union[ir.Attr, ir.RefAttr]]):
81
+ """Base class for an attribute pattern. Matches any attribute value by default."""
82
+
83
+ def __init__(self, name: str | None):
84
+ self._name = name
85
+
86
+ @property
87
+ def name(self) -> str | None:
88
+ return self._name
89
+
90
+ def matches(self, attr: ir.Attr | ir.RefAttr) -> bool:
91
+ return True
92
+
93
+ def __str__(self) -> str:
94
+ return self._name if self._name is not None else "anonymous:" + str(id(self))
95
+
96
+
97
+ # TODO: Support tensors. Align with usage elsewhere.
98
+ SupportedAttrTypes = Union[
99
+ int,
100
+ float,
101
+ str,
102
+ Sequence[int],
103
+ Sequence[float],
104
+ Sequence[str],
105
+ ]
106
+
107
+
108
+ class AttrConstantPattern(AttrPattern):
109
+ """Matches attributes with given value.
110
+
111
+ Uses standard equality for matching. For list-valued attributes, the order of elements matters.
112
+ If order is immaterial, we need to define a separate pattern for that.
113
+ """
114
+
115
+ def __init__(self, value: SupportedAttrTypes):
116
+ super().__init__(None)
117
+ self._value = value
118
+
119
+ def matches(self, attr: ir.Attr | ir.RefAttr) -> bool:
120
+ return isinstance(attr, ir.Attr) and attr.value == self._value
121
+
122
+ def __str__(self) -> str:
123
+ return str(self._value)
124
+
125
+
126
+ def _to_attr_pattern(value: AttrPattern | ValuePattern | SupportedAttrTypes) -> AttrPattern:
127
+ """Represents promotion of values allowed as keyword-arguments in a pattern-builder call to an AttrPattern."""
128
+ if isinstance(value, AttrPattern):
129
+ return value
130
+ if type(value) is ValuePattern:
131
+ # This is a hack. Currently, when we create pattern-variables, we create them as ValuePattern,
132
+ # and change them to AttrPattern if/when used in an attribute context. We could use type
133
+ # annotations to distinguish between ValuePattern and AttrPattern, but forces users to
134
+ # use these type annotations.
135
+ # TODO: check for misuse at rule-creation time. (Currently will be caught by matcher at match-time.)
136
+ return AttrPattern(value.name)
137
+ if isinstance(value, (int, float, str)):
138
+ return AttrConstantPattern(value)
139
+ if isinstance(value, Sequence):
140
+ if all(isinstance(i, (int, float)) for i in value):
141
+ return AttrConstantPattern(value)
142
+ if all(isinstance(i, str) for i in value):
143
+ return AttrConstantPattern(value)
144
+ raise ValueError("Only lists of int/float/str can be used as an AttrPattern")
145
+ raise TypeError(f"Cannot convert {type(value)} to AttrPattern")
146
+
147
+
148
+ class OpsetPatternBuilder:
149
+ """Represents an opset pattern and a pattern builder.
150
+
151
+ (i) It is used to create a NodePattern (via OpPatternBuilder).
152
+ Example usage:
153
+ ::
154
+
155
+ z = op.Matmul(x, y)
156
+
157
+ Here, `op` is an instance of OpsetPatternBuilder and `op.Matmul` is an instance
158
+ of OpPatternBuilder, and `op.Matmul(x, y)` is an instance of NodePattern.
159
+
160
+ (ii) It contains a domain pattern matched against the actual opset domain used in the
161
+ input model.
162
+ """
163
+
164
+ def __init__(self, domain: StringPattern | str, record: bool = False) -> None:
165
+ if isinstance(domain, str):
166
+ domain = StringConstantPattern(domain)
167
+ self._domain_pattern = domain
168
+ if record:
169
+ self._nodes: list[NodePattern] | None = []
170
+ else:
171
+ self._nodes = None
172
+
173
+ def domain_pattern(self) -> StringPattern:
174
+ return self._domain_pattern
175
+
176
+ def __getattr__(self, op_name: str) -> OpPatternBuilder:
177
+ return OpPatternBuilder(self, op_name)
178
+
179
+ def submodule(self, name: str) -> OpPatternBuilder:
180
+ """This method is used to match against submodule ops with prefix."""
181
+ return OpPatternBuilder(self, PrefixPattern(name))
182
+
183
+ def __str__(self) -> str:
184
+ return str(self._domain_pattern)
185
+
186
+ def add_node(self, node: NodePattern) -> None:
187
+ if self._nodes is not None:
188
+ self._nodes.append(node)
189
+
190
+ def nodes(self) -> Sequence[NodePattern]:
191
+ if self._nodes is None:
192
+ raise ValueError("Nodes were not recorded.")
193
+ return self._nodes
194
+
195
+
196
+ onnxop = OpsetPatternBuilder("")
197
+
198
+ torch_module_op = OpsetPatternBuilder(PrefixPattern("pkg.torch"))
199
+
200
+
201
+ class OpPatternBuilder:
202
+ """A utility class to build a NodePattern.
203
+
204
+ It is used primarily to create a NodePattern.
205
+ Example usage:
206
+ ::
207
+
208
+ z = op.Matmul(x, y)
209
+
210
+ Here, `op` is an instance of OpsetPatternBuilder and `op.Matmul` is an instance
211
+ of OpPatternBuilder, and `op.Matmul(x, y)` is an instance of NodePattern.
212
+
213
+ """
214
+
215
+ def __init__(
216
+ self,
217
+ pattern_builder: OpsetPatternBuilder,
218
+ op_name: str | Pattern[str],
219
+ ) -> None:
220
+ self.pattern_builder = pattern_builder
221
+ self.op_name = op_name
222
+
223
+ def __call__(
224
+ self,
225
+ *args,
226
+ _domain: str | None = None,
227
+ _version: int | None = None,
228
+ _outputs: int | list[str | None] = 1,
229
+ _allow_other_attributes: bool | None = None,
230
+ _allow_other_inputs: bool | None = None,
231
+ **kwargs,
232
+ ):
233
+ if _version is not None:
234
+ raise ValueError(
235
+ "The pattern builder does not support '_version' keyword argument. "
236
+ "Version restrictions should be handled by rewrite rules."
237
+ )
238
+ if _domain is None:
239
+ opset_pattern = self.pattern_builder.domain_pattern()
240
+ elif isinstance(_domain, str):
241
+ opset_pattern = StringConstantPattern(_domain)
242
+ else:
243
+ # TODO(rama): allow OpsetPatternBuilder as _domain.
244
+ raise TypeError("_domain must be a string.")
245
+
246
+ if isinstance(_outputs, int):
247
+ _outputs = [None for _ in range(_outputs)]
248
+ elif not isinstance(_outputs, Sequence) or not all(
249
+ isinstance(x, (str, type(None))) for x in _outputs
250
+ ):
251
+ raise ValueError("_outputs must be an int or a list[str|None].")
252
+ inputs = [_to_value_pattern(x) for x in args]
253
+ attributes = {name: _to_attr_pattern(value) for (name, value) in kwargs.items()}
254
+ node_pattern = NodePattern(
255
+ opset_pattern,
256
+ self.op_name,
257
+ inputs,
258
+ attributes,
259
+ _outputs,
260
+ allow_other_attributes=_allow_other_attributes,
261
+ allow_other_inputs=_allow_other_inputs,
262
+ )
263
+ self.pattern_builder.add_node(node_pattern)
264
+ output_values = node_pattern.outputs
265
+ # Unpack outputs if there is only one output, the common case.
266
+ if len(output_values) == 1:
267
+ return output_values[0]
268
+ else:
269
+ return output_values
270
+
271
+
272
+ def _to_value_pattern(
273
+ x: ValuePattern | int | float | None,
274
+ ) -> ValuePattern | None:
275
+ """Promotes an input-value used to construct a NodePattern to a ValuePattern.
276
+
277
+ Example usage:
278
+ ::
279
+ x = op.MatMul(a, b)
280
+ z = op.Add(x, 0)
281
+
282
+ In this example, `a, `b`, and `x` are ValuePatterns used to construct a NodePattern.
283
+ `0` is a constant (int) value, and is automatically promoted to a ValuePattern.
284
+
285
+ Note that this is a shorthand for creating a Constant pattern. The user can more
286
+ explicitly write this as:
287
+ ::
288
+ z = op.Add(x, op.Constant(0))
289
+ """
290
+ if x is None or isinstance(x, ValuePattern):
291
+ return x
292
+ if isinstance(x, (int, float)):
293
+ return Constant(x)
294
+ if isinstance(x, Sequence):
295
+ if all(isinstance(i, (int, float)) for i in x):
296
+ return Constant(x)
297
+ raise ValueError("Only lists of int/float can be used as a ValuePattern")
298
+
299
+ raise TypeError(f"Cannot convert {type(x)} to ValuePattern")
300
+
301
+
302
+ class MatchResult:
303
+ """Represents the result of a match operation.
304
+
305
+ A match can either succeed or fail.
306
+ If it succeeds, it returns a list of nodes that matched the pattern
307
+ and a set of bindings for the variables in the pattern.
308
+
309
+ Example:
310
+ ::
311
+ def pattern(x, shape1, shape2):
312
+ t1 = op.Reshape(x, shape1)
313
+ t2 = op.Reshape(t1, shape2)
314
+ return t2
315
+ The above pattern matches a sequence of two Reshape ops.
316
+ The matched_nodes will contain the two Reshape ops, and the bindings will
317
+ contain the values that are bound to the variables `x`, `shape1`, and `shape2`.
318
+ """
319
+
320
+ def __init__(self) -> None:
321
+ self._success: bool = True
322
+ # For a successful match, _matched_nodes is a list of values that matched the pattern.
323
+ # These include the internal nodes of the pattern that were matched, but not
324
+ # the leaves (sub-trees) that match against the variables in the pattern.
325
+ # These represent the values that will be replaced by the replacement pattern.
326
+ self._matched_nodes: MutableSequence[ir.Node] = []
327
+ # For a successful match, bindings is a dictionary of mapping pattern-variable-names
328
+ # to values.
329
+ self.bindings: dict[str, Any] = {}
330
+ self.outputs: list[ir.Value] = []
331
+ # For a failed match, _reason is a string that describes the reason for the failure.
332
+ self._reason: str = ""
333
+ # Track the node that caused the failure.
334
+ # TODO: May be useful to extend this to be a collection of Nodes and Values.
335
+ self._failure_node: ir.Node | None = None
336
+
337
+ def __bool__(self):
338
+ return self._success
339
+
340
+ def fail(self, reason: str = "", node: ir.Node | None = None) -> MatchResult:
341
+ self._success = False
342
+ self._reason = reason
343
+ self._failure_node = node
344
+ return self
345
+
346
+ @property
347
+ def reason(self) -> str:
348
+ return self._reason
349
+
350
+ @property
351
+ def nodes(self) -> MutableSequence[ir.Node]:
352
+ return self._matched_nodes
353
+
354
+ def bind(self, var: str, value: Any) -> bool:
355
+ """Binds a pattern variable name to a value from the matched IR.
356
+
357
+ Returns True if the binding is successful, False otherwise (when the binding is inconsistent).
358
+ """
359
+ if var in self.bindings:
360
+ # TODO(rama): Use appropriate equality-check here.
361
+ if self.bindings[var] == value:
362
+ return True
363
+ self._success = False
364
+ return False
365
+ self.bindings[var] = value
366
+ return True
367
+
368
+ def extend(self, other: MatchResult | bool):
369
+ if not self._success:
370
+ return
371
+ if not other:
372
+ self._success = False
373
+ return
374
+ if isinstance(other, bool):
375
+ return
376
+ for var, val in other.bindings.items():
377
+ if var in self.bindings:
378
+ # TODO: handle attribute var bindings
379
+ if self.bindings[var] != val:
380
+ self._success = False
381
+ return
382
+ else:
383
+ self.bindings[var] = val
384
+ assert self._matched_nodes is not None, "_matched_nodes should not be None."
385
+ self._matched_nodes.extend(other._matched_nodes) # type: ignore[attr-defined]
386
+
387
+
388
+ _pattern_builder: OpsetPatternBuilder = onnxop
389
+
390
+
391
+ @contextlib.contextmanager
392
+ def pattern_builder(builder: OpsetPatternBuilder):
393
+ global _pattern_builder
394
+ prev_builder = _pattern_builder
395
+ _pattern_builder = builder
396
+ yield
397
+ _pattern_builder = prev_builder
398
+
399
+
400
+ class ValuePattern:
401
+ """Base class for all patterns that match against IR values.
402
+
403
+ This is used primarily to provide operator overloadings for arithmetic
404
+ operations, so that we can write patterns like `x + 1` and `1 + x`.
405
+ """
406
+
407
+ def __init__(self, name: str | None) -> None:
408
+ self._name = name
409
+ # Note: uses will be computed only when the full graph-pattern is constructed.
410
+ self._uses: list[tuple[NodePattern, int]] = []
411
+
412
+ def clone(self, node_map: dict[NodePattern, NodePattern]) -> ValuePattern:
413
+ del node_map
414
+ return ValuePattern(self._name)
415
+
416
+ @property
417
+ def name(self) -> str | None:
418
+ return self._name
419
+
420
+ def producer(self) -> NodePattern | None:
421
+ return None
422
+
423
+ def uses(self) -> Sequence[tuple[NodePattern, int]]:
424
+ return self._uses
425
+
426
+ def append_use(self, node: NodePattern, index: int):
427
+ self._uses.append((node, index))
428
+
429
+ def __repr__(self) -> str:
430
+ return f"ValuePattern({self._name!r})"
431
+
432
+ def __add__(self, other):
433
+ return _pattern_builder.Add(self, other)
434
+
435
+ def __radd__(self, other):
436
+ return _pattern_builder.Add(other, self)
437
+
438
+ def __sub__(self, other):
439
+ return _pattern_builder.Sub(self, other)
440
+
441
+ def __rsub__(self, other):
442
+ return _pattern_builder.Sub(other, self)
443
+
444
+ def __mul__(self, other):
445
+ return _pattern_builder.Mul(self, other)
446
+
447
+ def __rmul__(self, other):
448
+ return _pattern_builder.Mul(other, self)
449
+
450
+ def __truediv__(self, other):
451
+ return _pattern_builder.Div(self, other)
452
+
453
+ def __rtruediv__(self, other):
454
+ return _pattern_builder.Div(other, self)
455
+
456
+ def __pow__(self, other):
457
+ return _pattern_builder.Pow(self, other)
458
+
459
+ def __str__(self) -> str:
460
+ return self._name if self._name is not None else "anonymous:" + str(id(self))
461
+
462
+
463
+ class NodePattern:
464
+ """Represents a pattern that matches against a Node.
465
+
466
+ This differs from a NodeOutputPattern in that it matches against a node (which
467
+ may produce 1 or more outputs), whereas a NodeOutputPattern matches against
468
+ a specific output of a node.
469
+
470
+ Args:
471
+ domain: pattern to match against the domain of the node.
472
+ op: pattern or string constant to match against the op_type of the node.
473
+ inputs: sequence of ValuePatterns (or constants) to match against the inputs of the node.
474
+ attributes: dictionary of attribute patterns to match against the attributes of the node.
475
+ outputs: specifies pattern-variable-name for outputs (or None)
476
+ allow_other_attributes: specifies whether other attributes (not mentioned in `attributes`)
477
+ are allowed in the node.
478
+ """
479
+
480
+ def __init__(
481
+ self,
482
+ domain: StringPattern,
483
+ op: str | Pattern[str],
484
+ inputs: Sequence[int | float | ValuePattern | None],
485
+ attributes: dict[str, AttrPattern],
486
+ outputs: Sequence[str | None],
487
+ *,
488
+ allow_other_attributes: bool | None,
489
+ allow_other_inputs: bool | None,
490
+ ):
491
+ if allow_other_attributes is None:
492
+ # Default behavior: allow other unmatched attributes in the node.
493
+ allow_other_attributes = True
494
+ if allow_other_inputs is None:
495
+ # TODO(rama): Should we default to True? For now, we preserve the current behavior.
496
+ allow_other_inputs = False
497
+ self.domain = domain
498
+ self.op = StringConstantPattern(op) if isinstance(op, str) else op
499
+ self.inputs = [_to_value_pattern(x) for x in inputs]
500
+ self.attributes = attributes
501
+ self.allow_other_attributes = allow_other_attributes
502
+ self.allow_other_inputs = allow_other_inputs
503
+ # In the common case, domain and op are constants, which can be used to optimize matching.
504
+ if isinstance(op, str) and isinstance(domain, StringConstantPattern):
505
+ # TODO(rama): support overloaded operators.
506
+ overload = ""
507
+ self._op_identifier: tuple[str, str, str] | None = (
508
+ domain.value(),
509
+ op,
510
+ overload,
511
+ )
512
+ else:
513
+ self._op_identifier = None
514
+ self.outputs = [NodeOutputPattern(self, i, name) for i, name in enumerate(outputs)]
515
+
516
+ # Update uses for inputs.
517
+ for index, value in enumerate(self.inputs):
518
+ if value is not None:
519
+ value.append_use(self, index)
520
+
521
+ def __str__(self) -> str:
522
+ inputs = ", ".join(str(v) for v in self.inputs)
523
+ outputs = ", ".join(str(v) for v in self.outputs)
524
+ attributes = ", ".join(f"{k}={v}" for k, v in self.attributes.items())
525
+ op = str(self.op)
526
+ domain = str(self.domain)
527
+ qualified_op = f"{domain}.{op}" if domain else op
528
+ inputs_and_attributes = f"{inputs}, {attributes}" if attributes else inputs
529
+ return f"{outputs} = {qualified_op} ({inputs_and_attributes})"
530
+
531
+ def op_identifier(self) -> Tuple[str, str, str] | None:
532
+ return self._op_identifier
533
+
534
+ @property
535
+ def op_type(self) -> str:
536
+ return str(self.op)
537
+
538
+ def matches(self, node: ir.Node, match: MatchResult) -> MatchResult:
539
+ """Matches the pattern represented by self against a node.
540
+
541
+ This is purely a local node-level match, and does not consider the subgraph rooted at the node.
542
+ We check the domain, op_type, and attributes of the node, but not the inputs.
543
+ """
544
+ # TODO(rama): Ensure we handle "" and "onnx.ai" correctly.
545
+ if not self.op.matches(node.op_type):
546
+ return match.fail(
547
+ f"OpType mismatch: expected {self.op}, got {node.op_type}.", node
548
+ )
549
+ if not self.domain.matches(node.domain):
550
+ return match.fail(
551
+ f"Domain mismatch: expected {self.domain}, got {node.domain}.", node
552
+ )
553
+
554
+ for name, attr_pattern in self.attributes.items():
555
+ attr_value = node.attributes.get(name)
556
+ if attr_value is None:
557
+ return match.fail(f"Attribute {name} not found in node.", node)
558
+ if not attr_pattern.matches(attr_value):
559
+ return match.fail(
560
+ f"Attribute {name} mismatch: expected {attr_pattern}, got {attr_value}.",
561
+ node,
562
+ )
563
+ if attr_pattern.name is not None:
564
+ if not match.bind(attr_pattern.name, attr_value):
565
+ return match
566
+
567
+ if not self.allow_other_attributes:
568
+ for name in node.attributes:
569
+ # TODO: Support matching default nodes for attributes.
570
+ if name not in self.attributes:
571
+ return match.fail(f"Attribute {name} not expected in node.", node)
572
+
573
+ return match
574
+
575
+ def clone(self, node_map: dict[NodePattern, NodePattern], swap: bool) -> NodePattern:
576
+ inputs = [(v.clone(node_map) if v is not None else None) for v in self.inputs]
577
+ if swap:
578
+ assert len(inputs) == 2, (
579
+ "Internal error: commutative swap applies only to binary ops."
580
+ )
581
+ inputs = [inputs[1], inputs[0]]
582
+ outputs = [value.name for value in self.outputs]
583
+ copied = NodePattern(
584
+ self.domain,
585
+ self.op,
586
+ inputs,
587
+ self.attributes,
588
+ outputs,
589
+ allow_other_attributes=self.allow_other_attributes,
590
+ allow_other_inputs=self.allow_other_inputs,
591
+ )
592
+ node_map[self] = copied
593
+ return copied
594
+
595
+
596
+ class NodeOutputPattern(ValuePattern):
597
+ """Represents a pattern that matches against a specific output of a Node.
598
+
599
+ This is the primary pattern used to match against computed values, that
600
+ is values computed using a specific op.
601
+ """
602
+
603
+ def __init__(
604
+ self, producer: NodePattern, output_index: int, name: str | None = None
605
+ ) -> None:
606
+ super().__init__(name)
607
+ self._producer = producer
608
+ self._output_index = output_index
609
+
610
+ def clone(self, node_map: dict[NodePattern, NodePattern]) -> NodeOutputPattern:
611
+ return node_map[self._producer].outputs[self._output_index]
612
+ # return NodeOutputPattern(node_map[self._producer], self._output_index, self._name)
613
+
614
+ @property
615
+ def output_index(self) -> int:
616
+ return self._output_index
617
+
618
+ def producer(self) -> NodePattern:
619
+ return self._producer
620
+
621
+
622
+ Var = ValuePattern
623
+
624
+
625
+ def _is_pattern_variable(x: Any) -> bool:
626
+ # The derived classes of ValuePattern represent constant patterns and node-output patterns.
627
+ return type(x) is ValuePattern
628
+
629
+
630
+ class Constant(ValuePattern):
631
+ """Represents a pattern that matches against a scalar constant value."""
632
+
633
+ def __init__(
634
+ self,
635
+ value: int | float | Sequence[int] | Sequence[float],
636
+ rel_tol: float = 1e-5,
637
+ abs_tol: float = 1e-8,
638
+ ) -> None:
639
+ super().__init__(None)
640
+ self._value = list(value) if isinstance(value, Sequence) else value
641
+ self._rel_tol = rel_tol
642
+ self._abs_tol = abs_tol
643
+
644
+ def clone(self, node_map: dict[NodePattern, NodePattern]) -> Constant:
645
+ del node_map
646
+ return Constant(self._value, self._rel_tol, self._abs_tol)
647
+
648
+ @property
649
+ def value(self) -> int | float | list[int] | list[float]:
650
+ return self._value
651
+
652
+ def matches(self, value: ir.Value, match: MatchResult) -> MatchResult:
653
+ constant_value = value.const_value
654
+ if constant_value is None:
655
+ return match.fail(f"Value is not a constant, expecting {self.value}.")
656
+
657
+ constant_value_numpy = constant_value.numpy()
658
+ if isinstance(self._value, list):
659
+ if constant_value_numpy.shape != (len(self._value),):
660
+ return match.fail(f"Value has mismatching shape, expecting ({self.value},).")
661
+ if not all(
662
+ math.isclose(
663
+ constant_value_numpy.item(i),
664
+ self._value[i],
665
+ rel_tol=self._rel_tol,
666
+ abs_tol=self._abs_tol,
667
+ )
668
+ for i in range(len(self._value))
669
+ ):
670
+ return match.fail(
671
+ f"Value mismatch: expected {self._value}, got {constant_value_numpy}."
672
+ )
673
+ return match
674
+
675
+ # Scalar constant case:
676
+ # TODO (rama): allow users to specify shape requirement, if desired.
677
+ if constant_value_numpy.size != 1:
678
+ return match.fail(f"Value is not a scalar, expecting {self.value}.")
679
+
680
+ if not math.isclose(
681
+ constant_value_numpy.item(),
682
+ self._value,
683
+ rel_tol=self._rel_tol,
684
+ abs_tol=self._abs_tol,
685
+ ):
686
+ match.fail(
687
+ f"Value mismatch: expected {self._value}, got {constant_value_numpy.item()}."
688
+ )
689
+
690
+ # Note: If the value is produced by a Constant node, we could include
691
+ # the Constant node in the return_value list. However, we don't do that.
692
+ # Instead, we will rely on DCE to remove the constant node if it is not
693
+ # used elsewhere.
694
+ return match
695
+
696
+ def __str__(self) -> str:
697
+ return str(self._value)
698
+
699
+
700
+ def _nodes_in_pattern(outputs: Sequence[ValuePattern]) -> list[NodePattern]:
701
+ """Returns all nodes used in a pattern, given the outputs of the pattern."""
702
+ node_patterns: list[NodePattern] = []
703
+
704
+ def visit(value_patterns: Sequence[ValuePattern | None]) -> None:
705
+ for value_pattern in value_patterns:
706
+ if isinstance(value_pattern, NodeOutputPattern):
707
+ node_pattern = value_pattern.producer()
708
+ if node_pattern not in node_patterns:
709
+ node_patterns.append(node_pattern)
710
+ visit(node_pattern.inputs)
711
+
712
+ visit(outputs)
713
+ node_patterns.reverse()
714
+ return node_patterns
715
+
716
+
717
+ def _add_backward_slice(node: NodePattern, backward_slice: set[NodePattern]) -> None:
718
+ """Adds all nodes in the backward slice of given node to the set `backward_slice`.
719
+
720
+ The backward slice of a node is the set of all nodes that are reachable from the node
721
+ in a backward traversal from the given node.
722
+ """
723
+ if node in backward_slice:
724
+ return
725
+ backward_slice.add(node)
726
+ for value_pattern in node.inputs:
727
+ if isinstance(value_pattern, NodeOutputPattern):
728
+ _add_backward_slice(value_pattern.producer(), backward_slice)
729
+
730
+
731
+ class GraphPattern:
732
+ """Represents a pattern that can be matched against a subgraph."""
733
+
734
+ def __init__(
735
+ self,
736
+ inputs: Sequence[ValuePattern],
737
+ outputs: Sequence[ValuePattern],
738
+ nodes: Sequence[NodePattern],
739
+ ) -> None:
740
+ self._inputs = inputs
741
+ self._outputs = outputs
742
+ if len(outputs) == 0:
743
+ raise ValueError("GraphPattern must have at least one output")
744
+ self._nodes = nodes # _nodes_in_pattern(outputs)
745
+
746
+ # Determine the output nodes of the pattern. These are a minimal set of nodes
747
+ # whose backward-slices cover the entire pattern.
748
+ output_nodes: set[NodePattern] = set()
749
+ covered: set[NodePattern] = set()
750
+ for value_pattern in outputs:
751
+ if not isinstance(value_pattern, ValuePattern):
752
+ raise TypeError(
753
+ f"Invalid type {type(value_pattern)} for graph pattern output."
754
+ )
755
+ if isinstance(value_pattern, Constant):
756
+ raise NotImplementedError(
757
+ "Constant values are not allowed as graph pattern outputs."
758
+ )
759
+ if isinstance(value_pattern, NodeOutputPattern):
760
+ candidate = value_pattern.producer()
761
+ if candidate not in covered:
762
+ output_nodes.add(candidate)
763
+ _add_backward_slice(candidate, covered)
764
+
765
+ self.output_nodes: list[NodePattern] = list(output_nodes)
766
+
767
+ @property
768
+ def output_node(self) -> NodePattern:
769
+ if len(self.output_nodes) != 1:
770
+ raise ValueError("GraphPattern does not have unique output node.")
771
+ return self.output_nodes[0]
772
+
773
+ def node(self, index: int) -> NodePattern:
774
+ return self._nodes[index]
775
+
776
+ def num_nodes(self) -> int:
777
+ return len(self._nodes)
778
+
779
+ def __len__(self) -> int:
780
+ return self.num_nodes()
781
+
782
+ @property
783
+ def inputs(self) -> Sequence[ValuePattern]:
784
+ return self._inputs
785
+
786
+ @property
787
+ def outputs(self) -> Sequence[ValuePattern]:
788
+ return self._outputs
789
+
790
+ def __iter__(self) -> Iterator[NodePattern]:
791
+ return iter(self._nodes)
792
+
793
+ def __reversed__(self) -> Iterator[NodePattern]:
794
+ return reversed(self._nodes)
795
+
796
+ @property
797
+ def has_single_output_node(self) -> bool:
798
+ return len(self.output_nodes) == 1
799
+
800
+ @property
801
+ def num_outputs(self) -> int:
802
+ return len(self._outputs)
803
+
804
+ def commute(self) -> Sequence[GraphPattern]:
805
+ def commute_node(node: NodePattern) -> Iterable[bool]:
806
+ if node.op_identifier() == ("", "Add", "") or node.op_identifier() == (
807
+ "",
808
+ "Mul",
809
+ "",
810
+ ):
811
+ # Try with and without swapping inputs.
812
+ return [False, True]
813
+ # No swapping of inputs
814
+ return [False]
815
+
816
+ iteration_space = [commute_node(node) for node in self._nodes]
817
+
818
+ def copy_graph(swap_list: Iterable[bool]) -> GraphPattern:
819
+ if not any(swap_list):
820
+ # No need to swap inputs of any node
821
+ return self
822
+ # Create a copy of the graph, with swapped inputs for the nodes that need it.
823
+ node_map: dict[NodePattern, NodePattern] = {}
824
+ new_inputs = [v.clone(node_map) for v in self._inputs]
825
+ new_nodes = [
826
+ node.clone(node_map, swap) for node, swap in zip(self._nodes, swap_list)
827
+ ]
828
+ new_outputs = [v.clone(node_map) for v in self._outputs]
829
+ return GraphPattern(new_inputs, new_outputs, new_nodes)
830
+
831
+ return [copy_graph(swap_list) for swap_list in itertools.product(*iteration_space)]
832
+
833
+ def __str__(self) -> str:
834
+ inputs = ", ".join(str(v) for v in self._inputs)
835
+ outputs = ", ".join(str(v) for v in self._outputs)
836
+ nodes = "\n ".join(str(n) for n in self._nodes)
837
+ return f"pattern ({inputs}) {{\n {nodes}\n return {outputs}\n}}"
838
+
839
+
840
+ def _to_graph_pattern(pattern_constructor: Callable) -> GraphPattern:
841
+ """Convert a pattern-construction function to a GraphPattern.
842
+
843
+ A pattern-construction function will return values as below:
844
+ ::
845
+ def pattern(op, x: Var, shape1: Var, shape2: Var):
846
+ ...
847
+ return outputs
848
+
849
+ We create a pattern graph by creating pattern-variables for each parameter of the function,
850
+ and calling the function. The returned values are normalized to a list of ValuePatterns,
851
+ which represent the outputs of the pattern graph.
852
+
853
+ Args:
854
+ pattern_constructor: Callable
855
+
856
+ Returns:
857
+ GraphPattern: A representation of the pattern that can be matched against a subgraph.
858
+ """
859
+ _pattern_vars = inspect.signature(pattern_constructor).parameters
860
+ pattern_inputs = [Var(v) for v in _pattern_vars][1:] # Skip the first parameter
861
+ builder = OpsetPatternBuilder("", record=True)
862
+ with pattern_builder(builder):
863
+ pattern_outputs = pattern_constructor(builder, *pattern_inputs)
864
+ # TODO(rama): classify inputs as value/attribute vars
865
+ # Returned value could be a single ValuePattern or a list of ValuePatterns.
866
+ # Normalize representation to a list of ValuePatterns.
867
+ if isinstance(pattern_outputs, ValuePattern):
868
+ pattern_outputs = [pattern_outputs]
869
+ return GraphPattern(pattern_inputs, pattern_outputs, builder.nodes())
870
+
871
+
872
+ def _valid_to_replace(
873
+ matched_nodes: Sequence[ir.Node], output_values: Sequence[ir.Value]
874
+ ) -> bool:
875
+ """Check that values computed by the matched_nodes, except for output_values, are used only by the matched_nodes."""
876
+ # * Must check that all values matched by pattern are used only by pattern,
877
+ # except for the value that is replaced.
878
+ # * Must ensure that replacement subgraph does not use any of the deleted
879
+ # (intermediate) values. (Not necessary for now. Guaranteed.)
880
+ for n in matched_nodes:
881
+ for v in n.outputs:
882
+ if v in output_values:
883
+ continue
884
+ if v.is_graph_output():
885
+ # value is an output-value of the graph/function.
886
+ return False
887
+ for consumer, _ in v.uses():
888
+ if consumer not in matched_nodes:
889
+ return False
890
+ return True
891
+
892
+
893
+ RewriterContext = _tape.Builder
894
+
895
+
896
+ @dataclasses.dataclass
897
+ class ReplacementSubgraph:
898
+ """A subgraph that will replace the matched pattern."""
899
+
900
+ match: MatchResult
901
+ new_outputs: Sequence[ir.Value]
902
+ new_nodes: Sequence[ir.Node]
903
+ new_initializers: Sequence[ir.Value]
904
+ used_opsets: _tape.UsedOpsets
905
+
906
+
907
+ def always_true(*args, **kwargs) -> bool:
908
+ """A condition function that always returns True.
909
+
910
+ This is used when no condition function is provided for a rewrite rule.
911
+ """
912
+ return True
913
+
914
+
915
+ class ReplacementPatternFunction:
916
+ """The replacement pattern that will replace the targeted pattern.
917
+
918
+ Attributes:
919
+ function (Callable): The replacement function that will be used to replace the matched pattern.
920
+ """
921
+
922
+ def __init__(self, function) -> None:
923
+ self._function = function
924
+
925
+ def get_replacement(self, match: MatchResult) -> ReplacementSubgraph | None:
926
+ context = RewriterContext()
927
+ new_outputs = self._function(context, **match.bindings)
928
+ if new_outputs is None:
929
+ return None # Failed to create replacement subgraph
930
+ if not isinstance(new_outputs, Sequence):
931
+ new_outputs = [new_outputs]
932
+ return ReplacementSubgraph(
933
+ match, new_outputs, context.nodes, context.initializers, context.used_opsets
934
+ )
935
+
936
+
937
+ def _update_opset_imports(
938
+ graph_or_function: ir.Graph | ir.Function, delta: ReplacementSubgraph
939
+ ):
940
+ imports = graph_or_function.opset_imports
941
+ for domain, version in delta.used_opsets:
942
+ if domain not in imports:
943
+ # use 1 as default version if not explicitly specified
944
+ imports[domain] = version if version is not None else 1
945
+ elif version is not None and version != imports[domain]:
946
+ raise ValueError(
947
+ f"Multiple versions of opset {domain} used. "
948
+ f"Expected version {imports[domain]}, but got {version}."
949
+ )
950
+
951
+
952
+ class PatternMatcher(abc.ABC):
953
+ def __init__(self, pattern: GraphPattern) -> None:
954
+ self.pattern = pattern
955
+
956
+ @abc.abstractmethod
957
+ def match(
958
+ self,
959
+ model: ir.Model,
960
+ graph_or_function: ir.Graph | ir.Function,
961
+ node: ir.Node,
962
+ *,
963
+ verbose: int = 0,
964
+ remove_nodes: bool = True,
965
+ tracer: MatchingTracer | None = None,
966
+ ) -> MatchResult:
967
+ """Match the pattern against the subgraph ending at the given node."""
968
+
969
+ def __str__(self) -> str:
970
+ return str(self.pattern)
971
+
972
+
973
+ class SimplePatternMatcher(PatternMatcher):
974
+ def __init__(self, pattern: GraphPattern) -> None:
975
+ super().__init__(pattern)
976
+ self._current_node: ir.Node | None = None
977
+
978
+ def fail(self, reason: str, node: ir.Node | None = None) -> bool:
979
+ if self._verbose:
980
+ if self._matched: # Print only if at least one node successfully matched.
981
+ count = len(self._matched)
982
+ print(f"Match failed after {count} nodes: {reason}")
983
+ self._match.fail(reason, node or self._current_node)
984
+ return False
985
+
986
+ def _match_constant(self, pattern_constant: Constant, value: ir.Value) -> bool:
987
+ """Match a Constant pattern against a value.
988
+
989
+ If the constant value is produced by a Constant node, we do not include
990
+ the constant node as part of the matched graph. Thus, it will not be deleted,
991
+ if subgraph replacement happens. But subsequent DCE will remove the constant
992
+ node if it is not used elsewhere.
993
+ """
994
+ constant_value = value.const_value
995
+ if constant_value is None:
996
+ return self.fail(
997
+ f"Value {value.name} is not a constant, expecting {pattern_constant.value}.",
998
+ )
999
+
1000
+ try:
1001
+ constant_value_numpy = constant_value.numpy()
1002
+ except FileNotFoundError:
1003
+ return self.fail(f"Constant value of {value.name} not available.")
1004
+
1005
+ pattern_constant_value = pattern_constant._value
1006
+
1007
+ if isinstance(pattern_constant_value, list):
1008
+ expected_shape = (len(pattern_constant_value),)
1009
+ if constant_value_numpy.shape != expected_shape:
1010
+ return self.fail(f"Value has mismatching shape, expecting {expected_shape}.")
1011
+ if not all(
1012
+ math.isclose(
1013
+ constant_value_numpy.item(i),
1014
+ pattern_constant_value[i],
1015
+ rel_tol=pattern_constant._rel_tol,
1016
+ abs_tol=pattern_constant._abs_tol,
1017
+ )
1018
+ for i in range(len(pattern_constant_value))
1019
+ ):
1020
+ return self.fail(
1021
+ f"Value mismatch: expected {pattern_constant_value}, got {constant_value_numpy}."
1022
+ )
1023
+ return True
1024
+
1025
+ # TODO (rama): allow users to specify shape requirement, if desired.
1026
+ if constant_value_numpy.size != 1:
1027
+ return self.fail(
1028
+ f"Value {value.name} is not a scalar, expecting {pattern_constant_value}.",
1029
+ )
1030
+
1031
+ if not math.isclose(
1032
+ constant_value_numpy.item(),
1033
+ pattern_constant_value,
1034
+ rel_tol=pattern_constant._rel_tol,
1035
+ abs_tol=pattern_constant._abs_tol,
1036
+ ):
1037
+ return self.fail(
1038
+ f"Constant value mismatch: expected {pattern_constant_value}, got {constant_value_numpy.item()}.",
1039
+ )
1040
+
1041
+ return True
1042
+
1043
+ def _match_node(self, pattern_node: NodePattern, node: ir.Node) -> bool:
1044
+ """Matches a pattern subgraph against subgraph rooted at node."""
1045
+ self._current_node = node
1046
+ # Graph-matching: we do not allow the same pattern node to be matched against
1047
+ # different graph nodes.
1048
+ if pattern_node in self._matched:
1049
+ if self._matched[pattern_node] is not node:
1050
+ return self.fail("Same pattern node is matched against different graph nodes.")
1051
+ return True
1052
+ match = self._match
1053
+ if not pattern_node.matches(node, match):
1054
+ return self.fail(match.reason)
1055
+
1056
+ if self._verbose:
1057
+ print(f"Matched: {node.op_type}")
1058
+
1059
+ match.nodes.append(node)
1060
+ self._matched[pattern_node] = node
1061
+
1062
+ # TODO: Revisit this to handle optional trailing inputs better.
1063
+ if pattern_node.allow_other_inputs:
1064
+ if len(node.inputs) < len(pattern_node.inputs):
1065
+ return self.fail(
1066
+ f"Number of inputs ({len(node.inputs)}) is less than expected ({len(pattern_node.inputs)})"
1067
+ )
1068
+ else:
1069
+ if len(node.inputs) != len(pattern_node.inputs):
1070
+ return self.fail(
1071
+ f"Input nums mismatch. {len(node.inputs)} vs {len(pattern_node.inputs)}"
1072
+ )
1073
+
1074
+ for arg_value, arg_pattern in zip(node.inputs, pattern_node.inputs):
1075
+ # arg_pattern could be a Var, if it's the original arg.
1076
+ if arg_pattern is None:
1077
+ if arg_value is None:
1078
+ continue
1079
+ else:
1080
+ return self.fail("(Optional) input is expected to be None but is not.")
1081
+ if not self._match_value(arg_pattern, arg_value):
1082
+ return False
1083
+
1084
+ for i, output_value_pattern in enumerate(pattern_node.outputs):
1085
+ if not self._bind_value(output_value_pattern, node.outputs[i]):
1086
+ return False
1087
+
1088
+ return True
1089
+
1090
+ def _bind_value(self, pattern_value: ValuePattern, value: ir.Value | None) -> bool:
1091
+ """Bind a ValuePattern var to ir Value."""
1092
+ if pattern_value.name is not None:
1093
+ match = self._match
1094
+ if pattern_value.name in match.bindings:
1095
+ # TODO(rama): Use appropriate equality-check here: future extension possibility.
1096
+ if match.bindings[pattern_value.name] == value:
1097
+ return True
1098
+ return self.fail(f"Variable {pattern_value.name} is bound to multiple values.")
1099
+ match.bindings[pattern_value.name] = value
1100
+ return True
1101
+
1102
+ def _match_value(self, pattern_value: ValuePattern, value: ir.Value | None) -> bool:
1103
+ """Match an IR value against a ValuePattern instance."""
1104
+ if not self._bind_value(pattern_value, value):
1105
+ return False
1106
+
1107
+ if isinstance(pattern_value, NodeOutputPattern):
1108
+ if value is None:
1109
+ return self.fail("Mismatch: Computed node pattern does not match None.")
1110
+ return self._match_node_output(pattern_value, value)
1111
+ if isinstance(pattern_value, Constant):
1112
+ if value is None:
1113
+ return self.fail("Mismatch: Constant pattern does not match None.")
1114
+ return self._match_constant(pattern_value, value)
1115
+ return True
1116
+
1117
+ def _match_node_output(self, pattern_value: NodeOutputPattern, value: ir.Value) -> bool:
1118
+ """Match an IR value against a NodeOutputPattern instance."""
1119
+ node = value.producer()
1120
+ if node is None:
1121
+ return self.fail(
1122
+ "Mismatch: Computed node pattern does not match uncomputed IR value."
1123
+ )
1124
+ if value.index() != pattern_value.output_index:
1125
+ return self.fail(
1126
+ f"Node output index mismatch: expected {pattern_value._output_index}, got {value.index()}."
1127
+ )
1128
+ return self._match_node(pattern_value.producer(), node)
1129
+
1130
+ def _init_match(self, verbose: int) -> None:
1131
+ """Initialize the match state. Invoked before starting a new match."""
1132
+ self._verbose = verbose
1133
+ self._matched: dict[NodePattern, ir.Node] = {}
1134
+ self._match: MatchResult = MatchResult()
1135
+ self._current_node = None
1136
+
1137
+ def _get_output_values(self) -> list[ir.Value] | None:
1138
+ """Get values bound to the output variables of the pattern."""
1139
+ output_values: list[ir.Value] = []
1140
+ unbound_values: list[str] = []
1141
+ for j, value_pattern in enumerate(self.pattern.outputs):
1142
+ if value_pattern.name is not None:
1143
+ if value_pattern.name in self._match.bindings:
1144
+ output_values.append(self._match.bindings[value_pattern.name])
1145
+ else:
1146
+ unbound_values.append(value_pattern.name)
1147
+ elif isinstance(value_pattern, NodeOutputPattern):
1148
+ i = value_pattern.output_index
1149
+ node = value_pattern.producer()
1150
+ if node in self._matched:
1151
+ output_values.append(self._matched[node].outputs[i])
1152
+ else:
1153
+ unbound_values.append(f"output_{j}")
1154
+ elif isinstance(value_pattern, Constant):
1155
+ raise NotImplementedError("Constant values as return-values not supported.")
1156
+ if unbound_values:
1157
+ self._match.fail(f"Error: Output values not found: {unbound_values}")
1158
+ return None
1159
+ return output_values
1160
+
1161
+ def _match_single_output_node(
1162
+ self,
1163
+ model: ir.Model,
1164
+ graph_or_function: ir.Graph | ir.Function,
1165
+ node: ir.Node,
1166
+ check_removable: bool,
1167
+ ) -> MatchResult:
1168
+ del model
1169
+ del graph_or_function
1170
+
1171
+ pattern = self.pattern
1172
+ match = self._match
1173
+
1174
+ if not pattern.has_single_output_node:
1175
+ return match.fail(
1176
+ "Internal Error: SimplePatternMatcher should not be used for patterns with multiple output nodes."
1177
+ )
1178
+
1179
+ if not self._match_node(pattern.output_node, node):
1180
+ return match
1181
+
1182
+ output_values = self._get_output_values()
1183
+ if output_values is None:
1184
+ # TODO(rama): Is this a valid (useful) case?
1185
+ return match
1186
+ if check_removable and not _valid_to_replace(match.nodes, output_values):
1187
+ # TODO(rama): Match status should be updated to reflect failure reason.
1188
+ return match.fail("Matched nodes have other uses preventing replacement.")
1189
+
1190
+ match.outputs.extend(output_values)
1191
+ return match
1192
+
1193
+ def _multi_match(self, candidate: Iterable[ir.Node], check_removable: bool) -> MatchResult:
1194
+ """Find a match for a pattern with multiple output nodes.
1195
+
1196
+ For a pattern with K output nodes, the input candidate should specify K nodes
1197
+ in the graph that will be matched against the pattern output nodes.
1198
+
1199
+ Args:
1200
+ candidate: An iterable of nodes that will be matched against the pattern output nodes.
1201
+ check_removable: If True, check that the matched nodes can be removed (that is, that
1202
+ they are not used elsewhere in the graph).
1203
+ """
1204
+ match = self._match
1205
+ for pattern_node, node in zip(self.pattern.output_nodes, candidate):
1206
+ if not self._match_node(pattern_node, node):
1207
+ return match
1208
+ output_values = self._get_output_values()
1209
+ if output_values is None:
1210
+ return match
1211
+
1212
+ if check_removable and not _valid_to_replace(match.nodes, output_values):
1213
+ return match.fail("Matched nodes have other uses preventing replacement.")
1214
+
1215
+ match.outputs.extend(output_values)
1216
+ return match
1217
+
1218
+ def match(
1219
+ self,
1220
+ model: ir.Model,
1221
+ graph_or_function: ir.Graph | ir.Function,
1222
+ node: ir.Node,
1223
+ *,
1224
+ verbose: int = 0,
1225
+ remove_nodes: bool = True,
1226
+ tracer: MatchingTracer | None = None,
1227
+ ) -> MatchResult:
1228
+ """Match the pattern against the subgraph ending at the given node.
1229
+
1230
+ For patterns with multiple output nodes, the given node is matched
1231
+ against the first output node in the pattern. For the remaining
1232
+ output nodes in the pattern, we use a brute-force algorithm that
1233
+ enumerates all possible combinations of nodes from the graph (with
1234
+ a filter based on op-type).
1235
+
1236
+ TODO: Consider omitting parameters model and graph_or_function. With
1237
+ the new IR, the graph can be obtained from the node, and the model is
1238
+ not used. But this is a shared abstract method of the Matcher interface,
1239
+ so other matcher implementation also needs to be updated. More importantly,
1240
+ matching in the presence of subgraphs (control-flow) can introduce some
1241
+ complications which require careful consideration.
1242
+ """
1243
+ self._tracer = tracer
1244
+ if self.pattern.has_single_output_node:
1245
+ self._init_match(verbose)
1246
+ return self._match_single_output_node(
1247
+ model, graph_or_function, node, check_removable=remove_nodes
1248
+ )
1249
+ else:
1250
+ # Note: This is a potentially expensive algorithm for matching patterns with
1251
+ # multiple output nodes. For patterns with N output nodes, we try all possible
1252
+ # combinations of N nodes from the graph, and check if they match the pattern.
1253
+ # The first node is fixed to the node argument in this method call. We do
1254
+ # some simple filtering by restricting the candidates for each remaining
1255
+ # output nodes to graph nodes with the same op_type as the corresponding pattern
1256
+ # node. For now, this is intended to be a simple, but robust, implementation
1257
+ # that can be used for debugging and testing. The GenericPatternMatcher is a
1258
+ # more sophisticated implementation, but incomplete.
1259
+ pattern_output_nodes = self.pattern.output_nodes
1260
+ op_to_nodes: dict[tuple[str, str, str], list[ir.Node]] = {}
1261
+ for n in graph_or_function:
1262
+ op_to_nodes.setdefault(n.op_identifier(), []).append(n)
1263
+ all_nodes = iter(graph_or_function)
1264
+
1265
+ def get_nodes(pattern_node):
1266
+ id = pattern_node.op_identifier()
1267
+ if id is None:
1268
+ return all_nodes
1269
+ return op_to_nodes.get(id, [])
1270
+
1271
+ candidates = [iter([node])] + [get_nodes(pn) for pn in pattern_output_nodes[1:]]
1272
+ match = None
1273
+ for combination in itertools.product(*candidates):
1274
+ self._init_match(verbose)
1275
+ match = self._multi_match(combination, check_removable=remove_nodes)
1276
+ if match:
1277
+ return match
1278
+ if match is None:
1279
+ return MatchResult().fail("No match found.")
1280
+ return match
1281
+
1282
+
1283
+ class RewriteRule:
1284
+ def __init__(
1285
+ self,
1286
+ target_pattern: GraphPattern | Callable,
1287
+ replacement_pattern: ReplacementPatternFunction | Callable,
1288
+ condition_function: Callable | None = None,
1289
+ matcher: PatternMatcher | Callable[[GraphPattern], PatternMatcher] | None = None,
1290
+ verbose: int = 0,
1291
+ name: str | None = None,
1292
+ remove_nodes: bool = True,
1293
+ graph_pre_visitor: Callable[[], None] | None = None,
1294
+ graph_post_visitor: Callable[[], None] | None = None,
1295
+ ) -> None:
1296
+ """Create a rewrite rule.
1297
+
1298
+ Args:
1299
+ target_pattern: The GraphPattern that will be matched against the IR.
1300
+ If a callable is provided, it will be converted to a GraphPattern.
1301
+ replacement_pattern: The ReplacementPatternFunction that will be used to
1302
+ replace the matched pattern. If a callable is provided, it will be
1303
+ converted to a ReplacementPatternFunction.
1304
+ condition_function: The condition function that will be used to check if
1305
+ the pattern match found should be rewritten.
1306
+ matcher: The pattern matcher that will be used to match the pattern.
1307
+ If not provided, a default matcher will be used.
1308
+ verbose: The verbosity level of the rule.
1309
+ name: An optional name for the pattern that will show up in verbose logging.
1310
+ remove_nodes: If True, the matched nodes will be removed from the graph.
1311
+ graph_pre_visitor: A function that will be called before applying the
1312
+ rewriting to the top-level graph or a function.
1313
+ graph_post_visitor: A function that will be called after the rewriting
1314
+ is complete for a graph or function.
1315
+ """
1316
+
1317
+ if not isinstance(target_pattern, GraphPattern):
1318
+ target_pattern = _to_graph_pattern(target_pattern)
1319
+ self._target_pattern = target_pattern
1320
+
1321
+ if not isinstance(replacement_pattern, ReplacementPatternFunction):
1322
+ replacement_pattern = ReplacementPatternFunction(replacement_pattern)
1323
+ self._replacement_pattern = replacement_pattern
1324
+ self._condition_function = condition_function or always_true
1325
+ if isinstance(matcher, PatternMatcher):
1326
+ self._matcher = matcher
1327
+ elif matcher is None:
1328
+ if target_pattern.has_single_output_node:
1329
+ self._matcher = SimplePatternMatcher(self._target_pattern)
1330
+ else:
1331
+ import onnxscript.rewriter.generic_pattern as generic_pattern
1332
+
1333
+ self._matcher = generic_pattern.GenericPatternMatcher(self._target_pattern)
1334
+ else:
1335
+ self._matcher = matcher(self._target_pattern)
1336
+ self._verbose = verbose
1337
+ self.name = name
1338
+ self.remove_nodes = remove_nodes
1339
+ self.graph_pre_visitor = graph_pre_visitor
1340
+ self.graph_post_visitor = graph_post_visitor
1341
+
1342
+ def __str__(self) -> str:
1343
+ return self.name if self.name else "Anonymous Rule"
1344
+
1345
+ def try_rewrite(
1346
+ self,
1347
+ model: ir.Model,
1348
+ graph_or_function: ir.Graph | ir.Function,
1349
+ node: ir.Node,
1350
+ *,
1351
+ verbose: int | None = None,
1352
+ tracer: MatchingTracer | None = None,
1353
+ ) -> ReplacementSubgraph | None:
1354
+ """If the node matches the pattern, then replace the node with the replacement pattern."""
1355
+ if verbose and verbose > 2:
1356
+ print(f"[try_rewrite] {self}")
1357
+ verbose = verbose if verbose is not None else self._verbose
1358
+ match = self._matcher.match(
1359
+ model, graph_or_function, node, verbose=verbose, remove_nodes=self.remove_nodes
1360
+ )
1361
+ if match:
1362
+ context = None # TODO(rama)
1363
+ for var in self._target_pattern.inputs:
1364
+ if var.name is not None:
1365
+ if var.name not in match.bindings:
1366
+ match.bindings[var.name] = None
1367
+ if not self._condition_function(context, **match.bindings):
1368
+ if tracer:
1369
+ tracer.log(
1370
+ self, graph_or_function, node, match, MatchStatus.CONDITION_FAILED
1371
+ )
1372
+ return None
1373
+ replacement_subgraph = self._replacement_pattern.get_replacement(match)
1374
+ if replacement_subgraph is None:
1375
+ if tracer:
1376
+ tracer.log(
1377
+ self, graph_or_function, node, match, MatchStatus.REPLACEMENT_FAILED
1378
+ )
1379
+ return None
1380
+ if len(replacement_subgraph.new_outputs) != self._target_pattern.num_outputs:
1381
+ raise ValueError(
1382
+ f"Number of outputs from replacement function does not match the number of outputs from the target pattern. "
1383
+ f"Expected {self._target_pattern.num_outputs}, but got {len(replacement_subgraph.new_outputs)}."
1384
+ )
1385
+ # TODO(rama): Remove the opset imports from deleted nodes?
1386
+ _update_opset_imports(graph_or_function, replacement_subgraph)
1387
+ _update_opset_imports(model.graph, replacement_subgraph)
1388
+ if tracer:
1389
+ tracer.log(self, graph_or_function, node, match, MatchStatus.SUCCESS)
1390
+ return replacement_subgraph
1391
+ if tracer:
1392
+ tracer.log(self, graph_or_function, node, match, MatchStatus.NO_MATCH)
1393
+ return None
1394
+
1395
+ def apply_to_model(
1396
+ self,
1397
+ model: ir.Model,
1398
+ *,
1399
+ commute: bool = False,
1400
+ verbose: int | None = None,
1401
+ debug: bool = False,
1402
+ ):
1403
+ # A convenience method to apply the rule to a model. We use a RewriteRuleSet to
1404
+ # handle commutative rules.
1405
+ return RewriteRuleSet([self], commute=commute).apply_to_model(
1406
+ model, verbose=verbose, debug=debug
1407
+ )
1408
+
1409
+ def commute(self) -> Sequence[RewriteRule]:
1410
+ def replace_pattern(new_pattern):
1411
+ """Return a shallow copy of self with node_pattern replaced by new_pattern."""
1412
+ # TODO(rama): Maybe we should use a better alternative to construct new matcher.
1413
+ matcher_class = type(self._matcher)
1414
+ return RewriteRule(
1415
+ new_pattern,
1416
+ self._replacement_pattern,
1417
+ self._condition_function,
1418
+ matcher_class(new_pattern),
1419
+ self._verbose,
1420
+ self.name,
1421
+ self.remove_nodes,
1422
+ self.graph_pre_visitor,
1423
+ self.graph_post_visitor,
1424
+ )
1425
+
1426
+ return [replace_pattern(p) for p in self._target_pattern.commute()]
1427
+
1428
+
1429
+ class RewriteRuleAsClass:
1430
+ """Defines a class grouping method pattern, rewrite, check.
1431
+ This class is then given to function :func:`make_rewrite_rule_from_class`
1432
+ to define a new rule.
1433
+ """
1434
+
1435
+ @classmethod
1436
+ def pattern(cls, op, *_) -> Any:
1437
+ raise NotImplementedError("Method 'pattern' must be overwritten.")
1438
+
1439
+ @classmethod
1440
+ def rewrite(cls, op, *_) -> Any:
1441
+ raise NotImplementedError("Method 'rewrite' must be overwritten.")
1442
+
1443
+ @classmethod
1444
+ def check(cls, context, *_, **__) -> bool:
1445
+ return True
1446
+
1447
+
1448
+ def make_rewrite_rule_from_class(
1449
+ rule_class: type | RewriteRuleAsClass, generic: bool = False
1450
+ ) -> RewriteRule:
1451
+ """Creates a RewriteRule from a class defining the function
1452
+ pattern, rewrite, check with class method. It makes it is easier
1453
+ to read when a module contains multiple patterns.
1454
+
1455
+ Example::
1456
+
1457
+ class TransposeIdentity(RewriteRuleAsClass):
1458
+ @classmethod
1459
+ def pattern(cls, op, x, perm):
1460
+ return op.Transpose(x, perm=perm)
1461
+
1462
+ @classmethod
1463
+ def check(cls, context, x: ir.Value, perm: ir.Attr | ir.RefAttr) -> bool:
1464
+ if isinstance(perm, ir.RefAttr):
1465
+ return False
1466
+ if perm.type == ir.AttributeType.INTS:
1467
+ if perm.value == list(range(len(perm.value))):
1468
+ return True
1469
+ return False
1470
+
1471
+ @classmethod
1472
+ def rewrite(cls, op, x: ir.Value, perm: ir.Attr | None = None):
1473
+ return op.Identity(x)
1474
+
1475
+ transpose_identity_rule = make_rewrite_rule_from_class(TransposeIdentity)
1476
+ """
1477
+ assert hasattr(rule_class, "pattern"), f"Method 'pattern' is missing from {rule_class!r}."
1478
+ assert hasattr(rule_class, "rewrite"), f"Method 'rewrite' is missing from {rule_class!r}."
1479
+ assert hasattr(rule_class, "check"), f"Method 'check' is missing from {rule_class!r}."
1480
+ if generic:
1481
+ import onnxscript.rewriter.generic_pattern as orpp
1482
+
1483
+ return RewriteRule(
1484
+ rule_class.pattern,
1485
+ rule_class.rewrite,
1486
+ rule_class.check,
1487
+ orpp.GenericPatternMatcher,
1488
+ name=rule_class.__name__, # type: ignore[union-attr]
1489
+ )
1490
+ return RewriteRule(
1491
+ rule_class.pattern,
1492
+ rule_class.rewrite,
1493
+ rule_class.check,
1494
+ name=rule_class.__name__, # type: ignore[union-attr]
1495
+ )
1496
+
1497
+
1498
+ # Variation of RewriteRuleAsClass that is based on instance methods instead of class methods.
1499
+ # Useful to implement a family of rules to support pattern variations.
1500
+ # TODO: cleanup the naming conventions for these inter-related classes.
1501
+ class RewriteRuleClassBase:
1502
+ @classmethod
1503
+ def rule(cls, *args, **kwargs):
1504
+ instance = cls(*args, **kwargs)
1505
+ setup = instance.setup if hasattr(instance, "setup") else None
1506
+ cleanup = instance.cleanup if hasattr(instance, "cleanup") else None
1507
+ return RewriteRule(
1508
+ instance.pattern,
1509
+ instance.rewrite,
1510
+ instance.check,
1511
+ name=instance.name,
1512
+ remove_nodes=instance.remove_nodes,
1513
+ graph_pre_visitor=setup,
1514
+ graph_post_visitor=cleanup,
1515
+ )
1516
+
1517
+ def __init__(self, name: str | None = None, remove_nodes: bool = True) -> None:
1518
+ self.name = name or self.__class__.__name__
1519
+ self.remove_nodes = remove_nodes
1520
+
1521
+ def pattern(self, op, *args, **kwargs):
1522
+ raise NotImplementedError("Method 'pattern' must be implemented by derived class.")
1523
+
1524
+ def check(self, op, *args, **kwargs):
1525
+ # Default check function that always returns True.
1526
+ return True
1527
+
1528
+ def rewrite(self, op, *args, **kwargs):
1529
+ raise NotImplementedError("Method 'rewrite' must be implemented by derived class.")
1530
+
1531
+
1532
+ class RewriteRuleSet:
1533
+ def __init__(self, rules: Sequence[RewriteRule], *, commute: bool = False) -> None:
1534
+ if commute:
1535
+ rules = list(itertools.chain.from_iterable([rule.commute() for rule in rules]))
1536
+ self.rules = rules
1537
+
1538
+ def _apply_to_graph_or_function(
1539
+ self,
1540
+ model: ir.Model,
1541
+ graph_or_function: ir.Graph | ir.Function,
1542
+ *,
1543
+ verbose: int | None,
1544
+ tracer: MatchingTracer | None = None,
1545
+ ) -> int:
1546
+ """
1547
+ Apply the rewrite rules to the given graph or function.
1548
+
1549
+ Args:
1550
+ model: The model to which the rewrite rules are applied.
1551
+ graph_or_function: The graph or function to which the rewrite rules are applied.
1552
+ verbose: The verbosity level. Defaults to None.
1553
+ tracer: The tracer for debugging. Defaults to None.
1554
+
1555
+ Returns:
1556
+ The number of rewrite rules applied.
1557
+ """
1558
+ count = 0
1559
+
1560
+ # NOTE: Rules should be prioritized in the order they are added to the RewriteRuleSet.
1561
+ # And the graph is applied in order.
1562
+ for rule in self.rules:
1563
+ if rule.graph_pre_visitor:
1564
+ rule.graph_pre_visitor()
1565
+ for node in graph_or_function:
1566
+ delta = rule.try_rewrite(
1567
+ model, graph_or_function, node, verbose=verbose, tracer=tracer
1568
+ )
1569
+ if delta is None or tracer is not None:
1570
+ continue
1571
+ assert isinstance(delta, ReplacementSubgraph)
1572
+ if delta.new_initializers:
1573
+ if isinstance(graph_or_function, ir.Function):
1574
+ # TODO(rama): Can't add initializers to functions. But currently this is not
1575
+ # an issue, as we apply inlining before applying rewrite rules.
1576
+ if verbose:
1577
+ print(
1578
+ f"Rewrites adding initializers not supported for functions: {rule}"
1579
+ )
1580
+ continue
1581
+ initializers = graph_or_function.initializers
1582
+ for initializer in delta.new_initializers:
1583
+ if initializer.name in initializers:
1584
+ if verbose:
1585
+ print(f"Initializer {initializer.name} already exists.")
1586
+ continue
1587
+ for initializer in delta.new_initializers:
1588
+ initializers[initializer.name] = initializer # type: ignore[index]
1589
+ # TODO: This does not yet handle the problem of determining the correct insertion point
1590
+ # for inserted nodes in the case of patterns with multiple output-nodes. The following
1591
+ # is sufficient for patterns with a single output-node "node", which can serve as the
1592
+ # insertion-point.
1593
+ onnxscript.optimizer.basic_constant_propagation(delta.new_nodes)
1594
+ _convenience.replace_nodes_and_values(
1595
+ graph_or_function,
1596
+ node,
1597
+ delta.match.nodes if rule.remove_nodes else [],
1598
+ delta.new_nodes,
1599
+ delta.match.outputs,
1600
+ delta.new_outputs,
1601
+ )
1602
+ count += 1
1603
+ if rule.graph_post_visitor:
1604
+ rule.graph_post_visitor()
1605
+
1606
+ return count
1607
+
1608
+ def apply_to_model(
1609
+ self, model: ir.Model, *, verbose: int | None = None, debug: bool = False
1610
+ ) -> int:
1611
+ """Apply the rewrite rules in the set to the model.
1612
+
1613
+ Args:
1614
+ model: The model to which the rewrite rules are applied.
1615
+ verbose: The verbosity level of messages. Defaults to None.
1616
+ debug: Whether to enable debugging. Defaults to False. In the
1617
+ debug mode, no changes are made to the model, only a report is produced at
1618
+ the end about the best matches found.
1619
+
1620
+ Returns:
1621
+ The number of applications of rewrite rules.
1622
+ """
1623
+ assert isinstance(model, ir.Model)
1624
+ tracer = MatchingTracer() if debug else None
1625
+ onnxscript.optimizer.basic_constant_propagation(model.graph)
1626
+ count = self._apply_to_graph_or_function(
1627
+ model, model.graph, verbose=verbose, tracer=tracer
1628
+ )
1629
+ for function in model.functions.values():
1630
+ onnxscript.optimizer.basic_constant_propagation(function)
1631
+ count += self._apply_to_graph_or_function(
1632
+ model, function, verbose=verbose, tracer=tracer
1633
+ )
1634
+ if tracer:
1635
+ tracer.report()
1636
+ return count
1637
+
1638
+ def __iter__(self):
1639
+ yield from self.rules
1640
+
1641
+
1642
+ class MatchStatus(enum.IntEnum):
1643
+ """The status of a pattern-matching operation."""
1644
+
1645
+ NO_MATCH = 0 # No successful match found for entire pattern graph
1646
+ CONDITION_FAILED = 1 # Subsequent validation check failed
1647
+ REPLACEMENT_FAILED = 2 # Replacement subgraph could not be created
1648
+ SUCCESS = 3 # A successful match was found
1649
+
1650
+
1651
+ @dataclasses.dataclass
1652
+ class MatchInfo:
1653
+ """The status of a pattern-matching operation. An extension of MatchResult."""
1654
+
1655
+ match_result: MatchResult
1656
+ root_node: ir.Node
1657
+ container: ir.Graph | ir.Function
1658
+ status: MatchStatus
1659
+
1660
+ def score(self) -> int:
1661
+ """Return a score for the match."""
1662
+ return len(self.match_result.nodes) + int(self.status.value) * 100
1663
+
1664
+
1665
+ class MatchingTracer:
1666
+ """A debugging helper class to trace the matching of a pattern against a graph.
1667
+
1668
+ This is used to track the best matches found for each rule, and to report the
1669
+ results at the end of the matching.
1670
+ """
1671
+
1672
+ def __init__(self) -> None:
1673
+ self._log: dict[RewriteRule, list[MatchInfo]] = defaultdict(list)
1674
+
1675
+ def log(
1676
+ self,
1677
+ rule: RewriteRule,
1678
+ container: ir.Graph | ir.Function,
1679
+ node: ir.Node,
1680
+ match_result: MatchResult,
1681
+ status: MatchStatus,
1682
+ ) -> None:
1683
+ this_match = MatchInfo(match_result, node, container, status)
1684
+ this_score = this_match.score()
1685
+ if this_score == 0:
1686
+ return
1687
+ best_matches = self._log[rule]
1688
+ if best_matches:
1689
+ if this_score < best_matches[0].score():
1690
+ return
1691
+ if this_score > best_matches[0].score():
1692
+ best_matches.clear()
1693
+ best_matches.append(this_match)
1694
+
1695
+ def report(self) -> None:
1696
+ import onnxscript.rewriter._ir_utils as ir_utils
1697
+
1698
+ print("===")
1699
+ for rule, matches in self._log.items():
1700
+ if not matches:
1701
+ continue
1702
+ print(f"Rule: {rule}")
1703
+ print(f"Best score: {matches[0].score()}")
1704
+ for match in matches:
1705
+ print(f"Status: {match.status}")
1706
+ if match.status == MatchStatus.NO_MATCH:
1707
+ print("Graph matching failed: " + match.match_result.reason)
1708
+ node = match.match_result._failure_node
1709
+ if node:
1710
+ print("Failure at or around node:")
1711
+ node.display()
1712
+ print("Matched nodes:")
1713
+ ir_utils.display_nodes(match.match_result.nodes)
1714
+ print("===")