激活为什么比参数还占显存?
本课一问:前四课把参数、梯度、矩阵乘都压下来了。可一旦把训练序列拉长,显存还是会爆——这时候占地方的已经不是参数了。激活为什么比参数还占显存?
这一课先不碰:把激活切到多张卡上(C10 的序列并行、上下文并行);线性注意力为什么没有 s² 这一项(C11);推理时的 KV cache(C3、C13)。
从已有出发
前四课一直在算「每个参数几个字节」。可训练一步留在显存里的,还有前向时存下来、留给反向用的中间结果——激活。点下面的「前向」:
- 解决了范围到 ±3.4 × 10³⁸,1 附近的间距 2⁻²³ ≈ 1.2 × 10⁻⁷,数值上几乎不用操心。卡在每个参数 4 + 4 + 4 + 4 = 16 字节(权重、梯度、Adam 的 m、v):锚点 27,781,427,952 个参数就是 444.5 GB,是 80 GB 单卡的 5.6 倍,还没算激活。
- 解决了矩阵乘和激活用 16 位,带宽与激活显存减半;FP32 主权重接住小更新,loss 乘 S 把小梯度推出下溢区,更新前再除回来。卡在指数只有 5 位:fp16(10⁻⁸) = 0,fp16(1 + 10⁻⁴) = 1,fp16(70,000) = inf;缩放倍数要一直调,溢出就得跳过这一步。
- 解决了8 位指数,范围与 FP32 相同:不再下溢,不需要 loss scaling。2026 年的默认训练精度。卡在1 附近间距粗到 1/128:bf16(257) = 256,bf16(1 + 10⁻³) = 1——主权重和优化器状态仍要 FP32,账本还是 16 字节/参数。
- 解决了激活每 1 × 128、权重每 128 × 128 一个缩放:张量 [1000, 0.01, 0.02, 0.5] 里,0.01 的误差从整张量缩放的 12.8% 降到 0.45%;DeepSeek-V3 相对 BF16 的 loss 相对误差 < 0.25%(官方自报)。卡在只有 256 个位型,E4M3 最大 448;块的形状要跟着离群值走——激活梯度改按 128 × 128 块量化,16B MoE 训到约 300B token 发散(附录 B.2);主权重和梯度仍是 FP32。
- 解决了MXFP4 每 32 个数共用一个 2 的幂(4.25 位/参数),NVFP4 每 16 个数一个 E4M3 缩放;K3、DeepSeek-V4 在后训练阶段对 MoE 专家权重做 QAT。卡在E2M1 只有 16 个码点:块 [0.3, −1.2, 2.7, 0.05] 用 MX 缩放后,0.05 → 0;全 FP4 预训练仍是前沿。
- 解决了不把 s × s 的注意力分数存下来:锚点一条 4,096 token 的序列,激活从 162.5 GiB 降到 42.5 GiB(选择性重计算 / FlashAttention);全量重计算每层只存输入,只剩 2.5 GiB(标准块公式近似)。卡在重计算用多算一遍前向换显存;可 444.5 GB 的模型状态一张 80 GB 卡仍然放不下 → C10 并行。
贯穿例句回来了。用「小明把苹果给了小王」这 5 个 token 训练锚点模型一步,按本课要讲的公式,64 层的激活一共约 55.9 MB——和上一课 444.5 GB 的模型状态比,可以忽略。可训练不会只喂 5 个 token:把这句话换成一条 4,096 个 token 的文档,激活就变成 162.5 GiB。s 从 5 变到 4,096,放大了约 820 倍,激活却放大了约 3,121 倍。多出来的那部分,就是本课要找的 s²。
先自己设计:前向的中间结果全部存下来
反向传播要用到前向的中间结果:算一个矩阵乘的权重梯度,要用到它的输入;算 softmax 的梯度,要用到它的输出。最直接的做法:前向每算出一个中间结果,就存着,等反向用完再释放。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
「激活怎么存」有四个选项,下面三节逐个讲。
这不只是锚点的账。ZeRO 论文算过一个更早的例子:GPT-2 1.5B(序列 1K、batch 32)的激活约 60 GB,而它的模型状态只有 24 GB 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条原文数字,核实 agent 在 PDF 原文核对。 出处ZeRO §3.2(需联网) ↗需联网 。R4 调研用 Qwen3-8B 的维度(s = 4,096、b = 1)算出 109 GiB、注意力分数占 82.5%,组件里切到 Qwen3-8B 可以复核。
这个公式从哪来
Korthikanti 等人(NVIDIA,2022)把一个标准 Transformer 块前向时要存的每一个张量逐个数了一遍。s 是序列长度,b 是 batch,h 是宽度,a 是注意力头数,每个数按 16 位(2 字节)存:
把「注意力分数」这一项画成张量,就看得见 s² 长在哪:
| 维度 | 含义 | 真实规模 | 小规模 |
|---|---|---|---|
| a | Q 头数(num_attention_heads) | 24 | 2 |
| s | 序列长度(一条 4,096 token 的训练序列) | 4096 | 4 |
| dh | 每头维度(head_dim) | 256 | 3 |
回到上面的堆叠条,试三件事:① 选锚点、「全存」,把 s 从 5 拖到 131,072,看「注意力分数」那一段怎么从看不见变成压倒一切;② 固定 s = 4,096,把 batch 从 1 拖到 8:激活正比增长,而模型状态那几段纹丝不动;③ 勾选「只有 16 个全注意力层带 s² 项」:锚点 64 层里只有 16 层是全注意力,激活从 162.5 GiB 降到 72.5 GiB——另外 48 层是线性注意力,没有 s × s 的表(C11 揭晓;这里 48 层仍按 34sbh 近似)。
常见误解一(旧版课程的错误):「激活和参数量成正比。」
旧版的显存组件把激活估成「每参数 3 字节」,于是拖参数量滑杆时,各部分比例永远不变。实际上激活由 s、b、h、L 决定:同一个模型,序列从 1K 拉到 8K,激活可以翻几十倍,参数一个没变;而注意力分数那一项和参数量完全无关,只和头数、序列长度有关。
常见误解二:「batch 翻倍,显存就翻倍。」
只有激活随 b 翻倍;16 字节的模型状态与 batch 无关。所以大模型训练里,「一张卡能放多大的 batch」是先扣掉模型状态,再看剩下的空间够放几条序列的激活。
改进一:梯度检查点——只存一部分,反向时重算
2016 年 Chen 等人的做法:前向时只在少数「检查点」上存激活,其余的算完就扔;反向走到哪一段,就从最近的检查点把那一段的前向再算一遍。n 层的网络选约 √n 个检查点,激活显存从 O(n) 降到 O(√n),代价是多一次前向。原文的例子:1,000 层的 ResNet 从 48G 降到 7G,多花约 30% 的时间 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条原文数字,Titan X;核实 agent 在 PDF 原文核对。 出处Chen et al. 2016 §5.4(需联网) ↗需联网 。
大模型里最常见的是每一层都做检查点:每层只存进入这一层的输入(16 位,每层 2sbh),反向时逐层重算。锚点 s = 4,096 时,激活从 162.5 GiB 降到 2.5 GiB,反向时再临时多一层完整激活。代价是整个前向多算一遍:C6 第 1 课说训练算力约 6ND(前向 2ND + 反向 4ND),开了全量重计算就是约 8ND——那里留的「重计算」的疑问在这里兑现。Korthikanti 的论文实测,全量重计算让训练慢 30% 到 40%。
改进二:选择性重计算——只重算最占地方、最不费算力的那一块
看公式的两项:34sbh 那部分来自矩阵乘的输入,重算它们要把矩阵乘再做一遍,很贵;5as²b 那部分来自 softmax 和 dropout,占的显存多,算起来却便宜。Korthikanti 等人因此只重算注意力的核心(QKᵀ、softmax、dropout、乘 V),其余照存:每层激活从 sbh(34 + 5as/h) 降到 34sbh。锚点 s = 4,096:42.5 GiB。论文把它和序列并行(C10)一起用,激活降到原来的约 1/5,重计算的额外开销减少 90% 以上;530B 模型在 2,240 张卡上的 MFU 从全量重计算的 42.1% 提到 54.2% 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条原文数字;54.2% 含 8 路数据并行,不含数据并行时为 56.0%。核实 agent 在 PDF 原文核对。 出处Korthikanti et al. 2022 摘要、§5(需联网) ↗需联网 。
改进三:FlashAttention——分数表根本不写进显存
2022 年 Dao 等人从另一个方向解决同一件事:注意力分数表 S、P 在朴素实现里要写进显存(HBM),再读回来做 softmax、乘 V,反向再读一遍。FlashAttention 把 Q、K、V 分块搬进芯片上的小而快的 SRAM,在块内算分数、用「在线 softmax」逐块累积结果,s × s 的表从不写进显存;反向时需要的分数,用前向存下的 softmax 统计量现算回来 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条分块 + 在线 softmax 在数学上与标准注意力逐位等价(除浮点舍入顺序外),所以论文称它为 exact attention;显存随序列长度线性增长。 出处Dao et al. 2022 摘要与 §3(本地 PDF) ↗ 。
它是精确的,不是近似;显存从 O(s²) 变成 O(s);而且因为省掉了大量显存读写,反而更快——GPT-2(序列 1K)端到端快 3 倍 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条原文:「3× speedup on GPT-2 (seq. length 1K)」,指端到端训练;常被引用的 7.6× 是只算注意力这一步的加速(原文图 1 右)。 出处FlashAttention 摘要(本地 PDF) ↗ 。C0 第 4 课说过,N² 这笔账后来被两条路分别处理,其中一条就是「不把整张表写出来」——在这里兑现;但 N² 次乘加一次都没省,另一条路(让大多数层根本没有这张表)在 C11。
在上面的堆叠条里,「FlashAttention」与「选择性重计算」在存储上是同一个效果(都去掉了 5as²b 那一项);差别在算力:前者少了显存读写、更快,后者要把分数重算一遍。
常见误解三:「FlashAttention 是一种近似注意力。」
不是。它改的是「怎么算」,不是「算什么」:分块 + 在线 softmax 与标准注意力在数学上等价,论文标题里就写着 exact attention。输出与标准实现的差别只来自浮点加法顺序的不同(和 C2 第 1 课那个「差值恰为 0」的注脚是同一件事)。
常见误解四:「重计算是免费的。」
全量重计算多一次前向,训练慢 30% 到 40%;选择性重计算便宜得多,但也不是零。显存和算力在这里是交换关系:省下的每一个字节,都要用重新算一遍来换。
一次迭代里,显存怎么涨、怎么落
上面的堆叠条是某一刻的截面。把时间轴展开:前向时激活逐层攒起来,反向时逐层释放、同时梯度逐层长出来,最后 optimizer.step()。这是按公式推出来的推演,不是实测:
试三件事:① 点「播放一次迭代」,看峰值出现在哪一刻(前向刚结束:激活攒到最多);② 把「第 1 次迭代」换成「第 2 次」:起点高了 222.3 GB——Adam 的 m、v 在第一次 step() 才分配,从第二次迭代起一直在(PyTorch 的显存博客记录过同一现象);③ 把「激活」换成「全量重计算」,看峰值降了多少、峰值的位置挪到了哪。
我们自己的模型上,实验脚本分阶段读了 4 次真实显存(建模型后 / 前向后 / 反向后 / 第一步后)。数据生成后,切到「我们的小模型」,黑点就是实测值,可以和推演的曲线对照:
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 长序列训练时激活放不下:不存注意力分数表,或只存检查点、反向时重算 |
| ② 数学上为什么可能有效? | 激活公式是逐张量记账 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条在论文 §4 写明的前提下(16 位、dropout、4h MLP、标准多头注意力)逐项相加得到;用于真实模型时是近似。 出处待补 ;FlashAttention 与标准注意力数学等价 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条在线 softmax 的分块累积是恒等变形。 出处FlashAttention §3(本地 PDF) ↗ |
| ③ 实验上为什么被选中? | 选择性重计算 + 序列并行把 530B 的 MFU 从 42.1% 提到 54.2%;FlashAttention 在 GPT-2 上快 3 倍;两者今天是训练的标配 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条原文自报的吞吐数字。 出处Korthikanti 2022;Dao 2022 ↗需联网 |
一句话
激活由 s、b、h、L 决定,和参数量无关;注意力分数那一项带 s²,序列一长就压倒一切——FlashAttention 不写出这张表,重计算用多算一遍前向换不存。
先停在这里:记住「激活 ∝ s·b·h·L,另有 a·s²·b 的分数表;FlashAttention 去掉它,重计算用算力换显存」就可以去 C10。
再深一层:去实验课,把我们自己模型的显存截面和公式对一遍账。
下一个缺口
激活压下去之后,27.8B 的 444.5 GB 模型状态仍放不进一张 80 GB 的卡——一张卡不够,怎么办? → C10 并行:一张卡不够怎么办
第 1 课那本 16 字节的账,在 C10 第 2 课(ZeRO)被切到多张卡上;本课的激活,在 C10 的序列并行、上下文并行里被沿序列切开。
你可能会问
- 「推理时也要存激活吗?」 不用。推理没有反向,前向算完一层就可以扔掉这一层的中间结果;推理要存的是 KV cache(C3 第 8 课),它随上下文线性增长。
- 「为什么不把激活放到 CPU 内存里?」 可以,这叫激活卸载(offload),代价是 GPU 与 CPU 之间的传输带宽远低于显存带宽。Kimi K3 的报告里把重计算、量化、卸载组合起来管理激活;这属于系统工程的取舍。
- 「logits 占多少?」 公式没算输出层。锚点词表 248,320,一条 4,096 token 的序列,logits 按 BF16 是 2.0 GB;为了算交叉熵转成 FP32 又是一份。所以大词表的模型常把输出层和 loss 分块计算。