# 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=redefined-builtin, invalid-name # ruff: noqa: RUF005 """PyTorch-like nn.Module API for constructing workloads.""" from collections.abc import Callable from typing import Any import numpy as np # type: ignore import tvm from tvm import relax, tirx, topi from tvm.relax.op.grad.grad import end_checkpoint, start_checkpoint def emit(expr: relax.Expr, name_hint: str = "") -> relax.Var: return relax.BlockBuilder.current().emit(expr, name_hint=name_hint) def emit_te(func: Callable, *args: Any, **kwargs: Any) -> relax.Var: return relax.BlockBuilder.current().emit_te(func, *args, **kwargs) def checkpoint( func: Callable, *args: Any, **kwargs: Any ) -> relax.Var | list[relax.Var] | list[Any]: """Mark function(*args, **kwargs) should be computed in a checkpointed manner during backward. To be specific, args and kwargs will be checkpointed, and func(*args, **kwargs) will be recomputed in the backward stage. """ args = [start_checkpoint(v) if isinstance(v, relax.Expr) else v for v in args] kwargs = {k: start_checkpoint(v) if isinstance(v, relax.Expr) else v for k, v in kwargs.items()} result = func(*args, **kwargs) if isinstance(result, list | tuple): result = [end_checkpoint(v) if isinstance(v, relax.Expr) else v for v in result] else: assert isinstance(result, relax.Expr) result = end_checkpoint(result) return result def emit_checkpoint( func: Callable, *args: Any, **kwargs: Any ) -> relax.Var | list[relax.Var] | list[Any]: """Mark function(*args, **kwargs) should be computed in a checkpointed manner during backward. To be specific, args and kwargs will be checkpointed, and func(*args, **kwargs) will be recomputed in the backward stage. This interface will additionally emit the exprs marked with start_checkpoint() and end_checkpoint() with suffix "_scp" and "_ecp" respectively, for easily understanding the result tvmscript. """ bb = relax.BlockBuilder.current() args = [ bb.emit(start_checkpoint(v), v.name_hint + "_scp") if isinstance(v, relax.Var) else v for v in args ] kwargs = { k: bb.emit(start_checkpoint(v), v.name_hint + "_scp") if isinstance(v, relax.Var) else v for k, v in kwargs.items() } result = func(*args, **kwargs) if isinstance(result, list | tuple): result = list(result) for i, v in enumerate(result): if isinstance(v, relax.Expr): if not isinstance(v, relax.Var): v = bb.emit(v) result[i] = bb.emit(end_checkpoint(v), v.name_hint + "_ecp") else: assert isinstance(result, relax.Expr) result_emit = bb.emit(result) result = bb.emit(end_checkpoint(result_emit), result_emit.name_hint + "_ecp") return result def emit_checkpoint_sequential( functions: list[Callable], segments: int | list[int], input: relax.Var, checkpoint_last: bool = False, ) -> relax.Var | list[relax.Var] | list[Any]: """A helper function for checkpointing sequential models. This interface has similar purpose as torch.utils.checkpoint.checkpoint_sequential. Sequential models execute a list of modules/functions in order (sequentially). Therefore, we can divide such a model in various segments and checkpoint each segment. By default, we will checkpoint all segments except the last, meaning their inputs will be saved from the forward stage and they will be recomputed in the backward stage. Parameters ---------- functions : List[Callable] The list of functions to be executed sequentially. segments : Union[int, List[int]] The segments. If segments is int `n`, functions will be evenly divided into `n` segments; if segments is a list of ints, it marks the start of every segment. input : relax.Var The input of the first function. checkpoint_last : bool Whether the last segment will be checkpointed. Default: False Returns ------- output : Union[relax.Var, List[relax.Var], List[Any]] The emited output of the last function. """ bb = relax.BlockBuilder.current() def run_function(start, end, functions): def forward(input): for j in range(start, end): input = functions[j](input) return input return forward n = len(functions) if not isinstance(segments, list): segments = list(range(0, n, n // segments)) + [n] if segments[-1] != n: segments = segments + [n] assert len(segments) >= 2 for i in range(len(segments) - 1): if i == len(segments) - 2 and not checkpoint_last: input = run_function(segments[i], segments[i + 1], functions)(input) else: input = emit_checkpoint(run_function(segments[i], segments[i + 1], functions), input) assert isinstance(input, relax.Expr) if not isinstance(input, relax.Var): input = bb.emit(input) return input def _try_unique_name(name: str): """Attempt to uniquify the name If a `relax.BlockBuilder` is active, use it to return a unique name. Otherwise, return the name itself. Two distinct variables in Relax may have identical names. However, for user readability, it is convenient to have all names be unique within a Relax function. If a Placeholder or Parameter is defined within an active `relax.BlockBuilder`, that context may be used to provide a unique name. Otherwise, allow the duplicate names. Parameters ---------- name: str The variable name Returns ------- updated_name: str The updated variable name """ block_builder = relax.BlockBuilder.current() if block_builder is None: return name else: return block_builder.get_unique_name(name) class Placeholder(relax.Var): """A placeholder variable that can represent model input.""" def __init__(self, shape: list[Any] | tuple[Any, ...], dtype="float32", name="data"): if not isinstance(shape, list | tuple): raise TypeError("the shape of Placeholder is expected to be a list or a tuple") super().__init__(_try_unique_name(name), relax.TensorType(shape, dtype)) class Parameter(relax.Var): """A special kind of relax Var that represents model parameter(weight).""" def __init__(self, shape: list[Any] | tuple[Any, ...], dtype="float32", name="param"): if not isinstance(shape, list | tuple): raise TypeError("the shape of Parameter is expected to be a list or a tuple") super().__init__(_try_unique_name(name), relax.TensorType(shape, dtype)) class Module(tvm.relax.frontend.nn.SubroutineMixin): """Base class for all model modules. A neural network or a layer can subclass this class. By default, calls into this module will generate the `relax.Expr` representing the output within the current function body. Setting the variable "define_subrouine" to True; either at the `nn.Module`, subclass, or instance level; will instead produce a subroutine within the same module, which is then called within the current function body. Example ------- .. code-block:: python # Define a linear layer class Linear(Module) def __init__(self, in_features, out_features, bias=True): self.in_features = in_features self.out_features = out_features self.weight = Parameter((in_features, out_features), name="linear_weight") if bias: self.bias = Parameter((out_features,), name="linear_bias") else: self.bias = None # All submodules should implement forward. # Defines the forward computation performed at every call. def forward(self, input: relax.Expr) -> relax.Var: y = emit_te(topi.matmul, input, self.weight) if self.bias is not None: y = emit_te(topi.add, y, self.bias) return y """ define_subroutine: bool = False def parameters(self) -> list[Parameter]: """Return the list of parameters in the module.""" return _unpack_params(self.__dict__) def forward(self, input: relax.Expr): """Define the computation performed at every call.""" raise NotImplementedError() def __call__(self, *args, **kwargs): return self.forward(*args, **kwargs) def _unpack_params(value: object) -> list[relax.Var]: if isinstance(value, Parameter): return [value] if isinstance(value, Module): return value.parameters() if isinstance(value, dict): params = [] for v in value.values(): params += _unpack_params(v) return params if isinstance(value, list | tuple): params = [] for v in value: params += _unpack_params(v) return params return [] def init_params(mod: tvm.IRModule) -> list[tvm.runtime.Tensor]: """Utility function to initialize model's parameters.""" shape_dict = {v.name_hint: v.ty.shape for v in mod["main"].params} params = [] for k, v in shape_dict.items(): if k.startswith("data"): continue if isinstance(v, relax.ShapeExpr): shape = [] for i in v: if isinstance(i, tirx.IntImm): shape.append(int(i)) else: raise TypeError("cannot initialize for unknown-shape parameters.") params.append(tvm.runtime.tensor(np.zeros(shape).astype(np.float32))) else: raise TypeError("cannot initialize for unknown-shape parameters.") return params class Sequential(Module): """A sequential container that concatenates modules in it. Example ------- .. code-block:: python model = nn.Sequential( nn.Conv2d(1, 20, 5), nn.ReLU(), nn.Conv2d(20, 64, 5), nn.ReLU() ) """ def __init__(self, *modules: Module): self.modules = modules def forward(self, input: relax.Expr) -> relax.Var: for module in self.modules: input = module(input) return input class ReLU(Module): """Applies the rectified linear unit activation function on the input.""" def forward(self, input: relax.Expr) -> relax.Var: return emit_te(topi.nn.relu, input) class LogSoftmax(Module): """Applies log softmax activation function on the input.""" def forward(self, input: relax.Expr) -> relax.Var: return emit_te(topi.nn.log_softmax, input) class Linear(Module): """Applies a linear transformation to the input data: :math:`y = xA + b`.""" def __init__(self, in_features, out_features, bias=True): self.in_features = in_features self.out_features = out_features self.weight = Parameter((in_features, out_features), name="linear_weight") if bias: self.bias = Parameter((out_features,), name="linear_bias") else: self.bias = None def forward(self, input: relax.Expr) -> relax.Var: y = emit_te(topi.matmul, input, self.weight) if self.bias is not None: y = emit_te(topi.add, y, self.bias) return y