Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

9.5 显存优化:混合精度、重计算与分片

9.2–9.4 讲的是切:把状态或层分给多张卡。但切分有代价(通信),而且有些场景根本没有多张卡可用。这一节讲同一条战线的另一半——省:不改变模型的并行结构,靠数值格式、重计算与算子替代把每一项账本压小。

它同时回答一个很实际的问题:如果只有一张 24 GB 的卡,能训多大的模型?答案不是「参数量除以字节数」那么简单,而是取决于五账单里哪几项被压掉了。本节按收益从大到小排列手段,每一项都给出「省多少 → 代价是什么」的明确对照。

9.5.1 混合精度:BF16 与 FP16 的差别在哪

定义 9.5.1 · 混合精度训练与损失缩放

混合精度(mixed precision)用 16 位浮点数做存储与矩阵运算,同时保留一份 fp32 数值用于参数更新:

  • 前向 / 反向:参数、激活、梯度用 16 位(bf16 或 fp16),享受更高的张量核心吞吐与更小的显存;
  • 更新:优化器在 fp32 主权重上做 w←w−ηg^w\leftarrow w-\eta\hat g,再把结果转成 16 位供下一步使用(9.1.2 的④)。

两种 16 位格式的差别全在位布局上:

格式 符号 / 指数 / 尾数 动态范围 有效精度 是否需要特殊处理
fp16 1 / 5 / 10 ≈6×10−8∼6.5×104\approx6\times10^{-8}\sim6.5\times10^{4} 约 3 位十进制 需要损失缩放
bf16 1 / 8 / 7 与 fp32 相同的指数范围 约 2 位十进制 通常不需要
fp32(基准) 1 / 8 / 23 ≈10±38\approx10^{\pm38} 约 7 位十进制 —

损失缩放(loss scaling):将损失乘以因子 SS 后再反向,使梯度整体放大、逃离 fp16 的下溢区;更新前再除以 SS 还原。实践中 SS 做成动态的:连续若干步没有溢出就乘 2,一旦检测到 inf/nan 就减半并跳过该步。

位布局就是「范围与精度的交易」

浮点数的位数分配决定它能表示什么:指数位管「多大、多小」,尾数位管「多精确」。

  • bf16 把 8 位全给了指数(与 fp32 一致),所以它能表示的范围和 fp32 一样宽,代价是尾数只剩 7 位——两个相近的数之间的间隔比 fp32 大得多;
  • fp16 反其道而行:10 位尾数让它「更细腻」,但指数只有 5 位,于是小于约 6×10−86\times10^{-8} 的数直接下溢为 0。

深度学习里真正会造成训练失败的是动态范围:反向传播的梯度可能小到 10−910^{-9} 量级,在 fp16 里直接变成 0(这一步的更新就丢失了)。这就是 fp16 必须配损失缩放、而 bf16 通常免配的原因。代价的另一面:bf16 的精度低,对「需要精确累加」的操作(如 softmax 的分母、LayerNorm 的方差、归约求和)要在 fp32 里做——这也是为什么「混合精度」而不是「全 bf16」。

例 9.5.1 · 混合精度到底省了什么

N=7×109N=7\times10^{9},比较三种写法的模型状态与显存收益:① 纯 fp32 Adam;② 混合精度 Adam(bf16 参数与梯度 + fp32 主权重);③ 混合精度 + 8-bit 优化器。

解

逐项代入(单位 GB,1 GB=1091\,\text{GB}=10^{9} 字节):

项 ① 纯 fp32 ② 混合精度 ③ 混合精度 + 8-bit 优化器
参数 2828(fp32) 1414(bf16) 1414(bf16)
梯度 2828(fp32) 1414(bf16) 1414(bf16)
mm、vv 28+2828+28 28+2828+28(fp32) 7+77+7(int8)
主权重 00 2828(fp32) 2828(fp32)
合计 112\mathbf{112} 112\mathbf{112} 70\mathbf{70}

