显存账本与混合精度
第 14 课:显存账本与混合精度
上一课讲了"一张卡放不下怎么切",但只用了一个结论——每参数 16 字节。这一课把这个结论拆到底:16 字节分别属于谁、为什么优化器状态占了四分之三、激活为什么会和权重同量级、ZeRO(即把优化器状态、梯度、参数分片存到各卡的省显存方案)三把刀各砍掉什么、FP8 到底省的是显存还是算力。最后合起来算一道实操题:训一个 70B,到底要几张 80 GB 卡。
② 写出 ZeRO-1/2/3 的每参数显存公式,并判断哪一档能把 70B 放进 80 GB 卡
③ 用激活公式与重算口径算出一份显存预算,给出"至少几张卡"的答案
上一课我们只借用了"16 B/参数"这一个结论,没有把它拆开。这一课拆到底——先把账算清,才谈得上省。
先把四个名词说白:权重(weight)是模型参数本身;梯度(gradient)是这一步要往哪个方向改;优化器状态(optimizer state)是优化器为了算出这个方向而替每个参数记的账;激活(activation)是前向算出来、反向还要再用的中间量。可以把这四项类比成一个装修现场:家具(权重)、搬运清单(梯度)、两份账本加一份原始图纸(优化器状态),以及施工中摊开的所有材料(激活)——只有最后一项能在收工后收起来。
| 项 | 内容 | 精度与字节 | 70B 合计 |
|---|---|---|---|
| 权重 | 参与前向与反向的模型参数 | bf16,2 B/参数 | 140 GB |
| 梯度 | 每个参数这一轮的更新方向 | bf16,2 B/参数 | 140 GB |
| 优化器状态 | fp32 master 权重 + Adam 两个动量 | 12 B/参数 | 840 GB |
| 激活 | 前向产出、反向要用的中间量 | 随序列长度变化 | 见 Card 4 |
| 前三项合计 | — | 16 B/参数 | 1.12 TB |
| 项 | 每参数字节 | 70B | 占 16 B 的比例 |
|---|---|---|---|
| bf16 权重 | 2 | 140 GB | 12.5% |
| bf16 梯度 | 2 | 140 GB | 12.5% |
| fp32 master 权重 | 4 | 280 GB | 25% |
| Adam 一阶动量 m | 4 | 280 GB | 25% |
| Adam 二阶动量 v | 4 | 280 GB | 25% |
| 合计 | 16 | 1.12 TB | 100% |
| 对比 | 按什么规模增长 | 70B 在 4K 序列下的量级 |
|---|---|---|
| 权重 / 梯度 / 优化器状态 | ∝ 参数量(固定) | 1.12 TB(与序列长度无关) |
| 激活 | ∝ 序列长度 × 批大小 × 隐藏维 × 层数 | 与 1.12 TB 同量级(Card 4 精算) |
训练显存只有四项,但它们的"增长方式"完全不同:权重、梯度、优化器状态 ∝ 参数量(与序列无关);激活 ∝ 序列长度(与参数量无关)。这一条决定了后面所有手段的分工——砍前三项要靠切分与低精度,砍激活只能靠"别存它"(重算)。先把四项分开算,是本节接下来 5 张卡片的共同地基。
上一张卡片看到四项里优化器状态最大。这一张把它拆开,看这 12 B/参数 到底花在哪。
先给个直觉:梯度(gradient)只告诉你"这一步往哪走",而 Adam 还想记住"过去几十步都往哪走、走得有多抖"——这就是两个动量的作用。可以把它类比成开车:梯度是这一刻的方向盘角度,一阶动量是你最近的行驶方向,二阶动量是路面的颠簸程度——后两者都要随时间累积,所以必须一直留着。
| 项 | 字节 | 作用 | 能不能省 |
|---|---|---|---|
| fp32 master 权重 | 4 | 累积更新、避免 bf16 舍入 | 不能(精度需要) |
| 一阶动量 m | 4 | 平滑梯度方向 | 不能(Adam 的定义) |
| 二阶动量 v | 4 | 自适应学习率 | 不能(Adam 的定义) |
| 合计 | 12 | — | — |
| 模型 | 参数量 | 前三项(16 B/参数) | 80 GB 卡至少 |
|---|---|---|---|
| 7B | 7e9 | 112 GB | 2 张(112 ÷ 80 = 1.4) |
| 13B | 13e9 | 208 GB | 3 张(208 ÷ 80 = 2.6) |
| 70B | 70e9 | 1.12 TB | 14 张(1120 ÷ 80 = 14,整除) |
| 405B | 405e9 | 6.48 TB | 81 张(6480 ÷ 80 = 81) |
| 优化器 | 优化器状态/参数 | 总账/参数 | 70B 总账 |
|---|---|---|---|
| SGD + 动量 | 4 B 动量 + 4 B master = 8 B | 12 B | 840 GB |
| AdamW | 8 B(m+v)+ 4 B master = 12 B | 16 B | 1.12 TB |
| 8-bit Adam | 2 B(量化 m+v)+ 4 B master = 6 B | 10 B | 700 GB |
优化器状态之所以最大,是因为它为每个参数存了三份 fp32(master 权重 + 两个动量 = 12 B),而参数本身只占 2 B。它不是"多余的开销",而是 Adam 能自适应调学习率的前提——想省,只有两条路:换成更省的优化器(SGD / 8-bit),或把它切开分给其他卡(这正是下一张卡片的 ZeRO)。
上一张卡片确定了一件事:优化器状态是最大的一项,所以先切它最划算。这就是 ZeRO 三把刀的分工。
先把机制说白:ZeRO 不改变训练数学,只改变"东西存在哪、什么时候通信"——梯度(gradient)与参数在该切的时候切、该拼的时候拼。可以把它类比成合租公寓——原本每人房间里都存一整套共用的工具(8 个人就 8 套,纯浪费),ZeRO 改成工具放公共储藏室、每人只负责其中一柜,用到时才去取。
| 方案 | 每参数显存(N = DP 度) | N = 8 时 | 70B(N = 8) |
|---|---|---|---|
| DDP | 16 B | 16 B | 1.12 TB |
| ZeRO-1 | 4 + 12/N | 5.5 B | 385 GB |
| ZeRO-2 | 2 + 14/N | 3.75 B | 262.5 GB |
| ZeRO-3 | 16/N | 2 B | 140 GB |
| 方案 | 每卡 | 8 张卡合计 | 冗余 |
|---|---|---|---|
| DDP | 1.12 TB | 8 × 1.12 TB = 8.96 TB | 8 份完全相同的状态 |
| ZeRO-1 | 385 GB | 3.08 TB | 1 份状态 + 8 份权重与梯度 |
| ZeRO-3 | 140 GB | 1.12 TB | 1 份 |
| 方案 | 每步通信 | 相对 DDP |
|---|---|---|
| DDP | 梯度 all-reduce(环形 ≈ 2 份量) | 1.0×(基准) |
| ZeRO-1 | 梯度 reduce-scatter + 参数 all-gather | 1.0×(与 DDP 相同) |
| ZeRO-2 | 梯度 reduce-scatter + 参数 all-gather | 1.0×(与 DDP 相同) |
| ZeRO-3 | 前向 all-gather 参数 + 反向 all-gather 参数 + 梯度 reduce-scatter | 约 1.5×(消息也更碎) |
三把刀砍的对象完全不同:ZeRO-1 砍优化器状态(12 B → 12/N)、ZeRO-2 再砍梯度(+2/N)、ZeRO-3 连权重也砍(+2/N)。但有一条容易忽略的推论:ZeRO-1 与 ZeRO-2 都要在每张卡上保留完整 bf16 权重(2 B/参数 = 140 GB);ZeRO-1 还多留一份完整梯度(再 2 B,合计 280 GB)——两者单卡都 ≥ 140 GB,永远装不进 80 GB 卡。真正能做到的只有 ZeRO-3(Card 6 会用到这个结论)。
上一张卡片把状态切完了,但还有一项切不动——激活。它不是"每个参数一份",而是"每个中间张量一份",所以换一套算法来省:不存,等反向用时重算(activation recomputation,激活重算)。
为什么值得这么做?因为激活里有一项的规模随序列长度平方增长(注意力分数矩阵,形状是 token 数 × token 数),而它"占地方大、算起来却便宜"。把这类比成做菜:与其把切好的备料全摆在台面上占满厨房,不如用到时再切一次——多花点时间,台面一下就空了。
| 变量 / 项 | 取值 | 算式 |
|---|---|---|
| s(序列长度) | 4096 | — |
| b(微批大小) | 1 | — |
| h(隐藏维) | 8192 | — |
| a(注意力头数) | 64 | — |
| 5·a·s/h(二次项) | 160 | 5 × 64 × 4096 ÷ 8192 |
| 每层系数 | 194 | 34 + 160 |
| 每层激活(元素) | 6.51e9 | 4096 × 8192 × 194 |
| 每层激活(bf16) | 13.0 GB | 6.51e9 × 2 B |
| 全模型(80 层) | 1.04 TB | 13.0 GB × 80 |
| 方式 | 存什么 | 算式 | 激活显存 |
|---|---|---|---|
| 全存 | 所有中间量 | 194 × 3.36e7 × 80 × 2 B | 1.04 TB |
| 选择性重算 | 存便宜的,重算 softmax 与注意力分数 | 34 × 3.36e7 × 80 × 2 B | 183 GB |
| 全量重算 | 只存每层输入 | 2 × 3.36e7 × 80 × 2 B | 10.7 GB |
| 方式 | 激活显存 | 额外时间开销 | 来源 |
|---|---|---|---|
| 全量重算 | 10.7 GB | +30 ~ 40% | NVIDIA 在 530B 上的实测 |
| 选择性重算 | 183 GB | < 10% | 论文:重算开销降低 > 90% |
| 不重算 | 1.04 TB | 0 | — |
激活是四项里唯一"可以不存"的——因为它是中间结果,丢了能重算。三种口径要按场景选:显存极紧就全量重算(10.7 GB,代价 30~40% 时间)、想两头兼顾就选择性重算(183 GB,代价 <10%)、显存够就直接存(1.04 TB)。工程上默认选择是第二种,而判断依据永远是同一句话:这一项占多少显存、重算它要多少 FLOPs。
上一张卡片用激活重算(activation recomputation)把激活压下来了,还有一路是低精度。但这里有一个常见误解要先破掉:FP8 不等于"显存减半"。
先把三个词分开:存储(一个数占几字节)、算力(张量核每秒能算多少次矩阵乘,也就是 FLOPs 的高低)、带宽(每秒能搬多少字节)。可以把它类比成换成更快的货车:跑得是快了(算力与带宽都上去),但仓库里那批必须常年存放的库存(优化器状态)一件都没少。这就是本张卡片要算清的事。
| 维度 | fp16 / bf16 | fp8 | 是否减半 |
|---|---|---|---|
| 表示一个数占的字节 | 2 | 1 | 是(权重与激活的存储副本可减半) |
| 张量核的矩阵乘吞吐 | 基准 | 约 2× | 是(是吞吐不是容量) |
| 参数与优化器状态的总账 | 16 B/参数 | 15 B/参数(只降 6%) | 否 |
| 项 | BF16 训练 | FP8 训练 | 变化 |
|---|---|---|---|
| 权重副本 | 2 B | 1 B | −1 B |
| 梯度 | 2 B | 2 B(通常留 bf16) | 0 |
| fp32 master 权重 | 4 B | 4 B | 0 |
| Adam m + v | 8 B | 8 B | 0 |
| 合计 | 16 B | 15 B | −6% |
| 精度 | 1.0 附近的舍入误差(roundoff,=半个间距 ULP) | 能否表示 1e-5 的更新 |
|---|---|---|
| fp8(E4M3,尾数 3 位,间距 0.125) | 约 0.0625 | 不能 |
| bf16(尾数 7 位,间距 7.8×10⁻³) | 约 3.9×10⁻³ | 不能 |
| fp32(尾数 23 位,间距 1.2×10⁻⁷) | 约 6.0×10⁻⁸ | 能 |
FP8 的收益几乎全在算力与带宽上(矩阵乘约 2× 吞吐、端到端 1.3~1.8×),显存只顺带降到 15 B/参数(−6%)。把它当"省显存手段"用,会失望;把它当"提速手段"用,很划算。而 fp32 master 必须留着,原因是纯算术的:更新量比低位宽的刻度还小,只有 32 位能累积。所以本课的省显存路线只有两条——ZeRO(切状态)与重算(不存激活),下一张卡片把两条合起来算总数。
上一张卡片证明了 FP8 省不了显存,所以能动的只剩两条:ZeRO 切状态与重算省激活。这一张把它们合起来,算出一个具体数字。
算账的顺序很重要,可以类比成搬家前估车:先量"必须带走的东西"(模型状态),再算"打包时才出现的临时占位"(激活),最后除以单车的容量——顺序反了就会反复推翻结论。
| 方案 | 每参数 | 理论最低(N→∞) | 80 GB 卡装得下吗 |
|---|---|---|---|
| DDP | 16 B | 1.12 TB | 装不下 |
| ZeRO-1 | 4 + 12/N | 4 B/参数 = 280 GB | 装不下 |
| ZeRO-2 | 2 + 14/N | 2 B/参数 = 140 GB | 装不下 |
| ZeRO-3 | 16/N | → 0 | 装得下 |
| 组合(s = 4096) | 状态 | 激活 | 合计 | 装得下吗 |
|---|---|---|---|---|
| ZeRO-3 N=16 + 全量重算 | 70 GB | 10.7 GB | 80.7 GB | 差 0.7 GB,不行 |
| ZeRO-3 N=32 + 全量重算 | 35 GB | 10.7 GB | 45.7 GB | 可行 |
| ZeRO-3 N=32 + 选择性重算 | 35 GB | 183 GB | 218 GB | 不行 |
| ZeRO-3 N=32 + 不重算 | 35 GB | 1.04 TB | 1.08 TB | 不行 |
| 余量来源 | 量级 | 为什么必须留 |
|---|---|---|
| 显存碎片与临时缓冲 | 若干 GB | 分配器做不到零碎片(按约 1.5× 余量示意估算,非官方默认值) |
| 集合通信缓冲 | 若干 GB | all-gather / reduce-scatter 需要临时区 |
| 吞吐目标 | 倍数关系 | 卡越多、每卡算得越少、越容易把通信藏起来 |
| 长序列与大批量 | 线性增长 | 激活随序列长度与批大小线性上涨 |
把三张卡片的结论串起来就是一条完整的账:① ZeRO-1/2 因为要留完整权重,永远装不进 80 GB,只有 ZeRO-3 可行;② 激活必须靠重算压到 10 GB 量级,否则永远超标;③ 两者的组合给出了明确的下限——32 张。算账的价值正在这里:它把"要几张卡"从一个经验数字,变成一句可以逐项复核的乘法与除法。