深蓝黑背景封面。左侧是一段自下而上的阶梯:最底层灰色卡片「数据并行基线 ≈16N 字节/卡」,注「优化器状态 12N + 梯度 2N + 参数 2N,每卡全量复制」;向上三张卡片以向上箭头串联:Stage 1「切优化器状态 → ≈4N」,注「Adam 的 m、v 与 fp32 主参数按卡均分」;Stage 2「再切梯度 → ≈2N」,注「梯度只保留本卡负责的那一份」;最顶层青色高亮「Stage 3:参数也切 → ≈16N/n」,注「用到哪块参数,临时从别的卡取回」。阶梯下方注「代价:通信量逐级上升,用带宽换显存」。右侧大标题「ZeRO 的三个阶段各切分什么状态」,副标题「优化器状态、梯度、参数,逐级去重」,补充「N 为参数量、n 为卡数;数据并行的冗余就是省钱空间」。左上标注「面试经典 · 显存优化」,右上圆角胶囊标注「Day 05 · 11-22」。 图 1:ZeRO 三级阶梯,每上一级多切一种状态。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。「ZeRO 三个阶段各切什么」是大模型训练方向的高频考点,答案本身一句话能背完,但面试官真正想听的是:为什么这些状态值得切,以及切了之后通信上付出了什么。

先算基线:数据并行的显存到底浪费在哪。

普通数据并行下,每张卡都完整保存一份模型状态。以混合精度训练加 Adam 优化器为例,设参数量为 N,每卡要扛的静态显存大约是 16N 字节:fp16 参数 2N、fp16 梯度 2N,再加上优化器状态 12N——Adam 要存 fp32 主参数副本 4N、一阶动量 m 4N、二阶动量 v 4N。注意这个比例:优化器状态占 12N,是全部静态显存的四分之三,参数本体反而只是零头。

n 张卡就存了 n 份一模一样的 16N,这就是 ZeRO 眼中的「冗余」。2019 年的 ZeRO 论文(arXiv:1910.02054)把核心思想说得很直白:每张卡只保存模型状态的 1/n,全集群合起来恰好一份,省出来的显存用来装更大的模型。

Stage 1:先切最肥的优化器状态。

第一刀砍向占比最大的那块。Stage 1 把优化器状态(fp32 主参数、m、v)按卡均分,每张卡只负责更新自己那 1/n 参数对应的状态。梯度仍然 AllReduce 后全量保留,参数也仍然每卡全量。每卡静态显存从 16N 降到约 4N + 12N/n。论文的关键结论是:这一阶段的通信量与普通数据并行相同,等于白捡显存。

Stage 2:梯度也只留自己那份。

既然每张卡只更新 1/n 的参数,那它只需要这 1/n 参数对应的梯度。Stage 2 在反向传播中把梯度按归属切分:一张卡算出别人的梯度就直接规约掉,只保留自己负责的那一份。每卡静态显存降到约 2N + 14N/n,通信量依然与数据并行持平。到这一步,8 卡集群已经把 16N 摊到了约 4N 以内。

Stage 3:连参数本体也切了。

最激进的一刀:fp16 参数本身也按卡分片,每张卡只存 1/n。前向、反向算到某一层时,临时从其他卡把该层参数 AllGather 回来,用完即释放;下一层再重复这个动作。每卡静态显存降到 16N/n,卡越多单卡越省,模型规模可以随卡数近似线性扩展。代价是通信明显更频繁——参数要「边算边取」,论文给出的量级是通信量较基线增加约五成,本质是用带宽换显存。

用一段脚本把三阶段的账算清楚。

下面用 Node.js 22.15.0 实测(node zero.js),按论文示例取 N=7.5B、n=64:

function perGPU(N, n, stage) {
  const param = 2 * N;                       // fp16 参数
  const grad = 2 * N;                        // fp16 梯度
  const optim = 4 * N + 4 * N + 4 * N;       // fp32 主参数 + m + v
  const base = param + grad + optim;
  const s1 = param + grad + optim / n;       // 只切优化器状态
  const s2 = param + (grad + optim) / n;     // 再切梯度
  const s3 = (param + grad + optim) / n;     // 参数也切
  return [base, s1, s2, s3][stage];
}
const N = 7.5e9, n = 64, GB = 1024 ** 3;
for (const s of [0, 1, 2, 3])
  console.log(`Stage ${s}: 每卡 ${(perGPU(N, n, s) / GB).toFixed(1)} GiB`);

实测输出:

Stage 0: 每卡 111.8 GiB
Stage 1: 每卡 29.2 GiB
Stage 2: 每卡 15.5 GiB
Stage 3: 每卡 1.7 GiB

64 卡时 Stage 3 把每卡静态显存压到基线的约 1/64,数字与论文的「随数据并行度线性下降」吻合。

深蓝黑背景概念图。主体是一张四列三行的切分对照矩阵:列分别为数据并行基线、ZeRO Stage 1、Stage 2、Stage 3;行分别为优化器状态(12N)、梯度(2N)、参数(2N)。基线列三行均为「每卡全量复制」;Stage 1 只把优化器状态变为「切分 · 每卡 1/n」;Stage 2 让优化器状态与梯度都切分,参数仍全量;Stage 3 三行全部切分,该列以青色描边高亮并注「每卡 ≈16N/n」。矩阵下方两张小卡:青色卡「省显存的本质:n 张卡各存一份冗余,合成 1 份按卡切片」;紫色卡「代价:Stage 越高通信越频繁,参数要边算边取」。底部总结条:选 Stage 看显存缺口——一般 Stage 1/2 已够用,Stage 3 留给单卡实在放不下的超大模型。 图 2:三阶段对照矩阵,Stage 越高切得越狠、通信越勤。

答题时把「怎么选」补上才是完整答案。

工程上的取舍很直接:Stage 1、2 通信量不增,属于稳赚,大多数场景够用;只有单卡实在放不下参数本体时才上 Stage 3,用通信换规模。如果显存还是不够,DeepSpeed 还提供了把状态继续下放到 CPU 内存甚至 NVMe 的 offload 路线(ZeRO-Offload / ZeRO-Infinity),那是「内存补显存」的另一层兜底。

回到面试现场,建议的结构是:先摆基线账 16N 并指出优化器状态占四分之三,再按「优化器状态 → 梯度 → 参数」的顺序讲三阶段各自切什么、每卡剩多少,最后主动补一句通信代价与选型原则。只背「Stage 1 切优化器、Stage 2 切梯度、Stage 3 切参数」是及格线;讲清「为什么先切优化器状态」和「Stage 3 的参数是边算边取的」才是加分项。你在实际训练里开到过 Stage 几?通信开销有没有成为过瓶颈?欢迎聊聊。