02 - 训练基础设施¶
⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。
学习目标:掌握大模型训练的混合精度、分布式训练(DDP/FSDP)、DeepSpeed 和 Megatron 等基础设施技术。
目录¶
混合精度训练¶
1.1 为什么需要混合精度¶
Text Only
FP32 (单精度浮点):
- 32位: 1位符号 + 8位指数 + 23位尾数
- 范围: ~1.18e-38 to ~3.4e38
- 内存占用大,计算慢
FP16 (半精度浮点):
- 16位: 1位符号 + 5位指数 + 10位尾数
- 范围: ~5.96e-8 to ~65504
- 内存占用小,计算快(Tensor Core 加速)
- 问题: 梯度下溢(underflow),精度损失
BF16 (Brain Floating Point):
- 16位: 1位符号 + 8位指数 + 7位尾数
- 范围与FP32相同,精度略低
- 更适合训练,不易下溢
- 需要 Ampere 架构及以上 GPU(A100/H100)
FP8 (2024-2025 新兴):
- E4M3 (前向): 4位指数 + 3位尾数
- E5M2 (反向): 5位指数 + 2位尾数
- H100 原生支持,训练/推理加速
- 主要用于推理阶段的量化,训练中仍在探索
1.2 PyTorch AMP 实现¶
Python
import os
import torch
import torch.nn as nn
from torch.amp import autocast, GradScaler
class MixedPrecisionTrainer:
"""
混合精度训练器
核心原理:
1. 前向传播使用低精度(FP16/BF16)加速计算
2. 使用 GradScaler 放大损失,防止梯度下溢
3. 参数更新在 FP32 下进行,保证精度
FP16 训练需要 GradScaler,BF16 不需要(因为动态范围足够大)
"""
def __init__(self, model, optimizer, device='cuda', dtype='fp16'):
self.model = model.to(device)
self.optimizer = optimizer
self.device = device
self.dtype = dtype
# FP16 需要 GradScaler 防止梯度下溢
# BF16 动态范围与 FP32 相同,不需要 scaler
self.scaler = GradScaler() if dtype == 'fp16' else None
def train_step(self, batch):
"""
单步混合精度训练
"""
self.model.train()
input_ids = batch['input_ids'].to(self.device)
labels = batch['labels'].to(self.device)
self.optimizer.zero_grad()
# 选择精度类型
amp_dtype = torch.float16 if self.dtype == 'fp16' else torch.bfloat16
# 自动混合精度上下文
with autocast(device_type='cuda', dtype=amp_dtype):
outputs = self.model(input_ids, labels=labels)
loss = outputs.loss
if self.scaler is not None:
# FP16 路径:缩放梯度
self.scaler.scale(loss).backward()
self.scaler.unscale_(self.optimizer)
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)
self.scaler.step(self.optimizer)
self.scaler.update()
else:
# BF16 路径:直接反向传播
loss.backward()
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)
self.optimizer.step()
return loss.item()
# BF16 训练完整示例(推荐,需要 Ampere+ GPU)
def bf16_training_example():
"""
BF16 训练示例
BF16 相比 FP16 的优势:
- 不需要 GradScaler(简化代码)
- 不容易梯度下溢(训练更稳定)
- 动态范围与 FP32 相同
"""
model = nn.Linear(1024, 1024).cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
dummy_input = torch.randn(32, 1024, device='cuda')
for step in range(100):
optimizer.zero_grad()
with autocast(device_type='cuda', dtype=torch.bfloat16):
output = model(dummy_input)
loss = output.sum()
loss.backward()
optimizer.step()
if step % 10 == 0:
print(f"Step {step}, Loss: {loss.item():.4f}")
分布式训练基础¶
2.1 数据并行 (Data Parallelism)¶
Python
import os
import torch
import torch.distributed as dist
import torch.multiprocessing as mp
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import DataLoader
from torch.utils.data.distributed import DistributedSampler
def setup_distributed(rank, world_size):
"""
初始化分布式训练环境
Args:
rank: 当前进程编号(0 到 world_size-1)
world_size: 总进程数(通常等于 GPU 数量)
"""
os.environ['MASTER_ADDR'] = 'localhost'
os.environ['MASTER_PORT'] = '12355'
dist.init_process_group(
backend='nccl', # NVIDIA GPU 使用 NCCL 后端
init_method='env://',
world_size=world_size,
rank=rank
)
torch.cuda.set_device(rank)
def cleanup_distributed():
"""清理分布式环境"""
dist.destroy_process_group()
def train_ddp(rank, world_size, dataset, model_fn, num_epochs=10):
"""
DDP 训练示例
DDP 工作原理:
1. 每个 GPU 持有模型的完整副本
2. 每个 GPU 处理不同的数据子集(数据并行)
3. 反向传播时,通过 AllReduce 同步梯度
4. 所有 GPU 的模型参数保持一致
通信开销:每个 step 一次 AllReduce(梯度同步)
内存开销:每卡存完整模型 + 优化器状态
"""
setup_distributed(rank, world_size)
# 创建模型并包装为 DDP
model = model_fn().to(rank)
model = DDP(model, device_ids=[rank])
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
# 创建分布式采样器(确保每个 GPU 看到不同数据)
train_sampler = DistributedSampler(
dataset,
num_replicas=world_size,
rank=rank,
shuffle=True
)
dataloader = DataLoader(
dataset,
batch_size=32,
sampler=train_sampler,
num_workers=4,
pin_memory=True,
)
for epoch in range(num_epochs):
# 每个 epoch 设置不同的 shuffle 种子
train_sampler.set_epoch(epoch)
for batch in dataloader:
optimizer.zero_grad()
loss = model(**batch)
loss.backward()
optimizer.step()
if rank == 0:
print(f"Epoch {epoch}/{num_epochs} completed")
cleanup_distributed()
# 启动多进程训练
if __name__ == '__main__':
world_size = torch.cuda.device_count()
mp.spawn(
train_ddp,
args=(world_size, dataset, model_fn, 10),
nprocs=world_size,
join=True
)
2.2 模型并行 (Model Parallelism)¶
Python
import torch
import torch.nn as nn
class ModelParallelModel(nn.Module):
"""
简单模型并行:将模型不同层放在不同 GPU 上
优点:
- 突破单卡内存限制
- 实现简单
缺点:
- 任何时刻只有一个 GPU 在计算(利用率低)
- GPU 间通信开销
- 仅适用于层数较多的模型
适用场景:
- 模型太大无法放入单卡
- 作为流水线并行的简化版本
"""
def __init__(self):
super().__init__()
# 不同层放在不同 GPU
self.layer1 = nn.Linear(4096, 4096).to('cuda:0')
self.layer2 = nn.Linear(4096, 4096).to('cuda:1')
self.layer3 = nn.Linear(4096, 4096).to('cuda:2')
def forward(self, x):
# 逐层计算,跨 GPU 传输中间结果
x = torch.relu(self.layer1(x.to('cuda:0')))
x = torch.relu(self.layer2(x.to('cuda:1')))
x = self.layer3(x.to('cuda:2'))
return x
2.3 流水线并行 (Pipeline Parallelism)¶
Text Only
流水线并行(Pipeline Parallelism)
核心思想:将模型按层分成多个阶段(stage),每个 GPU 负责一个阶段。
通过微批次(micro-batch)实现流水线,提高 GPU 利用率。
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ GPU 0 │───▶│ GPU 1 │───▶│ GPU 2 │───▶│ GPU 3 │
│ Stage 0 │ │ Stage 1 │ │ Stage 2 │ │ Stage 3 │
│ Layer 0-7│ │Layer 8-15│ │Layer16-23│ │Layer24-31│
└─────────┘ └─────────┘ └─────────┘ └─────────┘
朴素流水线(无微批次):
GPU利用率 = 1/N(N为stage数),大量空闲时间(bubble)
GPipe(微批次流水线):
将一个 batch 分成 M 个 micro-batch
依次送入流水线,bubble 比例 ≈ (N-1)/(N+M-1)
M 越大,bubble 越小,但延迟越高
1F1B(One Forward One Backward):
交替执行前向和反向,减少峰值内存
Megatron-LM 默认使用的调度策略
主流实现:
- PyTorch: torch.distributed.pipeline(实验性)
- Megatron-LM: 成熟的流水线并行实现
- DeepSpeed: 支持流水线并行
PyTorch FSDP¶
3.1 FSDP 核心概念¶
Text Only
FSDP (Fully Sharded Data Parallel)
FSDP 是 PyTorch 原生的全分片数据并行方案,对标 DeepSpeed ZeRO-3。
核心思想:
┌──────────────────────────────────────────────────────┐
│ DDP (每卡存完整模型) │
│ GPU 0: [完整参数] [完整梯度] [完整优化器状态] │
│ GPU 1: [完整参数] [完整梯度] [完整优化器状态] │
│ GPU 2: [完整参数] [完整梯度] [完整优化器状态] │
│ │
│ FSDP (参数分片) │
│ GPU 0: [1/3参数] [1/3梯度] [1/3优化器状态] │
│ GPU 1: [1/3参数] [1/3梯度] [1/3优化器状态] │
│ GPU 2: [1/3参数] [1/3梯度] [1/3优化器状态] │
│ │
│ 前向/反向时:All-Gather 收集需要的参数 │
│ 计算完成后:立即丢弃不需要的参数,释放显存 │
└──────────────────────────────────────────────────────┘
FSDP vs DeepSpeed ZeRO:
- FSDP: PyTorch 原生,与 PyTorch 生态无缝集成
- DeepSpeed: 功能更丰富(有 offload、流水线并行等),但依赖更重
- 2024-2025 趋势:FSDP 逐渐成为 PyTorch 社区的主流选择
3.2 FSDP 实战¶
Python
import os
import torch
import torch.distributed as dist
import torch.multiprocessing as mp
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp import MixedPrecision, ShardingStrategy
from torch.distributed.fsdp.wrap import transformer_auto_wrap_policy
def train_fsdp(rank, world_size):
"""
FSDP 训练示例
"""
setup_distributed(rank, world_size)
# 混合精度策略
mp_policy = MixedPrecision(
param_dtype=torch.bfloat16, # 参数使用 BF16
reduce_dtype=torch.bfloat16, # 梯度归约使用 BF16
buffer_dtype=torch.bfloat16, # Buffer 使用 BF16
)
# 创建模型
model = create_model().to(rank)
# FSDP 包装
model = FSDP(
model,
device_id=rank,
sharding_strategy=ShardingStrategy.FULL_SHARD, # 等价于 ZeRO-3
mixed_precision=mp_policy,
# 自动按 Transformer 层分片
auto_wrap_policy=transformer_auto_wrap_policy,
)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
# 训练循环(与普通 DDP 相同)
for batch in dataloader:
optimizer.zero_grad()
loss = model(**batch)
loss.backward()
optimizer.step()
cleanup_distributed()
# ShardingStrategy 对比
SHARDING_STRATEGIES = {
"NO_SHARD": "等价于 DDP,每卡存完整模型",
"SHARD_GRAD_OP": "等价于 ZeRO-2,分片梯度和优化器状态",
"FULL_SHARD": "等价于 ZeRO-3,分片参数、梯度和优化器状态",
}
DeepSpeed 详解¶
4.1 DeepSpeed ZeRO 优化器¶
JSON
{
"train_batch_size": 32,
"train_micro_batch_size_per_gpu": 4,
"gradient_accumulation_steps": 2,
"optimizer": {
"type": "AdamW",
"params": {
"lr": 1e-4,
"betas": [0.9, 0.999],
"eps": 1e-8,
"weight_decay": 0.01
}
},
"scheduler": {
"type": "WarmupLR",
"params": {
"warmup_min_lr": 0,
"warmup_max_lr": 1e-4,
"warmup_num_steps": 1000
}
},
"bf16": {
"enabled": true
},
"zero_optimization": {
"stage": 2,
"offload_optimizer": {
"device": "cpu",
"pin_memory": true
},
"allgather_partitions": true,
"allgather_bucket_size": 2e8,
"overlap_comm": true,
"reduce_scatter": true,
"reduce_bucket_size": 2e8,
"contiguous_gradients": true
}
}
Python
import json
import deepspeed
import torch.nn as nn
class DeepSpeedTrainer:
"""
DeepSpeed 训练器
DeepSpeed 的优势:
- ZeRO 优化器大幅减少显存占用
- 支持 CPU/NVMe Offload(进一步节省显存)
- 自动处理混合精度和梯度累积
- 支持流水线并行和张量并行
"""
def __init__(self, model, config_path):
# 加载配置
with open(config_path, 'r') as f:
ds_config = json.load(f)
# 初始化 DeepSpeed
model_engine, optimizer, _, _ = deepspeed.initialize(
model=model,
model_parameters=model.parameters(),
config=ds_config
)
self.model = model_engine
self.optimizer = optimizer
def train_step(self, batch):
"""
DeepSpeed 训练步骤
DeepSpeed 自动处理:
- 梯度累积(根据 gradient_accumulation_steps)
- 混合精度(根据 bf16/fp16 配置)
- 梯度分片和通信(根据 ZeRO stage)
"""
# 前向传播
outputs = self.model(**batch)
loss = outputs.loss
# 反向传播
self.model.backward(loss)
# 更新参数(自动处理梯度累积)
self.model.step()
return loss.item()
# ZeRO Stage 对比
ZERO_STAGES = """
ZeRO Stage 0: 标准DDP,无优化
每卡存储:完整参数 + 完整梯度 + 完整优化器状态
ZeRO Stage 1: 优化器状态分片(节省 ~4x 内存)
每卡存储:完整参数 + 完整梯度 + 1/N 优化器状态
ZeRO Stage 2: 优化器状态 + 梯度分片(节省 ~8x 内存)
每卡存储:完整参数 + 1/N 梯度 + 1/N 优化器状态
ZeRO Stage 3: 全分片(与 GPU 数量线性相关)
每卡存储:1/N 参数 + 1/N 梯度 + 1/N 优化器状态
例如 8 卡训练 7B 模型(FP16):
显存需求 ≈ 14GB 参数 + 14GB 梯度 + 84GB 优化器状态 = 112GB
- Stage 0: 每卡 112GB(不可能)
- Stage 1: 每卡 ~28GB(A100 40GB 可行)
- Stage 2: 每卡 ~18GB(A100 40GB 充裕)
- Stage 3: 每卡 ~14GB(更小显存也可行)
"""
Megatron-LM¶
5.1 张量并行 (Tensor Parallelism)¶
Text Only
张量并行(Tensor Parallelism)
核心思想:将单个矩阵乘法拆分到多个 GPU 上并行计算。
以线性层 Y = XA 为例(A 为权重矩阵):
列并行(Column Parallel):
将 A 按列切分:A = [A₁ | A₂]
GPU 1: Y₁ = XA₁
GPU 2: Y₂ = XA₂
结果:Y = [Y₁ | Y₂](直接拼接)
行并行(Row Parallel):
将 A 按行切分:A = [A₁; A₂]
GPU 1: Y₁ = X₁A₁(X 也需要按列切分)
GPU 2: Y₂ = X₂A₂
结果:Y = Y₁ + Y₂(AllReduce 求和)
Transformer 层的张量并行:
MLP: 第一个线性层用列并行,第二个用行并行
Attention: QKV 投影用列并行,输出投影用行并行
通信:每层只需 2 次 AllReduce
Python
import torch
import torch.nn as nn
import torch.nn.functional as F
class TensorParallelLinear(nn.Module):
"""
张量并行线性层(教学示例)
生产中应使用 Megatron-LM 或 Megatron-Core 的实现,
它们包含了通信优化、异步执行等高级特性。
"""
def __init__(self, input_size, output_size, rank, world_size):
super().__init__()
self.rank = rank
self.world_size = world_size
# 每个GPU只存储一部分权重
self.output_size_per_partition = output_size // world_size
self.weight = nn.Parameter(
torch.empty(self.output_size_per_partition, input_size)
)
self.bias = nn.Parameter(torch.empty(self.output_size_per_partition))
# 初始化
nn.init.kaiming_uniform_(self.weight)
nn.init.zeros_(self.bias)
def forward(self, input):
"""
列并行前向传播
每个GPU计算部分输出,然后通过 All-Gather 聚合
"""
# 本地计算
output_local = F.linear(input, self.weight, self.bias)
# All-Gather 聚合所有 GPU 的输出
# 生产中使用 torch.distributed.all_gather
output_list = [torch.empty_like(output_local) for _ in range(self.world_size)]
torch.distributed.all_gather(output_list, output_local)
output = torch.cat(output_list, dim=-1)
return output
5.2 序列并行 (Sequence Parallelism)¶
Text Only
序列并行(Sequence Parallelism)
核心思想:将序列维度(seq_len)分割到多个 GPU。
适用场景:
- 超长序列训练(128K+ tokens)
- KV Cache 显存压力大
- 与张量并行结合使用
实现方式:
- 将 Attention 的序列维度切分
- 每个 GPU 处理序列的一部分
- 需要在 Attention 计算时进行通信
代表工作:
- Megatron-SP (2023): 将 LayerNorm/Dropout 也并行化
- Ring Attention (2023): 支持无限长序列
- DeepSeek-DSA (2024): 稀疏注意力优化
训练优化技巧¶
6.1 梯度累积¶
Python
class GradientAccumulationTrainer:
"""
梯度累积训练器
核心思想:用多个小 batch 的梯度累积来模拟大 batch 训练。
等效 batch size = micro_batch_size × accumulation_steps × num_gpus
示例:
- 目标 batch size = 256
- GPU 数量 = 8
- 每 GPU micro batch = 4
- accumulation_steps = 256 / (8 × 4) = 8
注意:loss 需要除以 accumulation_steps 来保持梯度量级一致
"""
def __init__(self, model, optimizer, accumulation_steps=4):
self.model = model
self.optimizer = optimizer
self.accumulation_steps = accumulation_steps
self.step_count = 0
def train_step(self, batch):
"""
梯度累积训练步骤
"""
# 前向传播
outputs = self.model(**batch)
# 缩放 loss(保持梯度量级一致)
scaled_loss = outputs.loss / self.accumulation_steps
# 反向传播(梯度会累积)
scaled_loss.backward()
self.step_count += 1
# 每 accumulation_steps 更新一次参数
if self.step_count % self.accumulation_steps == 0:
# 梯度裁剪
torch.nn.utils.clip_grad_norm_(
self.model.parameters(), max_norm=1.0
)
self.optimizer.step()
self.optimizer.zero_grad()
# 返回未缩放的 loss
return scaled_loss.item() * self.accumulation_steps
6.2 梯度裁剪¶
Python
def clip_gradients(parameters, max_norm=1.0, norm_type=2.0):
"""
梯度裁剪
防止梯度爆炸(gradient explosion),在 Transformer 训练中必不可少。
原理:
- 计算 all 梯度的 L2 范数
- 如果范数 > max_norm,等比缩放所有梯度
- total_norm = ||∇θ||₂
- clip_coef = max_norm / total_norm
- ∇θ = ∇θ × min(clip_coef, 1.0)
经验值:
- max_norm = 1.0 是最常用的设置
- 某些任务可能需要 0.5 或 5.0
"""
total_norm = torch.nn.utils.clip_grad_norm_(
parameters,
max_norm=max_norm,
norm_type=norm_type
)
return total_norm
6.3 学习率调度¶
Python
import torch
from transformers import get_cosine_schedule_with_warmup
def create_scheduler(optimizer, num_warmup_steps, num_training_steps):
"""
创建余弦退火学习率调度器(LLM 训练最常用的调度策略)
调度过程:
1. Warmup 阶段:学习率从 0 线性增加到 lr
2. Decay 阶段:学习率按余弦函数从 lr 衰减到 0
为什么需要 Warmup:
- 训练初期模型参数随机,大学习率可能导致训练不稳定
- Warmup 让模型先用小学习率找到正确的优化方向
- 典型 warmup 步数:总步数的 1-10%
常见学习率调度对比:
- Cosine Decay: 最常用,平滑衰减
- Linear Decay: 简单线性衰减
- Constant with Warmup: warmup 后保持不变
- WSD (Warmup-Stable-Decay): 2024年新策略
"""
scheduler = get_cosine_schedule_with_warmup(
optimizer,
num_warmup_steps=num_warmup_steps,
num_training_steps=num_training_steps,
num_cycles=0.5 # 半个余弦周期
)
return scheduler
# 可视化学习率调度
def visualize_lr_schedule():
"""生成学习率调度曲线数据"""
model = torch.nn.Linear(10, 10)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
num_warmup = 1000
num_total = 10000
scheduler = get_cosine_schedule_with_warmup(
optimizer, num_warmup, num_total
)
lrs = []
for step in range(num_total):
lrs.append(optimizer.param_groups[0]['lr'])
scheduler.step()
return lrs # 返回学习率列表供绘图使用
6.4 Flash Attention¶
Text Only
Flash Attention(2022-2024)
Flash Attention 是大模型训练中最重要的优化之一:
标准 Attention:
Q × K^T → S → softmax(S) × V → O
内存:O(N²)(N 为序列长度)
瓶颈:HBM(高带宽内存)读写
Flash Attention 优化:
- 分块计算(Tiling):将 Q, K, V 分成小块
- 在 SRAM(片上缓存)中完成 softmax 计算
- 使用 Online Softmax 技巧避免二次遍历
- 重计算策略:反向时不存储注意力矩阵,需要时重算
效果:
- 内存:O(N)(从二次降为线性)
- 速度:2-4x 加速(减少 HBM 访问)
- 精确计算:不是近似,结果与标准 Attention 完全一致
使用方式:
PyTorch 2.0+: torch.nn.functional.scaled_dot_product_attention
自动选择最优 kernel(Flash Attention / Memory-Efficient Attention)
# 在 PyTorch 中自动启用
model = AutoModelForCausalLM.from_pretrained(
"model_name",
torch_dtype=torch.bfloat16,
attn_implementation="flash_attention_2" # 显式指定
)
故障恢复与检查点¶
7.1 检查点保存与加载¶
Python
import os
import torch
from pathlib import Path
class CheckpointManager:
"""
检查点管理器
大模型训练必须具备故障恢复能力:
- 训练可能持续数天到数周
- 硬件故障(GPU、网络)时有发生
- 检查点允许从最近的保存点恢复
检查点内容:
- 模型参数(state_dict)
- 优化器状态(动量、方差等)
- 学习率调度器状态
- 当前训练步数和 epoch
- 随机数生成器状态(确保可复现)
"""
def __init__(self, checkpoint_dir, max_keep=3):
self.checkpoint_dir = Path(checkpoint_dir)
self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
self.max_keep = max_keep # 最多保留的检查点数量
def save_checkpoint(self, model, optimizer, scheduler, step, loss):
"""
保存检查点
"""
checkpoint = {
'step': step,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'scheduler_state_dict': (
scheduler.state_dict() if scheduler else None
),
'loss': loss,
'rng_state': torch.random.get_rng_state(),
}
# 保存到编号文件
ckpt_path = self.checkpoint_dir / f'checkpoint_step_{step}.pt'
torch.save(checkpoint, ckpt_path)
# 更新 latest 指针(兼容 Windows 和 Linux)
latest_path = self.checkpoint_dir / 'checkpoint_latest.pt'
# Windows 不支持 symlink,使用 copy 替代
try:
if latest_path.exists():
latest_path.unlink()
os.symlink(ckpt_path.name, str(latest_path))
except OSError:
# Windows 可能不支持 symlink,直接复制
import shutil
shutil.copy2(str(ckpt_path), str(latest_path))
# 清理旧检查点
self._cleanup_old_checkpoints()
print(f"Checkpoint saved: {ckpt_path}")
def load_checkpoint(self, model, optimizer, scheduler=None, path=None):
"""
加载检查点
Args:
path: 指定路径,或 None 加载 latest
"""
if path is None:
path = self.checkpoint_dir / 'checkpoint_latest.pt'
checkpoint = torch.load(
path, map_location='cpu', weights_only=False
)
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
if scheduler and checkpoint.get('scheduler_state_dict'):
scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
# 恢复随机数状态
if 'rng_state' in checkpoint:
torch.random.set_rng_state(checkpoint['rng_state'])
step = checkpoint['step']
loss = checkpoint['loss']
print(f"Checkpoint loaded: {path}, step={step}, loss={loss:.4f}")
return step, loss
def _cleanup_old_checkpoints(self):
"""保留最近 max_keep 个检查点,删除更早的"""
checkpoints = sorted(
self.checkpoint_dir.glob('checkpoint_step_*.pt')
)
while len(checkpoints) > self.max_keep:
oldest = checkpoints.pop(0)
oldest.unlink()
print(f"Removed old checkpoint: {oldest}")
# DeepSpeed 检查点(自动处理分片保存)
# 保存:model.save_checkpoint(save_dir, tag='step_1000')
# 加载:model.load_checkpoint(load_dir, tag='step_1000')
总结¶
训练基础设施选择指南¶
Text Only
单卡训练:
├── 小模型 (<1B): 直接使用 PyTorch + AMP
└── 大模型 (1B-7B): 使用 DeepSpeed ZeRO-2 或 FSDP
多卡训练 (单机多卡):
├── 数据并行: DDP(模型能放入单卡)
├── 全分片: FSDP / DeepSpeed ZeRO-3(模型无法放入单卡)
└── 混合并行: FSDP + 流水线并行(超大模型)
大规模训练 (多机多卡):
├── 通用方案: FSDP(PyTorch 原生)
├── 极致性能: Megatron-LM(张量并行 + 流水线并行 + 数据并行)
├── 灵活方案: DeepSpeed(ZeRO + 流水线并行)
└── 2024-2025 趋势: FSDP2 + torch.compile 组合
关键要点¶
- 混合精度: 优先使用 BF16(Ampere+ GPU),无需 GradScaler
- 分布式: 根据模型大小选择并行策略——DDP → FSDP → Megatron
- FSDP: PyTorch 原生全分片方案,逐渐成为主流
- DeepSpeed: 功能最丰富,但依赖较重
- Flash Attention: 大模型训练必备,2-4x 加速
- 检查点: 定期保存,确保可恢复,注意清理旧检查点
- 监控: 跟踪 loss、学习率、梯度范数、GPU 利用率
下一步:03-推理服务部署 - 学习 vLLM、TGI 等推理框架的部署与优化。
练习题¶
思考题¶
-
混合精度选择:为什么 BF16 比 FP16 更适合训练?在什么情况下仍然需要使用 FP16?
-
并行策略选择:DDP、FSDP、张量并行、流水线并行分别解决什么问题?训练一个 70B 模型应该选择什么并行策略组合?
-
ZeRO Stage 选择:DeepSpeed ZeRO-½/3 分别节省多少显存?为什么不是所有场景都推荐使用 ZeRO-3?
-
Flash Attention:Flash Attention 如何将 Attention 的内存复杂度从 O(N²) 降低到 O(N)?它为什么是"精确计算"而非"近似计算"?
-
梯度累积:梯度累积和增大 batch size 在数学上等价吗?有什么细微差异?
代码实践¶
-
入门:使用 PyTorch AMP 实现一个简单的 BF16 混合精度训练循环。
-
进阶:实现梯度累积训练器,验证累积 4 步与 batch size × 4 的效果一致性。
-
高级:使用 PyTorch FSDP 在多 GPU 上训练一个小型 Transformer 模型,对比 DDP 和 FSDP 的显存占用。
💡 参考答案
**思考题 1**:BF16 的指数位与 FP32 相同(8位),动态范围更大,不容易出现梯度下溢(underflow),因此不需要 GradScaler,训练更稳定。FP16 的尾数位更多(10位 vs BF16 的 7位),精度更高,在需要高数值精度的场景(如某些科学计算)中仍有优势。此外,旧架构 GPU(V100 及更早)不支持 BF16,只能使用 FP16。 **思考题 2**:DDP 解决数据并行(每卡存完整模型),FSDP 解决显存不足(参数分片),张量并行解决单层计算太大(拆分矩阵乘法),流水线并行解决模型层数太多(按层切分)。70B 模型推荐:单机 8×A100 用 FSDP(或 FSDP + 张量并行),多机用 FSDP + 流水线并行,或 Megatron-LM(张量并行 + 流水线并行 + 数据并行 3D 并行)。 **思考题 3**:ZeRO-1 节省约 4x(仅分片优化器状态),ZeRO-2 节省约 8x(分片优化器状态 + 梯度),ZeRO-3 节省与 GPU 数量线性相关(全分片)。ZeRO-3 不总是推荐的原因:(1) 通信开销增大(每层前向/反向需要 All-Gather);(2) 训练速度可能比 ZeRO-2 慢 10-20%;(3) 如果模型能放入单卡,DDP/ZeRO-2 更高效。 **思考题 4**:Flash Attention 的核心是分块计算(Tiling):将 Q、K、V 分成小块,在 GPU 片上缓存(SRAM)中完成 softmax 计算,避免将完整的 N×N 注意力矩阵写入高带宽内存(HBM)。使用 Online Softmax 技巧实现分块 softmax 的数值等价。结果是精确计算(与标准 Attention 数学结果完全一致),只是改变了计算顺序和内存访问模式。 **思考题 5**:数学上几乎等价(梯度方向相同),但有细微差异:(1) BatchNorm 等依赖 batch 统计量的层会有差异(梯度累积时每次只用小 batch 的统计量);(2) 梯度累积中每步的 loss 缩放可能引入微小的浮点误差;(3) 数据顺序不同可能导致正则化效果略有差异。对于 LLM 训练(通常不用 BatchNorm),差异可以忽略。最后更新日期: 2026-04-21 适用版本: LLM 学习教程 v2026