522 lines
16 KiB
Python
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)
|