Files
microsoft--unilm/PFPO/trainer_base_ds_mul_fs_tp.py
2026-07-13 13:24:13 +08:00

503 lines
21 KiB
Python

# coding=utf-8
#
# Copyright 2023 Nanyang Technological University Fangkai Jiao
#
# Part of this code is based on the source code of Transformers
# (arXiv:1910.03771)
#
# 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.
import datetime
import glob
import logging
import os
import sys
from typing import Dict, Union
import deepspeed
import fairscale.nn.model_parallel.initialize as mpu
import hydra
import torch
import wandb
from deepspeed import comm as dist
from fairscale.nn.model_parallel.initialize import initialize_model_parallel
from omegaconf import DictConfig, OmegaConf
from torch import distributed as dist
from torch.utils.data import DataLoader, RandomSampler, SequentialSampler
from tqdm import tqdm, trange
from transformers import AutoTokenizer, PreTrainedTokenizer, PreTrainedModel
from general_util.dist_utils import get_pipeline_parallel_rank, get_pipeline_parallel_world_size, prepare_distributed_sampler
from general_util.evaluator import evaluate
from general_util.logger import setting_logger
from general_util.training_utils import batch_to_device, set_seed, note_best_checkpoint, load_and_cache_examples, set_seed_int, \
organize_multiple_dataset, get_last_checkpoint
logger: logging.Logger
torch.backends.cuda.matmul.allow_tf32 = True
torch._dynamo.config.capture_scalar_outputs = True # Updated since 2024/12/02, torch 2.5.1
torch._inductor.config.realize_opcount_threshold = 100 # https://github.com/pytorch/pytorch/issues/135028 AMD MI300x workaround
GLOBAL_SEED = 1
GLOBAL_WORKER_ID = None
def get_zero_stage(cfg: DictConfig):
if hasattr(cfg, "zero_optimization"):
return int(getattr(cfg.zero_optimization, "stage", 0))
return 0
def worker_init_fn(worker_id):
global GLOBAL_WORKER_ID
GLOBAL_WORKER_ID = worker_id
set_seed_int(GLOBAL_SEED + worker_id)
def save_model(model: Union[deepspeed.DeepSpeedEngine, deepspeed.PipelineEngine],
cfg: DictConfig, output_dir: str, tokenizer: PreTrainedTokenizer = None, state_dict: Dict = None):
unwrapped_model = model.module
assert isinstance(unwrapped_model, PreTrainedModel)
save_ds_state = getattr(cfg, "save_ds_state", True)
zero_stage = get_zero_stage(cfg.ds_cfg)
if not save_ds_state:
if zero_stage == 3:
logger.warning("Deepspeed ZeRO-3 has to save checkpoint states since the model is sharded.")
saving_ds_state = True
if save_ds_state:
model.save_checkpoint(cfg.output_dir)
if zero_stage == 3:
state_dict = model._zero3_consolidated_16bit_state_dict()
else:
state_dict = model.module.state_dict()
if mpu.model_parallel_is_initialized():
dp_rank = mpu.get_data_parallel_rank()
else:
if dist.is_initialized():
dp_rank = dist.get_rank()
else:
dp_rank = -1
if dist.is_initialized() and dp_rank != 0:
dist.barrier()
if dp_rank in [-1, 0]:
unwrapped_model.save_pretrained(output_dir, state_dict=state_dict, safe_serialization=False)
# if cfg.local_rank in [-1, 0]:
if tokenizer is not None:
tokenizer.save_pretrained(output_dir)
OmegaConf.save(cfg, os.path.join(output_dir, "training_config.yaml"))
logger.info("Saving model checkpoint to %s", output_dir)
if dist.is_initialized():
dist.barrier()
def forward_step(model, inputs: Dict[str, torch.Tensor]):
outputs = model(**inputs)
if isinstance(outputs, tuple):
loss = outputs[0]
else:
loss = outputs["loss"]
model.backward(loss)
model.step()
return loss.item(), outputs
def train(cfg, model, tokenizer, continue_from_global_step=0):
""" Train the model """
if cfg.local_rank in [-1, 0]:
tb_helper = hydra.utils.instantiate(cfg.summary_helper) if "summary_helper" in cfg and cfg.summary_helper else None
else:
tb_helper = None
cfg.train_batch_size = cfg.per_gpu_train_batch_size
files, total_dataset_len = organize_multiple_dataset(cfg, tokenizer, _split="train")
logger.warning(f"Rank No. {dist.get_rank()} has {total_dataset_len} samples.")
if getattr(cfg, "do_preprocess", False):
return
if "extended_vocab" in cfg and cfg.extended_vocab:
logger.info(f"Extended extra vocab size: {cfg.extended_vocab}")
model.resize_token_embeddings(model.config.vocab_size + cfg.extended_vocab)
dp_degree = cfg.dp_size
_actual_train_batch_size = cfg.train_batch_size * cfg.gradient_accumulation_steps * dp_degree
if cfg.max_steps > 0:
t_total = cfg.max_steps
cfg.num_train_epochs = cfg.max_steps // (total_dataset_len // _actual_train_batch_size) + 1
else:
t_total = total_dataset_len // _actual_train_batch_size * cfg.num_train_epochs
num_warmup_steps = int(t_total * cfg.warmup_proportion) if cfg.warmup_proportion else cfg.warmup_steps
ds_config = cfg.ds_cfg
if "total_num_steps" in ds_config.scheduler.params:
ds_config.scheduler.params.total_num_steps = t_total
ds_config.scheduler.params.warmup_num_steps = num_warmup_steps
ds_config = OmegaConf.to_container(ds_config, resolve=True)
optimizer = hydra.utils.instantiate(cfg.optimizer, model) if getattr(cfg, "optimizer", None) else None
if torch.__version__ >= "2" and (getattr(os.environ, "TORCH_COMPILE", False) or getattr(cfg, "compile", False)):
model = torch.compile(model, mode="max-autotune")
model, optimizer, _, scheduler = deepspeed.initialize(model=model,
model_parameters=[p for p in model.parameters() if p.requires_grad],
config=ds_config,
mpu=mpu if mpu.model_parallel_is_initialized() else None,
optimizer=optimizer)
# model.compile()
logger.info(optimizer.optimizer)
if hasattr(cfg, "ds_ref_model"):
logger.info("Lazy initialize deepspeed engine for reference model") # due to hpz distributed group setting, we need lazy initialize the engine
ref_model = hydra.utils.instantiate(cfg.ds_ref_model)
model.deepspeed_set_ref_engine_lazy(ref_model)
unwrapped_model = model.module
assert isinstance(unwrapped_model, PreTrainedModel)
# Train!
logger.info("***** Running training *****")
logger.info(" Num examples = %d", total_dataset_len)
logger.info(" Num Epochs = %d", cfg.num_train_epochs)
logger.info(" Instantaneous batch size per GPU = %d", cfg.per_gpu_train_batch_size)
logger.info(" Total train batch size (w. parallel, distributed & accumulation) = %d", _actual_train_batch_size)
logger.info(" Gradient Accumulation steps = %d", cfg.gradient_accumulation_steps)
logger.info(" Total optimization steps = %d", t_total)
logger.info(" Warmup steps = %d", num_warmup_steps)
if continue_from_global_step > 0:
from deepspeed.runtime.fp16.loss_scaler import LossScaler
from deepspeed.runtime.zero.config import ZeroStageEnum, DeepSpeedZeroConfig
from deepspeed.utils.tensor_fragment import fragment_address
torch.serialization.add_safe_globals([LossScaler, ZeroStageEnum, DeepSpeedZeroConfig, fragment_address])
logger.info("Fast forwarding to global step %d to resume training from latest checkpoint...", continue_from_global_step)
# resume = os.path.dirname(cfg.resume)
model.load_checkpoint(cfg.output_dir)
if cfg.local_rank == -1 or dist.get_rank() == 0:
wandb.init(
project=getattr(cfg, "wandb_project", "code-enhancement"),
name=cfg.exp_name,
notes=cfg.exp_notes,
config=OmegaConf.to_container(cfg, resolve=True),
settings=wandb.Settings(_service_wait=300)
)
wandb.define_metric(cfg.prediction_cfg.metric, summary=("max" if cfg.prediction_cfg.measure > 0 else "min"))
global_step = 0
tr_loss, logging_loss = 0.0, 0.0
train_iterator = trange(int(cfg.num_train_epochs), desc="Epoch", disable=cfg.local_rank not in [-1, 0])
dist.barrier()
set_seed(cfg) # Added here for reproducibility (even between python 2 and 3)
for epoch in train_iterator:
for _file in files:
dist.barrier()
sub_train_dataset = load_and_cache_examples(cfg, tokenizer, _split="train", _file=_file)
if cfg.local_rank == -1:
if getattr(cfg, "shuffle_dataset", True):
sub_train_sampler = RandomSampler(sub_train_dataset)
else:
sub_train_sampler = SequentialSampler(sub_train_dataset)
else:
sub_train_sampler = prepare_distributed_sampler(sub_train_dataset, cfg.seed, getattr(cfg, "shuffle_dataset", True))
sub_train_collator = hydra.utils.instantiate(cfg.collator) if "collator" in cfg and cfg.collator else None
sub_train_dataloader = DataLoader(dataset=sub_train_dataset,
sampler=sub_train_sampler,
batch_size=cfg.train_batch_size,
collate_fn=sub_train_collator,
num_workers=cfg.num_workers,
pin_memory=True,
prefetch_factor=cfg.prefetch_factor,
# worker_init_fn=worker_init_fn)
)
epoch_iterator = tqdm(sub_train_dataloader, desc="Iteration", disable=cfg.local_rank not in [-1, 0], dynamic_ncols=True)
if cfg.local_rank != -1:
sub_train_sampler.set_epoch(epoch)
if dist.is_initialized():
dist.barrier()
for step, batch in enumerate(epoch_iterator):
# If training is continued from a checkpoint, fast forward
# to the state of that checkpoint.
if global_step < continue_from_global_step:
if (step + 1) % cfg.gradient_accumulation_steps == 0:
# scheduler.step() # Update learning rate schedule # Done by `load_checkpoint` of DS.
global_step += 1
continue
model.train()
batch = batch_to_device(batch, cfg.device)
loss, outputs = forward_step(model, batch)
loss /= cfg.gradient_accumulation_steps
if tb_helper is not None:
tb_helper.update(last_batch=batch, last_outputs=outputs)
tr_loss += loss
if (step + 1) % cfg.gradient_accumulation_steps == 0:
global_step += 1
# Log metrics
log_metrics = {}
if cfg.local_rank in [-1, 0]:
log_metrics['lr'] = scheduler.get_lr()[0]
log_metrics['loss'] = tr_loss - logging_loss
logging_loss = tr_loss
if tb_helper is not None:
log_metrics.update(tb_helper(clear=True))
# Save model checkpoint
if cfg.save_steps > 0 and global_step % cfg.save_steps == 0:
output_dir = os.path.join(cfg.output_dir, 'checkpoint-{}'.format(global_step))
if cfg.local_rank in [-1, 0] and not os.path.exists(output_dir):
os.makedirs(output_dir, exist_ok=True)
dist.barrier()
save_model(model, cfg, output_dir, tokenizer)
# Evaluation
if cfg.evaluate_during_training and cfg.eval_steps > 0 and global_step % cfg.eval_steps == 0:
# state_dict = get_state_dict(model, cfg)
if cfg.ddp_eval or cfg.local_rank in [-1, 0]:
results = evaluate(cfg, model, tokenizer, prefix=str(global_step), _split="dev")
if cfg.local_rank in [-1, 0]:
for key, value in results.items():
log_metrics[f"eval/{key}"] = value
sub_path = os.path.join(cfg.output_dir, 'checkpoint-{}'.format(global_step))
flag = note_best_checkpoint(cfg, results, sub_path)
if cfg.save_best and flag:
save_model(model, cfg, cfg.output_dir, tokenizer)
if len(log_metrics) > 0 and (cfg.local_rank == -1 or dist.get_rank() == 0):
wandb.log(log_metrics)
if global_step % cfg.logging_steps == 0:
logger.info(log_metrics)
del batch
del log_metrics
if 0 < cfg.max_steps < global_step:
epoch_iterator.close()
break
if 0 < cfg.max_steps < global_step:
train_iterator.close()
break
del sub_train_dataloader
del sub_train_sampler
del sub_train_collator
del sub_train_dataset
if 0 < cfg.max_steps < global_step:
break
return global_step, tr_loss / global_step
@hydra.main(config_path="conf", config_name="config", version_base="1.2")
def main(cfg: DictConfig):
if "LOCAL_RANK" in os.environ and os.environ["LOCAL_RANK"] != -1:
cfg.local_rank = int(os.environ["LOCAL_RANK"])
if cfg.local_rank == -1 or cfg.no_cuda:
device = str(torch.device("cuda" if torch.cuda.is_available() and not cfg.no_cuda else "cpu"))
cfg.n_gpu = torch.cuda.device_count()
cfg.dp_size = 1
else: # Initializes the distributed backend which will take care of synchronizing nodes/GPUs
torch.cuda.set_device(cfg.local_rank)
device = str(torch.device("cuda", cfg.local_rank))
deepspeed.init_distributed(dist_backend="nccl", timeout=datetime.timedelta(seconds=7200000))
cfg.n_gpu = 1
cfg.world_size = dist.get_world_size()
cfg.dp_size = dist.get_world_size()
if cfg.tp_size > 1:
initialize_model_parallel(cfg.tp_size)
cfg.dp_size = mpu.get_data_parallel_world_size()
cfg.device = device
global logger
logger = setting_logger(cfg.output_dir, local_rank=cfg.local_rank)
logger.warning("Process rank: %s, device: %s, n_gpu: %s, distributed training: %s, 16-bits training: %s",
cfg.local_rank, cfg.device, cfg.n_gpu, bool(cfg.local_rank != -1), cfg.fp16)
logger.warning(f"CPU cores: {os.cpu_count()}")
logger.warning(f"Global rank: {dist.get_rank() if dist.is_initialized() else -1}")
if mpu.model_parallel_is_initialized():
dp_size = mpu.get_data_parallel_world_size()
dp_rank = mpu.get_data_parallel_rank()
mp_size = mpu.get_model_parallel_world_size()
mp_rank = mpu.get_model_parallel_rank()
pp_size = get_pipeline_parallel_world_size()
pp_rank = get_pipeline_parallel_rank()
logger.warning(f"Local Rank: {cfg.local_rank}, "
f"Global Rank: {dist.get_rank()}, "
f"Data Parallel: {dp_rank}/{dp_size}, "
f"Model Parallel: {mp_rank}/{mp_size}, "
f"Pipeline Parallel: {pp_rank}/{pp_size}")
# Set seed
set_seed(cfg)
# Training
if cfg.do_train:
use_barrier = not os.path.exists(cfg.model_name_or_path)
# Load pre-trained model and tokenizer
if use_barrier and cfg.local_rank not in [-1, 0]:
dist.barrier() # Make sure only the first process in distributed training will download model & vocab
if cfg.pretrain: # TODO: How to load pretrain state dict and then split it to different GPUs.
pretrain_state_dict = torch.load(cfg.pretrain, map_location='cpu')
else:
pretrain_state_dict = None
if getattr(cfg, "tokenizer_init", None):
tokenizer = hydra.utils.call(cfg.tokenizer_init)
else:
tokenizer = AutoTokenizer.from_pretrained(cfg.model_name_or_path)
from general_util.tokenization_utils import expand_special_tokenizer
expand_special_tokenizer(tokenizer)
try:
model = hydra.utils.call(cfg.model, cfg.model_name_or_path, state_dict=pretrain_state_dict)
except Exception as e:
logger.warning(e)
model = hydra.utils.call(cfg.model)
if use_barrier and cfg.local_rank == 0:
dist.barrier() # Make sure only the first process in distributed training will download model & vocab
if dist.is_initialized():
dist.barrier()
# logger.info("Training/evaluation parameters %s", OmegaConf.to_yaml(cfg))
if (cfg.local_rank == -1 or dist.get_rank() == 0) and cfg.do_train:
if not os.path.exists(cfg.output_dir):
os.makedirs(cfg.output_dir)
OmegaConf.save(cfg, os.path.join(cfg.output_dir, "training_config.yaml"))
continue_from_global_step = 0 # If set to 0, start training from the beginning
if os.path.exists(cfg.output_dir) and getattr(cfg, "resume", None):
if cfg.resume == "latest":
checkpoint = get_last_checkpoint(cfg.output_dir)
else:
checkpoint = cfg.resume
if checkpoint:
logger.info("Resuming training from the latest checkpoint: %s", checkpoint)
continue_from_global_step = int(checkpoint.split('-')[-1])
# Catch keyboard interrupts
try:
global_step, tr_loss = train(cfg, model, tokenizer, continue_from_global_step)
except KeyboardInterrupt:
logger.info("Keyboard interrupt, normally exiting...")
exit()
logger.info(" global_step = %s, average loss = %s", global_step, tr_loss)
# Test
results = {}
if cfg.do_eval:
if not cfg.ddp_eval and cfg.local_rank not in [-1, 0]:
return results
checkpoints = [cfg.output_dir]
if cfg.save_best:
logging.getLogger("transformers.modeling_utils").setLevel(logging.WARN) # Reduce logging
elif cfg.prediction_cfg.best_checkpoint and os.path.exists(cfg.prediction_cfg.best_checkpoint):
checkpoints = [cfg.prediction_cfg.best_checkpoint]
logging.getLogger("transformers.modeling_utils").setLevel(logging.WARN) # Reduce logging
elif cfg.eval_sub_path:
checkpoints = list(sorted(list(set(
os.path.dirname(c) for c in
glob.glob(cfg.output_dir + f"/{cfg.eval_sub_path}/" + "pytorch_model*.bin", recursive=True)
))))
logging.getLogger("transformers.modeling_utils").setLevel(logging.WARN) # Reduce logging
logger.info(" the following checkpoints: %s", checkpoints)
for checkpoint in checkpoints:
global_step = checkpoint.split("-")[-1] if len(checkpoints) > 1 else ""
prefix = checkpoint.split("/")[-1] if checkpoint.find("checkpoint") != -1 else ""
split = "dev"
if "model_eval" in cfg:
model = hydra.utils.call(cfg.model_eval, checkpoint)
else:
model = hydra.utils.call(cfg.model, checkpoint)
if cfg.n_gpu == 1:
model.to(cfg.device)
else:
# For model parallel (of mT5)
if getattr(cfg, "get_device_map", None):
model.parallelize(hydra.utils.call(cfg.get_device_map))
else:
model.parallelize()
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
cfg.model_name_or_path = checkpoint
if cfg.test_file:
prefix = f'test' + (f'-{prefix}' if prefix != "" else "")
split = "test"
result = evaluate(cfg, model, tokenizer, prefix=prefix, _split=split)
result = dict((k + "_{}".format(global_step), v) for k, v in result.items())
results.update(result)
return results
if __name__ == "__main__":
os.environ["NCCL_IB_GID_INDEX"] = "3"
os.environ["HYDRA_FULL_ERROR"] = "1"
# os.environ["WANDB__SERVICE_WAIT"] = "1200"
os.environ["TORCH_NCCL_BLOCKING_WAIT"] = "1"
os.environ["TORCH_NCCL_ASYNC_ERROR_HANDLING"] = "1"
hydra_formatted_args = []
# convert the cli params added by torch.distributed.launch into Hydra format
for arg in sys.argv:
if arg.startswith("--"):
hydra_formatted_args.append(arg[len("--"):])
else:
hydra_formatted_args.append(arg)
sys.argv = hydra_formatted_args
print(sys.argv)
main()