关键结论(容易被误解的一点):① 与 ② 的静态账本相同(都是 16N16N)——混合精度把参数与梯度从 4N+4N4N+4N 压到 2N+2N2N+2N,但新增的 fp32 主权重 4N4N 正好抵消。所以:

  • 混合精度的主要红利是算力吞吐(16 位张量核心)与激活 / 临时张量的显存(激活也用 bf16 存储,直接减半);
  • 想靠精度省静态显存,必须动优化器状态那一项(8-bit 优化器,16N→10N16N\to10N,省 37.5%)。

再看激活:若纯 fp32 下激活为 AA 字节,bf16 存储后约 A/2A/2 ——这在长序列 / 大批场景里往往是混合精度最大的一笔收益。■\blacksquare

∎

9.5.2 FP8 与逐张量缩放

定义 9.5.2 · FP8 训练与缩放

FP8 是 8 位浮点格式(NVIDIA、Arm 与 Intel 联合提出的两种编码,见延伸阅读),在 Hopper 及之后的 GPU 上由张量核心原生支持。它的两种常用变体是:

格式 符号 / 指数 / 尾数 动态范围(近似) 常见用途
E4M3 1 / 4 / 3 正数约 2−6∼4482^{-6}\sim448 前向的权重与激活、反向的权重梯度
E5M2 1 / 5 / 2 正数约 2−14∼5.7×1042^{-14}\sim5.7\times10^{4} 梯度(范围更宽,精度更低但底数更重要)

由于尾数只有 2–3 位,FP8 必须缩放才能用:把张量除以一个缩放因子 ss 使其落入可表示范围,计算后再乘回。与 fp16 的损失缩放(只看损失一个标量)不同,FP8 的缩放是逐张量(甚至逐块)的,且要实现「延迟缩放」——用上一步的统计量去缩放本步的张量,以便把缩放计算也放进重叠窗口。

FP8 的取舍与「为什么它需要新硬件」

把 FP8 想成「用更少的钱买同样的动态范围」:E4M3 的指数位比 fp16 还少,所以它既没有 bf16 的范围、也没有 fp16 的精度——它靠缩放把数据搬到合适的区间,再用硬件把它搬回来。

三件事因此同时成立:

  1. 收益是吞吐:同样的张量核心在 FP8 下的峰值吞吐约为 bf16 的两倍(具体倍数随架构与算子而定,需按你的硬件实测);
  2. 代价是复杂度:需要维护缩放因子、处理溢出与下溢、为不同张量选择 E4M3 / E5M2,并保证反向的数值稳定;
  3. 门槛是硬件:FP8 张量核心从 Hopper 一代起才原生支持;在没有 FP8 单元的卡上模拟 FP8 只会更慢。

因此社区现状是:BF16 是安全默认,FP8 是「有对应硬件 + 有官方配方」时的加速项。已公开的大规模训练报告(如 DeepSeek-V3 技术报告,2024)说明在特定框架与模型上 FP8 可以稳定训练,但那是在算法与工程都做了配套处理之后的结论,不可直接外推到任意代码库。

FP8 与 FP16 缩放的三个混淆点
  1. 「FP8 也需要损失缩放,做法和 fp16 一样」。粒度不同:fp16 的损失缩放是一个标量乘在损失上;FP8 的缩放是每个张量(或每个块)一个因子,且需要用前一步的统计量来选因子(延迟缩放)。把两者当成一回事会让实现完全跑偏。
  2. 「FP8 比 bf16 精度更低,所以一定更差」。决定精度的是有效位宽:在缩放得当的前提下,FP8 的矩阵乘结果误差与 bf16 同量级;真正会出问题的是不缩放或缩放因子长期不更新(某层数值范围变化后一直沿用旧因子)。
  3. 「用了 FP8 就能省一半显存」。FP8 省的是存 FP8 的那部分(激活、部分权重副本);主权重、优化器状态仍然是 fp32(12N12N),这些一分不减——与混合精度是同一个道理。

9.5.3 激活重计算:用算力换显存

定义 9.5.3 · 激活重计算(gradient checkpointing)

反向传播需要前向的中间激活(1.5)。激活重计算的做法是:前向时只保存少量「检查点」(checkpoint)处的激活,丢弃其余中间结果;反向传播需要某个中间激活时,从最近的检查点重新做一遍前向把它算出来。

