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

234 lines
12 KiB
Python

# Copyright (c) ModelScope Contributors. All rights reserved.
# Part of the implementation is borrowed from huggingface/transformers.
import inspect
import os
import torch
import torch.distributed as dist
from contextlib import contextmanager, nullcontext
from peft import PeftModel
from torch import nn
from torch.nn.utils.rnn import pad_sequence
from transformers import Seq2SeqTrainer as HfSeq2SeqTrainer
from transformers.models.auto.modeling_auto import MODEL_FOR_CAUSAL_LM_MAPPING_NAMES
from transformers.utils import is_peft_available
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from swift.infer_engine import InferRequest, RequestConfig, TransformersEngine
from swift.sequence_parallel import sequence_parallel
from swift.utils import HfConfigFactory, JsonlWriter, Serializer, gc_collect, get_logger, unwrap_model_for_generation
from .arguments import Seq2SeqTrainingArguments
from .mixin import DataLoaderMixin, SwiftMixin
from .utils import per_token_loss_func, per_token_loss_func_sp
logger = get_logger()
class Seq2SeqTrainer(SwiftMixin, DataLoaderMixin, HfSeq2SeqTrainer):
args: Seq2SeqTrainingArguments
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.model_accepts_loss_kwargs = True # fix transformers>=4.46.2
if self.template.model_accepts_loss_kwargs is not None:
self.model_accepts_loss_kwargs = self.template.model_accepts_loss_kwargs
if self.args.predict_with_generate:
self.infer_engine = TransformersEngine(
self.model, template=self.template, max_batch_size=self.args.per_device_eval_batch_size)
self.jsonl_writer = JsonlWriter(os.path.join(self.args.output_dir, 'predict.jsonl'))
@staticmethod
def _predict_data_collator(batch):
return {'_data': batch}
@contextmanager
def _patch_predict_with_generate(self):
origin_data_collator = self.data_collator
self.data_collator = self._predict_data_collator
packing = self.template.packing
padding_free = self.template.padding_free
self.template.packing = False
self.template.padding_free = False
try:
yield
finally:
self.template.packing = packing
self.template.padding_free = padding_free
self.data_collator = origin_data_collator
def evaluate(self, *args, **kwargs):
context = self._patch_predict_with_generate() if self.args.predict_with_generate else nullcontext()
with context:
res = super().evaluate(*args, **kwargs)
gc_collect()
return res
def prediction_step(
self,
model: nn.Module,
inputs: Dict[str, Union[torch.Tensor, Any]],
prediction_loss_only: bool,
ignore_keys: Optional[List[str]] = None,
**gen_kwargs,
) -> Tuple[Optional[float], Optional[torch.Tensor], Optional[torch.Tensor]]:
if not self.args.predict_with_generate or prediction_loss_only:
with self.template.forward_context(self.model, inputs):
return super().prediction_step(
model, inputs, prediction_loss_only=prediction_loss_only, ignore_keys=ignore_keys)
data_list = inputs['_data']
labels_list = [InferRequest.remove_response(data['messages']) for data in data_list]
with unwrap_model_for_generation(
self.model_wrapped, self.accelerator,
gather_deepspeed3_params=self.args.ds3_gather_for_generation), self.template.generate_context():
resp_list = self.infer_engine.infer(
data_list,
RequestConfig(max_tokens=self.model.generation_config.max_new_tokens),
use_tqdm=False,
)
response_list = []
jsonl_cache = []
device = self.args.device
for data, resp, labels in zip(data_list, resp_list, labels_list):
response = resp.choices[0].message.content
jsonl_cache.append({'response': response, 'labels': labels, **data})
response_list.append(Serializer.to_tensor(resp.choices[0].message.content).to(device=device))
self.jsonl_writer.append(jsonl_cache, gather_obj=True)
labels_list = [Serializer.to_tensor(labels).to(device=device) for labels in labels_list]
response_list = pad_sequence(response_list, batch_first=True, padding_value=0)
labels_list = pad_sequence(labels_list, batch_first=True, padding_value=0)
return None, response_list, labels_list
def _prepare_inputs(self, inputs):
args = self.args
inputs = super()._prepare_inputs(inputs)
if self.template.sequence_parallel_size > 1:
sequence_parallel.prepare_inputs(inputs)
use_logits_to_keep = self.get_use_logits_to_keep(self.template.sequence_parallel_size == 1)
if use_logits_to_keep:
self.prepare_logits_to_keep(inputs)
if args.tuner_backend == 'unsloth' and isinstance(inputs['logits_to_keep'], torch.Tensor):
inputs['logits_to_keep'] = int(inputs['logits_to_keep'].sum())
base_model = self.template.get_base_model(self.model)
forward_params = inspect.signature(base_model.forward).parameters
if self.model.model_info.is_moe_model and any(key in forward_params
for key in ['output_router_logits', 'kwargs']):
HfConfigFactory.set_config_attr(base_model.config, 'router_aux_loss_coef', args.router_aux_loss_coef)
base_model.router_aux_loss_coef = args.router_aux_loss_coef
logger.info_once(f'router_aux_loss_coef: {args.router_aux_loss_coef}')
if args.router_aux_loss_coef > 0:
inputs['output_router_logits'] = True
inputs['compute_loss_func'] = self.compute_loss_func
return inputs
def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None):
labels = None
compute_loss_func: Callable = inputs.pop('compute_loss_func', None)
loss_scale = inputs.pop('loss_scale', None)
text_position_ids = inputs.pop('text_position_ids', None)
if text_position_ids is None:
text_position_ids = inputs.get('position_ids')
channels = inputs.pop('channel', None)
if (self.label_smoother is not None or compute_loss_func is not None or loss_scale is not None
or self.args.enable_dft_loss or self.args.enable_channel_loss
or self.template.sequence_parallel_size > 1) and 'labels' in inputs:
if self.args.use_liger_kernel:
logger.warning_once('The cross_entropy loss function defined in Liger Kernel will not '
'take effect, potentially leading to increased GPU memory consumption.')
labels = inputs.pop('labels')
outputs = self.template.compute_sft_loss(model, inputs, num_items_in_batch=num_items_in_batch, trainer=self)
mode = 'train' if self.model.training else 'eval'
if getattr(outputs, 'aux_loss', None) is not None:
self.custom_metrics[mode]['aux_loss'].update(outputs.aux_loss)
# Save past state if it exists
# TODO: this needs to be fixed and made cleaner later.
if hasattr(self.args, 'past_index') and self.args.past_index >= 0:
self._past = outputs[self.args.past_index]
if labels is None:
labels = inputs['labels']
if isinstance(outputs, dict) and 'loss' not in outputs:
raise ValueError(
'The model did not return a loss from the inputs, only the following keys: '
f"{','.join(outputs.keys())}. For reference, the inputs it received are {','.join(inputs.keys())}.")
# We don't use .loss here since the model may return tuples instead of ModelOutput.
loss = outputs['loss'] if isinstance(outputs, dict) else outputs[0]
else:
outputs.loss = None
if (self.args.enable_dft_loss or loss_scale is not None or self.args.enable_channel_loss
or self.template.sequence_parallel_size > 1):
if self.template.sequence_parallel_size > 1:
outputs.loss = per_token_loss_func_sp(outputs, labels, enable_dft_loss=self.args.enable_dft_loss)
else:
outputs.loss = per_token_loss_func(outputs, labels, enable_dft_loss=self.args.enable_dft_loss)
if loss_scale is not None:
loss_scale = torch.roll(loss_scale, shifts=-1, dims=-1).view(-1)
outputs.loss = outputs.loss * loss_scale
if self.args.enable_channel_loss:
metrics = self.custom_metrics[mode]
masks = torch.roll(labels, shifts=-1, dims=-1).view(-1) != -100
if self.template.padding_free:
cu_seqlens = self.get_cu_seqlens(text_position_ids, inputs.get('logits_to_keep'))
else:
cu_seqlens = torch.arange(0, labels.shape[0] + 1) * labels.shape[1]
for i in range(cu_seqlens.shape[0] - 1):
channel = None if channels is None else channels[i]
slice_ = slice(cu_seqlens[i], cu_seqlens[i + 1])
metrics[f'loss_{channel}'].update(outputs.loss[slice_][masks[slice_]])
unwrapped_model = self.accelerator.unwrap_model(model)
if is_peft_available() and isinstance(unwrapped_model, PeftModel):
model_name = unwrapped_model.model._get_name()
else:
model_name = unwrapped_model._get_name()
# User-defined compute_loss function
if compute_loss_func is not None:
loss = compute_loss_func(
outputs, labels, num_items_in_batch=num_items_in_batch, loss_scale=loss_scale, trainer=self)
elif self.label_smoother is None:
# Handle the outputs.loss generated by loss_scale.
if num_items_in_batch is None:
# https://github.com/huggingface/transformers/blob/9dff7ca5c9693f4c02cdd2a9c2abc4772fcea5da/src/transformers/trainer.py#L2137
num_items_in_batch = (labels != -100).sum() # compat SP
if self.template.sequence_parallel_size > 1:
# labels are sharded by SP; outputs.loss was gathered
# to full length via GatherLoss. Reduce the denominator
# across the SP group so it matches the gathered loss.
dist.all_reduce(num_items_in_batch, op=dist.ReduceOp.SUM)
loss = outputs.loss.sum() / num_items_in_batch
else:
if model_name in MODEL_FOR_CAUSAL_LM_MAPPING_NAMES.values():
loss = self.label_smoother(outputs, labels, shift_labels=True)
else:
loss = self.label_smoother(outputs, labels)
if self.model.model_info.is_moe_model and self.args.router_aux_loss_coef is not None:
aux_loss = outputs.get('aux_loss')
if aux_loss is not None:
if num_items_in_batch is not None:
aux_loss = aux_loss * ((labels[:, 1:] != -100).sum() / num_items_in_batch)
loss = loss + self.args.router_aux_loss_coef * aux_loss.to(loss.device)
if getattr(self.args, 'average_tokens_across_devices',
False) and self.model_accepts_loss_kwargs and num_items_in_batch is not None:
loss *= self.accelerator.num_processes
if mode == 'eval' and self.template.sequence_parallel_size > 1:
loss /= self.template.sequence_parallel_size
if (outputs.logits is not None and labels is not None and self.args.tuner_backend != 'unsloth'):
cu_seqlens = None
if self.template.padding_free and self.args.acc_strategy == 'seq':
cu_seqlens = self.get_cu_seqlens(text_position_ids, inputs.get('logits_to_keep'))
# Liger does not have logits
# Unsloth has a bug with output logits
self._compute_acc(outputs, labels, cu_seqlens=cu_seqlens)
return (loss, outputs) if return_outputs else loss
def training_step(self, model, inputs, *args, **kwargs):
with self.template.forward_context(self.model, inputs):
return super().training_step(model, inputs, *args, **kwargs)