原创教学项目 · 核对:2026-10-09。显存与 batch 数字用于手算,不是 GPU benchmark;没有执行多机训练。
单卡能启动,不代表适合长跑;单卡 OOM,也不代表必须上张量并行。先把问题拆成两类:放不下,还是跑不完。前者需要找显存占用,后者要看时间花在哪。
先量一个真实 batch
不要只跑一条特别短的样本。用实际长度分布,至少覆盖短、中、长几档,经过 warmup 再记录前向、反向、优化器更新和保存。许多优化器状态第一次 step 才分配,刚加载完的显存不能代表训练峰值。
| 观察到的情况 | 先查什么 | 可能有效的改动 |
|---|---|---|
| 模型加载就 OOM | 权重、初始化副本、设备放置 | 量化基座、分片(sharding)初始化,或更小模型 |
| 长样本反向时 OOM | 激活、attention 实现、临时缓冲 | 小 microbatch、activation checkpointing、合理长度预算 |
| 第一次 optimizer step OOM | 梯度、优化器状态、master weights | LoRA、状态分片,必要时 offload |
| GPU 周期性空闲 | 数据读取、CPU tokenize、同步、checkpoint I/O | 缓存预处理、检查 profile,而不是先加卡 |
activation checkpointing 是少存激活、反向时重算;磁盘 checkpoint 是保存训练状态以便恢复。名字像,解决的不是同一个问题。
显存先算下界,再测峰值
一个简化的全参 Adam 预算:BF16 权重 2 bytes、BF16 梯度 2 bytes、FP32 master weights 4 bytes、两个 FP32 动量合计 8 bytes,每参数约 16 bytes。这是明确假设下的估算;具体实现可能保留 FP32 参数或梯度,也可能没有独立 master copy。
若参数量是 0.6B,仅上述状态就是 $0.6\times10^9\times16=9.6\times10^9$ bytes,约 8.94 GiB。这不包括激活、logits、通信缓冲、allocator 预留和碎片。 LoRA 的可训练状态与此不同,不能直接套全参公式。
因此“小模型为什么用了很多卡”不能只看参数量。长上下文、批量吞吐、迭代时限都会影响资源;但这些也不能反过来证明很多卡就合理。需要拿相同任务的耗时、峰值显存和成本来解释。
DDP、FSDP、ZeRO、TP、PP 放在一起看
| 方式 | 拆什么 | 适合先考虑的情况 | 主要代价 |
|---|---|---|---|
| DDP | 数据;每卡保留完整训练副本 | 单卡放得下,希望并行处理更多数据 | 状态仍完整复制,梯度同步有成本 |
| FSDP2 | 参数、梯度、优化器状态 | 状态太大,但分层聚合可承受 | 参数 all-gather 与梯度 reduce-scatter;峰值不等于总状态除卡数 |
| ZeRO 1 / 2 / 3 | 依次增加优化器、梯度、参数的分片 | 需要逐步增加状态节省,且训练栈支持 | 更深分片通常带来更多状态交换;offload 另算传输成本 |
| TP | 一层里的张量计算 | 单层很大,设备互联足够好 | 层内频繁通信,通常更依赖高速互联 |
| PP | 不同层放到不同阶段 | 按层切分模型,配合合适的 microbatch 调度 | 流水线气泡、阶段负载不均、激活传输 |
具体分片行为见 PyTorch FSDP2 与 DeepSpeed ZeRO。旧 FSDP1 配置不能当作 FSDP2 接口直接照抄。实际方案可以组合,但先证明一个维度有必要,再增加另一个。
对于这个小型 LoRA 项目,如果单卡已经放得下,先把单卡实验做好;需要更多吞吐时再测 DDP。若全参状态放不下,再比较 FSDP / ZeRO。若主要是长序列激活占用,先查长度、attention 与重算;单靠参数分片可能帮助有限。
加卡以后,batch 和 loss 有没有偷偷变
在无 packing、固定样本数的简单设置中:
$$ B_{\mathrm{update}}=B_{\mathrm{micro}}\times A\times D. $$
$B_{\mathrm{micro}}$ 是每个数据并行(data parallelism)副本每次处理的样本数,$A$ 是梯度累积次数,$D$ 是数据并行度。8 张卡做 TP=2、DP=4 时,microbatch=2、累积=8,一次更新对应 $2\times8\times4=64$ 条样本,不是 128。这里没有 PP,也没有重复样本或不完整尾批。
样本数相同还不够。两张数据并行卡,第一张有 1 个有效 target,loss 总和 0.2;第二张有 3 个,总和 3。局部均值再平均是 $(0.2+1)/2=0.6$,全局 token 均值却是 $3.2/4=0.8$。
若底层对梯度做普通的 $D$ 卡平均,整次更新共有 $N$ 个有效 target,局部 loss 总和为 $S_{r,a}$,可让每个累积 microstep 反传 $D S_{r,a}/N$。各卡平均、各 microstep 累加之后,得到全局 token 均值的梯度。
这个推导假设没有其他自动缩放。 如果 trainer 已处理有效 token、累积除数或分布式缩放,不要再乘一次。$N$ 必须覆盖完整 accumulation window;最后不足窗口的 batch 也要重算。对照单进程拼接 batch 的梯度,才能确认实现,而不是看到 loss 数字接近就算通过。
吞吐看有效工作,不只看利用率
假设同一数据、同一有效 token 总量,单卡是 1,000 个有效 target token/s,4 卡是 2,800。加速比为 2.8,扩展效率为 $2.8/4=70\%$;训练更快,但卡时约是原来的 $4/2.8=1.43$ 倍。这组数是教学假设,不是推荐配置。
同时报告全部非 pad token/s、有效 target token/s、step time、峰值显存与保存停顿。SFT 中 prompt 不计 loss,但仍要计算;改变 prompt/answer 比例会影响两种吞吐的关系,不能靠删掉难例“提升效率”。
| 当前优先级 | 如何比较 |
|---|---|
| 尽快拿到结论 | 固定任务与验证频率,比端到端时间 |
| 控制成本 | 比同等质量下的卡时与存储 / I/O 成本 |
| 避免 OOM | 检查长度尾部、首个 optimizer step 和保存峰值 |
| 保持训练语义 | 对照样本 ID、有效 token 分母和一小步梯度 |
项目的标准库检查可复算 batch 与 padding;测试还用可选的 CPU PyTorch 梯度检查验证不等长分母。它们不是 NCCL / FSDP 性能测试。下一步是保存与恢复:分布式能跑,不代表中断后能接着跑。
本页术语 2
- 分片 sharding
- 把参数、梯度或优化器状态切开分给多张卡,每张只存其中一份
- 数据并行 data parallelism
- 每张卡拿不同的数据、算同一个模型,再同步梯度