save_checkpoint, load_checkpoint and aggregate_checkpoints (#6136)

* save_checkpoint and load_checkpoint implementations

* checkpoint aggregation logic

* unit tests for save_checkpoint, load_checkpoint and aggregate_checkpoints
This commit is contained in:
baijumeswani 2020-12-17 21:01:36 -08:00 committed by GitHub
parent c339bb2da9
commit adc2071043
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 717 additions and 56 deletions

View file

@ -4,6 +4,7 @@
import h5py
from collections.abc import Mapping
import pickle
def _dfs_save(group, save_obj):
"""Recursively go over each level in the save_obj dictionary and save values to a hdf5 group"""
@ -79,3 +80,20 @@ def load(path, key=None):
_dfs_load(f, load_obj)
return load_obj
def to_serialized_hex(user_dict):
"""Serialize the user_dict and convert the serialized bytes to a hex string and return"""
return pickle.dumps(user_dict).hex()
def from_serialized_hex(serialized_hex):
"""Convert serialized_hex to bytes and deserialize it and return"""
# serialized_hex can be either a regular string or a byte string.
# if it is a byte string, convert to regular string using decode()
# if it is a regular string, do nothing to it
try:
serialized_hex = serialized_hex.decode()
except AttributeError:
pass
return pickle.loads(bytes.fromhex(serialized_hex))

View file

@ -202,4 +202,47 @@ def state_dict_trainer_options_key():
def state_dict_full_precision_key():
"""Returns the full precision key name in the state dictionary"""
return 'fp32'
return 'full_precision'
def state_dict_original_dimension_key():
"""Returns the original dimension key name in the state dictionary"""
return 'original_dim'
def state_dict_sharded_optimizer_keys():
"""Returns the optimizer key names that can be sharded in the state dictionary"""
return {
'Moment_1',
'Moment_2'
}
def state_dict_user_dict_key():
"""Returns the user dict key name in the state dictionary"""
return 'user_dict'
def state_dict_trainer_options_mixed_precision_key():
"""Returns the trainer options mixed precision key name in the state dictionary"""
return 'mixed_precision'
def state_dict_trainer_options_zero_stage_key():
"""Returns the trainer options zero_stage key name in the state dictionary"""
return 'zero_stage'
def state_dict_trainer_options_world_rank_key():
"""Returns the trainer options world_rank key name in the state dictionary"""
return 'world_rank'
def state_dict_trainer_options_world_size_key():
"""Returns the trainer options world_size key name in the state dictionary"""
return 'world_size'
def state_dict_trainer_options_optimizer_name_key():
"""Returns the trainer options optimizer_name key name in the state dictionary"""
return 'optimizer_name'

View file

@ -3,6 +3,7 @@ import onnx
import os
import torch
import warnings
from . import _checkpoint_storage, _utils
################################################################################
@ -108,6 +109,233 @@ def experimental_load_checkpoint(ort_trainer, checkpoint_dir, checkpoint_prefix=
else:
return _load_single_checkpoint(ort_trainer, checkpoint_dir, checkpoint_prefix, is_partitioned, strict)
def _order_paths(paths):
"""Reorders the given paths in ascending order of rank and return the ordered list"""
trainer_options_path_tuples = []
world_rank = _utils.state_dict_trainer_options_world_rank_key()
for path in paths:
trainer_options_path_tuples.append((_checkpoint_storage.load(path,
key=_utils.state_dict_trainer_options_key()), path))
ordered_paths = [path for _, path in sorted(trainer_options_path_tuples,
key=lambda trainer_options_path_pair: trainer_options_path_pair[0][world_rank])]
return ordered_paths
def _add_or_update_sharded_key_for_zero(state_key, state_value, state_sub_dict,
model_state_key, original_dim, sharded_states_original_dims):
"""Add or update the record for the sharded state_key in the state_sub_dict"""
# record the original dimension for this state
sharded_states_original_dims[model_state_key] = original_dim
if state_key in state_sub_dict:
# state_dict already contains a record for this state
# since this state is sharded, concatenate the state value to
# the record in the state_dict
state_sub_dict[state_key] = \
np.concatenate((state_sub_dict[state_key], state_value))
else:
# create a new entry for this state in the state_dict
state_sub_dict[state_key] = state_value
def _add_or_validate_unsharded_key_for_zero(state_key, state_value, state_sub_dict, mismatch_error_string):
"""Add or validate the record for the unsharded state_key in the state_sub_dict"""
if state_key in state_sub_dict:
# state_dict already contains a record for this unsharded state.
# assert that all values are the same for this previously loaded state
assert (state_sub_dict[state_key] == state_value).all(), mismatch_error_string
else:
# create a new entry for this state in the state_sub_dict
state_sub_dict[state_key] = state_value
def _aggregate_model_states(rank_state_dict, sharded_states_original_dims, state_dict):
"""Aggregates all model states from the rank_state_dict into state_dict"""
model = _utils.state_dict_model_key()
full_precision = _utils.state_dict_full_precision_key()
partition_info = _utils.state_dict_partition_info_key()
original_dim = _utils.state_dict_original_dimension_key()
# if there are no model states in the rank_state_dict, no model aggregation is needed
if model not in rank_state_dict:
return
if model not in state_dict:
state_dict[model] = {}
if full_precision not in state_dict[model]:
state_dict[model][full_precision] = {}
# iterate over all model state keys
for model_state_key, model_state_value in rank_state_dict[model][full_precision].items():
if model_state_key in rank_state_dict[partition_info]:
# this model state is sharded since a record exists in the partition_info subdict
_add_or_update_sharded_key_for_zero(model_state_key, model_state_value,
state_dict[model][full_precision], model_state_key,
rank_state_dict[partition_info][model_state_key][original_dim], sharded_states_original_dims)
else:
# this model state is not sharded since a record for it does not exist in the partition_info subdict
_add_or_validate_unsharded_key_for_zero(model_state_key, model_state_value,
state_dict[model][full_precision], "Value mismatch for model state {}".format(model_state_key))
def _aggregate_optimizer_states(rank_state_dict, sharded_states_original_dims, state_dict):
"""Aggregates all optimizer states from the rank_state_dict into state_dict"""
optimizer = _utils.state_dict_optimizer_key()
partition_info = _utils.state_dict_partition_info_key()
original_dim = _utils.state_dict_original_dimension_key()
sharded_optimizer_keys = _utils.state_dict_sharded_optimizer_keys()
# if there are no optimizer states in the rank_state_dict, no optimizer aggregation is needed
if optimizer not in rank_state_dict:
return
if optimizer not in state_dict:
state_dict[optimizer] = {}
# iterate over all optimizer state keys
for model_state_key, optimizer_dict in rank_state_dict[optimizer].items():
for optimizer_key, optimizer_value in optimizer_dict.items():
if model_state_key not in state_dict[optimizer]:
state_dict[optimizer][model_state_key] = {}
if optimizer_key in sharded_optimizer_keys and model_state_key in rank_state_dict[partition_info]:
# this optimizer state is sharded since a record exists in the partition_info subdict
_add_or_update_sharded_key_for_zero(optimizer_key, optimizer_value,
state_dict[optimizer][model_state_key], model_state_key,
rank_state_dict[partition_info][model_state_key][original_dim], sharded_states_original_dims)
else:
# this optimizer state is not sharded since a record for it does not exist in the partition_info subdict
# or this optimizer key is not one of the sharded optimizer keys
_add_or_validate_unsharded_key_for_zero(optimizer_key, optimizer_value,
state_dict[optimizer][model_state_key],
"Value mismatch for model state {} and optimizer state {}".format(model_state_key, optimizer_key))
def _reshape_states(sharded_states_original_dims, state_dict):
"""Reshape model and optimizer states in the state_dict according to dimensions in sharded_states_original_dims"""
model = _utils.state_dict_model_key()
full_precision = _utils.state_dict_full_precision_key()
optimizer = _utils.state_dict_optimizer_key()
sharded_optimizer_keys = _utils.state_dict_sharded_optimizer_keys()
for sharded_state_key, original_dim in sharded_states_original_dims.items():
# reshape model states to original_dim
if model in state_dict:
state_dict[model][full_precision][sharded_state_key] = \
state_dict[model][full_precision][sharded_state_key].reshape(original_dim)
# reshape optimizer states to original_dim
if optimizer in state_dict:
for optimizer_key, optimizer_value in state_dict[optimizer][sharded_state_key].items():
if optimizer_key in sharded_optimizer_keys:
state_dict[optimizer][sharded_state_key][optimizer_key] = optimizer_value.reshape(original_dim)
def _aggregate_trainer_options(rank_state_dict, state_dict):
"""Extracts trainer options from rank_state_dict and loads them accordingly on state_dict"""
state_dict[_utils.state_dict_trainer_options_key()] = {}
mixed_precision = _utils.state_dict_trainer_options_mixed_precision_key()
zero_stage = _utils.state_dict_trainer_options_zero_stage_key()
world_rank = _utils.state_dict_trainer_options_world_rank_key()
world_size = _utils.state_dict_trainer_options_world_size_key()
optimizer_name = _utils.state_dict_trainer_options_optimizer_name_key()
state_dict[_utils.state_dict_trainer_options_key()][mixed_precision] = \
rank_state_dict[_utils.state_dict_trainer_options_key()][mixed_precision]
state_dict[_utils.state_dict_trainer_options_key()][zero_stage] = 0
state_dict[_utils.state_dict_trainer_options_key()][world_rank] = 0
state_dict[_utils.state_dict_trainer_options_key()][world_size] = 1
state_dict[_utils.state_dict_trainer_options_key()][optimizer_name] = \
rank_state_dict[_utils.state_dict_trainer_options_key()][optimizer_name]
def aggregate_checkpoints(paths, pytorch_format=True):
"""Aggregate checkpoint files and return a single state dictionary
Aggregates checkpoint files specified by paths and laods the checkpoint file one at a time merging
them into a single state dictionary.
The checkpoint files represented by paths must be saved through ORTTrainer.save_checkpoint() function.
The schema of the state_dict returned will be in the same as the one returned by ORTTrainer.state_dict()
Args:
paths: list of more than one file represented as strings where the checkpoint is saved
pytorch_format: boolean flag to select either ONNX Runtime or PyTorch state schema of the returned state_dict
Returns:
state_dict that can be loaded into an ORTTrainer or into a PyTorch model
"""
# order the paths in ascending order of ranks
ordered_paths = _order_paths(paths)
state_dict = {}
sharded_states_original_dims = {}
world_rank = _utils.state_dict_trainer_options_world_rank_key()
mixed_precision = _utils.state_dict_trainer_options_mixed_precision_key()
zero_stage = _utils.state_dict_trainer_options_zero_stage_key()
world_size = _utils.state_dict_trainer_options_world_size_key()
optimizer_name = _utils.state_dict_trainer_options_optimizer_name_key()
loaded_mixed_precision = None
loaded_world_size = None
loaded_zero_stage = None
loaded_optimizer_name = None
for rank, path in enumerate(ordered_paths):
rank_state_dict = _checkpoint_storage.load(path)
assert _utils.state_dict_partition_info_key() in rank_state_dict, "Missing information: partition_info"
assert _utils.state_dict_trainer_options_key() in rank_state_dict, "Missing information: trainer_options"
assert rank == rank_state_dict[_utils.state_dict_trainer_options_key()][world_rank], \
"Unexpected rank in file at path {}. Expected {}, got {}".\
format(path, rank, rank_state_dict[_utils.state_dict_trainer_options_key()][world_rank])
if loaded_mixed_precision is None:
loaded_mixed_precision = rank_state_dict[_utils.state_dict_trainer_options_key()][mixed_precision]
else:
assert loaded_mixed_precision == rank_state_dict[_utils.state_dict_trainer_options_key()][mixed_precision], \
"Mixed precision state mismatch among checkpoint files. File: {}".format(path)
if loaded_world_size is None:
loaded_world_size = rank_state_dict[_utils.state_dict_trainer_options_key()][world_size]
else:
assert loaded_world_size == rank_state_dict[_utils.state_dict_trainer_options_key()][world_size], \
"World size state mismatch among checkpoint files. File: {}".format(path)
if loaded_zero_stage is None:
loaded_zero_stage = rank_state_dict[_utils.state_dict_trainer_options_key()][zero_stage]
else:
assert loaded_zero_stage == rank_state_dict[_utils.state_dict_trainer_options_key()][zero_stage], \
"Zero stage mismatch among checkpoint files. File: {}".format(path)
if loaded_optimizer_name is None:
loaded_optimizer_name = rank_state_dict[_utils.state_dict_trainer_options_key()][optimizer_name]
else:
assert loaded_optimizer_name == rank_state_dict[_utils.state_dict_trainer_options_key()][optimizer_name], \
"Optimizer name mismatch among checkpoint files. File: {}".format(path)
# aggregate all model states
_aggregate_model_states(rank_state_dict, sharded_states_original_dims, state_dict)
if not pytorch_format:
# aggregate all optimizer states if pytorch_format is False
_aggregate_optimizer_states(rank_state_dict, sharded_states_original_dims, state_dict)
# entry for trainer_options in the state_dict to perform other sanity checks
if _utils.state_dict_trainer_options_key() not in state_dict:
_aggregate_trainer_options(rank_state_dict, state_dict)
# entry for user_dict in the state_dict if not already present
if _utils.state_dict_user_dict_key() not in state_dict and \
_utils.state_dict_user_dict_key() in rank_state_dict:
state_dict[_utils.state_dict_user_dict_key()] = rank_state_dict[_utils.state_dict_user_dict_key()]
# reshape all the sharded tensors based on the original dimensions stored in sharded_states_original_dims
_reshape_states(sharded_states_original_dims, state_dict)
# return a flat structure for PyTorch model in case pytorch_format is True
# else return the hierarchical structure for ORTTrainer
return state_dict[_utils.state_dict_model_key()][_utils.state_dict_full_precision_key()] if pytorch_format else state_dict
################################################################################
# Helper functions
@ -201,7 +429,7 @@ class _CombineZeroCheckpoint(object):
name_split = name.split('_view_')
view_num = None
if(len(name_split) > 1):
view_num = int(name_split[1])
view_num = int(name_split[1])
optimizer_key = ''
mp_suffix = ''
if name_split[0].startswith('Moment_1'):

View file

@ -9,7 +9,7 @@ from functools import partial
import numpy as np
import onnxruntime as ort
from . import _utils, amp, checkpoint, optim, postprocess, ORTTrainerOptions
from . import _utils, amp, checkpoint, optim, postprocess, ORTTrainerOptions, _checkpoint_storage
from .model_desc_validation import _ORTTrainerModelDesc
from onnxruntime.tools.symbolic_shape_infer import SymbolicShapeInference
@ -568,7 +568,7 @@ class ORTTrainer(object):
return onnx_model
def _create_ort_training_session(self, state_dict = {}):
def _create_ort_training_session(self, optimizer_state_dict={}):
# Validating frozen_weights names
unused_frozen_weights = [n for n in self.options.utils.frozen_weights\
if n not in [i.name for i in self._onnx_model.graph.initializer]]
@ -639,8 +639,8 @@ class ORTTrainer(object):
ort_parameters.optimizer_int_attributes_map = optimizer_int_attributes_map
if bool(self._optim_state_dict):
ort_parameters.set_optimizer_initial_state(self._optim_state_dict)
if bool(state_dict) and bool(state_dict[_utils.state_dict_optimizer_key()]):
ort_parameters.set_optimizer_initial_state(state_dict[_utils.state_dict_optimizer_key()])
if bool(optimizer_state_dict):
ort_parameters.set_optimizer_initial_state(optimizer_state_dict)
ort_parameters.attn_dropout_recompute = self.options.graph_transformer.attn_dropout_recompute
ort_parameters.gelu_recompute = self.options.graph_transformer.gelu_recompute
@ -688,13 +688,13 @@ class ORTTrainer(object):
if self.options._internal_use.extra_postprocess:
self._onnx_model = self.options._internal_use.extra_postprocess(self._onnx_model)
state_dict = {}
optimizer_state_dict = {}
if self._load_state_dict:
state_dict = self._load_state_dict()
optimizer_state_dict = self._load_state_dict()
self._init_session(state_dict)
self._init_session(optimizer_state_dict)
def _init_session(self, state_dict = {}):
def _init_session(self, optimizer_state_dict={}):
if self._onnx_model is None:
return
@ -703,7 +703,7 @@ class ORTTrainer(object):
# Create training session used by train_step
# pass all optimizer states to the backend
self._create_ort_training_session(state_dict)
self._create_ort_training_session(optimizer_state_dict)
# Update model description to update dtype when mixed precision is enabled
# C++ backend modifies model's output dtype from float32 to float16 for mixed precision
@ -886,13 +886,19 @@ class ORTTrainer(object):
def _extract_trainer_options(self, state_dict):
"""Extract relevant trainer configuration and load it into the state_dict"""
mixed_precision = _utils.state_dict_trainer_options_mixed_precision_key()
zero_stage = _utils.state_dict_trainer_options_zero_stage_key()
world_rank = _utils.state_dict_trainer_options_world_rank_key()
world_size = _utils.state_dict_trainer_options_world_size_key()
optimizer_name = _utils.state_dict_trainer_options_optimizer_name_key()
state_dict[_utils.state_dict_trainer_options_key()] = {}
state_dict[_utils.state_dict_trainer_options_key()]['mixed_precision'] = self.options.mixed_precision.enabled
state_dict[_utils.state_dict_trainer_options_key()]['zero_stage'] = \
self.options.distributed.deepspeed_zero_optimization.stage or 0
state_dict[_utils.state_dict_trainer_options_key()]['world_rank'] = self.options.distributed.world_rank or 0
state_dict[_utils.state_dict_trainer_options_key()]['world_size'] = self.options.distributed.world_size or 1
state_dict[_utils.state_dict_trainer_options_key()]['optimizer_name'] = self.optim_config.name
state_dict[_utils.state_dict_trainer_options_key()][mixed_precision] = self.options.mixed_precision.enabled
state_dict[_utils.state_dict_trainer_options_key()][zero_stage] = \
self.options.distributed.deepspeed_zero_optimization.stage
state_dict[_utils.state_dict_trainer_options_key()][world_rank] = self.options.distributed.world_rank
state_dict[_utils.state_dict_trainer_options_key()][world_size] = self.options.distributed.world_size
state_dict[_utils.state_dict_trainer_options_key()][optimizer_name] = self.optim_config.name
def state_dict(self, pytorch_format=False):
"""Returns a dictionary with model, and optionally, optimizer states
@ -911,7 +917,7 @@ class ORTTrainer(object):
type: dict,
schema:
{
"fp32":
"full_precision":
{
type: dict,
schema:
@ -1082,9 +1088,30 @@ class ORTTrainer(object):
def _load_optimizer_states(self, current_state_dict, state_dict):
"""Load the optimizer states onto the training session state dictionary"""
def _check_optimizer_mismatch(state_dict):
"""Assert that the loaded optimizer has the same config as the current training session config"""
# the state_dict optimizer_name can be a byte string (if coming from checkpoint file)
# or can be a regular string (coming from user)
optimizer_name = \
state_dict[_utils.state_dict_trainer_options_key()][_utils.state_dict_trainer_options_optimizer_name_key()]
# optimizer_name can be either a regular string or a byte string.
# if it is a byte string, convert to regular string using decode()
# if it is a regular string, do nothing to it
try:
optimizer_name = optimizer_name.decode()
except AttributeError:
pass
assert self.optim_config.name == optimizer_name, \
"Optimizer mismatch: expected {}, got {}".format(self.optim_config.name, optimizer_name)
if _utils.state_dict_optimizer_key() not in state_dict:
return
# check optimizer config names are the same for current session and the sessino being loaded
_check_optimizer_mismatch(state_dict)
# create an entry for the optimizer in the training session state dictionary
if _utils.state_dict_optimizer_key() not in current_state_dict:
current_state_dict[_utils.state_dict_optimizer_key()] = {}
@ -1179,7 +1206,8 @@ class ORTTrainer(object):
# dictionary
self._load_optimizer_states(current_state_dict, state_dict)
return current_state_dict
return current_state_dict[_utils.state_dict_optimizer_key()] if \
_utils.state_dict_optimizer_key() in current_state_dict else {}
def load_state_dict(self, state_dict, strict=True):
"""Loads state_dict containing model/optimizer states into ORTTrainer
@ -1203,8 +1231,80 @@ class ORTTrainer(object):
return
# load states onto the frontend onnx graph
state_dict = self._load_state_dict_impl(state_dict, strict=strict)
optimizer_state_dict = self._load_state_dict_impl(state_dict, strict=strict)
# create a new training session after loading initializer states onto the onnx graph
# pass the populated states to the training session to populate the backend graph
self._init_session(state_dict)
self._init_session(optimizer_state_dict)
def save_checkpoint(self, path, user_dict={}, include_optimizer_states=True):
"""Persists ORTTrainer state dictionary on disk along with user_dict.
Saves the state_dict along with the user_dict to a file specified by path.
Args:
path: string representation to a file path or a python file-like object.
if file already exists at path, an exception is raised.
user_dict: custom data to be saved along with the state_dict. This data will be returned
to the user when load_checkpoint is called.
include_optimizer_states: boolean flag indicating whether or not to persist the optimizer states.
on load_checkpoint, only model states will be loaded if include_optimizer_states==True
"""
# extract state_dict to be saved in the checkpoint
state_dict = self.state_dict()
# if user_dict is provided, serialize to bytes and convert to hex string.
# this helps in loading the types as they are given by the user since hdf5
# converts to numpy types otherwise
if bool(user_dict):
state_dict[_utils.state_dict_user_dict_key()] = _checkpoint_storage.to_serialized_hex(user_dict)
# if include_optimizer_states is False, only save the model states in the checkpoint file
if not include_optimizer_states:
if _utils.state_dict_optimizer_key() in state_dict:
del state_dict[_utils.state_dict_optimizer_key()]
_checkpoint_storage.save(state_dict, path)
def _aggregation_required(self, loaded_trainer_options):
"""Checks if aggregation is required for the loading the state_dict into the ORTTrainer"""
# To load states in the backend, aggregation is required for every ZeRO checkpoint
return loaded_trainer_options[_utils.state_dict_trainer_options_zero_stage_key()] > 0
def load_checkpoint(self, *paths, strict=True):
"""Loads the saved checkpoint state dictionary into the ORTTrainer
Reads the saved checkpoint files specified by paths from disk and loads the state dictionary
onto the ORTTrainer.
Aggregates the checkpoint files if aggregation is required.
Args:
paths: one or more files represented as strings where the checkpoint is saved
strict: boolean flag to strictly enforce that the saved checkpoint state_dict
keys match the keys from ORTTrainer.state_dict
Returns:
dictionary that the user had saved when calling save_checkpoint
"""
state_dict = {}
# check if aggregation is required
loaded_trainer_options = _checkpoint_storage.load(paths[0], key=_utils.state_dict_trainer_options_key())
if self._aggregation_required(loaded_trainer_options):
# if aggregation is required, aggregation logic must be run on the saved checkpoints
state_dict = checkpoint.aggregate_checkpoints(paths, pytorch_format=False)
else:
# if aggregation is not required, there must only be a single file that needs to be loaded
assert len(paths) == 1, "Expected number of files to load: 1, got {}".format(len(paths))
state_dict = _checkpoint_storage.load(paths[0])
# extract user dict from the saved checkpoint
user_dict = {}
if _utils.state_dict_user_dict_key() in state_dict:
user_dict = _checkpoint_storage.from_serialized_hex(state_dict[_utils.state_dict_user_dict_key()])
del state_dict[_utils.state_dict_user_dict_key()]
self.load_state_dict(state_dict, strict=strict)
return user_dict

View file

@ -8,7 +8,6 @@ import numpy as np
import os
import shutil
import pickle
import binascii
from onnxruntime.training import _checkpoint_storage
@ -215,7 +214,7 @@ def test_checkpoint_storage_for_custom_user_dict_succeeds(checkpoint_storage_tes
'custom_class': custom_class
}
pickled_bytes = binascii.b2a_hex(pickle.dumps(user_dict))
pickled_bytes = pickle.dumps(user_dict).hex()
to_save = {
'a': torch.tensor(np.array([1.0, 2.0]), dtype=torch.float32),
'user_dict': pickled_bytes
@ -224,7 +223,11 @@ def test_checkpoint_storage_for_custom_user_dict_succeeds(checkpoint_storage_tes
loaded_dict = _checkpoint_storage.load(pytest.checkpoint_path)
assert (loaded_dict['a'] == to_save['a'].numpy()).all()
loaded_obj = pickle.loads(binascii.a2b_hex(loaded_dict['user_dict']))
try:
loaded_dict['user_dict'] = loaded_dict['user_dict'].decode()
except AttributeError:
pass
loaded_obj = pickle.loads(bytes.fromhex(loaded_dict['user_dict']))
assert torch.all(loaded_obj['tensor1'].eq(user_dict['tensor1']))
assert loaded_obj['custom_class'] == custom_class

View file

@ -1,7 +1,7 @@
import pytest
from unittest.mock import patch, Mock
from orttraining_test_orttrainer_frontend import _load_pytorch_transformer_model
from onnxruntime.training import amp, checkpoint, optim, orttrainer
from onnxruntime.training import amp, checkpoint, optim, orttrainer, _checkpoint_storage
import numpy as np
import onnx
import torch
@ -59,7 +59,7 @@ def _get_load_state_dict_strict_error_arguments():
training_session_state_dict = {
'model': {
'fp32': {
'full_precision': {
'a': np.arange(5),
'b': np.arange(7)
}
@ -77,20 +77,20 @@ def _get_load_state_dict_strict_error_arguments():
# input state dictionaries
precision_key_missing = {'model': {}, 'optimizer': {}}
precision_key_unexpected = {'model': {'fp32': {}, 'fp16': {}}, 'optimizer': {}}
model_state_key_missing = {'model': {'fp32': {}}, 'optimizer': {}}
model_state_key_unexpected = {'model': {'fp32': {'a': 2, 'b': 3, 'c': 4}}, 'optimizer': {}}
optimizer_model_state_key_missing = {'model': {'fp32': {'a': 2, 'b': 3}}, 'optimizer': {}}
optimizer_model_state_key_unexpected = {'model': {'fp32': {'a': 2, 'b': 3}}, 'optimizer': \
precision_key_unexpected = {'model': {'full_precision': {}, 'mixed_precision': {}}, 'optimizer': {}}
model_state_key_missing = {'model': {'full_precision': {}}, 'optimizer': {}}
model_state_key_unexpected = {'model': {'full_precision': {'a': 2, 'b': 3, 'c': 4}}, 'optimizer': {}}
optimizer_model_state_key_missing = {'model': {'full_precision': {'a': 2, 'b': 3}}, 'optimizer': {}}
optimizer_model_state_key_unexpected = {'model': {'full_precision': {'a': 2, 'b': 3}}, 'optimizer': \
{'a': {}, 'shared_optimizer_state': {}, 'b': {}}}
optimizer_state_key_missing = {'model': {'fp32': {'a': 2, 'b': 3}}, 'optimizer': \
optimizer_state_key_missing = {'model': {'full_precision': {'a': 2, 'b': 3}}, 'optimizer': \
{'a': {}, 'shared_optimizer_state': {'step': np.arange(5)}}}
optimizer_state_key_unexpected = {'model': {'fp32': {'a': 2, 'b': 3}}, 'optimizer': \
optimizer_state_key_unexpected = {'model': {'full_precision': {'a': 2, 'b': 3}}, 'optimizer': \
{'a': {'Moment_1': np.arange(5), 'Moment_2': np.arange(7)}, 'shared_optimizer_state': {'step': np.arange(5), 'another_step': np.arange(1)}}}
input_arguments = [
(training_session_state_dict, precision_key_missing, ['fp32']),
(training_session_state_dict, precision_key_unexpected, ['fp16']),
(training_session_state_dict, precision_key_missing, ['full_precision']),
(training_session_state_dict, precision_key_unexpected, ['mixed_precision']),
(training_session_state_dict, model_state_key_missing, ['a', 'b']),
(training_session_state_dict, model_state_key_unexpected, ['c']),
(training_session_state_dict, optimizer_model_state_key_missing, ['a', 'shared_optimizer_state']),
@ -126,7 +126,7 @@ def test_training_session_provides_empty_model_states(onnx_model_mock):
def test_training_session_provides_model_states(onnx_model_mock):
trainer = _create_trainer()
model_states = {
'fp32': {
'full_precision': {
'a': np.arange(5),
'b': np.arange(7)
}
@ -136,14 +136,14 @@ def test_training_session_provides_model_states(onnx_model_mock):
trainer._onnx_model = onnx_model_mock()
state_dict = trainer.state_dict()
assert (state_dict['model']['fp32']['a'] == np.arange(5)).all()
assert (state_dict['model']['fp32']['b'] == np.arange(7)).all()
assert (state_dict['model']['full_precision']['a'] == np.arange(5)).all()
assert (state_dict['model']['full_precision']['b'] == np.arange(7)).all()
@patch('onnx.ModelProto')
def test_training_session_provides_model_states_pytorch_format(onnx_model_mock):
trainer = _create_trainer()
model_states = {
'fp32': {
'full_precision': {
'a': np.arange(5),
'b': np.arange(7)
}
@ -160,7 +160,7 @@ def test_training_session_provides_model_states_pytorch_format(onnx_model_mock):
def test_onnx_graph_provides_frozen_model_states(onnx_model_mock):
trainer = _create_trainer()
model_states = {
'fp32': {
'full_precision': {
'a': np.arange(5),
'b': np.arange(7)
}
@ -176,11 +176,11 @@ def test_onnx_graph_provides_frozen_model_states(onnx_model_mock):
]
state_dict = trainer.state_dict()
assert (state_dict['model']['fp32']['a'] == np.arange(5)).all()
assert (state_dict['model']['fp32']['b'] == np.arange(7)).all()
assert (state_dict['model']['fp32']['a_frozen_weight'] == np.array([1, 2, 3], dtype=np.float32)).all()
assert 'a_non_fronzen_weight' not in state_dict['model']['fp32']
assert (state_dict['model']['fp32']['a_float16_weight'] == np.array([7, 8, 9], dtype=np.float32)).all()
assert (state_dict['model']['full_precision']['a'] == np.arange(5)).all()
assert (state_dict['model']['full_precision']['b'] == np.arange(7)).all()
assert (state_dict['model']['full_precision']['a_frozen_weight'] == np.array([1, 2, 3], dtype=np.float32)).all()
assert 'a_non_fronzen_weight' not in state_dict['model']['full_precision']
assert (state_dict['model']['full_precision']['a_float16_weight'] == np.array([7, 8, 9], dtype=np.float32)).all()
@patch('onnx.ModelProto')
def test_training_session_provides_empty_optimizer_states(onnx_model_mock):
@ -217,7 +217,7 @@ def test_training_session_provides_optimizer_states(onnx_model_mock):
def test_training_session_provides_optimizer_states_pytorch_format(onnx_model_mock):
trainer = _create_trainer()
model_states = {
'fp32': {
'full_precision': {
'a': np.arange(5),
'b': np.arange(7)
}
@ -267,7 +267,7 @@ def test_training_session_provides_partition_info_map(onnx_model_mock):
def test_training_session_provides_all_states(onnx_model_mock):
trainer = _create_trainer(zero_enabled=True)
model_states = {
'fp32': {
'full_precision': {
'a': np.arange(5),
'b': np.arange(7)
}
@ -291,8 +291,8 @@ def test_training_session_provides_all_states(onnx_model_mock):
trainer._onnx_model = onnx_model_mock()
state_dict = trainer.state_dict()
assert (state_dict['model']['fp32']['a'] == np.arange(5)).all()
assert (state_dict['model']['fp32']['b'] == np.arange(7)).all()
assert (state_dict['model']['full_precision']['a'] == np.arange(5)).all()
assert (state_dict['model']['full_precision']['b'] == np.arange(7)).all()
assert (state_dict['optimizer']['model_weight']['Moment_1'] == np.arange(5)).all()
assert (state_dict['optimizer']['model_weight']['Moment_2'] == np.arange(7)).all()
assert (state_dict['optimizer']['shared_optimizer_state']['step'] == np.arange(1)).all()
@ -302,7 +302,7 @@ def test_load_state_dict_holds_when_training_session_not_initialized():
trainer = _create_trainer()
state_dict = {
'model': {
'fp32': {
'full_precision': {
'a': np.arange(5),
'b': np.arange(7)
}
@ -321,7 +321,27 @@ def test_load_state_dict_holds_when_training_session_not_initialized():
state_dict = trainer.load_state_dict(state_dict)
assert trainer._load_state_dict
@pytest.mark.parametrize("state_dict, input_state_dict, error_key", [({'optimizer':{}}, {'optimizer':{}}, 'model'), ({'model':{}}, {'model':{}}, 'optimizer')])
@pytest.mark.parametrize("state_dict, input_state_dict, error_key", [
({
'optimizer':{},
},
{
'optimizer':{},
'trainer_options': {
'optimizer_name': 'LambOptimizer'
}
},
'model'),
({
'model':{}
},
{
'model':{},
'trainer_options': {
'optimizer_name': 'LambOptimizer'
}
},
'optimizer')])
def test_load_state_dict_warns_when_model_optimizer_key_missing(state_dict, input_state_dict, error_key):
trainer = _create_trainer()
trainer._training_session = _training_session_mock({}, {}, {})
@ -351,7 +371,7 @@ def test_load_state_dict_loads_the_states_and_inits_training_session(onnx_model_
trainer = _create_trainer()
training_session_state_dict = {
'model': {
'fp32': {
'full_precision': {
'a': np.arange(5),
'b': np.arange(7)
}
@ -369,7 +389,7 @@ def test_load_state_dict_loads_the_states_and_inits_training_session(onnx_model_
input_state_dict = {
'model': {
'fp32': {
'full_precision': {
'a': np.array([1, 2]),
'b': np.array([3, 4])
}
@ -382,6 +402,9 @@ def test_load_state_dict_loads_the_states_and_inits_training_session(onnx_model_
'shared_optimizer_state': {
'step': np.array([9])
}
},
'trainer_options': {
'optimizer_name': 'LambOptimizer'
}
}
trainer._training_session = _training_session_mock({}, {}, {})
@ -404,6 +427,252 @@ def test_load_state_dict_loads_the_states_and_inits_training_session(onnx_model_
assert 'b' in loaded_initializers[0]
assert (loaded_initializers[0]['b'] == np.array([3, 4])).all()
assert (state_dict_to_load[0]['optimizer']['a']['Moment_1'] == np.array([5, 6])).all()
assert (state_dict_to_load[0]['optimizer']['a']['Moment_2'] == np.array([7, 8])).all()
assert (state_dict_to_load[0]['optimizer']['shared_optimizer_state']['step'] == np.array([9])).all()
assert (state_dict_to_load[0]['a']['Moment_1'] == np.array([5, 6])).all()
assert (state_dict_to_load[0]['a']['Moment_2'] == np.array([7, 8])).all()
assert (state_dict_to_load[0]['shared_optimizer_state']['step'] == np.array([9])).all()
@patch('onnxruntime.training._checkpoint_storage.save')
def test_save_checkpoint_calls_checkpoint_storage_save(save_mock):
trainer = _create_trainer()
state_dict = {
'model': {},
'optimizer': {}
}
trainer.state_dict = Mock(return_value=state_dict)
trainer.save_checkpoint('abc')
save_args, _ = save_mock.call_args
assert 'model' in save_args[0]
assert not bool(save_args[0]['model'])
assert 'optimizer' in save_args[0]
assert not bool(save_args[0]['optimizer'])
assert save_args[1] == 'abc'
@patch('onnxruntime.training._checkpoint_storage.save')
def test_save_checkpoint_exclude_optimizer_states(save_mock):
trainer = _create_trainer()
state_dict = {
'model': {},
'optimizer': {}
}
trainer.state_dict = Mock(return_value=state_dict)
trainer.save_checkpoint('abc', include_optimizer_states=False)
save_args, _ = save_mock.call_args
assert 'model' in save_args[0]
assert not bool(save_args[0]['model'])
assert 'optimizer' not in save_args[0]
assert save_args[1] == 'abc'
@patch('onnxruntime.training._checkpoint_storage.save')
def test_save_checkpoint_user_dict(save_mock):
trainer = _create_trainer()
state_dict = {
'model': {},
'optimizer': {}
}
trainer.state_dict = Mock(return_value=state_dict)
trainer.save_checkpoint('abc', user_dict={'abc': np.arange(4)})
save_args, _ = save_mock.call_args
assert 'user_dict' in save_args[0]
assert save_args[0]['user_dict'] == _checkpoint_storage.to_serialized_hex({'abc': np.arange(4)})
@patch('onnxruntime.training._checkpoint_storage.load')
@patch('onnxruntime.training.checkpoint.aggregate_checkpoints')
def test_load_checkpoint(aggregate_checkpoints_mock, load_mock):
trainer = _create_trainer()
trainer_options = {
'mixed_precision': np.bool_(False),
'world_rank': np.int64(0),
'world_size': np.int64(1),
'zero_stage': np.int64(0)
}
state_dict = {
'model': {},
'optimizer': {},
'trainer_options': {
'mixed_precision': np.bool_(False),
'world_rank': np.int64(0),
'world_size': np.int64(1),
'zero_stage': np.int64(0)
}
}
trainer.load_state_dict = Mock()
load_mock.side_effect = [trainer_options, state_dict]
trainer.load_checkpoint('abc')
args_list = load_mock.call_args_list
load_args, load_kwargs = args_list[0]
assert load_args[0] == 'abc'
assert load_kwargs['key'] == 'trainer_options'
load_args, load_kwargs = args_list[1]
assert load_args[0] == 'abc'
assert 'key' not in load_kwargs
assert not aggregate_checkpoints_mock.called
@patch('onnxruntime.training._checkpoint_storage.load')
@patch('onnxruntime.training.checkpoint.aggregate_checkpoints')
@pytest.mark.parametrize("trainer_options", [
{
'mixed_precision': np.bool_(False),
'world_rank': np.int64(0),
'world_size': np.int64(4),
'zero_stage': np.int64(1)
},
{
'mixed_precision': np.bool_(True),
'world_rank': np.int64(0),
'world_size': np.int64(1),
'zero_stage': np.int64(1)
},
{
'mixed_precision': np.bool_(True),
'world_rank': np.int64(0),
'world_size': np.int64(1),
'zero_stage': np.int64(1)
}
])
def test_load_checkpoint_aggregation_required_zero_enabled(aggregate_checkpoints_mock, load_mock, trainer_options):
trainer = _create_trainer()
trainer.load_state_dict = Mock()
load_mock.side_effect = [trainer_options]
trainer.load_checkpoint('abc')
args_list = load_mock.call_args_list
load_args, load_kwargs = args_list[0]
assert load_args[0] == 'abc'
assert load_kwargs['key'] == 'trainer_options'
assert aggregate_checkpoints_mock.called
call_args, _ = aggregate_checkpoints_mock.call_args
assert call_args[0] == tuple(['abc'])
@patch('onnxruntime.training._checkpoint_storage.load')
@patch('onnxruntime.training.checkpoint.aggregate_checkpoints')
def test_load_checkpoint_user_dict(aggregate_checkpoints_mock, load_mock):
trainer = _create_trainer()
trainer_options = {
'mixed_precision': np.bool_(False),
'world_rank': np.int64(0),
'world_size': np.int64(1),
'zero_stage': np.int64(0)
}
state_dict = {
'model': {},
'optimizer': {},
'trainer_options': {
'mixed_precision': np.bool_(False),
'world_rank': np.int64(0),
'world_size': np.int64(1),
'zero_stage': np.int64(0)
},
'user_dict': _checkpoint_storage.to_serialized_hex({'array': torch.tensor(np.arange(5))})
}
trainer.load_state_dict = Mock()
load_mock.side_effect = [trainer_options, state_dict]
user_dict = trainer.load_checkpoint('abc')
assert torch.all(torch.eq(user_dict['array'], torch.tensor(np.arange(5))))
@patch('onnxruntime.training._checkpoint_storage.load')
def test_checkpoint_aggregation(load_mock):
trainer_options1 = {
'mixed_precision': np.bool_(False),
'world_rank': np.int64(0),
'world_size': np.int64(2),
'zero_stage': np.int64(1),
'optimizer_name': b'Adam'
}
trainer_options2 = {
'mixed_precision': np.bool_(False),
'world_rank': np.int64(1),
'world_size': np.int64(2),
'zero_stage': np.int64(1),
'optimizer_name': b'Adam'
}
state_dict1 = {
'model': {
'full_precision': {
'sharded': np.array([1, 2, 3]),
'non_sharded': np.array([11, 22, 33])
}
},
'optimizer': {
'sharded': {
'Moment_1': np.array([9, 8, 7]),
'Moment_2': np.array([99, 88, 77]),
'Step': np.array([5])
},
'non_sharded': {
'Moment_1': np.array([666, 555, 444]),
'Moment_2': np.array([6666, 5555, 4444]),
'Step': np.array([55])
}
},
'trainer_options': {
'mixed_precision': np.bool_(False),
'world_rank': np.int64(0),
'world_size': np.int64(1),
'zero_stage': np.int64(0),
'optimizer_name': b'Adam'
},
'partition_info': {
'sharded': {'original_dim': np.array([2, 3])}
}
}
state_dict2 = {
'model': {
'full_precision': {
'sharded': np.array([4, 5, 6]),
'non_sharded': np.array([11, 22, 33])
}
},
'optimizer': {
'sharded': {
'Moment_1': np.array([6, 5, 4]),
'Moment_2': np.array([66, 55, 44]),
'Step': np.array([5])
},
'non_sharded': {
'Moment_1': np.array([666, 555, 444]),
'Moment_2': np.array([6666, 5555, 4444]),
'Step': np.array([55])
}
},
'trainer_options': {
'mixed_precision': np.bool_(False),
'world_rank': np.int64(1),
'world_size': np.int64(1),
'zero_stage': np.int64(0),
'optimizer_name': b'Adam'
},
'partition_info': {
'sharded': {'original_dim': np.array([2, 3])}
}
}
load_mock.side_effect = [trainer_options1, trainer_options2, state_dict1, state_dict2]
state_dict = checkpoint.aggregate_checkpoints(['abc', 'def'], pytorch_format=False)
assert (state_dict['model']['full_precision']['sharded'] == np.array([[1, 2, 3], [4, 5, 6]])).all()
assert (state_dict['model']['full_precision']['non_sharded'] == np.array([11, 22, 33])).all()
assert (state_dict['optimizer']['sharded']['Moment_1'] == np.array([[9, 8, 7], [6, 5, 4]])).all()
assert (state_dict['optimizer']['sharded']['Moment_2'] == np.array([[99, 88, 77], [66, 55, 44]])).all()
assert (state_dict['optimizer']['sharded']['Step'] == np.array([5])).all()
assert (state_dict['optimizer']['non_sharded']['Moment_1'] == np.array([666, 555, 444])).all()
assert (state_dict['optimizer']['non_sharded']['Moment_2'] == np.array([6666, 5555, 4444])).all()
assert (state_dict['optimizer']['non_sharded']['Step'] == np.array([55])).all()
assert state_dict['trainer_options']['mixed_precision'] == False
assert state_dict['trainer_options']['world_rank'] == 0
assert state_dict['trainer_options']['world_size'] == 1
assert state_dict['trainer_options']['zero_stage'] == 0
assert state_dict['trainer_options']['optimizer_name'] == b'Adam'