chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
# isort: skip_file
|
||||
# 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.
|
||||
"""The Relax training APIs."""
|
||||
|
||||
from . import loss
|
||||
from . import optimizer
|
||||
from . import trainer
|
||||
from . import utils
|
||||
|
||||
from .setup_trainer import SetupTrainer
|
||||
from .trainer import Trainer
|
||||
from .utils import AppendLoss
|
||||
@@ -0,0 +1,21 @@
|
||||
# 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.
|
||||
"""FFI APIs for tvm.relax.training"""
|
||||
|
||||
import tvm_ffi
|
||||
|
||||
tvm_ffi.init_ffi_api("relax.training", __name__)
|
||||
@@ -0,0 +1,382 @@
|
||||
# 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: RUF012
|
||||
"""Loss functions library for relax."""
|
||||
|
||||
# isort: off
|
||||
from typing import Literal
|
||||
|
||||
# isort: on
|
||||
|
||||
from ..block_builder import BlockBuilder
|
||||
from ..expr import Expr, Function, Type, Var
|
||||
from ..op import abs, argmax, mean, multiply, reshape, subtract, sum
|
||||
from ..op.nn import log_softmax, nll_loss
|
||||
|
||||
|
||||
def _create_param_var(param: Var | Type, param_name: str) -> Var:
|
||||
"""If param is a Type, create a Var with the given Type and name.
|
||||
|
||||
If param is a Var, create a Var with the same Type and name as the given param Var."""
|
||||
if isinstance(param, Type):
|
||||
param = Var(param_name, param)
|
||||
if not isinstance(param, Var):
|
||||
raise TypeError("The type of param should be Var or Type, but got " + type(param))
|
||||
return Var(param.name_hint, param.ty)
|
||||
|
||||
|
||||
class Loss:
|
||||
r"""Base class of all loss.
|
||||
|
||||
Generally, loss function will take one or more **input parameters** (that is outputs of
|
||||
the backbone of a model), one or more **target parameters**, and generate a scalar value
|
||||
denoting the loss.
|
||||
|
||||
You can use `relax.transform.AppendLoss` to append the loss function to a one-dataflowblock
|
||||
backbone function in a IRModule. That will generate a one-dataflowblock function accepting
|
||||
instances and targets, and then returning the loss.
|
||||
|
||||
Most loss functions involve a reduction of losses from all instances in a batch. We use
|
||||
`reduction` parameter to denote the reduction method. Possible reduction methods include
|
||||
`"mean"`, `"sum"` and `"none"`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
loss_name : str
|
||||
The name of the loss function. Should be provided when calling `super().__init__` in
|
||||
constructor functions of subclasses.
|
||||
|
||||
num_backbone_outputs : int
|
||||
The number of `prediction_outputs` of the backbone function, alos the number of the
|
||||
backbone_prediction_outputs of the loss function. See `relax.transform.AppendLoss`.
|
||||
|
||||
Should be provided when calling `super().__init__` in constructor functions of subclasses.
|
||||
|
||||
For example, `CrossEntropyLoss` requires one backbone prediction output; `MarginRankingLoss`
|
||||
requires two backbone prediction outputs.
|
||||
|
||||
reduction : Literal["mean", "sum", "none"]
|
||||
The reduction method to apply to output. Can be "mean", "sum" or "none".
|
||||
|
||||
none : no reduction will be applied,
|
||||
mean : the sum of the output will be divided by the batch_size,
|
||||
sum : the output will be summed.
|
||||
"""
|
||||
|
||||
_valid_reductions = ["mean", "sum", "none"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
loss_name: str,
|
||||
num_backbone_outputs: int,
|
||||
reduction: Literal["mean", "sum", "none"] = "mean",
|
||||
) -> None:
|
||||
self._loss_name = loss_name
|
||||
self._reduction = reduction
|
||||
self._num_backbone_outputs = num_backbone_outputs
|
||||
|
||||
if self._reduction not in self._valid_reductions:
|
||||
raise ValueError("Reduction can only be one of these values: ", self._valid_reductions)
|
||||
|
||||
@property
|
||||
def num_backbone_outputs(self) -> int:
|
||||
"""Get the number of number of the outputs of the backbone function."""
|
||||
return self._num_backbone_outputs
|
||||
|
||||
def _with_reduction(self, expr: Expr) -> Expr:
|
||||
"""Add a reduction to the final loss.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
expr : Expr
|
||||
The loss expr.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ret : Expr
|
||||
The reduced result.
|
||||
"""
|
||||
if self._reduction == "sum":
|
||||
expr = sum(expr)
|
||||
elif self._reduction == "mean":
|
||||
expr = mean(expr)
|
||||
elif self._reduction != "none":
|
||||
raise ValueError("Reduction can only be one of these values: ", self._valid_reductions)
|
||||
return expr
|
||||
|
||||
|
||||
class L1Loss(Loss):
|
||||
r"""Mean element-wise absolute value difference.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
reduction : Literal["mean", "sum", "none"]
|
||||
The reduction method to apply to output. Can be "mean", "sum" or "none".
|
||||
|
||||
none : no reduction will be applied,
|
||||
mean : the sum of the output will be divided by the batch_size,
|
||||
sum : the output will be summed.
|
||||
"""
|
||||
|
||||
def __init__(self, reduction: Literal["mean", "sum", "none"] = "mean") -> None:
|
||||
super().__init__("l1_loss", 1, reduction)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
predictions: Var | Type,
|
||||
targets: Var | Type,
|
||||
) -> Function:
|
||||
"""Get the relax function of L1Loss. If the parameters are
|
||||
type, it will create corresponding variables.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
predictions : Union[Var, Type]
|
||||
The predictions of the model in the calculation of loss.
|
||||
targets : Union[Var, Type]
|
||||
The ground truth in the calculation of loss.
|
||||
|
||||
Returns
|
||||
-------
|
||||
The relax function of L1Loss with the loss name as its global symbol.
|
||||
"""
|
||||
bb = BlockBuilder()
|
||||
|
||||
predictions = _create_param_var(predictions, "predictions")
|
||||
targets = _create_param_var(targets, "targets")
|
||||
|
||||
with bb.function(self._loss_name, [predictions, targets]):
|
||||
with bb.dataflow():
|
||||
lv = abs(subtract(predictions, targets))
|
||||
loss = bb.emit_output(self._with_reduction(lv))
|
||||
bb.emit_func_output(loss)
|
||||
|
||||
return bb.get()[self._loss_name]
|
||||
|
||||
|
||||
class MSELoss(Loss):
|
||||
r"""Measures the element-wise mean squared error.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
reduction : Literal["mean", "sum", "none"]
|
||||
The reduction method to apply to output. Can be "mean", "sum" or "none".
|
||||
|
||||
none : no reduction will be applied,
|
||||
mean : the sum of the output will be divided by the batch_size,
|
||||
sum : the output will be summed.
|
||||
"""
|
||||
|
||||
def __init__(self, reduction: Literal["mean", "sum", "none"] = "mean") -> None:
|
||||
super().__init__("mse_loss", 1, reduction)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
predictions: Var | Type,
|
||||
targets: Var | Type,
|
||||
) -> Function:
|
||||
"""Get the relax function of MSELoss. If the parameters are
|
||||
type, it will create corresponding variables.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
predictions : Union[Var, Type]
|
||||
The predictions of the model in the calculation of loss.
|
||||
targets : Union[Var, Type]
|
||||
The ground truth in the calculation of loss.
|
||||
|
||||
Returns
|
||||
-------
|
||||
The relax function of MSELoss with the loss name as its global symbol.
|
||||
"""
|
||||
bb = BlockBuilder()
|
||||
|
||||
predictions = _create_param_var(predictions, "predictions")
|
||||
targets = _create_param_var(targets, "targets")
|
||||
|
||||
with bb.function(self._loss_name, [predictions, targets]):
|
||||
with bb.dataflow():
|
||||
lv = subtract(predictions, targets)
|
||||
lv = multiply(lv, lv)
|
||||
loss = bb.emit_output(self._with_reduction(lv))
|
||||
bb.emit_func_output(loss)
|
||||
|
||||
return bb.get()[self._loss_name]
|
||||
|
||||
|
||||
class CrossEntropyLoss(Loss):
|
||||
r"""CrossEntropyLoss. It is a combination of a log_softmax computation and a nll_loss.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
reduction : Literal["mean", "sum", "none"]
|
||||
The reduction method to apply to output. Can be "mean", "sum" or "none".
|
||||
|
||||
none : no reduction will be applied,
|
||||
mean : the sum of the output will be divided by the batch_size,
|
||||
sum : the output will be summed.
|
||||
|
||||
ignore_index : int
|
||||
Specifies a target value that is ignored and does not contribute to the input gradient.
|
||||
"""
|
||||
|
||||
ignore_index: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reduction: Literal["mean", "sum", "none"] = "mean",
|
||||
ignore_index: int = -100,
|
||||
) -> None:
|
||||
super().__init__("cross_entropy_loss", 1, reduction)
|
||||
self.ignore_index = ignore_index
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
predictions: Var | Type,
|
||||
targets: Var | Type,
|
||||
weights: Var | Type | None = None,
|
||||
) -> Function:
|
||||
"""Get the relax function of CrossEntropyLoss. If the parameters are
|
||||
type, it will create corresponding variables.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
predictions : Union[Var, Type]
|
||||
The predictions of the model in the calculation of loss.
|
||||
|
||||
targets : Union[Var, Type]
|
||||
The ground truth in the calculation of loss.
|
||||
|
||||
weights : Optional[Union[Var, Type]]
|
||||
a manual rescaling weight given to each class. It has to be a Tensor of size C.
|
||||
|
||||
Returns
|
||||
-------
|
||||
The relax function of CrossEntropyLoss with the loss name as its global symbol.
|
||||
"""
|
||||
bb = BlockBuilder()
|
||||
|
||||
predictions = _create_param_var(predictions, "predictions")
|
||||
targets = _create_param_var(targets, "targets")
|
||||
|
||||
arg_list = [predictions, targets]
|
||||
if weights:
|
||||
weights = _create_param_var(weights, "weights")
|
||||
arg_list.append(weights)
|
||||
|
||||
with bb.function(self._loss_name, arg_list):
|
||||
with bb.dataflow():
|
||||
logits = bb.emit(log_softmax(predictions))
|
||||
loss = bb.emit_output(
|
||||
nll_loss(logits, targets, weights, self._reduction, self.ignore_index)
|
||||
)
|
||||
bb.emit_func_output(loss)
|
||||
|
||||
return bb.get()[self._loss_name]
|
||||
|
||||
|
||||
class CategoricalCrossEntropyLoss(Loss):
|
||||
r"""CategoricalCrossEntropyLoss.
|
||||
It is a combination of a converting one-hot target vector to a label,
|
||||
a log_softmax computation and a nll_loss.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
reduction : Literal["mean", "sum", "none"]
|
||||
The reduction method to apply to output. Can be "mean", "sum" or "none".
|
||||
|
||||
none : no reduction will be applied,
|
||||
mean : the sum of the output will be divided by the batch_size,
|
||||
sum : the output will be summed.
|
||||
|
||||
ignore_index : int
|
||||
Specifies a target value that is ignored and does not contribute to the input gradient.
|
||||
"""
|
||||
|
||||
ignore_index: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reduction: Literal["mean", "sum", "none"] = "mean",
|
||||
ignore_index: int = -100,
|
||||
) -> None:
|
||||
super().__init__("categorical_cross_entropy_loss", 1, reduction)
|
||||
self.ignore_index = ignore_index
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
predictions: Var | Type,
|
||||
targets: Var | Type,
|
||||
weights: Var | Type | None = None,
|
||||
) -> Function:
|
||||
"""Get the relax function of CategoricalCrossEntropyLoss. If the parameters are
|
||||
type, it will create corresponding variables.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
predictions : Union[Var, Type]
|
||||
The predictions of the model in the calculation of loss.
|
||||
|
||||
targets : Union[Var, Type]
|
||||
The ground truth in the calculation of loss.
|
||||
|
||||
weights : Optional[Union[Var, Type]]
|
||||
a manual rescaling weight given to each class. It has to be a Tensor of size C.
|
||||
|
||||
Returns
|
||||
-------
|
||||
The relax function of CategoricalCrossEntropyLoss with the loss name as its global symbol.
|
||||
"""
|
||||
|
||||
if "int" not in str(targets.dtype):
|
||||
raise TypeError(
|
||||
f"Dtype of targets expected to be int/uint. \
|
||||
However, the dtype of targets is {targets.dtype}"
|
||||
)
|
||||
|
||||
bb = BlockBuilder()
|
||||
|
||||
predictions = _create_param_var(predictions, "predictions")
|
||||
targets = _create_param_var(targets, "targets")
|
||||
|
||||
arg_list = [predictions, targets]
|
||||
if weights:
|
||||
weights = _create_param_var(weights, "weights")
|
||||
arg_list.append(weights)
|
||||
|
||||
# In the case of ignore_index >= 0,
|
||||
# the nll_loss function is used to handle the ignore index.
|
||||
# In other cases where ignore_index is not needed, just use the simpe product.
|
||||
with bb.function(self._loss_name, arg_list):
|
||||
with bb.dataflow():
|
||||
logits = bb.emit(log_softmax(predictions))
|
||||
if self.ignore_index >= 0:
|
||||
targets = bb.emit(
|
||||
reshape(argmax(targets, axis=1), shape=(targets.ty.shape[0],))
|
||||
)
|
||||
loss = bb.emit_output(
|
||||
nll_loss(logits, targets, weights, self._reduction, self.ignore_index)
|
||||
)
|
||||
else:
|
||||
lv = bb.emit(-logits * targets.astype("float32"))
|
||||
if weights:
|
||||
lv = bb.emit(lv * weights)
|
||||
loss = bb.emit_output(self._with_reduction(lv))
|
||||
bb.emit_func_output(loss)
|
||||
|
||||
return bb.get()[self._loss_name]
|
||||
@@ -0,0 +1,722 @@
|
||||
# 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.
|
||||
# ruff: noqa: F401
|
||||
"""Provide abstraction for defining optimizers and a set of common optimizers."""
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Optional, Union
|
||||
|
||||
import numpy as np # type: ignore
|
||||
import tvm_ffi
|
||||
|
||||
import tvm
|
||||
|
||||
from ..block_builder import BlockBuilder
|
||||
from ..expr import Function, TupleGetItem, Var, const
|
||||
from ..expr import Tuple as RxTuple
|
||||
from ..op import add, divide, multiply, sqrt, subtract
|
||||
from ..type import TensorType, TupleType
|
||||
|
||||
|
||||
# TODO(chaofan, yixin): Migrate key logics to C++
|
||||
class Optimizer:
|
||||
"""Relax training optimizer. This class could generate relax Functions for optimizing specified
|
||||
parameters, and store the states used in the optimization process, such as momentum.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The name of the optimizer function. This parameter is provided by subclasses.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : str
|
||||
The only dtype of the optimizer. It will be used as the dtype of the optimizer states,
|
||||
and the dtype of necessary constants, such as the learning rate. Will be set in `init()`.
|
||||
|
||||
name : str
|
||||
The name of the optimizer.
|
||||
|
||||
param_list : List[Var]
|
||||
The list of variables to optimize. Will be set in `init()`.
|
||||
|
||||
state : tvm_ffi.Array
|
||||
`state` is an runtime Array representing the state of the optimizer. Will be set in
|
||||
`init()`.
|
||||
|
||||
The states of the optimizer can store necessary information in the optimization process at
|
||||
runtime, such as the number of steps, the momentum in momentum SGD, etc.
|
||||
|
||||
`opt.state` should be used as the last argument of the function that is got through
|
||||
`get_function()`, and its new value is returned as the last return value of that function.
|
||||
|
||||
See examples for more details.
|
||||
|
||||
Examples
|
||||
--------
|
||||
The usage of optimizers should resemble the following pattern. We will take SGD as an example.
|
||||
For detailed examples, please see the tutorial.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Construct the optimizer
|
||||
opt = relax.optimizer.SGD(0.1)
|
||||
|
||||
# Initialize the parameter list, the dtype and the optimizer state
|
||||
# x is the relax Var we want to optimize
|
||||
opt.init(x)
|
||||
|
||||
# The above two lines is equivalent to one line:
|
||||
opt = relax.optimizer.SGD(0.1).init(x)
|
||||
|
||||
# Get the optimizer function
|
||||
# mod is an IRModule constructed earlier
|
||||
mod["SGD"] = opt.get_function()
|
||||
|
||||
# Legalize and build mod
|
||||
lowered_mod = LegalizeOps()(mod)
|
||||
ex = build(lowered_mod, target="llvm")
|
||||
vm = VirtualMachine(ex, tvm.cpu())
|
||||
|
||||
# Optimization process
|
||||
# param_tuple is a runtime tuple of parameters
|
||||
# param_gradient is a runtime tuple of the gradient of the parameters in param_tuple,
|
||||
# respectively
|
||||
# param_gradient can be gained by the automatic differentiation pass. Please see
|
||||
# `relax.transform.Gradient`
|
||||
param_tuple, opt.state = vm["SGD"](param_tuple, param_gradient, opt.state)
|
||||
"""
|
||||
|
||||
dtype: str
|
||||
name: str
|
||||
param_list: list[Var]
|
||||
state: tvm_ffi.Array
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
self.param_list = None
|
||||
self.state = None
|
||||
self.dtype = None
|
||||
|
||||
def init(self, params: Var | list[Var]) -> "Optimizer":
|
||||
"""Set the parameters, determine the dtype, and construct the initial state for the
|
||||
optimizer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
params : Union[Var, List[Var]]
|
||||
The parameter or the list of parameters to optimize.
|
||||
|
||||
Parameters should all be Vars of floating point Tensors, including float32, float64,
|
||||
float16, etc. Currently, all parameters should have the same dtype, and that dtype
|
||||
will be used as the dtype of the optimizer states.
|
||||
|
||||
Returns
|
||||
-------
|
||||
self : Optimizer
|
||||
The optimizer itself.
|
||||
"""
|
||||
if not isinstance(params, list):
|
||||
params = [params]
|
||||
self._set_params_and_dtype(params)
|
||||
# State should be initialized in any implementation of optimizer.
|
||||
self.state = None
|
||||
return self
|
||||
|
||||
def _set_params_and_dtype(self, params: list[Var]) -> None:
|
||||
"""Check params is legal and set the param_list and dtype of the optimizer."""
|
||||
params_set = set()
|
||||
dtype = None
|
||||
for x in params:
|
||||
if not isinstance(x, Var):
|
||||
raise ValueError(f"Parameter {x} is not a Var")
|
||||
if not isinstance(x.ty, TensorType):
|
||||
raise ValueError(
|
||||
f"Optimizers only support Tensor parameters, but parameter {x.name_hint} has "
|
||||
f"type {x.ty}"
|
||||
)
|
||||
data_type = tvm.DataType(x.ty.dtype.dtype)
|
||||
if data_type.type_code not in (tvm.DataTypeCode.BFLOAT, tvm.DataTypeCode.FLOAT):
|
||||
raise ValueError(
|
||||
f"Optimizers only support Tensor parameters of floating point dtype, but dtype "
|
||||
f"of {x.name_hint} is {x.ty.dtype}"
|
||||
)
|
||||
if dtype is None:
|
||||
dtype = x.ty.dtype
|
||||
else:
|
||||
if dtype != x.ty.dtype:
|
||||
raise ValueError(
|
||||
f"All parameters should have the same dtype, but parameter {x.name_hint} "
|
||||
f"has dtype {x.ty.dtype}, which differs from the previous dtype "
|
||||
f"{dtype}"
|
||||
)
|
||||
if x in params_set:
|
||||
raise ValueError(f"Parameter {x.name_hint} appears more than once")
|
||||
params_set.add(x)
|
||||
self.param_list = params
|
||||
self.dtype = dtype
|
||||
|
||||
def _check_init(self):
|
||||
"""Check that the optimizer is initialized. This method should be called at the start of
|
||||
get_function().
|
||||
"""
|
||||
if self.param_list is None or self.state is None or self.dtype is None:
|
||||
raise RuntimeError("Please call init() for the optimizer before calling get_function()")
|
||||
|
||||
def get_function(self) -> Function:
|
||||
"""Use blockbuilder to construct an optimizer function that executes updates of the
|
||||
parameters and the optimizer state.
|
||||
|
||||
The optimizer function will take in a tuple of parameters, a tuple of gradients of
|
||||
parameters, and a tuple of optimizer states. It will return a tuple of updated parameters,
|
||||
and a tuple of optimizer states.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : Function
|
||||
The optimizer function.
|
||||
|
||||
Examples
|
||||
--------
|
||||
An example of the returned optimizer function. This function executes the stochastic
|
||||
gradient descent method with lr = 0.1.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@R.function
|
||||
def SGD(
|
||||
params: R.Tuple(R.Tensor((3, 3), "float32"), R.Tensor((3,), "float32")),
|
||||
gradients: R.Tuple(R.Tensor((3, 3), "float32"), R.Tensor((3,), "float32")),
|
||||
optim_states: R.Tuple(R.Tensor((), "int64")),
|
||||
) -> R.Tuple(
|
||||
R.Tuple(R.Tensor((3, 3), "float32"), R.Tensor((3,), "float32")),
|
||||
R.Tuple(R.Tensor((), "int64")),
|
||||
):
|
||||
with R.dataflow():
|
||||
num_steps: R.Tensor((), "int64") = optim_states[0]
|
||||
num_steps_new: R.Tensor((), "int64") = R.add(num_steps, R.const(1, "int64"))
|
||||
x: R.Tensor((3, 3), "float32") = params[0]
|
||||
x_grad: R.Tensor((3, 3), "float32") = gradients[0]
|
||||
lv: R.Tensor((3, 3), "float32") = R.multiply(R.const(0.01, "float32"), x_grad)
|
||||
x_new: R.Tensor((3, 3), "float32") = R.subtract(x, lv)
|
||||
y: R.Tensor((3,), "float32") = params[1]
|
||||
y_grad: R.Tensor((3,), "float32") = gradients[1]
|
||||
lv1: R.Tensor((3,), "float32") = R.multiply(R.const(0.01, "float32"), y_grad)
|
||||
y_new: R.Tensor((3,), "float32") = R.subtract(y, lv1)
|
||||
params_new: R.Tuple(R.Tensor((3, 3), "float32"), R.Tensor((3,), "float32")) = (
|
||||
x_new,
|
||||
y_new,
|
||||
)
|
||||
optim_states_new: R.Tuple(R.Tensor((), "int64")) = (num_steps_new,)
|
||||
R.output(params_new, optim_states_new)
|
||||
return (params_new, optim_states_new)
|
||||
"""
|
||||
self._check_init()
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
# TODO(chaofan, yixin): Support symbolic shapes
|
||||
def _get_shape_as_int_list(var: Var) -> list[int]:
|
||||
return [int(val) for val in var.ty.shape]
|
||||
|
||||
|
||||
# We need to subtract on hyperparameters, but do not want to introduce floating point error.
|
||||
# Floating point error would lead to a few problems, such as making assert_structural_equal not
|
||||
# pass in unit tests
|
||||
def _high_precision_subtract(lhs: float, rhs: float) -> float:
|
||||
return float(Decimal(str(lhs)) - Decimal(str(rhs)))
|
||||
|
||||
|
||||
class SGD(Optimizer):
|
||||
"""Implements stochastic gradient descent.
|
||||
|
||||
The returned function of `get_function()` is equivalent to the following numpy code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def SGD(param_tuple, grad_tuple, state_tuple):
|
||||
num_steps = state_tuple[0]
|
||||
param_tuple_new, state_tuple_new = [], []
|
||||
state_tuple_new.append(num_steps + 1)
|
||||
for i in range(len(param_tuple)):
|
||||
param = param_tuple[i]
|
||||
grad = grad_tuple[i]
|
||||
param_tuple_new.append(param - lr * (grad + weight_decay * param))
|
||||
return param_tuple_new, state_tuple_new
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lr : float
|
||||
learning rate
|
||||
|
||||
weight_decay : float
|
||||
weight decay (L2 penalty) (default: 0)
|
||||
"""
|
||||
|
||||
def __init__(self, lr: float, weight_decay: float = 0) -> None:
|
||||
super().__init__("SGD")
|
||||
self.lr = float(lr)
|
||||
self.weight_decay = float(weight_decay)
|
||||
|
||||
def init(self, params: Var | list[Var]) -> "SGD":
|
||||
"""Set the parameters, determine the dtype, and construct the initial state for the
|
||||
optimizer.
|
||||
|
||||
The state of SGD is `(num_steps,)`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
params : Union[Var, List[Var]]
|
||||
The parameter or the list of parameters to optimize.
|
||||
|
||||
Parameters should all be Vars of floating point Tensors, including float32, float64,
|
||||
float16, etc. Currently, all parameters should have the same dtype, and that dtype
|
||||
will be used as the dtype of the optimizer states.
|
||||
|
||||
Returns
|
||||
-------
|
||||
self : SGD
|
||||
The SGD optimizer itself.
|
||||
"""
|
||||
if not isinstance(params, list):
|
||||
params = [params]
|
||||
self._set_params_and_dtype(params)
|
||||
self.state = (
|
||||
# num_steps = 0
|
||||
tvm.runtime.tensor(np.zeros((), "int64")),
|
||||
)
|
||||
return self
|
||||
|
||||
def get_function(self) -> Function:
|
||||
"""Use blockbuilder to construct an optimizer function that executes updates of the
|
||||
parameters and the optimizer state. `init()` should be called before `get_function()`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : Function
|
||||
The optimizer function.
|
||||
"""
|
||||
self._check_init()
|
||||
|
||||
plist = self.param_list
|
||||
len_param = len(plist)
|
||||
dtype = self.dtype
|
||||
|
||||
# input variables
|
||||
param_var = Var("params", TupleType([p.ty for p in plist]))
|
||||
grad_var = Var("gradients", TupleType([p.ty for p in plist]))
|
||||
state_var = Var("optim_states", TupleType([TensorType((), "int64")]))
|
||||
|
||||
# constants
|
||||
lr = const(self.lr, dtype)
|
||||
weight_decay = const(self.weight_decay, dtype)
|
||||
one = const(1, "int64")
|
||||
|
||||
builder = BlockBuilder()
|
||||
with builder.function(self.name, [param_var, grad_var, state_var]):
|
||||
with builder.dataflow():
|
||||
param_list_new, state_list_new = [], []
|
||||
|
||||
# handle num_steps
|
||||
num_steps = builder.emit(TupleGetItem(state_var, 0), "num_steps")
|
||||
num_steps_new = builder.emit(add(num_steps, one), "num_steps_new")
|
||||
state_list_new.append(num_steps_new)
|
||||
|
||||
# computation logics
|
||||
for i in range(len_param):
|
||||
name = self.param_list[i].name_hint
|
||||
p = builder.emit(TupleGetItem(param_var, i), name)
|
||||
g = builder.emit(TupleGetItem(grad_var, i), name + "_grad")
|
||||
if self.weight_decay:
|
||||
g = builder.emit(add(multiply(weight_decay, p), g), name + "_grad_new")
|
||||
p_new = builder.emit(subtract(p, multiply(lr, g)), name + "_new")
|
||||
param_list_new.append(p_new)
|
||||
|
||||
# handle return values
|
||||
params_new = builder.emit_output(RxTuple(param_list_new), "params_new")
|
||||
optim_states_new = builder.emit_output(RxTuple(state_list_new), "optim_states_new")
|
||||
builder.emit_func_output((params_new, optim_states_new))
|
||||
return builder.get()[self.name]
|
||||
|
||||
|
||||
class MomentumSGD(Optimizer):
|
||||
"""Implements stochastic gradient descent with momentum. Optionally supports Nesterov
|
||||
momentum.
|
||||
|
||||
The returned function of `get_function()` is equivalent to the following numpy code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def MomentumSGD(param_tuple, grad_tuple, state_tuple):
|
||||
num_steps = state_tuple[0]
|
||||
param_tuple_new, state_tuple_new = [], []
|
||||
state_tuple_new.append(num_steps + 1)
|
||||
|
||||
for i in range(len(param_tuple)):
|
||||
param = param_tuple[i]
|
||||
grad = grad_tuple[i]
|
||||
velocity = state_tuple[i + 1]
|
||||
grad = param * weight_decay + grad
|
||||
velocity = momentum * velocity + grad * (1 - dampening)
|
||||
if nesterov:
|
||||
param = param - (grad + momentum * velocity) * lr
|
||||
else:
|
||||
param = param - velocity * lr
|
||||
param_tuple_new.append(param)
|
||||
state_tuple_new.append(velocity)
|
||||
|
||||
return param_tuple_new, state_tuple_new
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lr : float
|
||||
learning rate
|
||||
|
||||
momentum : float
|
||||
momentum factor (default: 0)
|
||||
|
||||
weight_decay : float
|
||||
weight decay (L2 penalty) (default: 0)
|
||||
|
||||
dampening : float
|
||||
dampening for momentum (default: 0)
|
||||
|
||||
nesterov : bool
|
||||
enables Nesterov momentum (default: False)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
lr: float,
|
||||
momentum: float,
|
||||
dampening: float = 0,
|
||||
weight_decay: float = 0,
|
||||
nesterov: bool = False,
|
||||
) -> None:
|
||||
super().__init__("MomentumSGD")
|
||||
self.lr = float(lr)
|
||||
self.momentum = float(momentum)
|
||||
self.weight_decay = float(weight_decay)
|
||||
self.dampening = float(dampening)
|
||||
self.nesterov = nesterov
|
||||
|
||||
def init(self, params: Var | list[Var]) -> "MomentumSGD":
|
||||
"""Set the parameters, determine the dtype, and construct the initial state for the
|
||||
optimizer.
|
||||
|
||||
The state of MomentumSGD is
|
||||
`(num_steps, velocity_of_param_0, ..., velocity_of_param_n-1)`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
params : Union[Var, List[Var]]
|
||||
The parameter or the list of parameters to optimize.
|
||||
|
||||
Parameters should all be Vars of floating point Tensors, including float32, float64,
|
||||
float16, etc. Currently, all parameters should have the same dtype, and that dtype
|
||||
will be used as the dtype of the optimizer states.
|
||||
|
||||
Returns
|
||||
-------
|
||||
self : MomentumSGD
|
||||
The MomentumSGD optimizer itself.
|
||||
"""
|
||||
if not isinstance(params, list):
|
||||
params = [params]
|
||||
self._set_params_and_dtype(params)
|
||||
self.state = (
|
||||
# num_steps = 0
|
||||
tvm.runtime.tensor(np.zeros((), "int64")),
|
||||
# v_{param} is initialized to all zeros
|
||||
*(
|
||||
tvm.runtime.tensor(np.zeros(_get_shape_as_int_list(p), p.ty.dtype.dtype))
|
||||
for p in self.param_list
|
||||
),
|
||||
)
|
||||
return self
|
||||
|
||||
def get_function(self) -> Function:
|
||||
"""Use blockbuilder to construct an optimizer function that executes updates of the
|
||||
parameters and the optimizer state. `init()` should be called before `get_function()`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : Function
|
||||
The optimizer function.
|
||||
"""
|
||||
self._check_init()
|
||||
plist = self.param_list
|
||||
len_param = len(plist)
|
||||
dtype = self.dtype
|
||||
|
||||
# input variables
|
||||
param_var = Var("params", TupleType([p.ty for p in plist]))
|
||||
grad_var = Var("gradients", TupleType([p.ty for p in plist]))
|
||||
state_var = Var(
|
||||
"optim_states",
|
||||
TupleType([TensorType((), "int64"), *(p.ty for p in plist)]),
|
||||
)
|
||||
|
||||
# constants
|
||||
lr = const(self.lr, dtype)
|
||||
momentum = const(self.momentum, dtype)
|
||||
weight_decay = const(self.weight_decay, dtype)
|
||||
dampening_inv = const(_high_precision_subtract(1, self.dampening), dtype)
|
||||
one = const(1, "int64")
|
||||
|
||||
builder = BlockBuilder()
|
||||
with builder.function(self.name, [param_var, grad_var, state_var]):
|
||||
with builder.dataflow():
|
||||
param_list_new, state_list_new = [], []
|
||||
|
||||
# handle num_steps
|
||||
num_steps = builder.emit(TupleGetItem(state_var, 0), "num_steps")
|
||||
num_steps_new = builder.emit(add(num_steps, one), "num_steps_new")
|
||||
state_list_new.append(num_steps_new)
|
||||
|
||||
# computation logics
|
||||
for i in range(len_param):
|
||||
name = self.param_list[i].name_hint
|
||||
p = builder.emit(TupleGetItem(param_var, i), name)
|
||||
g = builder.emit(TupleGetItem(grad_var, i), name + "_grad")
|
||||
v = builder.emit(TupleGetItem(state_var, i + 1), name + "_v")
|
||||
if self.weight_decay:
|
||||
g = builder.emit(add(multiply(weight_decay, p), g), name + "_grad_new")
|
||||
damp_g = multiply(dampening_inv, g) if self.dampening else g
|
||||
v_new = builder.emit(add(multiply(momentum, v), damp_g), name + "_v_new")
|
||||
g_new = (
|
||||
builder.emit(add(g, multiply(momentum, v_new)), name + "_g_nest")
|
||||
if self.nesterov
|
||||
else v_new
|
||||
)
|
||||
p_new = builder.emit(subtract(p, multiply(lr, g_new)), name + "_new")
|
||||
param_list_new.append(p_new)
|
||||
state_list_new.append(v_new)
|
||||
|
||||
# handle return values
|
||||
params_new = builder.emit_output(RxTuple(param_list_new), "params_new")
|
||||
optim_states_new = builder.emit_output(RxTuple(state_list_new), "optim_states_new")
|
||||
builder.emit_func_output((params_new, optim_states_new))
|
||||
return builder.get()[self.name]
|
||||
|
||||
|
||||
class Adam(Optimizer):
|
||||
"""Implements Adam optimization algorithm.
|
||||
|
||||
The returned function of `get_function()` is equivalent to the following numpy code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def Adam(param_tuple, grad_tuple, state_tuple):
|
||||
num_steps = state_tuple[0]
|
||||
num_steps_new = num_steps + 1
|
||||
|
||||
param_tuple_new = []
|
||||
state_tuple_new = [None] * len(state_tuple)
|
||||
state_tuple_new[0] = num_steps_new
|
||||
state_tuple_new[1] = state_tuple[1] * betas[0]
|
||||
state_tuple_new[2] = state_tuple[2] * betas[1]
|
||||
|
||||
for i in range(len(param_tuple)):
|
||||
param = param_tuple[i]
|
||||
grad = grad_tuple[i]
|
||||
m = state_tuple[i + 3]
|
||||
v = state_tuple[i + 3 + len(param_tuple)]
|
||||
grad = grad + weight_decay * param
|
||||
m = betas[0] * m + (1 - betas[0]) * grad
|
||||
v = betas[1] * v + (1 - betas[1]) * grad * grad
|
||||
m_hat = m / (1 - betas[0] ** num_steps_new)
|
||||
v_hat = v / (1 - betas[1] ** num_steps_new)
|
||||
param = param - lr * m_hat / (np.sqrt(v_hat) + eps)
|
||||
param_tuple_new.append(param)
|
||||
state_tuple_new[i + 3] = m
|
||||
state_tuple_new[i + 3 + len(param_tuple)] = v
|
||||
|
||||
return param_tuple_new, state_tuple_new
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lr : float
|
||||
learning rate
|
||||
|
||||
betas : Tuple[float, float]
|
||||
coefficients used for computing running averages of gradient and its square
|
||||
(default: (0.9, 0.999))
|
||||
|
||||
eps : float
|
||||
term added to the denominator to improve numerical stability (default: 1e-8)
|
||||
|
||||
weight_decay : float
|
||||
weight decay (L2 penalty) (default: 0)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
lr: float,
|
||||
betas: tuple[float, float] = (0.9, 0.999),
|
||||
eps: float = 1e-08,
|
||||
weight_decay: float = 0,
|
||||
) -> None:
|
||||
super().__init__("Adam")
|
||||
self.lr = float(lr)
|
||||
self.beta1 = float(betas[0])
|
||||
self.beta2 = float(betas[1])
|
||||
self.eps = float(eps)
|
||||
self.weight_decay = float(weight_decay)
|
||||
|
||||
def init(self, params: Var | list[Var]) -> "Adam":
|
||||
"""Set the parameters, determine the dtype, and construct the initial state for the
|
||||
optimizer.
|
||||
|
||||
The state of Adam is
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
(
|
||||
num_steps,
|
||||
beta_0_prod, # beta0 ** num_steps
|
||||
beta_1_prod, # beta1 ** num_steps
|
||||
first_momentum_of_param_0, ..., first_momentum_of_param_n-1,
|
||||
second_momentum_of_param_0, ..., second_momentum_of_param_n-1
|
||||
)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
params : Union[Var, List[Var]]
|
||||
The parameter or the list of parameters to optimize.
|
||||
|
||||
Parameters should all be Vars of floating point Tensors, including float32, float64,
|
||||
float16, etc. Currently, all parameters should have the same dtype, and that dtype
|
||||
will be used as the dtype of the optimizer states.
|
||||
|
||||
Returns
|
||||
-------
|
||||
self : Adam
|
||||
The Adam optimizer itself.
|
||||
"""
|
||||
if not isinstance(params, list):
|
||||
params = [params]
|
||||
self._set_params_and_dtype(params)
|
||||
self.state = (
|
||||
# num_steps, beta_0_prod, beta_1_prod
|
||||
tvm.runtime.tensor(np.zeros((), "int64")),
|
||||
tvm.runtime.tensor(np.ones((), self.dtype)),
|
||||
tvm.runtime.tensor(np.ones((), self.dtype)),
|
||||
# first_momentum
|
||||
*(
|
||||
tvm.runtime.tensor(np.zeros(_get_shape_as_int_list(p), p.ty.dtype.dtype))
|
||||
for p in self.param_list
|
||||
),
|
||||
# second_momentum
|
||||
*(
|
||||
tvm.runtime.tensor(np.zeros(_get_shape_as_int_list(p), p.ty.dtype.dtype))
|
||||
for p in self.param_list
|
||||
),
|
||||
)
|
||||
return self
|
||||
|
||||
def get_function(self) -> Function:
|
||||
"""Use blockbuilder to construct an optimizer function that executes updates of the
|
||||
parameters and the optimizer state. `init()` should be called before `get_function()`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
func : Function
|
||||
The optimizer function.
|
||||
"""
|
||||
self._check_init()
|
||||
plist = self.param_list
|
||||
len_param = len(plist)
|
||||
dtype = self.dtype
|
||||
|
||||
# input variables
|
||||
param_var = Var("params", TupleType([p.ty for p in plist]))
|
||||
grad_var = Var("gradients", TupleType([p.ty for p in plist]))
|
||||
state_var = Var(
|
||||
"optim_states",
|
||||
TupleType(
|
||||
[
|
||||
TensorType((), "int64"),
|
||||
TensorType((), dtype),
|
||||
TensorType((), dtype),
|
||||
*(p.ty for p in plist),
|
||||
*(p.ty for p in plist),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# constants
|
||||
lr = const(self.lr, dtype)
|
||||
beta1 = const(self.beta1, dtype)
|
||||
beta2 = const(self.beta2, dtype)
|
||||
beta1_inv = const(_high_precision_subtract(1, self.beta1), dtype)
|
||||
beta2_inv = const(_high_precision_subtract(1, self.beta2), dtype)
|
||||
eps = const(self.eps, dtype)
|
||||
weight_decay = const(self.weight_decay, dtype)
|
||||
one_int = const(1, "int64")
|
||||
one_float = const(1, dtype)
|
||||
|
||||
builder = BlockBuilder()
|
||||
with builder.function(self.name, [param_var, grad_var, state_var]):
|
||||
with builder.dataflow():
|
||||
param_list_new = []
|
||||
state_list_new = [None] * (len_param * 2 + 3) # type: List[Optional[Var]]
|
||||
|
||||
# handle num_steps
|
||||
num_steps = builder.emit(TupleGetItem(state_var, 0), "num_steps")
|
||||
num_steps_new = builder.emit(add(num_steps, one_int), "num_steps_new")
|
||||
state_list_new[0] = num_steps_new
|
||||
beta1_prod = builder.emit(multiply(TupleGetItem(state_var, 1), beta1), "beta1_prod")
|
||||
beta2_prod = builder.emit(multiply(TupleGetItem(state_var, 2), beta2), "beta2_prod")
|
||||
state_list_new[1] = beta1_prod
|
||||
state_list_new[2] = beta2_prod
|
||||
|
||||
# computation logics
|
||||
for i in range(len_param):
|
||||
name = self.param_list[i].name_hint
|
||||
p = builder.emit(TupleGetItem(param_var, i), name)
|
||||
g = builder.emit(TupleGetItem(grad_var, i), name + "_grad")
|
||||
m = builder.emit(TupleGetItem(state_var, i + 3), name + "_m")
|
||||
v = builder.emit(TupleGetItem(state_var, i + 3 + len_param), name + "_v")
|
||||
if self.weight_decay:
|
||||
g = builder.emit(add(multiply(weight_decay, p), g), name + "_grad_new")
|
||||
m_new = builder.emit(
|
||||
add(multiply(beta1, m), multiply(beta1_inv, g)), name + "_m_new"
|
||||
)
|
||||
v_new = builder.emit(
|
||||
add(multiply(beta2, v), multiply(beta2_inv, multiply(g, g))),
|
||||
name + "_v_new",
|
||||
)
|
||||
m_hat = builder.emit(
|
||||
divide(m_new, subtract(one_float, state_list_new[1])), name + "_m_hat"
|
||||
)
|
||||
v_hat = builder.emit(
|
||||
divide(v_new, subtract(one_float, state_list_new[2])), name + "_v_hat"
|
||||
)
|
||||
p_new = builder.emit(
|
||||
subtract(p, multiply(lr, divide(m_hat, add(sqrt(v_hat), eps)))),
|
||||
name + "_new",
|
||||
)
|
||||
param_list_new.append(p_new)
|
||||
state_list_new[i + 3] = m_new
|
||||
state_list_new[i + 3 + len_param] = v_new
|
||||
|
||||
# handle return values
|
||||
params_new = builder.emit_output(RxTuple(param_list_new), "params_new")
|
||||
optim_states_new = builder.emit_output(RxTuple(state_list_new), "optim_states_new")
|
||||
builder.emit_func_output((params_new, optim_states_new))
|
||||
return builder.get()[self.name]
|
||||
@@ -0,0 +1,213 @@
|
||||
# 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=not-callable, unused-argument
|
||||
"""Setup Trainer Pass."""
|
||||
|
||||
import tvm
|
||||
from tvm.ir.module import IRModule
|
||||
from tvm.tirx.expr import IntImm
|
||||
|
||||
from ..analysis import check_well_formed
|
||||
from ..expr import Tuple
|
||||
from ..training.utils import AppendLoss
|
||||
from ..transform import DecomposeOpsForInference, DecomposeOpsForTraining, Gradient, LegalizeOps
|
||||
from ..type import TensorType
|
||||
from .loss import Loss
|
||||
from .optimizer import Optimizer
|
||||
|
||||
|
||||
@tvm.transform.module_pass(opt_level=0, name="SetupTrainer")
|
||||
class SetupTrainer:
|
||||
"""Transform a backbone module to a complete, legalized trainer module.
|
||||
|
||||
The provided backbone module should contain at least a function named `backbone`, and has two
|
||||
int attributes `param_num` and `state_num`, as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@I.ir_module
|
||||
class Backbone:
|
||||
I.module_attrs({"param_num": 1, "state_num": 1})
|
||||
@R.function
|
||||
def backbone(input_instances, parameters, states):
|
||||
# Predicts the result
|
||||
# Should contain only one DataflowBlock
|
||||
...
|
||||
return backbone_result, updated_states
|
||||
|
||||
Here each of input_instances, parameters, states, backbone_result and updated_states can
|
||||
denote a number of parameters. The length of parameters and the length of states is specified
|
||||
by param_num and state_num respectively.
|
||||
|
||||
`states` denote the states that we need to maintain as the training process proceeds, such as
|
||||
the running mean and the running var of the batch norm operator. The updated states is returned
|
||||
in `updated_states`. States can be empty if there is no state that needs to be updated.
|
||||
|
||||
The transformed module will at least contain the functions and attributes listed below:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@I.ir_module
|
||||
class Module:
|
||||
I.module_attrs({"input_num": 1, "param_num": 1, "state_num": 1, "optim_states": ...})
|
||||
|
||||
@R.function
|
||||
def backbone(input_instances, parameters, states):
|
||||
# Predicts the result. It is provided in the input module.
|
||||
...
|
||||
return backbone_result, updated_states
|
||||
|
||||
@R.function
|
||||
def backbone_loss(input_instances, parameters, states, targets):
|
||||
# Runs like backbone and then computes the loss between the result and targets.
|
||||
...
|
||||
return loss, updated_states
|
||||
|
||||
@R.function
|
||||
def backbone_loss_adjoint(input_instances, parameters, states, targets):
|
||||
# Runs like backbone_loss and then calculates the gradient of parameters.
|
||||
...
|
||||
return (loss, updated_states), gradient_of_params
|
||||
|
||||
@R.function
|
||||
def optimizer(params, gradients, optim_states):
|
||||
# Update parameters and optimizer states with the gradient computed
|
||||
...
|
||||
return (updated_params, updated_optim_states)
|
||||
|
||||
The transformed module contains an attribute `optim_states` as the initial optimizer states.
|
||||
|
||||
Then the transformed module will be legalized by `relax.transform.LegalizeOps()` to lower
|
||||
relax operators into TIR functions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
loss : Loss
|
||||
The loss function. It will be appended to the backbone function using
|
||||
relax.transform.AppendLoss.
|
||||
|
||||
optimizer : Optimizer
|
||||
The optimizer. It will be put as the `optimizer` function of the transformed module.
|
||||
|
||||
loss_args : List[TensorType]
|
||||
The arguments to call the loss function.
|
||||
|
||||
legalize : bool
|
||||
Whether to legalize the module. Default: True.
|
||||
"""
|
||||
|
||||
BACKBONE_FUNC: str = "backbone"
|
||||
BACKBONE_LOSS_FUNC: str = "backbone_loss"
|
||||
ADJOINT_FUNC: str = "backbone_loss_adjoint"
|
||||
OPTIMIZER_FUNC: str = "optimizer"
|
||||
|
||||
PARAM_NUM_ATTR_KEY: str = "param_num"
|
||||
STATE_NUM_ATTR_KEY: str = "state_num"
|
||||
|
||||
def __init__(
|
||||
self, loss: Loss, optimizer: Optimizer, loss_args: list[TensorType], legalize=True
|
||||
):
|
||||
self._loss = loss
|
||||
self._optimizer = optimizer
|
||||
self._loss_args = loss_args
|
||||
self._legalize = legalize
|
||||
|
||||
def _check_well_formed(self, mod: IRModule):
|
||||
if not check_well_formed(mod):
|
||||
raise ValueError("SetupTrainer: The backbone module is not well formed.")
|
||||
try:
|
||||
func = mod[self.BACKBONE_FUNC]
|
||||
except (KeyError, ValueError) as exc:
|
||||
raise ValueError(
|
||||
f"SetupTrainer: The backbone module does not contain a function named "
|
||||
f"{self.BACKBONE_FUNC}"
|
||||
) from exc
|
||||
|
||||
# Check function attrs
|
||||
if self.PARAM_NUM_ATTR_KEY not in mod.attrs or not isinstance(
|
||||
mod.attrs[self.PARAM_NUM_ATTR_KEY], IntImm | int
|
||||
):
|
||||
raise ValueError(
|
||||
f"SetupTrainer: The backbone module should has an integer attribute named "
|
||||
f"{self.PARAM_NUM_ATTR_KEY}"
|
||||
)
|
||||
if self.STATE_NUM_ATTR_KEY not in mod.attrs or not isinstance(
|
||||
mod.attrs[self.STATE_NUM_ATTR_KEY], IntImm | int
|
||||
):
|
||||
raise ValueError(
|
||||
f"SetupTrainer: The backbone module should has an integer attribute named "
|
||||
f"{self.STATE_NUM_ATTR_KEY}"
|
||||
)
|
||||
|
||||
nparam = int(mod.attrs[self.PARAM_NUM_ATTR_KEY])
|
||||
nstate = int(mod.attrs[self.STATE_NUM_ATTR_KEY])
|
||||
|
||||
# Check parameters and return values
|
||||
if len(func.params) < nparam + nstate:
|
||||
raise ValueError(
|
||||
"SetupTrainer: The number of parameters of the predict function should be no less "
|
||||
"than the number of parameters and states"
|
||||
)
|
||||
|
||||
if nstate > 0:
|
||||
if not isinstance(func.body.body, Tuple) or len(func.body.body) <= nstate:
|
||||
raise ValueError(
|
||||
"SetupTrainer: When model state exists, the predict function should return a "
|
||||
"tuple of length more than the number of states"
|
||||
)
|
||||
|
||||
def transform_module(self, mod: IRModule, ctx: tvm.transform.PassContext) -> IRModule:
|
||||
"""Transform the backbone module into a trainer module."""
|
||||
self._check_well_formed(mod)
|
||||
|
||||
mod = AppendLoss(
|
||||
self.BACKBONE_FUNC,
|
||||
self._loss(*self._loss_args), # type: ignore
|
||||
self._loss.num_backbone_outputs,
|
||||
self.BACKBONE_LOSS_FUNC,
|
||||
)(mod)
|
||||
|
||||
# Decompose batch_norm operator, which behaves differently in inference and training stages
|
||||
mod = DecomposeOpsForInference(self.BACKBONE_FUNC)(mod)
|
||||
mod = DecomposeOpsForTraining(self.BACKBONE_LOSS_FUNC)(mod)
|
||||
|
||||
# Gradient pass.
|
||||
param_num = int(mod.attrs[self.PARAM_NUM_ATTR_KEY])
|
||||
state_num = int(mod.attrs[self.STATE_NUM_ATTR_KEY])
|
||||
input_num = len(mod[self.BACKBONE_FUNC].params) - param_num - state_num
|
||||
params = mod[self.BACKBONE_LOSS_FUNC].params[input_num : input_num + param_num]
|
||||
mod = Gradient(self.BACKBONE_LOSS_FUNC, require_grads=params, target_index=0)(mod)
|
||||
|
||||
# Add optimizer function.
|
||||
self._optimizer.init(params)
|
||||
# Need the global symbol to match the function's name
|
||||
mod[self.OPTIMIZER_FUNC] = self._optimizer.get_function().with_attr(
|
||||
"global_symbol", self.OPTIMIZER_FUNC
|
||||
)
|
||||
|
||||
# Module attrs
|
||||
mod = mod.with_attrs(
|
||||
{
|
||||
"input_num": input_num,
|
||||
"optim_state": self._optimizer.state,
|
||||
}
|
||||
)
|
||||
|
||||
if self._legalize:
|
||||
mod = LegalizeOps()(mod)
|
||||
|
||||
return mod
|
||||
@@ -0,0 +1,349 @@
|
||||
# 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=invalid-name
|
||||
"""Unified Trainer API for relax training."""
|
||||
|
||||
import numpy as np # type: ignore
|
||||
|
||||
import tvm
|
||||
from tvm import relax
|
||||
from tvm.ir.module import IRModule
|
||||
from tvm.runtime._tensor import Tensor
|
||||
|
||||
|
||||
class Trainer:
|
||||
r"""Unified wrapper for relax training. It accepts the IRModule (that is the result of
|
||||
SetupTrainer) and the relax VM (that contains the built result of the IRModule), and helps run
|
||||
the VM. It maintains the parameters, the model states and the optimizer states internally.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
train_mod : tvm.IRModule
|
||||
The IRModule that will be run. Should be the result of a backbone module being transformed
|
||||
by the SetupTrainer pass.
|
||||
|
||||
vm : tvm.relax.VirtualMachine
|
||||
The relax virtual machine that contains the built result of train_mod. Considering the
|
||||
complexity and flexibility of building, we require user build the train_mod outside of
|
||||
trainer and pass the result vm.
|
||||
|
||||
device : tvm.runtime.Device
|
||||
The device to place the parameters and states in.
|
||||
|
||||
zero_init_param_state : bool
|
||||
If true, all parameters and states will be inited to zero. It requires all parameters and
|
||||
states have static shape.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. code-block:: python
|
||||
|
||||
setup_trainer = SetupTrainer(
|
||||
MSELoss(reduction="sum"),
|
||||
SGD(0.001),
|
||||
[pred_ty, target_ty],
|
||||
)
|
||||
train_mod = setup_trainer(Backbone)
|
||||
ex = tvm.compile(train_mod, target)
|
||||
vm = relax.VirtualMachine(ex, dev)
|
||||
|
||||
trainer = training.Trainer(train_mod, vm, dev, False)
|
||||
|
||||
trainer.xaiver_uniform_init_params()
|
||||
trainer.predict(input_instances)
|
||||
trainer.update([input_instances], [labels])
|
||||
"""
|
||||
|
||||
BACKBONE_FUNC: str = "backbone"
|
||||
BACKBONE_LOSS_FUNC: str = "backbone_loss"
|
||||
ADJOINT_FUNC: str = "backbone_loss_adjoint"
|
||||
OPTIMIZER_FUNC: str = "optimizer"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
train_mod: IRModule,
|
||||
vm: relax.VirtualMachine,
|
||||
device: tvm.runtime.Device,
|
||||
zero_init_param_state: bool = True,
|
||||
) -> None:
|
||||
self.mod = train_mod.without_attr("optim_state")
|
||||
self.vm = vm
|
||||
self.device = device
|
||||
|
||||
self._optim_state = [d.copyto(device) for d in train_mod.attrs["optim_state"]]
|
||||
|
||||
self._input_num = int(train_mod.attrs["input_num"])
|
||||
self._param_num = int(train_mod.attrs["param_num"])
|
||||
self._state_num = int(train_mod.attrs["state_num"])
|
||||
|
||||
# are used to initialize params and states
|
||||
self._param_vars = train_mod[self.ADJOINT_FUNC].params[
|
||||
self._input_num : self._input_num + self._param_num
|
||||
]
|
||||
self._state_vars = train_mod[self.ADJOINT_FUNC].params[
|
||||
(self._input_num + self._param_num) : (
|
||||
self._input_num + self._param_num + self._state_num
|
||||
)
|
||||
]
|
||||
|
||||
self._params: list[Tensor | None] = [None] * self._param_num
|
||||
self._param_name_to_pos: dict[str, int] = {
|
||||
p.name_hint: i for i, p in enumerate(self._param_vars)
|
||||
}
|
||||
|
||||
self._states: list[Tensor | None] = [None] * self._state_num
|
||||
self._state_name_to_pos: dict[str, int] = {
|
||||
s.name_hint: i for i, s in enumerate(self._state_vars)
|
||||
}
|
||||
|
||||
if zero_init_param_state:
|
||||
self.zero_init_params()
|
||||
self.zero_init_states()
|
||||
|
||||
@staticmethod
|
||||
def _get_shape_list(expr):
|
||||
return [int(dim) for dim in expr.ty.shape]
|
||||
|
||||
def xaiver_uniform_init_params(self):
|
||||
"""Xaiver uniformly initialize parameters using the method described in `Understanding the
|
||||
difficulty of training deep feedforward neural networks` - Glorot, X. & Bengio, Y.
|
||||
(2010).
|
||||
|
||||
Requires all parameters have static shapes.
|
||||
"""
|
||||
self._params = []
|
||||
for p in self._param_vars:
|
||||
shape, dtype = self._get_shape_list(p), p.ty.dtype
|
||||
self._params.append(
|
||||
tvm.runtime.tensor(
|
||||
(np.sqrt(6.0 / np.sum(shape)) * np.random.uniform(-1.0, 1.0, shape)).astype(
|
||||
dtype
|
||||
),
|
||||
self.device,
|
||||
)
|
||||
)
|
||||
|
||||
def zero_init_params(self):
|
||||
"""Zero initialize all parameters. Requires all parameters have static shapes."""
|
||||
self._params = [
|
||||
tvm.runtime.tensor(np.zeros(self._get_shape_list(p), p.ty.dtype.dtype), self.device)
|
||||
for p in self._param_vars
|
||||
]
|
||||
|
||||
def zero_init_states(self):
|
||||
"""Zero initialize all states. Requires all states have static shapes."""
|
||||
self._states = [
|
||||
tvm.runtime.tensor(np.zeros(self._get_shape_list(s), s.ty.dtype.dtype), self.device)
|
||||
for s in self._state_vars
|
||||
]
|
||||
|
||||
def load_params(
|
||||
self,
|
||||
params: list[np.ndarray | Tensor] | dict[str, np.ndarray | Tensor],
|
||||
):
|
||||
"""Load parameters from a dict or a list. Will convert parameters into tvm.runtime.Tensor
|
||||
in self.device.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
params : List[Union[np.ndarray, Tensor]], Dict[str, Union[np.ndarray, Tensor]]
|
||||
The numerical value of the parameters.
|
||||
|
||||
If params is a list, its length should be param_num. The value of parameters at the
|
||||
corresponding index will be updated.
|
||||
|
||||
If params is a dict, it should map variable name to value. The name should be the same
|
||||
as the parameter name in the backbone function. The values of the corresponding
|
||||
parameters will be updated.
|
||||
"""
|
||||
if isinstance(params, list):
|
||||
if len(params) != self._param_num:
|
||||
raise ValueError(
|
||||
f"The length of extern parameters is {len(params)}, which does not "
|
||||
f"match the number of parameters {self._param_num}"
|
||||
)
|
||||
self._params = [tvm.runtime.tensor(v, self.device) for v in params]
|
||||
elif isinstance(params, dict):
|
||||
for key, val in params.items():
|
||||
if key not in self._param_name_to_pos:
|
||||
raise ValueError(f"Parameter {key} is not found in the model")
|
||||
self._params[self._param_name_to_pos[key]] = tvm.runtime.tensor(val, self.device)
|
||||
else:
|
||||
raise ValueError("The type of extern_params should be either list or dict")
|
||||
|
||||
def load_states(
|
||||
self,
|
||||
states: list[np.ndarray | Tensor] | dict[str, np.ndarray | Tensor],
|
||||
):
|
||||
"""Load model states from a dict or a list. Will convert states into tvm.runtime.Tensor
|
||||
in self.device.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
states : List[Union[np.ndarray, Tensor]], Dict[str, Union[np.ndarray, Tensor]]
|
||||
The numerical value of the model states.
|
||||
|
||||
If states is a list, its length should be state_num. The value of states at the
|
||||
corresponding index will be updated.
|
||||
|
||||
If params is a dict, it should map variable name to value. The name should be the same
|
||||
as the state name in the backbone function. The values of the corresponding states will
|
||||
be updated.
|
||||
"""
|
||||
if isinstance(states, list):
|
||||
if len(states) != self._state_num:
|
||||
raise ValueError(
|
||||
f"The length of extern states is {len(states)}, which does not match "
|
||||
f"the number of model states {self._state_num}"
|
||||
)
|
||||
self._states = [tvm.runtime.tensor(v, self.device) for v in states]
|
||||
elif isinstance(states, dict):
|
||||
for key, val in states.items():
|
||||
if key not in self._param_name_to_pos:
|
||||
raise ValueError(f"Parameter {key} is not found in the model")
|
||||
self._states[self._param_name_to_pos[key]] = tvm.runtime.tensor(val, self.device)
|
||||
else:
|
||||
raise ValueError("The type of extern_states should be either list or dict")
|
||||
|
||||
def export_params(self) -> dict[str, Tensor]:
|
||||
"""Export parameters to a dict (parameter name -> Tensor).
|
||||
|
||||
Returns
|
||||
-------
|
||||
exported_dict : Dict[str, Tensor]
|
||||
The exported dictionary of parameters.
|
||||
"""
|
||||
return {key: self._params[pos] for key, pos in self._param_name_to_pos.items()}
|
||||
|
||||
def export_states(self) -> dict[str, Tensor]:
|
||||
"""Export model states to a dict (parameter name -> Tensor).
|
||||
|
||||
Returns
|
||||
-------
|
||||
exported_dict : Dict[str, Tensor]
|
||||
The exported dictionary of model states.
|
||||
"""
|
||||
return {key: self._states[pos] for key, pos in self._state_name_to_pos.items()}
|
||||
|
||||
def _check_inited(self):
|
||||
"""Check that all parameters and model states are initialized."""
|
||||
idx_not_inited_param = next((i for i, p in enumerate(self._params) if p is None), -1)
|
||||
if idx_not_inited_param != -1:
|
||||
raise RuntimeError(
|
||||
f"The {idx_not_inited_param}-th parameter is not initialized before training or "
|
||||
"inference."
|
||||
)
|
||||
|
||||
idx_not_inited_state = next((i for i, s in enumerate(self._states) if s is None), -1)
|
||||
if idx_not_inited_state != -1:
|
||||
raise RuntimeError(
|
||||
f"The {idx_not_inited_state}-th model state is not initialized before training or "
|
||||
"inference."
|
||||
)
|
||||
|
||||
def predict(self, *input_instances: np.ndarray | Tensor) -> Tensor:
|
||||
"""Call the `backbone` function and return the prediction result of the backbone.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*input_instances : Union[np.ndarray, Tensor]
|
||||
The values corresponding to the input_instances part of the backbone function.
|
||||
Parameters and model states are not needed to provide.
|
||||
|
||||
Returns
|
||||
-------
|
||||
output : Tensor
|
||||
The result of the backbone function. If the backbone contains model states, the updated
|
||||
states WILL NOT be returned.
|
||||
"""
|
||||
self._check_inited()
|
||||
if len(input_instances) != self._input_num:
|
||||
raise ValueError("The length of the input does not match the backbone")
|
||||
all_inputs: list[Tensor] = (
|
||||
[tvm.runtime.tensor(i, self.device) for i in input_instances]
|
||||
+ self._params
|
||||
+ self._states
|
||||
)
|
||||
res = self.vm[self.BACKBONE_FUNC](*all_inputs)
|
||||
|
||||
# remove the states part, if they exist
|
||||
if self._state_num != 0:
|
||||
res = res[: -self._state_num]
|
||||
if len(res) == 1:
|
||||
res = res[0]
|
||||
return res
|
||||
|
||||
def update(
|
||||
self,
|
||||
input_instances: np.ndarray | Tensor | list[np.ndarray | Tensor],
|
||||
targets: np.ndarray | Tensor | list[np.ndarray | Tensor],
|
||||
) -> Tensor:
|
||||
"""Update parameters and model states. It will calculate the gradients of parameters
|
||||
and update them using the `optimizer` function.
|
||||
|
||||
Parameters, model states and optimizer states are provided in the function, so you do not
|
||||
need to provied them.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_instances : Union[np.ndarray, Tensor, List[Union[np.ndarray, Tensor]]]
|
||||
The values corresponding to the input_instances part of the backbone function.
|
||||
Parameters and model states are not needed to provide.
|
||||
|
||||
If there are more than one input instances, you can provide a list.
|
||||
|
||||
targets : Union[np.ndarray, Tensor, List[Union[np.ndarray, Tensor]]]
|
||||
The values corresponding to the targets part of the backbone function.
|
||||
|
||||
If there are more than one targets, you can provide a list.
|
||||
|
||||
Returns
|
||||
-------
|
||||
loss : Tensor
|
||||
The loss stored in tvm.runtime.Tensor.
|
||||
"""
|
||||
self._check_inited()
|
||||
|
||||
if not isinstance(input_instances, list):
|
||||
input_instances = [input_instances]
|
||||
|
||||
if not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
if len(input_instances) != self._input_num:
|
||||
raise ValueError("The length of the input does not match the backbone")
|
||||
|
||||
all_inputs: list[Tensor] = (
|
||||
[tvm.runtime.tensor(i, self.device) for i in input_instances]
|
||||
+ self._params
|
||||
+ self._states
|
||||
+ [tvm.runtime.tensor(i, self.device) for i in targets]
|
||||
)
|
||||
ret, grads = self.vm[self.ADJOINT_FUNC](*all_inputs)
|
||||
|
||||
# update model states
|
||||
if self._state_num != 0:
|
||||
self._states = list(ret[1:])
|
||||
ret = ret[0]
|
||||
|
||||
# update params
|
||||
new_params, self._optim_state = self.vm[self.OPTIMIZER_FUNC](
|
||||
self._params, grads, self._optim_state
|
||||
)
|
||||
self._params = list(new_params)
|
||||
|
||||
return ret
|
||||
@@ -0,0 +1,211 @@
|
||||
# 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=invalid-name, unused-argument
|
||||
"""Utility functions for relax training."""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from tvm_ffi import register_global_func
|
||||
|
||||
import tvm
|
||||
from tvm import relax
|
||||
from tvm.ir import Call
|
||||
from tvm.relax.block_builder import BlockBuilder
|
||||
|
||||
from ..expr import Function, Var
|
||||
from . import _ffi_api
|
||||
|
||||
|
||||
def AppendLoss(
|
||||
func_name: str,
|
||||
loss_function: Function,
|
||||
num_backbone_outputs: int = 1,
|
||||
new_func_name: str | None = None,
|
||||
) -> tvm.ir.transform.Pass:
|
||||
"""Append the loss function to the backbone function specified by `func_name`. Generally, the
|
||||
loss function is generated by instances of `relax.training.Loss`.
|
||||
|
||||
The backbone function and the loss function should satisfy a few restrictions:
|
||||
- Both backbone and loss should contain exactly one DataflowBlock.
|
||||
- Backbone should return either one Var, or a tuple of Vars
|
||||
- Loss should return a scalar(0-dim Tensor) Var
|
||||
|
||||
They should be like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@R.function
|
||||
def backbone(input_instances, parameters, states):
|
||||
with R.dataflow():
|
||||
# Predicts the result
|
||||
...
|
||||
return backbone_result, updated_states
|
||||
|
||||
@R.function
|
||||
def loss(backbone_result, targets):
|
||||
with R.dataflow():
|
||||
# calculate the loss between backbone_result and targets
|
||||
...
|
||||
# loss should be a scalar Var
|
||||
return loss
|
||||
|
||||
Here each of input_instances, parameters, states, backbone_result and updated_states can
|
||||
denote a number of parameters.
|
||||
|
||||
`states` denote the states that we need to maintain as the training process proceeds, such as
|
||||
the running mean and the running var of the batch norm operator. The updated states is returned
|
||||
in `updated_states`. States can be empty if there is no state that needs to be updated.
|
||||
|
||||
The appended result contains only one DataflowBlock containing all bindings in backbone and
|
||||
loss. It will be like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@R.function
|
||||
def backbone_loss(input_instances, parameters, states, targets):
|
||||
with R.dataflow():
|
||||
# all bindings in backbone and loss
|
||||
...
|
||||
return loss, updated_states
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func_name : str
|
||||
The name of the backbone function in the IRModule.
|
||||
|
||||
loss_func : Function
|
||||
The loss function.
|
||||
|
||||
num_backbone_outputs : int
|
||||
Specify the number of `prediction_outputs` of the backbone function. Default: 1.
|
||||
|
||||
new_func_name : Optional[str]
|
||||
Specify the name of the appended result. If it is not specified, the name will be
|
||||
`func_name + "_loss"`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ret : Function
|
||||
The result function.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. code-block:: python
|
||||
|
||||
@I.ir_module
|
||||
class Module
|
||||
@R.function
|
||||
def predict(x: R.Tensor((2, 4), "float32"), y: R.Tensor((2, 4), "float32")):
|
||||
with R.dataflow():
|
||||
out = R.add(x, y)
|
||||
R.output(out)
|
||||
return out
|
||||
|
||||
@R.function
|
||||
def loss(predictions: R.Tensor((2, 4), "float32"), labels: R.Tensor((2, 4), "float32")):
|
||||
with R.dataflow():
|
||||
lv = R.subtract(predictions, labels)
|
||||
lv1 = R.multiply(lv, lv)
|
||||
gv = R.sum(lv1)
|
||||
R.output(gv)
|
||||
return gv
|
||||
|
||||
expected = AppendLoss("predict", loss)(Module)
|
||||
expected.show()
|
||||
|
||||
Will get
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@I.ir_module
|
||||
class Module
|
||||
@R.function
|
||||
def predict(x: R.Tensor((2, 4), "float32"), y: R.Tensor((2, 4), "float32")):
|
||||
with R.dataflow():
|
||||
out = R.add(x, y)
|
||||
R.output(out)
|
||||
return out
|
||||
|
||||
@R.function
|
||||
def predict_loss(x: R.Tensor((2, 4), "float32"), y: R.Tensor((2, 4), "float32"),
|
||||
labels: R.Tensor((2, 4), "float32")) -> R.Tensor((), "float32"):
|
||||
with R.dataflow():
|
||||
out: R.Tensor((2, 4), "float32") = R.add(x, y)
|
||||
lv: R.Tensor((2, 4), "float32") = R.subtract(out, labels)
|
||||
lv1: R.Tensor((2, 4), "float32") = R.multiply(lv, lv)
|
||||
gv: R.Tensor((), "float32") = R.sum(lv1)
|
||||
R.output(gv)
|
||||
return gv
|
||||
|
||||
Notes
|
||||
-----
|
||||
This util can be replaced if we have inline pass. It is equivalent to inline a tail call in
|
||||
some sense.
|
||||
"""
|
||||
|
||||
return _ffi_api.AppendLoss( # type: ignore
|
||||
func_name,
|
||||
loss_function,
|
||||
num_backbone_outputs,
|
||||
new_func_name,
|
||||
)
|
||||
|
||||
|
||||
def register_te_gradient(te_grad_name: str, te_grad_func: Callable | None = None):
|
||||
"""Register a te gradient function bind with name te_grad_name. te_grad_name can be referenced
|
||||
later in call_tir_with_grad nodes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
te_grad_name : str
|
||||
The registered name of the te gradient function. Should be align with the te_grad_name in
|
||||
call_tir_with_grad nodes.
|
||||
|
||||
grad_func : Callable
|
||||
The te grad function.
|
||||
It must be a function taking (output_grad: Tensor, arg1: Tensor, arg2: Tensor, ...)
|
||||
as inputs and returning a list of Tensor created by te.compute.
|
||||
|
||||
Returns
|
||||
-------
|
||||
mod : IRModule
|
||||
The mod with corresponding attributes attached.
|
||||
"""
|
||||
|
||||
def register(func: Callable):
|
||||
func_prefix = "tvm.relax.te_grad._register."
|
||||
|
||||
# The handler function is used to let the backend (cpp side) to emit_te.
|
||||
# It's a wrapper of the te_grad_func.
|
||||
# It takes the blockbuilder, the gradient var of the output and the forward call expr.
|
||||
# It will return the emitted var.
|
||||
|
||||
def handler(
|
||||
orig_var: Var, call_tir_with_grad: Call, output_grad: Var, ctx: BlockBuilder
|
||||
) -> relax.Expr:
|
||||
return ctx.emit_te(
|
||||
func,
|
||||
output_grad,
|
||||
*call_tir_with_grad.args[1],
|
||||
**call_tir_with_grad.attrs.te_grad_kwargs,
|
||||
primfunc_name_hint=te_grad_name,
|
||||
)
|
||||
|
||||
register_global_func(func_prefix + te_grad_name, handler)
|
||||
return func
|
||||
|
||||
return register(te_grad_func) if te_grad_func else register
|
||||
Reference in New Issue
Block a user