minimind/trainer/train_full_sft.py

276 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 os
import sys
# 设置包名和添加项目根目录到系统路径
__package__ = "trainer"
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # 将项目根目录添加到系统路径
# 导入训练所需的库
import argparse
import time
import math
import warnings
import torch
import torch.distributed as dist
from contextlib import nullcontext
from torch import optim, nn
from torch.nn.parallel import DistributedDataParallel
from torch.utils.data import DataLoader, DistributedSampler
from transformers import AutoTokenizer, AutoModelForCausalLM
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
from dataset.lm_dataset import SFTDataset
warnings.filterwarnings('ignore') # 忽略警告信息
# 日志打印函数
# 在分布式训练时只在主进程(rank=0)上打印日志
def Logger(content):
if not ddp or dist.get_rank() == 0:
print(content)
# 余弦学习率调度器
# 在训练过程中逐渐降低学习率,最终降到初始值的1/10
def get_lr(current_step, total_steps, lr):
return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps))
# 训练一个epoch的函数
def train_epoch(epoch, wandb):
# 使用交叉熵损失函数,reduction='none'以便后续通过mask处理填充token
loss_fct = nn.CrossEntropyLoss(reduction='none')
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) # 损失掩码
# 更新学习率
lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args.learning_rate)
for param_group in optimizer.param_groups:
param_group['lr'] = lr
# 前向传播和损失计算
with ctx: # 使用自动混合精度训练
res = model(X) # 模型前向传播
# 计算交叉熵损失
loss = loss_fct(
res.logits.view(-1, res.logits.size(-1)), # 展平logits
Y.view(-1) # 展平目标序列
).view(Y.size())
# 应用损失掩码并计算平均损失
loss = (loss * loss_mask).sum() / loss_mask.sum()
loss += res.aux_loss # 添加辅助损失(如果有的话,例如MoE的负载均衡损失)
loss = loss / args.accumulation_steps # 根据梯度累积步数缩放损失
# 反向传播
scaler.scale(loss).backward() # 使用梯度缩放器缩放损失并反向传播
# 梯度更新
if (step + 1) % args.accumulation_steps == 0:
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} epoch_Time:{}min:'.format(
epoch + 1,
args.epochs,
step,
iter_per_epoch,
loss.item() * args.accumulation_steps,
optimizer.param_groups[-1]['lr'],
spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60))
# 记录wandb日志(如果启用)
if (wandb is not None) and (not ddp or dist.get_rank() == 0):
wandb.log({"loss": loss * args.accumulation_steps,
"lr": optimizer.param_groups[-1]['lr'],
"epoch_Time": 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设置检查点路径
moe_path = '_moe' if lm_config.use_moe else ''
ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth'
# 获取模型状态字典
if isinstance(model, torch.nn.parallel.DistributedDataParallel):
state_dict = model.module.state_dict()
else:
state_dict = model.state_dict()
# 将模型参数转换为半精度格式并保存
state_dict = {k: v.half() for k, v in state_dict.items()}
torch.save(state_dict, ckp)
model.train() # 切换回训练模式
# 初始化模型和分词器
def init_model(lm_config):
# 加载预训练的分词器
tokenizer = AutoTokenizer.from_pretrained('../model')
# 初始化因果语言模型
model = MiniMindForCausalLM(lm_config)
# 根据是否使用MoE设置权重路径
moe_path = '_moe' if lm_config.use_moe else ''
ckp = f'{args.save_dir}/pretrain_{lm_config.hidden_size}{moe_path}.pth'
# 加载预训练权重
state_dict = torch.load(ckp, map_location=args.device)
model.load_state_dict(state_dict, strict=False)
# 打印模型可训练参数总量
Logger(f'LLM可训练总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')
# 将模型移动到指定设备
model = model.to(args.device)
return model, tokenizer
# 初始化分布式训练环境
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"]) # 总进程数
# 设置当前进程使用的GPU
DEVICE = f"cuda:{ddp_local_rank}"
torch.cuda.set_device(DEVICE)
# 主函数
if __name__ == "__main__":
# 配置命令行参数
parser = argparse.ArgumentParser(description="MiniMind Full SFT")
# 基础训练参数
parser.add_argument("--out_dir", type=str, default="../out", help="输出目录")
parser.add_argument("--epochs", type=int, default=2, help="训练轮数")
parser.add_argument("--batch_size", type=int, default=16, help="批次大小")
parser.add_argument("--learning_rate", type=float, default=5e-7, help="学习率")
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
parser.add_argument("--dtype", type=str, default="bfloat16", help="训练精度")
# 日志和监控参数
parser.add_argument("--use_wandb", action="store_true", help="是否使用wandb记录训练过程")
parser.add_argument("--wandb_project", type=str, default="MiniMind-Full-SFT", help="wandb项目名称")
parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔")
parser.add_argument("--save_interval", type=int, default=100, help="模型保存间隔")
# 分布式训练参数
parser.add_argument("--num_workers", type=int, default=1, help="数据加载进程数")
parser.add_argument("--ddp", action="store_true", help="是否使用分布式训练")
parser.add_argument('--local_rank', type=int, default=-1, help="分布式训练的本地进程编号")
# 优化器参数
parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度累积步数")
parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")
parser.add_argument("--warmup_iters", type=int, default=0, help="学习率预热迭代次数")
# 模型参数
parser.add_argument('--hidden_size', default=512, type=int, help="隐藏层维度")
parser.add_argument('--num_hidden_layers', default=8, type=int, help="Transformer层数")
parser.add_argument('--max_seq_len', default=512, type=int, help="最大序列长度")
parser.add_argument('--use_moe', default=False, type=bool, help="是否使用MoE")
parser.add_argument("--data_path", type=str, default="../dataset/sft_mini_512.jsonl", help="训练数据路径")
parser.add_argument("--wandb_api_key", type=str, default=None, help="WandB API Key,用于无交互环境自动登录")
args = parser.parse_args()
# 初始化模型配置
lm_config = MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers,
use_moe=args.use_moe)
# 创建输出目录
args.save_dir = os.path.join(args.out_dir)
os.makedirs(args.save_dir, exist_ok=True)
os.makedirs(args.out_dir, exist_ok=True)
# 计算每次迭代处理的token数量
tokens_per_iter = args.batch_size * args.max_seq_len
device_type = "cuda" if "cuda" in args.device else "cpu"
# 设置wandb运行名称
args.wandb_run_name = f"MiniMind-Full-SFT-Epoch-{args.epochs}-BatchSize-{args.batch_size}-LearningRate-{args.learning_rate}"
# 设置自动混合精度训练上下文
ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast()
# 检查是否为分布式训练
ddp = int(os.environ.get("RANK", -1)) != -1
ddp_local_rank, DEVICE = 0, "cuda:0"
# 设置随机种子
base_seed = 1337
torch.manual_seed(base_seed)
torch.cuda.manual_seed(base_seed)
# 初始化分布式训练环境
if ddp:
init_distributed_mode()
args.device = torch.device(DEVICE)
rank = dist.get_rank()
torch.manual_seed(base_seed + rank)
torch.cuda.manual_seed(base_seed + rank)
# 初始化wandb(如果启用)
if args.use_wandb and (not ddp or ddp_local_rank == 0):
import wandb
# 优先使用命令行参数,其次环境变量
api_key = args.wandb_api_key or os.environ.get("WANDB_API_KEY", None)
if api_key is not None:
# 自动登录,适用于无交互环境
wandb.login(key=api_key)
# 初始化 wandb 任务
wandb.init(project=args.wandb_project, name=args.wandb_run_name)
else:
wandb = None
# 初始化模型和分词器
model, tokenizer = init_model(lm_config)
# 准备训练数据
train_ds = SFTDataset(args.data_path, tokenizer, max_length=args.max_seq_len)
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=False,
num_workers=args.num_workers,
sampler=train_sampler
)
# 初始化优化器和梯度缩放器
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16']))
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])
# 计算每个epoch的迭代次数
iter_per_epoch = len(train_loader)
# 开始训练循环
for epoch in range(args.epochs):
train_epoch(epoch, wandb)