# 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, unused-argument import functools import numbers from typing import Any import tvm_ffi import tvm from tvm import relax, tirx from tvm.ir import GlobalVar from tvm.relax import Expr, Type from tvm.relax.script import builder as R from tvm.relax.script.builder.frame import BindingBlockFrame from tvm.relax.utils import convert_to_expr from tvm.script.ir_builder import ir as I from tvm.script.ir_builder.base import IRBuilder from tvm.script.parser._core import Parser, dispatch, doc from .entry import ( MatchCastPair, TypeProxy, _normalize_ty, _normalize_ty_proxy, ) relax.Expr._dispatch_type = relax.Expr # pylint: disable=protected-access dispatch.register_op(relax.Expr, doc.GtE, 0)(lambda lhs, rhs: lhs >= rhs) dispatch.register_op(relax.Expr, doc.Gt, 0)(lambda lhs, rhs: lhs > rhs) dispatch.register_op(relax.Expr, doc.LtE, 0)(lambda lhs, rhs: lhs <= rhs) dispatch.register_op(relax.Expr, doc.Lt, 0)(lambda lhs, rhs: lhs < rhs) def bind_assign_value( self: Parser, node: doc.expr, var_name: str, value: Any, anno_ty: Type | None = None, emit_prim_expr: bool = False, ) -> Any: var_table = self.var_table.get() if isinstance(value, tirx.Var): if value.name and var_name != value.name: self.report_error( node, "Cannot define TIR variables with different names. The LHS of binding should " "has the same name provided in RHS.", ) if var_name in var_table: prev_value = var_table[var_name] if not isinstance(prev_value, tirx.Var): self.report_error( node, "Cannot redefine a non-TIR-variable object to a TIR variable. Please " "define the TIR variable with another name.", ) if prev_value.ty != value.ty: self.report_error( node, f"Expected the same dtype for TIR vars but got {value.ty} vs {prev_value.ty}", ) if not isinstance(value, type(prev_value)): self.report_error( node, f"Expected the same IR type for TIR vars " f"but existing value {type(value)} is mismatched " f"to previous {type(prev_value)}", ) value = prev_value IRBuilder.name(var_name, value) return value if tvm.ir.is_prim_expr(value): if not emit_prim_expr: return value if isinstance(value, tuple): value = convert_to_expr(value) if isinstance(value, numbers.Number): value = R.const(value) if isinstance(value, relax.Expr): var = R.emit(value, anno_ty) elif isinstance(value, MatchCastPair): if anno_ty is not None and not tvm_ffi.structural_equal(anno_ty, value.ty): self.report_error( node, "Cannot specify inconsistent annotation for a match cast pair. " ) var = R.emit_match_cast(value.value, value.ty) else: return value IRBuilder.name(var_name, var) return var def is_prim_value_call(node: doc.expr) -> bool: return isinstance(node, doc.Call) and getattr(node.func, "attr", None) == "prim_value" def eval_ty_proxy(self: Parser, node: doc.expr) -> TypeProxy: try: annotation = self.eval_expr(node) return _normalize_ty_proxy(annotation) except Exception as err: # pylint: disable=broad-except self.report_error(node, err) raise def eval_ty(self: Parser, node: doc.expr, eval_str: bool = False) -> Type: var_table = self.var_table.get() if eval_str else None try: ty = self.eval_expr(node) return _normalize_ty(ty, var_table) except Exception as err: # pylint: disable=broad-except self.report_error(node, err) raise def is_called(node: Any, func_name: str) -> bool: # Check if it calls into a func if isinstance(node, doc.Call): # Recursive call was found if isinstance(node.func, doc.Name) and node.func.id == func_name: return True elif isinstance(node, list | tuple): for stmt in node: if is_called(stmt, func_name): return True elif isinstance(node, doc.AnnAssign | doc.Assign | doc.Return | doc.Expr): return is_called(node.value, func_name) elif isinstance(node, doc.With): return is_called(node.body, func_name) elif isinstance(node, doc.If): smts = [] if node.body is not None: smts = smts + list(node.body) if node.orelse is not None: smts = smts + list(node.orelse) return is_called(smts, func_name) return False def is_recursive(node: doc.FunctionDef) -> bool: # Check if it is a recursive function for stmt in node.body: if is_called(stmt, node.name): return True return False def collect_symbolic_var_from_prelude( self: Parser, node: doc.FunctionDef, symbolic_vars: dict[str, tirx.Var] ) -> dict[str, tirx.Var]: prelude_vars = {} for stmt in node.body: if isinstance(stmt, doc.Assign) and all( isinstance(target, doc.Name) and target.id in symbolic_vars for target in stmt.targets ): values = self.eval_expr(stmt.value) try: iter(values) except TypeError: values = [values] assert len(stmt.targets) == len(values) for target, value in zip(stmt.targets, values): name = target.id prelude_vars[name] = value return {**symbolic_vars, **prelude_vars} def collect_symbolic_var_from_params(self: Parser, node: doc.FunctionDef) -> None: # Collect symbolic vars from parameters symbolic_vars = {} for arg in node.args.args: if arg.annotation is None: self.report_error(arg, "Type annotation is required for function parameters.") param_ty_proxy = eval_ty_proxy(self, arg.annotation) for var_name in param_ty_proxy.get_symbolic_vars(): if var_name not in symbolic_vars: symbolic_vars[var_name] = tirx.Var(var_name, "int64") # Update symbolic vars based on symbolic_vars = collect_symbolic_var_from_prelude(self, node, symbolic_vars) # Define symbolic vars to the current var_table frame for var_name, var in symbolic_vars.items(): self.var_table.add(var_name, var, allow_shadowing=False) @dispatch.register(token="relax", type_name="FunctionDef") def visit_function_def(self: Parser, node: doc.FunctionDef) -> None: is_inner_function = self.inside_function self.inside_function = True # reserve a var for local function func_val = self.var_table.get().get(node.name) if not func_val and is_recursive(node): collect_symbolic_var_from_params(self, node) if node.returns is None: ret_ty = relax.TupleType([]) else: ret_ty = eval_ty(self, node.returns, eval_str=True) params_ty = [] for arg in node.args.args: if arg.annotation is None: self.report_error(arg, "Type annotation is required for function parameters.") param_ty = eval_ty(self, arg.annotation, eval_str=True) params_ty.append(param_ty) # created a var for the local function, the same var could be used for recursive call local_func_var = relax.Var(node.name, relax.FuncType(params_ty, ret_ty)) self.var_table.add(node.name, local_func_var) purity = find_decorator_annotation(node, "pure") # treat the function as private if we are inside another function # or if it has a privacy annotation privacy = is_inner_function or find_decorator_annotation(node, "private", default=False) with self.var_table.with_frame(): with self.with_dispatch_token("relax"): with R.function(is_pure=purity, is_private=privacy): R.func_name(node.name) collect_symbolic_var_from_params(self, node) if node.returns is not None: ann_ty = eval_ty(self, node.returns, eval_str=True) R.func_ret_ty(ann_ty) self.visit(node.args) for stmt in node.body: if isinstance(stmt, doc.FunctionDef): if not stmt.decorator_list: self.report_error(stmt, "Function must be decorated") dec = self.eval_expr(stmt.decorator_list[-1]) # inline prim_func was found if dec.dispatch_token == "tirx": self.report_error(stmt, "inline prim_func is disallowed in Relax IR") self.visit_body(node.body) self.inside_function = is_inner_function def find_decorator_annotation(node: doc.FunctionDef, annotation: str, default: bool = True) -> bool: """ Check the value of given annotation (argument name) in the function decorator. Returns the value of the annotation if present, otherwise giving the default value. """ # look for the named argument in the function decorator for dec in node.decorator_list: if not isinstance(dec, doc.Call) or dec.func.attr != "function": continue for keyword in dec.keywords: if keyword.arg == annotation: return keyword.value.value return default @dispatch.register(token="relax", type_name="tvm_declare_function") def visit_tvm_declare_function(self: Parser, node: doc.FunctionDef) -> GlobalVar: with self.var_table.with_frame(): collect_symbolic_var_from_params(self, node) if node.returns is None: # Use AnyType as unknown return type # NOTE: Cannot use VoidType here because the return type can be refined later. ret_ty = relax.AnyType() else: ret_ty = eval_ty(self, node.returns, eval_str=True) params = [] for arg in node.args.args: if arg.annotation is None: self.report_error(arg, "Type annotation is required for function parameters.") param_ty = eval_ty(self, arg.annotation, eval_str=True) params.append(relax.Var(arg.arg, param_ty)) is_pure = find_decorator_annotation(node, "pure") func_signature = relax.Function.create_empty(params, ret_ty, is_pure=is_pure) return I.decl_function(node.name, func_signature) @dispatch.register(token="relax", type_name="pre_visit_local_function") def pre_visit_local_function(self: Parser, node: doc.Expr) -> None: ir_builder = IRBuilder() ir_builder.__enter__() @dispatch.register(token="relax", type_name="post_visit_local_function") def post_visit_local_function(self: Parser, node: doc.Expr) -> None: ir_builder = IRBuilder.current() result = ir_builder.get() ir_builder.__exit__(None, None, None) # reuse var if it is reserved reserved_var = self.var_table.get().get(node.name) if reserved_var: var = R.emit_var_binding(relax.VarBinding(reserved_var, result)) else: var = R.emit(result) IRBuilder.name(node.name, var) self.var_table.add(node.name, var, allow_shadowing=False) @dispatch.register(token="relax", type_name="Expr") def visit_expr_stmt(self: Parser, node: doc.Expr) -> None: value = self.eval_expr(node.value) if isinstance(value, relax.Expr): var = R.emit(value) IRBuilder.name("_", var) is_void_value = isinstance(var.ty, relax.TupleType) and len(var.ty.fields) == 0 if not is_void_value: self.report_error( node, f"Non-void relax expressions must be bound to a variable, " f"but expression of type {var.ty} was used as a statement.", ) elif value is not None: self.report_error(node, f"Unsupported Expr stmt type {value}.") @dispatch.register(token="relax", type_name="arguments") def visit_arguments(self: Parser, node: doc.arguments) -> None: arg: doc.arg for arg in node.args: if arg.annotation is None: self.report_error(arg, "Type annotation is required for function parameters.") param_ty = eval_ty(self, arg.annotation, eval_str=True) param = R.arg(arg.arg, param_ty) self.var_table.add(arg.arg, param) @dispatch.register(token="relax", type_name="tvm_annotation") def visit_tvm_annotation(self: Parser, node: doc.expr) -> Type: return eval_ty(self, node, eval_str=False) @dispatch.register(token="relax", type_name="With") def visit_with(self: Parser, node: doc.With) -> None: # Currently only `with R.dataflow()` is supported if len(node.items) != 1: self.report_error(node, "Only one item is allowed.") item = node.items[0] if item.optional_vars is not None: self.report_error( item.context_expr, "Relax syntax doesn't allow binding expressions in `with` to variables", ) frame = self.eval_expr(item.context_expr) with self.var_table.with_frame(): with frame: self.visit(node.body) if isinstance(frame, BindingBlockFrame) and frame.is_dataflow: output_vars = frame.output_vars for var in output_vars: self.var_table.add(var.name_hint, var, allow_shadowing=True) @dispatch.register(token="relax", type_name="Assign") def visit_assign(self: Parser, node: doc.Assign) -> None: if len(node.targets) != 1: self.report_error(node, "Consequential assignments like 'a = b = c' are not supported.") lhs = node.targets[0] rhs = self.eval_expr(node.value) self.eval_assign( target=lhs, source=rhs, bind_value=functools.partial( bind_assign_value, emit_prim_expr=is_prim_value_call(node.value), ), allow_shadowing=True, ) @dispatch.register(token="relax", type_name="AnnAssign") def visit_ann_assign(self: Parser, node: doc.AnnAssign) -> None: lhs = node.target rhs = self.eval_expr(node.value) anno_ty = self.visit_tvm_annotation(node.annotation) self.eval_assign( target=lhs, source=rhs, bind_value=functools.partial( bind_assign_value, anno_ty=anno_ty, emit_prim_expr=is_prim_value_call(node.value), ), allow_shadowing=True, ) @dispatch.register(token="relax", type_name="Return") def visit_return(self: Parser, node: doc.Assign) -> None: value = self.eval_expr(node.value) value = convert_to_expr(value) R.func_ret_value(value) @dispatch.register(token="relax", type_name="If") def visit_if(self: Parser, node: doc.If) -> None: if node.orelse is None: raise ValueError("Else statements are required for relax dialect.") with R.If(self.eval_expr(node.test)) as if_frame: with self.var_table.with_frame(): with R.Then(): self.visit_body(node.body) with self.var_table.with_frame(): with R.Else(): self.visit_body(node.orelse) self.var_table.add(if_frame.var_name, if_frame.var, allow_shadowing=True) @dispatch.register(token="relax", type_name="enter_token") def enter_token(self: Parser) -> dict[str, Any]: def relax_call(self, *args) -> Expr: args = [convert_to_expr(arg) if isinstance(arg, tuple) else arg for arg in args] if all(isinstance(x, Expr) for x in args): return relax.Call(self, args) arg_types = [type(x) for x in args] raise RuntimeError(f"Do not know how to handle GlobalVar.__call__ for types {arg_types}") context = {"GlobalVar.__call__": GlobalVar.__call__} GlobalVar.__call__ = relax_call return context @dispatch.register(token="relax", type_name="exit_token") def exit_token(self: Parser, context: dict[str, Any]) -> None: assert "GlobalVar.__call__" in context GlobalVar.__call__ = context.get("GlobalVar.__call__")