minimind/train_pretrain.py

299 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import io
import os
import sys
import platform
import argparse
import threading
import time
import math
import warnings
import pandas as pd
import torch
import torch.distributed as dist
from torch import optim, nn
from torch._C._profiler import ActiveProfilerType
from torch.nn.parallel import DistributedDataParallel
from torch.nn.utils.rnn import pad_sequence
from torch.optim.lr_scheduler import CosineAnnealingLR
from torch.utils.data import DataLoader, DistributedSampler
from contextlib import nullcontext
from transformers import AutoTokenizer, PreTrainedTokenizerFast
from mem_utils import log_memory_snapshot, ActivationMemoryTracker
from model.model import MiniMindLM
from model.LMConfig import LMConfig
from model.dataset import PretrainDataset
warnings.filterwarnings('ignore')
def Logger(content):
if not ddp or dist.get_rank() == 0:
print(content)
def get_dynamic_lr(cur_step, warmup_steps, decay_steps, lr):
min_lr = lr / 10
if cur_step < warmup_steps:
return lr * (cur_step / warmup_steps)
if cur_step > decay_steps:
return min_lr
step_ratio = (cur_step - warmup_steps)/(decay_steps-warmup_steps)
cos_scope = 0.5 * (1 + math.cos(math.pi * step_ratio))
return min_lr + (lr - min_lr) * cos_scope
def get_lr(current_step, total_steps, lr):
return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps))
def train_epoch(epoch, wandb):
# 初始化跟踪器
# tracker = ActivationMemoryTracker()
# tracker.track_activations(model)
loss_fct = nn.CrossEntropyLoss(reduction='none')
step_base = epoch * iter_per_epoch
total_steps = iter_per_epoch * args.epochs
warmup_steps = int(total_steps * 0.005)
decay_steps = int(total_steps * 0.9)
if epoch == 0:
for param_group in optimizer.param_groups: # 在此处设置学习率
param_group['lr'] = 5e-6
start_time = time.time()
for step, (X, Y, loss_mask) in enumerate(train_loader):
X = X.to(args.device)
Y = Y.to(args.device)
loss_mask = loss_mask.to(args.device)
with ctx:
res = model(X)
loss = loss_fct(
res.logits.view(-1, res.logits.size(-1)),
Y.view(-1)
).view(Y.size())
loss = (loss * loss_mask).sum() / loss_mask.sum()
loss += res.aux_loss
loss = loss / args.accumulation_steps
scaler.scale(loss).backward()
if (step + 1) % args.accumulation_steps == 0:
current_step = step_base + step # 计算当前总步数
lr = get_dynamic_lr(current_step, warmup_steps, decay_steps, args.learning_rate)
for param_group in optimizer.param_groups: # 在此处设置学习率
param_group['lr'] = lr
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad(set_to_none=True)
if step % args.log_interval == 0:
spend_time = time.time() - start_time
Logger(
'Epoch:[{}/{}]({}/{}) loss:{:.3f} lr:{:.12f} spend:{}min remain:{}min'.format(
epoch + 1,
args.epochs,
step,
iter_per_epoch,
loss.item() * args.accumulation_steps,
optimizer.param_groups[-1]['lr'],
spend_time // 60,
spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60))
if (wandb is not None) and (not ddp or dist.get_rank() == 0):
wandb.log({"loss": loss.item() * args.accumulation_steps,
"lr": optimizer.param_groups[-1]['lr'],
"remain": spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60})
if (step + 1) % args.save_interval == 0 and (not ddp or dist.get_rank() == 0):
model.eval()
moe_path = '_moe' if lm_config.use_moe else ''
ckp = f'{args.save_dir}/pretrain_{lm_config.dim}{moe_path}.pth'
if isinstance(model, torch.nn.parallel.DistributedDataParallel):
state_dict = model.module.state_dict()
else:
state_dict = model.state_dict()
buffer = io.BytesIO()
torch.save(state_dict, buffer)
threading.Thread(target=save_checkpoint, args=(buffer, ckp)).start()
model.train()
# 内存调试
# log_memory_snapshot(model, optimizer, step)
# # 输出激活值显存统计
# print("中间激活值显存占用:")
# total = 0
# for name, info in tracker.activation_info.items():
# print(f"Layer: {name} | {info}")
# total += info["size"]
# print(f"中间激活值显存总占用: {total:.2f} MB")
# print(torch.cuda.memory_summary())
# return
def save_checkpoint(buffer: io.BytesIO, path):
with open(path, 'wb') as f:
f.write(buffer.getvalue())
def init_distributed_mode():
if not ddp: return
global ddp_local_rank, DEVICE
dist.init_process_group(backend="nccl")
ddp_rank = int(os.environ["RANK"])
ddp_local_rank = int(os.environ["LOCAL_RANK"])
ddp_world_size = int(os.environ["WORLD_SIZE"])
DEVICE = f"cuda:{ddp_local_rank}"
torch.cuda.set_device(DEVICE)
# torchrun --nproc_per_node 2 1-pretrain.py
if __name__ == "__main__":
# 启用动态显存段扩展
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
parser = argparse.ArgumentParser(description="MiniMind Pretraining")
parser.add_argument("--save_dir", type=str, default="out")
# 若要以最快速度实现zero则epochs设置为1轮;否则应当利用有限的数据训练2~6个epochs。
parser.add_argument("--epochs", type=int, default=1)
parser.add_argument("--batch_size", type=int, default=32)
parser.add_argument("--learning_rate", type=float, default=5e-4)
parser.add_argument('--dim', default=512, type=int)
parser.add_argument('--n_layers', default=8, type=int)
parser.add_argument('--n_heads', default=8, type=int)
parser.add_argument('--n_kv_heads', default=2, type=int)
parser.add_argument('--max_seq_len', default=512, type=int)
parser.add_argument('--use_mla', default=False, action='store_true')
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu")
parser.add_argument("--dtype", type=str, default="bfloat16")
parser.add_argument("--use_wandb", action="store_true")
parser.add_argument("--wandb_project", type=str, default="MiniMind-Pretrain")
parser.add_argument("--num_workers", type=int, default=1)
parser.add_argument("--ddp", action="store_true")
parser.add_argument("--accumulation_steps", type=int, default=8)
parser.add_argument("--grad_clip", type=float, default=1.0)
parser.add_argument("--warmup_iters", type=int, default=0)
parser.add_argument("--log_interval", type=int, default=500)
parser.add_argument("--save_interval", type=int, default=200)
parser.add_argument('--local_rank', type=int, default=-1)
parser.add_argument('--vocab', default='minimind', type=str)
parser.add_argument('--use_moe', default=False, type=bool)
parser.add_argument("--data_paths", type=str, nargs="+", default=["./dataset/pretrain_hq.jsonl"])
parser.add_argument("--ckpt_path", type=str, default=None)
args = parser.parse_args()
args.log_interval = args.save_interval
os.makedirs(args.save_dir, exist_ok=True)
torch.manual_seed(1337)
device_type = "cuda" if "cuda" in args.device else "cpu"
args.wandb_run_name = f"MiniMind-Pretrain-Epoch-{args.epochs}-BatchSize-{args.batch_size}-LearningRate-{args.learning_rate}"
if device_type == "cpu":
ctx = nullcontext()
else:
# 映射dtype字符串到torch类型
amp_dtype = torch.bfloat16 if args.dtype == 'bfloat16' else torch.float16
ctx = torch.cuda.amp.autocast(dtype=amp_dtype)
ddp = int(os.environ.get("RANK", -1)) != -1 # is this a ddp run?
ddp_local_rank, DEVICE = 0, "cuda:0"
if ddp:
init_distributed_mode()
args.device = torch.device(DEVICE)
if args.use_wandb and (not ddp or ddp_local_rank == 0):
import wandb
wandb.init(project=args.wandb_project, name=args.wandb_run_name)
else:
wandb = None
lm_config = LMConfig(
dim=args.dim,
n_layers=args.n_layers,
n_heads=args.n_heads,
n_kv_heads=args.n_kv_heads,
max_seq_len=args.max_seq_len,
use_moe=args.use_moe,
use_mla=args.use_mla,
torch_dtype=args.dtype,
)
tokenizer = AutoTokenizer.from_pretrained(f'./model/{args.vocab}_tokenizer')
lm_config.vocab_size = tokenizer.vocab_size
model = MiniMindLM(lm_config)
Logger(f'LLM总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')
if args.ckpt_path is not None:
# 接着训练
state_dict = torch.load(args.ckpt_path, map_location=args.device)
model.load_state_dict({k: v for k, v in state_dict.items() if 'mask' not in k}, strict=True)
model.to(args.device)
train_ds = PretrainDataset(args.data_paths, tokenizer, max_length=args.max_seq_len)
def collate_fn(batch):
# 对 batch 中的每个样本进行 padding,动态padding到每个batch的最大长度
batch = pad_sequence(batch, batch_first=True, padding_value=tokenizer.pad_token_id)
# 以下是padding到最大长度,方便调试显存占用情况
# max_len = args.max_seq_len
# processed = []
# for input_ids in batch:
# # 截断超过 max_len 的部分
# if len(input_ids) > max_len:
# processed.append(input_ids[:max_len])
# else:
# # 不足则填充到 max_len
# padded = torch.full(
# (max_len,),
# tokenizer.pad_token_id,
# dtype=input_ids.dtype
# )
# padded[:len(input_ids)] = input_ids
# processed.append(padded)
# batch = torch.stack(processed) # 直接堆叠已等长的样本
# 直接通过切片生成输入和目标序列
bx = batch[:, :-1] # 输入序列(去掉最后一个token)
by = batch[:, 1:] # 目标序列(去掉第一个token)
# 生成attention mask(去掉第一个token的位置)
bl = (batch != tokenizer.pad_token_id)[:, 1:]
return bx, by, bl
train_sampler = DistributedSampler(train_ds) if ddp else None
train_loader = DataLoader(
train_ds,
batch_size=args.batch_size,
pin_memory=True,
drop_last=False,
shuffle=True,
num_workers=args.num_workers,
sampler=train_sampler,
collate_fn=collate_fn,
)
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == 'float16'))
optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
if ddp:
model._ddp_params_and_buffers_to_ignore = {"pos_cis"}
model = DistributedDataParallel(model, device_ids=[ddp_local_rank])
iter_per_epoch = len(train_loader)
for epoch in range(args.epochs):
train_epoch(epoch, wandb)