onnxscript 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- onnxscript/__init__.py +131 -0
- onnxscript/_framework_apis/__init__.py +3 -0
- onnxscript/_framework_apis/torch_2_5.py +117 -0
- onnxscript/_framework_apis/torch_2_6.py +45 -0
- onnxscript/_internal/__init__.py +0 -0
- onnxscript/_internal/analysis.py +229 -0
- onnxscript/_internal/ast_utils.py +64 -0
- onnxscript/_internal/autocast.py +250 -0
- onnxscript/_internal/deprecation.py +78 -0
- onnxscript/_internal/param_manipulation.py +148 -0
- onnxscript/_internal/runtime_typing.py +43 -0
- onnxscript/_internal/utils.py +99 -0
- onnxscript/_internal/version_utils.py +118 -0
- onnxscript/_legacy_ir/__init__.py +341 -0
- onnxscript/_legacy_ir/visitor.py +937 -0
- onnxscript/_thirdparty/asciichartpy.py +313 -0
- onnxscript/backend/__init__.py +2 -0
- onnxscript/backend/onnx_backend.py +303 -0
- onnxscript/backend/onnx_export.py +885 -0
- onnxscript/converter.py +1470 -0
- onnxscript/evaluator.py +619 -0
- onnxscript/function_libs/tools/torch_lib/deduce_type_constraints.py +403 -0
- onnxscript/function_libs/tools/torch_lib/generate_aten_signatures.py +333 -0
- onnxscript/function_libs/tools/torch_lib/generate_prims_signatures.py +331 -0
- onnxscript/function_libs/torch_lib/__init__.py +12 -0
- onnxscript/function_libs/torch_lib/_constants.py +5 -0
- onnxscript/function_libs/torch_lib/_flags.py +58 -0
- onnxscript/function_libs/torch_lib/graph_building/__init__.py +56 -0
- onnxscript/function_libs/torch_lib/graph_building/_graph_building_ir.py +723 -0
- onnxscript/function_libs/torch_lib/graph_building/_graph_building_torch.py +1125 -0
- onnxscript/function_libs/torch_lib/ops/__init__.py +27 -0
- onnxscript/function_libs/torch_lib/ops/common.py +80 -0
- onnxscript/function_libs/torch_lib/ops/core.py +8935 -0
- onnxscript/function_libs/torch_lib/ops/fft.py +385 -0
- onnxscript/function_libs/torch_lib/ops/linalg.py +399 -0
- onnxscript/function_libs/torch_lib/ops/nested.py +25 -0
- onnxscript/function_libs/torch_lib/ops/nn.py +2713 -0
- onnxscript/function_libs/torch_lib/ops/prims.py +850 -0
- onnxscript/function_libs/torch_lib/ops/quantized_decomposed.py +63 -0
- onnxscript/function_libs/torch_lib/ops/sparse.py +23 -0
- onnxscript/function_libs/torch_lib/ops/special.py +387 -0
- onnxscript/function_libs/torch_lib/ops/vision.py +25 -0
- onnxscript/function_libs/torch_lib/registration.py +151 -0
- onnxscript/function_libs/torch_lib/tensor_typing.py +74 -0
- onnxscript/ir/__init__.py +153 -0
- onnxscript/ir/_convenience.py +447 -0
- onnxscript/ir/_core.py +3119 -0
- onnxscript/ir/_display.py +49 -0
- onnxscript/ir/_enums.py +163 -0
- onnxscript/ir/_graph_comparison.py +23 -0
- onnxscript/ir/_io.py +97 -0
- onnxscript/ir/_linked_list.py +276 -0
- onnxscript/ir/_metadata.py +44 -0
- onnxscript/ir/_name_authority.py +72 -0
- onnxscript/ir/_polyfill.py +25 -0
- onnxscript/ir/_protocols.py +598 -0
- onnxscript/ir/_schemas.py +548 -0
- onnxscript/ir/_tape.py +120 -0
- onnxscript/ir/_type_casting.py +106 -0
- onnxscript/ir/convenience.py +32 -0
- onnxscript/ir/external_data.py +396 -0
- onnxscript/ir/passes/__init__.py +33 -0
- onnxscript/ir/passes/_pass_infra.py +172 -0
- onnxscript/ir/serde.py +1620 -0
- onnxscript/ir/tensor_adapters.py +122 -0
- onnxscript/ir/traversal.py +82 -0
- onnxscript/irbuilder.py +542 -0
- onnxscript/main.py +167 -0
- onnxscript/onnx_opset/__init__.py +232 -0
- onnxscript/onnx_opset/_impl/opset1.py +4100 -0
- onnxscript/onnx_opset/_impl/opset10.py +1227 -0
- onnxscript/onnx_opset/_impl/opset11.py +4013 -0
- onnxscript/onnx_opset/_impl/opset12.py +1078 -0
- onnxscript/onnx_opset/_impl/opset13.py +3924 -0
- onnxscript/onnx_opset/_impl/opset14.py +999 -0
- onnxscript/onnx_opset/_impl/opset15.py +604 -0
- onnxscript/onnx_opset/_impl/opset16.py +1255 -0
- onnxscript/onnx_opset/_impl/opset17.py +561 -0
- onnxscript/onnx_opset/_impl/opset18.py +1803 -0
- onnxscript/onnx_opset/_impl/opset19.py +1942 -0
- onnxscript/onnx_opset/_impl/opset2.py +218 -0
- onnxscript/onnx_opset/_impl/opset20.py +675 -0
- onnxscript/onnx_opset/_impl/opset21.py +1976 -0
- onnxscript/onnx_opset/_impl/opset22.py +2588 -0
- onnxscript/onnx_opset/_impl/opset3.py +199 -0
- onnxscript/onnx_opset/_impl/opset4.py +77 -0
- onnxscript/onnx_opset/_impl/opset5.py +84 -0
- onnxscript/onnx_opset/_impl/opset6.py +944 -0
- onnxscript/onnx_opset/_impl/opset7.py +1243 -0
- onnxscript/onnx_opset/_impl/opset8.py +444 -0
- onnxscript/onnx_opset/_impl/opset9.py +1485 -0
- onnxscript/onnx_opset/_impl/opset_ai_onnx_ml1.py +974 -0
- onnxscript/onnx_opset/_impl/opset_ai_onnx_ml2.py +112 -0
- onnxscript/onnx_opset/_impl/opset_ai_onnx_ml3.py +308 -0
- onnxscript/onnx_opset/_impl/opset_ai_onnx_ml4.py +129 -0
- onnxscript/onnx_opset/_impl/opset_ai_onnx_ml5.py +158 -0
- onnxscript/onnx_opset/_impl/opset_ai_onnx_preview_training1.py +577 -0
- onnxscript/onnx_types.py +229 -0
- onnxscript/optimizer/__init__.py +39 -0
- onnxscript/optimizer/_constant_folding.py +1083 -0
- onnxscript/optimizer/_inliner.py +312 -0
- onnxscript/optimizer/_legacy/_optimizer.py +98 -0
- onnxscript/optimizer/_legacy/_remove_unused_proto.py +144 -0
- onnxscript/optimizer/_legacy/_simple_function_folding.py +243 -0
- onnxscript/optimizer/_legacy/constant_folding.py +293 -0
- onnxscript/optimizer/_legacy/evaluator.py +439 -0
- onnxscript/optimizer/_optimizer.py +61 -0
- onnxscript/optimizer/_remove_unused.py +106 -0
- onnxscript/optimizer/_remove_unused_function.py +72 -0
- onnxscript/py.typed +1 -0
- onnxscript/rewriter/__init__.py +56 -0
- onnxscript/rewriter/_ir_utils.py +111 -0
- onnxscript/rewriter/broadcast_to_matmul.py +180 -0
- onnxscript/rewriter/cast_constant_of_shape.py +50 -0
- onnxscript/rewriter/collapse_slices.py +141 -0
- onnxscript/rewriter/erfgelu.py +27 -0
- onnxscript/rewriter/function_rule.py +232 -0
- onnxscript/rewriter/gemm_to_matmul_add.py +21 -0
- onnxscript/rewriter/generic_pattern.py +700 -0
- onnxscript/rewriter/llama_rule_sets.py +287 -0
- onnxscript/rewriter/no_op.py +56 -0
- onnxscript/rewriter/onnxruntime/__init__.py +50 -0
- onnxscript/rewriter/onnxruntime/bfloat16_utils/bfloat16_converter.py +99 -0
- onnxscript/rewriter/onnxruntime/fused_matmul_rule_sets.py +177 -0
- onnxscript/rewriter/onnxruntime/group_normalization_merge_silu.py +64 -0
- onnxscript/rewriter/onnxruntime/instance_to_group_normalization.py +155 -0
- onnxscript/rewriter/onnxruntime/softmax.py +63 -0
- onnxscript/rewriter/onnxruntime/transformers/__init__.py +21 -0
- onnxscript/rewriter/onnxruntime/transformers/biassplitgelu.py +31 -0
- onnxscript/rewriter/onnxruntime/transformers/fastgelu.py +29 -0
- onnxscript/rewriter/onnxruntime/transformers/layernorm.py +47 -0
- onnxscript/rewriter/onnxruntime/transformers/multihead_attention.py +715 -0
- onnxscript/rewriter/ort_fusions/__init__.py +9 -0
- onnxscript/rewriter/ort_fusions/_core.py +28 -0
- onnxscript/rewriter/ort_fusions/_smollm_1.py +253 -0
- onnxscript/rewriter/ort_fusions/_smollm_2.py +467 -0
- onnxscript/rewriter/ort_fusions/_test_models.py +122 -0
- onnxscript/rewriter/ort_fusions/_test_utils.py +42 -0
- onnxscript/rewriter/ort_fusions/cos_sin_cache.py +154 -0
- onnxscript/rewriter/ort_fusions/gqa.py +156 -0
- onnxscript/rewriter/ort_fusions/mha.py +198 -0
- onnxscript/rewriter/ort_fusions/rms_normalization.py +95 -0
- onnxscript/rewriter/ort_fusions/rotary_embedding.py +64 -0
- onnxscript/rewriter/ort_fusions/sdpa.py +75 -0
- onnxscript/rewriter/ort_fusions/skip_normalization.py +46 -0
- onnxscript/rewriter/pattern.py +1714 -0
- onnxscript/rewriter/testing.py +77 -0
- onnxscript/sourceinfo.py +59 -0
- onnxscript/tensor.py +227 -0
- onnxscript/testing/__init__.py +482 -0
- onnxscript/tools/__init__.py +4 -0
- onnxscript/tools/benchmark/__init__.py +23 -0
- onnxscript/tools/benchmark/benchmark_helpers.py +783 -0
- onnxscript/tools/benchmark/benchmark_run.py +140 -0
- onnxscript/tools/benchmark/export_model.py +207 -0
- onnxscript/tools/benchmark/export_model_batch.py +146 -0
- onnxscript/tools/memory_peak.py +244 -0
- onnxscript/tools/training_helper.py +50 -0
- onnxscript/tools/transformers_models/__init__.py +190 -0
- onnxscript/tools/transformers_models/llama.py +168 -0
- onnxscript/tools/transformers_models/mistral.py +238 -0
- onnxscript/tools/transformers_models/phi.py +248 -0
- onnxscript/tools/transformers_models/phi3.py +259 -0
- onnxscript/type_annotation.py +281 -0
- onnxscript/utils/__init__.py +0 -0
- onnxscript/utils/evaluation_utils.py +56 -0
- onnxscript/utils/timing_utils.py +33 -0
- onnxscript/utils/utils.py +84 -0
- onnxscript/values.py +790 -0
- onnxscript/version_converter/__init__.py +21 -0
- onnxscript/version_converter/_version_converter.py +314 -0
- onnxscript-0.1.0.dist-info/LICENSE +21 -0
- onnxscript-0.1.0.dist-info/METADATA +370 -0
- onnxscript-0.1.0.dist-info/RECORD +176 -0
- onnxscript-0.1.0.dist-info/WHEEL +5 -0
- onnxscript-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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("===")
|