两种粒度:

  • 全层重计算(full):只保存每一层的输入,层内所有中间激活在反向时全部重算;
  • 选择性重计算(selective):只对显存占用大、重算便宜的算子做重算(典型是注意力的 softmax 与 dropout),其余中间结果照常保存。

代价的量级:一次前向的 FLOPs 约为反向的 1/21/2(前向 2NT2NT、反向 4NT4NT,见 9.1.4),所以全层重计算要多花一次前向 ≈ 整步计算量的 1/31/3(前向 2NT2NT 相对整步 6NT6NT)。选择性重计算把这笔开销压到个位数百分比。

为什么「重算」比「存储」划算

本质上是一道用带宽换容量的题:存激活要占显存(容量受限),重算要花算力(通常有余量)。三种粒度的取舍:

粒度 激活显存 额外计算 适用
不重算 最大 00 显存充裕、追求最短单步时间
全层重计算 约降到「每层只留输入」 ≈+33%\approx+33\% 显存紧张、批或序列想开大
选择性重计算 介于两者之间 个位数百分比 现代默认:只对显存大户下手

「每层只留输入」还带来一个漂亮的推论:当按「每 L\sqrt{L} 层设一个检查点」时,激活显存可以从 O(L)O(L) 降到 O(L)O(\sqrt L) 量级(见延伸阅读中的原始论文口径)。但工程上更常用的是逐层重算(实现简单、与流水线 / 分片配合好),因为它已经把激活降到「每层的输入」这一最小必需量。

例 9.5.2 · 重计算能让批翻几倍

L=32L=32,h=4096h=4096,T=2048T=2048,bf16,单卡激活预算 1010 GB。不重算时按 34 字节 /(token·hidden 元素)估(9.1.3);全层重计算后只保留每层的输入激活(形状 B×T×hB\times T\times h,2 字节/元素)。

解

(1)不重算(对照基线)。 每个 token 在全部 L=32L=32 层上的激活量约 34×4096×32≈4.5×10634\times4096\times32\approx4.5\times10^{6} 字节(=34 字节×h×L=34\ \text{字节}\times h\times L,hh 不能漏)。1010 GB 可容纳的 token 数 Ntokennone≈10×1094.5×106≈2,200 token (即 T=2048 时约 1 条序列).N_{\text{token}}^{\text{none}}\approx\frac{10\times10^{9}}{4.5\times10^{6}}\approx2{,}200\ \text{token}\ (\text{即 }T=2048\ \text{时约 1 条序列}) . (这是把「每层内全部中间张量都保留」的口径;实际会被注意力矩阵与碎片进一步压低,此处只作量级对照。)

(2)重计算后的口径对照。 先澄清一个容易绕晕的点:「层输入」本身就是激活的一部分,重计算只是把「层内的中间张量」全部丢掉、只留「层输入」这一个检查点。三种口径的每 token 每 hidden 元素激活量如下(先给结论再看表):

口径 每 token·每 hidden 元素的激活字节数 说明
不重算(全部保留) 约 34 含 LayerNorm 输入输出、QKV、注意力输出、MLP 隐层等
选择性重计算 约 18 只保留必须的,注意力部分重算
全层重计算 约 2 只保留每层的输入(形状 B×T×hB\times T\times h、2 字节/元素)

代入 1010 GB 的预算(T=2048T=2048、L=32L=32、h=4096h=4096,即每 token 的总激活 = 系数 ×h×L\times h\times L): Ntokenfull=10×1092×4096×2048×32×2048≈38,000 token,Ntokensel=10×10918×4096×2048×32×2048≈4,200 token.N_{\text{token}}^{\text{full}}=\frac{10\times10^{9}}{2\times4096\times2048\times32}\times2048\approx38{,}000\ \text{token},\qquad N_{\text{token}}^{\text{sel}}=\frac{10\times10^{9}}{18\times4096\times2048\times32}\times2048\approx4{,}200\ \text{token}.

