mirror of
https://github.com/saymrwulf/onnxruntime.git
synced 2026-07-26 19:52:38 +00:00
On device training offline tooling (#11520)
This commit is contained in:
parent
d8a1531c37
commit
3a22a866a1
14 changed files with 1434 additions and 2 deletions
|
|
@ -320,6 +320,17 @@ if (onnxruntime_ENABLE_TRAINING)
|
|||
file(GLOB onnxruntime_python_utils_data_srcs CONFIGURE_DEPENDS
|
||||
"${ORTTRAINING_SOURCE_DIR}/python/training/utils/data/*"
|
||||
)
|
||||
if (onnxruntime_ENABLE_TRAINING_ON_DEVICE)
|
||||
file(GLOB onnxruntime_python_onnxblock_srcs CONFIGURE_DEPENDS
|
||||
"${ORTTRAINING_SOURCE_DIR}/python/training/onnxblock/*"
|
||||
)
|
||||
file(GLOB onnxruntime_python_onnxblock_loss_srcs CONFIGURE_DEPENDS
|
||||
"${ORTTRAINING_SOURCE_DIR}/python/training/onnxblock/loss/*"
|
||||
)
|
||||
file(GLOB onnxruntime_python_onnxblock_optim_srcs CONFIGURE_DEPENDS
|
||||
"${ORTTRAINING_SOURCE_DIR}/python/training/onnxblock/optim/*"
|
||||
)
|
||||
endif()
|
||||
else()
|
||||
file(GLOB onnxruntime_python_capi_training_srcs CONFIGURE_DEPENDS
|
||||
"${ONNXRUNTIME_ROOT}/python/training/*.py"
|
||||
|
|
@ -644,6 +655,23 @@ if (onnxruntime_ENABLE_TRAINING)
|
|||
${onnxruntime_python_utils_data_srcs}
|
||||
$<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/training/utils/data/
|
||||
)
|
||||
if (onnxruntime_ENABLE_TRAINING_ON_DEVICE)
|
||||
add_custom_command(
|
||||
TARGET onnxruntime_pybind11_state POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/training/onnxblock
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/training/onnxblock/loss
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/training/onnxblock/optim
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${onnxruntime_python_onnxblock_srcs}
|
||||
$<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/training/onnxblock/
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${onnxruntime_python_onnxblock_loss_srcs}
|
||||
$<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/training/onnxblock/loss/
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${onnxruntime_python_onnxblock_optim_srcs}
|
||||
$<TARGET_FILE_DIR:${build_output_target}>/onnxruntime/training/onnxblock/optim/
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (onnxruntime_USE_DNNL)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
# __init__.py
|
||||
|
||||
from .model import Model, TrainingModel
|
||||
from .checkpoint_utils import save_checkpoint
|
||||
from . import loss, optim
|
||||
from .model_accessor import onnx_model
|
||||
|
||||
import onnx
|
||||
|
||||
_producer_name = "onnxblock offline tooling"
|
||||
_opset_import = onnx.helper.make_opsetid("com.microsoft", 1)
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
# _building_blocks.py
|
||||
|
||||
import copy
|
||||
import onnx
|
||||
|
||||
import onnxruntime.training.onnxblock as onnxblock
|
||||
import onnxruntime.training.onnxblock.model_accessor as accessor
|
||||
import onnxruntime.training.onnxblock._graph_utils as graph_utils
|
||||
|
||||
|
||||
class Sub(onnxblock.Model):
|
||||
"""Adds Sub node to an onnx model."""
|
||||
|
||||
def __init__(self):
|
||||
super(Sub, self).__init__()
|
||||
|
||||
def build(self, sub_input_name1, sub_input_name2):
|
||||
# get the model to manipulate
|
||||
onnx_model = accessor.global_accessor.model
|
||||
|
||||
# create the graph node for sub
|
||||
sub_node_input_names = [sub_input_name1, sub_input_name2]
|
||||
sub_node_output_name = graph_utils.generate_random_graph_name("sub_output")
|
||||
sub_node_output_names = [sub_node_output_name]
|
||||
sub_node = onnx.helper.make_node(
|
||||
"Sub",
|
||||
sub_node_input_names,
|
||||
sub_node_output_names,
|
||||
name=graph_utils.generate_random_graph_name("Sub"),
|
||||
)
|
||||
onnx_model.graph.node.append(sub_node)
|
||||
|
||||
# create the graph output for sub
|
||||
graph_output = copy.deepcopy(
|
||||
graph_utils.get_output_from_output_name(onnx_model, sub_input_name1)
|
||||
)
|
||||
graph_output.name = sub_node_output_name
|
||||
del onnx_model.graph.output[:]
|
||||
onnx_model.graph.output.append(graph_output)
|
||||
|
||||
return sub_node_output_name
|
||||
|
||||
|
||||
class Pow(onnxblock.Model):
|
||||
"""Adds Pow node to the onnx model."""
|
||||
|
||||
def __init__(self, exponent):
|
||||
super(Pow, self).__init__()
|
||||
|
||||
self._exponent = exponent
|
||||
|
||||
def build(self, pow_input_name):
|
||||
# get the model to manipulate
|
||||
onnx_model = accessor.global_accessor.model
|
||||
|
||||
# create the graph initializer for the exponent
|
||||
pow_node_exponent_name = graph_utils.generate_random_graph_name("pow_exponent")
|
||||
onnx_model.graph.initializer.append(
|
||||
onnx.helper.make_tensor(
|
||||
pow_node_exponent_name, onnx.TensorProto.FLOAT, [1], [self._exponent]
|
||||
)
|
||||
)
|
||||
|
||||
# create the graph node for pow
|
||||
pow_node_input_names = [pow_input_name, pow_node_exponent_name]
|
||||
pow_node_output_name = graph_utils.generate_random_graph_name("pow_output")
|
||||
pow_node_output_names = [pow_node_output_name]
|
||||
pow_node = onnx.helper.make_node(
|
||||
"Pow",
|
||||
pow_node_input_names,
|
||||
pow_node_output_names,
|
||||
name=graph_utils.generate_random_graph_name("Pow"),
|
||||
)
|
||||
onnx_model.graph.node.append(pow_node)
|
||||
|
||||
# create the graph output for pow
|
||||
graph_output = copy.deepcopy(
|
||||
graph_utils.get_output_from_output_name(onnx_model, pow_input_name)
|
||||
)
|
||||
graph_output.name = pow_node_output_name
|
||||
del onnx_model.graph.output[:]
|
||||
onnx_model.graph.output.append(graph_output)
|
||||
|
||||
return pow_node_output_name
|
||||
|
||||
|
||||
class _Reduce(onnxblock.Model):
|
||||
"""Base class for the reduce blocks."""
|
||||
|
||||
def __init__(self):
|
||||
super(_Reduce, self).__init__()
|
||||
|
||||
def _reduce(self, reduce_input_name, reduction_op):
|
||||
# get the model to manipulate
|
||||
onnx_model = accessor.global_accessor.model
|
||||
|
||||
# create the graph node for reduce
|
||||
reduce_node_input_names = [reduce_input_name]
|
||||
reduce_node_output_name = graph_utils.generate_random_graph_name(
|
||||
"reduce_output"
|
||||
)
|
||||
reduce_node_output_names = [reduce_node_output_name]
|
||||
reduce_node = onnx.helper.make_node(
|
||||
reduction_op,
|
||||
reduce_node_input_names,
|
||||
reduce_node_output_names,
|
||||
name=graph_utils.generate_random_graph_name(reduction_op),
|
||||
)
|
||||
onnx_model.graph.node.append(reduce_node)
|
||||
|
||||
# create the graph output for reduce
|
||||
reduce_input = copy.deepcopy(
|
||||
graph_utils.get_output_from_output_name(onnx_model, reduce_input_name)
|
||||
)
|
||||
output_rank = len(reduce_input.type.tensor_type.shape.dim)
|
||||
graph_outputs = [
|
||||
onnx.helper.make_tensor_value_info(
|
||||
reduce_node_output_name, onnx.TensorProto.FLOAT, [1] * output_rank
|
||||
)
|
||||
]
|
||||
del onnx_model.graph.output[:]
|
||||
onnx_model.graph.output.extend(graph_outputs)
|
||||
|
||||
return reduce_node_output_name
|
||||
|
||||
|
||||
class ReduceMean(_Reduce):
|
||||
"""Adds ReduceMean node to the onnx model."""
|
||||
|
||||
def __init__(self):
|
||||
super(ReduceMean, self).__init__()
|
||||
|
||||
def build(self, reduce_input_name):
|
||||
return super()._reduce(reduce_input_name, "ReduceMean")
|
||||
|
||||
|
||||
class ReduceSum(_Reduce):
|
||||
"""Adds ReduceSum node to the onnx model."""
|
||||
|
||||
def __init__(self):
|
||||
super(ReduceSum, self).__init__()
|
||||
|
||||
def build(self, reduce_input_name):
|
||||
return super(ReduceSum, self)._reduce(reduce_input_name, "ReduceSum")
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
# _graph_utils.py
|
||||
|
||||
import copy
|
||||
import onnx
|
||||
import random
|
||||
|
||||
from onnxruntime.capi._pybind_state import GradientGraphBuilder
|
||||
|
||||
|
||||
def get_output_from_output_name(onnx_model, output_name):
|
||||
"""Returns the graph output given the output name"""
|
||||
|
||||
# Iterate over the graph outputs looking for output_name
|
||||
for output in onnx_model.graph.output:
|
||||
if output.name == output_name:
|
||||
return output
|
||||
|
||||
raise LookupError(f"The provided output name {output_name} is not a graph output.")
|
||||
|
||||
|
||||
def get_random_number():
|
||||
"""Return a random number in the range 0, 1000."""
|
||||
|
||||
return random.randint(0, 1000)
|
||||
|
||||
|
||||
def generate_random_graph_name(token):
|
||||
"""Return a string that can be used in the graph as a graph attribute name."""
|
||||
|
||||
return f"{get_random_number()}.{token}"
|
||||
|
||||
|
||||
def build_gradient_graph(
|
||||
accessor, user_args_requiring_grad, user_args_not_requiring_grad, output_names
|
||||
):
|
||||
"""Builds the gradient graph on top of the given input forward only graph."""
|
||||
|
||||
model = accessor.model
|
||||
|
||||
# Collect names of parameters that need gradients computed
|
||||
all_args_requiring_gradient = set()
|
||||
# Move all trainable and non trainable initializers to graph inputs.
|
||||
# This allows training to pass in the parameters from outside the graph
|
||||
# so as to share the parameters across multiple sessions.
|
||||
graph_inputs = model.graph.input
|
||||
initializers = []
|
||||
for initializer in model.graph.initializer:
|
||||
if not initializer.name[0].isdigit():
|
||||
# Move only those initializers as inputs that are not local
|
||||
# to the onnx model. i.e. initializers that are model parameters.
|
||||
# These are tpically those initializers without any number prefixed
|
||||
# to their names.
|
||||
graph_inputs.append(
|
||||
onnx.helper.make_tensor_value_info(
|
||||
initializer.name, initializer.data_type, initializer.dims
|
||||
)
|
||||
)
|
||||
if initializer.name not in user_args_not_requiring_grad:
|
||||
all_args_requiring_gradient.add(initializer.name)
|
||||
else:
|
||||
# All other initializers stay where they were.
|
||||
initializers.append(initializer)
|
||||
|
||||
# Update the initializers in the graph
|
||||
del model.graph.initializer[:]
|
||||
model.graph.initializer.extend(initializers)
|
||||
|
||||
# At this point, we have the eval model
|
||||
accessor.eval_model = copy.deepcopy(model)
|
||||
|
||||
# Any graph input that requires gradient, should have been already added to
|
||||
# args_requiring_grad. So, add these arguments to set of arguments
|
||||
# whose gradient should be built.
|
||||
for argument_name in user_args_requiring_grad:
|
||||
all_args_requiring_gradient.add(argument_name)
|
||||
|
||||
# Assumption is that the first graph output is the loss output
|
||||
if isinstance(output_names, str):
|
||||
output_names = [output_names]
|
||||
builder = GradientGraphBuilder(
|
||||
model.SerializeToString(),
|
||||
set(output_names),
|
||||
all_args_requiring_gradient,
|
||||
output_names[0],
|
||||
)
|
||||
builder.build()
|
||||
gradient_model = onnx.load_from_string(builder.get_model())
|
||||
|
||||
# copy the gradient graph into the user's graph
|
||||
model.graph.CopyFrom(gradient_model.graph)
|
||||
del model.opset_import[:]
|
||||
model.opset_import.extend(gradient_model.opset_import)
|
||||
|
||||
return all_args_requiring_gradient
|
||||
|
||||
|
||||
def build_gradient_accumulation_graph(grad_model, all_args_requiring_gradient_names):
|
||||
"""Builds gradient accumulation nodes on top of a training model.
|
||||
|
||||
Adds an InPlaceAccumulator node for every gradient so that the gradients
|
||||
are accumulated in a gradient buffer (which is an input to InPlaceAccumulator).
|
||||
|
||||
For example, if there is a gradient in the graph called fc1.weight_grad,
|
||||
an InPlaceAccumulator will be added for that gradient whose input will
|
||||
be a graph input (fc1.weight_grad.accumulation.buffer) and the newly
|
||||
computed gradient (fc1.weight_grad).
|
||||
|
||||
gradient.accumulation.buffer gradient
|
||||
| | |
|
||||
Ʌ v v
|
||||
| |_________________________|
|
||||
| |
|
||||
| InPlaceAccumulator
|
||||
| |
|
||||
| v
|
||||
|______________________|
|
||||
"""
|
||||
|
||||
# TODO: Avoid hard coded input/output strings
|
||||
gradient_output_names = {
|
||||
f"{arg_name}_grad" for arg_name in all_args_requiring_gradient_names
|
||||
}
|
||||
|
||||
graph_inputs = grad_model.graph.input
|
||||
graph_nodes = grad_model.graph.node
|
||||
graph_outputs = []
|
||||
|
||||
lazy_reset_grad_input_name = "lazy_reset_grad"
|
||||
gradient_accumulation_name = "accumulation"
|
||||
gradient_buffer_name_suffix = "buffer"
|
||||
gradient_output_name_suffix = "out"
|
||||
|
||||
for idx, graph_output in enumerate(grad_model.graph.output):
|
||||
if graph_output.name not in gradient_output_names:
|
||||
# If graph output is not a gradient, there is no
|
||||
# need to build the gradient accumulator node for it.
|
||||
graph_outputs.append(graph_output)
|
||||
continue
|
||||
|
||||
# gradient accumulation node inputs and output names
|
||||
grad_name = graph_output.name
|
||||
grad_accumulation_buffer_name = (
|
||||
f"{grad_name}.{gradient_accumulation_name}.{gradient_buffer_name_suffix}"
|
||||
)
|
||||
grad_accumulation_output_name = (
|
||||
f"{grad_name}.{gradient_accumulation_name}.{gradient_output_name_suffix}"
|
||||
)
|
||||
|
||||
# Gradient accumulation node
|
||||
acc_node = onnx.helper.make_node(
|
||||
"InPlaceAccumulator",
|
||||
[grad_accumulation_buffer_name, grad_name, lazy_reset_grad_input_name],
|
||||
[grad_accumulation_output_name],
|
||||
name=f"GradientAccumulator{idx}",
|
||||
domain="com.microsoft",
|
||||
)
|
||||
|
||||
graph_nodes.append(acc_node)
|
||||
|
||||
# grad buffer is also a graph input
|
||||
grad_accumulation_buffer_input = copy.deepcopy(graph_output)
|
||||
grad_accumulation_buffer_input.name = grad_accumulation_buffer_name
|
||||
graph_inputs.append(grad_accumulation_buffer_input)
|
||||
|
||||
# accumulated gradient is also a graph output
|
||||
grad_accumulation_output = copy.deepcopy(graph_output)
|
||||
grad_accumulation_output.name = grad_accumulation_output_name
|
||||
graph_outputs.append(grad_accumulation_output)
|
||||
|
||||
lazy_reset_grad_input = onnx.helper.make_tensor_value_info(
|
||||
lazy_reset_grad_input_name, onnx.TensorProto.BOOL, [1]
|
||||
)
|
||||
graph_inputs.append(lazy_reset_grad_input)
|
||||
|
||||
del grad_model.graph.output[:]
|
||||
grad_model.graph.output.extend(graph_outputs)
|
||||
|
||||
|
||||
def get_model_parameters(model, args_not_requiring_gradient):
|
||||
"""Returns trainable and non trainable onnx model parameters."""
|
||||
|
||||
trainable_params = []
|
||||
non_trainable_params = []
|
||||
for initializer in model.graph.initializer:
|
||||
# All model parameters should have their names not begin with
|
||||
# a digit. So, check to see if the initializer's first char
|
||||
# is a digit. If not, it is either a trainable, or a non
|
||||
# trainable parameter.
|
||||
# Note that this assumption can be made because the export logic
|
||||
# does not change the names of the original model parameters.
|
||||
# and the original model parameters don't have their names begin
|
||||
# with a digit.
|
||||
# On the other hand, const initializers are generated by export
|
||||
# logic and have a digit prefix.
|
||||
# TODO: validate this assumption. If assumption is not valid,
|
||||
# the alternative is to enforce the user to provide the parameter names.
|
||||
if not initializer.name[0].isdigit():
|
||||
if initializer.name in args_not_requiring_gradient:
|
||||
non_trainable_params.append(initializer)
|
||||
else:
|
||||
trainable_params.append(initializer)
|
||||
|
||||
return trainable_params, non_trainable_params
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
# checkpoint_utils.py
|
||||
|
||||
from onnxruntime.capi._pybind_state import save_checkpoint as _internal_save_checkpoint
|
||||
|
||||
|
||||
def save_checkpoint(parameters, path_to_checkpoint):
|
||||
"""Saves the parameters to the checkpoint directory path_to_checkpoint."""
|
||||
|
||||
if parameters is None:
|
||||
raise RuntimeError("No checkpoint parameters provided.")
|
||||
|
||||
# TODO: use Parameter class to pass information to backend
|
||||
# Serialize the parameters and save the checkpoint
|
||||
trainable_params, non_trainable_params = parameters
|
||||
trainable_params = [param.SerializeToString() for param in trainable_params]
|
||||
non_trainable_params = [param.SerializeToString() for param in non_trainable_params]
|
||||
_internal_save_checkpoint(
|
||||
trainable_params, non_trainable_params, path_to_checkpoint
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
# __init__.py
|
||||
|
||||
from .loss import MSELoss, CrossEntropyLoss
|
||||
157
orttraining/orttraining/python/training/onnxblock/loss/loss.py
Normal file
157
orttraining/orttraining/python/training/onnxblock/loss/loss.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
# loss.py
|
||||
|
||||
import copy
|
||||
import onnx
|
||||
|
||||
import onnxruntime.training.onnxblock as onnxblock
|
||||
import onnxruntime.training.onnxblock.model_accessor as accessor
|
||||
import onnxruntime.training.onnxblock._graph_utils as graph_utils
|
||||
import onnxruntime.training.onnxblock._building_blocks as building_blocks
|
||||
|
||||
|
||||
class MSELoss(onnxblock.Model):
|
||||
"""MSELoss onnxblock for adding MSE loss to an onnx model.
|
||||
|
||||
Parameters:
|
||||
reduction: string representing the reduction method on the loss output.
|
||||
can be one of "mean" or "sum"
|
||||
"""
|
||||
|
||||
def __init__(self, reduction="mean"):
|
||||
super(MSELoss, self).__init__()
|
||||
|
||||
# determine the reduction type
|
||||
if reduction != "mean" and reduction != "sum":
|
||||
raise RuntimeError(f"Reduction {reduction} not supported.")
|
||||
|
||||
self._reduce = (
|
||||
building_blocks.ReduceMean()
|
||||
if reduction == "mean"
|
||||
else building_blocks.ReduceSum()
|
||||
)
|
||||
self._sub = building_blocks.Sub()
|
||||
self._square = building_blocks.Pow(2.0)
|
||||
|
||||
def build(self, loss_input_name, target_name="target"):
|
||||
"""Adds an MSELoss subgraph on top of the base_model.
|
||||
|
||||
Creates a block that measures the mean squared error between
|
||||
loss_input_name and the target_name.
|
||||
|
||||
Args:
|
||||
loss_input_name: string input representing the loss input
|
||||
target_name: string input representing the target
|
||||
|
||||
Returns:
|
||||
Returns a string of the output name from the loss
|
||||
"""
|
||||
|
||||
# get the model to manipulate
|
||||
onnx_model = accessor.global_accessor.model
|
||||
|
||||
# create a new graph input. this is the target input needed to compare
|
||||
# the graph output against to calculate loss.
|
||||
target_input = copy.deepcopy(
|
||||
graph_utils.get_output_from_output_name(onnx_model, loss_input_name)
|
||||
)
|
||||
target_input.name = target_name
|
||||
onnx_model.graph.input.append(target_input)
|
||||
|
||||
# create the mse loss
|
||||
# loss = reduce(square(sub(output, target)))
|
||||
return self._reduce(self._square(self._sub(loss_input_name, target_name)))
|
||||
|
||||
|
||||
class CrossEntropyLoss(onnxblock.Model):
|
||||
"""CrossEntropyLoss onnxblock for adding Cross Entropy loss to an onnx model.
|
||||
|
||||
Parameters:
|
||||
weight: boolean representing whether a manual rescaling weight given to
|
||||
each class should be added to the inputs.
|
||||
reduction: string representing the reduction method on the loss output.
|
||||
can be one of "mean" or "sum"
|
||||
ignore_index: specifies a target value that is ignored and does not
|
||||
contribute to the input gradient.
|
||||
"""
|
||||
|
||||
def __init__(self, weight=False, reduction="mean", ignore_index=None):
|
||||
super(CrossEntropyLoss, self).__init__()
|
||||
|
||||
# determine the reduction type
|
||||
if reduction != "mean" and reduction != "sum":
|
||||
raise RuntimeError(f"Reduction {reduction} not supported.")
|
||||
|
||||
self._weight = weight
|
||||
self._reduction = reduction
|
||||
self._ignore_index = ignore_index
|
||||
|
||||
def build(self, scores_input_name, labels_name="labels", weight_name="loss_weight"):
|
||||
"""Adds a CrossEntropyLoss subgraph on top of an onnx model.
|
||||
|
||||
Creates a block that measures the softmax cross entropy between
|
||||
scores_input_name and the labels_name.
|
||||
|
||||
Args:
|
||||
loss_input_name: string input representing the loss input
|
||||
labels_name: string input representing the labels
|
||||
|
||||
Returns:
|
||||
Returns a string of the output name from the loss
|
||||
"""
|
||||
|
||||
# get the model to manipulate
|
||||
onnx_model = accessor.global_accessor.model
|
||||
|
||||
# create a new graph input. this is the labels input needed to compare
|
||||
# the graph output against to calculate loss.
|
||||
labels_input = copy.deepcopy(
|
||||
graph_utils.get_output_from_output_name(onnx_model, scores_input_name)
|
||||
)
|
||||
labels_input.name = labels_name
|
||||
labels_input.type.tensor_type.elem_type = onnx.TensorProto.INT32
|
||||
# if the predictions are (num_examples x num_classes)
|
||||
# labels should be (num_examples x 1)
|
||||
del labels_input.type.tensor_type.shape.dim[1]
|
||||
onnx_model.graph.input.append(labels_input)
|
||||
|
||||
if self._weight:
|
||||
weight_input = copy.deepcopy(
|
||||
graph_utils.get_output_from_output_name(onnx_model, scores_input_name)
|
||||
)
|
||||
weight_input.name = weight_name
|
||||
dim_to_keep = weight_input.type.tensor_type.shape.dim[1]
|
||||
del weight_input.type.tensor_type.shape.dim[:]
|
||||
weight_input.type.tensor_type.shape.dim.append(dim_to_keep)
|
||||
onnx_model.graph.input.append(weight_input)
|
||||
|
||||
# create a new graph node for the loss
|
||||
loss_node_input_names = [scores_input_name, labels_name]
|
||||
if self._weight:
|
||||
loss_node_input_names.append(weight_name)
|
||||
loss_node_output_name = graph_utils.generate_random_graph_name("loss")
|
||||
loss_node_output_names = [
|
||||
loss_node_output_name,
|
||||
graph_utils.generate_random_graph_name("log_prob"),
|
||||
]
|
||||
loss_node = onnx.helper.make_node(
|
||||
"SoftmaxCrossEntropyLoss",
|
||||
loss_node_input_names,
|
||||
loss_node_output_names,
|
||||
reduction=self._reduction,
|
||||
ignore_index=self._ignore_index,
|
||||
name=graph_utils.generate_random_graph_name("SoftmaxCrossEntropyLoss"),
|
||||
)
|
||||
onnx_model.graph.node.append(loss_node)
|
||||
|
||||
# create a new graph output for the loss
|
||||
graph_outputs = [
|
||||
onnx.helper.make_tensor_value_info(
|
||||
loss_node_output_name, onnx.TensorProto.FLOAT, []
|
||||
)
|
||||
]
|
||||
del onnx_model.graph.output[:]
|
||||
onnx_model.graph.output.extend(graph_outputs)
|
||||
|
||||
return loss_node_output_name
|
||||
123
orttraining/orttraining/python/training/onnxblock/model.py
Normal file
123
orttraining/orttraining/python/training/onnxblock/model.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
# model.py
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import onnx
|
||||
import onnxruntime.training.onnxblock.model_accessor as accessor
|
||||
import onnxruntime.training.onnxblock._graph_utils as graph_utils
|
||||
|
||||
|
||||
class Model(ABC):
|
||||
"""Builds the forward model based on user's build method."""
|
||||
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def build(self, *args, **kwargs):
|
||||
"""Customize and build the forward graph for this model.
|
||||
|
||||
This method is to be overriden by the user's implementation.
|
||||
"""
|
||||
...
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Calls the user's build method and runs validation on top.
|
||||
|
||||
The output onnx model is got by invoking the user's build method.
|
||||
"""
|
||||
# build the user model
|
||||
output = self.build(*args, **kwargs)
|
||||
|
||||
# validate and check the model
|
||||
onnx.checker.check_model(accessor.global_accessor.model, True)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class TrainingModel(Model):
|
||||
"""Builds the training model based on user's build method."""
|
||||
|
||||
def __init__(self):
|
||||
super(TrainingModel, self).__init__()
|
||||
self._arg_requiring_grad = set()
|
||||
self._arg_not_requiring_grad = set()
|
||||
self._parameters = None
|
||||
|
||||
@abstractmethod
|
||||
def build(self, *args, **kwargs):
|
||||
"""Customize and build the forward graph for this training model.
|
||||
|
||||
This method is to be overriden by the user's implementation.
|
||||
"""
|
||||
...
|
||||
|
||||
def requires_grad(self, argument_name, value=True):
|
||||
"""Control whether the given graph input/parameter requires gradient."""
|
||||
if value is True:
|
||||
if argument_name in self._arg_not_requiring_grad:
|
||||
self._arg_not_requiring_grad.remove(argument_name)
|
||||
self._arg_requiring_grad.add(argument_name)
|
||||
else:
|
||||
if argument_name in self._arg_requiring_grad:
|
||||
self._arg_requiring_grad.remove(argument_name)
|
||||
self._arg_not_requiring_grad.add(argument_name)
|
||||
|
||||
def parameters(self):
|
||||
"""Returns trainable and non trainable parameters.
|
||||
|
||||
Model parameters that are extracted while building the training model
|
||||
are returned by this method.
|
||||
|
||||
Note that the parameters are not known before the training model is
|
||||
built. As a result, if this method is invoked before the training model
|
||||
is built, an exception will be raised.
|
||||
"""
|
||||
if self._parameters is None:
|
||||
raise RuntimeError(
|
||||
"Please build the training model first before trying to "
|
||||
"retrieve the parameters."
|
||||
)
|
||||
|
||||
return self._parameters
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Calls the user's build method and builds the gradient graph on top.
|
||||
|
||||
The onnx model contains the user's training model such that:
|
||||
1. It contains the gradient graph.
|
||||
2. It contains inputs in the order: user inputs, weight parameters,
|
||||
gradient inputs.
|
||||
3. It contains the outputs in the order: user outputs, gradient outputs.
|
||||
4. Before the gradient is built, the eval model is stored in the global accessor.
|
||||
|
||||
Note that the model parameters are moved to be graph inputs.
|
||||
"""
|
||||
# build the user model
|
||||
output = self.build(*args, **kwargs)
|
||||
|
||||
# get all the model parameters for the user_model
|
||||
# and store them in self._parameters
|
||||
self._parameters = graph_utils.get_model_parameters(
|
||||
accessor.global_accessor.model, self._arg_not_requiring_grad
|
||||
)
|
||||
|
||||
# TODO: Perform shape inference before building gradient graph
|
||||
# build the gradient graph
|
||||
all_args_requiring_gradient_names = graph_utils.build_gradient_graph(
|
||||
accessor.global_accessor,
|
||||
self._arg_requiring_grad,
|
||||
self._arg_not_requiring_grad,
|
||||
output,
|
||||
)
|
||||
|
||||
# add gradient accumulation nodes
|
||||
graph_utils.build_gradient_accumulation_graph(
|
||||
accessor.global_accessor.model, all_args_requiring_gradient_names
|
||||
)
|
||||
|
||||
# validate and check the model
|
||||
onnx.checker.check_model(accessor.global_accessor.model, True)
|
||||
|
||||
return output
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
# global_model.py
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
class ModelAccessor:
|
||||
"""This class stores the onnx model that is manipulated by the onnx blocks."""
|
||||
|
||||
def __init__(self, model):
|
||||
self.model = model
|
||||
self.eval_model = None
|
||||
|
||||
|
||||
# This variable resides in the global namespace.
|
||||
# Different methods can access this global model and manipulate it.
|
||||
# Its construction and destruction is managed by the onnx_model contextmanager
|
||||
global_accessor = None
|
||||
|
||||
|
||||
@contextmanager
|
||||
def onnx_model(model=None):
|
||||
"""Context manager that is the entry point to graph manipulations on model.
|
||||
|
||||
Manages the construction and destruction of the global model.
|
||||
"""
|
||||
global global_accessor
|
||||
global_accessor = ModelAccessor(model)
|
||||
try:
|
||||
yield global_accessor
|
||||
finally:
|
||||
global_accessor = None
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
# __init__.py
|
||||
|
||||
from .optim import AdamW
|
||||
161
orttraining/orttraining/python/training/onnxblock/optim/optim.py
Normal file
161
orttraining/orttraining/python/training/onnxblock/optim/optim.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
# optim.py
|
||||
|
||||
import copy
|
||||
import onnx
|
||||
|
||||
import onnxruntime.training.onnxblock as onnxblock
|
||||
import onnxruntime.training.onnxblock.model_accessor as accessor
|
||||
|
||||
|
||||
class AdamW(onnxblock.Model):
|
||||
"""Builds AdamW optimizer onnxblock for the given training model."""
|
||||
|
||||
def __init__(
|
||||
self, bias_correction=True, betas=(0.9, 0.999), eps=1e-6, weight_decay=0.0
|
||||
):
|
||||
super(AdamW, self).__init__()
|
||||
self._bias_correction = bias_correction
|
||||
self._betas = betas
|
||||
self._eps = eps
|
||||
self._weight_decay = weight_decay
|
||||
self._max_norm_clip = 1.0
|
||||
|
||||
def build(self, parameters):
|
||||
"""Returns an AdamW optimizer model based on the input training model."""
|
||||
|
||||
# TODO: Avoid hard coded input/output strings
|
||||
learning_rate_name = "learning_rate"
|
||||
step_name = "step"
|
||||
gradient_output_suffix = "_grad.accumulation.out"
|
||||
first_order_moment_suffix = "exp_avg"
|
||||
second_order_moment_fuffix = "exp_avg_sq"
|
||||
output_name_suffix = "out"
|
||||
|
||||
trainable_parameters, _ = parameters
|
||||
|
||||
graph_nodes = []
|
||||
graph_inputs = [
|
||||
onnx.helper.make_tensor_value_info(
|
||||
learning_rate_name, onnx.TensorProto.FLOAT, [1]
|
||||
),
|
||||
onnx.helper.make_tensor_value_info(step_name, onnx.TensorProto.INT64, [1]),
|
||||
]
|
||||
graph_outputs = []
|
||||
|
||||
# Iterate over all training graph outputs that are gradient outputs
|
||||
for idx, param in enumerate(trainable_parameters):
|
||||
|
||||
param_name = param.name
|
||||
grad_name = f"{param_name}{gradient_output_suffix}"
|
||||
first_order_moment_name = f"{param_name}.{first_order_moment_suffix}"
|
||||
second_order_moment_name = f"{param_name}.{second_order_moment_fuffix}"
|
||||
# prepare node (and graph) inputs and outputs
|
||||
node_input_names = [
|
||||
learning_rate_name, # learning rate
|
||||
step_name, # training step (used for beta correction)
|
||||
param_name, # param to be updated
|
||||
grad_name, # gradient of the param to be used for update
|
||||
first_order_moment_name, # first order moment for this param
|
||||
second_order_moment_name, # second order moment for this param
|
||||
]
|
||||
|
||||
param_tensor_value_info = onnx.helper.make_tensor_value_info(
|
||||
param_name, param.data_type, param.dims
|
||||
)
|
||||
grad_tensor_value_info = onnx.helper.make_tensor_value_info(
|
||||
grad_name, param.data_type, param.dims
|
||||
)
|
||||
first_order_moment_tensor_value_info = onnx.helper.make_tensor_value_info(
|
||||
first_order_moment_name, param.data_type, param.dims
|
||||
)
|
||||
second_order_moment_tensor_value_info = onnx.helper.make_tensor_value_info(
|
||||
second_order_moment_name, param.data_type, param.dims
|
||||
)
|
||||
node_inputs = [
|
||||
param_tensor_value_info,
|
||||
grad_tensor_value_info,
|
||||
first_order_moment_tensor_value_info,
|
||||
second_order_moment_tensor_value_info,
|
||||
]
|
||||
graph_inputs.extend(node_inputs)
|
||||
|
||||
step_output_name = f"{param_name}.{step_name}.{output_name_suffix}"
|
||||
param_output_name = f"{param_name}.{output_name_suffix}"
|
||||
first_order_moment_output_name = (
|
||||
f"{first_order_moment_name}.{output_name_suffix}"
|
||||
)
|
||||
second_order_moment_output_name = (
|
||||
f"{second_order_moment_name}.{output_name_suffix}"
|
||||
)
|
||||
|
||||
param_output_tensor_value_info = onnx.helper.make_tensor_value_info(
|
||||
param_output_name, param.data_type, param.dims
|
||||
)
|
||||
first_order_moment_output_tensor_value_info = (
|
||||
onnx.helper.make_tensor_value_info(
|
||||
first_order_moment_output_name, param.data_type, param.dims
|
||||
)
|
||||
)
|
||||
second_order_moment_output_tensor_value_info = (
|
||||
onnx.helper.make_tensor_value_info(
|
||||
second_order_moment_output_name, param.data_type, param.dims
|
||||
)
|
||||
)
|
||||
|
||||
node_output_names = [
|
||||
step_output_name, # step out
|
||||
first_order_moment_output_name, # first order moment output
|
||||
second_order_moment_output_name, # second order moment output
|
||||
param_output_name, # updated weights
|
||||
]
|
||||
|
||||
node_outputs = [
|
||||
onnx.helper.make_tensor_value_info(
|
||||
step_output_name, onnx.TensorProto.INT64, [1]
|
||||
),
|
||||
first_order_moment_output_tensor_value_info,
|
||||
second_order_moment_output_tensor_value_info,
|
||||
param_output_tensor_value_info,
|
||||
]
|
||||
graph_outputs.extend(node_outputs)
|
||||
|
||||
# AdamOptimizer node attributes
|
||||
node_attributes = {
|
||||
"alpha": self._betas[0], # beta1
|
||||
"beta": self._betas[1], # beta2
|
||||
"lambda": self._weight_decay, # weight decay
|
||||
"epsilon": self._eps, # epsilon
|
||||
"do_bias_correction": 1
|
||||
if self._bias_correction
|
||||
else 0, # bias_correction
|
||||
"weight_decay_mode": 1, # weight decay mode
|
||||
"max_norm_clip": self._max_norm_clip, # used for gradient scaling
|
||||
}
|
||||
|
||||
# make the node
|
||||
optimizer_node = onnx.helper.make_node(
|
||||
"AdamOptimizer",
|
||||
node_input_names,
|
||||
node_output_names,
|
||||
name=f"AdamOptimizer{idx}",
|
||||
domain="com.microsoft",
|
||||
**node_attributes,
|
||||
)
|
||||
|
||||
graph_nodes.append(optimizer_node)
|
||||
|
||||
# make the graph and the model
|
||||
graph = onnx.helper.make_graph(
|
||||
graph_nodes, "Optimizer Graph", graph_inputs, graph_outputs
|
||||
)
|
||||
model = onnx.helper.make_model(
|
||||
graph,
|
||||
producer_name=onnxblock._producer_name,
|
||||
opset_imports=[onnxblock._opset_import],
|
||||
)
|
||||
|
||||
accessor.global_accessor.model = model
|
||||
|
||||
return [output.name for output in graph_outputs]
|
||||
|
|
@ -0,0 +1,526 @@
|
|||
import torch
|
||||
import io
|
||||
import onnx
|
||||
import onnxruntime.training.onnxblock as onnxblock
|
||||
import onnxruntime
|
||||
from onnxruntime.capi import _pybind_state as C
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
import copy
|
||||
import numpy as np
|
||||
|
||||
|
||||
# PyTorch Module definitions
|
||||
|
||||
|
||||
class SimpleNet(torch.nn.Module):
|
||||
def __init__(self, input_size, hidden_size, num_classes):
|
||||
super(SimpleNet, self).__init__()
|
||||
|
||||
self.fc1 = torch.nn.Linear(input_size, hidden_size)
|
||||
self.relu = torch.nn.ReLU()
|
||||
self.fc2 = torch.nn.Linear(hidden_size, num_classes)
|
||||
|
||||
def forward(self, model_input):
|
||||
out = self.fc1(model_input)
|
||||
out = self.relu(out)
|
||||
out = self.fc2(out)
|
||||
return out
|
||||
|
||||
|
||||
# onnxblock Model definitions
|
||||
|
||||
|
||||
class SimpleModelWithMSELoss(onnxblock.Model):
|
||||
def __init__(self):
|
||||
super(SimpleModelWithMSELoss, self).__init__()
|
||||
self.loss = onnxblock.loss.MSELoss()
|
||||
|
||||
def build(self, output_name):
|
||||
return self.loss(output_name)
|
||||
|
||||
|
||||
class SimpleModelWithCrossEntropyLoss(onnxblock.Model):
|
||||
def __init__(self):
|
||||
super(SimpleModelWithCrossEntropyLoss, self).__init__()
|
||||
self.loss = onnxblock.loss.CrossEntropyLoss()
|
||||
|
||||
def build(self, output_name):
|
||||
return self.loss(output_name)
|
||||
|
||||
|
||||
class SimpleTrainingModelWithMSELoss(onnxblock.TrainingModel):
|
||||
def __init__(self):
|
||||
super(SimpleTrainingModelWithMSELoss, self).__init__()
|
||||
self.loss = onnxblock.loss.MSELoss()
|
||||
|
||||
def build(self, output_name):
|
||||
return self.loss(output_name)
|
||||
|
||||
|
||||
class SimpleTrainingModelWithCrossEntropyLoss(onnxblock.TrainingModel):
|
||||
def __init__(self):
|
||||
super(SimpleTrainingModelWithCrossEntropyLoss, self).__init__()
|
||||
self.loss = onnxblock.loss.CrossEntropyLoss()
|
||||
|
||||
def build(self, output_name):
|
||||
return self.loss(output_name)
|
||||
|
||||
|
||||
# Test utility methods
|
||||
|
||||
|
||||
def _get_onnx_model(torch_model, model_inputs):
|
||||
model_outputs = torch_model(*model_inputs)
|
||||
if isinstance(model_outputs, torch.Tensor):
|
||||
model_outputs = [model_outputs]
|
||||
dynamic_axes = {}
|
||||
input_names = []
|
||||
output_names = []
|
||||
for i, model_input in enumerate(model_inputs):
|
||||
input_name = f"input-{i}"
|
||||
input_names.append(input_name)
|
||||
dynamic_axes[input_name] = {}
|
||||
for dim_idx in range(len(model_input.shape)):
|
||||
dynamic_axes[input_name].update({dim_idx: f"{input_name}_dim{dim_idx}"})
|
||||
|
||||
for i, model_output in enumerate(model_outputs):
|
||||
output_name = f"output-{i}"
|
||||
output_names.append(output_name)
|
||||
dynamic_axes[output_name] = {}
|
||||
for dim_idx in range(len(model_output.shape)):
|
||||
dynamic_axes[output_name].update({dim_idx: f"{output_name}_dim{dim_idx}"})
|
||||
|
||||
f = io.BytesIO()
|
||||
torch.onnx.export(
|
||||
torch_model,
|
||||
model_inputs,
|
||||
f,
|
||||
input_names=input_names,
|
||||
output_names=output_names,
|
||||
opset_version=14,
|
||||
do_constant_folding=False,
|
||||
training=torch.onnx.TrainingMode.TRAINING,
|
||||
dynamic_axes=dynamic_axes,
|
||||
export_params=True,
|
||||
keep_initializers_as_inputs=False,
|
||||
)
|
||||
return onnx.load_model_from_string(f.getvalue())
|
||||
|
||||
|
||||
def _to_numpy(tensor):
|
||||
return (
|
||||
tensor.detach().cpu().numpy() if tensor.requires_grad else tensor.cpu().numpy()
|
||||
)
|
||||
|
||||
|
||||
def _get_models(device, N, D_in, H, D_out):
|
||||
"""Returns the pt and onnx models for SimpleNet"""
|
||||
pt_model = SimpleNet(D_in, H, D_out).to(device)
|
||||
x = torch.randn(N, D_in, device=device)
|
||||
onnx_model = _get_onnx_model(pt_model, (x,))
|
||||
|
||||
return pt_model, onnx_model
|
||||
|
||||
|
||||
def _get_training_ort_output_names(pt_model, onnx_model):
|
||||
"""Returns the ort output names"""
|
||||
ort_output_names = [onnx_model.graph.output[0].name]
|
||||
for name, _ in pt_model.named_parameters():
|
||||
ort_output_names.append(f"{name}_grad.accumulation.out")
|
||||
|
||||
return ort_output_names
|
||||
|
||||
|
||||
def _get_training_ort_inputs(x, target, pt_model, onnx_model, target_type=None):
|
||||
"""Returns the ort inputs"""
|
||||
|
||||
ort_inputs = {
|
||||
onnx_model.graph.input[0].name: _to_numpy(copy.deepcopy(x)),
|
||||
onnx_model.graph.input[1].name: _to_numpy(copy.deepcopy(target))
|
||||
if target_type is None
|
||||
else _to_numpy(copy.deepcopy(target).type(target_type)),
|
||||
}
|
||||
if target_type is not None:
|
||||
ort_inputs[onnx_model.graph.input[1].name]
|
||||
for name, param in pt_model.named_parameters():
|
||||
ort_inputs[name] = _to_numpy(copy.deepcopy(param))
|
||||
ort_inputs[f"{name}_grad.accumulation.buffer"] = _to_numpy(
|
||||
torch.zeros_like(param)
|
||||
)
|
||||
ort_inputs["lazy_reset_grad"] = np.full(1, True)
|
||||
|
||||
return ort_inputs
|
||||
|
||||
|
||||
# All unit tests
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"graph",
|
||||
[
|
||||
SimpleModelWithMSELoss,
|
||||
SimpleModelWithCrossEntropyLoss,
|
||||
SimpleTrainingModelWithMSELoss,
|
||||
SimpleTrainingModelWithCrossEntropyLoss,
|
||||
],
|
||||
)
|
||||
def test_loss_composition(graph):
|
||||
# Given
|
||||
device = "cuda"
|
||||
N, D_in, H, D_out = 64, 784, 500, 10
|
||||
_, onnx_model = _get_models(device, N, D_in, H, D_out)
|
||||
|
||||
# When / Then no error occurs
|
||||
simple_model = graph()
|
||||
with onnxblock.onnx_model(onnx_model):
|
||||
_ = simple_model(onnx_model.graph.output[0].name)
|
||||
|
||||
|
||||
def test_mse_loss_execution():
|
||||
# Given
|
||||
device = "cuda"
|
||||
N, D_in, H, D_out = 64, 784, 500, 10
|
||||
pt_model, onnx_model = _get_models(device, N, D_in, H, D_out)
|
||||
|
||||
x = torch.randn(N, D_in, device=device)
|
||||
target = torch.randn(N, D_out, device=device)
|
||||
|
||||
# Build the onnx model with loss
|
||||
simple_model = SimpleModelWithMSELoss()
|
||||
with onnxblock.onnx_model(onnx_model):
|
||||
_ = simple_model(onnx_model.graph.output[0].name)
|
||||
|
||||
ort_output_names = [onnx_model.graph.output[0].name]
|
||||
ort_inputs = {
|
||||
onnx_model.graph.input[0].name: _to_numpy(copy.deepcopy(x)),
|
||||
onnx_model.graph.input[1].name: _to_numpy(copy.deepcopy(target)),
|
||||
}
|
||||
|
||||
def mse_loss(prediction, target):
|
||||
loss = torch.nn.MSELoss()
|
||||
return loss(prediction, target)
|
||||
|
||||
# When
|
||||
with tempfile.NamedTemporaryFile(suffix=".onnx") as onnx_fo:
|
||||
onnx.save(onnx_model, onnx_fo.name)
|
||||
ort_session = onnxruntime.InferenceSession(
|
||||
onnx_fo.name, providers=C.get_available_providers()
|
||||
)
|
||||
|
||||
ort_outs = ort_session.run(ort_output_names, ort_inputs)
|
||||
torch_outs = mse_loss(pt_model(x), target)
|
||||
|
||||
# Then
|
||||
assert np.allclose(ort_outs[0], _to_numpy(torch_outs))
|
||||
|
||||
|
||||
def test_crossentropy_loss_execution():
|
||||
# Given
|
||||
device = "cuda"
|
||||
N, D_in, H, D_out = 64, 784, 500, 10
|
||||
pt_model, onnx_model = _get_models(device, N, D_in, H, D_out)
|
||||
|
||||
x = torch.randn(N, D_in, device=device)
|
||||
target = torch.randint(high=D_out, size=(N,), dtype=torch.int64, device=device)
|
||||
|
||||
# Build the onnx model with loss
|
||||
simple_model = SimpleModelWithCrossEntropyLoss()
|
||||
with onnxblock.onnx_model(onnx_model):
|
||||
_ = simple_model(onnx_model.graph.output[0].name)
|
||||
|
||||
ort_output_names = [onnx_model.graph.output[0].name]
|
||||
ort_inputs = {
|
||||
onnx_model.graph.input[0].name: _to_numpy(copy.deepcopy(x)),
|
||||
onnx_model.graph.input[1].name: _to_numpy(
|
||||
copy.deepcopy(target).type(torch.int32)
|
||||
),
|
||||
}
|
||||
|
||||
def crossentropy_loss(prediction, target):
|
||||
loss = torch.nn.CrossEntropyLoss()
|
||||
return loss(prediction, target)
|
||||
|
||||
# When
|
||||
with tempfile.NamedTemporaryFile(suffix=".onnx") as onnx_fo:
|
||||
onnx.save(onnx_model, onnx_fo.name)
|
||||
ort_session = onnxruntime.InferenceSession(
|
||||
onnx_fo.name, providers=C.get_available_providers()
|
||||
)
|
||||
|
||||
ort_outs = ort_session.run(ort_output_names, ort_inputs)
|
||||
torch_outs = crossentropy_loss(pt_model(x), target)
|
||||
|
||||
# Then
|
||||
assert np.allclose(ort_outs[0], _to_numpy(torch_outs))
|
||||
|
||||
|
||||
def test_mse_loss_training_graph_execution():
|
||||
# Given
|
||||
device = "cuda"
|
||||
N, D_in, H, D_out = 64, 784, 500, 10
|
||||
pt_model, onnx_model = _get_models(device, N, D_in, H, D_out)
|
||||
|
||||
x = torch.randn(N, D_in, device=device)
|
||||
target = torch.randn(N, D_out, device=device)
|
||||
|
||||
# Build the onnx trainingmodel with loss
|
||||
simple_model = SimpleTrainingModelWithMSELoss()
|
||||
with onnxblock.onnx_model(onnx_model):
|
||||
_ = simple_model(onnx_model.graph.output[0].name)
|
||||
|
||||
ort_output_names = _get_training_ort_output_names(pt_model, onnx_model)
|
||||
ort_inputs = _get_training_ort_inputs(x, target, pt_model, onnx_model)
|
||||
|
||||
def mse_loss(prediction, target):
|
||||
loss = torch.nn.MSELoss()
|
||||
return loss(prediction, target)
|
||||
|
||||
# When
|
||||
with tempfile.NamedTemporaryFile(suffix=".onnx") as onnx_fo:
|
||||
onnx.save(onnx_model, onnx_fo.name)
|
||||
ort_session = onnxruntime.InferenceSession(
|
||||
onnx_fo.name, providers=C.get_available_providers()
|
||||
)
|
||||
|
||||
ort_outs = ort_session.run(ort_output_names, ort_inputs)
|
||||
torch_outs = mse_loss(pt_model(x), target)
|
||||
torch_outs.backward()
|
||||
|
||||
# Then
|
||||
# assert loss is close
|
||||
assert np.allclose(ort_outs[0], _to_numpy(torch_outs))
|
||||
|
||||
# assert all the gradients are close
|
||||
for ort_grad, pt_param in zip(ort_outs[1:], pt_model.parameters()):
|
||||
assert np.allclose(ort_grad, _to_numpy(pt_param.grad))
|
||||
|
||||
|
||||
def test_crossentropy_loss_training_graph_execution():
|
||||
# Given
|
||||
device = "cuda"
|
||||
N, D_in, H, D_out = 64, 784, 500, 10
|
||||
pt_model, onnx_model = _get_models(device, N, D_in, H, D_out)
|
||||
|
||||
x = torch.randn(N, D_in, device=device)
|
||||
target = torch.randint(high=D_out, size=(N,), dtype=torch.int64, device=device)
|
||||
|
||||
# Build the onnx trainingmodel with loss
|
||||
simple_model = SimpleTrainingModelWithCrossEntropyLoss()
|
||||
with onnxblock.onnx_model(onnx_model):
|
||||
_ = simple_model(onnx_model.graph.output[0].name)
|
||||
|
||||
ort_output_names = _get_training_ort_output_names(pt_model, onnx_model)
|
||||
ort_inputs = _get_training_ort_inputs(
|
||||
x, target, pt_model, onnx_model, target_type=torch.int32
|
||||
)
|
||||
|
||||
def crossentropy_loss(prediction, target):
|
||||
loss = torch.nn.CrossEntropyLoss()
|
||||
return loss(prediction, target)
|
||||
|
||||
# When
|
||||
with tempfile.NamedTemporaryFile(suffix=".onnx") as onnx_fo:
|
||||
onnx.save(onnx_model, onnx_fo.name)
|
||||
ort_session = onnxruntime.InferenceSession(
|
||||
onnx_fo.name, providers=C.get_available_providers()
|
||||
)
|
||||
|
||||
ort_outs = ort_session.run(ort_output_names, ort_inputs)
|
||||
torch_outs = crossentropy_loss(pt_model(x), target)
|
||||
torch_outs.backward()
|
||||
|
||||
# Then
|
||||
# assert loss is close
|
||||
assert np.allclose(ort_outs[0], _to_numpy(torch_outs))
|
||||
|
||||
# assert all the gradients are close
|
||||
for ort_grad, pt_param in zip(ort_outs[1:], pt_model.parameters()):
|
||||
assert np.allclose(ort_grad, _to_numpy(pt_param.grad))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"graph", [SimpleTrainingModelWithMSELoss, SimpleTrainingModelWithCrossEntropyLoss]
|
||||
)
|
||||
def test_adamw_optimizer_composition(graph):
|
||||
# Given
|
||||
device = "cuda"
|
||||
N, D_in, H, D_out = 64, 784, 500, 10
|
||||
_, onnx_model = _get_models(device, N, D_in, H, D_out)
|
||||
|
||||
# When / Then no error occurs
|
||||
simple_model = graph()
|
||||
with onnxblock.onnx_model(onnx_model):
|
||||
_ = simple_model(onnx_model.graph.output[0].name)
|
||||
|
||||
optimizer = onnxblock.optim.AdamW()
|
||||
with onnxblock.onnx_model() as accessor:
|
||||
_ = optimizer(simple_model.parameters())
|
||||
optimizer_model = accessor.model
|
||||
assert optimizer_model
|
||||
|
||||
|
||||
def test_adamw_optimizer_execution():
|
||||
# Given
|
||||
device = "cuda"
|
||||
N, D_in, H, D_out = 64, 784, 500, 10
|
||||
pt_model, onnx_model = _get_models(device, N, D_in, H, D_out)
|
||||
|
||||
x = torch.randn(N, D_in, device=device)
|
||||
target = torch.randn(N, D_out, device=device)
|
||||
|
||||
simple_model = SimpleTrainingModelWithMSELoss()
|
||||
with onnxblock.onnx_model(onnx_model):
|
||||
_ = simple_model(onnx_model.graph.output[0].name)
|
||||
|
||||
optimizer = onnxblock.optim.AdamW()
|
||||
with onnxblock.onnx_model() as accessor:
|
||||
_ = optimizer(simple_model.parameters())
|
||||
optimizer_model = accessor.model
|
||||
|
||||
learning_rate = 0.001
|
||||
step = 1
|
||||
ort_output_names = []
|
||||
for name, _ in pt_model.named_parameters():
|
||||
ort_output_names.append(f"{name}.out")
|
||||
|
||||
def mse_loss(prediction, target):
|
||||
loss = torch.nn.MSELoss()
|
||||
return loss(prediction, target)
|
||||
|
||||
# When
|
||||
with tempfile.NamedTemporaryFile(suffix=".onnx") as onnx_fo:
|
||||
onnx.save(optimizer_model, onnx_fo.name)
|
||||
|
||||
loss = mse_loss(pt_model(x), target)
|
||||
loss.backward()
|
||||
|
||||
ort_inputs = {
|
||||
"learning_rate": np.full(1, learning_rate, dtype=np.float32),
|
||||
"step": np.full(1, step, dtype=np.int64),
|
||||
}
|
||||
for name, param in pt_model.named_parameters():
|
||||
ort_inputs[name] = _to_numpy(copy.deepcopy(param))
|
||||
ort_inputs[f"{name}_grad.accumulation.out"] = _to_numpy(
|
||||
copy.deepcopy(param.grad)
|
||||
)
|
||||
ort_inputs[f"{name}.exp_avg"] = _to_numpy(torch.zeros_like(param))
|
||||
ort_inputs[f"{name}.exp_avg_sq"] = _to_numpy(torch.zeros_like(param))
|
||||
|
||||
# Then no error occurs when executing the model
|
||||
ort_session = onnxruntime.InferenceSession(
|
||||
onnx_fo.name, providers=C.get_available_providers()
|
||||
)
|
||||
_ = ort_session.run(ort_output_names, ort_inputs)
|
||||
|
||||
|
||||
def test_retrieve_parameters():
|
||||
# Given
|
||||
device = "cuda"
|
||||
N, D_in, H, D_out = 64, 784, 500, 10
|
||||
pt_model, onnx_model = _get_models(device, N, D_in, H, D_out)
|
||||
|
||||
simple_model = SimpleTrainingModelWithMSELoss()
|
||||
with onnxblock.onnx_model(onnx_model):
|
||||
_ = simple_model(onnx_model.graph.output[0].name)
|
||||
|
||||
# When
|
||||
trainable_params, non_trainable_params = simple_model.parameters()
|
||||
|
||||
# Then
|
||||
assert not non_trainable_params
|
||||
for ort_param, (pt_param_name, pt_param) in zip(
|
||||
trainable_params, pt_model.named_parameters()
|
||||
):
|
||||
assert ort_param.name == pt_param_name
|
||||
assert np.allclose(
|
||||
np.frombuffer(ort_param.raw_data, dtype=np.float32).reshape(pt_param.shape),
|
||||
_to_numpy(pt_param),
|
||||
)
|
||||
|
||||
|
||||
def test_retrieve_parameters_before_building_gradient_graph():
|
||||
# Given
|
||||
device = "cuda"
|
||||
N, D_in, H, D_out = 64, 784, 500, 10
|
||||
_, onnx_model = _get_models(device, N, D_in, H, D_out)
|
||||
|
||||
simple_model = SimpleTrainingModelWithMSELoss()
|
||||
|
||||
# When / Then
|
||||
with pytest.raises(Exception) as ex_info:
|
||||
_, _ = simple_model.parameters()
|
||||
assert (
|
||||
"Please build the training model first before trying to retrieve the parameters."
|
||||
in str(ex_info.value)
|
||||
)
|
||||
|
||||
|
||||
def test_save_checkpoint():
|
||||
# Given
|
||||
device = "cuda"
|
||||
N, D_in, H, D_out = 64, 784, 500, 10
|
||||
_, onnx_model = _get_models(device, N, D_in, H, D_out)
|
||||
|
||||
simple_model = SimpleTrainingModelWithMSELoss()
|
||||
with onnxblock.onnx_model(onnx_model):
|
||||
_ = simple_model(onnx_model.graph.output[0].name)
|
||||
trainable_params, non_trainable_params = simple_model.parameters()
|
||||
|
||||
# When
|
||||
with tempfile.TemporaryDirectory() as checkpoint_dir_name:
|
||||
checkpoint_file_path = os.path.join(checkpoint_dir_name, "checkpoint")
|
||||
onnxblock.save_checkpoint(
|
||||
(trainable_params, non_trainable_params), checkpoint_file_path
|
||||
)
|
||||
|
||||
# Then
|
||||
assert os.path.exists(checkpoint_file_path)
|
||||
|
||||
|
||||
def test_set_requires_grad_on_parameters():
|
||||
# Given
|
||||
device = "cuda"
|
||||
N, D_in, H, D_out = 64, 784, 500, 10
|
||||
_, onnx_model = _get_models(device, N, D_in, H, D_out)
|
||||
|
||||
simple_model = SimpleTrainingModelWithMSELoss()
|
||||
|
||||
# When
|
||||
simple_model.requires_grad("fc2.weight", False)
|
||||
simple_model.requires_grad("fc1.bias", False)
|
||||
|
||||
with onnxblock.onnx_model(onnx_model):
|
||||
_ = simple_model(onnx_model.graph.output[0].name)
|
||||
trainable_params, non_trainable_params = simple_model.parameters()
|
||||
|
||||
# Then
|
||||
expected_trainable_parameters = {"fc1.weight", "fc2.bias"}
|
||||
expected_non_trainable_parameters = {"fc2.weight", "fc1.bias"}
|
||||
for param in trainable_params:
|
||||
assert param.name in expected_trainable_parameters
|
||||
for param in non_trainable_params:
|
||||
assert param.name in expected_non_trainable_parameters
|
||||
|
||||
|
||||
def test_set_requires_grad_on_inputs():
|
||||
# Given
|
||||
device = "cuda"
|
||||
N, D_in, H, D_out = 64, 784, 500, 10
|
||||
_, onnx_model = _get_models(device, N, D_in, H, D_out)
|
||||
|
||||
# When
|
||||
simple_model = SimpleTrainingModelWithMSELoss()
|
||||
simple_model.requires_grad("input-0")
|
||||
with onnxblock.onnx_model(onnx_model):
|
||||
_ = simple_model(onnx_model.graph.output[0].name)
|
||||
|
||||
# Then
|
||||
expected_input_gradient_buffer_name = "input-0_grad.accumulation.buffer"
|
||||
expected_input_gradient_output_name = "input-0_grad.accumulation.out"
|
||||
graph_input_names = {graph_input.name for graph_input in onnx_model.graph.input}
|
||||
graph_output_names = {graph_output.name for graph_output in onnx_model.graph.output}
|
||||
|
||||
assert expected_input_gradient_buffer_name in graph_input_names
|
||||
assert expected_input_gradient_output_name in graph_output_names
|
||||
3
setup.py
3
setup.py
|
|
@ -329,6 +329,7 @@ requirements_file = "requirements.txt"
|
|||
|
||||
local_version = None
|
||||
enable_training = parse_arg_remove_boolean(sys.argv, '--enable_training')
|
||||
enable_training_on_device = parse_arg_remove_boolean(sys.argv, '--enable_training_on_device')
|
||||
disable_auditwheel_repair = parse_arg_remove_boolean(sys.argv, '--disable_auditwheel_repair')
|
||||
default_training_package_device = parse_arg_remove_boolean(sys.argv, '--default_training_package_device')
|
||||
|
||||
|
|
@ -374,6 +375,8 @@ if enable_training:
|
|||
'onnxruntime.training.ortmodule.torch_cpp_extensions.cuda.torch_gpu_allocator',
|
||||
'onnxruntime.training.ortmodule.torch_cpp_extensions.cuda.fused_ops',
|
||||
'onnxruntime.training.utils.data'])
|
||||
if enable_training_on_device:
|
||||
packages.append('onnxruntime.training.onnxblock')
|
||||
package_data['onnxruntime.training.ortmodule.torch_cpp_extensions.cpu.aten_op_executor'] = ['*.cc']
|
||||
package_data['onnxruntime.training.ortmodule.torch_cpp_extensions.cpu.torch_interop_utils'] = ['*.cc']
|
||||
package_data['onnxruntime.training.ortmodule.torch_cpp_extensions.cuda.torch_gpu_allocator'] = ['*.cc']
|
||||
|
|
|
|||
|
|
@ -176,6 +176,8 @@ def parse_arguments():
|
|||
"--enable_training_ops", action='store_true', help="Enable training ops in inference graph.")
|
||||
parser.add_argument(
|
||||
"--enable_training_torch_interop", action='store_true', help="Enable training kernels interop with torch.")
|
||||
parser.add_argument(
|
||||
"--enable_training_on_device", action='store_true', help="Enable on device training in ORT.")
|
||||
parser.add_argument(
|
||||
"--disable_nccl", action='store_true', help="Disable Nccl.")
|
||||
parser.add_argument(
|
||||
|
|
@ -836,6 +838,7 @@ def generate_build_tree(cmake_path, source_dir, build_dir, cuda_home, cudnn_home
|
|||
"-Donnxruntime_ENABLE_TRAINING=" + ("ON" if args.enable_training else "OFF"),
|
||||
"-Donnxruntime_ENABLE_TRAINING_OPS=" + ("ON" if args.enable_training_ops else "OFF"),
|
||||
"-Donnxruntime_ENABLE_TRAINING_TORCH_INTEROP=" + ("ON" if args.enable_training_torch_interop else "OFF"),
|
||||
"-Donnxruntime_ENABLE_TRAINING_ON_DEVICE=" + ("ON" if args.enable_training_on_device else "OFF"),
|
||||
# Enable advanced computations such as AVX for some traininig related ops.
|
||||
"-Donnxruntime_ENABLE_CPU_FP16_OPS=" + ("ON" if args.enable_training else "OFF"),
|
||||
"-Donnxruntime_USE_NCCL=" + ("OFF" if args.disable_nccl else "ON"),
|
||||
|
|
@ -1801,7 +1804,7 @@ def build_python_wheel(
|
|||
source_dir, build_dir, configs, use_cuda, cuda_version, use_rocm, rocm_version, use_dnnl,
|
||||
use_tensorrt, use_openvino, use_nuphar, use_tvm, use_vitisai, use_acl, use_armnn, use_dml,
|
||||
wheel_name_suffix, enable_training, nightly_build=False, default_training_package_device=False,
|
||||
use_ninja=False, build_eager_mode=False):
|
||||
use_ninja=False, build_eager_mode=False, enable_training_on_device=False):
|
||||
for config in configs:
|
||||
cwd = get_config_build_dir(build_dir, config)
|
||||
if is_windows() and not use_ninja:
|
||||
|
|
@ -1819,6 +1822,8 @@ def build_python_wheel(
|
|||
args.append('--wheel_name_suffix={}'.format(wheel_name_suffix))
|
||||
if enable_training:
|
||||
args.append("--enable_training")
|
||||
if enable_training_on_device:
|
||||
args.append("--enable_training_on_device")
|
||||
if build_eager_mode:
|
||||
args.append("--disable_auditwheel_repair")
|
||||
|
||||
|
|
@ -2451,7 +2456,8 @@ def main():
|
|||
nightly_build=nightly_build,
|
||||
default_training_package_device=default_training_package_device,
|
||||
use_ninja=(args.cmake_generator == 'Ninja'),
|
||||
build_eager_mode=args.build_eager_mode
|
||||
build_eager_mode=args.build_eager_mode,
|
||||
enable_training_on_device=args.enable_training_on_device
|
||||
)
|
||||
if args.build_nuget:
|
||||
build_nuget_package(
|
||||
|
|
|
|||
Loading…
Reference in a new issue