第 14 章:分布式训练
现代大语言模型的参数量从数十亿到数千亿不等,单张 GPU 的显存(40GB--80GB)和算力都无法独立承担训练任务。分布式训练将计算与存储分摊到多个设备上,是大模型训练的必经之路。本章从 3D 并行框架出发,逐层深入:先讲 DDP 与 NCCL 通信机制,再分别剖析数据并行、张量并行、流水线并行的原理与取舍,接着覆盖序列并行、FSDP、Packing 等进阶技术,最后介绍 HuggingFace 基础设施、Ray/Slurm 集群管理、量化与实验追踪等工程实践。
14.1 分布式训练总览:3D 并行
DP、TP、PP 三维并行的正交组合与进程组划分,构成大模型训练的基础框架。
为什么需要"3D"
业界将主流分布式策略归纳为三个正交的并行维度——数据并行(DP)、张量并行(TP)、流水线并行(PP),合称 3D 并行。三者分别回答了不同的核心问题:
| 并行类型 | 切分对象 | 通信算子 | 核心问题 |
|---|---|---|---|
| 数据并行(DP) | 训练数据 | AllReduce / ReduceScatter + AllGather | 如何加速单步训练 |
| 张量并行(TP) | 单层权重矩阵 | AllReduce(高频) | 如何把一层放到多卡上 |
| 流水线并行(PP) | 模型层 | Point-to-Point Send/Recv(低频) | 如何把深层模型分到多机 |
三者的乘积等于总 GPU 数:
直觉理解:
- DP 是"同一个模型、不同数据"——每张卡拿不同的 mini-batch,算完梯度后同步。
- TP 是"一层算子横向切开"——矩阵乘法的列或行被分摊到多卡并行计算。
- PP 是"模型层纵向切开"——不同层在不同卡上串行执行,数据像流水线一样流过。
DDP、DeepSpeed ZeRO、FSDP 的关系
这三者本质上都是数据并行,区别在于显存管理策略:
- DDP(Distributed Data Parallel):PyTorch 原生实现。每卡保有完整的模型参数、梯度和优化器状态的副本,梯度通过 AllReduce 同步。显存开销最大。
- DeepSpeed ZeRO:在 DP 框架下,将参数(Stage 1)、梯度(Stage 2)、优化器状态(Stage 3)切片存储到不同 GPU 上。计算时通过 AllGather 收集参数,ReduceScatter 规约梯度。每个 GPU 逻辑上仍拥有完整模型,只是参数暂时存储在不同 GPU 上。
- FSDP(Fully Sharded Data Parallel):继承了 ZeRO Stage 3 的设计哲学,是 PyTorch 原生的全分片数据并行实现。参数/梯度/优化器状态全部分片。
进程组划分
在 3D 并行中,每张 GPU 同时属于多个通信组:
- FSDP 的
process_group=dp_group——只在 DP 维度通信 - TP 层内部算子用
tp_group——同一 TP 组内做 AllReduce / AllGather - PP 在相邻 pipeline 段之间做点对点 Send/Recv
SP=4 (列) -->
DP=2 GPU(0,0) GPU(0,1) GPU(0,2) GPU(0,3) <- DP Group 0
(行) GPU(1,0) GPU(1,1) GPU(1,2) GPU(1,3) <- DP Group 114.2 DDP 与 NCCL 通信
NCCL 通信原语与 DDP 的 Bucket 机制,实现计算与通信的重叠执行。
NCCL 集合通信算子
NCCL(NVIDIA Collective Communications Library)是 NVIDIA 为 GPU 间通信优化的底层库,提供以下核心集合操作:
| 算子 | 功能 | 典型用途 |
|---|---|---|
| AllReduce | 所有节点规约(求和/均值),结果广播到所有节点 | DDP 梯度同步 |
| Broadcast / Reduce | 单对多广播 / 多对一规约 | 参数初始化广播 |
| AllGather | 每个节点贡献一块,所有节点获得完整数据 | FSDP 前向参数收集 |
| ReduceScatter | 先规约后分散,每节点只拿到部分结果 | FSDP 反向梯度分片 |
| AlltoAll | 转置通信——按维度重新分配数据 | 序列并行(Ulysses) |
参考文档:https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/usage/collectives.html
GPU 拓扑与通信路径

