Files
wehub-resource-sync 2aaeece67c
Codestyle Check / Lint (push) Has been cancelled
Codestyle Check / Check bypass (push) Has been cancelled
Pipelines-Test / Pipelines-Test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:37:14 +08:00

4303 lines
201 KiB
Python

# Copyright 2020-present the HuggingFace Inc. team.
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This file is modified from
# https://github.com/huggingface/transformers/blob/main/src/transformers/trainer.py
import collections
import contextlib
import inspect
import json
import math
import os
import random
import re
import shutil
import sys
import time
import types
import warnings
from collections import OrderedDict
from collections.abc import Mapping
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
import numpy as np
import paddle
import paddle.amp.auto_cast as autocast
import paddle.distributed as dist
import paddle.nn as nn
import psutil
from packaging import version
from paddle import framework
from paddle.distributed.fleet.meta_parallel import PipelineLayer
try:
from paddle.distributed.fleet.meta_parallel import PipelineDatasetPreprocessor
except:
PipelineDatasetPreprocessor = None
try:
from paddle.base import core
except:
core = None
from paddle.distributed import fleet
from paddle.distributed.fleet.meta_optimizers.dygraph_optimizer.hybrid_parallel_optimizer import (
HybridParallelOptimizer,
)
from paddle.distributed.fleet.meta_parallel.sharding.group_sharded_optimizer_stage2 import (
GroupShardedOptimizerStage2,
)
try:
from paddle.distributed.fleet.utils.hybrid_parallel_util import (
obtain_optimizer_parameters_list,
)
_obtain_optimizer_parameters_list = obtain_optimizer_parameters_list
except:
try:
from paddle.distributed.fleet.meta_optimizers.dygraph_optimizer.hybrid_parallel_optimizer import (
_obtain_optimizer_parameters_list,
)
except:
_obtain_optimizer_parameters_list = None
from paddle.distributed.fleet.utils.hybrid_parallel_util import (
fused_allreduce_gradients,
)
from paddle.io import DataLoader, Dataset, DistributedBatchSampler
from tqdm.auto import tqdm
from ..data import (
DataCollator,
DataCollatorWithPadding,
DistDataLoader,
default_data_collator,
init_dataloader_comm_group,
)
from ..peft import (
DisLoRAModel,
LoKrModel,
LoRAModel,
PrefixModelForCausalLM,
ReFTModel,
VeRAModel,
)
from ..quantization.quantization_linear import (
ColumnParallelQuantizationLinear,
QuantizationLinear,
RowParallelQuantizationLinear,
)
try:
from paddle.distributed.fleet.utils.sequence_parallel_utils import (
register_sequence_parallel_allreduce_hooks,
)
except:
pass
from ..transformers.context_parallel_utils import split_inputs_sequence_dim_load_balance
from ..transformers.model_utils import (
PretrainedModel,
_add_variant,
load_sharded_checkpoint,
unwrap_model,
)
from ..transformers.segment_parallel_utils import split_inputs_sequence_dim
from ..transformers.tokenizer_utils import PretrainedTokenizer
from ..utils.batch_sampler import DistributedBatchSampler as NlpDistributedBatchSampler
from ..utils.env import (
DISLORA_WEIGHTS_NAME,
EMA_STATE_DIC,
LOKR_WEIGHTS_NAME,
LORA_WEIGHTS_NAME,
MASTER_WEIGHT_DIC,
MODEL_META_NAME,
MODEL_STATE_DIC,
OPTIMIZER_STATE_DIC,
PADDLE_MASTER_WEIGHTS_INDEX_NAME,
PADDLE_OPTIMIZER_NAME,
PADDLE_PEFT_WEIGHTS_INDEX_NAME,
PADDLE_WEIGHTS_INDEX_NAME,
PADDLE_WEIGHTS_NAME,
PREFIX_CHECKPOINT_DIR,
PREFIX_HF_CHECKPOINT_DIR,
PREFIX_WEIGHTS_NAME,
SAFE_MASTER_WEIGHTS_INDEX_NAME,
SAFE_PEFT_WEIGHTS_INDEX_NAME,
SAFE_WEIGHTS_INDEX_NAME,
SCALER_NAME,
SCHEDULER_NAME,
TRAINER_STATE_NAME,
TRAINING_ARGS_NAME,
VERA_WEIGHTS_NAME,
)
from ..utils.fault_tolerance import LOSS_INF_ERROR, LOSS_NAN_ERROR
from ..utils.import_utils import is_datasets_available, is_paddle_cuda_available
from ..utils.log import MetricsDumper, logger
from ..utils.pdc_sdk import FLASH_DEVICE
from ..utils.tools import get_env_device
from .argparser import strtobool
from .integrations import get_reporting_integration_callbacks
from .plugins.timer import RuntimeTimer, get_timers, set_timers
from .trainer_callback import (
CallbackHandler,
DefaultFlowCallback,
PrinterCallback,
ProgressCallback,
SPGradSyncCallback,
TrainerCallback,
TrainerControl,
TrainerState,
)
from .trainer_utils import ( # set_hyrbid_parallel_seed,
EvalLoopOutput,
EvalPrediction,
IntervalStrategy,
IterableDatasetShard,
OptimizerNames,
PredictionOutput,
RemoveColumnsCollator,
ShardingOption,
TrainerMemoryTracker,
TrainOutput,
download_recovery_ckpt_from_pdc,
find_batch_size,
get_last_checkpoint,
get_scheduler,
has_length,
init_optimizer,
set_seed,
should_skip_data,
speed_metrics,
split_parallel_config,
)
from .training_args import TrainingArguments
from .unified_checkpoint import UnifiedCheckpointHandler
from .utils import reshard as reshard_util
from .utils.async_save import AsyncSaver
from .utils.reshard import SHARDING_STRATEGY_V1, split_opt_state
from .utils.sharding_io import GroupGetter, to_device
try:
from .utils.zero_cost_checkpoint import (
DistInfoCollectorValidator,
NonZCCEMACallback,
ZeroCostCheckpointCallback,
ZeroCostCheckpointCallbackFcBased,
ZeroCostCheckpointManager,
ZeroCostCheckpointWorker,
ZeroCostCheckpointWorkerFcBased,
)
except (ImportError, ModuleNotFoundError):
ZeroCostCheckpointManager, NonZCCEMACallback = None, None
from .utils.helper import ( # nested_truncate,
broadcast_dataset_rank0_model,
broadcast_dp_optimizer,
broadcast_moe_optimizer,
distributed_concat,
distributed_file,
distributed_isfile,
nested_concat,
nested_detach,
nested_numpify,
nested_truncate,
)
from .utils.sharding_io import ShardingIO
DEFAULT_CALLBACKS = [DefaultFlowCallback]
DEFAULT_PROGRESS_CALLBACK = ProgressCallback
if is_datasets_available():
import datasets
try:
from paddle.distributed.fleet.utils import mix_precision_utils
except:
mix_precision_utils = None
try:
from paddle.io.dataloader.dataloader_iter import _DataLoaderIterBase
except:
from paddle.fluid.dataloader.dataloader_iter import _DataLoaderIterBase
try:
from paddle.distributed import in_auto_parallel_align_mode
except:
def in_auto_parallel_align_mode():
"""
hack for paddlenlp develop branch.
"""
return False
__all__ = ["Trainer"]
class Trainer:
"""
Trainer is a simple but feature-complete training and eval loop for PaddlePaddle, optimized for PaddleNLP.
Args:
model ([`PretrainedModel`] or `paddle.nn.Layer`, *optional*):
The model to train, evaluate or use for predictions.
[`Trainer`] is optimized to work with the [`PretrainedModel`] provided by the library. You can still use
your own models defined as `paddle.nn.Layer` as long as they work the same way as the PaddleNLP
models.
criterion(`paddle.nn.Layer`, *optional*):
The model may only output the loggit, if you want do more computation for the output of model, you can
add the criterion Layer.
args ([`TrainingArguments`], *optional*):
The arguments to tweak for training. Will default to a basic instance of [`TrainingArguments`] with the
`output_dir` set to a directory named *tmp_trainer* in the current directory if not provided.
data_collator (`DataCollator`, *optional*):
The function to use to form a batch from a list of elements of `train_dataset` or `eval_dataset`. Will
default to [`default_data_collator`] if no `tokenizer` is provided, an instance of
[`DataCollatorWithPadding`] otherwise.
train_dataset (`paddle.io.Dataset` or `paddle.io.IterableDataset`, *optional*):
The dataset to use for training. If it is an `datasets.Dataset`, columns not accepted by the
`model.forward()` method are automatically removed.
eval_dataset (Union[`paddle.io.Dataset`, Dict[str, `paddle.io.Dataset`]], *optional*):
The dataset to use for evaluation. If it is a [`~datasets.Dataset`], columns not accepted by the
`model.forward()` method are automatically removed. If it is a dictionary, it will evaluate on each
dataset prepending the dictionary key to the metric name.
tokenizer ([`PretrainedTokenizer`], *optional*):
The tokenizer used to preprocess the data. If provided, will be used to automatically pad the inputs the
maximum length when batching inputs, and it will be saved along the model to make it easier to rerun an
interrupted training or reuse the fine-tuned model.
compute_metrics (`Callable[[EvalPrediction], Dict]`, *optional*):
The function that will be used to compute metrics at evaluation. Must take a [`EvalPrediction`] and return
a dictionary string to metric values.
callbacks (List of [`TrainerCallback`], *optional*):
A list of callbacks to customize the training loop. Will add those to the list of default callbacks.
If you want to remove one of the default callbacks used, use the [`Trainer.remove_callback`] method.
optimizers (`Tuple[paddle.optimizer.Optimizer, paddle.optimizer.lr.LRScheduler]`, *optional*): A tuple
containing the optimizer and the scheduler to use. Will default to an instance of [`AdamW`] on your model
and a scheduler given by [`get_linear_schedule_with_warmup`] controlled by `args`.
preprocess_logits_for_metrics (`Callable[[paddle.Tensor, paddle.Tensor], paddle.Tensor]`, *optional*):
A function that preprocess the logits right before caching them at each evaluation step. Must take two
tensors, the logits and the labels, and return the logits once processed as desired. The modifications made
by this function will be reflected in the predictions received by `compute_metrics`.
Important attributes:
- **model** -- Always points to the core model. If using a transformers model, it will be a [`PretrainedModel`]
subclass.
- **model_wrapped** -- Always points to the most external model in case one or more other modules wrap the
original model. This is the model that should be used for the forward pass. For example, the inner model is
wrapped in `paddle.DataParallel`. If model hasn't been wrapped, then `self.model_wrapped` is the same
as `self.model`.
"""
from .trainer_utils import log_metrics, metrics_format, save_metrics, save_state
def __init__(
self,
model: Union[PretrainedModel, nn.Layer] = None,
criterion: nn.Layer = None,
args: TrainingArguments = None,
data_collator: Optional[DataCollator] = None,
train_dataset: Optional[Dataset] = None,
eval_dataset: Union[Dataset, Dict[str, Dataset]] = None,
tokenizer: Optional[PretrainedTokenizer] = None,
compute_metrics: Optional[Callable[[EvalPrediction], Dict]] = None,
callbacks: Optional[List[TrainerCallback]] = None,
optimizers: Tuple[paddle.optimizer.Optimizer, paddle.optimizer.lr.LRScheduler] = (None, None),
preprocess_logits_for_metrics: Callable[[paddle.Tensor, paddle.Tensor], paddle.Tensor] = None,
):
if args is None:
output_dir = "tmp_trainer"
logger.info(f"No `TrainingArguments` passed, using `output_dir={output_dir}`.")
args = TrainingArguments(output_dir=output_dir)
self.args = args
self.is_in_train = False
# self.do_grad_scaling = args.fp16
# memory metrics - must set up as early as possible
self._memory_tracker = TrainerMemoryTracker(self.args.skip_memory_metrics)
self._memory_tracker.start()
# Seed must be set before instantiating the model when using model
set_seed(seed=self.args.seed)
self._skip_global_steps = 0 # total skip global steps
self._skip_steps_since_last_logged = 0 # skip steps since last logged
if model is None:
logger.warning("Model is None.")
self.model = None
self.train_dataset = train_dataset
self.tokenizer = tokenizer
default_collator = default_data_collator if tokenizer is None else DataCollatorWithPadding(tokenizer)
self.data_collator = data_collator if data_collator is not None else default_collator
return
if self.args.to_static:
model = paddle.jit.to_static(model)
logger.info("Successfully to apply @to_static to the whole model.")
if self.args.should_save or self.args.should_save_model_state:
os.makedirs(self.args.output_dir, exist_ok=True)
self.sharding = None
if len(args.sharding) > 0:
if args.local_rank == -1:
raise ValueError("Using sharding only works in distributed training.")
self.sharding = True
# init parallel env
if paddle.distributed.get_world_size() > 1:
if self.args.use_hybrid_parallel:
self.hcg = fleet.get_hybrid_communicate_group()
self.dp_group = self.hcg.get_data_parallel_group()
self.sharding_group = self.hcg.get_sharding_parallel_group()
default_collator = default_data_collator if tokenizer is None else DataCollatorWithPadding(tokenizer)
self.data_collator = data_collator if data_collator is not None else default_collator
self.train_dataset = train_dataset
self.eval_dataset = eval_dataset
self.tokenizer = tokenizer
if not args.skip_profile_timer:
set_timers()
self.timers = get_timers()
self.runtime_timer = RuntimeTimer("RuntimeTimer")
self.model_wrapped = model
self.model = model
self.criterion = criterion
self.compute_metrics = compute_metrics
self.preprocess_logits_for_metrics = preprocess_logits_for_metrics
self.optimizer, self.lr_scheduler = optimizers
# Label smoothing
# if self.args.label_smoothing_factor != 0:
# self.label_smoother = LabelSmoother(epsilon=self.args.label_smoothing_factor)
# else:
self.label_smoother = None
self.state = TrainerState()
self.control = TrainerControl()
self._signature_columns = None
self.optimizer_grouped_parameters = None
self.sharding_io = None
if self.args.should_save_sharding_stage1_model or self.args.should_load_sharding_stage1_model:
self.sharding_io = ShardingIO(
self.args,
self.model,
self.optimizer,
remap_parameter_name=self.args.load_sharded_model_remap_parameter_name,
is_ema=self.args.sharded_model_from_ema,
)
if (
self.args.save_checkpoint_format == "unified_checkpoint"
or self.args.load_checkpoint_format == "unified_checkpoint"
):
self.unified_checkpoint_handler = UnifiedCheckpointHandler(self.args)
if self.sharding is not None and self.optimizer is not None:
raise RuntimeError(
"Passing `optimizers` is not allowed if sharding is enabled."
"You should subclass `Trainer` and override the `create_optimizer_and_scheduler` method."
)
if self.args.pipeline_parallel_degree > 1 and self.args.use_hybrid_parallel:
assert (isinstance(model, LoRAModel) and isinstance(model.model, PipelineLayer)) or isinstance(
model, PipelineLayer
), "Only support pipeline parallel mode when model is PipelineLayer!!!"
default_callbacks = DEFAULT_CALLBACKS + get_reporting_integration_callbacks(self.args.report_to)
callbacks = default_callbacks if callbacks is None else default_callbacks + callbacks
self.callback_handler = CallbackHandler(
callbacks, self.model, self.tokenizer, self.optimizer, self.lr_scheduler
)
self.add_callback(PrinterCallback if self.args.disable_tqdm else DEFAULT_PROGRESS_CALLBACK)
def _save_ckpt_func(state_dict, path, signal_path=None):
if self.args.enable_auto_parallel:
dist.save_state_dict(state_dict, path)
else:
paddle.save(state_dict, path)
if signal_path is not None:
with open(signal_path, mode="w+") as f:
f.write("1")
self.metrics_dumper = None
if self.args.metrics_output_path is not None:
if not os.path.exists(self.args.metrics_output_path):
os.makedirs(self.args.metrics_output_path, exist_ok=True)
metrics_output_file = os.path.join(self.args.metrics_output_path, f"metrics_rank{dist.get_rank()}.json")
logger.info(f"create/append metrics dumper at {metrics_output_file}")
self.metrics_dumper = MetricsDumper(metrics_output_file)
self._save_ckpt_func = _save_ckpt_func
self._load_ckpt_func = dist.load_state_dict if self.args.enable_auto_parallel else paddle.load
if ZeroCostCheckpointManager is None and self.args.enable_zero_cost_checkpoint:
logger.warning(
"enable_zero_cost_checkpoint has been set as True, but paddle version is too old to support this function, please upgrade it."
)
self.args.enable_zero_cost_checkpoint = False
if self.args.enable_zero_cost_checkpoint:
# Currently, zero cost checkpoint only support pretraining mode with hybrid parallel enabled
assert (
not self.args.ignore_save_lr_and_optim
), "ignore_save_lr_and_optim should be False when using zero cost checkpoint"
assert self.args.use_hybrid_parallel, "use_hybrid_parallel must be True when using zero cost checkpoint"
assert not (
self.args.save_checkpoint_format == "unified_checkpoint"
or self.args.load_checkpoint_format == "unified_checkpoint"
), "use_unified_checkpoint should be False when using zero cost checkpoint"
assert not strtobool(
os.getenv("FLAG_LLM_PDC", "False")
), "Dont support FLAG_LLM_PDC when using zero cost checkpoint"
assert (
self.args.should_save_sharding_stage1_model or self.args.save_checkpoint_format == "flex_checkpoint"
), "should_save_sharding_stage1_model should be True or save_checkpoint_format is flex_checkpoint when using zero cost checkpoint"
assert (
ShardingOption.FULL_SHARD not in self.args.sharding
), "FULL_SHARD is not supported when using flash save mode"
assert not self.args.save_tokenizer, "save_tokenizer is not supported when using flash save mode"
# init attributes for zero cost checkpoint mode
self.zcc_manager = None
if self.args.ordered_save_group_size > 0:
logger.info(f"using save in order, its group size is {self.args.ordered_save_group_size}")
assert not self.args.use_async_save, "Not support async save in ordered save"
assert self.args.tensor_parallel_degree % self.args.ordered_save_group_size == 0
self._save_ckpt_func = self._ordered_save
if self.args.use_async_save:
self._async_optimizer_saver = AsyncSaver()
if args.max_steps > 0:
logger.info("max_steps is given, it will override any value given in num_train_epochs")
if train_dataset is not None and not isinstance(train_dataset, collections.abc.Sized) and args.max_steps <= 0:
raise ValueError("train_dataset does not implement __len__, max_steps has to be specified")
if (
isinstance(self.model, LoRAModel)
or isinstance(self.model, PrefixModelForCausalLM)
or isinstance(self.model, VeRAModel)
or isinstance(self.model, LoKrModel)
or isinstance(self.model, DisLoRAModel)
or isinstance(self.model, ReFTModel)
):
if (
self.args.save_checkpoint_format == "unified_checkpoint"
and "skip_save_model_weight" in self.args.unified_checkpoint_config
):
self.args.unified_checkpoint_config.remove("skip_save_model_weight")
logger.warning(
"We do not support skip_save_model_weight in peft model when using unified checkpoint, remove this config."
)
self.do_grad_scaling = False
self.enable_autocast_context_manager = False
if args.fp16 or args.bf16:
# set do_grad_scaling, enable_autocast_context_manager
self._wrap_amp_model(args, model)
if args.recompute:
def fn(layer):
if hasattr(layer, "enable_recompute") and (
layer.enable_recompute is False or layer.enable_recompute == 0
):
layer.enable_recompute = True
model.apply(fn)
self._pp_data_group = None
if self.args.pipeline_parallel_degree > 1 and self.args.distributed_dataloader:
self._pp_data_group = init_dataloader_comm_group()
default_label_names = (
["start_positions", "end_positions"]
if "QusetionAnswering" in type(self.model).__name__ or "UIE" in type(self.model).__name__
else ["labels"]
)
self.label_names = default_label_names if self.args.label_names is None else self.args.label_names
self.control = self.callback_handler.on_init_end(self.args, self.state, self.control)
self.print_config()
# very last
self._memory_tracker.stop_and_update_metrics()
if self.args.count_trained_tokens:
self.trained_effective_tokens = 0
self.trained_tokens = 0
def _wrap_amp_model(self, args, model):
logger.info("Using half precision")
self.enable_autocast_context_manager = True
self.do_grad_scaling = True if args.fp16 else False
self.amp_dtype = "float16" if args.fp16 else "bfloat16"
# fix for load saved fp16 or bf16 ckpt, decorate model first.
if self.args.fp16_opt_level == "O2":
paddle.amp.decorate(
models=model,
level=self.args.fp16_opt_level,
dtype=self.amp_dtype,
excluded_layers=[QuantizationLinear, ColumnParallelQuantizationLinear, RowParallelQuantizationLinear]
+ self._decorate_exclude_layers(model),
)
# for pipeline mode and pure tensor parallel
if self.args.pipeline_parallel_degree > 1 or (self.args.tensor_parallel_degree > 1 and self.sharding is None):
self.scaler = paddle.amp.GradScaler(init_loss_scaling=self.args.scale_loss)
if self.args.amp_master_grad:
mix_precision_utils.MixPrecisionScaler(self.scaler) # return value has no use
self.scaler = fleet.distributed_scaler(self.scaler)
elif self.sharding is not None:
self.scaler = paddle.amp.GradScaler(init_loss_scaling=self.args.scale_loss)
if self.amp_dtype == "float16" or self.amp_dtype == "bfloat16":
if ShardingOption.SHARD_OP in self.args.sharding:
if self.args.amp_master_grad:
mix_precision_utils.MixPrecisionScaler(self.scaler) # return value has no use
self.scaler = fleet.distributed_scaler(self.scaler)
else:
# scaler for stage2 and stage3
from paddle.distributed.fleet.meta_parallel.sharding.group_sharded_utils import (
GroupShardedScaler,
)
if self.args.amp_master_grad:
mix_precision_utils.MixPrecisionScaler(self.scaler) # return value has no use
self.scaler = GroupShardedScaler(self.scaler)
else:
self.do_grad_scaling = False
self.use_cuda_amp = False
self.amp_dtype = None
else:
self.scaler = paddle.amp.GradScaler(init_loss_scaling=self.args.scale_loss)
def add_callback(self, callback):
"""
Add a callback to the current list of [`~TrainerCallback`].
Args:
callback (`type` or [`~TrainerCallback`]):
A [`~TrainerCallback`] class or an instance of a [`~TrainerCallback`]. In the
first case, will instantiate a member of that class.
"""
self.callback_handler.add_callback(callback)
def pop_callback(self, callback):
"""
Remove a callback from the current list of [`~TrainerCallback`] and returns it.
If the callback is not found, returns `None` (and no error is raised).
Args:
callback (`type` or [`~TrainerCallback`]):
A [`~TrainerCallback`] class or an instance of a [`~TrainerCallback`]. In the
first case, will pop the first member of that class found in the list of callbacks.
Returns:
[`~TrainerCallback`]: The callback removed, if found.
"""
return self.callback_handler.pop_callback(callback)
def remove_callback(self, callback):
"""
Remove a callback from the current list of [`~TrainerCallback`].
Args:
callback (`type` or [`~TrainerCallback`]):
A [`~TrainerCallback`] class or an instance of a [`~TrainerCallback`]. In the
first case, will remove the first member of that class found in the list of callbacks.
"""
self.callback_handler.remove_callback(callback)
def _load_from_peft_checkpoint(self, resume_from_checkpoint=None):
"""load state_dict from checkpoint, Only for PEFT Model.
Args:
resume_from_checkpoint (`str` or `bool`, *optional*):
If a `str`, local path to a saved checkpoint as saved by a previous instance of [`Trainer`]. If a
`bool` and equals `True`, load the last checkpoint in *args.output_dir* as saved by a previous instance
of [`Trainer`]. Only load model state dict.
"""
if resume_from_checkpoint is not None:
convert_tp = False
if isinstance(self.model, LoRAModel):
if self.model.quantized or self.args.pipeline_parallel_degree > 1:
weights_file = os.path.join(
resume_from_checkpoint, _add_variant(LORA_WEIGHTS_NAME, self.args.weight_name_suffix)
)
else:
weights_file = os.path.join(resume_from_checkpoint, LORA_WEIGHTS_NAME)
if self.model.lora_config.tensor_parallel_degree > 1:
convert_tp = True
elif isinstance(self.model, PrefixModelForCausalLM):
weights_file = os.path.join(resume_from_checkpoint, PREFIX_WEIGHTS_NAME)
if self.model.prefix_config.tensor_parallel_degree > 1:
convert_tp = True
elif isinstance(self.model, VeRAModel):
weights_file = os.path.join(resume_from_checkpoint, VERA_WEIGHTS_NAME)
elif isinstance(self.model, LoKrModel):
weights_file = os.path.join(resume_from_checkpoint, LOKR_WEIGHTS_NAME)
elif isinstance(self.model, DisLoRAModel):
weights_file = os.path.join(resume_from_checkpoint, DISLORA_WEIGHTS_NAME)
elif isinstance(self.model, ReFTModel):
self.model.from_pretrained(resume_from_checkpoint, self.model.model)
return
if self.args.dataset_rank == 0:
logger.info(f"Loading model from {resume_from_checkpoint} .")
if os.path.isfile(weights_file):
# We load the model state dict on the CPU to avoid an OOM error.
state_dict = paddle.load(weights_file, return_numpy=True)
if convert_tp:
state_dict = self.model._convert_tensor_parallel(state_dict)
# If the model is on the GPU, it still works!
self._set_state_dict_in_model(state_dict)
# release memory
del state_dict
elif resume_from_checkpoint is not None:
logger.info(f"not loading ckpt :{self.args.dataset_rank}")
def _load_flex_checkpoint(self, resume_from_checkpoint):
def get_metadata_file_name(path):
files = os.listdir(path)
metadata_files = [f for f in files if f.endswith(".metadata")]
assert len(metadata_files) > 0, f"Found no metadata files in {path}"
assert len(metadata_files) == 1, f"Found multiple metadata files in {path}"
return metadata_files[0]
model_sharded_state_dict = self.model.sharded_state_dict()
hf_aoa_config = self.model._gen_aoa_config(self.model.config)
master_weights_path = os.path.join(resume_from_checkpoint, MASTER_WEIGHT_DIC)
opt_states_path = os.path.join(resume_from_checkpoint, OPTIMIZER_STATE_DIC)
model_states_path = os.path.join(resume_from_checkpoint, MODEL_STATE_DIC)
if self.args.load_from_hf:
hcg = dist.fleet.get_hybrid_communicate_group()
assert (
self.args.ignore_load_lr_and_optim
), "Loading from HuggingFace format is only allowed when learning rate and optimizer state are ignored."
try:
moe_sharding_group = hcg.get_moe_sharding_parallel_group()
except Exception:
moe_sharding_group = None
if moe_sharding_group is None or moe_sharding_group.nranks <= 1:
# when moe_sharding_group is None, we use the default process_group
logger.info(f"Loading model weights from '{resume_from_checkpoint}' in safetensors format.")
dist.load_state_dict(
model_sharded_state_dict,
resume_from_checkpoint,
aoa_config=hf_aoa_config,
offload=self.args.load_via_cpu,
safetensors=True,
process_group=None,
comm_method=self.args.flex_ckpt_comm_method,
)
else:
try:
pp_group = hcg.get_pipe_parallel_group()
if pp_group is None or pp_group.nranks < 1:
raise NotImplementedError("Only support when pp_group is not None.")
except Exception:
raise RuntimeError("Only support when pp_group is not None.")
try:
moe_group = hcg.get_expert_parallel_group()
if moe_group is None or moe_group.nranks < 1:
raise NotImplementedError("Only support when moe_group is not None.")
except Exception:
raise RuntimeError("Only support when moe_group is not None.")
moe_sharding_rank = moe_sharding_group.rank
cur_rank = dist.get_rank()
if moe_sharding_rank == 0:
moe_group_ranks = []
dist.all_gather_object(moe_group_ranks, cur_rank, group=moe_group)
pp_group_ranks = []
dist.all_gather_object(pp_group_ranks, moe_group_ranks, group=pp_group)
process_group_ranks = [rank for ranks in pp_group_ranks for rank in ranks]
else:
process_group_ranks = [0] * (pp_group.nranks * moe_group.nranks)
src_rank = hcg.get_moe_sharding_parallel_group_src_rank()
dist.broadcast_object_list(process_group_ranks, src=src_rank, group=moe_sharding_group)
assert any(process_group_ranks), "process_group_ranks should not be all 0"
logger.info(f"Creating a temporary process group with ranks: {process_group_ranks}")
process_group = dist.new_group(process_group_ranks)
if moe_sharding_rank == 0:
logger.info(f"Loading model weights from '{resume_from_checkpoint}' in safetensors format.")
# Only the first moe_sharding process is allowed to load the model weights.
dist.load_state_dict(
model_sharded_state_dict,
resume_from_checkpoint,
aoa_config=hf_aoa_config,
offload=self.args.load_via_cpu,
safetensors=True,
process_group=process_group,
comm_method=self.args.flex_ckpt_comm_method,
)
dist.barrier()
logger.info("Destroying the temporary process group.")
dist.destroy_process_group(process_group)
# The first moe_sharding group loads the model weights and then broadcasts them to all other moe_sharding groups.
logger.info(
"First shard (moe_sharding_group) has loaded safetensors weights, starting broadcast on moe_sharding_groups."
)
for param_name, param in self.model.state_dict().items():
dist.broadcast(param, src=src_rank, group=moe_sharding_group)
logger.info("Safetensors format weights have been loaded successfully.")
return
if not self.args.ignore_load_lr_and_optim:
state_dict_metadata = {}
metadata_paths = [
os.path.join(model_states_path, get_metadata_file_name(model_states_path)),
os.path.join(opt_states_path, get_metadata_file_name(opt_states_path)),
os.path.join(master_weights_path, get_metadata_file_name(master_weights_path)),
]
for metadata_file in metadata_paths:
if not os.path.exists(metadata_file):
raise FileNotFoundError(f"Metadata file not found: {metadata_file}")
metadata = paddle.load(metadata_file)
state_dict_metadata.update(metadata.state_dict_metadata)
if not self.args.sharded_model_from_ema:
init_optimizer(self.optimizer, model_sharded_state_dict, state_dict_metadata)
optimizer_sharded_state_dict = self.optimizer.sharded_state_dict(model_sharded_state_dict)
opt_states = {}
master_weights = {}
for k, v in optimizer_sharded_state_dict.items():
if k.endswith(".w_0"):
master_weights[k] = v
else:
opt_states[k] = v
dist.load_state_dict(
opt_states,
opt_states_path,
aoa_config=self.args.aoa_config,
offload=self.args.load_via_cpu,
comm_method=self.args.flex_ckpt_comm_method,
)
dist.load_state_dict(
master_weights,
master_weights_path,
aoa_config=self.args.aoa_config,
offload=self.args.load_via_cpu,
comm_method=self.args.flex_ckpt_comm_method,
)
self._load_scheduler(resume_from_checkpoint)
if self.args.sharded_model_from_ema:
ema_states_path = os.path.join(resume_from_checkpoint, EMA_STATE_DIC, f"{dist.get_rank()}_0.distcp")
ema_state_dict = paddle.load(ema_states_path)
ema_master_weights = ema_state_dict.pop("master_weights", None)
opt_state_dict = {"master_weights": ema_master_weights}
self.optimizer.set_state_dict(opt_state_dict)
self.model.set_state_dict(ema_state_dict)
else:
def bf16_filtered_sharded_state_dict(sharded_state_dict):
new_state_dict = {}
for k, v in sharded_state_dict.items():
if v.local_tensor.dtype == paddle.bfloat16:
continue
new_state_dict[k] = v
return new_state_dict
fp32_sharded_state_dict = bf16_filtered_sharded_state_dict(model_sharded_state_dict)
dist.load_state_dict(
fp32_sharded_state_dict,
model_states_path,
aoa_config=self.args.aoa_config,
offload=self.args.load_via_cpu,
comm_method=self.args.flex_ckpt_comm_method,
)
if self.args.bf16 and (not self.args.ignore_load_lr_and_optim):
opt_state_dict = self.optimizer.state_dict()
def recover_params_from_master_weight(opt_state_dict, group):
master_weights = opt_state_dict["master_weights"]
tmp = OrderedDict()
(master_weights, tmp) = (tmp, master_weights)
# cast to before
for (k, v) in tmp.items():
name = v.name
master_weights[k] = paddle.cast(to_device(v), paddle.bfloat16).cpu()
master_weights[k].name = name
structure_name_map = {k: v.name for (k, v) in self.model.state_dict().items()}
node_model_state = reshard_util.NodeModelState(group=group)
node_model_state_tmp = reshard_util.NodeModelState(group=group)
node_model_state_tmp.add_master_weights(master_weights)
node_model_state_tmp.pack_keys(structure_name_map)
node_model_state.merge_from(node_model_state_tmp, max(group.rank, 0))
del node_model_state_tmp
sharding_strategy = reshard_util.get_sharding_strategy(self.optimizer)
logger.debug(f"sharding_strategy: {sharding_strategy}")
restore_func = (
reshard_util.sharding_v1.restore
if sharding_strategy == SHARDING_STRATEGY_V1
else reshard_util.sharding_v2.restore
)
node_model_state = restore_func(node_model_state, self.model, self.optimizer)
node_model_state.unpack_keys()
master_weights = node_model_state.master_weights
master_weights = reshard_util.all_gather_state_dict(master_weights, lambda x: True, group)
model_state_dict = self.model.state_dict()
for key, param in model_state_dict.items():
if param.name in master_weights and param.dtype == paddle.bfloat16:
logger.debug(
f"key {key}, convert master weights {param.name} shape {master_weights[param.name].shape} to param {param.name} shape{param.shape}"
)
assert (
param.shape == master_weights[param.name].shape
), f"got {param.shape} vs {master_weights[param.name].shape}"
master_weight = paddle.reshape(master_weights[param.name], param.shape)
paddle.assign(paddle.cast(to_device(master_weight), paddle.bfloat16), model_state_dict[key])
group_getter = GroupGetter(self.model)
opt_state_dict = split_opt_state(opt_state_dict, group_getter)
for gid in group_getter.get_group_ids():
sub_opt_state_dict = opt_state_dict[gid]
group = group_getter.get_group_by_id(gid)
if self.args.bf16:
recover_params_from_master_weight(sub_opt_state_dict, group)
def _save_flex_model_state(self, output_dir):
model_sharded_state_dict = self.model.sharded_state_dict()
model_state_dict_path = os.path.join(output_dir, MODEL_STATE_DIC)
os.makedirs(model_state_dict_path, exist_ok=True)
dist.save_state_dict(
model_sharded_state_dict,
model_state_dict_path,
)
def _save_flex_optimizer_state(self, output_dir):
optimizer_state_dict_path = os.path.join(output_dir, OPTIMIZER_STATE_DIC)
optimizer_states = {}
master_weights = {}
model_sharded_state_dict = self.model.sharded_state_dict()
optimizer_sharded_state_dict = self.optimizer.sharded_state_dict(model_sharded_state_dict)
for k, v in optimizer_sharded_state_dict.items():
if k.endswith(".w_0"):
master_weights[k] = v
else:
optimizer_states[k] = v
dist.save_state_dict(
optimizer_states,
optimizer_state_dict_path,
)
master_weights_path = os.path.join(output_dir, MASTER_WEIGHT_DIC)
dist.save_state_dict(
master_weights,
master_weights_path,
)
saved_signal_path = os.path.join(output_dir, f"saved_signal_{dist.get_rank()}")
with open(saved_signal_path, mode="w+") as f:
f.write("1")
def _load_from_checkpoint(self, resume_from_checkpoint=None):
"""load state_dict from_checkpoint, Only load model state dict.
Args:
resume_from_checkpoint (`str` or `bool`, *optional*):
If a `str`, local path to a saved checkpoint as saved by a previous instance of [`Trainer`]. If a
`bool` and equals `True`, load the last checkpoint in *args.output_dir* as saved by a previous instance
of [`Trainer`]. Only load model state dict.
"""
self.runtime_timer.start("checkpoint loading time")
resume_from_checkpoint = None if not resume_from_checkpoint else resume_from_checkpoint
# Load potential model checkpoint
if isinstance(resume_from_checkpoint, bool) and resume_from_checkpoint:
uc_async_save = (
self.args.load_checkpoint_format == "unified_checkpoint"
and "async_save" in self.args.unified_checkpoint_config
)
resume_from_checkpoint = get_last_checkpoint(
self.args.output_dir, signal_folder=self.args.output_signal_dir, uc_async_save=uc_async_save
)
if resume_from_checkpoint is None:
raise ValueError(f"No valid checkpoint found in output directory ({self.args.output_dir})")
if self.args.load_checkpoint_format == "unified_checkpoint":
if resume_from_checkpoint is not None:
use_unified_checkpoint = False
if self.is_unified_checkpoint(resume_from_checkpoint):
use_unified_checkpoint = True
else:
logger.info("Loading origin checkpoint, the next checkpoint will be saved as unified checkpoint")
if use_unified_checkpoint:
self.unified_checkpoint_handler.load_unified_checkpoint(
self.model,
resume_from_checkpoint,
)
if isinstance(self.model, LoRAModel) and self.model.lora_config.loraga:
self.model.reinit_base_model = True
logger.info(f"Loading model from {resume_from_checkpoint} using unified checkpoint.")
self.runtime_timer.stop()
return
if (
isinstance(self.model, LoRAModel)
or isinstance(self.model, PrefixModelForCausalLM)
or isinstance(self.model, VeRAModel)
or isinstance(self.model, LoKrModel)
or isinstance(self.model, DisLoRAModel)
or isinstance(self.model, ReFTModel)
):
self._load_from_peft_checkpoint(resume_from_checkpoint)
if isinstance(self.model, LoRAModel) and self.model.lora_config.loraga:
self.model.reinit_base_model = True
self.runtime_timer.stop()
return
weight_name = PADDLE_WEIGHTS_NAME
weight_index_name = PADDLE_WEIGHTS_INDEX_NAME # currently set paddle as default, do not support safetensors.
if self.args.should_load_sharding_stage1_model:
state_dict = self.sharding_io.load_state_dict_from_checkpoint_with_reshard(
resume_from_checkpoint,
base_weight_name=weight_name,
model_wrapped=self.model_wrapped,
)
old_state_dict = self.model.state_dict()
new_state_dict = {}
for k, v in state_dict.items():
if k not in old_state_dict or id(v) != id(old_state_dict[k]):
new_state_dict[k] = v
self.model.set_state_dict(new_state_dict)
else:
if resume_from_checkpoint is not None and (self.args.dataset_rank == 0 or self.args.use_expert_parallel):
weights_file = os.path.join(
resume_from_checkpoint, _add_variant(weight_name, self.args.weight_name_suffix)
)
weights_index_file = os.path.join(
resume_from_checkpoint, _add_variant(weight_index_name, self.args.weight_name_suffix)
)
if not any(
os.path.isfile(f)
for f in [
weights_file,
weights_index_file,
]
):
raise ValueError(f"Can't find a valid checkpoint at {resume_from_checkpoint} -- {weights_file}")
logger.info(f"Loading model from {resume_from_checkpoint} .")
if os.path.isfile(weights_file):
# We load the model state dict on the CPU to avoid an OOM error.
state_dict = paddle.load(weights_file, return_numpy=True)
# If the model is on the GPU, it still works!
self._set_state_dict_in_model(state_dict)
# release memory
del state_dict
else:
# We load the sharded checkpoint.
missing_keys, unexpected_keys = load_sharded_checkpoint(
self.model, resume_from_checkpoint, self.args.weight_name_suffix, prefer_safe=False
)
logger.info(f"set state_dict: {missing_keys, unexpected_keys}")
elif resume_from_checkpoint is not None:
logger.info(f"not loading ckpt :{self.args.dataset_rank}")
self.runtime_timer.stop()
def _wrap_model_and_load_sharded_checkpoint(self, resume_from_checkpoint):
# In the sharded mode, should invoke _load_from_checkpoint after _wrap_model.
# In this mode, each sharding rank load sharded params, do not need to implement the broadcast logic.
model = self._wrap_model(self.model_wrapped)
if self.sharding_io is not None:
# the self.optimizer should be wrapped and it is done in _wrap_model
self.sharding_io.set_optimizer(self.optimizer)
if model is not self.model:
self.model_wrapped = model
# Should invoke _load_from_checpoint after _load_optimizer_and_scheduler
# because the _load_from_checkpoint method rely on the optimizer in the shareded mode.
if resume_from_checkpoint:
self._load_optimizer_and_scheduler(resume_from_checkpoint)
self._load_from_checkpoint(resume_from_checkpoint)
return model
def _get_zcc_implementation_classes(self):
"""Get appropriate ZCC implementation classes based on checkpoint format."""
if self.args.save_checkpoint_format == "flex_checkpoint":
return ZeroCostCheckpointCallbackFcBased, ZeroCostCheckpointWorkerFcBased
return ZeroCostCheckpointCallback, ZeroCostCheckpointWorker
def _create_zcc_manager_instance(self, unwrapped_model, zcc_worker_class):
"""Create ZCC manager instance with appropriate configuration."""
if isinstance(self.model, PipelineLayer):
pipeline_hooks_capacity = (
unwrapped_model.forward_pipeline_parallel_hook_capacity
+ unwrapped_model.backward_pipeline_parallel_hook_capacity
)
else:
pipeline_hooks_capacity = self.args.gradient_accumulation_steps
return ZeroCostCheckpointManager(
worker_num=self.args.zcc_workers_num,
pipeline_hooks_capacity=pipeline_hooks_capacity,
capacity_usage=self.args.zcc_pipeline_hooks_capacity_usage,
use_expert_parallel=self.args.use_expert_parallel,
ema_coef=self.args.zcc_save_ema_coef,
zcc_worker_class=zcc_worker_class,
)
def _register_pipeline_hooks(self, unwrapped_model):
"""Register forward and backward pipeline hooks."""
# Register forward hooks
for i in range(unwrapped_model.forward_pipeline_parallel_hook_capacity):
unwrapped_model.register_forward_pipeline_parallel_hook(
location=i, hook=self.zcc_manager.zcc_pipeline_hook
)
# Register backward hooks
for i in range(unwrapped_model.backward_pipeline_parallel_hook_capacity):
unwrapped_model.register_backward_pipeline_parallel_hook(
location=i, hook=self.zcc_manager.zcc_pipeline_hook
)
def _setup_zcc_callback(self, zcc_callback_class):
"""Setup ZCC callback with required dependencies."""
callback = zcc_callback_class(self.args, self.zcc_manager, self.runtime_timer, self.sharding_io)
self.add_callback(callback)
def _handle_checkpoint_resume(self, resume_from_checkpoint):
"""Handle resumption from previous checkpoint if provided."""
if resume_from_checkpoint is None:
return
ema_state_path = self._get_ema_state_path(resume_from_checkpoint)
if not os.path.exists(ema_state_path):
logger.info(f"ZCC EMA state dict not found at: {ema_state_path}")
return
# Validate distributed strategy compatibility
should_load_ema = self._should_load_ema_state(resume_from_checkpoint, ema_state_path)
if should_load_ema:
logger.info(f"Loading ZCC EMA state from: {ema_state_path}")
self.zcc_manager.set_ema_state_dict(ema_state_path)
def _get_ema_state_path(self, checkpoint_path):
"""Get the path to EMA state based on checkpoint format."""
if self.args.save_checkpoint_format == "flex_checkpoint":
return os.path.join(checkpoint_path, "ema_state", f"{dist.get_rank()}_0.distcp")
else:
optimizer_name = _add_variant(PADDLE_OPTIMIZER_NAME, self.args.optimizer_name_suffix)
return os.path.join(checkpoint_path, optimizer_name).replace("optimizer", "ema")
def _should_load_ema_state(self, checkpoint_path, ema_state_path):
"""Determine if EMA state should be loaded based on configuration and compatibility."""
if self.args.zcc_save_ema_coef is None:
logger.info("EMA coefficient is None, skipping EMA state loading")
return False
success, err_msg = DistInfoCollectorValidator(self.args, self.hcg).check_same_strategy(checkpoint_path)
if not success:
logger.warning(f"Cannot load EMA state due to strategy mismatch: {err_msg}")
return False
return True
def create_zcc_manager(self, unwrapped_model, resume_from_checkpoint=None):
"""
Create zero cost checkpoint manager.
Has to be called after pipeline model is created.
resume_from_checkpoint: if use Flash checkpoint EMA, load previous checkpoint status
"""
assert isinstance(
self.model, PretrainedModel
), "model should be a PretrainedModel when using zero cost checkpoint"
logger.info("Create zero cost checkpoint manager...")
zcc_callback_class, zcc_worker_class = self._get_zcc_implementation_classes()
# Create ZCC manager with appropriate configuration
self.zcc_manager = self._create_zcc_manager_instance(unwrapped_model, zcc_worker_class)
# Register pipeline hooks if using pipeline parallelism
if isinstance(self.model, PipelineLayer):
self._register_pipeline_hooks(unwrapped_model)
# Add callback and handle checkpoint resumption
self._setup_zcc_callback(zcc_callback_class)
self._handle_checkpoint_resume(resume_from_checkpoint)
logger.info("Zero cost checkpoint manager created successfully.")
def add_non_zcc_ema_callback(self, resume_from_checkpoint):
non_zcc_ema_callback = NonZCCEMACallback.create_nonzcc_callback(
args=self.args,
resume_from_checkpoint=resume_from_checkpoint,
sharding_io=self.sharding_io,
model=self.model,
optimizer=self.optimizer,
hcg=self.hcg,
)
self.add_callback(non_zcc_ema_callback)
def train(
self,
resume_from_checkpoint: Optional[Union[str, bool]] = None,
ignore_keys_for_eval: Optional[List[str]] = None,
):
"""
Main training entry point.
Args:
resume_from_checkpoint (`str` or `bool`, *optional*):
If a `str`, local path to a saved checkpoint as saved by a previous instance of [`Trainer`]. If a
`bool` and equals `True`, load the last checkpoint in *args.output_dir* as saved by a previous instance
of [`Trainer`]. If present, training will resume from the model/optimizer/scheduler states loaded here.
ignore_keys_for_eval (`List[str]`, *optional*)
A list of keys in the output of your model (if it is a dictionary) that should be ignored when
gathering predictions for evaluation during the training.
"""
args = self.args
self.is_in_train = True
logger.info(f"Starting training from resume_from_checkpoint : {resume_from_checkpoint}")
# The resume_from_checkpoint could be None in some machine node.
# Here we reset None to temp directory.
if args.world_size > 1:
is_resume_from_checkpoint = paddle.to_tensor([resume_from_checkpoint is not None], dtype="int32")
paddle.distributed.all_reduce(is_resume_from_checkpoint)
is_resume_from_checkpoint = is_resume_from_checkpoint.item()
if is_resume_from_checkpoint > 0 and is_resume_from_checkpoint < paddle.distributed.get_world_size():
if resume_from_checkpoint is None:
resume_from_checkpoint = os.path.join(self.args.output_dir, "local_tempdir")
if os.path.exists(resume_from_checkpoint) and self.args.local_rank == 0:
shutil.rmtree(resume_from_checkpoint)
os.makedirs(resume_from_checkpoint, exist_ok=True)
logger.info(f"Reset resume_from_checkpoint to temp directory : {resume_from_checkpoint}")
if (
resume_from_checkpoint is not None
and self.args.pdc_download_ckpt
and FLASH_DEVICE not in resume_from_checkpoint
):
if self.is_local_process_zero():
download_recovery_ckpt_from_pdc(resume_from_checkpoint, self.args.pdc_download_timeout)
if self.args.world_size > 1:
logger.info("Wait all processes finish downloading...")
paddle.distributed.barrier()
logger.info("All processes finished downloading from pdc")
train_dataloader = self.get_train_dataloader()
total_train_batch_size = args.train_batch_size * args.gradient_accumulation_steps * args.dataset_world_size
len_dataloader = None
if has_length(train_dataloader):
len_dataloader = len(train_dataloader)
num_update_steps_per_epoch = len(train_dataloader) // args.gradient_accumulation_steps
num_update_steps_per_epoch = max(num_update_steps_per_epoch, 1)
num_examples = len(self.train_dataset)
if args.max_steps > 0:
max_steps = args.max_steps
num_train_epochs = args.max_steps // num_update_steps_per_epoch + int(
args.max_steps % num_update_steps_per_epoch > 0
)
num_train_samples = args.max_steps * total_train_batch_size
else:
max_steps = int(num_update_steps_per_epoch * args.num_train_epochs)
num_train_epochs = math.ceil(args.num_train_epochs)
num_train_samples = int(len(self.train_dataset) * args.num_train_epochs)
if args.minimum_eval_times is not None and args.minimum_eval_times > 0:
if max_steps // args.eval_steps < args.minimum_eval_times:
exp_step = max_steps / args.minimum_eval_times
exp_step = max(int(exp_step - exp_step % 10), 10)
logger.info("Reset eval step by minimum_eval_times to %d" % exp_step)
args.eval_steps = exp_step
elif args.max_steps > 0: # Rely on max_steps when dataloader does not have a working size
max_steps = args.max_steps
# Setting a very large number of epochs so we go as many times as necessary over the iterator.
num_train_epochs = sys.maxsize
num_update_steps_per_epoch = max_steps
num_examples = total_train_batch_size * args.max_steps
num_train_samples = args.max_steps * total_train_batch_size
else:
raise ValueError(
f"args.max_steps must be set to a positive value if dataloader does not have a length, was {args.max_steps}"
)
# delay_optimizer_creation = (
# self.sharding is not None
# and ShardingOption.SHARD_OP in self.args.sharding
# )
delay_optimizer_creation = False
if not delay_optimizer_creation:
self.create_optimizer_and_scheduler(num_training_steps=max_steps)
self.state = TrainerState()
# memory metrics - must set up as early as possible
self._memory_tracker.start()
if not self.args.enable_auto_parallel:
if (
not self.args.should_load_sharding_stage1_model
and not self.args.load_checkpoint_format == "flex_checkpoint"
):
self._load_from_checkpoint(resume_from_checkpoint)
if self.args.should_load_sharding_stage1_model:
model = self._wrap_model_and_load_sharded_checkpoint(resume_from_checkpoint)
elif self.args.should_save_sharding_stage1_model and not (
self.args.load_checkpoint_format == "flex_checkpoint"
):
# In the non-sharded mode, should invoke _load_from_checkpoint before _wrap_model.
# In this mode, the rank0 load all params and the _wrap_model implicitly broadcast params from rank0 to the other ranks.
model = self._wrap_model(self.model_wrapped)
if self.sharding_io is not None:
assert delay_optimizer_creation is False, "delay_optimizer_creation should be False"
# the self.optimizer should be wrapped and it is done in _wrap_model
self.sharding_io.set_optimizer(self.optimizer)
# for the rest of this function `model` is the outside model, whether it was wrapped or not
if model is not self.model:
self.model_wrapped = model
if delay_optimizer_creation:
self.create_optimizer_and_scheduler(num_training_steps=max_steps)
self._load_optimizer_and_scheduler(resume_from_checkpoint)
elif self.args.load_checkpoint_format == "flex_checkpoint":
model = self._wrap_model(self.model_wrapped)
if model is not self.model:
self.model_wrapped = model
if delay_optimizer_creation:
self.create_optimizer_and_scheduler(num_training_steps=max_steps)
self._load_flex_checkpoint(resume_from_checkpoint)
else:
model = self._wrap_model(self.model_wrapped)
# for the rest of this function `model` is the outside model, whether it was wrapped or not
if model is not self.model:
self.model_wrapped = model
if delay_optimizer_creation:
self.create_optimizer_and_scheduler(num_training_steps=max_steps)
self._load_optimizer_and_scheduler(resume_from_checkpoint)
else:
model = self.model_wrapped
if delay_optimizer_creation:
self.create_optimizer_and_scheduler(num_training_steps=max_steps)
if self.args.enable_zero_cost_checkpoint:
self.create_zcc_manager(model, resume_from_checkpoint)
elif self.args.zcc_save_ema_coef is not None:
self.add_non_zcc_ema_callback(resume_from_checkpoint)
logger.info(f"{self.runtime_timer.log()}")
logger.info("***** Running training *****")
logger.info(f" Num examples = {num_examples:,}")
logger.info(f" Num Epochs = {num_train_epochs}")
logger.info(f" Instantaneous batch size per device = {args.per_device_train_batch_size}")
logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_train_batch_size}")
logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}")
logger.info(f" Total optimization steps = {max_steps:,}")
logger.info(f" Total num train samples = {num_train_samples:,}")
# per_device_trainable_numel = sum(p.numel().item() for p in model.parameters() if not p.stop_gradient)
# TODO: Temporary fix since Tensor.numel() not supported in distributed mode
if self.args.enable_auto_parallel:
per_device_trainable_numel = 0
for p in model.parameters():
if not p.stop_gradient:
per_device_trainable_numel += np.prod(p._local_shape) if p.is_dist() else np.prod(p.shape)
else:
per_device_trainable_numel = sum(np.prod(p.shape) for p in model.parameters() if not p.stop_gradient)
logger.debug(f" Number of trainable parameters = {per_device_trainable_numel:,} (per device)")
if self.args.use_hybrid_parallel:
# todo fix for pipeline_parallel_degree
parts_num = max(self.args.tensor_parallel_degree, 1) * max(self.args.pipeline_parallel_degree, 1)
if parts_num > 1:
all_reduce_dtype = "int64"
if paddle.get_device().split(":")[0] in ["npu", "xpu"]:
# TODO(duanyanhui): fix when NPU all_reduce supports int64
all_reduce_dtype = "float32"
trainable_numel_tensor = paddle.to_tensor(per_device_trainable_numel, dtype=all_reduce_dtype)
paddle.distributed.all_reduce(trainable_numel_tensor)
trainable_numel = int(trainable_numel_tensor.item()) // self.args.dataset_world_size
if self.args.sep_parallel_degree > 0:
trainable_numel = trainable_numel // self.args.sep_parallel_degree
if self.args.context_parallel_degree > 0:
trainable_numel = trainable_numel // self.args.context_parallel_degree
# the numel is roughly, because the tensor parallel still hold own bias or layer_norm weight without splited
# so, the trainable numel is a little bigger than real.
logger.debug(f" Number of trainable parameters = {trainable_numel:,} (all devices, roughly)")
return self._inner_training_loop(
args,
model,
train_dataloader,
len_dataloader,
max_steps,
num_train_epochs,
num_update_steps_per_epoch,
num_train_samples,
resume_from_checkpoint,
ignore_keys_for_eval,
)
def _inner_training_loop(
self,
args,
model,
train_dataloader,
len_dataloader,
max_steps,
num_train_epochs,
num_update_steps_per_epoch,
num_train_samples,
resume_from_checkpoint,
ignore_keys_for_eval,
):
start_time = time.time()
self._globalstep_last_start_time = time.time()
self.state.epoch = 0
epochs_trained = 0
steps_trained_in_current_epoch = 0
steps_trained_progress_bar = None
# Check if continuing training from a checkpoint
if (
resume_from_checkpoint is not None
and distributed_isfile(os.path.join(resume_from_checkpoint, TRAINER_STATE_NAME))
and not self.args.ignore_load_lr_and_optim
):
self.state = TrainerState.load_from_json(
distributed_file(os.path.join(resume_from_checkpoint, TRAINER_STATE_NAME))
)
if self.args.world_size > 1:
global_step_list = []
paddle.distributed.all_gather(
global_step_list, paddle.to_tensor([self.state.global_step], dtype="int64")
)
assert (
paddle.sum(paddle.stack(global_step_list) - global_step_list[0]) == 0
), f"Error, get different global step, please check! step list: {[x.item() for x in global_step_list]}"
epochs_trained = self.state.global_step // num_update_steps_per_epoch
if not args.ignore_data_skip:
steps_trained_in_current_epoch = self.state.global_step % (num_update_steps_per_epoch)
steps_trained_in_current_epoch *= args.gradient_accumulation_steps
else:
steps_trained_in_current_epoch = 0
logger.info(" Continuing training from checkpoint, will skip to saved global_step")
logger.info(f" Continuing training from epoch {epochs_trained}")
logger.info(f" Continuing training from global step {self.state.global_step}")
if not args.ignore_data_skip:
logger.info(
f" Will skip the first {epochs_trained} epochs then the first {steps_trained_in_current_epoch} "
"batches in the first epoch. If this takes a lot of time, you can add the `--ignore_data_skip` "
"flag to your launch command, but you will resume the training on data already seen by your model."
)
if self.is_local_process_zero() and not args.disable_tqdm:
steps_trained_progress_bar = tqdm(total=steps_trained_in_current_epoch)
steps_trained_progress_bar.set_description("Skipping the first batches")
if not args.ignore_data_skip:
if isinstance(train_dataloader, paddle.io.DataLoader) and isinstance(
train_dataloader.batch_sampler, NlpDistributedBatchSampler
):
consumed_samples = (
self.state.global_step
* args.train_batch_size
* args.gradient_accumulation_steps
* args.dataset_world_size
)
train_dataloader.batch_sampler.set_epoch(consumed_samples=consumed_samples)
logger.info(f"Set DistributedBatchSampler consumed_samples to {consumed_samples}")
epoch_iterator = train_dataloader
# steps_in_epoch = len(epoch_iterator)
steps_in_epoch = (
len(epoch_iterator) if len_dataloader is not None else args.max_steps * args.gradient_accumulation_steps
)
if len_dataloader is not None:
if self.args.gradient_accumulation_steps > len(epoch_iterator):
logger.warning(
f"changing accumulation step from `{self.args.gradient_accumulation_steps}` to `{len(epoch_iterator)}` to avoid, cross epoch accumulate"
)
self.args.gradient_accumulation_steps = len(epoch_iterator)
self.callback_handler.model = self.model
self.callback_handler.optimizer = self.optimizer
self.callback_handler.lr_scheduler = self.lr_scheduler
self.callback_handler.train_dataloader = train_dataloader
self.state.max_steps = int(max_steps)
self.state.num_train_epochs = num_train_epochs
self.state.is_local_process_zero = self.is_local_process_zero()
self.state.is_world_process_zero = self.is_world_process_zero()
self.state.consumed_samples = 0
self.control = self.callback_handler.on_train_begin(args, self.state, self.control)
tr_loss = paddle.to_tensor(0.0)
self._total_loss_scalar = 0.0
self._globalstep_last_logged = self.state.global_step
if self.args.device == "npu" and self.args.flatten_param_grads:
from .plugins.npu_plugin import npu_accelerate_plugin
npu_accelerate_plugin(self.optimizer)
if self.args.ignore_data_skip:
self.timers and self.timers("read-data").start()
for epoch in range(epochs_trained, num_train_epochs):
if isinstance(train_dataloader, paddle.io.DataLoader) and isinstance(
train_dataloader.batch_sampler, DistributedBatchSampler
):
train_dataloader.batch_sampler.set_epoch(epoch)
step_control = 0 # used in loop control, reset to 0 after every step
self.control = self.callback_handler.on_epoch_begin(args, self.state, self.control)
step = -1
for step, inputs in enumerate(epoch_iterator):
if (
self.args.use_hybrid_parallel
and self.args.sep_parallel_degree > 1
and self.args.split_inputs_sequence_dim
):
inputs = split_inputs_sequence_dim(inputs)
if (
self.args.use_hybrid_parallel
and self.args.context_parallel_degree > 1
and self.args.split_inputs_sequence_dim
):
inputs = split_inputs_sequence_dim_load_balance(inputs)
if self.args.ignore_data_skip:
self.timers and self.timers("read-data").stop()
os.environ["TRAINER_GLOBAL_STEP"] = str(self.state.global_step)
self.callback_handler.on_load_data_end(args, self.state, self.control, inputs=inputs)
# Skip past any already trained steps if resuming training
# for paddlenlp.utils.batch_sampler.DistributedBatchSampler
# We use consumed_samples to reset the status
if isinstance(train_dataloader, paddle.io.DataLoader) and isinstance(
train_dataloader.batch_sampler, NlpDistributedBatchSampler
):
if step == 0:
if steps_trained_progress_bar is not None:
steps_trained_progress_bar.update(steps_trained_in_current_epoch)
steps_trained_progress_bar.close()
steps_trained_progress_bar = None
self._load_rng_state(resume_from_checkpoint)
step += steps_trained_in_current_epoch
elif steps_trained_in_current_epoch > 0:
steps_trained_in_current_epoch -= 1
if steps_trained_progress_bar is not None:
steps_trained_progress_bar.update(1)
if steps_trained_in_current_epoch == 0:
self._load_rng_state(resume_from_checkpoint)
self.timers and self.timers("read-data").start()
continue
elif steps_trained_progress_bar is not None:
steps_trained_progress_bar.close()
steps_trained_progress_bar = None
if should_skip_data(self.state.global_step, self.args.skip_data_intervals):
# skip this step
if (step_control + 1) % self.args.gradient_accumulation_steps == 0 or (
# last step in epoch but step is always smaller than gradient_accumulation_steps
steps_in_epoch <= args.gradient_accumulation_steps
and (step + 1) == steps_in_epoch
):
# update current global step and skip step
self.state.global_step += 1
self._skip_global_steps += 1
self._skip_steps_since_last_logged += 1
self.state.epoch = epoch + (step + 1) / steps_in_epoch
self.state.consumed_samples = (
self.state.global_step
* args.per_device_train_batch_size
* args.gradient_accumulation_steps
* args.dataset_world_size
)
if self.state.global_step == 1 and self.args.logging_first_step:
self.control.should_log = True
if (
self.args.logging_strategy == IntervalStrategy.STEPS
and self.state.global_step % self.args.logging_steps == 0
):
self.control.should_log = True
self.control.should_evaluate = False
self.control.should_save = False
# log loss and memory usage
self._maybe_log_save_evaluate(tr_loss, model, epoch, ignore_keys_for_eval, inputs=inputs)
self._print_timer()
step_control = 0
else:
step_control += 1
if self.state.global_step >= self.state.max_steps:
break
self.timers and self.timers("read-data").start()
continue
if step_control % args.gradient_accumulation_steps == 0:
self.control = self.callback_handler.on_step_begin(args, self.state, self.control)
self.timers and self.timers("forward-backward").start()
# stage2 and stage3 should not no_sync, because the is no DDP wrapper and no_sync API
# hybrid_parallel (tp or pp or sharding stage 1) should not no_sync
available_no_sync = hasattr(model, "no_sync")
is_no_sync = (
(
((step_control + 1) % args.gradient_accumulation_steps != 0)
and args._no_sync_in_gradient_accumulation
)
or args.recompute
or args.use_expert_parallel
) and available_no_sync
# sharding
# stage1. the same as ddp
# stage2. manually collect gradient on dp group
dp_master_grad = (
self.args.world_size > 1 and self.args.amp_master_grad and not self.args.use_hybrid_parallel
)
if dp_master_grad:
is_no_sync = True
sync_context = model.no_sync() if is_no_sync else contextlib.nullcontext()
if self.args.count_trained_tokens:
self.trained_effective_tokens += (inputs["input_ids"] != self.args.pad_token_id).sum()
self.trained_tokens += inputs["input_ids"].numel()
with sync_context:
if "step_control" in inspect.signature(self.training_step).parameters:
tr_loss_step = self.training_step(model, inputs, step_control=step_control)
else:
tr_loss_step = self.training_step(model, inputs)
tr_loss += tr_loss_step
def fused_allreduce_gradients_no_sync(paramlist, hcg):
paramlist = list(paramlist)
nonmoe_list = [p for p in paramlist if not getattr(p, "no_sync", False)]
moelist = [p for p in paramlist if getattr(p, "no_sync", False)]
if moelist and not self.args.use_expert_parallel:
logger.warning("found `no sync` param when `use_expert_parallel=False`")
fused_allreduce_gradients(nonmoe_list, hcg)
if (step_control + 1) % args.gradient_accumulation_steps == 0 or (
# last step in epoch but step is always smaller than gradient_accumulation_steps
steps_in_epoch <= args.gradient_accumulation_steps
and (step + 1) == steps_in_epoch
):
if self.args.pipeline_parallel_degree <= 1 and self._enable_delay_scale_loss():
tr_loss /= self.args.gradient_accumulation_steps
# assert if loss is invalid
self._check_loss_valid(tr_loss)
self.timers and self.timers("forward-backward").stop()
# Manually collect gradients
# Case 1: Use recompute and dp
# Case 2: Hack dp with master_grad
# Case 3: Pipeline or sharding overlap
# local_rank != -1 don't means dp in networks.
self.timers and self.timers("all-reduce").start()
# Case 1: Use recompute and dp / sharding stage1,
# manually collect gradient for dp.
if (args.recompute or args.use_expert_parallel) and available_no_sync:
fused_allreduce_gradients_no_sync(list(model.parameters()), None)
# Case 2: hack dp with master_grad
elif dp_master_grad:
fused_allreduce_gradients_no_sync(list(model.parameters()), None)
# Pipeline parallel mode, handle gradient reduce here to overlap
enable_dp_comm_overlap = (
self.args.pipeline_parallel_degree > 1
and "enable_dp_comm_overlap" in args.pipeline_parallel_config
)
enable_release_grads = False
if args.sharding_parallel_degree > 1:
enable_release_grads = "enable_release_grads" in args.sharding_parallel_config
if not enable_release_grads and args.pipeline_parallel_degree > 1:
enable_release_grads = "enable_release_grads" in args.pipeline_parallel_config
# Case 3: Pipeline parallel mode, overlap with dp
if isinstance(self.optimizer, HybridParallelOptimizer) and not self.do_grad_scaling:
parameters_list = _obtain_optimizer_parameters_list(self.optimizer._inner_opt)
if not enable_dp_comm_overlap:
if self.optimizer._sharding_enable:
assert reshard_util.is_sharding_opt(self.optimizer)
self.optimizer._inner_opt.reduce_gradients(list(parameters_list), self.optimizer._hcg)
if self.optimizer._dp_enable or getattr(self.optimizer, "_sep_enable", False):
fused_allreduce_gradients_no_sync(list(parameters_list), self.optimizer._hcg)
self.timers and self.timers("all-reduce").stop()
self.timers and self.timers("optimizer-step").start()
if self.args.gradient_accumulation_steps > 1 and self._enable_delay_scale_loss():
paddle.device.synchronize()
for p in model._layers.parameters():
with paddle.no_grad():
if hasattr(p, "main_grad") and p.main_grad is not None:
assert p.grad is None
p.main_grad.scale_(1.0 / self.args.gradient_accumulation_steps)
elif p.grad is not None:
p.grad.scale_(1.0 / self.args.gradient_accumulation_steps)
# Optimizer step
self.callback_handler.on_optimizer_begin(
args, self.state, self.control, scaler=self.scaler if self.do_grad_scaling else None
)
optimizer_was_run = True
if self.args.offload_optim:
self._reload_optimizer()
if self.do_grad_scaling:
if args.pipeline_parallel_degree > 1:
assert not self.args.use_expert_parallel, "pipeline moe not work under fp16"
scale_before = paddle.assign(self.scaler._scale)
self.scaler.step(self.optimizer)
self.scaler.update()
scale_after = self.scaler._scale
# Compatible with paddlepaddle 2.6.0 using typo word.
if hasattr(self.scaler, "_cache_founf_inf"):
optimizer_was_run = not self.scaler._cache_founf_inf
else:
optimizer_was_run = not self.scaler._cache_found_inf
if not optimizer_was_run:
scale_before_value = scale_before.cpu().numpy()
scale_after_value = scale_after.cpu().numpy()
logger.warning(
f"optimizer not run, scale_before: {scale_before_value[0]}, scale_after: {scale_after_value[0]}"
)
elif isinstance(self.optimizer, HybridParallelOptimizer):
self.optimizer._step(parameters_list)
else:
self.optimizer.step()
if self.args.offload_optim:
self._offload_optimizer()
self.timers and self.timers("optimizer-step").stop()
if optimizer_was_run:
self.lr_scheduler.step()
if args.release_grads or enable_release_grads:
self.optimizer.clear_grad(set_to_zero=False)
if args.pipeline_parallel_degree > 1:
for _, buffers in model._chunk_2_comm_buffers.items():
for buffer in buffers:
buffer._clear_grad_storage()
else:
self.optimizer.clear_grad()
self.callback_handler.on_optimizer_end(
args, self.state, self.control, scaler=self.scaler if self.do_grad_scaling else None
)
self.state.global_step += 1
self.state.epoch = epoch + (step + 1) / steps_in_epoch
# For ZCC EMA
if self.args.enable_zero_cost_checkpoint or self.args.zcc_save_ema_coef is not None:
tr_loss_for_zcc = tr_loss.clone()
dist.all_reduce(
tr_loss_for_zcc, dist.ReduceOp.SUM
) # 3级并行时,每个pp下的loss会广播,全局reduce-mean的时候,分子分母都会乘以pp_world_size,结果会被约掉
tr_loss_for_zcc_scalar = tr_loss_for_zcc.item() / dist.get_world_size()
self.state.loss = tr_loss_for_zcc_scalar
self.state.consumed_samples = (
self.state.global_step
* args.per_device_train_batch_size
* args.gradient_accumulation_steps
* args.dataset_world_size
)
self.control = self.callback_handler.on_step_end(args, self.state, self.control)
self._maybe_log_save_evaluate(tr_loss, model, epoch, ignore_keys_for_eval, inputs=inputs)
self._print_timer()
step_control = 0
else:
self.control = self.callback_handler.on_substep_end(args, self.state, self.control)
step_control += 1
if self.control.should_epoch_stop or self.control.should_training_stop:
break
if self.args.ignore_data_skip:
self.timers and self.timers("read-data").start()
if step < 0:
logger.warning(
f"There seems to be not a single sample in your epoch_iterator, stopping training at step"
f" {self.state.global_step}! This is expected if you're using an IterableDataset and set"
f" num_steps ({self.state.max_steps}) higher than the number of available samples."
)
self.control.should_training_stop = True
self.control = self.callback_handler.on_epoch_end(args, self.state, self.control)
self._maybe_log_save_evaluate(tr_loss, model, epoch, ignore_keys_for_eval, inputs=inputs)
if self.control.should_training_stop:
break
if args.past_index and hasattr(self, "_past"):
# Clean the state at the end of training
delattr(self, "_past")
if self.args.enable_zero_cost_checkpoint:
self.zcc_manager.finalize()
logger.info("\nTraining completed. \n")
# unlink shared_memory if used.
if (
self.args.save_checkpoint_format == "unified_checkpoint"
or self.args.load_checkpoint_format == "unified_checkpoint"
):
self.unified_checkpoint_handler.unlink_shared_memory()
if args.load_best_model_at_end and self.state.best_model_checkpoint is not None:
if args.local_rank != -1:
dist.barrier()
logger.info(
f"Loading best model from {self.state.best_model_checkpoint} (score: {self.state.best_metric})."
)
if isinstance(self.model, LoRAModel) or isinstance(self.model, PrefixModelForCausalLM):
self._load_best_model_from_peft_checkpoint()
else:
if self.args.load_checkpoint_format == "unified_checkpoint":
self.unified_checkpoint_handler.load_unified_checkpoint(
self.model,
self.state.best_model_checkpoint,
)
if self.args.sharding_parallel_degree > 1 or self.args.data_parallel_degree > 1:
broadcast_dataset_rank0_model(self.model)
else:
weight_name = PADDLE_WEIGHTS_NAME
best_model_path = os.path.join(
self.state.best_model_checkpoint, _add_variant(weight_name, self.args.weight_name_suffix)
)
if os.path.exists(best_model_path):
# We load the model state dict on the CPU to avoid an OOM error.
state_dict = paddle.load(best_model_path, return_numpy=True)
# If the model is on the GPU, it still works!
self._set_state_dict_in_model(state_dict)
else:
logger.warning(
f"Could not locate the best model at {best_model_path}, if you are running a distributed training "
"on multiple nodes, you should activate `--save_on_each_node`."
)
self._total_loss_scalar += tr_loss.item()
# In case all steps were skipped, the total loss is set to 0.
if self.state.global_step == self._skip_global_steps:
logger.info("All steps were skipped, the total loss is set to 0.")
train_loss = 0.0
else:
train_loss = self._total_loss_scalar / (self.state.global_step - self._skip_global_steps)
metrics = speed_metrics("train", start_time, num_samples=num_train_samples, num_steps=self.state.max_steps)
metrics["train_loss"] = train_loss
self.is_in_train = False
self._memory_tracker.stop_and_update_metrics(metrics)
self.log(metrics)
kwargs = {
"metrics_dumper": self.metrics_dumper,
}
self.control = self.callback_handler.on_train_end(args, self.state, self.control, **kwargs)
return TrainOutput(self.state.global_step, train_loss, metrics)
def _load_best_model_from_peft_checkpoint(self):
if self.args.load_checkpoint_format == "unified_checkpoint":
self.unified_checkpoint_handler.load_unified_checkpoint(
self.model,
self.state.best_model_checkpoint,
)
if self.args.sharding_parallel_degree > 1 or self.args.data_parallel_degree > 1:
broadcast_dataset_rank0_model(self.model)
return
convert_tp = False
if isinstance(self.model, LoRAModel):
if self.model.quantized or self.args.pipeline_parallel_degree > 1:
best_model_path = os.path.join(
self.state.best_model_checkpoint, _add_variant(LORA_WEIGHTS_NAME, self.args.weight_name_suffix)
)
else:
best_model_path = os.path.join(self.state.best_model_checkpoint, LORA_WEIGHTS_NAME)
if self.model.lora_config.tensor_parallel_degree > 1:
convert_tp = True
elif isinstance(self.model, PrefixModelForCausalLM):
best_model_path = os.path.join(self.state.best_model_checkpoint, PREFIX_WEIGHTS_NAME)
if self.model.prefix_config.tensor_parallel_degree > 1:
convert_tp = True
if os.path.exists(best_model_path):
# We load the model state dict on the CPU to avoid an OOM error.
state_dict = paddle.load(best_model_path, return_numpy=True)
if convert_tp:
state_dict = self.model._convert_tensor_parallel(state_dict)
# If the model is on the GPU, it still works!
self._set_state_dict_in_model(state_dict)
else:
logger.warning(
f"Could not locate the best model at {best_model_path}, if you are running a distributed training "
"on multiple nodes, you should activate `--save_on_each_node`."
)
def _get_train_sampler(self) -> Optional[paddle.io.Sampler]:
if self.train_dataset is None or not has_length(self.train_dataset):
return None
if self.args.world_size <= 1:
return paddle.io.BatchSampler(
dataset=self.train_dataset,
shuffle=self.args.dataloader_shuffle,
batch_size=self.args.per_device_train_batch_size,
drop_last=self.args.dataloader_drop_last,
)
return DistributedBatchSampler(
self.train_dataset,
batch_size=self.args.per_device_train_batch_size,
shuffle=self.args.dataloader_shuffle,
num_replicas=self.args.dataset_world_size,
rank=self.args.dataset_rank,
drop_last=self.args.dataloader_drop_last,
)
def _set_state_dict_in_model(self, state_dict):
# TODO @ZHUI paddle need return the results of set_state_dict.
logger.info(f"set state-dict :{self.model.set_state_dict(state_dict)}")
def _print_timer(self):
"""print timer and clear states"""
paddle_timer_info = ""
try:
from paddle.distributed.fleet.utils.timer_helper import (
get_timers as paddle_get_timers,
)
paddle_pipeline_timers = paddle_get_timers()
for name, timer in paddle_pipeline_timers.timers.items():
elapsed_time = timer.elapsed(reset=False) * 1000.0
paddle_timer_info += f" | {name}: {elapsed_time:.2f}"
paddle_pipeline_timers.log(paddle_pipeline_timers.timers.keys(), reset=True)
except ImportError: # paddle version too old, timer not support
warnings.warn(f"paddle version:{paddle.__git_commit__} does not support pipeline timer")
except AssertionError: # paddle timer not enabled
pass
if self.timers is not None:
timer_info = self.timers.log(self.timers.timers.keys(), reset=True)
else:
timer_info = ""
if timer_info or paddle_timer_info:
logger.info(f"[Profile global_step: {self.state.global_step}] {timer_info} {paddle_timer_info}")
def _check_loss_valid(self, loss):
assert isinstance(loss, paddle.Tensor) and loss._is_initialized()
loss_value = loss.item()
if not self.args.fp16:
if not np.isfinite(loss_value).all():
err_msg = LOSS_NAN_ERROR if np.isnan(loss_value).any() else LOSS_INF_ERROR
raise ValueError(f"{err_msg}. Loss contains inf or nan values, its value is {loss_value}")
def _get_item_from_loss(self, loss):
assert isinstance(loss, paddle.Tensor) and loss._is_initialized()
loss_value = loss.item()
return loss_value
def _maybe_log_save_evaluate(self, tr_loss, model, epoch, ignore_keys_for_eval, **kwargs):
if self.control.should_log:
logs: Dict[str, float] = {}
num_steps = self.state.global_step - self._globalstep_last_logged - self._skip_steps_since_last_logged
self._skip_steps_since_last_logged = 0
# all_gather + mean() to get average loss over all processes
avg_loss = self._nested_gather(tr_loss).mean()
tr_loss_scalar = self._get_item_from_loss(avg_loss)
# reset tr_loss to zero
tr_loss.subtract_(tr_loss)
# set loss to zero if all steps are skipped since last log
if num_steps == 0:
logs["loss"] = 0.0
else:
logs["loss"] = round(tr_loss_scalar / num_steps, 8)
logs["learning_rate"] = float("{0:.3e}".format(self._get_learning_rate()))
logs["global_step"] = int(self.state.global_step)
if in_auto_parallel_align_mode():
logs["loss_md5"] = avg_loss._md5sum()
divisor = 2**30
# TODO(@gexiao): replace these codes with unified APIs in Paddle
current_device = framework._current_expected_place_()
if str(current_device) != "Place(cpu)":
device_id = current_device.get_device_id()
current_memory_allocated = core.device_memory_stat_current_value("Allocated", device_id)
current_memory_reserved = core.device_memory_stat_current_value("Reserved", device_id)
max_memory_allocated = core.device_memory_stat_peak_value("Allocated", device_id)
max_memory_reserved = core.device_memory_stat_peak_value("Reserved", device_id)
logs["current_memory_allocated"] = current_memory_allocated / divisor
logs["current_memory_reserved"] = current_memory_reserved / divisor
logs["max_memory_allocated"] = max_memory_allocated / divisor
logs["max_memory_reserved"] = max_memory_reserved / divisor
total_train_batch_size = (
self.args.train_batch_size * self.args.gradient_accumulation_steps * self.args.dataset_world_size
)
seq_length = None
model_flops_per_token = None
if getattr(self, "is_pretraining", False) and hasattr(self.model, "config"):
seq_length = getattr(self.model.config, "seq_length", None)
try:
model_flops_per_token = self.model.get_hardware_flops()
except NotImplementedError:
model_flops_per_token = None
# Do not log speed metrics if all steps are skipped since last log.
if num_steps > 0:
logs.update(
speed_metrics(
"interval",
self._globalstep_last_start_time,
num_samples=total_train_batch_size * num_steps,
num_steps=num_steps,
seq_length=seq_length,
model_flops_per_token=model_flops_per_token,
)
)
self._total_loss_scalar += tr_loss_scalar
self._globalstep_last_logged = self.state.global_step
self._globalstep_last_start_time = time.time()
# Add additional memory in log.
if not self.args.skip_memory_metrics:
logs.update(
{
"cpu_mem_used": self._memory_tracker.cpu_mem_used() >> 20,
"cpu_mem_used_peak": self._memory_tracker.cpu_mem_used_peak >> 20,
}
)
if is_paddle_cuda_available():
logs.update(
{
"gpu_max_memory_allocated": paddle.device.cuda.max_memory_allocated() >> 20,
"gpu_max_memory_reserved": paddle.device.cuda.max_memory_reserved() >> 20,
}
)
self.log(logs, **kwargs)
metrics = None
if self.control.should_evaluate:
if isinstance(self.optimizer, GroupShardedOptimizerStage2) and self.optimizer._broadcast_overlap:
paddle.device.synchronize()
if isinstance(self.eval_dataset, dict):
for eval_dataset_name, eval_dataset in self.eval_dataset.items():
metrics = self.evaluate(
eval_dataset=eval_dataset,
ignore_keys=ignore_keys_for_eval,
metric_key_prefix=f"eval_{eval_dataset_name}",
)
else:
metrics = self.evaluate(ignore_keys=ignore_keys_for_eval)
if self.control.should_save:
if isinstance(self.optimizer, GroupShardedOptimizerStage2) and self.optimizer._broadcast_overlap:
paddle.device.synchronize()
self._save_checkpoint(model, metrics=metrics)
logger.info(f"{self.runtime_timer.log()}")
self.control = self.callback_handler.on_save(self.args, self.state, self.control)
self.log_trained_tokens()
def log_trained_tokens(self):
if self.args.count_trained_tokens:
token_list = []
for token_num in [self.trained_effective_tokens, self.trained_tokens]:
tensors = token_num.reshape([1])
if self.hcg._sharding_degree > 1:
output_tensors = []
paddle.distributed.all_gather(output_tensors, tensors, group=self.hcg._sharding_comm_group)
tensors = paddle.concat(output_tensors).sum().reshape([1])
if self.hcg._dp_degree > 1:
output_tensors = []
paddle.distributed.all_gather(output_tensors, tensors, group=self.hcg._dp_comm_group)
tensors = paddle.concat(output_tensors).sum().reshape([1])
token_list.append(tensors.item())
if self.is_local_process_zero():
logger.info(
f"Update to now, trained_effective_tokens: {token_list[0]}, trained_tokens: {token_list[1]}."
)
def _get_learning_rate(self):
return self.optimizer.get_lr()
def get_train_dataloader(self):
"""
Returns the training [`~paddle.io.DataLoader`].
Will use no sampler if `self.train_dataset` does not implement `__len__`, a random sampler (adapted to
distributed training if necessary) otherwise.
Subclass and override this method if you want to inject some custom behavior.
"""
if self.args.should_load_dataset and self.train_dataset is None:
raise ValueError("Training requires a train_dataset when should_load_dataset is True.")
if not self.args.should_load_dataset and self.train_dataset is not None:
raise ValueError("We don't need train_dataset when should_load_dataset is False.")
train_dataset = self.train_dataset
if self.args.distributed_dataloader:
is_iterable_dataset = self._is_iterable_dataset_distributed(train_dataset)
else:
is_iterable_dataset = self._is_iterable_dataset(train_dataset)
if is_datasets_available() and train_dataset is not None and isinstance(train_dataset, datasets.Dataset):
train_dataset = self._remove_unused_columns(train_dataset, description="training")
_DataLoader = DistDataLoader if self.args.distributed_dataloader else DataLoader
additional_configs = {}
if is_iterable_dataset: # For iterable dataset
if self.args.dataset_world_size > 1 and train_dataset is not None:
train_dataset = IterableDatasetShard(
train_dataset,
batch_size=self.args.per_device_train_batch_size,
drop_last=self.args.dataloader_drop_last,
num_processes=self.args.dataset_world_size,
process_index=self.args.dataset_rank,
)
if self.args.distributed_dataloader:
logger.info("Training using DistDataLoader.")
additional_configs = {"is_iterable_dataset": True, "pp_data_group": self._pp_data_group}
return _DataLoader(
train_dataset,
batch_size=self.args.per_device_train_batch_size,
collate_fn=self.data_collator,
num_workers=self.args.dataloader_num_workers,
**additional_configs,
)
else:
train_sampler = self._get_train_sampler()
if self.args.distributed_dataloader:
logger.info("Training using DistDataLoader.")
additional_configs = {"pp_data_group": self._pp_data_group}
return _DataLoader(
train_dataset,
batch_sampler=train_sampler,
collate_fn=self.data_collator,
num_workers=self.args.dataloader_num_workers,
**additional_configs,
)
def _get_eval_sampler(self, eval_dataset: Dataset):
if eval_dataset is None or not has_length(eval_dataset):
return None
if self.args.world_size <= 1:
return paddle.io.BatchSampler(
eval_dataset,
batch_size=self.args.per_device_eval_batch_size,
shuffle=False,
drop_last=False,
)
else:
if self.args.pipeline_parallel_degree > 1:
# In pipeline parallelism, batch size will be strictly checked
# Use LastBatchPaddingSampler to pad the last batch with the first batch
from .trainer_utils import LastBatchPaddingSampler
return LastBatchPaddingSampler(
eval_dataset,
num_replicas=self.args.dataset_world_size,
rank=self.args.dataset_rank,
batch_size=self.args.per_device_eval_batch_size,
shuffle=False,
drop_last=False,
)
else:
return DistributedBatchSampler(
eval_dataset,
num_replicas=self.args.dataset_world_size,
rank=self.args.dataset_rank,
batch_size=self.args.per_device_eval_batch_size,
shuffle=False,
drop_last=False,
)
def get_eval_dataloader(self, eval_dataset: Optional[Dataset] = None) -> DataLoader:
"""
Returns the evaluation [`~paddle.io.DataLoader`].
Subclass and override this method if you want to inject some custom behavior.
Args:
eval_dataset (`paddle.io.Dataset`, *optional*):
If provided, will override `self.eval_dataset`. If it is an `datasets.Dataset`, columns not accepted by
the `model.forward()` method are automatically removed. It must implement `__len__`.
"""
if self.args.should_load_dataset and eval_dataset is None and self.eval_dataset is None:
raise ValueError("Evaluation requires an eval_dataset when should_load_dataset is True.")
if not self.args.should_load_dataset and not (eval_dataset is None and self.eval_dataset is None):
raise ValueError("We don't need eval_dataset when should_load_dataset is False.")
eval_dataset = eval_dataset if eval_dataset is not None else self.eval_dataset
if self.args.distributed_dataloader:
is_iterable_dataset = self._is_iterable_dataset_distributed(eval_dataset)
else:
is_iterable_dataset = self._is_iterable_dataset(eval_dataset)
if is_datasets_available() and eval_dataset is not None and isinstance(eval_dataset, datasets.Dataset):
eval_dataset = self._remove_unused_columns(eval_dataset, description="evaluation")
_DataLoader = DistDataLoader if self.args.distributed_dataloader else DataLoader
additional_configs = {}
if is_iterable_dataset:
if (
self.args.dataset_world_size > 1 or self.args.pipeline_parallel_degree > 1
) and eval_dataset is not None:
eval_dataset = IterableDatasetShard(
eval_dataset,
batch_size=self.args.per_device_eval_batch_size,
drop_last=self.args.dataloader_drop_last,
num_processes=self.args.dataset_world_size,
process_index=self.args.dataset_rank,
)
if self.args.distributed_dataloader:
logger.info("Eval using DistDataLoader.")
additional_configs = {"eval": True, "is_iterable_dataset": True, "pp_data_group": self._pp_data_group}
return _DataLoader(
eval_dataset,
batch_size=self.args.per_device_eval_batch_size,
collate_fn=self.data_collator,
num_workers=0,
**additional_configs,
)
else:
eval_sampler = self._get_eval_sampler(eval_dataset)
if self.args.distributed_dataloader:
logger.info("Eval using DistDataLoader.")
additional_configs = {"eval": True, "pp_data_group": self._pp_data_group}
return _DataLoader(
eval_dataset,
batch_sampler=eval_sampler,
collate_fn=self.data_collator,
num_workers=self.args.dataloader_num_workers,
**additional_configs,
)
def get_test_dataloader(self, test_dataset: Dataset) -> DataLoader:
"""
Returns the test [`~paddle.io.DataLoader`].
Subclass and override this method if you want to inject some custom behavior.
Args:
test_dataset (`paddle.io.Dataset`, *optional*):
The test dataset to use. If it is an `datasets.Dataset`, columns not accepted by the `model.forward()`
method are automatically removed. It must implement `__len__`.
"""
if self.args.should_load_dataset and not test_dataset:
raise ValueError("Test requires an test_dataset when should_load_dataset is True.")
if not self.args.should_load_dataset and test_dataset is not None:
raise ValueError("We don't need test_dataset when should_load_dataset is False.")
if self.args.distributed_dataloader:
is_iterable_dataset = self._is_iterable_dataset_distributed(test_dataset)
else:
is_iterable_dataset = self._is_iterable_dataset(test_dataset)
if is_datasets_available() and test_dataset is not None and isinstance(test_dataset, datasets.Dataset):
test_dataset = self._remove_unused_columns(test_dataset, description="test")
_DataLoader = DistDataLoader if self.args.distributed_dataloader else DataLoader
additional_config = {}
if is_iterable_dataset:
if self.args.dataset_world_size > 1 and test_dataset is not None:
test_dataset = IterableDatasetShard(
test_dataset,
batch_size=self.args.per_device_eval_batch_size,
drop_last=self.args.dataloader_drop_last,
num_processes=self.args.dataset_world_size,
process_index=self.args.dataset_rank,
)
if self.args.distributed_dataloader:
logger.info("Test using DistDataLoader.")
additional_config = {"eval": True, "is_iterable_dataset": True, "pp_data_group": self._pp_data_group}
return _DataLoader(
test_dataset,
batch_size=self.args.per_device_eval_batch_size * self.world_size,
collate_fn=self.data_collator,
num_workers=self.args.dataloader_num_workers,
**additional_config,
)
else:
test_sampler = self._get_eval_sampler(test_dataset)
if self.args.distributed_dataloader:
logger.info("Test using DistDataLoader.")
additional_config = {"eval": True, "pp_data_group": self._pp_data_group}
# We use the same batch_size as for eval.
return _DataLoader(
test_dataset,
batch_sampler=test_sampler,
collate_fn=self.data_collator,
drop_last=self.args.dataloader_drop_last,
**additional_config,
)
def create_optimizer_and_scheduler(self, num_training_steps: int):
"""
Setup the optimizer and the learning rate scheduler.
We provide a reasonable default that works well. If you want to use something else, you can pass a tuple in the
Trainer's init through `optimizers`, or subclass and override this method (or `create_optimizer` and/or
`create_scheduler`) in a subclass.
"""
self.create_scheduler(num_training_steps=num_training_steps)
self.create_optimizer(self.lr_scheduler)
def create_optimizer(self, lr_scheduler=None):
"""
Setup the optimizer.
We provide a reasonable default that works well. If you want to use something else, you can pass a tuple in the
Trainer's init through `optimizers`, or subclass and override this method in a subclass.
"""
if self.optimizer is None:
if self.optimizer_grouped_parameters is not None:
params = self.optimizer_grouped_parameters
apply_decay_param_fun = None
else:
params = self.model.parameters()
decay_parameters = [
p.name for n, p in self.model.named_parameters() if not any(nd in n for nd in ["bias", "norm"])
]
def apply_decay_param_fun(x):
return x in decay_parameters
optimizer_cls, optimizer_kwargs = Trainer.get_optimizer_cls_and_kwargs(self.args)
if self.args.optim == OptimizerNames.ADAMW_CUSTOM:
optimizer_kwargs["quantization_config"] = self.model.config.quantization_config
optimizer_kwargs["use_lowprecision_moment"] = self.args.use_lowprecision_moment
optimizer_kwargs["tensorwise_offload_optimizer"] = self.args.tensorwise_offload_optimizer
if hasattr(optimizer_cls, "_create_master_weight") and self.args.fp16_opt_level == "O2":
optimizer_kwargs["multi_precision"] = True
self.optimizer = optimizer_cls(
learning_rate=self.lr_scheduler if lr_scheduler is None else lr_scheduler,
apply_decay_param_fun=apply_decay_param_fun,
parameters=params,
weight_decay=self.args.weight_decay,
grad_clip=nn.ClipGradByGlobalNorm(self.args.max_grad_norm) if self.args.max_grad_norm > 0 else None,
**optimizer_kwargs,
)
return self.optimizer
def _apply_to_optimizer(self, action):
attributes = [
("_accumulators", "_moment1_acc_str"),
("_accumulators", "_moment2_acc_str"),
("_master_weights",),
("_accumulators_holder",),
]
for attr in attributes:
if all(hasattr(self.optimizer, a) for a in attr):
target_attr = getattr(self.optimizer, attr[0])
if len(attr) == 2:
target_attr = target_attr[getattr(self.optimizer, attr[1])]
for key, value in target_attr.items():
if get_env_device() == "gpu":
target_attr[key] = getattr(value, action)()
else:
target_attr[key] = getattr(value, "to")(action)
def _offload_optimizer(self):
if get_env_device() == "gpu":
self._apply_to_optimizer("pin_memory")
else:
self._apply_to_optimizer("cpu")
def _reload_optimizer(self):
if get_env_device() == "gpu":
self._apply_to_optimizer("cuda")
else:
self._apply_to_optimizer(get_env_device())
def _load_rng_state(self, checkpoint):
# Load RNG states from `checkpoint`
if checkpoint is None:
return
rng_file = os.path.join(checkpoint, f"rng_state_{dist.get_rank()}.pth")
if not os.path.isfile(rng_file):
logger.info(
"Didn't find an RNG file, if you are resuming a training that was launched in a distributed "
"fashion, reproducibility is not guaranteed."
)
return
rng_file = os.path.join(checkpoint, f"rng_state_{dist.get_rank()}.pth")
if not os.path.isfile(rng_file):
logger.info(
"Didn't find an RNG file, if you are resuming a training that was launched in a distributed "
"fashion, reproducibility is not guaranteed."
)
return
checkpoint_rng_state = paddle.load(rng_file, return_numpy=True)
if checkpoint_rng_state.get("world_size", None) != self.args.world_size:
logger.warning("Cannot load rng states when changing world size of training job.")
return
random.setstate(checkpoint_rng_state["python"])
np.random.set_state(checkpoint_rng_state["numpy"])
core.default_cpu_generator().set_state(checkpoint_rng_state["cpu"])
if core.is_compiled_with_cuda():
if not len(checkpoint_rng_state["cuda"]) == core.get_cuda_device_count():
raise ValueError("Length of gpu state list should be equal to the gpu device count")
for i in range(core.get_cuda_device_count()):
core.default_cuda_generator(i).set_state(checkpoint_rng_state["cuda"][i])
if core.is_compiled_with_xpu():
if not len(checkpoint_rng_state["cuda"]) == core.get_xpu_device_count():
raise ValueError("Length of xpu state list should be equal to the xpu device count")
for i in range(core.get_xpu_device_count()):
core.default_xpu_generator(i).set_state(checkpoint_rng_state["cuda"][i])
if paddle.device.get_all_custom_device_type() is not None:
custom_device_type = paddle.device.get_all_custom_device_type()
for device in custom_device_type:
if not len(checkpoint_rng_state["cuda"]) == core.get_custom_device_count(device):
raise ValueError("Length of custom device state list should be equal to the custom device count")
for i in range(core.get_custom_device_count(device)):
core.default_custom_device_generator(paddle.CustomPlace(device, i)).set_state(
checkpoint_rng_state["cuda"][i]
)
if self.args.use_hybrid_parallel:
if "hybrid_parallel_rng_state_tracker" in checkpoint_rng_state:
if self.args.tensor_parallel_degree <= 1:
checkpoint_rng_state["hybrid_parallel_rng_state_tracker"].pop("model_parallel_rng", None)
try:
fleet.meta_parallel.get_rng_state_tracker().set_states_tracker(
checkpoint_rng_state["hybrid_parallel_rng_state_tracker"]
)
except:
logger.warning(
"Hybrid parallel rng states change when training environment differs, so we dot not set state tracker here."
)
else:
logger.warning("Not found hybrid parallel RNG state.")
@staticmethod
def get_optimizer_cls_and_kwargs(args: TrainingArguments) -> Tuple[Any, Any]:
"""
Returns the optimizer class and optimizer parameters based on the training arguments.
Args:
args (`paddlenlp.training_args.TrainingArguments`):
The training arguments for the training session.
"""
# optimizer_kwargs = {"lr": args.learning_rate}
optimizer_kwargs = {}
adam_kwargs = {
"beta1": args.adam_beta1,
"beta2": args.adam_beta2,
"epsilon": args.adam_epsilon,
}
if args.optim == OptimizerNames.ADAMW:
from paddle.optimizer import AdamW
optimizer_cls = AdamW
optimizer_kwargs.update(adam_kwargs)
elif args.optim == OptimizerNames.ADAMW_MINI:
from ..utils import AdamWMini
optimizer_cls = AdamWMini
optimizer_kwargs.update(adam_kwargs)
elif args.optim == OptimizerNames.ADAMW_CUSTOM:
from ..utils import AdamWCustom
optimizer_cls = AdamWCustom
optimizer_kwargs.update(adam_kwargs)
else:
raise ValueError(f"Trainer cannot instantiate unsupported optimizer: {args.optim}")
return optimizer_cls, optimizer_kwargs
def create_scheduler(self, num_training_steps: int):
"""
Setup the scheduler. The optimizer of the trainer must have been set up either before this method is called or
passed as an argument.
Args:
num_training_steps (int): The number of training steps to do.
"""
warmup = (
self.args.warmup_steps if self.args.warmup_steps > 0 else int(self.args.warmup_ratio * num_training_steps)
)
decay_steps = num_training_steps
if getattr(self.args, "decay_steps", None) and self.args.decay_steps > 0:
decay_steps = self.args.decay_steps
if self.lr_scheduler is None:
self.lr_scheduler = get_scheduler(
self.args.lr_scheduler_type,
learning_rate=self.args.learning_rate,
num_warmup_steps=warmup,
num_training_steps=decay_steps,
num_cycles=self.args.num_cycles,
lr_end=self.args.lr_end,
power=self.args.power,
min_lr=self.args.min_lr,
)
return self.lr_scheduler
def num_examples(self, dataloader: DataLoader) -> int:
"""
Helper to get number of samples in a [`~paddle.io.DataLoader`] by accessing its dataset. When
dataloader.dataset does not exist or has no length, estimates as best it can
"""
try:
dataset = dataloader.dataset
# Special case for IterableDatasetShard, we need to dig deeper
if isinstance(dataset, IterableDatasetShard):
return len(dataloader.dataset.dataset)
return len(dataloader.dataset)
except (NameError, AttributeError, TypeError): # no dataset or length, estimate by length of dataloader
return len(dataloader) * self.args.per_device_train_batch_size
def _decorate_exclude_layers(self, model: nn.Layer):
"""
Exclude layers from the model for paddle.amp.decorate.
Args:
model (`nn.Layer`): The model to exclude layers from.
Returns:
A list of excluded layers.
"""
exclude_layers = []
return exclude_layers
def _wrap_model(self, model, training=True):
# train/eval could be run multiple-times - if already wrapped, don't re-wrap it again
if unwrap_model(model) is not model:
return model
# Note: in paddle.distributed mode, there's no point in wrapping the model
# inside a DistributedDataParallel as we'll be under `no_grad` anyways.
if not training:
return model
# Mixed precision training
if training and self.do_grad_scaling: # self.args.fp16_opt_level=="O2":
# model, self.optimizer
decorated = paddle.amp.decorate(
models=model,
optimizers=self.optimizer,
level=self.args.fp16_opt_level,
dtype=self.amp_dtype,
excluded_layers=[QuantizationLinear, ColumnParallelQuantizationLinear, RowParallelQuantizationLinear]
+ self._decorate_exclude_layers(model),
)
if self.optimizer is None:
model = decorated
else:
model, self.optimizer = decorated
if self.args.world_size == 1:
if self.args.amp_master_grad:
mix_precision_utils.MixPrecisionLayer(model, dtype=self.amp_dtype)
assert self.optimizer is not None, "optimizer is empty!"
self.optimizer = mix_precision_utils.MixPrecisionOptimizer(self.optimizer)
in_pipeline_parallel_mode = self.args.pipeline_parallel_degree > 1
in_sharding_parallel_mode = self.sharding is not None
in_tensor_parallel_mode = self.args.tensor_parallel_degree > 1
in_sep_parallel_mode = self.args.sep_parallel_degree > 1
in_cp_parallel_mode = self.args.context_parallel_degree > 1
# Multi-gpu training
if self.args.world_size > 1 and (not self.args.use_hybrid_parallel):
# MOE use DDP to broadcaset parameters.
ddp_kwargs = {}
if self.args.ddp_find_unused_parameters is not None:
ddp_kwargs["find_unused_parameters"] = self.args.ddp_find_unused_parameters
elif isinstance(model, PretrainedModel):
# find_unused_parameters breaks checkpointing as per
# https://github.com/huggingface/transformers/pull/4659#issuecomment-643356021
ddp_kwargs["find_unused_parameters"] = not any(
hasattr(m, "enable_recompute") and m.enable_recompute for m in model.sublayers(include_self=True)
)
else:
ddp_kwargs["find_unused_parameters"] = True
model = paddle.DataParallel(model, **ddp_kwargs)
# Distributed training (should be after fp16 initialization)
if self.args.amp_master_grad:
mix_precision_utils.MixPrecisionLayer(model, dtype=self.amp_dtype)
assert self.optimizer is not None, "optimizer is empty!"
self.optimizer = mix_precision_utils.MixPrecisionOptimizer(self.optimizer)
# Pipeline mode
if in_pipeline_parallel_mode:
if self.args.amp_master_grad:
mix_precision_utils.MixPrecisionLayer(model, dtype=self.amp_dtype) # return value has no use
# hack for pipeline model mini batch to batch
# need batter solution @ZHUI
# make batch_fn compatible for fleet.distributed_model decorate.
prepare_pipeline_inputs_func = (
model._prepare_pipeline_inputs_func if hasattr(model, "_prepare_pipeline_inputs_func") else None
)
if isinstance(model, LoRAModel):
model = model.model
model = fleet.distributed_model(model)
if prepare_pipeline_inputs_func is not None:
model._prepare_pipeline_inputs_func = prepare_pipeline_inputs_func
else:
def _prepare_pipeline_inputs_func(inputs):
first_stage_keys = ["input_ids", "attention_mask", "position_ids"]
last_stage_keys = ["labels"]
def get_expected_keys(inputs, keys):
ret = tuple([inputs.pop(k) for k in keys if k in inputs])
if len(ret) == 1:
ret = ret[0]
return ret
if type(inputs) is dict or type(inputs) is OrderedDict:
return [
get_expected_keys(inputs, first_stage_keys),
get_expected_keys(inputs, last_stage_keys),
]
keys = list(inputs[0].keys())
inputs_batch = {key: [data.pop(key) for data in inputs] for key in keys}
return [
get_expected_keys(inputs_batch, first_stage_keys),
get_expected_keys(inputs_batch, last_stage_keys),
]
logger.warning(
"Using default prepare pipeline inputs func, only support input_ids and labels as inputs."
)
model._prepare_pipeline_inputs_func = _prepare_pipeline_inputs_func
assert self.optimizer is not None, "Pipeline mode need decorate optimizer, pelease init optimizer."
if self.args.amp_master_grad:
self.optimizer = mix_precision_utils.MixPrecisionOptimizer(self.optimizer)
self.optimizer = fleet.distributed_optimizer(self.optimizer)
if (
hasattr(self.args, "enable_sharding_comm_overlap")
and self.args.enable_sharding_comm_overlap
and (
self.args.save_checkpoint_format == "unified_checkpoint"
or self.args.load_checkpoint_format == "unified_checkpoint"
)
and "split_param" in split_parallel_config(self.args.sharding_parallel_config)
):
model.register_sharding_comm_overlap_hook(self.optimizer)
# No pipeline mode, sharding only
if not in_pipeline_parallel_mode and in_sharding_parallel_mode:
# Sharded DDP!
if self.args.tensor_parallel_degree > 1:
hcg = fleet.get_hybrid_communicate_group()
assert (
ShardingOption.SHARD_GRAD_OP in self.args.sharding or ShardingOption.SHARD_OP in self.args.sharding
), "Only support tensor parallel + sharding stage1/stage2 hybrid parallel now."
# NOTE: TensorParallel will be called in distributed_model when sharding stage1, so no need to call here
if ShardingOption.SHARD_GRAD_OP in self.args.sharding:
model = paddle.distributed.fleet.meta_parallel.TensorParallel(
model, hcg, strategy=fleet.fleet._user_defined_strategy
)
if ShardingOption.SHARD_OP in self.args.sharding:
if self.args.amp_master_grad:
mix_precision_utils.MixPrecisionLayer(model, dtype=self.amp_dtype) # return value has no use
model = fleet.distributed_model(model)
if self.args.amp_master_grad:
self.optimizer = mix_precision_utils.MixPrecisionOptimizer(self.optimizer)
self.optimizer = fleet.distributed_optimizer(self.optimizer)
else:
cpu_offload = ShardingOption.OFFLOAD in self.args.sharding
assert self.optimizer is not None, "optimizer is empty!"
level = None
if ShardingOption.SHARD_GRAD_OP in self.args.sharding:
level = "os_g"
if ShardingOption.FULL_SHARD in self.args.sharding:
level = "p_g_os"
from paddle.distributed.sharding import group_sharded_parallel
# add dp_group and exclude_layer params
# https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/api/paddle/distributed/sharding/group_sharded_parallel_cn.html#group-sharded-parallel
extra_kwargs = {}
extra_kwargs["dp_group"] = self.dp_group
extra_kwargs["exclude_layer"] = ["GroupNorm"]
if self.args.amp_master_grad:
assert (
self.args.data_parallel_degree == 1
), "Sharding stage 2 / Sharding stage 3 main grad is not compatible with dp for now."
mix_precision_utils.MixPrecisionLayer(model, dtype=self.amp_dtype) # return value has no use
self.optimizer = mix_precision_utils.MixPrecisionOptimizer(self.optimizer)
model, optimizer, _ = group_sharded_parallel(
model,
self.optimizer,
level=level,
scaler=None,
group=self.sharding_group,
offload=cpu_offload,
**extra_kwargs,
)
if ShardingOption.SHARD_GRAD_OP in self.args.sharding and self.args.amp_master_grad:
assert hasattr(optimizer, "use_main_grad"), (
"Current installed paddle doesn't support sharding stage 2 with main grad, "
"please upgrade your paddle (using nightly version)."
)
if level == "os_g" and "enable_stage2_overlap" in self.args.sharding_parallel_config:
model._set_reduce_overlap(True)
optimizer._set_broadcast_overlap(True, model)
self.optimizer = optimizer
# pure tesnor parallel mode, no pipeline_parallel, no sharding.
if (
not in_pipeline_parallel_mode
and not in_sharding_parallel_mode
and (in_tensor_parallel_mode or in_sep_parallel_mode or in_cp_parallel_mode)
):
if self.args.amp_master_grad:
mix_precision_utils.MixPrecisionLayer(model, dtype=self.amp_dtype) # return value has no use
model = fleet.distributed_model(model)
assert self.optimizer is not None, "Tensor parallel mode need decorate optimizer, pelease init optimizer."
if self.args.amp_master_grad:
self.optimizer = mix_precision_utils.MixPrecisionOptimizer(self.optimizer)
self.optimizer = fleet.distributed_optimizer(self.optimizer)
# stage1 has v1 and v2 version
if in_sharding_parallel_mode and ShardingOption.SHARD_OP in self.args.sharding:
if "split_param" in self.args.sharding_parallel_config:
if (
hasattr(self.optimizer, "_set_all_gather_overlap_forward")
and "enable_stage1_allgather_overlap" in self.args.sharding_parallel_config
):
self.optimizer._set_all_gather_overlap_forward(True, model)
else:
if (
hasattr(self.optimizer, "_set_broadcast_overlap")
and "enable_stage1_broadcast_overlap" in self.args.sharding_parallel_config
):
self.optimizer._set_broadcast_overlap(True, model)
# use callback for sp grad sync in case of unexpected behaviour (except sharding stage 2&3)
if self.args.tensor_parallel_degree > 1 and self.args.sequence_parallel:
if ShardingOption.SHARD_GRAD_OP in self.args.sharding or ShardingOption.FULL_SHARD in self.args.sharding:
register_sequence_parallel_allreduce_hooks(
unwrap_model(model),
self.args.gradient_accumulation_steps,
self.args.fuse_sequence_parallel_allreduce,
)
else:
self.add_callback(SPGradSyncCallback(model._layers))
return model
def _prepare_input(self, data: Union[paddle.Tensor, Any]) -> Union[paddle.Tensor, Any]:
"""
Prepares one `data` before feeding it to the model, be it a tensor or a nested list/dictionary of tensors.
"""
if isinstance(data, Mapping):
return type(data)({k: self._prepare_input(v) for k, v in data.items()})
elif isinstance(data, (tuple, list)):
return type(data)(self._prepare_input(v) for v in data)
elif isinstance(data, paddle.Tensor):
# kwargs = dict(device=self.args.current_device)
# update data type for pure fp16
if data.place.is_cuda_pinned_place():
return data.cuda()
return data
# return data.to(**kwargs)
return data
def _prepare_inputs(self, inputs: Dict[str, Union[paddle.Tensor, Any]]) -> Dict[str, Union[paddle.Tensor, Any]]:
"""
Prepare `inputs` before feeding them to the model, converting them to tensors if they are not already and
handling potential state.
"""
inputs = self._prepare_input(inputs)
if self.args.past_index >= 0 and self._past is not None:
inputs["mems"] = self._past
return inputs
def autocast_smart_context_manager(self):
"""
A helper wrapper that creates an appropriate context manager for `autocast` while feeding it the desired
arguments, depending on the situation.
"""
if self.enable_autocast_context_manager:
custom_black_list = ["reduce_sum", "c_softmax_with_cross_entropy"]
custom_white_list = []
if self.args.fp16_opt_level == "O2":
# https://github.com/PaddlePaddle/Paddle/blob/eb97f4f0adca40b16a309b927e480178beb8ae96/python/paddle/amp/amp_lists.py#L85-L86
# the lookup_table is in black_list, but in O2, we need it return fp16
custom_white_list.extend(["lookup_table", "lookup_table_v2"])
if self.args.amp_custom_white_list is not None:
custom_white_list.extend(self.args.amp_custom_white_list)
if self.args.amp_custom_black_list is not None:
custom_black_list.extend(self.args.amp_custom_black_list)
ctx_manager = autocast(
True,
custom_black_list=set(custom_black_list),
custom_white_list=set(custom_white_list),
level=self.args.fp16_opt_level,
dtype=self.amp_dtype,
)
else:
ctx_manager = contextlib.nullcontext() if sys.version_info >= (3, 7) else contextlib.suppress()
return ctx_manager
def compute_loss(self, model, inputs, return_outputs=False):
"""
How the loss is computed by Trainer. By default, all models return the loss in the first element.
Subclass and override for custom behavior.
"""
if self.criterion is not None:
if "labels" in inputs:
labels = inputs.pop("labels")
elif "start_positions" in inputs and "end_positions" in inputs:
labels = (inputs.pop("start_positions"), inputs.pop("end_positions"))
elif self.args.label_names is not None:
labels = []
for label in self.label_names:
labels.append(inputs.pop(label))
labels = tuple(labels)
elif "generator_labels" in inputs:
labels = inputs["generator_labels"]
else:
labels = None
outputs = model(**inputs)
if self.criterion is not None:
loss = self.criterion(outputs, labels)
outputs = (loss, outputs)
# Save past state if it exists
# TODO: this needs to be fixed and made cleaner later.
if self.args.past_index >= 0:
self._past = outputs[self.args.past_index]
# We don't use .loss here since the model may return tuples instead of ModelOutput.
loss = outputs["loss"] if isinstance(outputs, dict) else outputs
if isinstance(outputs, dict):
loss = outputs["loss"]
elif isinstance(outputs, tuple):
loss = outputs[0]
else:
loss = outputs
return (loss, outputs) if return_outputs else loss
def _enable_delay_scale_loss(self):
if in_auto_parallel_align_mode():
return True
key = "enable_delay_scale_loss"
if self.args.pipeline_parallel_degree > 1:
return key in self.args.pipeline_parallel_config
elif self.args.tensor_parallel_degree > 1:
return key in self.args.tensor_parallel_config
else:
return False
def training_step(
self, model: nn.Layer, inputs: Dict[str, Union[paddle.Tensor, Any]], step_control=0
) -> paddle.Tensor:
"""
Perform a training step on a batch of inputs.
Subclass and override to inject custom behavior.
Args:
model (`nn.Layer`):
The model to train.
inputs (`Dict[str, Union[paddle.Tensor, Any]]`):
The inputs and targets of the model.
The dictionary will be unpacked before being fed to the model. Most models expect the targets under the
argument `labels`. Check your model's documentation for all accepted arguments.
Return:
`paddle.Tensor`: The tensor with training loss on this batch.
"""
if self.args.pipeline_parallel_degree > 1:
return self.training_pipeline_step(model, inputs)
model.train()
inputs = self._prepare_inputs(inputs)
with self.autocast_smart_context_manager():
loss = self.compute_loss(model, inputs)
if self.args.gradient_accumulation_steps > 1 and not self._enable_delay_scale_loss():
loss = loss / self.args.gradient_accumulation_steps
if self.do_grad_scaling:
self.scaler.scale(loss).backward()
else:
loss.backward()
return loss.detach()
def training_pipeline_step(self, model: nn.Layer, inputs: Dict[str, Union[paddle.Tensor, Any]]) -> paddle.Tensor:
"""
Perform a training step on a batch of inputs.
Subclass and override to inject custom behavior.
Args:
model (`nn.Layer`):
The model to train.
inputs (`Dict[str, Union[paddle.Tensor, Any]]`):
The inputs and targets of the model.
The dictionary will be unpacked before being fed to the model. Most models expect the targets under the
argument `labels`. Check your model's documentation for all accepted arguments.
Return:
`paddle.Tensor`: The tensor with training loss on this batch.
"""
# accumulation data
if not hasattr(self, "_pp_data_buffer"):
self._pp_data_buffer = []
self._pp_data_buffer.append(inputs)
if len(self._pp_data_buffer) != self.args.gradient_accumulation_steps:
return paddle.zeros([])
# for v in self._pp_data_buffer[0].values():
# assert isinstance(v, paddle.Tensor), f"Only support tensor as pipeline mode input, got type {type(v)}"
model.train()
if model._dp_comm_overlap or model._sharding_comm_overlap:
for _, buffers in model._chunk_2_comm_buffers.items():
for buffer in buffers:
buffer._acc_steps = self.args.gradient_accumulation_steps
model.optimizer = None # we do not use `PipelineParallel` to handler optimizer step
model.lr_scheduler = None
def _dataset_process_function():
# Pass a local function to forward_backward_pipeline instead of the dataset itself.
# This prevents the dataset from being passed as a direct argument to forward_backward_pipeline,
# which would create additional reference counts that cannot be cleared, leading to GPU memory leaks.
with self.autocast_smart_context_manager():
inputs = model._prepare_pipeline_inputs_func(self._pp_data_buffer)
self._pp_data_buffer = []
return model._prepare_training(
inputs, self.optimizer, self.lr_scheduler
) # None, None => [optimizer, lr_scheduler]
if PipelineDatasetPreprocessor is None:
inputs = _dataset_process_function()
else:
inputs = PipelineDatasetPreprocessor(_dataset_process_function)
with self.autocast_smart_context_manager():
loss = model.forward_backward_pipeline(inputs, self.scaler if self.do_grad_scaling else None)
return loss.detach()
def save_model(
self,
output_dir: Optional[str] = None,
merge_tensor_parallel: Optional[bool] = False,
):
"""
Will save the model, so you can reload it using `from_pretrained()`.
Will only save from the main process.
"""
if output_dir is None:
output_dir = self.args.output_dir
if PREFIX_CHECKPOINT_DIR in os.path.split(output_dir)[-1]:
signal_dir = os.path.join(self.args.output_signal_dir, os.path.split(output_dir)[-1])
else:
signal_dir = self.args.output_signal_dir
if ShardingOption.FULL_SHARD in self.args.sharding:
self.model_wrapped.get_all_parameters(convert2cpu=True)
if self.args.should_save_model_state:
self._save(output_dir=output_dir, merge_tensor_parallel=merge_tensor_parallel)
else:
if (
self.args.save_checkpoint_format == "unified_checkpoint"
and "async_save" in self.args.unified_checkpoint_config
):
os.makedirs(signal_dir, exist_ok=True)
if self.is_in_train:
global_rank = paddle.distributed.get_rank() if paddle.distributed.get_world_size() > 1 else -1
paddle.save(global_rank, os.path.join(signal_dir, f".model_weight.done.{global_rank}"))
if strtobool(os.getenv("FLAG_LLM_PDC", "False")):
# save model_done file to ensure model is complete
if (
self.args.should_save_model_state
and self.args.should_save
and not ("async_save" in self.args.unified_checkpoint_config)
):
# For ckpt integrity
paddle.save(self.state.global_step, os.path.join(output_dir, ".model_done"))
if (
self.args.save_checkpoint_format == "unified_checkpoint"
and "async_save" in self.args.unified_checkpoint_config
and not self.is_in_train
):
os.makedirs(signal_dir, exist_ok=True)
global_rank = paddle.distributed.get_rank() if paddle.distributed.get_world_size() > 1 else -1
paddle.save(self.state.global_step, os.path.join(signal_dir, f".model_weight.done.{global_rank}"))
def _filter_moe_no_sync_optimizer_params(self):
"""
filter optimizer params which should not sync
"""
state_dict = self.model.state_dict()
optimzier_state_dict = self.optimizer.state_dict()
filter_optimzier_state_dict = OrderedDict()
param_names_in_master_weights = list(optimzier_state_dict["master_weights"].keys()) if self.args.bf16 else []
filter_optimzier_state_dict["master_weights"] = OrderedDict()
for _, v in state_dict.items():
if getattr(v, "no_sync", False):
if v.name in param_names_in_master_weights:
filter_optimzier_state_dict["master_weights"][v.name] = optimzier_state_dict["master_weights"][
v.name
]
for op_k, op_v in optimzier_state_dict.items():
if op_k.startswith(v.name):
filter_optimzier_state_dict[op_k] = op_v
return filter_optimzier_state_dict
def _ordered_save(self, state_dict, save_path, signal_path=None):
group_size = self.args.ordered_save_group_size
hcg = fleet.get_hybrid_communicate_group()
if hcg.get_sharding_parallel_world_size() > 1 or hcg.get_model_parallel_world_size() <= 1:
return paddle.save(state_dict, save_path)
mp_group = hcg.get_model_parallel_group()
ranks = list(mp_group.ranks)
n = len(ranks)
group_num = (n + group_size - 1) // group_size
groups = []
for i in range(group_num):
groups.append([ranks[j] for j in range(i, n, group_num)])
for group in groups:
if dist.get_rank() in group:
paddle.save(state_dict, save_path)
dist.barrier(mp_group)
if signal_path is not None:
with open(signal_path, mode="w+") as f:
f.write("1")
def _save_checkpoint(self, model, metrics=None):
# assert unwrap_model(model) is self.model, "internal model should be a reference to self.model"
if self.args.enable_zero_cost_checkpoint:
return
self.runtime_timer.start("checkpoint saving time")
# Save model checkpoint
checkpoint_folder = f"{PREFIX_CHECKPOINT_DIR}-{self.state.global_step}"
run_dir = self.args.output_dir
run_signal_dir = self.args.output_signal_dir
output_dir = os.path.join(run_dir, checkpoint_folder)
signal_dir = os.path.join(run_signal_dir, checkpoint_folder)
if isinstance(self.model, LoRAModel) and (self.model.quantized or self.args.pipeline_parallel_degree > 1):
self.save_model(output_dir)
elif isinstance(self.model, LoRAModel) or isinstance(self.model, PrefixModelForCausalLM):
self.save_model(output_dir, True)
else:
self.save_model(output_dir)
if self.args.save_checkpoint_format == "flex_checkpoint":
self._save_flex_model_state(output_dir)
# Determine the new best metric / best model checkpoint
if metrics is not None and self.args.metric_for_best_model is not None:
metric_to_check = self.args.metric_for_best_model
if not metric_to_check.startswith("eval_"):
metric_to_check = f"eval_{metric_to_check}"
metric_value = metrics[metric_to_check]
operator = np.greater if self.args.greater_is_better else np.less
if (
self.state.best_metric is None
or self.state.best_model_checkpoint is None
or operator(metric_value, self.state.best_metric)
):
self.state.best_metric = metric_value
self.state.best_model_checkpoint = output_dir
# Save the Trainer state
if self.args.should_save:
self.state.save_to_json(os.path.join(output_dir, TRAINER_STATE_NAME))
if self.args.save_rng_states:
# Save RNG state in non-distributed training
rng_states = {
"python": random.getstate(),
"numpy": np.random.get_state(),
"cuda": paddle.get_rng_state(),
"cpu": paddle.framework.core.default_cpu_generator().get_state(),
"world_size": self.args.world_size,
}
if self.args.use_hybrid_parallel:
rng_states[
"hybrid_parallel_rng_state_tracker"
] = fleet.meta_parallel.get_rng_state_tracker().get_states_tracker()
if self.args.save_rng_states:
rng_state_file = os.path.join(output_dir, f"rng_state_{dist.get_rank()}.pth")
os.makedirs(output_dir, exist_ok=True)
paddle.save(rng_states, rng_state_file)
# only save model state dict, ignore optimizer and scheduler
if not self.args.ignore_save_lr_and_optim:
optimizer_name = _add_variant(PADDLE_OPTIMIZER_NAME, self.args.optimizer_name_suffix)
saved_signal_path = os.path.join(output_dir, f"saved_signal_{dist.get_rank()}")
if self.args.save_checkpoint_format == "unified_checkpoint" and (
self.args.offload_optim or self.args.tensorwise_offload_optimizer
):
self._reload_optimizer()
if self.args.use_hybrid_parallel:
if self.dp_group.rank <= 0 or self.args.use_expert_parallel:
os.makedirs(output_dir, exist_ok=True)
logger.info("Saving optimizer files.")
if self.args.save_checkpoint_format == "unified_checkpoint":
self.unified_checkpoint_handler.save_unified_optimizer(
self.model,
self.optimizer,
output_dir,
signal_dir,
)
else:
if self.args.save_checkpoint_format == "flex_checkpoint":
self._save_flex_optimizer_state(output_dir)
if self.args.should_save:
if self.tokenizer is not None and self.args.save_tokenizer:
self.tokenizer.save_pretrained(output_dir)
# Good practice: save your training arguments together with the trained model
paddle.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME))
else:
if self.dp_group.rank > 0: # this should only work for MoE saving
self._save_ckpt_func(
self._filter_moe_no_sync_optimizer_params(),
os.path.join(output_dir, optimizer_name),
saved_signal_path,
)
else:
state_dict = self.optimizer.state_dict()
save_path = os.path.join(output_dir, optimizer_name)
if self.args.use_async_save:
assert not strtobool(
os.getenv("FLAG_LLM_PDC", "False")
), "Dont support FLAG_LLM_PDC"
self._async_optimizer_saver.run(
state_dict, save_path, saved_signal_path=saved_signal_path
)
else:
self._save_ckpt_func(state_dict, save_path, saved_signal_path)
else:
if (
self.args.save_checkpoint_format == "unified_checkpoint"
and "async_save" in self.args.unified_checkpoint_config
):
global_rank = paddle.distributed.get_rank() if paddle.distributed.get_world_size() > 1 else -1
os.makedirs(signal_dir, exist_ok=True)
paddle.save(global_rank, os.path.join(signal_dir, f".optimizer_weight.done.{global_rank}"))
if (
"skip_save_model_weight" not in self.args.unified_checkpoint_config
or "remove_master_weight" not in self.args.unified_checkpoint_config
):
paddle.save(global_rank, os.path.join(signal_dir, f".master_weight.done.{global_rank}"))
if (
self.args.should_save
or self.args.use_expert_parallel
or (self.args.data_parallel_degree > 1 and self.args.save_checkpoint_format == "flex_checkpoint")
):
if not self.args.use_hybrid_parallel:
logger.info("Saving optimizer files.")
if self.args.save_checkpoint_format == "unified_checkpoint":
self.unified_checkpoint_handler.save_unified_optimizer(
self.model,
self.optimizer,
output_dir,
signal_dir,
)
elif self.args.save_checkpoint_format == "flex_checkpoint":
self._save_flex_optimizer_state(output_dir)
if self.args.should_save:
if self.tokenizer is not None and self.args.save_tokenizer:
self.tokenizer.save_pretrained(output_dir)
# Good practice: save your training arguments together with the trained model
paddle.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME))
else:
if self.args.data_parallel_rank > 0 and self.args.use_expert_parallel:
self._save_ckpt_func(
self._filter_moe_no_sync_optimizer_params(),
os.path.join(output_dir, optimizer_name),
saved_signal_path,
)
else:
self._save_ckpt_func(
self.optimizer.state_dict(),
os.path.join(output_dir, optimizer_name),
saved_signal_path,
)
# FIXME: maybe only save one copy
paddle.save(self.lr_scheduler.state_dict(), os.path.join(output_dir, SCHEDULER_NAME))
if self.do_grad_scaling:
paddle.save(self.scaler.state_dict(), os.path.join(output_dir, SCALER_NAME))
else:
if self.args.save_checkpoint_format == "unified_checkpoint" and not self.args.use_hybrid_parallel:
if "async_save" in self.args.unified_checkpoint_config:
global_rank = paddle.distributed.get_rank() if paddle.distributed.get_world_size() > 1 else -1
os.makedirs(signal_dir, exist_ok=True)
paddle.save(global_rank, os.path.join(signal_dir, f".optimizer_weight.done.{global_rank}"))
if (
"skip_save_model_weight" not in self.args.unified_checkpoint_config
or "remove_master_weight" not in self.args.unified_checkpoint_config
):
paddle.save(global_rank, os.path.join(signal_dir, f".master_weight.done.{global_rank}"))
if self.args.save_checkpoint_format == "unified_checkpoint" and (
self.args.offload_optim or self.args.tensorwise_offload_optimizer
):
self._offload_optimizer()
else:
if self.args.save_checkpoint_format == "flex_checkpoint":
self._save_flex_model_state(output_dir)
if self.args.should_save:
if self.tokenizer is not None and self.args.save_tokenizer:
self.tokenizer.save_pretrained(output_dir)
# Good practice: save your training arguments together with the trained model
paddle.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME))
self.runtime_timer.stop()
# Maybe delete some older checkpoints.
# For hybrid parallel training, the checkpoint files maybe on different node.
need_to_rotate_checkpoints = False
if self.args.use_hybrid_parallel:
if self.dp_group.rank <= 0 or self.args.use_expert_parallel:
need_to_rotate_checkpoints = True
else:
need_to_rotate_checkpoints = self.args.should_save_model_state
# Delete only by one process
need_to_rotate_checkpoints = need_to_rotate_checkpoints and self.args.local_rank == 0
if need_to_rotate_checkpoints:
self._rotate_checkpoints(use_mtime=True, output_dir=run_dir)
self._rotate_checkpoints(use_mtime=True, output_dir=run_signal_dir)
if strtobool(os.getenv("FLAG_LLM_PDC", "False")) and not ("async_save" in self.args.unified_checkpoint_config):
# save checkpoint_done file to ensure checkpoint is complete
if self.args.should_save_model_state and self.args.should_save:
# For ckpt integrity
paddle.save(self.state.global_step, os.path.join(output_dir, ".checkpoint_done"))
def set_optimizer_grouped_parameters(self, optimizer_grouped_parameters=None):
"""
set optimizer grouped parameters:
you can set optimizer_grouped_parameters with whatever arguments on whatever parameters to train.
"""
self.optimizer_grouped_parameters = optimizer_grouped_parameters
def disable_autocast_context_manager(self):
"""
For pure fp16 or pure bf16 training, the paddle.amp.autocast is annoy for always cast fp32 to fp16.
if you networks cast fp16 to fp32 manually to get higher precision, autocast make it not work, since it cast fp32 to fp16 back.
"""
assert self.args.fp16_opt_level == "O2", "disable_autocast_context_manager should only work for pure fp16/bf16"
self.enable_autocast_context_manager = False
def _sorted_checkpoints(
self, output_dir=None, checkpoint_prefix=PREFIX_CHECKPOINT_DIR, use_mtime=False
) -> List[str]:
ordering_and_checkpoint_path = []
glob_checkpoints = [str(x) for x in Path(output_dir).glob(f"{checkpoint_prefix}-*")]
for path in glob_checkpoints:
if use_mtime:
ordering_and_checkpoint_path.append((os.path.getmtime(path), path))
else:
regex_match = re.match(f".*{checkpoint_prefix}-([0-9]+)", path)
if regex_match is not None and regex_match.groups() is not None:
ordering_and_checkpoint_path.append((int(regex_match.groups()[0]), path))
checkpoints_sorted = sorted(ordering_and_checkpoint_path)
checkpoints_sorted = [checkpoint[1] for checkpoint in checkpoints_sorted]
# Make sure we don't delete the best model.
if self.state.best_model_checkpoint is not None:
best_model_index = checkpoints_sorted.index(str(Path(self.state.best_model_checkpoint)))
for i in range(best_model_index, len(checkpoints_sorted) - 2):
checkpoints_sorted[i], checkpoints_sorted[i + 1] = checkpoints_sorted[i + 1], checkpoints_sorted[i]
return checkpoints_sorted
def _rotate_checkpoints(self, use_mtime=False, output_dir=None) -> None:
if self.args.save_total_limit is None or self.args.save_total_limit <= 0:
return
for checkpoint_prefix in [PREFIX_CHECKPOINT_DIR, PREFIX_HF_CHECKPOINT_DIR]:
# Check if we should delete older checkpoint(s)
checkpoints_sorted = self._sorted_checkpoints(
use_mtime=use_mtime, checkpoint_prefix=checkpoint_prefix, output_dir=output_dir
)
if len(checkpoints_sorted) <= self.args.save_total_limit:
return
# If save_total_limit=1 with load_best_model_at_end=True, we could end up deleting the last checkpoint, which
# we don't do to allow resuming.
save_total_limit = self.args.save_total_limit
if (
self.state.best_model_checkpoint is not None
and self.args.save_total_limit == 1
and checkpoints_sorted[-1] != self.state.best_model_checkpoint
):
save_total_limit = 2
number_of_checkpoints_to_delete = max(0, len(checkpoints_sorted) - save_total_limit)
checkpoints_to_be_deleted = checkpoints_sorted[:number_of_checkpoints_to_delete]
for checkpoint in checkpoints_to_be_deleted:
logger.info(f"Deleting older checkpoint [{checkpoint}] due to args.save_total_limit")
# ignore_errors for shared disks between train nodes.
shutil.rmtree(checkpoint, ignore_errors=True)
def _save(
self,
output_dir: Optional[str] = None,
state_dict=None,
merge_tensor_parallel=False,
):
output_dir = output_dir if output_dir is not None else self.args.output_dir
os.makedirs(output_dir, exist_ok=True)
logger.info(f"Saving model checkpoint to {output_dir}")
# signal_dir is used for asynchronous saving situations.
signal_dir = self.args.output_signal_dir
if (
self.args.save_checkpoint_format == "unified_checkpoint"
and "async_save" in self.args.unified_checkpoint_config
):
if PREFIX_CHECKPOINT_DIR in os.path.split(output_dir)[-1]:
signal_dir = os.path.join(signal_dir, os.path.split(output_dir)[-1])
os.makedirs(signal_dir, exist_ok=True)
logger.info(f"Saving model checkpoint finish signal to {signal_dir}")
# Save a trained model and configuration using `save_pretrained()`.
# They can then be reloaded using `from_pretrained()`
if (
strtobool(os.getenv("FLAG_LLM_PDC", "False"))
and paddle.distributed.get_rank() == 0
and self.args.save_checkpoint_format == "unified_checkpoint"
and "async_save" in self.args.unified_checkpoint_config
):
world_size = paddle.distributed.get_world_size()
save_info = {
"world_size": world_size,
"ignore_save_lr_and_optim": self.args.ignore_save_lr_and_optim,
"skip_save_model_weight": "skip_save_model_weight" in self.args.unified_checkpoint_config,
"remove_master_weight": "remove_master_weight" in self.args.unified_checkpoint_config,
}
if os.path.exists(
os.path.join(self.args.output_signal_dir, "async_save_info.json")
): # afs cannot overwrite
os.remove(os.path.join(self.args.output_signal_dir, "async_save_info.json"))
with open(os.path.join(self.args.output_signal_dir, "async_save_info.json"), "w") as f:
json.dump(save_info, f)
if self.args.should_save:
if self.tokenizer is not None and self.args.save_tokenizer:
self.tokenizer.save_pretrained(output_dir)
# Good practice: save your training arguments together with the trained model
paddle.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME))
if self.args.save_checkpoint_format == "unified_checkpoint":
unified_checkpoint_config_backup = self.args.unified_checkpoint_config
# backup and remove unified_checkpoint_config for not trine stage
if not self.is_in_train:
self.args.unified_checkpoint_config = []
self.unified_checkpoint_handler.save_unified_checkpoint(self.model, self.optimizer, output_dir, signal_dir)
# recover unified_checkpoint_config for not trine stage
if not self.is_in_train:
self.args.unified_checkpoint_config = unified_checkpoint_config_backup
return
merge_tensor_parallel = merge_tensor_parallel and self.args.use_hybrid_parallel
# peft model
if (
isinstance(self.model, LoRAModel)
or isinstance(self.model, PrefixModelForCausalLM)
or isinstance(self.model, VeRAModel)
or isinstance(self.model, LoKrModel)
or isinstance(self.model, DisLoRAModel)
or isinstance(self.model, ReFTModel)
):
self.model.save_pretrained(
output_dir,
variant=self.args.weight_name_suffix,
save_function=self._save_ckpt_func,
merge_tensor_parallel=merge_tensor_parallel,
is_main_process=self.args.should_save,
max_shard_size="1024GB",
)
# TODO: @ZHUI unify unwrap_model(self.model) and self.model
elif not isinstance(self.model, PretrainedModel):
if isinstance(unwrap_model(self.model), PretrainedModel):
if self.args.should_save_sharding_stage1_model:
config_to_save = None
state_dict, config_to_save, weight_name_suffix = self.sharding_io.manipulate_state_dict_and_config(
unwrap_model(self.model), merge_tensor_parallel=merge_tensor_parallel
)
unwrap_model(self.model).save_pretrained(
output_dir,
state_dict=state_dict,
config_to_save=config_to_save,
merge_tensor_parallel=merge_tensor_parallel,
variant=weight_name_suffix,
save_function=self._save_ckpt_func,
is_main_process=self.args.should_save,
max_shard_size="1024GB",
)
else:
unwrap_model(self.model).save_pretrained(
output_dir,
merge_tensor_parallel=merge_tensor_parallel,
variant=self.args.weight_name_suffix,
save_function=self._save_ckpt_func,
is_main_process=self.args.should_save,
max_shard_size="1024GB",
)
else:
logger.info("Trainer.model is not a `PretrainedModel`, only saving its state dict.")
if merge_tensor_parallel:
logger.warning("Trainer.model is not a `PretrainedModel`, not support for merge_tensor_parallel.")
if state_dict is None:
state_dict = self.model.state_dict()
if self.args.should_save_sharding_stage1_model:
state_dict, _, _ = self.sharding_io.manipulate_state_dict_and_config(
unwrap_model(self.model), merge_tensor_parallel=False, state_dict=state_dict
)
variant = _add_variant(PADDLE_WEIGHTS_NAME, self.args.sharded_name_suffix())
else:
variant = _add_variant(PADDLE_WEIGHTS_NAME, self.args.weight_name_suffix)
self._save_ckpt_func(state_dict, os.path.join(output_dir, variant))
else:
if isinstance(self.model, PretrainedModel) and self.args.should_save_sharding_stage1_model:
config_to_save = None
self.sharding_io.set_optimizer(self.optimizer)
state_dict, config_to_save, weight_name_suffix = self.sharding_io.manipulate_state_dict_and_config(
self.model, merge_tensor_parallel=merge_tensor_parallel
)
self.model.save_pretrained(
output_dir,
state_dict=state_dict,
config_to_save=config_to_save,
merge_tensor_parallel=merge_tensor_parallel,
variant=weight_name_suffix,
save_function=self._save_ckpt_func,
is_main_process=self.args.should_save,
max_shard_size="1024GB",
)
else:
self.model.save_pretrained(
output_dir,
merge_tensor_parallel=merge_tensor_parallel,
variant=self.args.weight_name_suffix,
save_function=self._save_ckpt_func,
is_main_process=self.args.should_save,
max_shard_size="1024GB",
)
if self.args.should_save_sharding_stage1_model:
model_meta = self.sharding_io.gather_distributed_model_meta()
if self.args.should_save:
path = os.path.join(output_dir, MODEL_META_NAME)
with open(path, "w") as f:
json.dump(model_meta, f)
def _load_scheduler(self, checkpoint):
if checkpoint is None:
self.runtime_timer.stop()
return
if not self.args.ignore_load_lr_and_optim:
if distributed_isfile(os.path.join(checkpoint, SCHEDULER_NAME)):
self.lr_scheduler.set_state_dict(
paddle.load(distributed_file(os.path.join(checkpoint, SCHEDULER_NAME)))
)
else:
raise ValueError(f"scheduler-file not found, scheduler:{os.path.join(checkpoint, SCHEDULER_NAME)}")
if self.do_grad_scaling and distributed_isfile(os.path.join(checkpoint, SCALER_NAME)):
self.scaler.load_state_dict(
paddle.load(distributed_file(os.path.join(checkpoint, SCALER_NAME)), return_numpy=True)
)
def _load_optimizer_and_scheduler(self, checkpoint):
"""If optimizer and scheduler states exist, load them."""
self.runtime_timer.start("checkpoint loading time")
if checkpoint is None:
self.runtime_timer.stop()
return
logger.info("Loading optimizer and scheduler...")
if (not self.args.should_load_sharding_stage1_model) and self.args.ignore_load_lr_and_optim:
self.runtime_timer.stop()
return
opt_state_dict = None
if self.args.should_load_sharding_stage1_model:
opt_state_dict = self.sharding_io.load_optimizer_state_with_reshard(
checkpoint, PADDLE_OPTIMIZER_NAME, self.model_wrapped
)
else:
use_unified_checkpoint = False
if self.args.load_checkpoint_format == "unified_checkpoint":
if self.is_unified_checkpoint(checkpoint):
use_unified_checkpoint = True
else:
logger.info("Loading checkpoint, the next checkpoint will be saved as unified checkpoint")
if not use_unified_checkpoint:
if self.args.data_parallel_rank == 0 or self.args.use_expert_parallel:
optimizer_name = _add_variant(PADDLE_OPTIMIZER_NAME, self.args.optimizer_name_suffix)
path = os.path.join(checkpoint, optimizer_name)
if os.path.isfile(path):
opt_state_dict = paddle.load(path)
else:
opt_state_dict = None
else:
model = self.model
if (
hasattr(self.args, "enable_sharding_comm_overlap")
and self.args.enable_sharding_comm_overlap
and "split_param" in split_parallel_config(self.args.sharding_parallel_config)
):
model = self.model_wrapped
opt_state_dict = self.unified_checkpoint_handler.load_unified_optimizer(
model=model,
optimizer=self.optimizer,
resume_from_checkpoint=checkpoint,
)
if self.args.ignore_load_lr_and_optim and opt_state_dict:
tmp = self.optimizer.state_dict()
tmp["master_weights"] = opt_state_dict["master_weights"]
opt_state_dict = tmp
# broadcast optimizer state in dp group
if self.args.local_rank != -1:
dist.barrier()
if self.args.use_expert_parallel:
opt_state_dict = broadcast_moe_optimizer(
opt_state_dict,
model_state_dict=self.model.state_dict(),
broadcast_dp=not self.args.should_load_sharding_stage1_model,
)
else:
if not self.args.should_load_sharding_stage1_model:
opt_state_dict = broadcast_dp_optimizer(opt_state_dict)
if opt_state_dict is not None:
# Load in optimizer and scheduler states
self.optimizer.set_state_dict(opt_state_dict)
else:
optimizer_name = _add_variant(PADDLE_OPTIMIZER_NAME, self.args.optimizer_name_suffix)
raise ValueError(f"optimizer-state-dict not found, opt: {os.path.join(checkpoint, optimizer_name)}.")
self._load_scheduler(checkpoint)
if self.args.offload_optim:
logger.info("Offloading optimizer state...")
self._offload_optimizer()
self.runtime_timer.stop()
def log(self, logs: Dict[str, float], **kwargs) -> None:
"""
Log `logs` on the various objects watching training.
Subclass and override this method to inject custom behavior.
Args:
logs (`Dict[str, float]`):
The values to log.
"""
try:
from paddle.distributed.fleet.utils.timer_helper import (
get_timers as paddle_get_timers,
)
paddle_pipeline_timers = paddle_get_timers()
except ImportError: # paddle version too old, timer not support
warnings.warn(f"paddle version:{paddle.__git_commit__} does not support pipeline timer")
paddle_pipeline_timers = None
except AssertionError:
paddle_pipeline_timers = None
kwargs.update(
timer=self.timers, paddle_pipeline_timers=paddle_pipeline_timers, metrics_dumper=self.metrics_dumper
)
if self.state.epoch is not None:
logs["progress_or_epoch"] = round(self.state.epoch, 4)
if self.timers:
logs.update(self.timers.info(self.timers.timers.keys()))
mem_info = psutil.virtual_memory()
logs["cpu_used_memory"] = round(mem_info.used / (1024**3), 2)
logs["cpu_available_memory"] = round(mem_info.available / (1024**3), 2)
self.state.log_history = []
self.control = self.callback_handler.on_log(self.args, self.state, self.control, logs, **kwargs)
def evaluate(
self,
eval_dataset: Optional[Dataset] = None,
ignore_keys: Optional[List[str]] = None,
metric_key_prefix: str = "eval",
) -> Dict[str, float]:
"""
Run evaluation and returns metrics.
The calling script will be responsible for providing a method to compute metrics, as they are task-dependent
(pass it to the init `compute_metrics` argument).
You can also subclass and override this method to inject custom behavior.
Args:
eval_dataset (`Dataset`, *optional*):
Pass a dataset if you wish to override `self.eval_dataset`. If it is an `datasets.Dataset`, columns not
accepted by the `model.forward()` method are automatically removed. It must implement the `__len__`
method.
ignore_keys (`Lst[str]`, *optional*):
A list of keys in the output of your model (if it is a dictionary) that should be ignored when
gathering predictions.
metric_key_prefix (`str`, *optional*, defaults to `"eval"`):
An optional prefix to be used as the metrics key prefix. For example the metrics "bleu" will be named
"eval_bleu" if the prefix is "eval" (default)
Returns:
A dictionary containing the evaluation loss and the potential metrics computed from the predictions. The
dictionary also contains the epoch number which comes from the training state.
"""
# memory metrics - must set up as early as possible
self._memory_tracker.start()
eval_dataloader = self.get_eval_dataloader(eval_dataset)
start_time = time.time()
output = self.evaluation_loop(
eval_dataloader,
description="Evaluation",
# No point gathering the predictions if there are no metrics, otherwise we defer to
# self.args.prediction_loss_only
prediction_loss_only=True if self.compute_metrics is None else None,
ignore_keys=ignore_keys,
metric_key_prefix=metric_key_prefix,
max_eval_iters=self.args.max_evaluate_steps,
)
total_batch_size = self.args.eval_batch_size * self.args.dataset_world_size
output.metrics.update(
speed_metrics(
metric_key_prefix,
start_time,
num_samples=output.num_samples,
num_steps=math.ceil(output.num_samples / total_batch_size),
)
)
self.log(output.metrics)
self.control = self.callback_handler.on_evaluate(self.args, self.state, self.control, output.metrics)
self._memory_tracker.stop_and_update_metrics(output.metrics)
return output.metrics
def evaluation_loop(
self,
dataloader: DataLoader,
description: str,
prediction_loss_only: Optional[bool] = None,
ignore_keys: Optional[List[str]] = None,
metric_key_prefix: str = "eval",
max_eval_iters: Optional[int] = -1,
) -> EvalLoopOutput:
"""
Prediction/evaluation loop, shared by `Trainer.evaluate()` and `Trainer.predict()`.
Works both with or without labels.
"""
args = self.args
prediction_loss_only = prediction_loss_only if prediction_loss_only is not None else args.prediction_loss_only
if self.args.pipeline_parallel_degree > 1:
from paddle.distributed.fleet.meta_parallel import PipelineLayer
_prepare_pipeline_inputs_func = getattr(self.model_wrapped, "_prepare_pipeline_inputs_func", None)
# Only accept wrapped model for pipeline_parallel mode
if self.model is self.model_wrapped and isinstance(self.model_wrapped, PipelineLayer):
# NOTE(gongenlei): when do_train=False, do_eval=True, we need to wrap model for pipeline
self.model_wrapped = fleet.distributed_model(self.model_wrapped)
if isinstance(self.model_wrapped, LoRAModel) and isinstance(self.model_wrapped.model, PipelineLayer):
# NOTE(liuting): when do_train=False, do_eval=True, lora=True, we need to wrap model for pipeline
self.model_wrapped = fleet.distributed_model(self.model_wrapped.model)
model = self.model_wrapped
if _prepare_pipeline_inputs_func is not None:
model._prepare_pipeline_inputs_func = _prepare_pipeline_inputs_func
else:
model = self.model
if isinstance(dataloader, paddle.io.DataLoader):
batch_size = dataloader.batch_sampler.batch_size
elif isinstance(dataloader, _DataLoaderIterBase):
# support for inner dataloader
batch_size = dataloader._batch_sampler.batch_size
# alias for inner dataloader
dataloader.dataset = dataloader._dataset
else:
raise ValueError("Only support for paddle.io.DataLoader")
num_samples = None
if max_eval_iters > 0:
# on eval limit steps
num_samples = batch_size * self.args.dataset_world_size * max_eval_iters
if isinstance(dataloader, _DataLoaderIterBase) and isinstance(
dataloader._batch_sampler, NlpDistributedBatchSampler
):
consumed_samples = (
((self.state.global_step) // args.eval_steps)
* max_eval_iters
* args.per_device_eval_batch_size
* args.dataset_world_size
)
dataloader._batch_sampler.set_epoch(consumed_samples=consumed_samples)
logger.info(f"***** Running {description} *****")
if not self.args.distributed_dataloader or (
self.args.distributed_dataloader and self.args.should_load_dataset
):
if has_length(dataloader):
logger.info(f" Num examples = {self.num_examples(dataloader)}")
if max_eval_iters > 0:
logger.info(f" Total prediction steps = {max_eval_iters}")
else:
logger.info(f" Total prediction steps = {len(dataloader)}")
else:
logger.info(" Num examples: Unknown")
if max_eval_iters > 0:
logger.info(f" Total prediction steps = {max_eval_iters}")
logger.info(f" Pre device batch size = {batch_size}")
logger.info(f" Total Batch size = {batch_size * self.args.dataset_world_size}")
model.eval()
self.callback_handler.eval_dataloader = dataloader
# Do this before wrapping.
eval_dataset = dataloader.dataset
if args.past_index >= 0:
self._past = None
# Initialize containers
# losses/preds/labels on GPU (accumulated for eval_accumulation_steps)
losses_host = None
preds_host = None
labels_host = None
# losses/preds/labels on CPU (final containers)
all_losses = None
all_preds = None
all_labels = None
# Will be useful when we have an iterable dataset so don't know its length.
observed_num_examples = 0
# Main evaluation loop
losses = []
for step, inputs in enumerate(dataloader):
# Update the observed num examples
observed_batch_size = find_batch_size(inputs)
if observed_batch_size is not None:
observed_num_examples += observed_batch_size
# For batch samplers, batch_size is not known by the dataloader in advance.
batch_size = observed_batch_size
# Prediction step
loss, logits, labels = self.prediction_step(model, inputs, prediction_loss_only, ignore_keys=ignore_keys)
# Update containers on host
if loss is not None:
# losses = self._nested_gather(loss.repeat(batch_size))
losses = self._nested_gather(paddle.tile(loss, repeat_times=[batch_size, 1]))
losses_host = losses if losses_host is None else paddle.concat((losses_host, losses), axis=0)
if labels is not None:
labels = self._pad_across_processes(labels)
labels = self._nested_gather(labels)
labels_host = labels if labels_host is None else nested_concat(labels_host, labels, padding_index=-100)
if logits is not None:
logits = self._pad_across_processes(logits)
logits = self._nested_gather(logits)
if self.preprocess_logits_for_metrics is not None:
logits = self.preprocess_logits_for_metrics(logits, labels)
preds_host = logits if preds_host is None else nested_concat(preds_host, logits, padding_index=-100)
self.control = self.callback_handler.on_prediction_step(args, self.state, self.control)
# Gather all tensors and put them back on the CPU if we have done enough accumulation steps.
if args.eval_accumulation_steps is not None and (step + 1) % args.eval_accumulation_steps == 0:
if losses_host is not None:
losses = nested_numpify(losses_host)
all_losses = losses if all_losses is None else np.concatenate((all_losses, losses), axis=0)
if preds_host is not None:
logits = nested_numpify(preds_host)
all_preds = logits if all_preds is None else nested_concat(all_preds, logits, padding_index=-100)
if labels_host is not None:
labels = nested_numpify(labels_host)
all_labels = (
labels if all_labels is None else nested_concat(all_labels, labels, padding_index=-100)
)
# Set back to None to begin a new accumulation
losses_host, preds_host, labels_host = None, None, None
if max_eval_iters > 0 and step >= max_eval_iters - 1:
break
# Gather all remaining tensors and put them back on the CPU
if losses_host is not None:
losses = nested_numpify(losses_host)
all_losses = losses if all_losses is None else np.concatenate((all_losses, losses), axis=0)
if preds_host is not None:
logits = nested_numpify(preds_host)
all_preds = logits if all_preds is None else nested_concat(all_preds, logits, padding_index=-100)
if labels_host is not None:
labels = nested_numpify(labels_host)
all_labels = labels if all_labels is None else nested_concat(all_labels, labels, padding_index=-100)
# Number of samples
if num_samples is not None:
pass
elif has_length(eval_dataset):
num_samples = len(eval_dataset)
# The instance check is weird and does not actually check for the type, but whether the dataset has the right
# methods. Therefore we need to make sure it also has the attribute.
elif isinstance(eval_dataset, IterableDatasetShard) and hasattr(eval_dataset, "num_examples"):
num_samples = eval_dataset.num_examples
else:
if has_length(dataloader):
num_samples = self.num_examples(dataloader)
else: # both len(dataloader.dataset) and len(dataloader) fail
num_samples = observed_num_examples
# Number of losses has been rounded to a multiple of batch_size and in a distributed training, the number of
# samplers has been rounded to a multiple of batch_size, so we truncate.
if all_losses is not None:
all_losses = all_losses[: num_samples * int(self.args.world_size / self.args.dataset_world_size)]
if all_preds is not None:
all_preds = nested_truncate(
all_preds, num_samples * int(self.args.world_size / self.args.dataset_world_size)
)
if all_labels is not None:
all_labels = nested_truncate(
all_labels, num_samples * int(self.args.world_size / self.args.dataset_world_size)
)
model.train()
# Metrics!
if self.compute_metrics is not None and all_preds is not None and all_labels is not None:
# all_labels maybe is a tuple when prediction_steps output label_mask
batch_labels = all_labels[0] if isinstance(all_labels, (list, tuple)) else all_labels
metrics = self.compute_metrics(EvalPrediction(predictions=all_preds, label_ids=batch_labels))
else:
metrics = {}
if all_losses is not None:
metrics[f"{metric_key_prefix}_loss"] = all_losses.mean().item()
# Prefix all keys with metric_key_prefix + '_'
for key in list(metrics.keys()):
if not key.startswith(f"{metric_key_prefix}_"):
metrics[f"{metric_key_prefix}_{key}"] = metrics.pop(key)
return EvalLoopOutput(predictions=all_preds, label_ids=all_labels, metrics=metrics, num_samples=num_samples)
def predict(
self, test_dataset: Dataset, ignore_keys: Optional[List[str]] = None, metric_key_prefix: str = "test"
) -> PredictionOutput:
"""
Run prediction and returns predictions and potential metrics.
Depending on the dataset and your use case, your test dataset may contain labels. In that case, this method
will also return metrics, like in `evaluate()`.
Args:
test_dataset (`Dataset`):
Dataset to run the predictions on. If it is an `datasets.Dataset`, columns not accepted by the
`model.forward()` method are automatically removed. Has to implement the method `__len__`
ignore_keys (`Lst[str]`, *optional*):
A list of keys in the output of your model (if it is a dictionary) that should be ignored when
gathering predictions.
metric_key_prefix (`str`, *optional*, defaults to `"test"`):
An optional prefix to be used as the metrics key prefix. For example the metrics "bleu" will be named
"test_bleu" if the prefix is "test" (default)
<Tip>
If your predictions or labels have different sequence length (for instance because you're doing dynamic padding
in a token classification task) the predictions will be padded (on the right) to allow for concatenation into
one array. The padding index is -100.
</Tip>
Returns: *NamedTuple* A namedtuple with the following keys:
- predictions (`np.ndarray`): The predictions on `test_dataset`.
- label_ids (`np.ndarray`, *optional*): The labels (if the dataset contained some).
- metrics (`Dict[str, float]`, *optional*): The potential dictionary of metrics (if the dataset contained
labels).
"""
# memory metrics - must set up as early as possible
self._memory_tracker.start()
test_dataloader = self.get_test_dataloader(test_dataset)
start_time = time.time()
eval_loop = self.evaluation_loop
output = eval_loop(
test_dataloader,
description="Prediction",
ignore_keys=ignore_keys,
prediction_loss_only=True if self.compute_metrics is None else None,
metric_key_prefix=metric_key_prefix,
max_eval_iters=self.args.max_evaluate_steps,
)
total_batch_size = self.args.per_device_eval_batch_size * self.args.dataset_world_size
output.metrics.update(
speed_metrics(
metric_key_prefix,
start_time,
num_samples=output.num_samples,
num_steps=math.ceil(output.num_samples / total_batch_size),
)
)
self._memory_tracker.stop_and_update_metrics(output.metrics)
return PredictionOutput(predictions=output.predictions, label_ids=output.label_ids, metrics=output.metrics)
def prediction_pipeline_step(
self,
model: nn.Layer,
inputs: Dict[str, Union[paddle.Tensor, Any]],
prediction_loss_only: bool,
ignore_keys: Optional[List[str]] = None,
) -> Tuple[Optional[paddle.Tensor], Optional[paddle.Tensor], Optional[paddle.Tensor]]:
"""
prediction_step function for pipeline parallel mode.
"""
if hasattr(model, "_prepare_pipeline_inputs_func"):
inputs, labels = model._prepare_pipeline_inputs_func(inputs)
has_labels = labels is not None
else:
has_labels = all(inputs.get(k) is not None for k in self.label_names)
inputs = self._prepare_inputs(inputs)
# labels may be popped when computing the loss (label smoothing for instance) so we grab them first.
if has_labels:
labels = nested_detach(tuple(inputs.get(name) for name in self.label_names))
if len(labels) == 1:
labels = labels[0]
else:
labels = None
inputs = inputs.pop("input_ids")
# train & eval share the same p2p_helper, so clear it before and after each step
model._p2p_helper.clear_meta_cache()
with paddle.no_grad():
if has_labels:
with self.autocast_smart_context_manager():
loss = model.eval_batch([inputs, labels], compute_loss=True)
# loss, outputs = self.compute_loss(model, inputs, return_outputs=True)
loss = loss.mean().detach()
else:
raise ValueError("pipeline mode eval need label!")
# train & eval share the same p2p_helper, so clear it before and after each step
model._p2p_helper.clear_meta_cache()
return (loss, None, labels)
def prediction_step(
self,
model: nn.Layer,
inputs: Dict[str, Union[paddle.Tensor, Any]],
prediction_loss_only: bool,
ignore_keys: Optional[List[str]] = None,
) -> Tuple[Optional[paddle.Tensor], Optional[paddle.Tensor], Optional[paddle.Tensor]]:
"""
Perform an evaluation step on `model` using `inputs`.
Subclass and override to inject custom behavior.
Args:
model (`nn.Layer`):
The model to evaluate.
inputs (`Dict[str, Union[paddle.Tensor, Any]]`):
The inputs and targets of the model.
The dictionary will be unpacked before being fed to the model. Most models expect the targets under the
argument `labels`. Check your model's documentation for all accepted arguments.
prediction_loss_only (`bool`):
Whether or not to return the loss only.
ignore_keys (`Lst[str]`, *optional*):
A list of keys in the output of your model (if it is a dictionary) that should be ignored when
gathering predictions.
Return:
Tuple[Optional[paddle.Tensor], Optional[paddle.Tensor], Optional[paddle.Tensor]]: A tuple with the loss,
logits and labels (each being optional).
"""
if self.args.pipeline_parallel_degree > 1:
# hack for pipeline mode
inputs = self._prepare_inputs(inputs)
return self.prediction_pipeline_step(model, inputs, prediction_loss_only, ignore_keys)
has_labels = all(inputs.get(k) is not None for k in self.label_names)
inputs = self._prepare_inputs(inputs)
if ignore_keys is None:
if hasattr(self.model, "config"):
ignore_keys = getattr(self.model.config, "keys_to_ignore_at_inference", [])
else:
ignore_keys = []
# labels may be popped when computing the loss (label smoothing for instance) so we grab them first.
if has_labels:
labels = nested_detach(tuple(inputs.get(name) for name in self.label_names))
if len(labels) == 1:
labels = labels[0]
else:
labels = None
with paddle.no_grad():
if has_labels:
with self.autocast_smart_context_manager():
loss, outputs = self.compute_loss(model, inputs, return_outputs=True)
loss = loss.mean().detach()
if isinstance(outputs, dict):
logits = tuple(v for k, v in outputs.items() if k not in ignore_keys + ["loss"])
else:
logits = outputs[1:]
else:
loss = None
with self.autocast_smart_context_manager():
outputs = model(**inputs)
if isinstance(outputs, dict):
logits = tuple(v for k, v in outputs.items() if k not in ignore_keys)
else:
logits = outputs
# TODO: this needs to be fixed and made cleaner later.
if self.args.past_index >= 0:
self._past = outputs[self.args.past_index - 1]
if prediction_loss_only:
return (loss, None, None)
logits = nested_detach(logits)
if isinstance(logits, (list, tuple)) and len(logits) == 1:
logits = logits[0]
return (loss, logits, labels)
def is_local_process_zero(self) -> bool:
"""
Whether or not this process is the local (e.g., on one machine if training in a distributed fashion on several
machines) main process.
"""
return self.args.local_process_index == 0
def is_world_process_zero(self) -> bool:
"""
Whether or not this process is the global main process (when training in a distributed fashion on several
machines, this is only going to be `True` for one process).
"""
return self.args.process_index == 0
def _nested_gather(self, tensors):
"""
Gather value of `tensors` (tensor or list/tuple of nested tensors) and convert them to numpy before
concatenating them to `gathered`
"""
if tensors is None:
return
if self.args.local_rank != -1:
tensors = distributed_concat(tensors)
return tensors
# Copied from Accelerate.
def _pad_across_processes(self, tensor, pad_index=-100):
"""
Recursively pad the tensors in a nested list/tuple/dictionary of tensors from all devices to the same size so
they can safely be gathered.
"""
if isinstance(tensor, (list, tuple)):
return type(tensor)(self._pad_across_processes(t, pad_index=pad_index) for t in tensor)
elif isinstance(tensor, dict):
return type(tensor)({k: self._pad_across_processes(v, pad_index=pad_index) for k, v in tensor.items()})
elif not isinstance(tensor, paddle.Tensor):
raise TypeError(
f"Can't pad the values of type {type(tensor)}, only of nested list/tuple/dicts of tensors."
)
if len(tensor.shape) < 2:
return tensor
# Gather all sizes
size = paddle.to_tensor(tensor.shape)[None]
sizes = self._nested_gather(size).cpu()
max_size = max(s[1] for s in sizes)
if tensor.shape[1] == max_size:
return tensor
# Then pad to the maximum size
old_size = tensor.shape
new_size = list(old_size)
new_size[1] = max_size
# new_tensor = tensor.new_zeros(tuple(new_size)) + pad_index
new_tensor = paddle.zeros(tuple(new_size), dtype=tensor.dtype) + pad_index
new_tensor[:, : old_size[1]] = tensor
return new_tensor
def _set_signature_columns_if_needed(self):
if self._signature_columns is None:
# Inspect model forward signature to keep only the arguments it accepts.
signature = inspect.signature(self.model.forward)
self._signature_columns = list(signature.parameters.keys())
# Labels may be named label or label_ids, the default data collator handles that.
self._signature_columns += list(set(["label", "label_ids"] + self.label_names))
def _remove_unused_columns(self, dataset: "datasets.Dataset", description: Optional[str] = None):
if not self.args.remove_unused_columns or self.model is None:
return dataset
if self._signature_columns is None:
# Inspect model forward signature to keep only the arguments it accepts.
signature = inspect.signature(self.model.forward)
self._signature_columns = list(signature.parameters.keys())
# Labels may be named label or label_ids, the default data collator handles that.
self._signature_columns += ["label", "label_ids", "labels", "start_positions", "end_positions"]
ignored_columns = list(set(dataset.column_names) - set(self._signature_columns))
if len(ignored_columns) > 0:
dset_description = "" if description is None else f"in the {description} set "
logger.info(
f"The following columns {dset_description} don't have a corresponding argument in "
f"`{self.model.__class__.__name__}.forward` and have been ignored: {', '.join(ignored_columns)}."
f" If {', '.join(ignored_columns)} are not expected by `{self.model.__class__.__name__}.forward`, "
f" you can safely ignore this message."
)
columns = [k for k in self._signature_columns if k in dataset.column_names]
if version.parse(datasets.__version__) < version.parse("1.4.0"):
dataset.set_format(
type=dataset.format["type"], columns=columns, format_kwargs=dataset.format["format_kwargs"]
)
return dataset
else:
return dataset.remove_columns(ignored_columns)
def _get_collator_with_removed_columns(
self, data_collator: Callable, description: Optional[str] = None
) -> Callable:
"""Wrap the data collator in a callable removing unused columns."""
if not self.args.remove_unused_columns:
return data_collator
self._set_signature_columns_if_needed()
signature_columns = self._signature_columns
remove_columns_collator = RemoveColumnsCollator(
data_collator=data_collator,
signature_columns=signature_columns,
logger=logger,
description=description,
model_name=self.model.__class__.__name__,
)
return remove_columns_collator
def _is_iterable_dataset(self, dataset):
return isinstance(dataset, paddle.io.IterableDataset)
def _is_iterable_dataset_distributed(self, dataset):
# For distributed dataloaer.
is_iterable_dataset_tensor = paddle.to_tensor(self._is_iterable_dataset(dataset)).astype("int32").reshape([1])
if dist.get_world_size() > 1:
dist.all_reduce(is_iterable_dataset_tensor, op=dist.ReduceOp.MAX)
if is_iterable_dataset_tensor.item() == 1:
return True
return False
def print_config(self, args=None, key=""):
"""
print config values
"""
logger.debug("=" * 60)
if args is None:
args = self.args
key = "Training"
import paddlenlp
logger.debug("{:^40}".format("{} Configuration Arguments".format(key)))
logger.debug("{:30}: {}".format("paddle commit id", paddle.version.commit))
logger.debug("{:30}: {}".format("paddlenlp commit id", paddlenlp.version.commit))
for a in dir(args):
if a[:2] != "__": # don't print double underscore methods
v = getattr(args, a)
if not isinstance(v, types.MethodType):
logger.debug("{:30}: {}".format(a, v))
logger.debug("")
def is_unified_checkpoint(self, resume_from_checkpoint, safe_serialization=True):
is_unified_checkpoint_type = False
if isinstance(self.model, LoRAModel) or isinstance(self.model, PrefixModelForCausalLM):
weights_index_name = (
PADDLE_PEFT_WEIGHTS_INDEX_NAME if not safe_serialization else SAFE_PEFT_WEIGHTS_INDEX_NAME
)
else:
weights_index_name = PADDLE_WEIGHTS_INDEX_NAME if not safe_serialization else SAFE_WEIGHTS_INDEX_NAME
master_weights_index_name = (
PADDLE_MASTER_WEIGHTS_INDEX_NAME if not safe_serialization else SAFE_MASTER_WEIGHTS_INDEX_NAME
)
weights_index_file = os.path.join(
resume_from_checkpoint,
weights_index_name,
)
master_weights_index_file = os.path.join(
resume_from_checkpoint,
master_weights_index_name,
)
if distributed_isfile(weights_index_file) or distributed_isfile(master_weights_index_file):
is_unified_checkpoint_type = True
return is_unified_checkpoint_type