Files
apache--tvm/python/tvm/relax/script/parser/entry.py
T
wehub-resource-sync 26446540fa
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

522 lines
16 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=missing-docstring, invalid-name
import inspect
from collections.abc import Callable as _Callable
from typing import Any, TypeVar
import tvm
from tvm.ir import PrimType
from tvm.relax import (
AnyType,
Expr,
Function,
FuncType,
SeqExpr,
ShapeExpr,
ShapeType,
TensorType,
TupleType,
Type,
)
from tvm.relax.expr import Var
from tvm.relax.script import builder as R
from tvm.runtime import ObjectConvertible
from tvm.script.ir_builder.ir import lookup_vdevice
from tvm.script.parser._core import doc, parse, utils
from tvm.script.parser.core.entry import scan_macro
from tvm.script.parser.core.parser import Parser, ScriptMacro
FType = TypeVar("FType", bound=_Callable)
############################## R.function ##############################
# this formulation allows us to support having @R.function
# appear as a decorator by itself or to have optional arguments
# like @R.function(pure=False)
def function(
f: FType | None = None, pure: bool = True, private: bool = False, check_well_formed=True
) -> Function | FType:
# pylint: disable=unused-argument
# (pure and private aren't used here, but are used later in parsing)
# need to inspect the stack first because is_defined_in_class expects the outer class
# to be in a particular position in the stack
orig_stack = inspect.stack()
def decorator_wrapper(f):
if not inspect.isfunction(f):
raise TypeError(f"Expect a function, but got: {f}")
if utils.is_defined_in_class(orig_stack, f):
return f
return parse(f, utils.inspect_function_capture(f), check_well_formed=check_well_formed)
if f is not None:
# if there are no optional args given, this will directly invoke the wrapper
return decorator_wrapper(f)
else:
# if there is a optional arg given, it returns the wrapper function
# as a new decorator and applies it
setattr(decorator_wrapper, "dispatch_token", "relax")
return decorator_wrapper
setattr(function, "dispatch_token", "relax")
############################## R.macro ##############################
class RelaxMacro(ScriptMacro):
"""Specialization of the ScriptMacro class for Relax."""
def parse_macro(self, parser: Parser) -> Expr:
macro_def = self.get_macro_def()
ret_value = None
with R.SeqExpr() as seq:
for idx, stmt in enumerate(macro_def.body):
# Normally, a "return" statement is only allowed in a R.function. We don't
# want to parse the macro's body as if it was a body of a function, because
# the latter imposes some constraints that we want to avoid.
# At the same time, we want to use "return" to indicate the value of the
# macro (since in Relax everything is an expression), so add special handling
# of "return".
if isinstance(stmt, doc.Return):
ret_value = parser.eval_expr(stmt.value)
if idx + 1 != len(macro_def.body):
parser.report_error(macro_def, "'return' should be the last statement")
break
parser.visit(stmt)
if ret_value is None:
parser.report_error(macro_def, "Macros must end with a return statement")
return SeqExpr(seq.binding_blocks, ret_value)
def macro(*args, hygienic: bool = True) -> _Callable:
"""Decorator for macro definitions.
Parameters
----------
hygienic: bool
Specifies whether the macro is hygienic or not.
A macro is hygienic if all symbols used in the macro's body are resolved
to values from the location of the macro definition. A non-hygienic macro
will have its symbols resolved to values at the time of the macro's use.
"""
def _decorator(func: _Callable) -> ScriptMacro:
source, closure_vars = scan_macro(func, utils.inspect_function_capture(func))
obj = RelaxMacro(source, closure_vars, func, hygienic)
def wrapper(*args, **kwargs):
return obj(*args, **kwargs)
return wrapper
if len(args) == 0:
return _decorator
if len(args) == 1 and inspect.isfunction(args[0]):
return _decorator(args[0])
raise ValueError(
"Invalid use of R.macro. Usage: @R.macro, @R.macro(), @R.macro(hygienic=[True|False])"
)
############################# Type ##############################
class TypeProxy(ObjectConvertible):
def as_ty(self, dict_globals: dict[str, Any] | None = None) -> Type:
raise NotImplementedError()
def get_symbolic_vars(self) -> set[str]:
return {}
def asobject(self):
return self.as_ty(None)
############################### R.Any ################################
class AnyProxy(TypeProxy):
"""The proxy for AnyType.
Parameters
----------
values : Optional[List[Expr]]
The symbolic shape values if known.
ndim : Optional[int]
The size of the shape.
"""
def __init__(self) -> None:
pass
def get_symbolic_vars(self) -> set[str]:
return set()
def as_ty(self, dict_globals: dict[str, Any] | None = None) -> AnyType:
return AnyType()
def Any() -> AnyProxy:
return AnyProxy()
ObjectProxy = AnyProxy
def Object() -> AnyProxy:
return AnyProxy()
############################### R.Tensor ###############################
def _eval_shape(expr: str | Expr, dict_globals: dict[str, Any] | None) -> Expr:
if isinstance(expr, str):
code = compile(expr, "<string>", "eval")
return eval(code, dict_globals or {}) # pylint: disable=eval-used
else:
return expr
class TensorProxy(TypeProxy):
shape: list[str | Expr] | None
dtype: str
vdevice: str | None
ndim: int
def __init__(
self,
shape: list[Expr | str] | Expr | None = None,
dtype: str | None = None,
vdevice: str | None = None,
ndim: int = -1,
) -> None:
if isinstance(shape, Expr):
if not isinstance(shape, ShapeExpr | Var):
raise ValueError(
"When the shape is an Expr, it must be a ShapeExpr or a Var with ShapeExpr "
f"value. But got: {shape} with type: {type(shape)}"
)
if isinstance(shape, Var) and not isinstance(shape.ty, ShapeType):
raise ValueError(
"When the shape is a Var, it must have shape ty. But got "
f"{shape} with ty: {shape.ty}"
)
self.shape = shape
self.dtype = dtype
self.vdevice = vdevice
self.ndim = ndim
def get_symbolic_vars(self) -> set[str]:
if self.shape is None or isinstance(self.shape, Expr):
return {}
else:
return {s for s in self.shape if isinstance(s, str) and s.isidentifier()}
def as_ty(self, dict_globals: dict[str, Any] | None = None) -> TensorType:
vdev = self.vdevice
if isinstance(self.vdevice, str):
if ":" in self.vdevice:
split_vdev = self.vdevice.split(":")
vdev = lookup_vdevice(split_vdev[0], int(split_vdev[1]))
else:
vdev = lookup_vdevice(self.vdevice, 0)
if self.shape is None:
return TensorType(None, self.dtype, vdev, self.ndim)
elif isinstance(self.shape, ShapeExpr | Var):
return TensorType(self.shape, self.dtype, vdev, self.ndim)
else:
if dict_globals is None and any([isinstance(s, str) for s in self.shape]):
raise ValueError(
"String-defined shape expr is only allowed when parsing function parameters "
"and return annotations for TVMScript."
)
shape = [_eval_shape(s, dict_globals) for s in self.shape]
return TensorType(shape, self.dtype, vdev, self.ndim)
def Tensor(
shape: list[Expr | str] | Expr | None = None,
dtype: str | None = None,
vdevice: str | None = None,
ndim: int = -1,
) -> TensorProxy:
# scalar tensor case
if shape is not None and not isinstance(shape, Var) and len(shape) == 0:
shape = []
if isinstance(shape, str) and dtype is None:
dtype = shape
shape = None
if shape is not None and not isinstance(shape, tuple | list) and not isinstance(shape, Expr):
raise ValueError(f"shape must be a list/tuple or an Expr, but got: {shape}")
return TensorProxy(shape, dtype, vdevice, ndim)
############################## R.Callable ##############################
class CallableProxy(TypeProxy):
params: list[TypeProxy]
ret: TypeProxy
purity: bool
derive_func: str | tvm.ir.EnvFunc | None
"""Function type.
A function type consists of a list of type parameters to enable
the definition of generic functions,
a set of type constraints which we omit for the time being,
a sequence of argument types, the purity of the function, and a return type.
Parameters
----------
params : List[TypeProxy]
The argument TypeProxy
ret : TypeProxy
The return TypeProxy.
purity : bool
Whether the callable is pure.
derive_func: Optional[Union[str, tvm.ir.EnvFunc]]
The derivation function to determine the output Type,
based on the arguments provided to the function. The
specified function should be accessible using
`tvm.get_global_func`, and should have a signature
`Callable[[relax.Call, relax.BlockBuilder], relax.Type]`.
"""
def __init__(
self,
params: TypeProxy | list[TypeProxy] | None = None,
ret: TypeProxy | None = None,
purity: bool | None = None,
derive_func: str | tvm.ir.EnvFunc | None = None,
) -> None:
if params is None:
self.params = params
else:
if not isinstance(params, list | tuple):
params = [params]
# convert `R.Callable` to `R.Callable()`
self.params = [param() if callable(param) else param for param in params]
# Mimic the C++ defaults, where an opaque function is assumed
# to be impure, and a non-opaque function is assumed to be
# pure.
if purity is None:
purity = params is not None
self.ret = ret() if callable(ret) else ret
self.purity = purity
self.derive_func = derive_func
def get_symbolic_vars(self) -> set[str]:
if self.params is None:
return set()
else:
return set().union(*[p.get_symbolic_vars() for p in self.params])
def as_ty(self, dict_globals: dict[str, Any] | None = None) -> FuncType:
if self.ret is None:
ret = None
else:
ret = self.ret.as_ty(dict_globals)
if self.params is None:
params = None
else:
params = [param.as_ty(dict_globals) for param in self.params]
if params is None:
return FuncType.opaque_func(ret=ret, derive_func=self.derive_func, purity=self.purity)
else:
return FuncType(params, ret, purity=self.purity)
def Callable(
params: TypeProxy | list[TypeProxy] | None = None,
ret: TypeProxy | None = None,
purity: bool | None = None,
derive_func: str | tvm.ir.EnvFunc | None = None,
) -> CallableProxy:
return CallableProxy(params, ret, purity=purity, derive_func=derive_func)
############################### R.Tuple ################################
class TupleProxy(TypeProxy):
fields: list[TypeProxy]
"""The type of tuple values.
Parameters
----------
fields : List[TypeProxy]
The fields in the tuple
"""
def __init__(
self,
*fields: list[TypeProxy],
) -> None:
if len(fields) == 1 and isinstance(fields[0], tuple | list):
fields = fields[0]
# convert `R.Tensor` to `R.Tensor()`
self.fields = [field() if callable(field) else field for field in fields]
def get_symbolic_vars(self) -> set[str]:
return set().union(*[f.get_symbolic_vars() for f in self.fields])
def as_ty(self, dict_globals: dict[str, Any] | None = None) -> TupleType:
fields = [field.as_ty(dict_globals) for field in self.fields]
return TupleType(fields)
def Tuple(*fields: list[TypeProxy]) -> TupleProxy:
return TupleProxy(*fields)
############################### R.Shape ################################
class ShapeProxy(TypeProxy):
values: list[Expr] | None
ndim: int
"""The type of shape values.
Parameters
----------
values : Optional[List[Expr]]
The symbolic shape values if known.
ndim : Optional[int]
The size of the shape.
"""
def __init__(
self,
values: list[Expr] | None = None,
ndim: int = -1,
) -> None:
self.values = values
self.ndim = ndim
def get_symbolic_vars(self) -> set[str]:
if self.values is None:
return set()
else:
return {v for v in self.values if isinstance(v, str) and v.isidentifier()}
def as_ty(self, dict_globals: dict[str, Any] | None = None) -> ShapeType:
values = [_eval_shape(v, dict_globals) for v in self.values] if self.values else None
return ShapeType(values, self.ndim)
def Shape(values: list[Expr] | None = None, ndim: int = -1) -> ShapeProxy:
return ShapeProxy(values, ndim)
################################ R.Prim ################################
class PrimProxy(TypeProxy):
dtype: str
"""The type of TIR-representable values.
Parameters
----------
dtype : str
The data type.
"""
def __init__(
self,
dtype: str,
) -> None:
self.dtype = dtype
def get_symbolic_vars(self) -> set[str]:
return set()
def as_ty(self, dict_globals: dict[str, Any] | None = None) -> PrimType:
return PrimType(self.dtype)
def Prim(
dtype: str,
) -> PrimProxy:
return PrimProxy(dtype)
############################ R.match_cast #############################
class MatchCastPair:
value: Expr
ty: Type
def __init__(self, value: Expr, ty: Type) -> None:
self.value = value
self.ty = ty
def match_cast(value: Expr, ty: Type):
ty = _normalize_ty(ty)
if value is None:
raise ValueError("value of match_cast cannot be None")
if ty is None:
raise ValueError("ty of match_cast cannot be None")
return MatchCastPair(value, ty)
def _normalize_ty_proxy(annotation) -> TypeProxy:
if annotation is None:
return TupleProxy([])
elif callable(annotation):
annotation = annotation()
if tvm.ir.is_prim_expr(annotation):
return PrimProxy(annotation.ty.dtype)
return annotation
elif isinstance(annotation, TypeProxy):
return annotation
else:
raise TypeError(f"Expected TypeProxy but got {type(annotation)}.")
def _normalize_ty(ty, dict_globals: dict[str, Any] | None = None) -> Type:
if isinstance(ty, Type):
return ty
else:
proxy = _normalize_ty_proxy(ty)
return proxy.as_ty(dict_globals)