结论:按这个口径,全层重计算把可容纳的批放大约 17 倍(系数 34→234\to2,即 ×17\times17),选择性重计算约 1.9 倍(34→1834\to18)。务必注意:这些数字高度依赖「哪些中间张量被计入」的口径,不同实现(是否重算注意力、是否实体化 softmax 矩阵、是否启用 FlashAttention)差异极大——报价必须用你自己的实现实测(本处口径标注为需核实)。■\blacksquare

∎
# ── 激活重计算的两种用法(接口名稳定,语义按版本核对)──────────────
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
「手段 → 省多少 → 代价」总对照表

以 NN 参数量、混合精度 Adam(基线 16N16N)为单位;括号内为 7B 模型的量级(1N=21N=2 字节):

手段 省的账 省多少 代价
BF16/FP16 混合精度 参数与梯度精度 参数+梯度 8N→4N8N\to4N,但新增主权重 4N4N → 静态账不变;激活约减半 需要数值处理(fp16 需损失缩放);bf16 精度略低
8-bit 优化器(bitsandbytes 一类) m,vm,v 的存储 8N→2N8N\to2N(总账 16N→10N16N\to10N,7B:112→70112\to70 GB) 需要分块量化与反量化的算子开销;对优化器超参的等效性需验证
CPU offload m,vm,v(甚至主权重) 再省 2N2N 或更多(7B 再省 14–28 GB) 每步要在 PCIe 上往返传输 → 训练变慢,通常是 1.5–3 倍甚至更多(需实测)
激活重计算(全层) 激活 每 token 每层从约 34 字节降到约 2 字节 额外约 1/31/3 的前向计算
选择性重计算 激活(只算注意力等大户) 每 token 每层约 34 → 18 字节 额外个位数百分比计算
FP8 训练 激活与部分权重副本 存储部分再减半;吞吐约 ×2 需要缩放管理与 Hopper+ 硬件;配方需匹配
ZeRO-3 / FSDP2 分片 全部模型状态 16N→16N/k16N\to16N/k(见 9.2) 通信量上升约 1.5 倍
LoRA / QLoRA 梯度和优化器状态(只对适配器) 见 9.5.5:7B 从 112112 GB 降到 1010 GB 量级 只适用于微调;表达能力受秩 rr 限制

读表方法:先问「缺的是模型状态还是激活」——缺状态就不可能靠重计算解决,缺激活就不可能靠 8-bit 优化器解决(9.1.1 的两半账)。

9.5.4 八位优化器与 CPU offload

定义 9.5.4 · 8-bit 优化器与 CPU offload

8-bit 优化器:Adam 的 mm、vv 是逐参数的连续统计量,可以用分块量化存储——把参数切成若干块,每块计算一个缩放常数,块内元素用 8 位整数表示。更新时先反量化到 fp32 算更新的增量,再量化回 8 位。这样 m,vm,v 从 4+44+4 字节降到 1+11+1 字节。

CPU offload:把优化器状态(甚至参数)放在主机内存里,更新那一步把需要的分块搬到 GPU、算完再搬回主机。显存因此大幅下降,但每一参数都要经历「主机 → 设备 → 主机」的往返。

「省显存」与「省时间」在这里刚好反着走

8-bit 优化器的代价很小(几个量化算子),所以在单卡微调里几乎是默认动作:7B 模型从 112 GB 降到 70 GB,再配上分片就能进 24 GB 卡。

CPU offload 的代价大得多,值得把算式写清楚:一个参数要让 m,vm,v(各 1 字节)与主权重(4 字节)在一步内往返一次,每参数约 2×6=122\times6=12 字节的 PCIe 流量;7B 参数下每步约 7×109×12≈847\times10^{9}\times12\approx84 GB。PCIe 4.0 ×16 的有效带宽约 20 GB/s 量级,于是每步光搬运就 4 秒以上——而一张 80 GB 卡上 7B 模型的一步计算只需秒级甚至更少。所以:

  • offload 是「跑得起来」与「跑得快」之间的取舍,适合「偶发的大模型微调」,不适合长周期预训练;
  • 若必须 offload,优先只 offload 优化器状态(m,vm,v)而不是参数本身,因为参数每步都要被计算用到多次。
例 9.5.3 · 24 GB 卡能训多大的模型(全参微调口径)

