9.5 显存优化:混合精度、重计算与分片
9.2–9.4 讲的是切:把状态或层分给多张卡。但切分有代价(通信),而且有些场景根本没有多张卡可用。这一节讲同一条战线的另一半——省:不改变模型的并行结构,靠数值格式、重计算与算子替代把每一项账本压小。
它同时回答一个很实际的问题:如果只有一张 24 GB 的卡,能训多大的模型?答案不是「参数量除以字节数」那么简单,而是取决于五账单里哪几项被压掉了。本节按收益从大到小排列手段,每一项都给出「省多少 → 代价是什么」的明确对照。
9.5.1 混合精度:BF16 与 FP16 的差别在哪
混合精度(mixed precision)用 16 位浮点数做存储与矩阵运算,同时保留一份 fp32 数值用于参数更新:
- 前向 / 反向:参数、激活、梯度用 16 位(bf16 或 fp16),享受更高的张量核心吞吐与更小的显存;
- 更新:优化器在 fp32 主权重上做 ,再把结果转成 16 位供下一步使用(9.1.2 的④)。
两种 16 位格式的差别全在位布局上:
| 格式 | 符号 / 指数 / 尾数 | 动态范围 | 有效精度 | 是否需要特殊处理 |
|---|---|---|---|---|
| fp16 | 1 / 5 / 10 | 约 3 位十进制 | 需要损失缩放 | |
| bf16 | 1 / 8 / 7 | 与 fp32 相同的指数范围 | 约 2 位十进制 | 通常不需要 |
| fp32(基准) | 1 / 8 / 23 | 约 7 位十进制 | — |
损失缩放(loss scaling):将损失乘以因子 后再反向,使梯度整体放大、逃离 fp16 的下溢区;更新前再除以 还原。实践中 做成动态的:连续若干步没有溢出就乘 2,一旦检测到 inf/nan 就减半并跳过该步。
浮点数的位数分配决定它能表示什么:指数位管「多大、多小」,尾数位管「多精确」。
- bf16 把 8 位全给了指数(与 fp32 一致),所以它能表示的范围和 fp32 一样宽,代价是尾数只剩 7 位——两个相近的数之间的间隔比 fp32 大得多;
- fp16 反其道而行:10 位尾数让它「更细腻」,但指数只有 5 位,于是小于约 的数直接下溢为 0。
深度学习里真正会造成训练失败的是动态范围:反向传播的梯度可能小到 量级,在 fp16 里直接变成 0(这一步的更新就丢失了)。这就是 fp16 必须配损失缩放、而 bf16 通常免配的原因。代价的另一面:bf16 的精度低,对「需要精确累加」的操作(如 softmax 的分母、LayerNorm 的方差、归约求和)要在 fp32 里做——这也是为什么「混合精度」而不是「全 bf16」。
,比较三种写法的模型状态与显存收益:① 纯 fp32 Adam;② 混合精度 Adam(bf16 参数与梯度 + fp32 主权重);③ 混合精度 + 8-bit 优化器。
逐项代入(单位 GB, 字节):
| 项 | ① 纯 fp32 | ② 混合精度 | ③ 混合精度 + 8-bit 优化器 |
|---|---|---|---|
| 参数 | (fp32) | (bf16) | (bf16) |
| 梯度 | (fp32) | (bf16) | (bf16) |
| 、 | (fp32) | (int8) | |
| 主权重 | (fp32) | (fp32) | |
| 合计 |
关键结论(容易被误解的一点):① 与 ② 的静态账本相同(都是 )——混合精度把参数与梯度从 压到 ,但新增的 fp32 主权重 正好抵消。所以:
- 混合精度的主要红利是算力吞吐(16 位张量核心)与激活 / 临时张量的显存(激活也用 bf16 存储,直接减半);
- 想靠精度省静态显存,必须动优化器状态那一项(8-bit 优化器,,省 37.5%)。
再看激活:若纯 fp32 下激活为 字节,bf16 存储后约 ——这在长序列 / 大批场景里往往是混合精度最大的一笔收益。
∎9.5.2 FP8 与逐张量缩放
FP8 是 8 位浮点格式(NVIDIA、Arm 与 Intel 联合提出的两种编码,见延伸阅读),在 Hopper 及之后的 GPU 上由张量核心原生支持。它的两种常用变体是:
| 格式 | 符号 / 指数 / 尾数 | 动态范围(近似) | 常见用途 |
|---|---|---|---|
| E4M3 | 1 / 4 / 3 | 正数约 | 前向的权重与激活、反向的权重梯度 |
| E5M2 | 1 / 5 / 2 | 正数约 | 梯度(范围更宽,精度更低但底数更重要) |
由于尾数只有 2–3 位,FP8 必须缩放才能用:把张量除以一个缩放因子 使其落入可表示范围,计算后再乘回。与 fp16 的损失缩放(只看损失一个标量)不同,FP8 的缩放是逐张量(甚至逐块)的,且要实现「延迟缩放」——用上一步的统计量去缩放本步的张量,以便把缩放计算也放进重叠窗口。
把 FP8 想成「用更少的钱买同样的动态范围」:E4M3 的指数位比 fp16 还少,所以它既没有 bf16 的范围、也没有 fp16 的精度——它靠缩放把数据搬到合适的区间,再用硬件把它搬回来。
三件事因此同时成立:
- 收益是吞吐:同样的张量核心在 FP8 下的峰值吞吐约为 bf16 的两倍(具体倍数随架构与算子而定,需按你的硬件实测);
- 代价是复杂度:需要维护缩放因子、处理溢出与下溢、为不同张量选择 E4M3 / E5M2,并保证反向的数值稳定;
- 门槛是硬件:FP8 张量核心从 Hopper 一代起才原生支持;在没有 FP8 单元的卡上模拟 FP8 只会更慢。
因此社区现状是:BF16 是安全默认,FP8 是「有对应硬件 + 有官方配方」时的加速项。已公开的大规模训练报告(如 DeepSeek-V3 技术报告,2024)说明在特定框架与模型上 FP8 可以稳定训练,但那是在算法与工程都做了配套处理之后的结论,不可直接外推到任意代码库。
- 「FP8 也需要损失缩放,做法和 fp16 一样」。粒度不同:fp16 的损失缩放是一个标量乘在损失上;FP8 的缩放是每个张量(或每个块)一个因子,且需要用前一步的统计量来选因子(延迟缩放)。把两者当成一回事会让实现完全跑偏。
- 「FP8 比 bf16 精度更低,所以一定更差」。决定精度的是有效位宽:在缩放得当的前提下,FP8 的矩阵乘结果误差与 bf16 同量级;真正会出问题的是不缩放或缩放因子长期不更新(某层数值范围变化后一直沿用旧因子)。
- 「用了 FP8 就能省一半显存」。FP8 省的是存 FP8 的那部分(激活、部分权重副本);主权重、优化器状态仍然是 fp32(),这些一分不减——与混合精度是同一个道理。
9.5.3 激活重计算:用算力换显存
反向传播需要前向的中间激活(1.5)。激活重计算的做法是:前向时只保存少量「检查点」(checkpoint)处的激活,丢弃其余中间结果;反向传播需要某个中间激活时,从最近的检查点重新做一遍前向把它算出来。
两种粒度:
- 全层重计算(full):只保存每一层的输入,层内所有中间激活在反向时全部重算;
- 选择性重计算(selective):只对显存占用大、重算便宜的算子做重算(典型是注意力的 softmax 与 dropout),其余中间结果照常保存。
代价的量级:一次前向的 FLOPs 约为反向的 (前向 、反向 ,见 9.1.4),所以全层重计算要多花一次前向 ≈ 整步计算量的 (前向 相对整步 )。选择性重计算把这笔开销压到个位数百分比。
本质上是一道用带宽换容量的题:存激活要占显存(容量受限),重算要花算力(通常有余量)。三种粒度的取舍:
| 粒度 | 激活显存 | 额外计算 | 适用 |
|---|---|---|---|
| 不重算 | 最大 | 显存充裕、追求最短单步时间 | |
| 全层重计算 | 约降到「每层只留输入」 | 显存紧张、批或序列想开大 | |
| 选择性重计算 | 介于两者之间 | 个位数百分比 | 现代默认:只对显存大户下手 |
「每层只留输入」还带来一个漂亮的推论:当按「每 层设一个检查点」时,激活显存可以从 降到 量级(见延伸阅读中的原始论文口径)。但工程上更常用的是逐层重算(实现简单、与流水线 / 分片配合好),因为它已经把激活降到「每层的输入」这一最小必需量。
,,,bf16,单卡激活预算 GB。不重算时按 34 字节 /(token·hidden 元素)估(9.1.3);全层重计算后只保留每层的输入激活(形状 ,2 字节/元素)。
(1)不重算(对照基线)。 每个 token 在全部 层上的激活量约 字节(, 不能漏)。 GB 可容纳的 token 数 (这是把「每层内全部中间张量都保留」的口径;实际会被注意力矩阵与碎片进一步压低,此处只作量级对照。)
(2)重计算后的口径对照。 先澄清一个容易绕晕的点:「层输入」本身就是激活的一部分,重计算只是把「层内的中间张量」全部丢掉、只留「层输入」这一个检查点。三种口径的每 token 每 hidden 元素激活量如下(先给结论再看表):
| 口径 | 每 token·每 hidden 元素的激活字节数 | 说明 |
|---|---|---|
| 不重算(全部保留) | 约 34 | 含 LayerNorm 输入输出、QKV、注意力输出、MLP 隐层等 |
| 选择性重计算 | 约 18 | 只保留必须的,注意力部分重算 |
| 全层重计算 | 约 2 | 只保留每层的输入(形状 、2 字节/元素) |
代入 GB 的预算(、、,即每 token 的总激活 = 系数 ):
结论:按这个口径,全层重计算把可容纳的批放大约 17 倍(系数 ,即 ),选择性重计算约 1.9 倍()。务必注意:这些数字高度依赖「哪些中间张量被计入」的口径,不同实现(是否重算注意力、是否实体化 softmax 矩阵、是否启用 FlashAttention)差异极大——报价必须用你自己的实现实测(本处口径标注为需核实)。
∎# ── 激活重计算的两种用法(接口名稳定,语义按版本核对)──────────────
import torch
from torch.utils.checkpoint import checkpoint
class Block(torch.nn.Module):
def __init__(self, layer):
super().__init__()
self.layer = layer
def forward(self, x):
# ① 显式重计算:把整个 block 包起来,前向不保存内部激活
if self.training:
return checkpoint(self.layer, x, use_reentrant=False)
return self.layer(x)
# ② 选择性重计算:只包住注意力(显存大户、重算便宜)
class AttnBlock(torch.nn.Module):
def forward(self, x):
if self.training:
h = checkpoint(self.attn, x, use_reentrant=False) # 只对注意力重算
else:
h = self.attn(x)
return self.mlp(h) # MLP 的中间结果照常保存(它相对便宜)
# ③ 用配置开关打开(Megatron 系一类字段,名称以你的版本为准,需核实):
# --recompute-activations # 全层重计算
# --recompute-granularity selective # 选择性重计算
# --recompute-method block --recompute-num-layers 1
以 参数量、混合精度 Adam(基线 )为单位;括号内为 7B 模型的量级( 字节):
| 手段 | 省的账 | 省多少 | 代价 |
|---|---|---|---|
| BF16/FP16 混合精度 | 参数与梯度精度 | 参数+梯度 ,但新增主权重 → 静态账不变;激活约减半 | 需要数值处理(fp16 需损失缩放);bf16 精度略低 |
8-bit 优化器(bitsandbytes 一类) |
的存储 | (总账 ,7B: GB) | 需要分块量化与反量化的算子开销;对优化器超参的等效性需验证 |
| CPU offload | (甚至主权重) | 再省 或更多(7B 再省 14–28 GB) | 每步要在 PCIe 上往返传输 → 训练变慢,通常是 1.5–3 倍甚至更多(需实测) |
| 激活重计算(全层) | 激活 | 每 token 每层从约 34 字节降到约 2 字节 | 额外约 的前向计算 |
| 选择性重计算 | 激活(只算注意力等大户) | 每 token 每层约 34 → 18 字节 | 额外个位数百分比计算 |
| FP8 训练 | 激活与部分权重副本 | 存储部分再减半;吞吐约 ×2 | 需要缩放管理与 Hopper+ 硬件;配方需匹配 |
| ZeRO-3 / FSDP2 分片 | 全部模型状态 | (见 9.2) | 通信量上升约 1.5 倍 |
| LoRA / QLoRA | 梯度和优化器状态(只对适配器) | 见 9.5.5:7B 从 GB 降到 GB 量级 | 只适用于微调;表达能力受秩 限制 |
读表方法:先问「缺的是模型状态还是激活」——缺状态就不可能靠重计算解决,缺激活就不可能靠 8-bit 优化器解决(9.1.1 的两半账)。
9.5.4 八位优化器与 CPU offload
8-bit 优化器:Adam 的 、 是逐参数的连续统计量,可以用分块量化存储——把参数切成若干块,每块计算一个缩放常数,块内元素用 8 位整数表示。更新时先反量化到 fp32 算更新的增量,再量化回 8 位。这样 从 字节降到 字节。
CPU offload:把优化器状态(甚至参数)放在主机内存里,更新那一步把需要的分块搬到 GPU、算完再搬回主机。显存因此大幅下降,但每一参数都要经历「主机 → 设备 → 主机」的往返。
8-bit 优化器的代价很小(几个量化算子),所以在单卡微调里几乎是默认动作:7B 模型从 112 GB 降到 70 GB,再配上分片就能进 24 GB 卡。
CPU offload 的代价大得多,值得把算式写清楚:一个参数要让 (各 1 字节)与主权重(4 字节)在一步内往返一次,每参数约 字节的 PCIe 流量;7B 参数下每步约 GB。PCIe 4.0 ×16 的有效带宽约 20 GB/s 量级,于是每步光搬运就 4 秒以上——而一张 80 GB 卡上 7B 模型的一步计算只需秒级甚至更少。所以:
- offload 是「跑得起来」与「跑得快」之间的取舍,适合「偶发的大模型微调」,不适合长周期预训练;
- 若必须 offload,优先只 offload 优化器状态()而不是参数本身,因为参数每步都要被计算用到多次。
单张 24 GB 卡,混合精度 Adam,允许开 8-bit 优化器、激活重计算,激活预算 6 GB。问:能全参训练多大的模型?(用「模型状态 + 激活 ≤ 22 GB(留 2 GB 碎片与缓冲)」作为约束)
(1)不开 8-bit。 模型状态 ,约束 GB (10 亿参数)。 更稳妥的取法是留出余量,实际约 0.5–0.8B。
(2)开 8-bit 优化器。 模型状态 : GB (16 亿参数)。
(3)再叠 CPU offload(只 offload )。 模型状态 ( 在主机)= : GB (20 亿参数)。但每步的 PCIe 流量约 字节 GB,训练显著变慢。
(4)对照:7B 为什么必须分片或 PEFT。
- 若 、8-bit 优化器:模型状态 GB 一张 24 GB 卡无论如何都放不下(即使 offload 到 GB 也超);
- 需要 张卡做 ZeRO-2/3()或改用 LoRA / QLoRA(9.5.5)。
结论表:
| 配置 | 单卡 24 GB 可全参训练的最大规模 |
|---|---|
| 混合精度 Adam | ≈ 0.5–0.8B |
| + 8-bit 优化器 | ≈ 1.2–1.6B |
| + CPU offload( 上主机) | ≈ 2B(速度明显下降) |
∎
9.5.5 量化与 PEFT:把 7B 放进一张卡
三者都"用更低的比特表示模型",但目标不同:
LoRA(低秩适配):冻结预训练权重 ,只训练低秩增量 训练开始时令 (如 )以免破坏已有能力。梯度与优化器状态只属于 ,于是 的账本被 取代。
QLoRA:在 LoRA 之上把冻结的主干量化到 4 bit(NF4 数据类型),并用双重量化(对缩放常数再量化)与分页优化器进一步压占用。主干只读、以低位宽驻留,适配器仍以高精度训练。
推理量化(GPTQ / AWQ):把权重压到 4 bit 以便部署。GPTQ 用逐层的误差补偿方式量化(量化一列后把误差传播到剩余列);AWQ 则保护激活显著的通道,避免把最影响输出的权重压坏。二者都是推理工具,与训练显存账不是一回事。
回到 9.1.2 的五账单: 里真正被优化器照料的是 与主权重()。全参微调时它们也属于 个参数;而 LoRA 冻结主干后,需要优化器照料的只有那对极小的 (参数量 , 时通常不到全参的 )。于是:
- 主干:只需「以低位宽驻留 + 前向」,不需要梯度、不需要优化器状态;
- 适配器:需要全部五账单,但它的规模只有千分之几。
一句话记住:LoRA 把 换成 。这正是 Lab 3 能在一张消费级卡上微调的原因。代价是表达能力受限:低秩假设在某些任务上不够(需要满秩更新),此时要么增大 ,要么回到全参 + 分片。
,,。在 q_proj / v_proj 上挂 LoRA,,即 个矩阵,每个矩阵含 (),并假设不放置(,忽略 GQA 下 KV 投影更窄的细节)。自适应:主干 4 bit(NF4,含双重量化的额外开销,按 0.5 字节/参数计),适配器 fp32 训练。
求:主干显存、适配器参数量与其五账单、总静态占用;并说明 24 GB 卡上还剩多少给激活。
(1)主干(冻结、4 bit)。
(2)适配器参数量。 每个矩阵:、,共 个参数。 占全参比例:
(3)适配器的五账单(fp32)。 参数 MB;梯度 MB; 各 MB;主权重(fp32 与参数同算一份) MB。合计 MB —— 小于 0.1 GB,可忽略。
(4)总静态占用。 这就是 QLoRA 的核心结论:一张 24 GB 卡上还剩约 20 GB 给激活、KV cache 与优化器临时张量。
(5)与全参对照。
| 方案 | 静态占用 / 卡(单卡口径) | 能否在 24 GB 上跑 |
|---|---|---|
| 全参 + 混合精度 Adam | GB | 否 |
| 全参 + 8-bit 优化器 | GB | 否 |
| 全参 + 8-bit 优化器 + 分片 | GB | 勉强(激活余量紧张) |
| LoRA(bf16 主干) | GB + GB | 勉强(激活余量小) |
| QLoRA(4 bit 主干 + LoRA) | GB | 可以,余量充足 |
注意两个易错点:① 「4 bit 主干」占 字节/参数是 NF4 单重量化的理想值,双重量化与分块缩放会引入少量额外开销(工程上常按 0.55–0.6 字节/参数估计,精确值需实测);② 训练时若把主干临时反量化到 bf16 做矩阵乘,会瞬时多占一份主干显存(现代实现按层反量化以抑制峰值,具体行为需按实现核对)。
∎- 「混合精度能省一半静态显存」。静态账(模型状态)从 到 :省下的 被主权重吃回去了(例 9.5.1)。省的主要是激活与临时张量。
- 「bf16 不需要任何数值处理」。bf16 免损失缩放,但累加、softmax、LayerNorm、归约仍需 fp32;全 bf16 会让长序列训练误差累积。
- 「fp16 配 bf16 的配方也行」。fp16 的指数范围窄,必须配损失缩放;若缩放因子设置不当,会出现「梯度静默变 0」或「损失突然 nan」两类相反故障。
- 「开了激活重计算,参数量就能变大」。它只管激活那半本账。模型状态要靠分片或 8-bit 优化器。
- 「CPU offload 只是慢一点」。慢的倍数常在 1.5–3 倍甚至更高,且每步都要在 PCIe 上搬 GB 级数据(9.5.4)。它适合「偶发、一次性」的微调,不适合长训练。
- 「LoRA 能替代并行」。它替代的是「改动的规模」:如果任务需要满秩更新,LoRA 的秩 就是表达瓶颈,此时仍要回到全参 + 分片。
- 「4-bit 主干 + LoRA 的显存就是 」。还有激活、KV cache、适配器、临时反量化副本与分片常数(例 9.5.4 的注意点)。
- 「量化(GPTQ/AWQ)也能省训练显存」。它们的目标是部署:把权重压到 4 bit 供推理;训练时的瓶颈( 与主权重)不在它们的解决范围内。
Q1 写出 fp16 与 bf16 的位布局,并解释「为什么 fp16 需要损失缩放而 bf16 通常不需要」。
Q2 、单卡 80 GB、混合精度 Adam。列出「不开分片」情况下能把静态占用压到 80 GB 以下的两条手段及其结果,并说明各自代价。
Q3 一个训练任务激活 OOM,已知模型状态只占 30 GB(卡有 80 GB)。给出三条处方并排序(说明为什么先做哪条)。
Q4 QLoRA 为什么能把 7B 微调压到 24 GB 卡?用「五账单」的语言回答,并指出其中哪一项被彻底绕开了。
参考要点
Q1 fp16:符号 1 / 指数 5 / 尾数 10,动态范围窄(下溢阈值约 );bf16:符号 1 / 指数 8 / 尾数 7,指数范围与 fp32 相同。反向梯度常小到 量级,在 fp16 里会下溢为 0(更新丢失),所以要把损失放大 倍让梯度落在可表示区;bf16 的范围足够宽,不需要这个放大,代价是尾数少、精度低(累加类算子要在 fp32 做)。
Q2 两条手段:① 8-bit 优化器: 从 降到 ,静态从 GB 降到 GB( GB);代价是量化/反量化算子开销与超参等效性验证。② 换 SGD(无动量)或减少优化器状态: 与主权重中 全额省掉( GB,仍超 80)——不充分;更现实的是 的 ZeRO-1/2 分片(属于「分片」而非「省」,若允许则可到 GB)。也可答「CPU offload 优化器状态」:静态降到 量级,但每步 PCIe 往返使训练显著变慢。
Q3 三条处方与顺序:① 梯度累积 + 减小 micro-batch(最便宜、不改变数值,立刻可做);② 激活重计算(选择性优先,额外计算只有个位数百分比);③ 序列并行 / 减小序列长度(涉及模型改造,长序列才需要)。排序理由:代价递增;前两条只改配置/包一层 checkpoint,第三条要改模型切分。
Q4 五账单 = 参数、梯度、优化器状态、主权重、激活。LoRA 只让适配器承担梯度与优化器状态( 极小量),主干被量化到 4 bit 后只保留「参数」这一项(且以 0.5 字节/参数计)。被彻底绕开的是主干的梯度与优化器状态()——也就是基线账本中最贵的部分。
- Micikevicius et al., Mixed Precision Training(2017,arXiv:1710.03740)——损失缩放与 fp32 主权重的原始方案。
- Wang et al., FP8 Formats for Deep Learning(2022,arXiv:2209.05433)——E4M3 / E5M2 两种 FP8 编码的定义(NVIDIA、Arm、Intel 联合)。
- Micikevicius et al., FP8-LM: Training FP8 Large Language Models(2023,arXiv:2310.18313)——FP8 训练的缩放与稳定性实践。
- Chen et al., Training Deep Nets with Sublinear Memory Cost(2016,arXiv:1604.06174)——激活重计算的原始论文( 显存的来源)。
- Korthikanti et al., Reducing Activation Recomputation in Large Transformer Models(2022,arXiv:2205.05198)——选择性重计算与序列并行。
- Dettmers et al., QLoRA: Efficient Finetuning of Quantized LLMs(2023,arXiv:2305.14314)与 Hu et al., LoRA: Low-Rank Adaptation of Large Language Models(2021,arXiv:2106.09685)——本节的 PEFT 与 4-bit 配方。
- Frantar et al., GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers(2022,arXiv:2210.17323);Lin et al., AWQ: Activation-aware Weight Quantization(2023,arXiv:2306.00978)——推理量化的两条主流路线。
本节小结:显存优化的第一位问题是「缺哪半本账」。模型状态只能靠低精度存储(8-bit 优化器把 压到 )、CPU offload(用 PCIe 换容量,速度代价大)或分片(9.2)来降;激活靠混合精度(减半)、激活重计算(全层或选择性)与梯度累积来降。混合精度与 FP8 的显存收益主要在激活,静态账几乎不变;FP8 的代价是缩放管理与新硬件。LoRA / QLoRA 用「只训练极少量参数」把最贵的 绕开,是单卡微调 7B 的唯一实用路径。下一节处理切与省都用上之后剩下的问题:通信、容错与确定性。