Files
apache--tvm/python/tvm/relax/testing/nn.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

355 lines
12 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=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