单张 24 GB 卡,混合精度 Adam,允许开 8-bit 优化器、激活重计算,激活预算 6 GB。问:能全参训练多大的模型?(用「模型状态 + 激活 ≤ 22 GB(留 2 GB 碎片与缓冲)」作为约束)

解

(1)不开 8-bit。 模型状态 =16N=16N,约束 16N≤22−6=1616N\le22-6=16 GB ⇒N≤1.0×109\Rightarrow N\le1.0\times10^{9}(10 亿参数)。 更稳妥的取法是留出余量,实际约 0.5–0.8B。

(2)开 8-bit 优化器。 模型状态 =10N=10N:10N≤1610N\le16 GB ⇒N≤1.6×109\Rightarrow N\le1.6\times10^{9}(16 亿参数)。

(3)再叠 CPU offload(只 offload m,vm,v)。 模型状态 =(2+2+4)N+=(2+2+4)N+(m,vm,v 在主机)= 8N8N:8N≤168N\le16 GB ⇒N≤2.0×109\Rightarrow N\le2.0\times10^{9}(20 亿参数)。但每步的 PCIe 流量约 2N×2×22N\times2\times2 字节 =16=16 GB,训练显著变慢。

(4)对照:7B 为什么必须分片或 PEFT。

  • 若 N=7×109N=7\times10^{9}、8-bit 优化器:模型状态 =70=70 GB ⇒\Rightarrow 一张 24 GB 卡无论如何都放不下(即使 offload 到 8N=568N=56 GB 也超);
  • 需要 k≥3k\ge3 张卡做 ZeRO-2/3(70/k≤2270/k\le22)或改用 LoRA / QLoRA(9.5.5)。

结论表:

配置 单卡 24 GB 可全参训练的最大规模
混合精度 Adam ≈ 0.5–0.8B
+ 8-bit 优化器 ≈ 1.2–1.6B
+ CPU offload(m,vm,v 上主机) ≈ 2B(速度明显下降)

■\blacksquare

∎

9.5.5 量化与 PEFT:把 7B 放进一张卡

定义 9.5.5 · LoRA、QLoRA 与推理量化

三者都"用更低的比特表示模型",但目标不同:

LoRA(低秩适配):冻结预训练权重 W0W_0,只训练低秩增量 W=W0+ΔW=W0+αrBA,B∈Rd×r, A∈Rr×k, r≪min⁡(d,k),W=W_0+\Delta W=W_0+\frac{\alpha}{r}BA,\qquad B\in\mathbb R^{d\times r},\ A\in\mathbb R^{r\times k},\ r\ll\min(d,k), 训练开始时令 ΔW=0\Delta W=0(如 B=0B=0)以免破坏已有能力。梯度与优化器状态只属于 A,BA,B,于是 16N16N 的账本被 O(r(d+k))O(r(d+k)) 取代。

QLoRA:在 LoRA 之上把冻结的主干量化到 4 bit(NF4 数据类型),并用双重量化(对缩放常数再量化)与分页优化器进一步压占用。主干只读、以低位宽驻留,适配器仍以高精度训练。

推理量化(GPTQ / AWQ):把权重压到 4 bit 以便部署。GPTQ 用逐层的误差补偿方式量化(量化一列后把误差传播到剩余列);AWQ 则保护激活显著的通道,避免把最影响输出的权重压坏。二者都是推理工具,与训练显存账不是一回事。

LoRA 为什么改变了整个账本

回到 9.1.2 的五账单:16N16N 里真正被优化器照料的是 m,vm,v 与主权重(12N12N)。全参微调时它们也属于 NN 个参数;而 LoRA 冻结主干后,需要优化器照料的只有那对极小的 A,BA,B(参数量 r(d+k)r(d+k),r=8r=8 时通常不到全参的 1%1\%)。于是:

  • 主干:只需「以低位宽驻留 + 前向」,不需要梯度、不需要优化器状态;
  • 适配器:需要全部五账单,但它的规模只有千分之几。

一句话记住:LoRA 把 12N12N 换成 12×0.001N12\times0.001N。这正是 Lab 3 能在一张消费级卡上微调的原因。代价是表达能力受限:低秩假设在某些任务上不够(需要满秩更新),此时要么增大 rr,要么回到全参 + 分片。

