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

808 lines
27 KiB
Python

# 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.
# pylint: disable=no-else-return, invalid-name, unused-argument, import-outside-toplevel
# ruff: noqa: RUF012
"""Developer API of constructing Relax AST."""
from collections.abc import Callable, Sequence
from typing import Any, Optional
import tvm_ffi
import tvm
from tvm import relax as rx
from tvm import tirx
from tvm.ir.module import IRModule
from tvm.runtime import Object
from . import _ffi_api
from .expr import BaseFunc, Binding, BindingBlock, Expr, GlobalVar, Tuple, Var
from .op.base import call_tir, call_tir_with_grad
from .type import Type
from .utils import gen_call_tir_inputs
class FunctionScope:
"""Auxiliary scope for function"""
def __init__(self, block_builder, name, params, attrs, is_pure):
self._bb = block_builder
self._name = name
self._params = params
self._attrs = attrs
self._is_pure = is_pure
# Blocks that have been collected within the function
self._blocks = []
# a boolean flag that tracks if emit_func_output has been called
self._is_emit_func_output_called = False
def __enter__(self):
self._bb._enter_function_scope(self)
def __exit__(self, exc_type, exc_val, exc_tb):
# __exit__ should properly handle the case where the with block exits with an exception
# when handling error case in exit, always check if there is already an exception
# been thrown in the with block
self._bb._exit_function_scope(exc_type, exc_val, exc_tb)
class DataflowScope:
"""Auxiliary scope for Dataflow block"""
def __init__(self, block_builder):
self._bb = block_builder
def __enter__(self):
block = self._bb._end_block()
if len(block.bindings) > 0:
self._bb._func._blocks.append(block)
self._bb._begin_dataflow_block()
def __exit__(self, ptype, value, trace):
block = self._bb._end_block()
if len(block.bindings) > 0:
self._bb._func._blocks.append(block)
self._bb._begin_binding_block()
class TestingScope:
"""Auxiliary scope for testing purposes"""
def __init__(self, block_builder, def_vars):
self._bb = block_builder
shape_vars = []
for var in def_vars:
if isinstance(var, tvm.tirx.Var):
shape_vars.append(var)
else:
raise ValueError("def_vars only can take tirx.Var")
# setup a dummy var so shape is in scope.
sparam = rx.Var("sparam", rx.ShapeType(shape_vars))
self._scope_params = [sparam]
def __enter__(self):
self._bb.begin_scope(self._scope_params)
self._bb._begin_dataflow_block()
def __exit__(self, ptype, value, trace):
self._bb._end_block()
self._bb.end_scope()
@tvm_ffi.register_object("relax.BlockBuilder")
class BlockBuilder(Object):
"""A builder to build Relax IR for testing and dev.
Examples
--------
.. code-block:: python
m = tirx.Var("m", "int32")
n = tirx.Var("n", "int32")
x = rx.Var("x", rx.TensorType([m, n], "float16"))
y = rx.Var("y", rx.TensorType([n], "float16"))
bb = rx.BlockBuilder()
with bb.function([x, y], "func"):
with bb.dataflow() as df:
lv0 = bb.emit(rx.add(x, y))
lv1 = bb.emit(rx.multiply(lv0, y))
gv0 = bb.emit_output(lv1)
bb.emit_func_output(gv0)
mod = bb.get()
BlockBuilder can also be used to construct neural networks with nn.Module API
.. code-block:: python
from tvm.relax.testing import nn
n = tirx.Var("n", "int64")
input_size = 784
hidden_sizes = [128, 32]
output_size = 10
bb = rx.BlockBuilder()
with bb.function("main"):
model = nn.Sequential(
nn.Linear(input_size, hidden_sizes[0]),
nn.ReLU(),
nn.Linear(hidden_sizes[0], hidden_sizes[1]),
nn.ReLU(),
nn.Linear(hidden_sizes[1], output_size),
nn.LogSoftmax(),
)
data = nn.Placeholder((n, input_size), name="data")
output = model(data)
params = [data] + model.parameters()
builder.emit_func_output(output, params=params)
mod = bb.get()
"""
__slots__ = ("__dict__",)
_stack = []
@staticmethod
def current() -> Optional["BlockBuilder"]:
"""Returns the current BlockBuilder."""
if BlockBuilder._stack:
return BlockBuilder._stack[-1]
else:
return None
def __init__(self, mod: IRModule = None):
# Which functions are currently being defined
self._func_stack: list[FunctionScope] = []
self.__init_handle_by_constructor__(_ffi_api.BlockBuilderCreate, mod) # type: ignore
def _begin_dataflow_block(self) -> None:
_ffi_api.BlockBuilderBeginDataflowBlock(self) # type: ignore
def _begin_binding_block(self) -> None:
_ffi_api.BlockBuilderBeginBindingBlock(self) # type: ignore
def _end_block(self) -> BindingBlock:
return _ffi_api.BlockBuilderEndBlock(self) # type: ignore
@property
def _func(self):
if self._func_stack:
return self._func_stack[-1]
else:
raise RuntimeError(
"Cannot access BlockBuilder._func when outside a bb._function() block"
)
def _enter_function_scope(self, func_scope):
BlockBuilder._stack.append(self)
self._func_stack.append(func_scope)
self.begin_scope(func_scope._params)
self._begin_binding_block()
def _exit_function_scope(self, exc_type, exc_val, exc_tb):
# record
is_emit_func_output_called = self._func._is_emit_func_output_called
# recover to default state
self._func_stack.pop()
assert BlockBuilder._stack
assert BlockBuilder._stack[-1] is self
BlockBuilder._stack.pop()
# NOTE: we must raise after we recover the state so future
# block builder scoping functions correctly
if exc_type is None:
if not is_emit_func_output_called:
raise RuntimeError("emit_func_output must be called in a relax function.")
def function(
self,
name: str,
params: Var | Tuple | list[Var] | None = None,
attrs: dict[str, Object] | None = None,
pure: bool = True,
private: bool = False,
) -> FunctionScope:
"""Annotate a Relax function.
Parameters
----------
name : str, optional
The name of the function
params : tvm.relax.Var | Tuple | List[tvm.relax.Var], optional
The parameters of the function.
If params is None, it means deferring initialization of function parameters
until emit_func_output.
attrs : Dict[str, Object], optional
The function attrs
pure : bool, optional
Whether the function is annotated as pure.
private : bool, optional
Whether the function is annotated as private.
If the function is private, it will not have a global symbol attribute.
If it is not private and not an inner function, then it will have
a global symbol attribute (mapped to the function's name)
Returns
-------
ret: FunctionScope
A FunctionScope for building a Relax function node.
"""
if isinstance(params, rx.Var):
params = [params]
elif isinstance(params, list | tuple):
for param in params:
if not isinstance(param, rx.Var):
raise TypeError(
f"each element of function parameters must be of type tvm.relax.Var,\
but got: {type(param)}"
)
if attrs is None:
attrs = {}
# The block builder does not permit nesting functions, per above comment,
# so no further check should be needed
if not private:
attrs["global_symbol"] = name
return FunctionScope(self, name, params, attrs, is_pure=pure)
def testing_scope(self, def_vars: list[tirx.Var]) -> TestingScope:
"""Start a scope for unit-testing purposes.
Parameters
----------
def_vars: List[tirx.Var]
List of symbolic variables that are marked as defined in scope.
Returns
-------
ret: TestingScope
A TestingScope to setup builder for emit and other purposes.
"""
return TestingScope(self, def_vars)
def dataflow(self) -> DataflowScope:
"""Annotate a Relax dataflow block.
Returns
-------
ret: DataflowScope
A DataflowScope for building a Relax dataflow block.
"""
return DataflowScope(self)
def _normalize_python_tuple(self, expr: Expr | Sequence[Expr]):
"""Internal utility function to convert to relax.Tuple
The `emit`, `emit_output`, and `emit_func_output` can be
called with python `list` or `tuple` objects. These objects
should be converted to `relax.Tuple` prior to calling an FFI
function, as they would otherwise be converted to
`tvm_ffi.Array`. In addition, any nested tuple objects
should be converted.
"""
if isinstance(expr, list | tuple):
return Tuple([self._normalize_python_tuple(element) for element in expr])
elif expr is None:
from . import op
return op.null_value()
else:
return expr
def emit(self, expr: Expr, name_hint: str = "") -> Var:
"""Emit an expr.
This infers the shape and type of the expr, create a variable,
and bind the expr to the variable.
Parameters
----------
expr : tvm.relax.Expr
The Expr to be emitted.
name_hint : str
Name hint for the bound variable.
Returns
-------
ret : tvm.relax.Var
A newly created variable that gets bound to the input expr.
"""
expr = self._normalize_python_tuple(expr)
return _ffi_api.BlockBuilderEmit(self, expr, name_hint) # type: ignore
def call_te(self, func: Callable, *args: Any, **kwargs: Any) -> Expr:
"""Generate a call node according to the te function.
This function converts arguments from relax expression to te tensor,
The callback func should return a te tensor or a list of te tensors.
Please see detailed example in emit_te
Parameters
----------
func : Callable
A function that returns a te tensor or a list of te tensors.
args : Any, optional
arguments passed to the function.
kwargs : Any, optional
The keyword arguments passed to the function.
Note that the following keyword args are reserved:
- 'primfunc_name_hint' for passing name hint to the PrimFunc
that gets generated.
- 'primfunc_attrs' is reserved for passing func attributes to
be added to the PrimFunc that gets created.
Returns
-------
ret : tvm.relax.Call
A newly created call node
"""
primfunc_name = kwargs.pop("primfunc_name_hint", None)
tir_func, call_args, output_ty, tir_vars = gen_call_tir_inputs(func, *args, **kwargs)
if not primfunc_name:
primfunc_name = func.__name__
gvar = self.add_func(tir_func, primfunc_name)
return call_tir(gvar, call_args, output_ty, tir_vars)
def call_te_with_grad(
self,
func: Callable,
*args: Any,
te_grad_name: str,
te_grad_kwargs: dict[str, Object] | None = None,
**kwargs: Any,
) -> Expr:
"""Generate a call node according to the te function.
This method will generate a call_tir_with_grad node, i.e. a call_tir node bound with a
te gradient function (refered by te_grad_name).
Parameters
----------
func : Callable
A function that returns a te tensor or a list of te tensors.
args : Any, optional
arguments passed to the function.
te_grad_name : str
The registered name of the te gradient function associated with the call_tir_with_grad
node. Must be provided as a keyword argument.
te_grad_kwargs : Dict[str, Object], optional
The keyword arguments passed to the te gradient function.
Optionally provided as a keyword argument. Default: {}.
kwargs : Any, optional
The keyword arguments passed to the function.
Note that the following keyword args are reserved:
- 'primfunc_name_hint' for passing name hint to the PrimFunc
that gets generated.
- 'primfunc_attrs' is reserved for passing func attributes to
be added to the PrimFunc that gets created.
Returns
-------
ret : tvm.relax.Call
A newly created call node
"""
primfunc_name = kwargs.pop("primfunc_name_hint", None)
tir_func, call_args, output_ty, tir_vars = gen_call_tir_inputs(func, *args, **kwargs)
if te_grad_kwargs is None:
te_grad_kwargs = {}
if not primfunc_name:
primfunc_name = func.__name__
gvar = self.add_func(tir_func, primfunc_name)
return call_tir_with_grad(
gvar, call_args, output_ty, te_grad_name, te_grad_kwargs, tir_vars
)
def emit_te(self, func: Callable, *args: Any, **kwargs: Any) -> Var:
"""Emit a call node according to the te function.
This function converts arguments from relax expression to te tensor,
The callback func should return a te tensor or a list of te tensors.
Parameters
----------
func : Callable
A function that returns a te tensor or a list of te tensors.
args : Any, optional
arguments passed to the function.
kwargs : Any, optional
The keyword arguments passed to the function.
Note that the key "primfunc_name_hint" is reserved for passing name hint
to the PrimFunc that gets generated.
Returns
-------
ret : tvm.relax.Var
A newly created variable that gets bound to the call code.
Example
-------
.. code-block:: python
bb = rx.BlockBuilder()
n, m = tirx.Var("n", "int64"), tirx.Var("m", "int64")
x = rx.Var("x", rx.TensorType([n, m], "float32"))
y = rx.Var("y", rx.TensorType([n, m], "float32"))
def te_func(args, args_dict, msg):
A = args[0]
B = args_dict["B"]
return te.compute((128, 128), lambda i, j: A[i, j] + B[i, j])
with bb.function([x, y], "rx_func"):
out = bb.emit_te(te_func, [x], {"B": y}, msg="hello")
bb.emit_func_output(out)
will result in TVMScript
.. code-block:: python
@tvm.script.ir_module
class Module:
@T.prim_func(s_tir=True)
def te_func(var_rxplaceholder: T.handle, var_rxplaceholder_1: T.handle,
var_compute: T.handle) -> None:
# function attr dict
T.func_attr({"tirx.noalias": True})
m = T.int64()
n = T.int64()
rxplaceholder = T.match_buffer(var_rxplaceholder, [n, m], dtype="float32")
rxplaceholder_1 = T.match_buffer(var_rxplaceholder_1, [n, m], dtype="float32")
compute = T.match_buffer(var_compute, [128, 128], dtype="float32")
# body
# with T.sblock("root")
for i0, i1 in T.grid(128, 128):
with T.sblock("compute"):
i, j = T.axis.remap("SS", [i0, i1])
T.reads([rxplaceholder[i, j], rxplaceholder_1[i, j]])
T.writes([compute[i, j]])
compute[i, j] = rxplaceholder[i, j] + rxplaceholder_1[i, j]
@R.function
def rx_func(x: Tensor((n, m), "float32"), y: Tensor((n, m), "float32")) -> Tensor:
# block 0
gv = relax.call_tir("te_func", (x, y), R.Tensor((128, 128), "float32"))
return gv
Example
-------
.. code-block:: python
bb = relax.BlockBuilder()
n = tirx.Var("n", "int64")
x = relax.Var("x", relax.TensorType([n], "float32"))
y = relax.Var("y", relax.TensorType([n + 1], "float32"))
def te_func(A):
C = te.compute((n + 1), lambda i: A[i])
return C
with bb.function("rx_func", [x, y]):
x1 = bb.emit_te(te_func, y)
bb.emit_func_output(x1)
will result in TVMScript
.. code-block:: python
@tvm.script.ir_module
class Module:
@T.prim_func(s_tir=True)
def te_func(var_rxplaceholder: T.handle, var_compute: T.handle, n: T.int64) -> None:
rxplaceholder = T.match_buffer(var_rxplaceholder, [n + T.int64(1)],
dtype="float32")
compute = T.match_buffer(var_compute, [n + T.int64(1)], dtype="float32")
# body
# with T.sblock("root")
for i0 in T.serial(0, n + T.int64(1)):
with T.sblock("compute"):
i = T.axis.spatial(n + T.int64(1), i0)
T.reads([rxplaceholder[i]])
T.writes([compute[i]])
compute[i] = rxplaceholder[i]
@R.function
def rx_func(x: Tensor((n,), "float32"), y: Tensor(((n + 1),), "float32"))
-> Tensor(None, "float32", ndim=-1):
# block 0
gv = relax.call_tir(te_func, (y,), R.Tensor((n + 1,), "float32"), (n,))
return gv
"""
name_hint = kwargs.pop("name_hint", "")
return self.emit(self.call_te(func, *args, **kwargs), name_hint=name_hint)
def match_cast(self, value: Expr, ty: Type, name_hint: str = "") -> Var:
"""Emit a MatchCast.
Parameters
----------
value : tvm.relax.Expr
The value of the MatchCast to be emitted.
ty : Type
The type to be matched.
name_hint : str
The name of the match cast
Returns
-------
ret : tvm.relax.Var
A newly created variable that get bounds to be the casted result.
"""
return _ffi_api.BlockBuilderEmitMatchCast(
self,
value,
ty,
name_hint,
) # type: ignore
def emit_output(self, output: Expr | Tuple | list[Expr], name_hint: str = "") -> Var:
"""Emit output for the current dataflow block or function.
Parameters
----------
output : Expr | Tuple | List[Expr]
The output of the current block/function.
name_hint : str
Name hint for the bound variable.
Returns
-------
ret : tvm.relax.Var
The return variable which gets bound to the output.
"""
output = self._normalize_python_tuple(output)
return _ffi_api.BlockBuilderEmitOutput(self, output, name_hint) # type: ignore
def emit_func_output(
self,
output: Expr | Tuple | list[Expr],
params: Var | Tuple | list[Var] | None = None,
) -> GlobalVar:
"""Emit output for the function.
Parameters
----------
output : Expr | Tuple | List[Expr]
The output of the current block/function.
params : tvm.relax.Var | Tuple | List[tvm.relax.Var], optional
The parameters of the function to be built.
If params is None, it means the params have been initialized in the function with scope.
Returns
-------
gvar: tvm.ir.GlobalVar
A GlobalVar representing the function
"""
if self._func._is_emit_func_output_called:
raise RuntimeError("emit_func_output must be called exactly once in a relax function.")
self._func._is_emit_func_output_called = True
if self._func._params is not None and params is not None:
raise RuntimeError(
"function parameters have been initialized in the function with scope."
)
if self._func._params is None and params is None:
raise RuntimeError("Relax function must have parameter.")
if self._func._params is None:
self._func._params = params
if BlockBuilder.current() is not self:
raise RuntimeError("BlockBuilder.current() must be self.")
output = self._normalize_python_tuple(output)
block = self._end_block()
if len(block.bindings) > 0:
self._func._blocks.append(block)
seqe = rx.SeqExpr(self._func._blocks, output)
# If the parameters were not provided as part of
# `bb.function()`, then any variables provided from the params
# are not in scope. Otherwise, TIR variables used in dynamic
# inputs are removed as undefined (e.g. Replacing
# `R.Tensor(["batch_size"])` with `R.Tensor(ndims=1)`).
self.begin_scope(self._func._params)
try:
seqe = self.normalize(seqe)
finally:
self.end_scope()
# do not specify ret_ty and let constructor deduce
# from seqe.ty
func = rx.Function(self._func._params, seqe, is_pure=self._func._is_pure)
for key, value in self._func._attrs.items():
func = func.with_attr(key, value)
self.end_scope()
return self.add_func(func, self._func._name)
def normalize(self, expr: Expr) -> Expr:
"""Normalize an Expr to complete its shape and type.
Parameters
----------
expr : Expr
The input expr.
Returns
-------
ret : Expr
The expr with normalized shape and type.
"""
return _ffi_api.BlockBuilderNormalize(self, expr) # type: ignore
def get(self) -> tvm.IRModule:
"""Return intermediate IRModule. For the situation where the IRModule is needed in the
middle of a building process.
Returns
-------
ret : tvm.IRModule
An IRModule with Relax and TIR functions being built.
"""
return _ffi_api.BlockBuilderGetContextIRModule(self) # type: ignore
def finalize(self) -> tvm.IRModule:
"""Finalize the building process and return the result IRModule.
Possibly rename GlobalVars in the IRModule to ensure name uniqueness and the invariant:
every public function has the same name as its "global_symbol" attribute.
Note this method should be called only once at the end of the building process, since it may
invalidate global vars previously returned by this builder.
See also tvm.relax.transform.NormalizeGlobalVar.
Returns
-------
ret : tvm.IRModule
An IRModule with Relax and TIR functions being built.
"""
return _ffi_api.BlockBuilderFinalize(self) # type: ignore
def get_unique_name(self, name_prefix: str) -> str:
"""Generate a unique name with a specified prefix.
Parameters
----------
name_hint : str
The name prefix.
Returns
-------
ret : str
The generated name.
"""
return _ffi_api.BlockBuilderGetUniqueName(self, name_prefix) # type: ignore
def add_func(self, func: BaseFunc, func_name: str) -> GlobalVar:
"""Add a Relax function or a TIR PrimFunc to the IRModule being built.
Parameters
----------
func : BaseFunc
The function to be added.
func_name : str
The name of the function to be added.
Returns
-------
gvar : GlobalVar
The global var bound to the added function.
"""
return _ffi_api.BlockBuilderAddFunction(self, func, func_name) # type: ignore
def update_func(self, gv: GlobalVar, updated_func: BaseFunc) -> None:
"""Add a Relax function or a TIR PrimFunc to the IRModule being built.
Parameters
----------
gv : GlobalVar
The global var referring the function to be updated.
updated_func : BaseFunc
The updated function.
"""
return _ffi_api.BlockBuilderUpdateFunction(self, gv, updated_func) # type: ignore
def current_block_is_dataflow(self) -> bool:
"""Check if the block being built is DataflowBlock or not.
Returns
-------
ret : bool
A boolean that indicates if the block being built is DataflowBlock or not.
"""
return _ffi_api.BlockBuilderCurrentBlockIsDataFlow(self) # type: ignore
def emit_normalized(self, binding: Binding) -> None:
"""Emit an already normalized binding.
Parameters
----------
binding: Binding
The binding to be emitted.
"""
_ffi_api.BlockBuilderEmitNormalized(self, binding) # type: ignore
def lookup_binding(self, var: Var) -> Expr | None:
"""Lookup a var in the binding table.
Parameters
----------
var: Var
The input var.
Returns
-------
expr: Expr
The Expr bound to the input var.
"""
return _ffi_api.BlockBuilderLookupBinding(self, var) # type: ignore
def begin_scope(self, params: list[Var] | None = None) -> None:
"""Begin a new scope, with optional parameters that
are visible within the scope.
Parameters
----------
params: Optional[List[Var]]
Parameters that are visible within the scope.
Note
----
This function should be called when new scope is introduced
(function, seq) to properly track the variable availability
and help the best effort deduction.
"""
return _ffi_api.BlockBuilderBeginScope(self, params) # type: ignore
def end_scope(self) -> None:
"""End the current scope. Please see `begin_scope` for details"""
return _ffi_api.BlockBuilderEndScope(self) # type: ignore