NCCL 会根据硬件拓扑自动选择最优通信路径:
单机多卡(Intra-node):
- 有 NVLink 时,直接 P2P(Peer-to-Peer)传输,带宽高达 600--900 GB/s,完全不经过 CPU 和系统内存
- 无 NVLink 时,走 PCIe 总线,NCCL 仍尽量利用 P2P Direct Access 绕过 CPU
多机多卡(Inter-node):
- 数据必须离开机器,经过网卡走网络
- GPUDirect RDMA:关键技术——RDMA(InfiniBand 或 RoCE)让网卡直接读取 GPU 显存并发送,无需先拷贝到 CPU 内存,大幅降低延迟和 CPU 占用
- 网络带宽(100--400 Gbps)远低于机内 NVLink 速度,因此多机训练中梯度压缩、大 batch size(减少通信频率)变得更重要
AllReduce 的数学含义
在 DDP 训练中,AllReduce 的物理意义是梯度平均。设共有
- NCCL 执行 AllReduce(Op=Sum):
- 每卡本地除以
(无需通信):
工程细节:虽然 NCCL 也有 Avg 模式,但 PyTorch 通常先做 Sum 再本地除以
。实际算法采用 Ring-AllReduce 或 Tree-AllReduce——数据像接力赛一样通过切片轮转(Scatter-Reduce + AllGather),在传输过程中顺便完成计算,没有单一瓶颈节点。
DDP 详细执行流程
以 2 卡、2 层模型、总 batch=32 为例:
数据分发:DDP 通过 DistributedSampler 将数据切分——GPU 0 拿前 16 条(Data_0),GPU 1 拿后 16 条(Data_1)。
Forward:完全并行,无通信。每卡独立计算
Backward + 通信重叠:
| 时间轴 | GPU 计算核心(Compute Stream) | NCCL 通信通道(Comm Stream) | 状态 |
|---|---|---|---|
| T1 | 正在算 Layer 2 梯度 | 空闲 | 刚开始 Backward |
| T2 | Layer 2 算完 → 提交通信请求 | 准备启动 | Bucket A 满了 |
| T3 | 正在算 Layer 1 梯度 | 正在传输 Layer 2 梯度 | Overlap(双轨并行) |
| T4 | Layer 1 算完 → 提交通信请求 | Layer 2 传输完成 | Layer 2 梯度已同步 |
| T5 | 计算结束,等待 | 正在传输 Layer 1 梯度 | 最后的通信无法掩盖 |
| T6 | Optimizer 更新参数 | 空闲 | 全局同步完成 |
关键设计——Bucket(桶)机制:PyTorch 将参数按顺序装入 Bucket,一旦某个 Bucket 里的所有梯度都算好,AllReduce 立即触发。梯度是从后往前算的(Layer 2 → Layer 1),因此 Layer 2 的梯度先算好、先通信,Layer 1 的计算与 Layer 2 的通信并行执行——这就是计算与通信重叠(overlap)。
最后一个 Bucket(Layer 1)的通信无法被后续计算掩盖,是 DDP 的固有开销。
参数更新:同步完成后,两卡都持有相同的 optimizer.step():
14.3 数据并行、张量并行、流水线并行详解
数据并行、张量并行、流水线并行的工作原理与选型指南。
数据并行(DP)
数据并行是最简单的并行形式——每张卡持有完整的模型副本,只是输入数据不同:
GPU 0: Data_0 → Forward → Backward → 梯度 g0
GPU 1: Data_1 → Forward → Backward → 梯度 g1
↓ AllReduce ↓
GPU 0 & GPU 1: g_avg → Optimizer.step()所有卡在 Optimizer 步骤后保持参数一致。
张量并行(TP)
TP 将单层的矩阵计算"竖着切",分摊到多卡上:
- 切分方式:对
等权重矩阵按列或行切分
- 工作流:GPU 0 算矩阵上半,GPU 1 算矩阵下半 → AllReduce 同步结果 → 进入下一层
- 通信特征:极高频、高带宽——每一层的前向和反向传播都需要通信
直觉:TP 好比把一道大菜的刀工分给两个厨师同时切,但两人之间需要频繁交换中间结果。
流水线并行(PP)
PP 将模型的层"横着切",不同层运行在不同卡上:
GPU 0: Layer 1~16 ──→ Activations ──→ GPU 1
GPU 1: Layer 17~32 ──────────────────→ Loss- 通信特征:低频、点对点——只在切分点(层边界)传输 Activation
- 流水线气泡(Bubble):PP 的核心损耗。当 GPU 1 在计算时,GPU 0 可能空闲等待。气泡比为:
其中
TP vs. PP 选择指南
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 单机双卡(NVLink 互联) | TP=2 | 利用 NVLink 高带宽,降低单次推理延迟,显存均衡,几乎没有流水线气泡 |
| 双机单卡各一(跨节点) | PP=2 | PP 对带宽要求低,是跨机运行大模型的唯一可行方案 |
| 大规模集群 | DP | 三维组合——TP 放机内(利用 NVLink),PP 跨机(容忍网络延迟),DP 扩数据吞吐 |
关键原则:TP 要求高带宽低延迟(NVLink),PP 容忍低带宽但有气泡损耗。跨机做 TP 的高频通信会被网络延迟直接锁死。
14.4 3D 并行高级话题
张量并行的通信量分析、流水线并行的 micro-batch 策略与 3D 组合配置实例。
张量并行的通信分析
TP 的通信量随 TP 度线性增加。对于隐层维度
- 每个 Transformer 层需要 2 次 AllReduce(前向一次、反向一次)
- 每次 AllReduce 的通信量约为
bytes
实践约束:TP 一般不超过单机 GPU 数(4 或 8),超出则通信开销抵消并行收益。
流水线并行的 micro-batch 策略
PP 通过将 mini-batch 切分为多个 micro-batch 来减少气泡:
时间 →
GPU 0: [F0][F1][F2][F3] [B3][B2][B1][B0]
GPU 1: [F0][F1][F2][F3] [B3][B2][B1][B0]
GPU 2: [F0][F1][F2][F3] ... [B0]
|← 气泡→| |← 气泡→|:第 个 micro-batch 的前向传播 :第 个 micro-batch 的反向传播 - micro-batch 数
越大,气泡占比越小,GPU 利用率越高
组合配置示例
以 64 张 A100(8 节点,每节点 8 卡 NVLink 互联)为例:
# 模型有 96 层,隐层维度 12288
dp = 4 # 4 路数据并行(4 组副本处理不同数据)
tp = 4 # 每机 4 卡张量并行(机内 NVLink 高速互联)
pp = 4 # 4 级流水线(跨 4 台机器)
# 总计 dp × tp × pp = 64 张 GPU14.5 序列并行(SP)
超长序列训练的显存瓶颈解决方案:Ring Attention 与 DeepSpeed Ulysses 的原理对比。
问题背景
注意力机制的显存消耗与序列长度的平方成正比:
序列并行(Sequence Parallelism)的核心思想:将输入序列沿长度维度切分到多个 GPU 上,每张卡只处理一个 sub-sequence。
核心论文:
- Ring Attention: [arXiv:2105.13120] Sequence Parallelism: Long Sequence Training from System Perspective
- DeepSpeed Ulysses: [arXiv:2309.14509] System Optimizations for Enabling Training of Extreme Long Sequence Transformer Models
Ring Attention
Ring Attention 的策略是通信换内存:
- 序列
按序列维度切分为 块,每卡持有子序列并计算本地的 - 通过 Ring 通信(接力赛式)轮转
:每一轮,每卡将自己的 发送给下一卡,并接收上一卡的 - 在接收
时同步计算局部注意力分数 - 经过
轮后,每卡拥有完整的注意力分数,可以做 softmax 并计算输出
下面的代码验证了 Ring Attention 与标准 Attention 的等价性:
# 标准注意力
output_standard = F.softmax(Q @ K.T / scale, dim=-1) @ V
# Ring Attention 模拟:每个 device 用 Q_i 与所有 K 块逐步计算
for i in range(num_devices):
q_local = Q_chunks[i]
for j in range(num_devices):
k_idx = (i - j + num_devices) % num_devices
scores_part = torch.matmul(q_local, K_chunks[k_idx].T) * scale
ordered_scores[k_idx] = scores_part
all_scores = torch.cat(ordered_scores, dim=-1)
attn_weights = F.softmax(all_scores, dim=-1)
output_chunk = torch.matmul(attn_weights, full_V)
output_ring = torch.cat(output_chunks, dim=1)
# torch.allclose(output_standard, output_ring) == TrueDeepSpeed Ulysses
DeepSpeed Ulysses(名字取自西方著名长篇小说"尤利西斯")采用不同策略:通过 All-to-All 通信在序列维度和注意力头维度之间做转置。
核心思路——两次 All-to-All:
初始状态:每 GPU 持有完整序列的
,所有注意力头 : [bsz, seq/N, num_heads, head_dim]
gather_seq_scatter_heads(第一次 All-to-All):收集完整序列,分散注意力头
[bsz, seq/N, num_heads, ...] → [bsz, seq, num_heads/N, ...]- 每 GPU 得到完整序列但只负责部分注意力头
Flash Attention 计算:每 GPU 用完整序列、部分注意力头做本地 Attention
gather_heads_scatter_seq(第二次 All-to-All):收集所有头结果,恢复序列分片
[bsz, seq, num_heads/N, ...] → [bsz, seq/N, num_heads, ...]
Ulysses 的关键优势:MLP 层的计算是 token-level 独立投影(token 之间没有交互),因此MLP 层不需要任何额外通信,只有 Attention 层需要 2 次 All-to-All。核心复杂性全在 Attention 层。
SP 与 FSDP 的协同
| 并行维度 | 优化对象 | 主要通信算子 |
|---|---|---|
| FSDP | 模型参数显存 | AllGather + ReduceScatter |
| SP(Ulysses) | 激活值显存 | All-to-All |
两者可以正交组合:SP 维度内做序列切分,DP 维度内做 FSDP 参数分片。
SP=4 (列) -->
DP=2 GPU(0,0) GPU(0,1) GPU(0,2) GPU(0,3) <- DP Group 0
(行) GPU(1,0) GPU(1,1) GPU(1,2) GPU(1,3) <- DP Group 1verl 中的 SP 实现
verl 框架通过 Monkey Patch 实现 Ulysses SP:
# verl/models/transformers/monkey_patch.py
# 将标准 Flash Attention 替换为 Ulysses 版本
_flash_attention_forward => _ulysses_flash_attention_forward数据处理流程:先将输入 padding 到能被 sp_size 整除的长度,再通过 slice_input_tensor 切分。dp_size = world_size // sp_size。
# verl 配置示例
ulysses_sequence_parallel_size = 4
# 每卡最大 token 数配置
actor_rollout_ref.actor.ppo_max_token_len_per_gpu = 8192
actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu = 8192
actor_rollout_ref.ref.log_prob_max_token_len_per_gpu = 8192SFT vs. RL 中的 SP 差异:
- SFT:对 prompt + response 全部切分(response 是监督数据,已知)
- RL:inference engine 先完成 response 的 rollout,之后仅需对训练阶段做 SP
注意:LLaMA-Factory 官方版本不支持序列并行,需要使用 360-LLaMA-Factory(社区 fork)等方案。
14.6 FSDP 与 FSDP2
FSDP 的全分片机制、前向/反向流程、CPU Offload 权衡,以及 FSDP2 的改进。
FSDP 核心概念
FSDP 将模型的三类数据全部分片到所有 GPU:
| 分片对象 | DDP 行为 | FSDP 行为 |
|---|---|---|
| 模型参数(Parameters) | 每卡完整副本 | 切分,每卡持有 1/N |
| 梯度(Gradients) | 每卡完整梯度 | 切分,每卡持有 1/N |
| 优化器状态(Optimizer States) | 每卡完整副本 | 切分,每卡持有 1/N |
关键术语:
- FSDP unit:被
FSDP(...)包裹的模块,是分片的基本单元(可以是整个模型,也可以是每个 Transformer block) - flat param:该 unit 的所有参数被拼成一个连续大张量
- local shard:flat param 按 world size 切分后,本 rank 持有的那一片
- unsharded / full param:计算时临时 AllGather 回来的完整参数
- FULL_SHARD:参数/梯度都做分片管理(ZeRO-3 风格)
FSDP 的本质是数据并行:每个 rank 都在跑完整模型的计算图,只是输入数据不同(各自的 mini-batch shard)。每个 rank 不是"只算自己那部分 FSDP unit",而是对当前要执行的 unit 先 AllGather 出完整参数(临时),然后用本 rank 的本地数据做前向/反向,结束后再 reshard。
FSDP 前向/反向流程
前向传播(对每个 FSDP unit):
- 拿到 local shard(
cpu_offload=True时 shard 在 CPU) - CPU → GPU 传输(如有 offload)
- AllGather:所有 rank 贡献自己的 shard,临时拼成 full param
- 用 full param 执行该 unit 的前向计算
- 计算完成后立即 reshard(释放完整参数;offload 场景下 shard 可回 CPU)
反向传播:
- 再次 AllGather 当前 unit 的完整参数(计算梯度需要完整参数)
- 计算该 unit 的梯度
- ReduceScatter:将梯度规约并分散,每卡只保留对应 shard 的梯度
- 若
cpu_offload=True,梯度 shard 也可驻留 CPU
FSDP unit = Layer i:
Forward: [local_shard_i] --AllGather--> [full_param_i] --compute--> reshard
Backward: [local_shard_i] --AllGather--> [full_param_i] --grad--> ReduceScatter --> [grad_shard_i]CPU Offload:显存 vs. 速度的权衡
CPU Offload 将不参与计算的参数/梯度驻留到 CPU(cpu_offload=True),降低 GPU 常驻显存,但引入 CPU↔GPU 传输延迟。以 GPT-2 级别模型、2 张 GPU 的实测数据:
==================== CPU Offload = True ====================
FSDP 包装后静态显存:0.00 MB ← 参数全在 CPU
Forward 峰值显存:4082.86 MB
平均单步时延:59.40 ms ← 较慢(CPU↔GPU 传输开销)
==================== CPU Offload = False ====================
FSDP 包装后静态显存:1364.94 MB ← 参数分片驻留 GPU
Forward 峰值显存:4086.73 MB
平均单步时延:5.24 ms ← 快约 11 倍结论:CPU Offload 在峰值显存相近的前提下,将常驻显存降至接近零,但速度代价约 10x。适合显存极度紧张、对速度不敏感的场景。
FSDP2(fully_shard)的改进
FSDP2 是 PyTorch v2.4+ 引入的新 API(fully_shard),相比 FSDP1 的主要改进:
| 特性 | FSDP1 | FSDP2 |
|---|---|---|
| 参数表示 | FlatParameter(整 unit 拼平) | 逐参数 DTensor |
| AllGather 时机 | 进入模块时整块 AllGather | 算子级按需 JIT AllGather |
| 副本存留窗口 | 较宽 | 更窄(快速 reshard) |
| 重叠机会 | 有限 | 更多 |
| API 风格 | FullyShardedDataParallel(module) | fully_shard(module) |
| torch.compile | 兼容性一般 | 更好 |
FSDP2 使用 PyTorch 原生的
DTensor抽象,对每个参数独立管理分片,而非将整个 unit 拍平为一个大张量。这使得内存效率更高,与torch.compile的兼容性也更好。
参数/优化器 Offload 注意事项
actor.fsdp_config.param_offload = True
actor.fsdp_config.optimizer_offload = True开启 offload 时需注意:部分 worker 可能将参数保留在 CPU,而其他 worker 已经分片到 GPU,导致不对称的内存布局(asymmetric memory layout),需在框架层面协调。
torchrun 多机启动
# 单机多卡(--standalone 自动处理 addr/port,默认 127.0.0.1:29500)
torchrun --standalone --nproc_per_node=8 train.py
# 多机多卡(2 节点 × 8 卡)
# 节点 0:
torchrun --nnodes=2 --nproc_per_node=8 --node_rank=0 \
--master_addr=node0_host --master_port=29500 train.py
# 节点 1:
torchrun --nnodes=2 --nproc_per_node=8 --node_rank=1 \
--master_addr=node0_host --master_port=29500 train.py关键参数说明:
master_addr:主节点(Rank 0 节点)的 IP,是分布式通信的协调中心master_port:主节点上用于通信的空闲端口node_rank:当前机器在集群中的全局序号- 所有节点的
master_addr和master_port必须相同
14.7 Packing 技术
去除 Padding 浪费:Packing 技术通过密集排列有效 token 消除无效计算。
问题:Padding 的浪费
批量处理时,不同长度的序列需要 padding 到同一长度,产生大量无效计算:
句子 A:[tok1, tok2, tok3, tok4, tok5, tok6, tok7, tok8] (8 个有效 token)
句子 B:[tok1, tok2, tok3, tok4, PAD, PAD, PAD, PAD] (4 个有效 + 4 个 PAD)即便 PAD 位置在 attention_mask 中被屏蔽,模型仍需为这些位置分配显存和做部分计算。
unpad_input / pad_input
Flash Attention 库提供了高效的 padding 去除工具:
from flash_attn.bert_padding import unpad_input, pad_input, index_first_axis, rearrange
# 原始输入:input_ids (batch, seqlen), attention_mask (batch, seqlen)
# 去除 padding,得到密集 token 序列
input_ids_rmpad, indices, *_ = unpad_input(
input_ids.unsqueeze(-1), attention_mask
)
# input_ids_rmpad.shape = (total_valid_tokens,)
# indices.shape = (total_valid_tokens,) # 记录每个有效 token 在原始张量中的位置
# 位置编码同样需要对应去除
position_ids_reshaped = rearrange(position_ids.unsqueeze(-1), "b s ... -> (b s) ...")
position_ids_unpad = index_first_axis(position_ids_reshaped, indices).squeeze(-1)一个具体例子(使用 Qwen2.5-0.5B-Instruct):
prompts = ["你好,请给我介绍一下大型语言模型。", "今天天气怎么样?"]
# Tokenize 后:
# input_ids shape: (2, 8)
# attention_mask: [[1,1,1,1,1,1,1,1], [1,1,1,1,0,0,0,0]]
# unpad 后:
# input_ids_unpad shape: (12,) ← 8+4=12 个有效 token
# position_ids_unpad: [0,1,2,3,4,5,6,7, 0,1,2,3] ← 两个句子各自的位置为什么 unpad 不影响正确性
- Flash Attention 2 原生支持变长序列(
flash_attn_varlen),不需要方形注意力矩阵 position_ids_unpad保留了正确的位置信息,RoPE 等位置编码可正常工作- Transformer 对每个 token 的处理本质上是并行的,只要位置信息和 token 关系正确即可
实测结果验证:unpad 后的输出 logits 与标准 padded 输入的有效位置 logits 完全一致(torch.allclose == True)。
最优实践
在第一个 encoder block 之前做一次 unpad,在最后一个 block 之后做一次 pad,而不是每层都 unpad/pad。这样所有中间层(LayerNorm、FFN 等)都在密集 token 上计算,效率最高。
——Flash Attention 官方建议(GitHub Issue #11)
verl 中的 Packing 配置
# verl 性能调优参数
use_remove_padding = True # 开启 sequence packing(即 rmpad)14.8 HuggingFace Transformers 基础设施
HuggingFace 模型加载配置、Base 与 Instruct 模型的区别,以及 CUDA 初始化要点。
模型加载关键参数
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16, # 使用 BF16 精度
attn_implementation="flash_attention_2", # 启用 Flash Attention
device_map="auto", # 自动分配到多 GPU
trust_remote_code=True,
)device_map='auto':按模块粒度自动将模型层分配到多张 GPU(pipeline-style),必要时还会用 CPU/磁盘做 offload。适合推理场景,训练时更推荐用 FSDP/DDP 显式控制。attn_implementation='flash_attention_2':启用 Flash Attention 2,显存效率与速度大幅提升。
Base 模型 vs. Instruct 模型
以 Qwen2.5 系列为例:
| 特性 | Base 模型 | Instruct 模型 |
|---|---|---|
| 用途 | 预训练基座,用于继续训练/微调 | 直接用于对话 |
| Chat Template | 通常无,需自己写 | 有官方 ChatML 模板,强依赖 |
| 对话稳定性 | 能对话但不稳定,未充分 align | 稳定 |
Qwen 官方明确建议:不推荐直接使用 Base 模型进行对话。Base 模型应通过 SFT、RLHF 等后训练步骤后再投入使用。
一个有趣的观察——对 Qwen2.5-7B 分析特殊 token 的 embedding 范数:
Qwen2.5-7B-Base:
全 vocab L2 范数:mean=0.7915, std=0.2046
<|endoftext|> (id=151643): norm=0.9746, z-score= 0.89 ← 有意义
<|im_start|> (id=151644): norm=0.0000, z-score=-3.87 ← 零向量!
<|im_end|> (id=151645): norm=0.0000, z-score=-3.87 ← 零向量!
Qwen2.5-7B-Instruct:
<|im_start|> (id=151644): norm=0.0100, z-score=-3.82 ← 经 SFT 后有了值
<|im_end|> (id=151645): norm=0.0105, z-score=-3.82Base 模型中 <|im_start|> 和 <|im_end|> 是零向量——这说明这两个特殊 token 是在 SFT 阶段才被训练的,在预训练阶段根本没被使用过。
进一步分析邻居 token:Base 模型中零向量的"邻居"全是 cos similarity=0 的随机 token($, %, #),而 Instruct 模型的邻居开始呈现有意义的模式。
Contamination 实验:Base 模型的 Benchmark 污染
Base 模型虽然缺少对话对齐,但在某些 benchmark 上可能表现出"虚高"的准确率。这是因为预训练语料中可能包含了 benchmark 数据本身(即 数据污染/contamination)。
实验设计:使用 MATH-500 数据集(HuggingFace HuggingFaceH4/MATH-500),仅向 Base 模型输入题目的前半句(prompt prefix),用 greedy decoding 观察模型能否续写出完整题目并给出答案。
from vllm import LLM, SamplingParams
model_id = "Qwen/Qwen2.5-7B" # Base 模型
prompts = [
"For how many positive integers $n>1$ is", # MATH-500
"Convert the point $(0,3)$ in rectangular coordinates", # MATH-500
"Josh decides to try flipping a house. He buys a house for $80,000 and then puts", # GSM8K
]
sampling_params = SamplingParams(
temperature=0, # Greedy decoding
top_p=1.0,
max_tokens=1024,
)
llm = LLM(model=model_id, max_model_len=4096, trust_remote_code=True)
outputs = llm.generate(prompts, sampling_params)关键观察——Base 模型在仅给定题目前缀时,能逐字续写出完整题目并生成正确答案:
Prompt: "For how many positive integers $n>1$ is"
Generation:
it true that $2^{24}$ is a perfect $n^{\text{th}}$ power?
... (完整推理过程) ...
The final answer is \(\boxed{7}\).
finish_reason: stop模型不仅补全了题目的后半部分(it true that $2^{24}$ is a perfect $n^{\text{th}}$ power?),还给出了完整的解题步骤和正确答案。这强烈暗示该题目(或极相似的文本)出现在了预训练语料中。
结论:在使用 Base 模型评估 benchmark 成绩时,必须警惕 contamination 导致的虚假高分。相关讨论可参考 arXiv:2507.10532。仅凭 benchmark 分数无法区分"模型真正学会了推理"和"模型记住了答案"。
Qwen3 系列模型
Qwen3 引入了思考模式的分化:
- Qwen3-Instruct:支持
enable_thinking=False关闭思考模式 - Qwen3-Instruct-2507:仅支持非思考模式,不生成
<think></think>块 - Qwen3-Thinking-2507:仅支持思考模式,默认模板自动包含
<think>
CUDA 初始化相关
# 延迟 CUDA 模块加载
export CUDA_MODULE_LOADING=LAZY在多进程分布式训练中,延迟 CUDA 初始化可以避免启动时多个进程同时抢占 GPU 资源。需注意:
| 操作 | 是否触发 CUDA 初始化 |
|---|---|
import torch | 否 |
torch.cuda.is_available() | 否 |
from torch.distributed.device_mesh import init_device_mesh | 是 |
# CUDA 版本检查常用命令
python -c "import torch; print(torch.__version__)"
python -c "import torch; print(torch.version.cuda)"
python -c "import torch; print(torch.cuda.get_arch_list())"nvidia-smi vs. nvitop:
nvidia-smi是 GPU 卡级别查看工具,nvitop是 GPU 进程级别查看工具,调试时后者更常用。
环境 Bring-up 与常见排障
分布式训练环境的首次搭建(bring-up)往往比训练本身更耗时。以下是经验总结的排障清单:
pip 安装注意事项:
# 使用 --no-cache-dir 避免旧缓存干扰
pip install --no-cache-dir vllm默认情况下 pip 会缓存下载的 .whl 文件(~/.cache/pip)。当频繁切换 CUDA 版本或 PyTorch 版本时,旧缓存可能导致安装了不兼容的包。--no-cache-dir 强制重新下载,安装后不保留 .whl 文件。
CUDA Runtime 安装:
# 在 conda 虚拟环境中安装独立的 CUDA 运行库(不影响宿主系统)
conda install -c nvidia/label/cuda-12.6.0 cuda-toolkit
nvcc --version # 验证安装注意:
nvidia-smi显示的是驱动支持的最高 CUDA 版本,而非实际安装版本。例如驱动显示 12.1,仍可运行torch 2.8.0+cu128的 GPU 程序——只要驱动版本向下兼容。
verl 安装:
verl 的安装脚本位于 verl/scripts/install_vllm_sglang_mcore.sh,也可手动安装。关键依赖顺序:先安装 vLLM(耗时较长,会自动安装所需的 PyTorch 版本),再安装 verl 本身。
常见错误诊断流程:
- CUDA OOM:先用
nvitop确认是否有残留进程占用显存;检查device_map配置是否合理 - 版本不兼容:运行上述 CUDA 版本检查命令,确认 PyTorch 编译的 CUDA 版本与系统 CUDA 匹配
- 分布式通信失败:检查
NCCL_DEBUG=INFO日志,确认网络互通、端口未被占用
14.9 Ray 集群与调试
Ray 集群的启动、资源管理与调试方法,在 verl 框架中的实践要点。
Ray 基本概念
Ray 是一个通用的分布式计算框架。在 LLM 训练中(尤其是 RLHF/verl 框架),Ray 被广泛用于:
- 统一管理多节点 GPU 资源(通过
ray status查看) - 协调 Actor(训练模型)、Rollout Engine(推理)、Reward Model 等多角色
- 提供 remote function/actor 的任务调度
集群启动
Ray 采用 Head Node + Worker Node 架构:
# Head Node(头节点)
ray start --head --port 6379 --dashboard-host 0.0.0.0
# Worker Node(工作节点)
ray start --address='head_node_ip:6379'
# 查看集群状态(所有被 Ray 统一管理的资源)
ray status避免 RAY_ADDRESS 冲突
在共享集群上使用 verl 时,如果已有其他用户启动的 Ray service,verl 可能会连接到错误的集群:
# 方案 1:清除环境变量,让 verl 启动 local ray cluster
unset RAY_ADDRESS
# 方案 2:在 verl 配置中显式指定
+ray_kwargs.ray_init.address=localRay Debugger
在 Slurm 集群上配合 Ray 使用调试器:
# 启动 Head 节点并开启外部调试器
ray start --head --ray-debugger-external --port 6379 --dashboard-host 0.0.0.0注意事项:
- 检查 ray 和 debugpy 的版本兼容性(
pip show ray/pip show debugpy) - 在 Slurm 集群上,配置 Ray distributed debugger 时应使用具体计算节点的 IP,而非 127.0.0.1
14.10 Slurm 集群管理
Slurm 作业调度系统的核心概念、资源申请与监控命令。
Slurm 核心概念
Slurm 是高性能计算中心(HPC)最常用的作业调度系统。核心角色:
- Login Node(登录节点):用户 SSH 登录的入口,不直接做 GPU 计算
- Compute Node(计算节点):拥有 GPU 的实际计算节点(如 DGX/HGX)
- 共享文件系统:"文件共享,环境复制,但硬件隔离"——Login Node 和 Compute Node 挂载同一 NFS/Lustre/GPFS,代码文件双向可见
当执行
srun时,Slurm 默认会把 Login Node 当前激活的 Python 环境带到 Compute Node 上执行。
环境配置
# 加载 Slurm 和 CUDA 模块
module purge
module avail slurm
module load <slurm_version>
module avail cuda
module load cuda12.6
echo $CUDA_HOME
nvcc --version资源申请与作业提交
交互式申请(常用于调试):
# 1. 申请资源("包场")
salloc --partition=gpu --qos=high --account=mylab \
-c 32 --mem=768G --gres=gpu:4 --time=72:00:00 -N 1
# -c 即 --cpus-per-task
# 2. 进入计算节点
srun -J $JOB_ID --pty /bin/bash
# 3. 在计算节点上执行训练
# srun 会读取 salloc 创建的环境变量,将命令远程传送到计算节点执行
srun python train.py批量提交:
sbatch submit_job.sh作业监控
# 查看队列
squeue -u $USER # 当前用户所有作业
squeue -t PD # 排队中的作业
squeue -t R # 运行中的作业
# 查看作业详情
scontrol show job $JOBID
# 查看资源使用
seff $JOBID # CPU 使用情况
sacct -j $JOBID -o JobID,AllocTRES%50 # GPU 使用情况
# 查看节点状态(推荐设置 alias)
alias sf='sinfo --Format "NodeList:|,NodeAddr:|,StateLong:|,Partition:|,Time:|,CPUs:|,CPUsState:|,CPUsLoad:|,FreeMem:|,Gres:|,GresUsed" | column -t -s "|"'14.11 量化参数
量化的基本原理与主流格式(INT4/INT8/MXFP4),以及 BitsAndBytes 配置方法。
量化的基本原理
量化(Quantization)通过降低参数的数值精度来减少显存占用:
| 精度 | 每参数字节数 | 120B 模型所需显存 |
|---|---|---|
| FP32 | 4 bytes | 480 GB |
| BF16 / FP16 | 2 bytes | 240 GB |
| INT8 | 1 byte | 120 GB |
| INT4 / FP4 | 0.5 bytes | 60 GB |
计算示例:GPT-OSS 系列的 120B 模型使用 MXFP4 量化:
理论上只需 60 GB 显存即可加载。
主流量化格式
- INT4:最常用的量化格式。Kimi-K2.5 采用与 Kimi-K2-Thinking 相同的原生 INT4 量化方法。
- MXFP4(Microscaling FP4):更精确的 FP4 格式,GPT-OSS 系列提供官方 MXFP4 量化版本。
- BF16:训练常用精度,兼顾精度与效率,是大多数训练框架的默认选择。
模型加载中的量化配置
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
# INT8 量化
quantization_config = BitsAndBytesConfig(load_in_8bit=True)
# INT4 量化(QLoRA 常用配置)
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=quantization_config,
)14.12 WandB 实验追踪
WandB 实验追踪的基本配置与 RLHF 训练中的关键监控指标。
基本配置
# 登录 WandB
export WANDB_MODE=online
wandb loginverl 中的 WandB 集成
verl 框架内置了 WandB 支持,通过配置直接启用:
trainer:
logger: ['console', 'wandb']
project_name: 'my-rl-training'
experiment_name: 'qwen2.5-7b-grpo'关键监控指标
训练 RLHF 时,WandB 中常见需要追踪的指标:
| 指标 | 含义 | 异常信号 |
|---|---|---|
train/reward | 平均奖励 | 应稳定上升 |
train/kl_div | KL 散度(与 ref policy 的距离) | 过大则 reward hacking |
train/response_len | 生成长度 | 长度崩塌是训练问题的信号 |
train/entropy | 策略熵 | 过低说明模式崩溃 |
train/loss | Actor loss | 需与 reward 结合判断 |
本章小结
3D 并行(DP/TP/PP)是大模型训练的核心框架。DP 切数据、TP 切层内算子、PP 切层——三者正交组合,按
配置。TP 放机内(利用 NVLink 高带宽),PP 跨机(容忍网络延迟),DP 扩吞吐。 NCCL 提供 AllReduce、AllGather、ReduceScatter、All-to-All 等高效通信原语。DDP 通过 Bucket 机制实现计算与通信重叠——梯度从后往前算,算好的 Bucket 立即发起 AllReduce,与后续梯度计算并行执行。
FSDP/FSDP2 将参数、梯度、优化器状态全部分片,显著降低单卡显存占用。FSDP2 基于 DTensor 逐参数管理,AllGather 时机更精细,内存效率更高。CPU Offload 可进一步降低常驻显存但有约 10 倍速度代价。
序列并行(Ring Attention、DeepSpeed Ulysses)解决了超长序列(>128K)训练中的显存瓶颈。Ring Attention 通过 Ring 通信轮转 K/V;Ulysses 通过两次 All-to-All 在序列维度和注意力头维度间转置,MLP 层零额外通信。
Packing 技术通过去除 padding、密集排列有效 token,消除无效计算。配合 Flash Attention 的变长序列支持,在第一层 unpad、最后一层 pad,所有中间层都在密集数据上运行。
Ray + Slurm 是业界主流的集群管理组合。Slurm 负责 HPC 资源调度,Ray 负责多角色任务编排。verl 框架原生集成 Ray,用
unset RAY_ADDRESS或address=local避免集群冲突。量化(INT4/INT8/MXFP4)是降低显存需求的核心手段——120B 模型使用 INT4 理论仅需 60 GB 显存。BitsAndBytes 提供 HuggingFace 生态内的开箱即用量化方案。
延伸阅读
- NCCL 官方文档:https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/usage/collectives.html
- FSDP1 vs. FSDP2 对比:https://huggingface.co/docs/accelerate/concept_guides/fsdp1_vs_fsdp2
- DeepSpeed Ulysses 论文:[arXiv:2309.14509] DeepSpeed Ulysses: System Optimizations for Enabling Training of Extreme Long Sequence Transformer Models
- Ring Attention 论文:[arXiv:2105.13120] Sequence Parallelism: Long Sequence Training from System Perspective
- HuggingFace 优化综述:https://huggingface.co/blog/Isayoften/optimization-rush
- verl 性能调优文档:https://verl.readthedocs.io/en/latest/perf/perf_tuning.html
- MILES(FSDP 深度解析):https://lmsys.org/blog/2025-12-03-miles-fsdp/
- FSDP 详解博客:https://karthick.ai/blog/2024/Fully-Sharded-Data-Parallel-(FSDP)/
- verl 系统设计文档:https://github.com/zhaochenyang20/Awesome-ML-SYS-Tutorial/blob/main/rlhf/sys-design/readme-2.md