Files
wehub-resource-sync a203934033
Lint test / lint (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:34:58 +08:00

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__