Files
apache--tvm/python/tvm/relax/training/setup_trainer.py
T
wehub-resource-sync 26446540fa
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

214 lines
8.2 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=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