例 9.5.4 · 7B 的 QLoRA 账本重算(可复算)

N=7×109N=7\times10^{9},L=32L=32,h=4096h=4096。在 q_proj / v_proj 上挂 LoRA,r=8r=8,即 L×2=64L\times2=64 个矩阵,每个矩阵含 Wd×dW_{d\times d}(d=hd=h),并假设不放置(din=dout=4096d_{\text{in}}=d_{\text{out}}=4096,忽略 GQA 下 KV 投影更窄的细节)。自适应:主干 4 bit(NF4,含双重量化的额外开销,按 0.5 字节/参数计),适配器 fp32 训练。

求:主干显存、适配器参数量与其五账单、总静态占用;并说明 24 GB 卡上还剩多少给激活。

解

(1)主干(冻结、4 bit)。 0.5 字节×7×109=3.5 GB.0.5\ \text{字节}\times7\times10^{9}=3.5\ \text{GB}.

(2)适配器参数量。 每个矩阵:A∈Rr×dA\in\mathbb R^{r\times d}、B∈Rd×rB\in\mathbb R^{d\times r},共 2rd2rd 个参数。 PLoRA=64×(2×8×4096)=64×65,536=4.19×106 (约 4.2M).P_{\text{LoRA}}=64\times(2\times8\times4096)=64\times65{,}536=4.19\times10^{6}\ \text{(约 4.2M)}. 占全参比例: 4.19×1067×109≈0.06%.\frac{4.19\times10^{6}}{7\times10^{9}}\approx0.06\%.

(3)适配器的五账单(fp32)。 参数 4P=16.84P=16.8 MB;梯度 4P=16.84P=16.8 MB;m,vm,v 各 4P=16.84P=16.8 MB;主权重(fp32 与参数同算一份)16.816.8 MB。合计 ≈84\approx84 MB —— 小于 0.1 GB,可忽略。

(4)总静态占用。 3.5 GB(主干)+0.08 GB(适配器)≈3.6 GB.3.5\ \text{GB(主干)}+0.08\ \text{GB(适配器)}\approx\mathbf{3.6\ GB}. 这就是 QLoRA 的核心结论:一张 24 GB 卡上还剩约 20 GB 给激活、KV cache 与优化器临时张量。

(5)与全参对照。

方案 静态占用 / 卡(单卡口径) 能否在 24 GB 上跑
全参 + 混合精度 Adam 112112 GB 否
全参 + 8-bit 优化器 7070 GB 否
全参 + 8-bit 优化器 + k=4k=4 分片 17.517.5 GB 勉强(激活余量紧张)
LoRA(bf16 主干) 1414 GB + 0.080.08 GB 勉强(激活余量小)
QLoRA(4 bit 主干 + LoRA) ≈3.6\approx3.6 GB 可以,余量充足

注意两个易错点:① 「4 bit 主干」占 0.50.5 字节/参数是 NF4 单重量化的理想值,双重量化与分块缩放会引入少量额外开销(工程上常按 0.55–0.6 字节/参数估计,精确值需实测);② 训练时若把主干临时反量化到 bf16 做矩阵乘,会瞬时多占一份主干显存(现代实现按层反量化以抑制峰值,具体行为需按实现核对)。■\blacksquare

∎
显存优化手段的八个高频误解
  1. 「混合精度能省一半静态显存」。静态账(模型状态)从 16N16N 到 16N16N:省下的 4N4N 被主权重吃回去了(例 9.5.1)。省的主要是激活与临时张量。
  2. 「bf16 不需要任何数值处理」。bf16 免损失缩放,但累加、softmax、LayerNorm、归约仍需 fp32;全 bf16 会让长序列训练误差累积。
  3. 「fp16 配 bf16 的配方也行」。fp16 的指数范围窄,必须配损失缩放;若缩放因子设置不当,会出现「梯度静默变 0」或「损失突然 nan」两类相反故障。
  4. 「开了激活重计算,参数量就能变大」。它只管激活那半本账。模型状态要靠分片或 8-bit 优化器。
  5. 「CPU offload 只是慢一点」。慢的倍数常在 1.5–3 倍甚至更高,且每步都要在 PCIe 上搬 GB 级数据(9.5.4)。它适合「偶发、一次性」的微调,不适合长训练。
  6. 「LoRA 能替代并行」。它替代的是「改动的规模」:如果任务需要满秩更新,LoRA 的秩 rr 就是表达瓶颈,此时仍要回到全参 + 分片。
  7. 「4-bit 主干 + LoRA 的显存就是 0.5N0.5N」。还有激活、KV cache、适配器、临时反量化副本与分片常数(例 9.5.4 的注意点)。
  8. 「量化(GPTQ/AWQ)也能省训练显存」。它们的目标是部署:把权重压到 4 bit 供推理;训练时的瓶颈(m,vm,v 与主权重)不在它们的解决范围内。
