350 lines
13 KiB
Python
350 lines
13 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=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
|