这是全书最实用的一章。上一章讲了怎么把模型切开,这一章回答更前置的问题: 到底需要多大?以及——为什么一个 7B 的模型,微调时光是权重、梯度和优化器状态就要 112 GB, 而"跑"它只需要 14 GB?
一条值得背下来的经验:用 Adam 训练一个模型,需要的
为什么是 16 倍:每一份都要单独存
为什么要分成五份?权重(BF16,2 字节)是模型本身;梯度(BF16,2 字节)是反向传播算出的方向; 参数主副本(FP32,4 字节)是另存的一份精确权重——16 位下每次更新都会被舍入吃掉一点, 小更新累不起来,所以用 FP32 主副本累积,更新完再转回 16 位;Adam 再用两份 FP32(各 4 字节) 记住梯度的滑动平均(一阶动量 m)和它的波动幅度(二阶动量 v)。五份加起来,每个参数 16 字节。 (FP32 / FP16 / BF16 里的数字是位数,FP 指浮点。)
这一章后面所有的账,都从上面这张图里长出来。
① 推理 : 训练 = 1 : 8(2 字节 vs 16 字节)。这就是为什么微调比推理贵得多。
② 优化器状态占训练的 3/4(12 字节 / 16 字节)。省显存第一刀永远砍这里。
③ 权重本身只占 1/8。你为模型大小准备的显存,只是实际需求的零头。
像请一个厨师做菜。"模型权重"是食材(2 份),"梯度"是他记下的笔记(2 份),
但真正占地方的是他的厨房设备——炒锅、备料盆、调味罐(12 份)。
你请的是一个厨师,但要准备的是一整套厨房。
这个类比管到「账目多寡」为止:厨房设备不会因为客人变多而变多,而优化器状态是每个参数固定的 12 字节。
下面把四项分开算。
互动 · 显存账本计算器(真实逐项计算)
权重、梯度、优化器状态都是精确可算的——就是参数量的倍数。
但
所以这一章把激活值标成"数量级估算",而前三项是精确值。
看到别人说"7B 微调要 80GB"或者"要 160GB",两个都可能对——差的不只是激活值怎么管,
还包括换没换省显存的手段:8 位优化器和 LoRA 会直接改掉固定成本那一大块。
换个优化器自己看:SGD + 动量没有二阶动量 v,每参数从 16 字节降到 12。
同样一个 7B,三种用法差多少
试试这几个真实的场景
换个卡自己套:24 GB 卡上,纯固定成本按 16 字节/参数算,参数量上限约 24 ÷ 16 = 1.5B,还没算激活值。
第 1 节说「训练要 16 字节/参数,推理只要 2」。这一节把那 16 字节拆开摆出来—— 而且下面每一根柱子都是当场按公式算的,你拖滑块它就变。
互动 · 六根柱子:前五根一起动,第六根自己动
固定成本(前五根)= 你带了几个人出差,每个人都要买一张票。人越多,票钱越多,和住几天无关。
可变成本(第六根)= 每个人要在外面住几天,天数越多,住宿费越多。
这两笔账不是完全互不相干:人数一多,每个人带的行李也跟着变重。
对应到模型上,参数量变大时隐藏维度和层数也会变,激活值跟着涨——只是涨得比固定成本慢得多。
所以显存不够时,先搞清楚是哪一笔爆了,再决定砍什么。
把「序列长度」一路往右拖。前面那五根一动不动,第六根成倍往上翻——
在很长上下文下,
这就是为什么第 2 节那个「7B 全量微调要 125 GB」之外,人们还常常被再吓一跳。
混合精度几乎不省参数侧的显存。两笔账都是 16 字节/参数:
· 全 FP32:权重 4 + 梯度 4 + 一阶动量 4 + 二阶动量 4 = 16——没有单独的主副本,权重自己就是那份精确副本。
· 混合精度:权重 2 + 梯度 2 + FP32 主副本 4 + 一阶动量 4 + 二阶动量 4 = 16。
两笔账里,跟更新绑在一起的那 12 字节(精确权重/主副本 4 + 动量 4 + 方差 4)都占 3/4,「省显存第一刀砍优化器状态」对两者都成立。混合精度真正买的是速度和激活值:
16 位张量核心的矩阵乘比 FP32 快得多(A100:312 vs 19.5 TFLOPS),激活值只要一半。所以业界都做"混合":计算用 16 位,状态用 32 位。
每个浮点数由三部分组成:符号位(正负)、指数位(决定能表示多大的范围)、
尾数位(决定能表示多细)。下面这张表按这三段拆开比。
| 格式 | 位数(符号+指数+尾数) | 指数位 | 尾数位 | 能表示的范围 | 精度 |
|---|---|---|---|---|---|
| FP32 | 32(1+8+23) | 8 | 23 | ±3.4e38 | 约 7 位十进制 |
| TF32 | 19(1+8+10,存储 32) | 8 | 10 | ±3.4e38 | 计算用,存储还是 32 位 |
| BF16 | 16(1+8+7) | 8 | 7 | ±3.4e38 | 约 2~3 位十进制 |
| FP16 | 16(1+5+10) | 5 | 10 | ±65504 | 约 3~4 位十进制 |
| FP8(E4M3) | 8(1+4+3) | 4 | 3 | ±448 | 很粗,但训练大规模模型时够用 |
三种格式的位分配:差别就在那几格“指数”里
每一格是一个比特。“指数”那一段决定能表示多大的范围,“尾数”那一段决定能表示多细。看图就明白:BF16 把指数原封不动留下,FP16 却把它砍到了 5 格。
互动 · 同一把尺子上的“相对精度”:越小的地方谁先坏掉
看"能表示的范围"那一栏。BF16 的指数位和 FP32 完全一样(8 位),
所以它能表示的范围和 FP32 一模一样,只是尾数短、精度低。FP16 只有 5 位指数,
最大只能到 65504,最小正规数只有 6.1e-5,范围窄了 30 多个数量级。
一个数小到表示不了叫下溢,大到表示不了叫溢出。
所以 BF16 是"范围换精度",FP16 是"精度换范围"。
深度学习里范围比精度重要得多——梯度可以算得糙一点,但绝不能变成 0 或者溢出。
这就是今天几乎所有模型都用 BF16 的原因。
下面做的是真实的位级转换——把你的输入按 IEEE 754(浮点数的国际标准)的规则 真的转成 FP16 / BF16 / FP32 的位模式,不是查表。拖滑块把一个数变小,看它什么时候变成 0。
互动 · 位级转换与下溢(含损失缩放)
FP32:一直到 1e-45 都还有值。
BF16:到 1e-38 附近才归零——和 FP32 几乎一样宽。
FP16:低于 6.1e-5 就进入"非正规数"区间,精度急剧下降;
低于 5.96e-8 就直接变成 0。
一个梯度值真的变成 0,对随机梯度下降是这一步收不到新信号;对 Adam 只是丢了一次信息,
旧的动量还能让它动一点。但被碾平的那部分精度,两种优化器都找不回来。
像用一把最小刻度是 1 毫米的尺子去量一根头发丝。量不出来不是因为你不会量,
是这把尺子根本表示不了这么小的数。
FP16 就是那把尺子。而"损失缩放"就是先把头发丝放大 1024 倍再量,
量完再除回去。
一张图看清损失缩放这四步
梯度就是损失对每个参数的导数,所以损失乘 S、梯度也乘 S。 注意一个精妙之处:损失缩放并没有让 FP16 的精度变好,它只是绕开了 FP16 表达不了的那个区间。 缩放完,相对精度还是那个相对精度。
| 手段 | 省什么 | 省多少 | 代价 |
|---|---|---|---|
| 权重和梯度从 FP32 降到 16 位 | 权重和梯度各减半(主副本补回 4 字节,参数侧不省) | 几乎免费,还更快。今天没有理由不做 | |
| 梯度累积 | 不用真的放大 batch,用多次小 batch 累加梯度 | 训练变慢(同样的数据量要跑更多步)。但数学上等价于大 batch | |
| 梯度检查点 activation checkpointing |
反向传播时不保存中间激活,需要时重新算一遍 | 慢 20~30%。用时间换空间。要选好切分点—— 分段太少省不了,分段太多重算开销大 | |
| 优化器状态 |
Adam 的一阶动量和二阶动量降到 8 位(主副本仍 FP32) | 12 → 6 字节/参数 | 可能影响收敛,需要专门的量化实现(8-bit Adam) |
| CPU 卸载 offload |
把优化器状态搬到内存里,用的时候再传回来 | 显存省一大块 | 受 PCIe 带宽限制,可能慢好几倍。适合"显存不够但又必须训"的场景 |
| LoRA / 参数高效微调 | 只训练 0.1%~1% 的参数 | 优化器状态几乎归零 | 只能改行为,不能往里灌新知识(见阶段 5 那一章) |
显存不够时,这六步从哪一步开始试(越往下越慢 / 越难)
互动 · 数值精度时间轴
每种格式能表示多大范围:为什么 FP16 会被淘汰
| 格式 | 什么时候用 | 注意 |
|---|---|---|
| FP32 | 参数主副本、优化器状态、损失计算 | 基准。不用来存 |
| TF32 | NVIDIA Ampere 之后的默认矩阵乘法精度 | 改个开关就能提速,几乎无损——最容易被忽略的一次免费优化 |
| BF16 | 权重、梯度、 |
范围足够大,基本不用担心下溢。不需要损失缩放 |
| FP16 | 老一代的 |
必须配损失缩放,否则梯度会变成 0 |
| FP8 | H100 之后的新选项,用于超大规模训练 | 需要精细的缩放策略(按张量或按块)。已有大规模成功案例 |
| INT8 / INT4 | 推理量化 | 主要给推理用,训练的梯度不适合用整数表示 |
如果你在用 NVIDIA 的卡训练,检查一下 TF32 有没有打开。
它让矩阵乘法用 19 位而不是 32 位算,速度提升明显,精度损失在大多数任务上看不出来。
做过实验的话你会发现:这是性价比最高的一次改动——改一行配置,什么都不用重构。
| 你省下了 | 你在哪里付账 | 什么时候真的会痛 → 换什么 |
|---|---|---|
| 用 BF16 主要省激活值(约一半)和搬运量;参数侧几乎不省 | 数值精度降到 2~3 位十进制 | 对数值敏感的任务(某些强化学习、小规模微调)→ 留 FP32 主副本兜底 |
| 用梯度检查点省 70% 激活 | 训练速度慢 20~30% | 算力本来就紧、多花的时间换不回显存 → 先减小 batch 或梯度累积 |
| 减小 batch 省激活 | 梯度噪声变大,归一化层的统计量不稳 | 需要稳定收敛 → 用梯度累积凑回大 batch |
| 用 8 位优化器省状态 | 收敛可能变差,还多一层实现复杂度 | 大模型对精度敏感 → 换回 FP32 Adam,或只对部分状态用 8 位 |
| CPU 卸载省显存 | 慢好几倍,瓶颈从显存换成 PCIe | 要反复迭代训练 → 换成多卡 ZeRO / FSDP |
| LoRA 省掉优化器状态 | 只能学"怎么做",不能学"知道什么" | 要往里灌新知识 → 全量微调或继续 |
权重、梯度、优化器状态都是精确可算的,所以你总能提前规划。
但
· 用不用 FlashAttention —— 影响注意力的中间矩阵(长序列下差异巨大)
· 用不用重计算 —— 影响 2~5 倍
· 框架怎么安排算子 —— 影响峰值还是均值
所以真实的显存规划,不能只靠公式,必须实测。
惯用做法是:先按公式估一个下界,然后在目标硬件上跑一个最小配置,
实测峰值显存,再线性外推。
回到第 2 节那个「显存账本计算器」。把「序列长度」滑块从 4096 一路往右拖到 32K,
盯着「
那一栏涨的就是「为了能回头」付的钱:权重、梯度、优化器状态是为「记住过程」付的
固定成本,而
这一章是账本章,回答集中在「参数账本」和「它假设了什么」两行。
| 暗线 | 这一章的回答 |
|---|---|
| C 参数账本 | 7B 模型逐项算:权重 2×7 = 14 GB,梯度 2×7 = 14 GB,
FP32 主副本 4×7 = 28 GB,Adam 的一阶 + 二阶动量 4×7 + 4×7 = 56 GB——
固定成本合计 16 × 7 = 112 GB,单张 80GB 的卡根本放不下,还没算 激活值(用本章计算器的同一套公式,B=1、L=4096、h=4096、32 层): ≈ 2 × 4096 × 4096 × 32 × 12 = 12.9 GB;把 L 拉到 32K 就是 约 103 GB—— 比模型本身还贵得多。只推理只要 14 GB;LoRA 优化器状态几乎归零,总需求约 27 GB。 |
| E 它假设了什么 | 假设「能回头」是必要的——也就是默认用反向传播。
页面上那几种省法(检查点、量化、卸载、LoRA)全都是在反向传播这个框架里做优化,
没有一条是「不用回头」。 第二个假设更隐蔽:它假设数值精度的损失是可以容忍的。 这在绝大多数视觉/语言任务上成立,但在数值敏感的场景(某些强化学习、 小规模精确微调)不成立——所以那些场景必须留 FP32 主副本兜底。 |
它接住了前面阶段的什么:并行训练那一章的 ZeRO / FSDP 切的就是这里的四份状态; 没有这一章的 16 字节账,ZeRO 的三个 stage 到底在切什么就是背下来的。
它给后面几章留了什么:训练算完了,服务的时候显存又变成了瓶颈—— 只不过换成了另一个东西:《推理与 KV Cache》 和 《服务化与吞吐》。
记住 16 倍:Adam 训练一个模型要参数量的 16 倍字节,其中优化器状态占 3/4、
权重只占 1/8,数值格式用 BF16 而不是 FP16——BF16 的指数和 FP32 一样宽,不会下溢。
省显存按 混合精度 → 小 batch / 梯度累积 → 梯度检查点 → LoRA → ZeRO → CPU 卸载 的顺序上,
每一步都在别处付账;公式只给下界,
上面讲的都是「够用」的版本。想往下挖,这里有三个入口—— 它们不是必修内容,是给想再往前走一步的读者准备的。