训练 Infra 05:框架景观——从 2.5 天到一夜跑完 2 epoch
拆解 transformers+DeepSpeed 与 Megatron-core 在 MoE 长序列上约 10 倍的性能差从哪来,梳理 mcore/SWIFT/FSDP/verl 的分层定位与 vllm 在训练管线中的三个角色,并把 cu128 这条约束链完整画出来。
训练 Infra 05:训练框架景观
本节属于大模型训练 Infra 实战课。前四节讲的是原理,这一节讲承载这些原理的软件栈——以及为什么换个框架能快一个数量级。
项目里有一个极有说服力的对照实验:
v1: transformers + DeepSpeed → 同数据 2.5 天
v2: Megatron-SWIFT (mcore 0.16) → 一夜跑完 2 epoch
保守折算(2.5 天 = 60 小时算 1 epoch,一夜 10 小时算 2 epoch):约 12 倍。
同样的模型、同样的数据、同样的 40 张卡。这一节就是解释这 12 倍从哪来,顺带把整个框架栈的分层理清。
1. 分层:谁在解决什么问题
先建立一张分层图,避免把不同层级的东西当成竞品对比:
┌─────────────────────────────────────────────────────┐
│ 任务层 SFT / DPO / RL(GRPO,PPO) │
│ Megatron-SWIFT · verl · TRL │
├─────────────────────────────────────────────────────┤
│ 并行引擎 megatron-core · DeepSpeed · FSDP │
│ (TP/PP/EP/CP + ZeRO + 通信重叠) │
├─────────────────────────────────────────────────────┤
│ 算子层 PyTorch · FlashAttention · Apex · TE │
├─────────────────────────────────────────────────────┤
│ 通信/运行时 NCCL · CUDA · 驱动 │
└─────────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────────┐
│ 推理引擎 vllm · SGLang (评测 / rollout / judge) │
└─────────────────────────────────────────────────────┘
关键认知:Megatron-SWIFT 不是 megatron-core 的竞品,它是骑在 mcore 上的任务层。 同理 verl 也是任务层,而且用的也是 mcore——这正是"一套 infra 两个入口"能成立的基础(第 8 节)。
2. transformers + DeepSpeed:好用,但不适合这个形状
定位
transformers 提供模型实现和训练循环,DeepSpeed 提供 ZeRO 分片。这套组合的最大优点是上手快:模型定义就是 HF 那份,配置就是一个 json,几乎不用改代码。
对于中小模型、短序列、稠密结构,它非常够用。
为什么在这个项目上慢 12 倍
原因不是一个,是一串。按影响排序:
(1) ZeRO-3 的每层 all-gather
35B 模型,DeepSpeed 想装下就基本只能上 ZeRO-3(切权重)。而 ZeRO-3 的代价是前向每一层都要 all-gather 参数、反向再来一次:
ZeRO-3 每步的参数通信 ≈ 2 × 层数 × (参数量 × 2B) / 有效带宽
对比 Megatron distributed optimizer(≈ZeRO-1/2):每步只通信一次梯度(第 4 节算过 24.1GB,占 0.8%)。ZeRO-3 是把这个量乘上层数量级。
这是最大的一块。
(2) MoE 的实现质量
MoE 的核心算子是"把 token 按路由重排 → 分组做 GEMM → 再排回来"。这里的工程差距很大:
- 朴素实现:为每个专家单独做一次小 GEMM,48 层 × N 个专家 = 上千次小 kernel 启动,GPU 大部分时间在等 kernel launch
- mcore 的做法:grouped GEMM(把多个专家的矩阵乘合成一次调用)+ 融合的 permute/unpermute kernel
3B 激活参数意味着单次 GEMM 本来就不大,kernel 启动开销占比会被放大。这一项在 MoE 上是数倍差距。
(3) 通信与计算重叠
第 4 节讲过的 bucket + 连续梯度 buffer。DeepSpeed 也有重叠机制,但 mcore 在长序列、EP 混合场景下的重叠做得更彻底。
(4) 算子融合
FusedAdam、fused layernorm/RMSNorm、fused RoPE、fused cross-entropy、TransformerEngine 的 fused attention。单项都是几个百分点,累起来可观。而且 fused CE 直接关系到第 2 节那个 15GB 的 logits 问题。
(5) recompute 粒度
mcore 支持按层、按块、selective 多种粒度,可以精细控制"省显存"和"多算"的比例。粗粒度的 full recompute 是 1.33 倍算力(第 2 节),而 selective 可以做到 1.1 倍。
结论
这 12 倍不是"DeepSpeed 差",是"形状不匹配"。 MoE + 49K 长序列 + 35B 参数,恰好踩中了 ZeRO-3 通信、小 GEMM、EP 实现三个短板。
如果是 7B 稠密模型 + 4K 序列,两者差距可能只有 20~30%,而 transformers+DeepSpeed 的开发效率优势会占上风。选框架要看形状,不看排行榜。
3. Megatron-LM / megatron-core
定位
megatron-core(mcore)是把 Megatron-LM 里的并行能力抽出来的库。它提供:
- 全部并行维度:TP / PP / EP / CP / SP,以及它们的组合
- distributed optimizer(第 3 节)
- 连续梯度 buffer + bucket 重叠
- 各种融合算子
- distributed checkpoint(每 rank 写自己的分片)
代价是侵入性强:模型必须按 mcore 的方式定义(TransformerLayerSpec 那一套),不能直接拿 HF 的实现来跑。这是它上手陡的根本原因。
项目底座
megatron-core 0.16
这个版本号很重要——它同时约束了 Megatron-SWIFT 和 verl 的可用版本,是整个技术栈的地基。
4. Megatron-SWIFT:任务层的粘合剂
它解决什么
裸 mcore 能跑训练,但不管这些事:
- HF 模型权重怎么转成 mcore 的分片格式(以及训完怎么转回去)
- SFT 的数据处理:chat template、多轮对话的 loss mask、packing
- 多模态输入怎么接
- LoRA、DPO 等各种训练目标
- 命令行参数、日志、评测的工程化
Megatron-SWIFT 就是把这些补齐,让你能用接近 HF 的体验拿到 mcore 的性能。这是项目选它的核心理由。
与 verl 共用底座的价值
Megatron-SWIFT (SFT) ─┐
├─→ megatron-core 0.16 ─→ safetensors
verl (RL) ─┘
同一个 mcore 底座 + safetensors 作为交换格式,意味着 SFT 产出的 checkpoint 可以直接喂给 RL,不需要写格式转换和分片重排的胶水代码。这在工程上省下的时间以周计,也是第 8 节"一套 infra 两个入口"的技术前提。
5. FSDP:PyTorch 原生路线
定位
FullyShardedDataParallel 是 PyTorch 官方的分片方案,思路上等价于 ZeRO-3(切参数+梯度+优化器状态),但是原生集成、没有额外依赖。
| FSDP | Megatron-core | |
|---|---|---|
| 并行维度 | 主要是分片 DP,TP/PP 要额外拼装 | TP/PP/EP/CP 齐全 |
| MoE 支持 | 需要自己搭 EP | 内建 |
| 侵入性 | 低(包一层就行) | 高(要按它的方式定义模型) |
| 长序列 | 需配合 CP 实现 | 内建 |
| 生态 | PyTorch 原生,版本跟得紧 | 独立发版 |
什么时候选 FSDP
- 稠密模型、不需要 TP/PP
- 想紧跟 PyTorch 版本、不想被第三方库的版本矩阵绑住
- 模型结构非标准,用 mcore 要大改
对本项目(MoE + EP + 长序列)不合适——你会把 mcore 已经做好的 EP 和 CP 重新实现一遍。
6. verl:RL 框架的架构
核心难题
RL(PPO/GRPO 这类)比 SFT 复杂的地方在于,一步里要做两种性质完全不同的计算:
1. rollout(生成) : 自回归 decode,memory-bound,要 KV cache,batch 动态
2. train(更新) : 大 GEMM,compute-bound,要梯度和优化器状态
这两件事的最优系统设计是相反的——推理引擎(vllm/SGLang)为 decode 优化,训练引擎(mcore)为 GEMM 优化。硬凑到一个引擎里,两边都不好。
verl 的思路
verl(HybridFlow)的做法是混合引擎 + 单控制器:
┌──────────── controller(单点,写起来像单机代码) ─────────┐
│ │
▼ ▼
┌──────────┐ prompt ┌─────────────┐ 经验 ┌───────────────┐
│ rollout │◀───────────│ 经验/缓冲 │────────▶│ train worker │
│ (vllm) │──响应─────▶│ │ │ (mcore/FSDP) │
└──────────┘ └─────────────┘ └───────────────┘
▲ ▲ │
│ │ │
└────── 权重同步 ────────┴──── reward/judge 服务 ──┘
三个关键设计:
- single-controller + multi-worker:控制流写成单机的顺序代码(“生成→打分→更新”),数据流由框架分发到各 worker。这让 RL 算法的逻辑可读,不至于淹没在通信代码里。
- 引擎可替换:rollout 用 vllm 或 SGLang,train 用 mcore 或 FSDP,按需组合。
- 权重同步是一等公民:训练更新完的权重要送回 rollout 引擎,这是 RL infra 最脏的一块(第 8 节详解)。
对本项目的含义
选 verl + mcore 后端,等于 SFT 和 RL 共享:并行配置的经验、显存账的算法、排障的工具链。第 1 到 4 节学的东西在 RL 阶段一条都不浪费。
7. 推理引擎在训练管线里的三个角色
很多人以为 vllm/SGLang 只是"部署用的"。在训练管线里它们至少有三个位置:
| 角色 | 干什么 | 要求 |
|---|---|---|
| 评测 | 每 N 个 checkpoint 跑一遍 benchmark | 吞吐优先,可离线批量 |
| rollout | RL 里生成候选响应 | 延迟+吞吐都要,需频繁热更新权重 |
| judge 服务 | 用另一个模型给响应打分 | 稳定、可并发、常驻 |
其中 rollout 的要求最苛刻:它需要在训练过程中反复被灌入新权重(每个 RL step 一次),而大多数推理引擎的设计假设是"权重加载一次就不动了"。这就是第 8 节要讲的权重同步问题。
8. 版本兼容矩阵思维:cu128 约束链
起点是一个不能动的数
GPU 驱动 → 支持的 CUDA 上限 = 12.8
驱动升级需要停机、需要运维窗口、在共享集群上可能根本不被批准。所以这是一个硬约束,其他所有东西都要绕着它走。
约束怎么传播
CUDA 12.8 (cu128)
│
├─→ PyTorch: 只能用有 cu128 wheel 的版本 → torch ≤ 2.11
│ │
│ ├─→ megatron-core: 需匹配 torch ABI → 0.16
│ │
│ ├─→ vllm: 每个版本钉死一个 torch → vllm ≤ 0.19
│ │
│ └─→ FlashAttention / TransformerEngine / Apex:
│ 都是编译型扩展,必须对齐 torch + CUDA
│
└─→ SGLang: 依赖链(flashinfer 等)要求更高的 CUDA → 出局
为什么"编译型扩展"是关键词
纯 Python 包升级无所谓。但训练栈里一大半是 C++/CUDA 编译出来的:
- FlashAttention、Apex、TransformerEngine
- vllm 的 attention/paged kernel
- megatron-core 的融合算子
这些包和 torch 的 C++ ABI 绑死。torch 版本一动,它们全部要重新编译或换版本。这是为什么"升级一个包"经常演变成"重建整个环境"。
实践规则
- 先定驱动/CUDA,再往上定 torch,最后定所有依赖。反过来做一定翻车。
- 写下来并锁死。一份
requirements.lock+ 一个镜像 tag,不要靠"pip install 最新版"。 - 矩阵表要包含编译型扩展的版本,不只是 Python 包名。
- RL 阶段约束更紧:训练侧要 mcore+torch,rollout 侧要 vllm+torch,两边必须是同一个 torch。这让可行版本的交集变得很小——本项目
torch ≤ 2.11且vllm ≤ 0.19就是这个交集的边界。 - 容器化。这是唯一能让"环境"变成可版本化、可回滚对象的办法(第 7 节)。
一个值得画出来的表
建议在项目里维护这样一张表,每次动任何一格都要全表复核:
| 组件 | 当前版本 | 被谁约束 | 升级会连带什么 |
|---|---|---|---|
| 驱动 / CUDA | 12.8 | 集群运维策略 | 一切 |
| torch | ≤ 2.11 | cu128 wheel 可用性 | mcore, vllm, FA, TE, Apex |
| megatron-core | 0.16 | torch ABI | Megatron-SWIFT, verl |
| Megatron-SWIFT | 对应 mcore 0.16 | mcore | SFT 入口 |
| verl | 对应 mcore 0.16 | mcore + vllm | RL 入口 |
| vllm | ≤ 0.19 | torch | rollout / 评测 / judge |
| SGLang | — | CUDA 需求过高 | 出局 |
9. 常见坑
- 按 benchmark 选框架。别人的 benchmark 是别人的形状。MoE+长序列这个形状会把结论完全颠倒。
- 以为 SWIFT 和 mcore 是竞品。它们是上下层关系,SWIFT 的性能就是 mcore 的性能。
- 在 ZeRO-3 上硬扛 MoE。每层 all-gather 参数 + 小 GEMM,两个短板叠加。
- 先装最新版再解决冲突。约束链是自顶向下的,反着来会陷入依赖地狱。
- 忘了 rollout 和 train 必须共享 torch 版本。这是 RL 阶段最容易被忽略的约束。
- 没有锁文件。三个月后"同一份代码"跑不出同样的结果,因为某个依赖悄悄升级了。
- 在宿主机上直接 pip install。第 7 节会讲这为什么是事故温床。
思考题
1. 假设明天运维批准把驱动升到支持 CUDA 12.9,SGLang 因此可用了。
- (a) 按约束链,你需要按什么顺序重新验证哪些组件?
- (b) 你会在什么时机做这件事——SFT 阶段中途,还是等 RL 开始前?为什么?
- © 换 SGLang 相比 vllm,对 RL 的 rollout 有什么可能的收益和风险?
2. 有人建议 RL 阶段 rollout 用 FSDP 后端、train 也用 FSDP(放弃 mcore),理由是"版本依赖更简单"。
- (a) 你会失去什么?(提示:回到第 3 节,EP 和 CP 怎么办)
- (b) 会得到什么?
- © 用 SFT 那 12 倍的教训来判断,这个交易划算吗?
下一节讲吞吐与效率:手把手用项目数字算 MFU——6ND 公式、recompute 系数 4/3、MFU 与 HFU 的区别,然后回答"H20 上 8-10% 是什么水平",并算出一个可能被忽略的 41% 的浪费。