411 lines
15 KiB
Python
411 lines
15 KiB
Python
# Copyright (c) ModelScope Contributors. All rights reserved.
|
|
# Part of the implementation is borrowed from huggingface/transformers.
|
|
import inspect
|
|
import math
|
|
import os
|
|
import torch
|
|
import torch.distributed as dist
|
|
import torch.nn.functional as F
|
|
from contextlib import contextmanager
|
|
from modelscope.hub.api import HubApi
|
|
from peft import PeftModel
|
|
from torch import nn
|
|
from torch.nn import CrossEntropyLoss, Module
|
|
from transformers import PreTrainedModel
|
|
from types import FunctionType, MethodType
|
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
|
|
from swift.model import ModelMeta
|
|
from swift.sequence_parallel import ChunkedCrossEntropyLoss, GatherLoss, sequence_parallel
|
|
from swift.utils import deep_getattr, get_dist_setting, get_logger
|
|
|
|
if TYPE_CHECKING:
|
|
from .arguments import TrainingArguments
|
|
|
|
logger = get_logger()
|
|
|
|
|
|
def _get_deepspeed_elastic_world_size():
|
|
if dist.is_available() and dist.is_initialized():
|
|
return dist.get_world_size()
|
|
return get_dist_setting()[2]
|
|
|
|
|
|
def _enable_load_universal(ds_config):
|
|
if isinstance(ds_config, dict):
|
|
checkpoint = ds_config.get('checkpoint')
|
|
if not isinstance(checkpoint, dict):
|
|
checkpoint = {}
|
|
ds_config['checkpoint'] = checkpoint
|
|
checkpoint['load_universal'] = True
|
|
|
|
|
|
def enable_deepspeed_load_universal(args: 'TrainingArguments', trainer=None):
|
|
_enable_load_universal(getattr(args, 'deepspeed', None))
|
|
|
|
hf_ds_config = getattr(args, 'hf_deepspeed_config', None)
|
|
_enable_load_universal(getattr(hf_ds_config, 'config', None))
|
|
|
|
deepspeed_plugin = getattr(args, 'deepspeed_plugin', None)
|
|
if trainer is not None and deepspeed_plugin is None:
|
|
accelerator = getattr(trainer, 'accelerator', None)
|
|
state = getattr(accelerator, 'state', None)
|
|
deepspeed_plugin = getattr(state, 'deepspeed_plugin', None)
|
|
if deepspeed_plugin is not None:
|
|
_enable_load_universal(getattr(deepspeed_plugin, 'deepspeed_config', None))
|
|
plugin_hf_ds_config = getattr(deepspeed_plugin, 'hf_ds_config', None)
|
|
_enable_load_universal(getattr(plugin_hf_ds_config, 'config', None))
|
|
|
|
|
|
def prepare_deepspeed_elastic_config(args: 'TrainingArguments', state=None):
|
|
ds_config = args.deepspeed
|
|
if not ds_config:
|
|
return
|
|
if not isinstance(ds_config, dict):
|
|
logger.warning('DeepSpeed elastic expects args.deepspeed to be a dict, but got '
|
|
f'{type(ds_config).__name__}. Skip elastic config.')
|
|
return
|
|
|
|
from deepspeed.elasticity import compute_elastic_config
|
|
from deepspeed.git_version_info import version as __version__
|
|
|
|
enable_deepspeed_load_universal(args)
|
|
elasticity = ds_config.get('elasticity') or {}
|
|
if not elasticity:
|
|
logger.warning_once('DeepSpeed elastic callback is enabled, but no `elasticity` section is found in '
|
|
'the DeepSpeed config. Only `checkpoint.load_universal` is enabled.')
|
|
return
|
|
if elasticity.get('enabled') is False:
|
|
return
|
|
|
|
world_size = _get_deepspeed_elastic_world_size()
|
|
final_batch_size, _, micro_batch_size = compute_elastic_config(
|
|
ds_config=ds_config,
|
|
target_deepspeed_version=__version__,
|
|
world_size=world_size,
|
|
)
|
|
if world_size <= 0 or micro_batch_size <= 0:
|
|
raise ValueError('DeepSpeed elastic config produced invalid batch settings: '
|
|
f'world_size={world_size}, micro_batch_size={micro_batch_size}.')
|
|
gradient_accu_steps = max(1, final_batch_size // (micro_batch_size * world_size))
|
|
args.per_device_train_batch_size = micro_batch_size
|
|
args.gradient_accumulation_steps = gradient_accu_steps
|
|
if state is not None:
|
|
state.train_batch_size = args.per_device_train_batch_size * max(1, args.n_gpu)
|
|
logger.info_once('DeepSpeed elastic config is enabled. '
|
|
f'world_size: {world_size}, '
|
|
f'per_device_train_batch_size: {args.per_device_train_batch_size}, '
|
|
f'gradient_accumulation_steps: {args.gradient_accumulation_steps}')
|
|
|
|
|
|
def can_return_loss(model: Module) -> bool:
|
|
"""Check if a given model can return loss."""
|
|
if isinstance(model, PeftModel):
|
|
signature = inspect.signature(model.model.forward)
|
|
else:
|
|
signature = inspect.signature(model.forward)
|
|
for p in signature.parameters:
|
|
if p == 'return_loss' and signature.parameters[p].default is True:
|
|
return True
|
|
return False
|
|
|
|
|
|
def find_labels(model: Module) -> List[str]:
|
|
"""Find the labels used by a given model."""
|
|
model_name = model.__class__.__name__
|
|
if isinstance(model, PeftModel):
|
|
signature = inspect.signature(model.model.forward)
|
|
else:
|
|
signature = inspect.signature(model.forward)
|
|
if 'QuestionAnswering' in model_name:
|
|
return [p for p in signature.parameters if 'label' in p or p in ('start_positions', 'end_positions')]
|
|
else:
|
|
return [p for p in signature.parameters if 'label' in p]
|
|
|
|
|
|
def get_function(method_or_function: Union[MethodType, FunctionType]) -> FunctionType:
|
|
if isinstance(method_or_function, MethodType):
|
|
method_or_function = method_or_function.__func__
|
|
return method_or_function
|
|
|
|
|
|
def is_instance_of_ms_model(model: Module) -> bool:
|
|
"""avoid import modelscope: circular dependency problem"""
|
|
for m_cls in model.__class__.__mro__:
|
|
cls_name = m_cls.__name__
|
|
cls_module = m_cls.__module__
|
|
if cls_name == 'Model' and cls_module.startswith('modelscope'):
|
|
return True
|
|
return False
|
|
|
|
|
|
def per_token_loss_func_sp(outputs, labels, enable_dft_loss=False, **kwargs) -> torch.Tensor:
|
|
"""Common loss function for sequence parallel training"""
|
|
if hasattr(outputs, 'logits'):
|
|
logits = outputs.logits
|
|
else:
|
|
logits = outputs
|
|
device = logits.device
|
|
|
|
batch_size = logits.shape[0]
|
|
logits = logits.view(-1, logits.shape[-1])
|
|
labels = labels.flatten().to(device)
|
|
sploss_parallel_size = int(os.environ.get('CELOSS_PARALLEL_SIZE', '0'))
|
|
if sploss_parallel_size > 0:
|
|
loss = ChunkedCrossEntropyLoss.apply(logits, labels, sploss_parallel_size)
|
|
else:
|
|
loss_fct = CrossEntropyLoss(reduction='none')
|
|
loss = loss_fct(logits, labels)
|
|
if enable_dft_loss:
|
|
with torch.no_grad():
|
|
target_probs = torch.exp(-loss)
|
|
loss *= target_probs
|
|
position_ids = sequence_parallel.real_position_ids
|
|
if position_ids is not None:
|
|
position_ids = sequence_parallel.pad(position_ids, padding_value=-1, position_ids=position_ids)
|
|
loss, labels = GatherLoss.apply(loss.reshape(batch_size, -1), labels.reshape(batch_size, -1), 1, position_ids)
|
|
if position_ids is not None and position_ids.min() == -1:
|
|
_pos_mask = position_ids >= 0
|
|
loss = loss[_pos_mask].contiguous()
|
|
|
|
return loss
|
|
|
|
|
|
def per_token_loss_func(outputs, labels, enable_dft_loss: bool = False, **kwargs):
|
|
logits = outputs.logits
|
|
# Upcast to float if we need to compute the loss to avoid potential precision issues
|
|
logits = logits.float()
|
|
labels = torch.roll(labels, shifts=-1, dims=-1).view(-1)
|
|
|
|
# Flatten the tokens
|
|
logits = logits.view(-1, logits.shape[-1])
|
|
# Enable model parallelism
|
|
labels = labels.to(logits.device)
|
|
loss = F.cross_entropy(logits, labels, ignore_index=-100, reduction='none')
|
|
if enable_dft_loss:
|
|
with torch.no_grad():
|
|
target_probs = torch.exp(-loss)
|
|
loss *= target_probs
|
|
return loss
|
|
|
|
|
|
def _kwargs_to_args(func, args, kwargs) -> Optional[List[Any]]:
|
|
parameters = inspect.signature(func).parameters
|
|
args = list(args)
|
|
parameters = list(parameters.items())[len(args):]
|
|
for key, param in parameters:
|
|
if key in kwargs:
|
|
args.append(kwargs[key])
|
|
elif param.default != param.empty:
|
|
args.append(param.default)
|
|
else:
|
|
return
|
|
return args
|
|
|
|
|
|
def _add_gradient_checkpointing(module_list):
|
|
|
|
requires_grad = None
|
|
|
|
def _new_forward(self, *args, **kwargs):
|
|
nonlocal requires_grad
|
|
if requires_grad is None:
|
|
requires_grad = any(p.requires_grad for p in self.parameters())
|
|
|
|
new_args = _kwargs_to_args(self.__old_forward, args, kwargs)
|
|
if new_args is not None and self.gradient_checkpointing and self.training:
|
|
if new_args and isinstance(new_args[0], torch.Tensor) and requires_grad and not new_args[0].requires_grad:
|
|
new_args[0].requires_grad_(True)
|
|
layer_ret = self._gradient_checkpointing_func(self.__old_forward, *new_args)
|
|
logger.info_once('Successfully using dynamic gradient checkpointing.')
|
|
else:
|
|
layer_ret = self.__old_forward(*args, **kwargs)
|
|
return layer_ret
|
|
|
|
for module in module_list:
|
|
module.gradient_checkpointing = False
|
|
if hasattr(module, '_old_forward'): # device_map
|
|
__old_forward = module._old_forward
|
|
module._old_forward = MethodType(_new_forward, module)
|
|
else:
|
|
__old_forward = module.forward
|
|
module.forward = MethodType(_new_forward, module)
|
|
module.__old_forward = __old_forward
|
|
|
|
|
|
def find_module_list(model) -> Optional[nn.ModuleList]:
|
|
module_lists = []
|
|
for m in model.modules():
|
|
if hasattr(m, 'gradient_checkpointing') or m.__class__.__name__ == 'CheckpointWrapper':
|
|
return
|
|
if (isinstance(m, (nn.ModuleList, nn.Sequential)) and len(m) >= 10
|
|
and 'mlp' not in m[0].__class__.__name__.lower()): # fix moe
|
|
module_lists.append(m)
|
|
if module_lists:
|
|
return max(module_lists, key=lambda x: len(x))
|
|
|
|
|
|
def dynamic_gradient_checkpointing(model, including_vit: bool = False) -> None:
|
|
if isinstance(model, PeftModel):
|
|
model = model.model
|
|
model_meta: ModelMeta = getattr(model, 'model_meta', None)
|
|
if model_meta is not None and model_meta.is_multimodal and model_meta.model_arch:
|
|
tower_names = model_meta.model_arch.language_model.copy()
|
|
if including_vit:
|
|
tower_names += model_meta.model_arch.vision_tower
|
|
else:
|
|
tower_names = [None]
|
|
|
|
model.supports_gradient_checkpointing = True
|
|
for tower_name in tower_names:
|
|
if tower_name is None:
|
|
model_tower = model
|
|
else:
|
|
model_tower = deep_getattr(model, tower_name)
|
|
if model_tower is None:
|
|
continue
|
|
model_tower.supports_gradient_checkpointing = True
|
|
module_list = find_module_list(model_tower)
|
|
if module_list is None:
|
|
continue
|
|
_add_gradient_checkpointing(module_list)
|
|
logger.info(f'Automatically add gradient_checkpointing to {model_tower.__class__}.')
|
|
|
|
|
|
@contextmanager
|
|
def disable_gradient_checkpointing(model: PreTrainedModel, gradient_checkpointing_kwargs: Optional[Dict] = None):
|
|
"""
|
|
Temporarily disable gradient checkpointing, restoring the previous state afterward.
|
|
|
|
When gradient checkpointing is enabled with use_reentrant=True (default), calling the model inside a
|
|
torch.no_grad() block triggers a harmless PyTorch warning ("None of the inputs have requires_grad=True").
|
|
Temporarily disable checkpointing to avoid this warning during inference.
|
|
|
|
Args:
|
|
model (`PreTrainedModel`):
|
|
Model for which to temporarily disable gradient checkpointing.
|
|
gradient_checkpointing_kwargs (`dict` or `None`, *optional*):
|
|
Additional kwargs for gradient checkpointing enabling.
|
|
"""
|
|
was_enabled = getattr(model, 'is_gradient_checkpointing', False)
|
|
if was_enabled:
|
|
model.gradient_checkpointing_disable()
|
|
try:
|
|
yield
|
|
finally:
|
|
if was_enabled:
|
|
model.gradient_checkpointing_enable(gradient_checkpointing_kwargs)
|
|
|
|
|
|
def gather_for_unpadded_tensors(input_data, use_gather_object=False):
|
|
from accelerate.utils import gather_object
|
|
if getattr(sequence_parallel, 'dp_group', None) is not None:
|
|
input_data = sequence_parallel._gather_object_dp(input_data)
|
|
else:
|
|
input_data = gather_object(input_data)
|
|
output = []
|
|
for _data in input_data:
|
|
if len(_data.shape) == 0:
|
|
_data = _data.unsqueeze(0)
|
|
_data = _data.cpu()
|
|
output.append(_data)
|
|
if len(output[0].shape) == 1 and output[0].shape[0] > 1:
|
|
data = torch.stack(output, dim=0)
|
|
else:
|
|
data = torch.concat(output, dim=0)
|
|
return data
|
|
|
|
|
|
def calculate_max_steps(args: 'TrainingArguments', dataset) -> int:
|
|
if args.max_steps and args.max_steps > 0:
|
|
max_steps = args.max_steps
|
|
else:
|
|
len_dataset = len(dataset)
|
|
_, _, world_size, _ = get_dist_setting()
|
|
total_train_batch_size = args.per_device_train_batch_size * args.gradient_accumulation_steps * world_size
|
|
num_update_steps_per_epoch = len_dataset // total_train_batch_size
|
|
num_update_steps_per_epoch = max(num_update_steps_per_epoch, 1)
|
|
max_steps = math.ceil(args.num_train_epochs * num_update_steps_per_epoch)
|
|
return max_steps
|
|
|
|
|
|
def extract_version(name: str) -> Optional[int]:
|
|
if not name.startswith('v'):
|
|
return None
|
|
try:
|
|
num = name[1:].split('-', 1)[0]
|
|
return int(num)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def get_previous_version_from_path(current_path: str) -> Optional[str]:
|
|
from pathlib import Path
|
|
current = Path(current_path)
|
|
parent = current.parent
|
|
current_name = current.name
|
|
|
|
candidates = [d for d in parent.iterdir() if d.is_dir()]
|
|
|
|
valid = [(d.name, extract_version(d.name)) for d in candidates]
|
|
valid = [(name, ver) for name, ver in valid if ver is not None]
|
|
|
|
valid.sort(key=lambda x: x[1])
|
|
names = [name for name, _ in valid]
|
|
|
|
if current_name not in names:
|
|
return None
|
|
|
|
idx = names.index(current_name)
|
|
if idx == 0:
|
|
return None
|
|
|
|
prev_name = names[idx - 1]
|
|
return str(parent / prev_name)
|
|
|
|
|
|
def get_resume_dir(output_dir):
|
|
return get_previous_version_from_path(output_dir)
|
|
|
|
|
|
def replace_index_file(output_dir: str):
|
|
import json
|
|
import os
|
|
from transformers.utils import SAFE_WEIGHTS_INDEX_NAME, WEIGHTS_INDEX_NAME
|
|
index_file = os.path.join(output_dir, WEIGHTS_INDEX_NAME)
|
|
|
|
if not os.path.exists(index_file):
|
|
return
|
|
with open(index_file, 'r', encoding='utf-8') as f:
|
|
bin_data = json.load(f)
|
|
if 'weight_map' not in bin_data:
|
|
return
|
|
bin_data['weight_map'] = {
|
|
k: v.replace('pytorch_model', 'model').replace('.bin', '.safetensors')
|
|
for k, v in bin_data['weight_map'].items()
|
|
}
|
|
safe_path = os.path.join(output_dir, SAFE_WEIGHTS_INDEX_NAME)
|
|
with open(safe_path, 'w', encoding='utf-8') as f:
|
|
json.dump(bin_data, f, indent=2)
|
|
from contextlib import suppress
|
|
with suppress(FileNotFoundError):
|
|
os.remove(os.path.join(output_dir, WEIGHTS_INDEX_NAME))
|
|
|
|
|
|
@contextmanager
|
|
def patch_modelscope_hub_timeout():
|
|
__init__ = HubApi.__init__
|
|
|
|
def __new_init__(self, *args, **kwargs):
|
|
timeout = kwargs.get('timeout')
|
|
if timeout is not None and timeout > 5:
|
|
kwargs['timeout'] = 5
|
|
__init__(self, *args, **kwargs)
|
|
|
|
HubApi.__init__ = __new_init__
|
|
|
|
try:
|
|
yield
|
|
finally:
|
|
HubApi.__init__ = __init__
|