chore: import upstream snapshot with attribution
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:25 +08:00
commit 26446540fa
3151 changed files with 974126 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
.. 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-abstraction:
Graph Abstraction for ML Models
-------------------------------
Graph abstraction is a key technique used in machine learning (ML) compilers
to represent and reason about the structure and data flow of ML models. By
abstracting the model into a graph representation, the compiler can perform
various optimizations to improve performance and efficiency. This tutorial will
cover the basics of graph abstraction, its key elements of Relax IR, and how it enables optimization in ML compilers.
What is Graph Abstraction?
~~~~~~~~~~~~~~~~~~~~~~~~~~
Graph abstraction is the process of representing an ML model as a directed graph,
where the nodes represent computational operations (e.g., matrix multiplication,
convolution) and the edges represent the flow of data between these operations.
This abstraction allows the compiler to analyze the dependencies and
relationships between different parts of the model.
.. code:: python
from tvm.script import relax as R
@R.function
def main(
x: R.Tensor((1, 784), dtype="float32"),
weight: R.Tensor((784, 256), dtype="float32"),
bias: R.Tensor((256,), dtype="float32"),
) -> R.Tensor((1, 256), dtype="float32"):
with R.dataflow():
lv0 = R.matmul(x, weight)
lv1 = R.add(lv0, bias)
gv = R.nn.relu(lv1)
R.output(gv)
return gv
Key Features of Relax
~~~~~~~~~~~~~~~~~~~~~
Relax, the graph representation utilized in Apache TVM,
facilitates end-to-end optimization of ML models through several crucial
features:
- **First-class symbolic shape**: Relax employs symbolic shapes to represent
tensor dimensions, enabling global tracking of dynamic shape relationships
across tensor operators and function calls.
- **Multi-level abstractions**: Relax supports cross-level abstractions, from
high-level neural network layers to low-level tensor operations, enabling
optimizations that span different hierarchies within the model.
- **Composable transformations**: Relax offers a framework for composable
transformations that can be selectively applied to different model components.
This includes capabilities such as partial lowering and partial specialization,
providing flexible customization and optimization options.
These features collectively empower Relax to offer a powerful and adaptable approach
to ML model optimization within the Apache TVM ecosystem.
+557
View File
@@ -0,0 +1,557 @@
.. 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
+35
View File
@@ -0,0 +1,35 @@
.. 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-deep-dive:
Relax
=====
Relax is a high-level abstraction for graph optimization and transformation in Apache TVM stack.
Additionally, Apache TVM combines Relax and TensorIR together for cross-level
optimization. Hence, Relax is usually working closely with TensorIR for representing and optimizing
the whole IRModule
.. toctree::
:maxdepth: 2
abstraction
learning
dpl
tutorials/relax_creation
tutorials/relax_transformation
+276
View File
@@ -0,0 +1,276 @@
.. 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-learning:
Understand Relax Abstraction
============================
Relax is a graph abstraction used in Apache TVM, which
helps to end-to-end optimize ML models. The principal objective of Relax
is to depict the structure and data flow of ML models, including the
dependencies and relationships between different parts of the model, as
well as how to execute the model on hardware.
End to End Model Execution
--------------------------
In this chapter, we will use the following model as an example. This is
a two-layer neural network that consists of two linear operations with
relu activation.
.. image:: /_static/img/e2e_fashionmnist_mlp_model.png
:width: 85%
:align: center
High-Level Operations Representation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Let us begin by reviewing a Numpy implementation of the model.
.. code:: python
def numpy_mlp(data, w0, b0, w1, b1):
lv0 = data @ w0 + b0
lv1 = np.maximum(lv0, 0)
lv2 = lv1 @ w1 + b1
return lv2
The above example code shows the high-level array operations to perform the end-to-end model
execution. Of course, we can rewrite the above code using Relax as follows:
.. code:: python
from tvm.script import relax as R
@R.function
def relax_mlp(
data: R.Tensor(("n", 784), dtype="float32"),
w0: R.Tensor((784, 128), dtype="float32"),
b0: R.Tensor((128,), dtype="float32"),
w1: R.Tensor((128, 10), dtype="float32"),
b1: R.Tensor((10,), dtype="float32"),
) -> R.Tensor(("n", 10), dtype="float32"):
with R.dataflow():
lv0 = R.matmul(data, w0) + b0
lv1 = R.nn.relu(lv0)
lv2 = R.matmul(lv1, w1) + b1
R.output(lv2)
return lv2
Low-Level Integration
~~~~~~~~~~~~~~~~~~~~~
However, again from the pov of machine learning compilation (MLC), we would like to see
through the details under the hood of these array computations.
For the purpose of illustrating details under the hood, we will again write examples in low-level numpy:
We will use a loop instead of array functions when necessary to demonstrate the possible loop computations.
When possible, we always explicitly allocate arrays via numpy.empty and pass them around.
The code block below shows a low-level numpy implementation of the same model.
.. code:: python
def lnumpy_linear(X: np.ndarray, W: np.ndarray, B: np.ndarray, Z: np.ndarray):
n, m, K = X.shape[0], W.shape[1], X.shape[1]
Y = np.empty((n, m), dtype="float32")
for i in range(n):
for j in range(m):
for k in range(K):
if k == 0:
Y[i, j] = 0
Y[i, j] = Y[i, j] + X[i, k] * W[k, j]
for i in range(n):
for j in range(m):
Z[i, j] = Y[i, j] + B[j]
def lnumpy_relu0(X: np.ndarray, Y: np.ndarray):
n, m = X.shape
for i in range(n):
for j in range(m):
Y[i, j] = np.maximum(X[i, j], 0)
def lnumpy_mlp(data, w0, b0, w1, b1):
n = data.shape[0]
lv0 = np.empty((n, 128), dtype="float32")
lnumpy_linear(data, w0, b0, lv0)
lv1 = np.empty((n, 128), dtype="float32")
lnumpy_relu0(lv0, lv1)
out = np.empty((n, 10), dtype="float32")
lnumpy_linear(lv1, w1, b1, out)
return out
With the low-level NumPy example in mind, now we are ready to introduce an Relax abstraction
for the end-to-end model execution. The code block below shows a TVMScript implementation of the model.
.. code:: python
from tvm.script import ir as I
from tvm.script import tirx as T
from tvm.script import relax as R
@I.ir_module
class Module:
@T.prim_func(private=True)
def linear(x: T.handle, w: T.handle, b: T.handle, z: T.handle):
M, N, K = T.int64(), T.int64(), T.int64()
X = T.match_buffer(x, (M, K), "float32")
W = T.match_buffer(w, (K, N), "float32")
B = T.match_buffer(b, (N,), "float32")
Z = T.match_buffer(z, (M, N), "float32")
Y = T.alloc_buffer((M, N), "float32")
for i, j, k in T.grid(M, N, K):
with T.sblock("Y"):
v_i, v_j, v_k = T.axis.remap("SSR", [i, j, k])
with T.init():
Y[v_i, v_j] = T.float32(0.0)
Y[v_i, v_j] = Y[v_i, v_j] + X[v_i, v_k] * W[v_k, v_j]
for i, j in T.grid(M, N):
with T.sblock("Z"):
v_i, v_j = T.axis.remap("SS", [i, j])
Z[v_i, v_j] = Y[v_i, v_j] + B[v_j]
@T.prim_func(private=True)
def relu(x: T.handle, y: T.handle):
M, N = T.int64(), T.int64()
X = T.match_buffer(x, (M, N), "float32")
Y = T.match_buffer(y, (M, N), "float32")
for i, j in T.grid(M, N):
with T.sblock("Y"):
v_i, v_j = T.axis.remap("SS", [i, j])
Y[v_i, v_j] = T.max(X[v_i, v_j], T.float32(0.0))
@R.function
def main(
x: R.Tensor(("n", 784), dtype="float32"),
w0: R.Tensor((784, 256), dtype="float32"),
b0: R.Tensor((256,), dtype="float32"),
w1: R.Tensor((256, 10), dtype="float32"),
b1: R.Tensor((10,), dtype="float32")
) -> R.Tensor(("n", 10), dtype="float32"):
cls = Module
n = T.int64()
with R.dataflow():
lv = R.call_tir(cls.linear, (x, w0, b0), out_ty=R.Tensor((n, 256), dtype="float32"))
lv1 = R.call_tir(cls.relu, (lv,), out_ty=R.Tensor((n, 256), dtype="float32"))
lv2 = R.call_tir(cls.linear, (lv1, w1, b1), out_ty=R.Tensor((n, 10), dtype="float32"))
R.output(lv2)
return lv2
The above code contains kinds of functions: the primitive tensor functions (``T.prim_func``) and a
``R.function`` (relax function). Relax function is a new type of abstraction representing
high-level neural network executions.
Note that the above relax module natively supports symbolic shapes, see the ``"n"`` in the
tensor shapes in ``main`` function and ``M``, ``N``, ``K`` in the ``linear`` function. This is
a key feature of Relax abstraction, which enables the compiler to track dynamic shape relations
globally across tensor operators and function calls.
Again it is helpful to see the TVMScript code and low-level numpy code side-by-side and check the
corresponding elements, and we are going to walk through each of them in detail. Since we already
learned about primitive tensor functions, we are going to focus on the high-level execution part.
Key Elements of Relax
---------------------
This section will introduce the key elements of Relax abstraction and how it enables optimization
in ML compilers.
Type
~~~~
Type is the Relax representation of expression type information. It can
be ``TensorType``, ``TupleType``, etc. In the above example, we use ``TensorType``
(short in ``R.Tensor`` in TVMScript) to represent the shape and dtype of the tensor of the inputs,
outputs, and intermediate results.
R.call_tir
~~~~~~~~~~
The ``R.call_tir`` function is a new abstraction in Relax that allows calling primitive tensor
functions in the same IRModule. This is a key feature of Relax that enables cross-level
abstractions, from high-level neural network layers to low-level tensor operations.
Taking one line from the above code as an example:
.. code:: python
lv = R.call_tir(cls.linear, (x, w0, b0), out_ty=R.Tensor((n, 256), dtype="float32"))
To explain what does ``R.call_tir`` work, let us review an equivalent low-level numpy
implementation of the operation, as follows:
.. code:: python
lv0 = np.empty((n, 256), dtype="float32")
lnumpy_linear(x, w0, b0, lv0)
Specifically, ``call_tir`` allocates an output tensor res, then pass the inputs and the output
to the prim_func. After executing prim_func the result is populated in res, then we can return
the result.
This convention is called **destination passing**, The idea is that input and output are explicitly
allocated outside and passed to the low-level primitive function. This style is commonly used
in low-level library designs, so higher-level frameworks can handle that memory allocation
decision. Note that not all tensor operations can be presented in this style (specifically,
there are operations whose output shape depends on the input). Nevertheless, in common practice,
it is usually helpful to write the low-level function in this style when possible.
Dataflow Block
~~~~~~~~~~~~~~
Another important element in a relax function is the R.dataflow() scope annotation.
.. code:: python
with R.dataflow():
lv = R.call_tir(cls.linear, (x, w0, b0), out_ty=R.Tensor((n, 256), dtype="float32"))
lv1 = R.call_tir(cls.relu, (lv,), out_ty=R.Tensor((n, 256), dtype="float32"))
lv2 = R.call_tir(cls.linear, (lv1, w1, b1), out_ty=R.Tensor((n, 10), dtype="float32"))
R.output(lv2)
Before we talk about the dataflow block, let us first introduce the concept of **pure** and
**side-effect**. A function is **pure** or **side-effect free** if:
- it only reads from its inputs and returns the result via its output
- it will not change other parts of the program (such as incrementing a global counter).
For example, all ``R.call_tir`` functions are pure functions, as they only read from their inputs
and write the output to another new allocated tensor. However, the **inplace operations** are not
pure functions, in other words, they are side-effect functions, because they will change the existing
intermediate or input tensors.
A dataflow block is a way for us to mark the computational graph regions of the program.
Specifically, within a dataflow block, all the operations need to be **side-effect free**.
Outside a dataflow block, the operations can contain side-effect.
.. note::
A common question that arises is why we need to manually mark dataflow blocks instead of
automatically inferring them. There are two main reasons for this approach:
- Automatic inference of dataflow blocks can be challenging and imprecise, particularly
when dealing with calls to packed functions (such as cuBLAS integrations). By manually
marking dataflow blocks, we enable the compiler to accurately understand and optimize
the program's dataflow.
- Many optimizations can only be applied within dataflow blocks. For instance, fusion
optimization is limited to operations within a single dataflow block. If the compiler
were to incorrectly infer dataflow boundaries, it might miss crucial optimization
opportunities, potentially impacting the program's performance.
By allowing manual marking of dataflow blocks, we ensure that the compiler has the most
accurate information to work with, leading to more effective optimizations.
@@ -0,0 +1,2 @@
Deep Dive: Relax
----------------
@@ -0,0 +1,285 @@
# 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.
# ruff: noqa: E402
"""
.. _relax-creation:
Relax Creation
==============
This tutorial demonstrates how to create Relax functions and programs.
We'll cover various ways to define Relax functions, including using TVMScript,
and relax NNModule API.
"""
######################################################################
# Create Relax programs using TVMScript
# -------------------------------------
# TVMScript is a domain-specific language for representing Apache TVM's
# intermediate representation (IR). It is a Python dialect that can be used
# to define an IRModule, which contains both TensorIR and Relax functions.
#
# In this section, we will show how to define a simple MLP model with only
# high-level Relax operators using TVMScript.
from tvm import relax, topi
from tvm.script import ir as I
from tvm.script import relax as R
from tvm.script import tirx as T
@I.ir_module
class RelaxModule:
@R.function
def forward(
data: R.Tensor(("n", 784), dtype="float32"),
w0: R.Tensor((128, 784), dtype="float32"),
b0: R.Tensor((128,), dtype="float32"),
w1: R.Tensor((10, 128), dtype="float32"),
b1: R.Tensor((10,), dtype="float32"),
) -> R.Tensor(("n", 10), dtype="float32"):
with R.dataflow():
lv0 = R.matmul(data, R.permute_dims(w0)) + b0
lv1 = R.nn.relu(lv0)
lv2 = R.matmul(lv1, R.permute_dims(w1)) + b1
R.output(lv2)
return lv2
RelaxModule.show()
######################################################################
# Relax is not only a graph-level IR, but also supports cross-level
# representation and transformation. To be specific, we can directly call
# TensorIR functions in Relax function.
@I.ir_module
class RelaxModuleWithTIR:
@T.prim_func(s_tir=True)
def relu(x: T.handle, y: T.handle):
n = T.int64()
m = T.int64()
X = T.match_buffer(x, (n, m), "float32")
Y = T.match_buffer(y, (n, m), "float32")
for i, j in T.grid(n, m):
with T.sblock("relu"):
vi, vj = T.axis.remap("SS", [i, j])
Y[vi, vj] = T.max(X[vi, vj], T.float32(0))
@R.function
def forward(
data: R.Tensor(("n", 784), dtype="float32"),
w0: R.Tensor((128, 784), dtype="float32"),
b0: R.Tensor((128,), dtype="float32"),
w1: R.Tensor((10, 128), dtype="float32"),
b1: R.Tensor((10,), dtype="float32"),
) -> R.Tensor(("n", 10), dtype="float32"):
n = T.int64()
cls = RelaxModuleWithTIR
with R.dataflow():
lv0 = R.matmul(data, R.permute_dims(w0)) + b0
lv1 = R.call_tir(cls.relu, lv0, R.Tensor((n, 128), dtype="float32"))
lv2 = R.matmul(lv1, R.permute_dims(w1)) + b1
R.output(lv2)
return lv2
RelaxModuleWithTIR.show()
######################################################################
# .. note::
#
# You may notice that the printed output is different from the written
# TVMScript code. This is because we print the IRModule in a standard
# format, while we support syntax sugar for the input
#
# For example, we can combine multiple operators into a single line, as
#
# .. code-block:: python
#
# lv0 = R.matmul(data, R.permute_dims(w0)) + b0
#
# However, the normalized expression requires only one operation in one
# binding. So the printed output is different from the written TVMScript code,
# as
#
# .. code-block:: python
#
# lv: R.Tensor((784, 128), dtype="float32") = R.permute_dims(w0, axes=None)
# lv1: R.Tensor((n, 128), dtype="float32") = R.matmul(data, lv)
# lv0: R.Tensor((n, 128), dtype="float32") = R.add(lv1, b0)
#
######################################################################
# Create Relax programs using NNModule API
# ----------------------------------------
# Besides TVMScript, we also provide a PyTorch-like API for defining neural networks.
# It is designed to be more intuitive and easier to use than TVMScript.
#
# In this section, we will show how to define the same MLP model using
# Relax NNModule API.
from tvm.relax.frontend import nn
class NNModule(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 128)
self.relu1 = nn.ReLU()
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.fc1(x)
x = self.relu1(x)
x = self.fc2(x)
return x
######################################################################
# After we define the NNModule, we can export it to TVM IRModule via
# ``export_tvm``.
mod, params = NNModule().export_tvm({"forward": {"x": nn.spec.Tensor(("n", 784), "float32")}})
mod.show()
######################################################################
# We can also insert customized function calls into the NNModule, such as
# Tensor Expression(TE), TensorIR functions or other TVM packed functions.
@T.prim_func(s_tir=True)
def tir_linear(x: T.handle, w: T.handle, b: T.handle, z: T.handle):
M = T.int64()
N = T.int64()
K = T.int64()
X = T.match_buffer(x, (M, K), "float32")
W = T.match_buffer(w, (N, K), "float32")
B = T.match_buffer(b, (N,), "float32")
Z = T.match_buffer(z, (M, N), "float32")
for i, j, k in T.grid(M, N, K):
with T.sblock("linear"):
vi, vj, vk = T.axis.remap("SSR", [i, j, k])
with T.init():
Z[vi, vj] = 0
Z[vi, vj] = Z[vi, vj] + X[vi, vk] * W[vj, vk]
for i, j in T.grid(M, N):
with T.sblock("add"):
vi, vj = T.axis.remap("SS", [i, j])
Z[vi, vj] = Z[vi, vj] + B[vj]
class NNModuleWithTIR(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
n = x.shape[0]
# We can call external functions using nn.extern
x = nn.extern(
"env.linear",
[x, self.fc1.weight, self.fc1.bias],
out=nn.Tensor.placeholder((n, 128), "float32"),
)
# We can also call TensorIR via Tensor Expression API in TOPI
x = nn.tensor_expr_op(topi.nn.relu, "relu", [x])
# We can also call other TVM packed functions
x = nn.tensor_ir_op(
tir_linear,
"tir_linear",
[x, self.fc2.weight, self.fc2.bias],
out=nn.Tensor.placeholder((n, 10), "float32"),
)
return x
mod, params = NNModuleWithTIR().export_tvm(
{"forward": {"x": nn.spec.Tensor(("n", 784), "float32")}}
)
mod.show()
######################################################################
# Create Relax programs using Block Builder API
# ---------------------------------------------
# In addition to the above APIs, we also provide a Block Builder API for
# creating Relax programs. It is a IR builder API, which is more
# low-level and widely used in TVM's internal logic, e.g writing a
# customized pass.
bb = relax.BlockBuilder()
n = T.int64()
x = relax.Var("x", R.Tensor((n, 784), "float32"))
fc1_weight = relax.Var("fc1_weight", R.Tensor((128, 784), "float32"))
fc1_bias = relax.Var("fc1_bias", R.Tensor((128,), "float32"))
fc2_weight = relax.Var("fc2_weight", R.Tensor((10, 128), "float32"))
fc2_bias = relax.Var("fc2_bias", R.Tensor((10,), "float32"))
with bb.function("forward", [x, fc1_weight, fc1_bias, fc2_weight, fc2_bias]):
with bb.dataflow():
lv0 = bb.emit(relax.op.matmul(x, relax.op.permute_dims(fc1_weight)) + fc1_bias)
lv1 = bb.emit(relax.op.nn.relu(lv0))
gv = bb.emit(relax.op.matmul(lv1, relax.op.permute_dims(fc2_weight)) + fc2_bias)
bb.emit_output(gv)
bb.emit_func_output(gv)
mod = bb.get()
mod.show()
######################################################################
# Also, Block Builder API supports building cross-level IRModule with both
# Relax functions, TensorIR functions and other TVM packed functions.
bb = relax.BlockBuilder()
with bb.function("forward", [x, fc1_weight, fc1_bias, fc2_weight, fc2_bias]):
with bb.dataflow():
lv0 = bb.emit(
relax.call_dps_packed(
"env.linear",
[x, fc1_weight, fc1_bias],
out_ty=relax.TensorType((n, 128), "float32"),
)
)
lv1 = bb.emit_te(topi.nn.relu, lv0)
tir_gv = bb.add_func(tir_linear, "tir_linear")
gv = bb.emit(
relax.call_tir(
tir_gv,
[lv1, fc2_weight, fc2_bias],
out_ty=relax.TensorType((n, 10), "float32"),
)
)
bb.emit_output(gv)
bb.emit_func_output(gv)
mod = bb.get()
mod.show()
######################################################################
# Note that the Block Builder API is not as user-friendly as the above APIs,
# but it is lowest-level API and works closely with the IR definition. We
# recommend using the above APIs for users who only want to define and
# transform a ML model. But for those who want to build more complex
# transformations, the Block Builder API is a more flexible choice.
######################################################################
# Summary
# -------
# This tutorial demonstrates how to create Relax programs using TVMScript,
# NNModule API, Block Builder API and PackedFunc API for different use cases.
@@ -0,0 +1,142 @@
# 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.
# ruff: noqa: E402
"""
.. _relax-transform:
Transformation
--------------
In this section, we will dive into the transformation of Relax programs.
Transformations is one of the key ingredients of the compilation flows
for optimizing and integrating with hardware backends.
"""
######################################################################
# Let's first create a simple Relax program as what we have done in
# the :ref:`previous section <relax-creation>`.
import tvm
from tvm import IRModule, relax
from tvm.relax.frontend import nn
class NNModule(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 128)
self.relu1 = nn.ReLU()
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.fc1(x)
x = self.relu1(x)
x = self.fc2(x)
return x
origin_mod, params = NNModule().export_tvm(
{"forward": {"x": nn.spec.Tensor(("n", 784), "float32")}}
)
origin_mod.show()
######################################################################
# Apply transformations
# ~~~~~~~~~~~~~~~~~~~~~
# Passes are the main way to apply transformations to the program.
# We can apply passes to the program. As first step, let's apply
# a built-in pass ``LegalizeOps`` to lower the high-level operators
# into low-level operators.
mod = tvm.relax.transform.LegalizeOps()(origin_mod)
mod.show()
######################################################################
# As we can see from the output, the high-level operators (aka ``relax.op``) in the program
# are replaced by their corresponding low-level operators (aka ``relax.call_tir``).
#
# Then let's trying to apply the operator fusion, which is a wide-used optimization technique
# in ML compilers. Note that in relax, fusion optimizations are done with the collaboration of
# a set of passes. We can apply them in a sequence.
mod = tvm.ir.transform.Sequential(
[
tvm.relax.transform.AnnotateTIROpPattern(),
tvm.relax.transform.FuseOps(),
tvm.relax.transform.FuseTIR(),
]
)(mod)
mod.show()
######################################################################
# As result, we can see that the ``matmul``, ``add`` and ``relu`` operators are fused
# into one kernel (aka one ``call_tir``).
#
# For all built-in passes, please refer to :py:class:`relax.transform`.
#
# Custom Passes
# ~~~~~~~~~~~~~
# We can also define our own passes. Let's take an example of rewriting the ``relu``
# operator to ``gelu`` operator.
#
# First, we need to write a Relax IR Mutator to do the rewriting.
from tvm.relax.expr_functor import PyExprMutator, mutator
@mutator
class ReluRewriter(PyExprMutator):
def __init__(self, mod):
super().__init__(mod)
def visit_call_(self, call: relax.Call) -> relax.Expr:
# visit the relax.Call expr, and only handle the case when op is relax.nn.relu
if call.op.name == "relax.nn.relu":
return relax.op.nn.gelu(call.args[0])
return super().visit_call_(call)
######################################################################
# Then we can write a pass to apply the mutator to the whole module.
@tvm.transform.module_pass(opt_level=0, name="ReluToGelu")
class ReluToGelu: # pylint: disable=too-few-public-methods
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""IRModule-level transformation"""
rewriter = ReluRewriter(mod)
for g_var, func in mod.functions_items():
if isinstance(func, relax.Function):
func = rewriter.visit_expr(func)
rewriter.builder_.update_func(g_var, func)
return rewriter.builder_.get()
mod = ReluToGelu()(origin_mod)
mod.show()
######################################################################
# The printed output shows that the ``relax.nn.relu`` operator is
# rewritten to ``relax.nn.gelu`` operator.
#
# For the details of the mutator, please refer to :py:class:`relax.expr_functor.PyExprMutator`.
#
# Summary
# ~~~~~~~
# In this section, we have shown how to apply transformations to the Relax program.
# We have also shown how to define and apply custom transformations.
+72
View File
@@ -0,0 +1,72 @@
.. 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.
.. _tir-abstraction:
Tensor Program Abstraction
--------------------------
Before we dive into the details of TensorIR, let's first introduce what is a primitive tensor
function. Primitive tensor functions are functions that correspond to a single "unit" of
computational operation. For example, a convolution operation can be a primitive tensor function,
and a fused convolution + relu operation can also be a primitive tensor function.
Usually, a typical abstraction for primitive tensor function implementation contains the following
elements: multi-dimensional buffers, loop nests that drive the tensor computations, and finally,
the compute statements themselves.
.. code:: python
from tvm.script import tirx as T
@T.prim_func
def main(
A: T.Buffer((128,), "float32"),
B: T.Buffer((128,), "float32"),
C: T.Buffer((128,), "float32"),
) -> None:
for i in range(128):
with T.sblock("C"):
vi = T.axis.spatial(128, i)
C[vi] = A[vi] + B[vi]
Key Elements of Tensor Programs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The demonstrated primitive tensor function calculates the element-wise sum of two vectors.
The function:
- Accepts three **multi-dimensional buffers** as parameters, and generates one **multi-dimensional
buffer** as output.
- Incorporates a solitary **loop nest** ``i`` that facilitates the computation.
- Features a singular **compute statement** that calculates the element-wise sum of the two
vectors.
Extra Structure in TensorIR
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Crucially, we are unable to execute arbitrary transformations on the program, as certain
computations rely on the loop's sequence. Fortunately, the majority of primitive tensor
functions we focus on possess favorable properties, such as independence among loop iterations.
For instance, the aforementioned program includes block and iteration annotations:
- The **block annotation** ``with T.sblock("C")`` signifies that the block is the fundamental
computation unit designated for scheduling. A block may encompass a single computation
statement, multiple computation statements with loops, or opaque intrinsics such as Tensor
Core instructions.
- The **iteration annotation** ``T.axis.spatial``, indicating that variable ``vi`` is mapped
to ``i``, and all iterations are independent.
While this information isn't crucial for *executing* the specific program, it proves useful when
transforming the program. Consequently, we can confidently parallelize or reorder loops associated
with ``vi``, provided we traverse all the index elements from 0 to 128.
+43
View File
@@ -0,0 +1,43 @@
.. 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.
.. _tensor-ir-deep-dive:
TensorIR
========
TensorIR is one of the core abstractions in the Apache TVM stack, used to
represent and optimize primitive tensor functions.
The TensorIR codebase consists of two modules (split from the former ``tir``):
- **tirx** — Core IR definitions and lowering (PrimFunc, Buffer, SBlock,
expressions, statements, lowering passes).
- **s_tir** (Schedulable TIR) — Schedule primitives, MetaSchedule, DLight,
and tensor intrinsics.
In TVMScript, both modules are accessed via
``from tvm.script import tirx as T``.
.. toctree::
:maxdepth: 2
abstraction
learning
tutorials/tir_creation
tutorials/tir_transformation
tutorials/dlight_gpu_scheduling
tutorials/meta_schedule
+255
View File
@@ -0,0 +1,255 @@
.. 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.
.. _tirx-learning:
Understand TensorIR Abstraction
===============================
TensorIR is the tensor program abstraction in Apache TVM, which is one of the standard
machine learning compilation frameworks. The principal objective of tensor program abstraction
is to depict loops and associated hardware acceleration options, including threading, the
application of specialized hardware instructions, and memory access.
To help our explanations, let us use the following sequence of tensor computations as
a motivating example. Specifically, for two :math:`128 \times 128` matrices ``A`` and ``B``, let us perform the
following two steps of tensor computations.
.. math::
Y_{i, j} &= \sum_k A_{i, k} \times B_{k, j} \\
C_{i, j} &= \mathbb{relu}(Y_{i, j}) = \mathbb{max}(Y_{i, j}, 0)
The above computations resemble a typical primitive tensor function commonly seen in neural networks,
a linear layer with relu activation. We use TensorIR to depict the above computations as follows.
Before we invoke TensorIR, let's use native Python codes with NumPy to show the computation:
.. code:: python
def lnumpy_mm_relu(A: np.ndarray, B: np.ndarray, C: np.ndarray):
Y = np.empty((128, 128), dtype="float32")
for i in range(128):
for j in range(128):
for k in range(128):
if k == 0:
Y[i, j] = 0
Y[i, j] = Y[i, j] + A[i, k] * B[k, j]
for i in range(128):
for j in range(128):
C[i, j] = max(Y[i, j], 0)
With the low-level NumPy example in mind, now we are ready to introduce TensorIR. The code block
below shows a TensorIR implementation of ``mm_relu``. The particular code is implemented in a
language called TVMScript, which is a domain-specific dialect embedded in python AST.
.. code:: python
from tvm.script import tirx as T
@tvm.script.ir_module
class MyModule:
@T.prim_func
def mm_relu(A: T.Buffer((128, 128), "float32"),
B: T.Buffer((128, 128), "float32"),
C: T.Buffer((128, 128), "float32")):
Y = T.alloc_buffer((128, 128), dtype="float32")
for i, j, k in T.grid(128, 128, 128):
with T.sblock("Y"):
vi = T.axis.spatial(128, i)
vj = T.axis.spatial(128, j)
vk = T.axis.reduce(128, k)
with T.init():
Y[vi, vj] = T.float32(0)
Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]
for i, j in T.grid(128, 128):
with T.sblock("C"):
vi = T.axis.spatial(128, i)
vj = T.axis.spatial(128, j)
C[vi, vj] = T.max(Y[vi, vj], T.float32(0))
Next, let's invest the elements in the above TensorIR program.
Function Parameters and Buffers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**The function parameters correspond to the same set of parameters on the numpy function.**
.. code:: python
# TensorIR
def mm_relu(A: T.Buffer((128, 128), "float32"),
B: T.Buffer((128, 128), "float32"),
C: T.Buffer((128, 128), "float32")):
...
# NumPy
def lnumpy_mm_relu(A: np.ndarray, B: np.ndarray, C: np.ndarray):
...
Here ``A``, ``B``, and ``C`` takes a type named ``T.Buffer``, which with shape
argument ``(128, 128)`` and data type ``float32``. This additional information
helps possible MLC process to generate code that specializes in the shape and data
type.
**Similarly, TensorIR also uses a buffer type in intermediate result allocation.**
.. code:: python
# TensorIR
Y = T.alloc_buffer((128, 128), dtype="float32")
# NumPy
Y = np.empty((128, 128), dtype="float32")
Loop Iterations
~~~~~~~~~~~~~~~
**There are also direct correspondence of loop iterations.**
``T.grid`` is a syntactic sugar in TensorIR for us to write multiple nested iterators.
.. code:: python
# TensorIR with `T.grid`
for i, j, k in T.grid(128, 128, 128):
...
# TensorIR with `range`
for i in range(128):
for j in range(128):
for k in range(128):
...
# NumPy
for i in range(128):
for j in range(128):
for k in range(128):
...
Computational Block
~~~~~~~~~~~~~~~~~~~
A significant distinction lies in computational statements:
**TensorIR incorporates an additional construct termed** ``T.sblock``.
.. code:: python
# TensorIR
with T.sblock("Y"):
vi = T.axis.spatial(128, i)
vj = T.axis.spatial(128, j)
vk = T.axis.reduce(128, k)
with T.init():
Y[vi, vj] = T.float32(0)
Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]
# NumPy
vi, vj, vk = i, j, k
if vk == 0:
Y[vi, vj] = 0
Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]
A **block** represents a fundamental computation unit within TensorIR. Importantly,
a block encompasses more information than standard NumPy code. It comprises a set of block axes
``(vi, vj, vk)`` and the computations delineated around them.
.. code:: python
vi = T.axis.spatial(128, i)
vj = T.axis.spatial(128, j)
vk = T.axis.reduce(128, k)
The above three lines declare the **key properties** about block axes in the following syntax.
.. code:: python
[block_axis] = T.axis.[axis_type]([axis_range], [mapped_value])
These three lines convey the following details:
- They specify the binding of ``vi``, ``vj``, ``vk`` (in this instance, to ``i``, ``j``, ``k``).
- They declare the original range intended for ``vi``, ``vj``, ``vk``
(the 128 in ``T.axis.spatial(128, i)``).
- They announce the properties of the iterators (spatial, reduce).
Block Axis Properties
~~~~~~~~~~~~~~~~~~~~~
Let's delve deeper into the properties of the block axis. These properties signify the axis's
relationship to the computation in progress. The block comprises three axes ``vi``, ``vj``, and
``vk``, meanwhile the block reads the buffer ``A[vi, vk]``, ``B[vk, vj]`` and writes the buffer
``Y[vi, vj]``. Strictly speaking, the block performs (reduction) updates to Y, which we label
as write for the time being, as we don't require the value of Y from another block.
Significantly, for a fixed value of ``vi`` and ``vj``, the computation block yields a point
value at a spatial location of ``Y`` (``Y[vi, vj]``) that is independent of other locations in ``Y``
(with different ``vi``, ``vj`` values). We can refer to ``vi``, ``vj`` as **spatial axes** since
they directly correspond to the start of a spatial region of buffers that the block writes to.
The axes involved in reduction (``vk``) are designated as **reduce axes**.
Why Extra Information in Block
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
One crucial observation is that the additional information (block axis range and their properties)
makes the block to be **self-contained** when it comes to the iterations that it is supposed to
carry out independent from the external loop-nest ``i, j, k``.
The block axis information also provides additional properties that help us to validate the correctness of the
external loops that are used to carry out the computation. For example, the above code block will result in an
error because the loop expects an iterator of size 128, but we only bound it to a for loop of size 127.
.. code:: python
# wrong program due to loop and block iteration mismatch
for i in range(127):
with T.sblock("C"):
vi = T.axis.spatial(128, i)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
error here due to iterator size mismatch
...
Sugars for Block Axes Binding
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In situations where each of the block axes is directly mapped to an outer loop iterator,
we can use ``T.axis.remap`` to declare the block axis in a single line.
.. code:: python
# SSR means the properties of each axes are "spatial", "spatial", "reduce"
vi, vj, vk = T.axis.remap("SSR", [i, j, k])
which is equivalent to
.. code:: python
vi = T.axis.spatial(range_of_i, i)
vj = T.axis.spatial(range_of_j, j)
vk = T.axis.reduce (range_of_k, k)
So we can also write the programs as follows.
.. code:: python
@tvm.script.ir_module
class MyModuleWithAxisRemapSugar:
@T.prim_func
def mm_relu(A: T.Buffer((128, 128), "float32"),
B: T.Buffer((128, 128), "float32"),
C: T.Buffer((128, 128), "float32")):
Y = T.alloc_buffer((128, 128), dtype="float32")
for i, j, k in T.grid(128, 128, 128):
with T.sblock("Y"):
vi, vj, vk = T.axis.remap("SSR", [i, j, k])
with T.init():
Y[vi, vj] = T.float32(0)
Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]
for i, j in T.grid(128, 128):
with T.sblock("C"):
vi, vj = T.axis.remap("SS", [i, j])
C[vi, vj] = T.max(Y[vi, vj], T.float32(0))
@@ -0,0 +1,2 @@
Deep Dive: TensorIR
-------------------
@@ -0,0 +1,316 @@
# 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.
# ruff: noqa: E402, E501
"""
.. _dlight_gpu_scheduling:
DLight: Rule-Based GPU Scheduling
==================================
TIR functions produced by Relax legalization need GPU-specific scheduling — thread binding,
loop tiling, shared memory usage — before they can run efficiently on a GPU. There are two
main approaches in TVM:
- **MetaSchedule**: explores a search space to find the best schedule. High quality, but
compilation takes minutes to hours.
- **DLight**: applies pre-defined scheduling rules deterministically. No tuning required,
compilation completes in seconds. Performance is excellent for well-known patterns
(e.g., GEMM, GEMV in LLM workloads) and fair for the rest.
This tutorial covers how DLight works, what rules are available, how to diagnose scheduling
quality, and how to write custom rules.
.. contents:: Table of Contents
:local:
:depth: 1
"""
######################################################################
# Prepare a Model
# ---------------
# We build a small model with ``nn.Module`` that is rich enough to trigger multiple DLight
# rules: ``Linear`` layers produce GEMM (matrix multiplication) kernels, ``LayerNorm``
# produces a general-reduction kernel, and ``ReLU`` is a simple elementwise op.
import tvm
from tvm import relax, tirx
from tvm.relax.frontend import nn
from tvm.s_tir import dlight as dl
class DemoModel(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(768, 768)
self.relu = nn.ReLU()
self.norm = nn.LayerNorm(768)
self.fc2 = nn.Linear(768, 256)
def forward(self, x):
x = self.norm(self.relu(self.fc1(x)))
return self.fc2(x)
mod, params = DemoModel().export_tvm({"forward": {"x": nn.spec.Tensor((1, 768), "float32")}})
######################################################################
# Legalize Relax operators into TIR functions so that DLight has concrete kernels to schedule.
device = tvm.cuda(0)
target = tvm.target.Target.from_device(device)
with target:
mod = relax.get_pipeline("zero")(mod)
######################################################################
# At this point every TIR function in ``mod`` is **unscheduled** — it has no thread bindings
# and would not run efficiently on a GPU. Let's see what functions we have:
for gv, func in mod.functions_items():
if isinstance(func, tirx.PrimFunc):
print(f" {gv.name_hint}")
######################################################################
# Basic Usage: ApplyDefaultSchedule
# ---------------------------------
# ``ApplyDefaultSchedule`` is an ``IRModule`` pass. It iterates over every TIR function in the
# module and tries the given rules **in order**. For each function the first rule whose
# ``apply()`` returns a non-``None`` schedule wins; subsequent rules are skipped.
# After scheduling, the function is marked with ``tirx.is_scheduled`` so it won't be
# scheduled again by a later ``ApplyDefaultSchedule`` call.
######################################################################
# Here we use a common subset of rules. The full catalog (including ``LowBatchGEMV``,
# ``Transpose``, ``RMSNorm``) is listed in the next section.
with target:
scheduled_mod = dl.ApplyDefaultSchedule(
dl.gpu.Matmul(), # GEMM: dense matrix multiplication
dl.gpu.GEMV(), # matrix-vector products
dl.gpu.Reduction(), # simple reductions (sum, max, ...)
dl.gpu.GeneralReduction(), # compound reductions (softmax, layer norm, ...)
dl.gpu.Fallback(), # catch-all for anything unmatched above
)(mod)
scheduled_mod.show()
######################################################################
# Compared with the unscheduled IR, you can now see thread bindings
# (``blockIdx.x``, ``threadIdx.x``, ...) and loop transformations in each TIR function.
######################################################################
# Rule Catalog
# ------------
# DLight ships a set of GPU scheduling rules. Each rule is a subclass of
# ``ScheduleRule`` and implements an ``apply(func, target, tunable)`` method that returns
# a ``Schedule`` if the rule matches, or ``None`` to pass.
#
# The built-in GPU rules, roughly from most specific to most general:
#
# .. list-table::
# :header-rows: 1
# :widths: 20 40 40
#
# * - Rule
# - Pattern
# - Typical operators
# * - ``Matmul``
# - GEMM index pattern ``C[S,I,J] += A[S,I,K] * B[S,J,K]``
# - ``nn.Linear``, batched matmul
# * - ``GEMV``
# - Matrix-vector multiply (one dimension is 1)
# - single-batch decode in attention
# * - ``LowBatchGEMV``
# - Low-batch GEMM scheduled with a GEMV strategy
# - small-batch decode
# * - ``Reduction``
# - Simple accumulation ``X[...] += Y[...]``
# - sum, max, argmax
# * - ``GeneralReduction``
# - Spatial dims followed by reduction dims (``S* R*``)
# - softmax, layer norm, RMS norm
# * - ``Transpose``
# - Read/write indices are permutations of each other
# - 2-D transpose
# * - ``RMSNorm``
# - Contains an ``rsqrt`` operation
# - RMS normalization
# * - ``Fallback``
# - Any function (always matches)
# - generic catch-all
#
# **Rule order matters.** ``ApplyDefaultSchedule`` stops at the first match, so:
#
# - Put **specialized** rules first (``Matmul``, ``GEMV``) — they have strict matching
# conditions but produce high-quality schedules.
# - Put **general** rules later (``GeneralReduction``, ``Fallback``) — they match broadly
# but with less optimal schedules.
# - If you put ``Fallback`` first, it would "steal" every function and no specialized
# rule would ever run.
######################################################################
# Diagnosing Schedule Quality
# ---------------------------
# A common question is: *which rule scheduled which function?* ``ApplyDefaultSchedule``
# does not log this directly, but you can figure it out by applying rules one at a time.
#
# **Step 1**: Apply each rule individually and record which functions it claims.
from collections import OrderedDict
rules = OrderedDict(
[
("Matmul", dl.gpu.Matmul()),
("GEMV", dl.gpu.GEMV()),
("LowBatchGEMV", dl.gpu.LowBatchGEMV()),
("Reduction", dl.gpu.Reduction()),
("GeneralReduction", dl.gpu.GeneralReduction()),
("Transpose", dl.gpu.Transpose()),
("RMSNorm", dl.gpu.RMSNorm()),
]
)
rule_assignment = {}
for rule_name, rule in rules.items():
with target:
test_mod = dl.ApplyDefaultSchedule(rule)(mod)
for gv, func in test_mod.functions_items():
if isinstance(func, tirx.PrimFunc) and gv.name_hint not in rule_assignment:
if "tirx.is_scheduled" in func.attrs and func.attrs["tirx.is_scheduled"] == 1:
rule_assignment[gv.name_hint] = rule_name
######################################################################
# **Step 2**: Functions not claimed by any specialized rule will fall through to ``Fallback``.
all_tir_funcs = [
gv.name_hint for gv, func in mod.functions_items() if isinstance(func, tirx.PrimFunc)
]
fallback_funcs = [name for name in all_tir_funcs if name not in rule_assignment]
print("Rule assignments:")
for name, rule_name in sorted(rule_assignment.items()):
print(f" {name:40s} -> {rule_name}")
if fallback_funcs:
print("Handled by Fallback (may have suboptimal performance):")
for name in sorted(fallback_funcs):
print(f" {name}")
######################################################################
# If an important kernel lands in the Fallback bucket, you have three options:
#
# 1. Write a **custom DLight rule** for it (see below).
# 2. Use **MetaSchedule** to auto-tune that specific function.
# 3. Manually schedule it with the ``tvm.s_tir.Schedule`` API.
######################################################################
# DLight vs MetaSchedule
# ----------------------
# The two systems are complementary, not competing:
#
# .. list-table::
# :header-rows: 1
# :widths: 20 40 40
#
# * -
# - DLight
# - MetaSchedule
# * - Mechanism
# - Deterministic rule matching
# - Search-space exploration
# * - Compile time
# - Seconds
# - Minutes to hours
# * - Performance
# - Excellent on known patterns, fair otherwise
# - Near-optimal with sufficient search budget
# * - Best for
# - Default path, rapid iteration, CI
# - Hot-spot tuning in production
#
# A practical workflow:
#
# 1. Run ``ApplyDefaultSchedule`` with the full rule set to cover all functions.
# 2. Profile the compiled model to identify hot-spot kernels.
# 3. Use ``MetaScheduleTuneTIR`` to auto-tune only those kernels.
#
# Note that ``MetaScheduleTuneTIR`` does **not** automatically skip functions already
# scheduled by DLight — it processes every ``PrimFunc`` in the module. In practice this
# is harmless (tuning an already-scheduled function simply re-explores its space), but if
# you want to avoid the extra search cost, filter the module or use ``MetaScheduleTuneIRMod``
# with ``op_names`` to target specific functions.
######################################################################
# Writing a Custom Rule
# ---------------------
# You can extend DLight by writing your own ``ScheduleRule``. The simplest way is
# ``ScheduleRule.from_callable``, which wraps a plain function into a rule **instance**.
from tvm import s_tir
from tvm.s_tir.dlight.analysis import normalize_prim_func
from tvm.s_tir.dlight.base.schedule_rule import ScheduleRule
@ScheduleRule.from_callable("MyTileAndBind")
def my_tile_and_bind(func: tirx.PrimFunc, target: tvm.target.Target, tunable: bool):
"""A minimal rule: for single-block injective functions, tile and bind to GPU threads."""
if not isinstance(func, tirx.PrimFunc):
return None
sch = s_tir.Schedule(func)
# Use normalize_prim_func to get block info with correct spatial/reduction classification.
# This is the same analysis used by built-in DLight rules.
block_infos = normalize_prim_func(sch)
if block_infos is None or len(block_infos) != 1:
return None # only handle single-block functions
info = block_infos[0]
if not info.is_injective():
return None # skip reductions — dom_kind() uses iter_type, not loop kind
loops = sch.get_loops(info.block_rv)
if len(loops) == 0:
return None
fused = sch.fuse(*loops)
bx, tx = sch.split(fused, factors=[None, 256])
sch.bind(bx, "blockIdx.x")
sch.bind(tx, "threadIdx.x")
return sch
######################################################################
# Insert the custom rule into the rule chain. Note that ``from_callable`` returns an
# **instance**, so pass it directly — do not call ``my_tile_and_bind()`` again.
with target:
custom_mod = dl.ApplyDefaultSchedule(
dl.gpu.Matmul(),
dl.gpu.GeneralReduction(),
my_tile_and_bind, # our custom rule, tried before Fallback
dl.gpu.Fallback(),
)(mod)
custom_mod.show()
######################################################################
# To build a production-quality rule, subclass ``ScheduleRule`` directly and implement
# ``apply()`` with full analysis logic (see ``tvm.s_tir.dlight.gpu.Matmul`` for an example).
######################################################################
# Summary
# -------
# - **DLight** provides fast, deterministic GPU scheduling via rule matching.
# - Rules are tried in order; the first match wins. Put specialized rules before general ones.
# - Use the **single-rule probing** technique to diagnose which rule handles each function.
# - Combine DLight with MetaSchedule: DLight for baseline coverage, MetaSchedule for hot-spot tuning.
# - Extend DLight by writing custom ``ScheduleRule`` implementations.
#
# For DLight's role in the broader optimization pipeline, see :ref:`customize_opt`.
@@ -0,0 +1,307 @@
# 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.
# ruff: noqa: E402
"""
.. _meta_schedule_deep_dive:
MetaSchedule: Search-Based Auto-Tuning
=======================================
MetaSchedule is TVM's search-based auto-tuning framework, located in
``python/tvm/s_tir/meta_schedule/``. It explores different TIR schedules
(loop tiling, vectorization, thread binding, etc.) and measures them on real
hardware to find the fastest implementation for each operator.
While **DLight** (see :ref:`dlight_gpu_scheduling`) provides rule-based scheduling with zero
search time, MetaSchedule trades compilation time for better performance by searching over
the space of possible schedules.
.. contents:: Table of Contents
:local:
:depth: 1
"""
######################################################################
# Architecture Overview
# ---------------------
# A MetaSchedule tuning session involves the following components:
#
# - **ExtractedTask**: A unique TIR workload extracted from a Relax IRModule,
# with a ``task_name`` and ``weight`` (call frequency in the graph).
# - **TuneContext**: Container holding all resources for a single tuning task
# (module, target, space generator, search strategy, etc.).
# - **SpaceGenerator** (default: ``PostOrderApply``): Generates the design space
# of possible schedules by applying ``ScheduleRule`` instances to each block.
# - **SearchStrategy** (default: ``EvolutionarySearch``): Explores the design
# space using an evolutionary algorithm guided by a cost model.
# - **CostModel** (default: ``XGBModel``): Predicts schedule performance using
# XGBoost, reducing the number of actual hardware measurements needed.
# Alternatives include ``MLPModel`` (neural network) and ``RandomModel``
# (baseline).
# - **Builder** / **Runner**: Compile and execute candidates on real hardware to
# obtain measured run times.
# - **Database** (default: ``JSONDatabase``): Persistently stores tuning records
# (schedule traces + measured run times) for later retrieval.
# - **TaskScheduler** (default: ``GradientBasedScheduler``): Allocates tuning
# budget across multiple tasks based on their weights and estimated improvement
# potential.
#
# The tuning loop works as follows:
#
# 1. The **TaskScheduler** picks a task to tune.
# 2. The **SpaceGenerator** produces candidate schedules from the design space.
# 3. The **SearchStrategy** selects candidates (guided by the **CostModel**),
# sends them to the **Builder** and **Runner** for measurement.
# 4. Measured results are committed to the **Database** and used to update the
# **CostModel** for the next iteration.
# 5. Repeat until the trial budget is exhausted.
######################################################################
# Prepare a Model
# ---------------
# We reuse a simple model to demonstrate MetaSchedule APIs.
import os
import tempfile
import tvm
from tvm import relax
from tvm.relax.frontend import nn
class DemoModel(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(256, 10, bias=False)
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
input_shape = (1, 784)
mod, params = DemoModel().export_tvm({"forward": {"x": nn.spec.Tensor(input_shape, "float32")}})
device = tvm.cuda(0)
target = tvm.target.Target.from_device(device)
######################################################################
# User-Facing Entry Points
# ------------------------
# MetaSchedule provides several levels of API, from high-level transforms to
# low-level tuning functions.
#
# Transform-Based API (Recommended)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# These are Relax passes that can be composed into a ``Sequential`` pipeline:
#
# - **MetaScheduleTuneIRMod**: Tunes an entire IRModule. Supports ``op_names``
# for selective operator tuning.
# - **MetaScheduleTuneTIR**: Tunes all TIR functions individually (no
# ``op_names`` filtering).
# - **MetaScheduleApplyDatabase**: Applies the best schedules from the tuning
# database. Only replaces functions that have records; the rest are left
# unchanged.
#
# Here is a typical tune-and-apply pipeline:
#
# .. note::
#
# To save CI time and avoid flakiness, we skip the tuning process in CI.
if os.getenv("CI", "") != "true":
with target, tempfile.TemporaryDirectory() as tmp_dir:
tuned_mod = tvm.ir.transform.Sequential(
[
relax.get_pipeline("zero"),
relax.transform.MetaScheduleTuneTIR(
work_dir=tmp_dir,
max_trials_global=300,
),
relax.transform.MetaScheduleApplyDatabase(work_dir=tmp_dir),
]
)(mod)
tuned_mod.show()
######################################################################
# Inspecting Tunable Tasks
# ------------------------
# Before tuning, use ``extract_tasks`` to see what MetaSchedule will tune:
from tvm.s_tir.meta_schedule.relax_integration import extract_tasks
with target:
legalized_mod = relax.get_pipeline("zero")(mod)
tasks = extract_tasks(legalized_mod, target)
for i, task in enumerate(tasks):
print(f"Task {i}: {task.task_name} (weight={task.weight})")
######################################################################
# Each ``ExtractedTask`` has:
#
# - ``task_name``: Derived from the PrimFunc name (e.g., ``"fused_matmul_add_relu"``).
# - ``weight``: How many ``call_tir`` sites invoke this workload. The task
# scheduler uses weights to allocate more budget to frequently-called operators.
# - ``dispatched``: List of candidate TIR modules for this workload.
######################################################################
# Selective Operator Tuning
# -------------------------
# ``MetaScheduleTuneIRMod`` accepts an ``op_names`` parameter to tune only
# operators whose task name contains any of the given strings:
#
# .. code-block:: python
#
# with target:
# mod = tvm.ir.transform.Sequential([
# relax.transform.MetaScheduleTuneIRMod(
# params={},
# work_dir="./tuning_logs",
# max_trials_global=300,
# op_names=["matmul"], # Only tune matmul-related operators
# ),
# relax.transform.MetaScheduleApplyDatabase(work_dir="./tuning_logs"),
# ])(mod)
#
# Operators without tuning records are left unscheduled -- you can apply DLight or
# other rule-based schedules to cover them afterward.
#
# .. note::
#
# ``MetaScheduleTuneTIR`` does not support ``op_names`` filtering. Use
# ``MetaScheduleTuneIRMod`` when you need selective tuning.
######################################################################
# Database
# --------
# When using a fixed ``work_dir``, tuning results are persisted in two
# newline-delimited JSON files:
#
# - ``database_workload.json``: One line per unique workload (structural hash +
# serialized IRModule).
# - ``database_tuning_record.json``: One line per tuning record (workload index +
# schedule trace + measured run times).
#
# Records are appended incrementally as tuning progresses.
#
# Resumption Semantics
# ~~~~~~~~~~~~~~~~~~~~
# When you re-run tuning with the same ``work_dir``, existing records are loaded
# and used as warm-start seeds for the evolutionary search. The tuner does
# **not** skip already-seen workloads entirely -- it starts from a better initial
# population, so re-runs are faster than starting from scratch but still consume
# trials.
#
# Once tuning is done, subsequent compilations only need
# ``MetaScheduleApplyDatabase``:
#
# .. code-block:: python
#
# with target:
# mod = relax.transform.MetaScheduleApplyDatabase(
# work_dir="./tuning_logs"
# )(mod)
#
# Database Implementations
# ~~~~~~~~~~~~~~~~~~~~~~~~
# MetaSchedule ships several database backends:
#
# - **JSONDatabase**: Persistent file-based storage (default). Created
# automatically when you pass ``work_dir``.
# - **MemoryDatabase**: In-memory, non-persistent. Useful for testing.
# - **UnionDatabase**: Queries all sub-databases and returns the globally best
# record.
# - **OrderedUnionDatabase**: Queries sub-databases in order; returns from the
# first one that has a match.
# - **ScheduleFnDatabase**: Wraps a user-provided scheduling function.
######################################################################
# Cross-Model Database Reuse
# --------------------------
# MetaSchedule identifies workloads by their structural hash. If two models
# contain operators with the same shape, dtype, and computation, they share the
# same hash and can reuse tuning records.
#
# module_equality Options
# ~~~~~~~~~~~~~~~~~~~~~~~
# - ``"structural"`` (default): Exact structural match. Safe but strict.
# - ``"anchor-block"``: Match based on the dominant compute block, ignoring
# surrounding context. More permissive -- enables sharing across fused operators
# that have the same core computation but different fusion boundaries.
#
# ``OrderedUnionDatabase`` enables a layered lookup strategy: check a local
# database first, then fall back to a shared team database:
#
# .. code-block:: python
#
# from tvm.s_tir.meta_schedule.database import JSONDatabase, OrderedUnionDatabase
#
# local_db = JSONDatabase(work_dir="./my_tuning_logs")
# shared_db = JSONDatabase(work_dir="/shared/tuning_db")
# combined_db = OrderedUnionDatabase(local_db, shared_db)
#
# with target, combined_db:
# mod = relax.transform.MetaScheduleApplyDatabase()(mod)
######################################################################
# Key Parameters Reference
# ------------------------
#
# .. list-table::
# :header-rows: 1
# :widths: 25 75
#
# * - Parameter
# - Description
# * - ``max_trials_global``
# - Total trial budget shared across all tasks. Set proportional to the
# number of tasks (e.g., 200-500 trials per task for good results).
# * - ``max_trials_per_task``
# - Per-task trial cap. Defaults to ``max_trials_global`` if not set.
# * - ``op_names``
# - List of strings to filter tasks by name (substring match).
# ``MetaScheduleTuneIRMod`` only.
# * - ``work_dir``
# - Directory for database files and logs. Use a fixed path to enable
# persistence and resumption.
# * - ``cost_model``
# - ``"xgb"`` (XGBoost, default), ``"mlp"`` (neural network), or
# ``"random"`` (baseline). Only available via ``tune_relax``.
# * - ``runner``
# - ``"local"`` (default) or an ``RPCRunner`` instance for remote devices.
# Only available via ``tune_relax``.
# * - ``module_equality``
# - ``"structural"`` (default) or ``"anchor-block"`` for more permissive
# cross-model matching. Only available via ``tune_relax``.
######################################################################
# Summary
# -------
# - **MetaSchedule** finds high-quality TIR schedules by searching over the
# design space and measuring on real hardware.
# - Use ``MetaScheduleTuneTIR`` for full-module tuning, or
# ``MetaScheduleTuneIRMod`` with ``op_names`` for selective tuning.
# - Tuning records persist in ``work_dir`` and can be reused across runs and
# models with the same operator shapes.
# - Combine with DLight: use DLight for fast baseline coverage, then MetaSchedule
# for hot-spot tuning (see :ref:`dlight_gpu_scheduling`).
@@ -0,0 +1,289 @@
# 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.
# ruff: noqa: E402
"""
.. _tir-creation:
TensorIR Creation
-----------------
In this section, we will introduce the methods to write a TensorIR function
in Apache TVM. This tutorial presumes familiarity with the fundamental concepts of TensorIR.
If not already acquainted, please refer to :ref:`tirx-learning` initially.
.. note::
This tutorial concentrates on the construction of **standalone** TensorIR functions. The
techniques presented here are not requisite for end users to compile Relax models.
"""
######################################################################
# Create TensorIR using TVMScript
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# The most straightforward way to create a TensorIR function via TVMScript.
# TVMScript is a TVM Python dialect that represents TensorIR in TVM.
#
# .. important::
#
# While TVMScript employs Python syntax and AST, ensuring full compatibility
# with Python tools like auto-completion and linting, it is not a native Python
# language and cannot be executed by a Python interpreter.
#
# More precisely, the decorator **@tvm.script** extracts the Python AST from
# the decorated function, subsequently parsing it into TensorIR.
#
# Standard Format
# ***************
# Let's take an example of ``mm_relu`` from :ref:`tirx-learning`. Here is the complete
# format of the ir_module and in TVMScript:
import numpy as np
import tvm_ffi
import tvm
from tvm.script import ir as I
from tvm.script import tirx as T
@I.ir_module
class MyModule:
@T.prim_func(s_tir=True)
def mm_relu(
A: T.Buffer((128, 128), "float32"),
B: T.Buffer((128, 128), "float32"),
C: T.Buffer((128, 128), "float32"),
):
Y = T.alloc_buffer((128, 128), dtype="float32")
for i in range(128):
for j in range(128):
for k in range(128):
with T.sblock("Y"):
vi = T.axis.spatial(128, i)
vj = T.axis.spatial(128, j)
vk = T.axis.reduce(128, k)
T.reads(A[vi, vk], B[vk, vj])
T.writes(Y[vi, vj])
with T.init():
Y[vi, vj] = T.float32(0)
Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]
for i in range(128):
for j in range(128):
with T.sblock("C"):
vi = T.axis.spatial(128, i)
vj = T.axis.spatial(128, j)
T.reads(Y[vi, vj])
T.writes(C[vi, vj])
C[vi, vj] = T.max(Y[vi, vj], T.float32(0))
######################################################################
# Concise with Syntactic Sugar
# ****************************
# For ease of writing, we can employ the following syntactic sugar to
# streamline the code:
#
# - Utilize ``T.grid`` to condense nested loops;
# - Employ ``T.axis.remap`` to abbreviate block iterator annotations;
# - Exclude ``T.reads`` and ``T.writes`` for blocks whose content can
# be inferred from the block body;
@I.ir_module
class ConciseModule:
@T.prim_func(s_tir=True)
def mm_relu(
A: T.Buffer((128, 128), "float32"),
B: T.Buffer((128, 128), "float32"),
C: T.Buffer((128, 128), "float32"),
):
Y = T.alloc_buffer((128, 128), dtype="float32")
for i, j, k in T.grid(128, 128, 128):
with T.sblock("Y"):
vi, vj, vk = T.axis.remap("SSR", [i, j, k])
with T.init():
Y[vi, vj] = T.float32(0)
Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]
for i, j in T.grid(128, 128):
with T.sblock("C"):
vi, vj = T.axis.remap("SS", [i, j])
C[vi, vj] = T.max(Y[vi, vj], T.float32(0))
######################################################################
# We can use the following code to verify that the two modules are equivalent:
print(tvm_ffi.structural_equal(MyModule, ConciseModule))
######################################################################
# Interactive with Python Variables
# *********************************
# Despite TVMScript not being executed by a Python interpreter, limited
# interaction with Python is feasible. For instance, Python variables can
# be used to ascertain the shape and data type of a TensorIR.
# Python variables
M = N = K = 128
dtype = "float32"
# IRModule in TVMScript
@I.ir_module
class ConciseModuleFromPython:
@T.prim_func(s_tir=True)
def mm_relu(
A: T.Buffer((M, K), dtype),
B: T.Buffer((K, N), dtype),
C: T.Buffer((M, N), dtype),
):
Y = T.alloc_buffer((M, N), dtype)
for i, j, k in T.grid(M, N, K):
with T.sblock("Y"):
vi, vj, vk = T.axis.remap("SSR", [i, j, k])
with T.init():
Y[vi, vj] = T.cast(T.float32(0), dtype)
Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]
for i, j in T.grid(M, N):
with T.sblock("C"):
vi, vj = T.axis.remap("SS", [i, j])
C[vi, vj] = T.max(Y[vi, vj], T.cast(T.float32(0), dtype))
######################################################################
# Check the equivalence:
print(tvm_ffi.structural_equal(ConciseModule, ConciseModuleFromPython))
######################################################################
# TensorIR Function with Dynamic Shapes
# *************************************
# Despite TVMScript not being executed by a Python interpreter, limited
# interaction with Python is feasible. For instance, Python variables can
# be used to ascertain the shape and data type of a TensorIR.
@I.ir_module
class DynamicShapeModule:
@T.prim_func(s_tir=True)
def mm_relu(a: T.handle, b: T.handle, c: T.handle):
# Dynamic shape definition
M = T.int32()
N = T.int32()
K = T.int32()
# Bind the input buffers with the dynamic shapes
A = T.match_buffer(a, [M, K], dtype)
B = T.match_buffer(b, [K, N], dtype)
C = T.match_buffer(c, [M, N], dtype)
Y = T.alloc_buffer((M, N), dtype)
for i, j, k in T.grid(M, N, K):
with T.sblock("Y"):
vi, vj, vk = T.axis.remap("SSR", [i, j, k])
with T.init():
Y[vi, vj] = T.cast(T.float32(0), dtype)
Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]
for i, j in T.grid(M, N):
with T.sblock("C"):
vi, vj = T.axis.remap("SS", [i, j])
C[vi, vj] = T.max(Y[vi, vj], T.cast(T.float32(0), dtype))
######################################################################
# Now let's check the runtime dynamic shape inference:
def evaluate_dynamic_shape(lib: tvm.runtime.Module, m: int, n: int, k: int):
A = tvm.runtime.tensor(np.random.uniform(size=(m, k)).astype("float32"))
B = tvm.runtime.tensor(np.random.uniform(size=(k, n)).astype("float32"))
C = tvm.runtime.tensor(np.zeros((m, n), dtype="float32"))
lib(A, B, C)
return C.numpy()
# Compile lib only once
dyn_shape_lib = tvm.compile(DynamicShapeModule, target="llvm")
# Able to handle different shapes
print(evaluate_dynamic_shape(dyn_shape_lib, m=4, n=4, k=4))
print(evaluate_dynamic_shape(dyn_shape_lib, m=64, n=64, k=128))
######################################################################
# Create TensorIR using Tensor Expression
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Often, the specifics of TensorIR are disregarded in favor of expressing the computation more
# succinctly, leading to the pragmatic generation of TensorIR. This is where Tensor Expression
# (TE) becomes relevant.
#
# Tensor Expression (TE) serves as a domain-specific language delineating a sequence of
# computations through an expression-like API.
#
# .. note::
#
# Tensor Expression comprises two components within the TVM stack: the expression and the
# schedule. The expression is the domain-specific language embodying the computation pattern,
# precisely what we're addressing in this section. Conversely, the TE schedule is the legacy
# scheduling method, has been superseded by the TensorIR schedule in the current TVM stack.
#
# Create Static-Shape Functions
# *****************************
# We use the same example of ``mm_relu`` from the last subsection to demonstrate the
# TE creation method.
from tvm import te
A = te.placeholder((128, 128), "float32", name="A")
B = te.placeholder((128, 128), "float32", name="B")
k = te.reduce_axis((0, 128), "k")
Y = te.compute((128, 128), lambda i, j: te.sum(A[i, k] * B[k, j], axis=k), name="Y")
C = te.compute((128, 128), lambda i, j: te.max(Y[i, j], 0), name="C")
######################################################################
# Here ``te.compute`` takes the signature ``te.compute(output_shape, fcompute)``.
# And the fcompute function describes how we want to compute the value of each
# element ``Y[i, j]`` for a given index:
#
# .. code:: python
#
# lambda i, j: te.sum(A[i, k] * B[k, j], axis=k)
#
# The aforementioned lambda expression encapsulates the computation:
# :math:`Y_{i, j} = \sum_k A_{i, k} \times B_{k, j}`. Upon defining the computation,
# we can formulate a TensorIR function by incorporating the pertinent parameters of interest.
# In this specific instance, we aim to construct a function with two input parameters **A, B**
# and one output parameter **C**.
te_func = te.create_prim_func([A, B, C]).with_attr({"global_symbol": "mm_relu"})
TEModule = tvm.IRModule({"mm_relu": te_func})
TEModule.show()
######################################################################
# Create Dynamic-Shape Functions
# ******************************
# We can also create a dynamic-shape function using Tensor Expression. The only difference
# is that we need to specify the shape of the input tensors as symbolic variables.
# Declare symbolic variables
M, N, K = te.var("m"), te.var("n"), te.var("k")
A = te.placeholder((M, N), "float32", name="A")
B = te.placeholder((K, N), "float32", name="B")
k = te.reduce_axis((0, K), "k")
Y = te.compute((M, N), lambda i, j: te.sum(A[i, k] * B[k, j], axis=k), name="Y")
C = te.compute((M, N), lambda i, j: te.max(Y[i, j], 0), name="C")
dyn_te_func = te.create_prim_func([A, B, C]).with_attr({"global_symbol": "mm_relu"})
DynamicTEModule = tvm.IRModule({"mm_relu": dyn_te_func})
DynamicTEModule.show()
@@ -0,0 +1,177 @@
# 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.
# ruff: noqa: E402
"""
.. _tirx-transform:
Transformation
--------------
In this section, we will get to the main ingredients of the compilation flows -
transformations of primitive tensor functions.
"""
######################################################################
# In the :ref:`previous section <tirx-learning>`, we have given an example of how to write
# ``mm_relu`` using TensorIR. In practice, there can be multiple ways to implement
# the same functionality, and each implementation can result in different performance.
#
# .. note::
# This tutorial primarily illustrates the application of TensorIR Transformation,
# rather than delving into optimization techniques.
#
# First, let's take a look at the implementation of ``mm_relu`` in the previous section:
import tvm
from tvm.script import ir as I
from tvm.script import tirx as T
@I.ir_module
class MyModule:
@T.prim_func(s_tir=True)
def main(
A: T.Buffer((128, 128), "float32"),
B: T.Buffer((128, 128), "float32"),
C: T.Buffer((128, 128), "float32"),
):
T.func_attr({"tirx.noalias": True})
with T.sblock("root"):
T.reads()
T.writes()
Y = T.sblock_alloc_buffer((128, 128))
for i, j, k in T.grid(128, 128, 128):
with T.sblock("Y"):
vi, vj, vk = T.axis.remap("SSR", [i, j, k])
with T.init():
Y[vi, vj] = T.float32(0)
Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]
for i, j in T.grid(128, 128):
with T.sblock("C"):
vi, vj = T.axis.remap("SS", [i, j])
C[vi, vj] = T.max(Y[vi, vj], T.float32(0))
######################################################################
# Before we transform the function, let's first evaluate the performance of the
# original implementation.
import numpy as np
a_np = np.random.uniform(size=(128, 128)).astype("float32")
b_np = np.random.uniform(size=(128, 128)).astype("float32")
c_np = a_np @ b_np
a_nd = tvm.runtime.tensor(a_np)
b_nd = tvm.runtime.tensor(b_np)
c_nd = tvm.runtime.tensor(np.zeros((128, 128), dtype="float32"))
def evaluate(mod: tvm.IRModule):
lib = tvm.tirx.build(mod, target="llvm")
# check correctness
lib(a_nd, b_nd, c_nd)
np.testing.assert_allclose(c_nd.numpy(), c_np, rtol=1e-5)
# evaluate performance
f_timer = lib.time_evaluator("main", tvm.cpu())
print(f_timer(a_nd, b_nd, c_nd))
evaluate(MyModule)
######################################################################
# Initialization Schedule
# ***********************
# We initiate the process of code transformation by establishing a Schedule helper class,
# utilizing the provided **MyModule** as input.
sch = tvm.s_tir.Schedule(MyModule)
######################################################################
# Loop Tiling
# ***********
# Subsequently, we execute the requisite operations to acquire a reference to
# block **Y** and its associated loops.
block_Y = sch.get_sblock("Y")
i, j, k = sch.get_loops(block_Y)
######################################################################
# We now proceed to execute the transformations. The initial modification involves
# splitting loop ``j`` into two separate loops, with the inner loop possessing a
# length of 8. It is crucial to understand that the transformation process is procedural;
# thus, inadvertent execution of the block twice will yield an error stating the
# non-existence of variable ``j``.
j0, j1 = sch.split(j, factors=[None, 8])
######################################################################
# The outcome of the transformation can be examined, as it is retained within ``sch.mod``.
sch.mod.show()
######################################################################
# Following the initial transformation phase, two supplementary loops, ``j_0`` and ``j_1``,
# have been generated with respective ranges of 16 and 8. The subsequent
# action involves reordering these two loops.
sch.reorder(j0, k, j1)
sch.mod.show()
evaluate(sch.mod)
######################################################################
# Leverage Localities
# *******************
# Subsequently, we will execute two additional transformation steps to achieve a different
# variant. First, we employ a primitive known as **reverse_compute_at** to relocate block
# **C** to an inner loop of **Y**.
block_C = sch.get_sblock("C")
sch.reverse_compute_at(block_C, j0)
sch.mod.show()
######################################################################
# Rewrite Reduction
# *****************
# Until now, the reduction initialization and update step have been maintained together
# within a single block body. This amalgamated form facilitates loop transformations,
# as the outer loops ``i``, ``j`` of initialization and updates generally need to remain
# synchronized.
#
# Following the loop transformations, we can segregate the initialization of Y's elements
# from the reduction update via the **decompose_reduction** primitive.
sch.decompose_reduction(block_Y, k)
sch.mod.show()
evaluate(sch.mod)
######################################################################
# Trace the Transformation
# ************************
# TensorIR schedule is a procedural language, and the transformation is executed in a
# step-by-step manner. We can trace the transformation by printing the schedule or the
# history of the schedule.
#
# We've already see the schedule by printing ``sch.mod``. We can also print the history
# of the schedule by ``sch.trace``.
sch.trace.show()
######################################################################
# Alternatively, we can output the IRModule in conjunction with the historical trace.
sch.show()