# 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, "", "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)