即时自测 · 9.5

Q1 写出 fp16 与 bf16 的位布局,并解释「为什么 fp16 需要损失缩放而 bf16 通常不需要」。

Q2 N=7×109N=7\times10^{9}、单卡 80 GB、混合精度 Adam。列出「不开分片」情况下能把静态占用压到 80 GB 以下的两条手段及其结果,并说明各自代价。

Q3 一个训练任务激活 OOM,已知模型状态只占 30 GB(卡有 80 GB)。给出三条处方并排序(说明为什么先做哪条)。

Q4 QLoRA 为什么能把 7B 微调压到 24 GB 卡?用「五账单」的语言回答,并指出其中哪一项被彻底绕开了。

参考要点

Q1 fp16:符号 1 / 指数 5 / 尾数 10,动态范围窄(下溢阈值约 6×10−86\times10^{-8});bf16:符号 1 / 指数 8 / 尾数 7,指数范围与 fp32 相同。反向梯度常小到 10−810^{-8} 量级,在 fp16 里会下溢为 0(更新丢失),所以要把损失放大 SS 倍让梯度落在可表示区;bf16 的范围足够宽,不需要这个放大,代价是尾数少、精度低(累加类算子要在 fp32 做)。

Q2 两条手段:① 8-bit 优化器:m,vm,v 从 8N8N 降到 2N2N,静态从 112112 GB 降到 7070 GB(<80&lt;80 GB);代价是量化/反量化算子开销与超参等效性验证。② 换 SGD(无动量)或减少优化器状态:m,vm,v 与主权重中 m,vm,v 全额省掉(112→84112\to84 GB,仍超 80)——不充分;更现实的是 k=2k=2 的 ZeRO-1/2 分片(属于「分片」而非「省」,若允许则可到 5959 GB)。也可答「CPU offload 优化器状态」:静态降到 8N8N 量级,但每步 PCIe 往返使训练显著变慢。

Q3 三条处方与顺序:① 梯度累积 + 减小 micro-batch(最便宜、不改变数值,立刻可做);② 激活重计算(选择性优先,额外计算只有个位数百分比);③ 序列并行 / 减小序列长度(涉及模型改造,长序列才需要)。排序理由:代价递增;前两条只改配置/包一层 checkpoint,第三条要改模型切分。

Q4 五账单 = 参数、梯度、优化器状态、主权重、激活。LoRA 只让适配器承担梯度与优化器状态(12×12\times 极小量),主干被量化到 4 bit 后只保留「参数」这一项(且以 0.5 字节/参数计)。被彻底绕开的是主干的梯度与优化器状态(12N12N)——也就是基线账本中最贵的部分。

延伸阅读
  • 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)——激活重计算的原始论文(O(L)O(\sqrt L) 显存的来源)。
  • 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 优化器把 16N16N 压到 10N10N)、CPU offload(用 PCIe 换容量,速度代价大)或分片(9.2)来降;激活靠混合精度(减半)、激活重计算(全层或选择性)与梯度累积来降。混合精度与 FP8 的显存收益主要在激活,静态账几乎不变;FP8 的代价是缩放管理与新硬件。LoRA / QLoRA 用「只训练极少量参数」把最贵的 12N12N 绕开,是单卡微调 7B 的唯一实用路径。下一节处理切与省都用上之后剩下的问题:通信、容错与确定性。