Files
wehub-resource-sync 26446540fa
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

558 lines
18 KiB
ReStructuredText

.. Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
.. http://www.apache.org/licenses/LICENSE-2.0
.. Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
.. _relax-dpl:
Dataflow Pattern Language (DPL)
===============================
The Dataflow Pattern Language (DPL) is Relax's built-in facility for
**pattern matching and rewriting** on computation graphs. It lets you describe a
sub-graph structure you are looking for, search for it inside a Relax function,
and optionally replace it with a new structure -- all without hand-writing a
full IR visitor.
DPL is used throughout the TVM stack:
- **Operator fusion** -- ``FuseOpsByPattern`` groups matched operators into a
single fused function.
- **Backend dispatch** -- CUTLASS, cuBLAS, cuDNN and other backends register
patterns so the compiler can route sub-graphs to optimized library kernels.
- **Custom graph transforms** -- users write their own patterns and rewriters
to perform project-specific optimizations.
The typical workflow has three steps:
1. **Build a pattern** that describes the sub-graph shape (e.g. ``matmul`` followed
by ``add``).
2. **Match** the pattern against Relax IR to locate all occurrences.
3. **Rewrite** each match into a replacement expression.
The public API lives in ``tvm.relax.dpl`` (source: ``python/tvm/relax/dpl/``).
Building Patterns
-----------------
A *pattern* is a lightweight description of what an expression should look like.
Patterns are built by combining small building blocks.
Basic Patterns
~~~~~~~~~~~~~~
The most common leaf patterns are:
- ``wildcard()`` -- matches any expression.
- ``is_op("relax.add")`` -- matches a specific Relax operator.
- ``is_const()`` -- matches any constant value.
- ``is_var(name)`` -- matches a ``Var`` node (optionally with a given name).
- ``is_dfv(name)`` -- matches a ``DataflowVar`` node.
- ``is_gv(name)`` -- matches a ``GlobalVar``.
.. code:: python
from tvm.relax.dpl import wildcard, is_op, is_const
# Match any relax.add call, regardless of arguments
add_pattern = is_op("relax.add")(wildcard(), wildcard())
Call Patterns
~~~~~~~~~~~~~
Calling a pattern as a function produces a ``CallPattern``. The callee is the
pattern itself, and the positional arguments are patterns for each operand:
.. code:: python
x = wildcard()
w = wildcard()
# Match: relax.matmul(x, w)
matmul = is_op("relax.matmul")(x, w)
For operators with variadic arguments, pass ``varg_default_wildcard=True`` so
that extra arguments are matched by implicit wildcards:
.. code:: python
# Match relax.concat with any number of inputs
concat = is_op("relax.concat")(wildcard(), varg_default_wildcard=True)
DPL also provides specialized helpers for common call patterns:
- ``is_call_tir(func_name, args)`` -- matches ``R.call_tir(func_name, (args...,))``.
- ``is_call_dps_packed(func_name, args)`` -- matches ``R.call_dps_packed``.
- ``is_call_packed(func_name, args)`` -- matches ``R.call_packed``.
.. code:: python
from tvm.relax.dpl import is_call_tir, wildcard
# Match a call_tir that calls the function "decode"
decode = is_call_tir("decode", args=[wildcard(), wildcard()])
Tuple Patterns
~~~~~~~~~~~~~~
``TuplePattern`` matches a Relax tuple with a fixed number of fields.
It supports indexing with ``[]`` to create ``TupleGetItemPattern``:
.. code:: python
from tvm.relax.dpl import is_tuple, wildcard
a, b = wildcard(), wildcard()
tup = is_tuple([a, b])
# Match: getting the first element from the tuple
first = tup[0]
Constraints
~~~~~~~~~~~
Any pattern can be further narrowed by attaching constraints:
- ``.has_dtype(dtype)`` -- the matched expression must have the given data type.
- ``.has_shape(shape)`` -- the matched expression must have the given shape.
- ``.has_attr(attrs)`` -- the matched call must carry the given attributes.
- ``.has_ty(ty)`` -- the matched expression must have the given type.
.. code:: python
# Match a float16 matmul
fp16_matmul = is_op("relax.matmul")(wildcard(), wildcard()).has_dtype("float16")
Logical Combinators
~~~~~~~~~~~~~~~~~~~
Patterns can be combined with logical operators:
- ``pat_a | pat_b`` -- match if **either** pattern matches (``OrPattern``).
- ``pat_a & pat_b`` -- match if **both** patterns match (``AndPattern``).
- ``~pat`` -- match anything **except** ``pat`` (``NotPattern``).
.. code:: python
# Match either relu or gelu activation
activation = is_op("relax.nn.relu")(wildcard()) | is_op("relax.nn.gelu")(wildcard())
Sequence Patterns
~~~~~~~~~~~~~~~~~
When a pattern spans multiple bindings inside a ``DataflowBlock``, use
*sequence operators* to express producer-consumer relationships:
- ``a ^ b`` (``used_by``) -- ``a`` is used by ``b`` (``a`` may also be used
elsewhere).
- ``a >> b`` (``only_used_by``) -- ``a`` is **only** used by ``b`` (no other
consumers).
These return a ``PatternSeq`` that can be chained:
.. code:: python
x = wildcard()
matmul = is_op("relax.matmul")(x, wildcard())
add = is_op("relax.add")(matmul, wildcard())
# matmul result is exclusively consumed by the add
seq = matmul >> add
High-level Helpers
~~~~~~~~~~~~~~~~~~
``make_fused_bias_activation_pattern`` builds a common
``op -> optional bias -> optional activation`` chain in one call:
.. code:: python
from tvm.relax.dpl import make_fused_bias_activation_pattern
# conv2d + bias + relu
pattern = make_fused_bias_activation_pattern(
"relax.nn.conv2d",
with_bias=True,
activation="relax.nn.relu",
)
Matching Without Rewriting
--------------------------
Sometimes you only need to **detect** a structure without replacing it.
Every ``DFPattern`` exposes two matching methods:
- ``pattern.match(expr)`` -- returns ``True`` if the pattern matches.
- ``pattern.extract_matched_expr(expr)`` -- returns a
``dict[DFPattern, Expr]`` mapping each sub-pattern to the concrete
expression it matched, or ``None`` on failure.
.. code:: python
from tvm.relax.dpl import wildcard, is_op
x = wildcard()
y = wildcard()
add_pat = is_op("relax.add")(x, y)
# Assume `expr` is a Relax expression: R.add(a, b)
if add_pat.match(expr):
matched = add_pat.extract_matched_expr(expr)
# matched[x] -> the expression that matched `x`
# matched[y] -> the expression that matched `y`
When matching across variable bindings (e.g., ``lv0 = ...; lv1 = f(lv0)``),
the matcher needs a ``var2val`` map so it can see through binding
boundaries. Use ``tvm.relax.analysis.get_var2val(func)`` to build one:
.. code:: python
from tvm.relax.analysis import get_var2val
var2val = get_var2val(func)
matched = pattern.extract_matched_expr(expr, var2val=var2val)
Rewriting Matched Patterns
--------------------------
``rewrite_call``
~~~~~~~~~~~~~~~~
``rewrite_call`` is the simplest rewrite API. It walks every expression in a
function, and when the pattern matches, it calls your callback to produce a
replacement.
.. code:: python
rewrite_call(pattern, rewriter, func) -> Function
The callback signature is:
.. code:: python
def rewriter(
matched_expr: Expr,
matchings: dict[DFPattern, Expr],
) -> Expr:
...
**Example -- replace** ``reshape(reshape(x, s1), s2)`` **with**
``reshape(x, s2)``:
.. code:: python
from tvm import relax
from tvm.relax.dpl import wildcard, is_op, rewrite_call
inp = wildcard()
shape1, shape2 = wildcard(), wildcard()
inner = is_op("relax.reshape")(inp, shape1)
outer = is_op("relax.reshape")(inner, shape2)
def rewriter(expr, matchings):
# Keep the original input but use the outermost target shape
return relax.op.reshape(matchings[inp], matchings[outer].args[1])
new_func = rewrite_call(outer, rewriter, func)
``rewrite_call`` is best for **local, single-expression** rewrites.
``rewrite_bindings`` with ``PatternContext``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When a rewrite involves **multiple bindings** across a ``DataflowBlock``
(e.g., merging three separate matmuls into one), use ``rewrite_bindings``
together with ``PatternContext``.
``PatternContext`` enables topological (graph-level) matching on an entire
dataflow block rather than on individual expressions.
.. code:: python
rewrite_bindings(ctx, rewriter, func) -> Function
The callback receives *variables* rather than expressions:
.. code:: python
def rewriter(
matchings: dict[DFPattern, Var],
bindings: dict[Var, Expr],
) -> dict[Var, Expr]:
...
- ``matchings[pat]`` returns the **bound variable** (``Var``) whose right-hand
side matched ``pat``. The ``Var`` itself carries ``ty`` and can be
used directly in new expressions.
- ``bindings`` maps each ``Var`` to its bound ``Expr`` (the right-hand side),
useful when you need to inspect the original expression.
**Example -- merge three parallel matmuls into one**:
.. code:: python
from tvm.script import relax as R
from tvm.relax.dpl import wildcard, is_op, rewrite_bindings, PatternContext
with PatternContext() as ctx:
inp_pat = wildcard()
w1, w2, w3 = wildcard(), wildcard(), wildcard()
matmul1 = is_op("relax.matmul")(inp_pat, w1)
matmul2 = is_op("relax.matmul")(inp_pat, w2)
matmul3 = is_op("relax.matmul")(inp_pat, w3)
def rewriter(matchings, _bindings):
inp = matchings[inp_pat]
W1 = matchings[w1]
W2 = matchings[w2]
W3 = matchings[w3]
width = W1.ty.shape[1]
concat_w = R.concat([W1, W2, W3], axis=1)
merged = R.matmul(inp, concat_w)
return {
matchings[matmul1]: R.strided_slice(
merged, axes=[2], begin=[0], end=[width],
),
matchings[matmul2]: R.strided_slice(
merged, axes=[2], begin=[width], end=[width * 2],
),
matchings[matmul3]: R.strided_slice(
merged, axes=[2], begin=[width * 2], end=[width * 3],
),
}
new_func = rewrite_bindings(ctx, rewriter, func)
Declarative Rewriting with ``@R.rewriter``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For straightforward one-to-one replacements you can declare the pattern and
its replacement as two Relax functions in a single ``IRModule``. The
``@R.rewriter`` decorator turns the module into a ``PatternMatchingRewriter``
object that can be applied directly.
.. code:: python
from tvm.script import relax as R
@R.rewriter
class RewriteAddToPackedCall:
@R.function
def pattern(
A: R.Tensor([16], "float32"),
B: R.Tensor([16], "float32"),
):
C = R.add(A, B)
return C
@R.function
def replacement(
A: R.Tensor([16], "float32"),
B: R.Tensor([16], "float32"),
):
C = R.call_pure_packed(
"my_fast_add",
A,
B,
ty_args=R.Tensor([16], "float32"),
)
return C
# Apply to an IRModule or a single function
rewritten_mod = RewriteAddToPackedCall(mod)
Composing Rewriters
~~~~~~~~~~~~~~~~~~~
Multiple ``PatternMatchingRewriter`` objects can be combined with the ``|``
operator so they run as a single pass:
.. code:: python
combined = rewriter_a | rewriter_b
result = combined(mod)
The left-hand rewriter is tried first; the right-hand rewriter only applies to
bindings that were **not** already modified by the left.
Using DPL in Compiler Passes
-----------------------------
The most common way DPL appears in the TVM codebase is through the
``FuseOpsByPattern`` pass, which uses ``FusionPattern`` objects to drive
operator fusion.
``FusionPattern``
~~~~~~~~~~~~~~~~~
A ``FusionPattern`` bundles four pieces of information:
- ``name`` -- a string label (e.g., ``"cutlass.matmul"``).
- ``pattern`` -- a ``DFPattern`` that describes the sub-graph to match.
- ``annotation_patterns`` -- a ``dict[str, DFPattern]`` that names interesting
sub-patterns so the check function can inspect them.
- ``check`` -- an optional ``Callable[[PatternCheckContext], bool]`` that
performs additional validation after a structural match succeeds.
.. code:: python
from tvm.relax.dpl import wildcard, is_op
from tvm.relax.transform import FusionPattern
x = wildcard()
w = wildcard()
matmul = is_op("relax.matmul")(x, w)
bias = wildcard()
add = is_op("relax.add")(matmul, bias)
pattern = FusionPattern(
name="my_backend.matmul_bias",
pattern=add,
annotation_patterns={"matmul": matmul, "bias": bias, "lhs": x, "rhs": w},
check=my_check_fn,
)
``PatternCheckContext``
~~~~~~~~~~~~~~~~~~~~~~~
When ``FuseOpsByPattern`` finds a structural match, it calls the ``check``
function with a ``PatternCheckContext`` that provides:
- ``matched_expr`` -- the root expression of the match.
- ``annotated_expr`` -- a ``dict[str, Expr]`` resolved from the
``annotation_patterns``.
- ``matched_bindings`` -- a ``dict[Var, Expr]`` of bindings being fused.
- ``var_usages`` -- a ``dict[Var, Sequence[Var]]`` of variable use chains.
- ``value_to_bound_var`` -- a ``dict[Expr, Var]`` mapping values back to
their bound variables.
Use the check function to enforce constraints that cannot be expressed
structurally (dtype restrictions, shape compatibility, attribute values, etc.):
.. code:: python
from tvm.relax.transform import PatternCheckContext
def my_check_fn(ctx: PatternCheckContext) -> bool:
matmul_expr = ctx.annotated_expr["matmul"]
# Only accept float16 output
if matmul_expr.ty.dtype != "float16":
return False
return True
``FuseOpsByPattern``
~~~~~~~~~~~~~~~~~~~~
``FuseOpsByPattern`` is a module-level pass that takes a list of
``FusionPattern`` (or equivalent tuples) and groups every match into a fused
sub-function.
.. code:: python
from tvm.relax.dpl import wildcard, is_op
from tvm.relax.transform import FuseOpsByPattern
# 1. Define the pattern
w = wildcard()
x = wildcard()
wT = is_op("relax.permute_dims")(w)
o = is_op("relax.matmul")(x, wT)
annotations = {"o": o, "w": w, "x": x, "wT": wT}
def check(ctx):
transpose_call = ctx.annotated_expr["wT"]
ndim = transpose_call.args[0].ty.ndim
if ndim == -1:
return False
if ndim == 2 and transpose_call.attrs.axes is None:
return True
axes = list(range(ndim))
axes[-1], axes[-2] = axes[-2], axes[-1]
return list(transpose_call.attrs.axes) == axes
# 2. Run the pass
mod = FuseOpsByPattern(
[("transpose_matmul_fuse", o, annotations, check)],
bind_constants=False,
)(mod)
When ``annotate_codegen=True``, each fused function is additionally wrapped
with ``Codegen`` and ``global_symbol`` attributes, which is how backends like
CUTLASS and cuBLAS register themselves for external code generation.
Quick Reference
---------------
**Pattern construction**
.. list-table::
:header-rows: 1
:widths: 35 65
* - API
- Description
* - ``wildcard()``
- Match any expression
* - ``is_op(op_name)``
- Match a Relax operator by name
* - ``is_const()``
- Match any constant
* - ``is_var(name)`` / ``is_dfv(name)`` / ``is_gv(name)``
- Match ``Var`` / ``DataflowVar`` / ``GlobalVar``
* - ``is_tuple(fields)``
- Match a tuple with given field patterns
* - ``is_call_tir(name, args)``
- Match ``R.call_tir``
* - ``is_call_dps_packed(name, args)``
- Match ``R.call_dps_packed``
* - ``is_call_packed(name, args)``
- Match ``R.call_packed``
* - ``make_fused_bias_activation_pattern(...)``
- Build ``op + bias + activation`` chain
* - ``.has_dtype()`` / ``.has_shape()`` / ``.has_attr()`` / ``.has_ty()``
- Attach constraints
* - ``|`` / ``&`` / ``~``
- Or / And / Not combinators
* - ``^`` / ``>>``
- used_by / only_used_by (sequence)
**Matching and rewriting**
.. list-table::
:header-rows: 1
:widths: 35 65
* - API
- Description
* - ``pattern.match(expr)``
- Returns ``True`` if pattern matches
* - ``pattern.extract_matched_expr(expr)``
- Returns ``dict[DFPattern, Expr]`` or ``None``
* - ``rewrite_call(pattern, rewriter, func)``
- Rewrite individual expressions
* - ``rewrite_bindings(ctx, rewriter, func)``
- Rewrite across bindings in a ``DataflowBlock``
* - ``PatternMatchingRewriter.from_module(mod)``
- Declarative rewriter from ``IRModule``
* - ``@R.rewriter``
- Decorator shorthand for ``from_module``
**Pass integration**
.. list-table::
:header-rows: 1
:widths: 35 65
* - API
- Description
* - ``FusionPattern(name, pattern, annotations, check)``
- Bundle pattern with metadata for ``FuseOpsByPattern``
* - ``PatternCheckContext``
- Runtime context passed to check functions
* - ``FuseOpsByPattern(patterns, ...)``
- Module pass that fuses matched sub-graphs