文章类型技术长文 所属专栏Agent 算法 预计阅读75 分钟 文档状态已发布
返回

第 04 篇:QLoRA 原理

从 4-bit 量化底座、NF4、Double Quantization 到 Paged Optimizer,完整解释 QLoRA 的精度、梯度、显存和训练稳定性边界。

开始阅读全文15020 字 · 75 分钟 查看系列目录Agent 算法
关键词 AgentQLoRALoRANF4量化PEFT
栏目 AgentAlgorithms;专栏 Agent 算法;标签 Agent、QLoRA、LoRA、NF4、量化、PEFT

QLoRA 最容易被一句“4-bit LoRA”说模糊。它并不是把 LoRA 适配器压成 4-bit,也不是用 4-bit 梯度训练整个模型。QLoRA 的关键分工是:冻结的预训练底座以 4-bit 形式存储;使用时按需反量化到较高精度参与计算;梯度穿过底座,但只更新 LoRA 等显式放开的参数。

导读#

普通 LoRA 已经不再为底座保存梯度和优化器状态,但完整底座通常仍以 FP16 或 BF16 驻留显存。对于一个含 PP 个底座参数的模型,仅 16-bit 权重就约需要:

Mbase,162P Byte.M_{\text{base,16}} \approx 2P\ \text{Byte}.

QLoRA 进一步量化这个冻结但仍参与计算的底座。若暂时忽略量化元数据,4-bit 权重主体约为:

Mbase,44P8=0.5P Byte.M_{\text{base,4}} \approx \frac{4P}{8} = 0.5P\ \text{Byte}.

它在一个线性层上的核心形式可以写为:

W^=Dequant(Q4,S),\widehat W = \operatorname{Dequant} \left( Q_4,\mathcal{S} \right),y=W^x+αrBAx,y = \widehat W x + \frac{\alpha}{r}BAx,

其中:

  • Q4Q_4 是冻结权重的 4-bit 编码;
  • S\mathcal{S} 是 Scale、码本和 Double Quantization 状态等元数据;
  • W^\widehat W 是在计算精度下重建的近似权重;
  • A,BA,B 是可训练 LoRA 矩阵;
  • 底座路径与 LoRA 路径在输出端相加。

本文沿着以下链路展开:

LoRA 的剩余显存瓶颈4-bit 冻结底座NF4Double Quantization按需反量化与梯度传播Paged Optimizer工程配置与实验边界.\text{LoRA 的剩余显存瓶颈} \rightarrow \text{4-bit 冻结底座} \rightarrow \text{NF4} \rightarrow \text{Double Quantization} \rightarrow \text{按需反量化与梯度传播} \rightarrow \text{Paged Optimizer} \rightarrow \text{工程配置与实验边界}.

读完以后,应能够独立回答以下问题:

  1. QLoRA 为什么仍然属于 LoRA 参数高效微调;
  2. 4-bit 存储、BF16/FP16 计算和适配器精度分别是什么;
  3. NF4 为什么不是普通 INT4,也不是带指数位的标准 FP4;
  4. Double Quantization 的 0.3730.373 bit/parameter 如何推导;
  5. 底座被冻结以后,梯度为什么仍需穿过量化线性层;
  6. Paged Optimizer 解决的是哪类峰值,而不是哪类平均显存;
  7. 理论上的 0.50.5 Byte/parameter 为什么不等于真实训练显存;
  8. 训练所得 Adapter 应如何保存、合并和重新量化。

1. QLoRA 的定义与方法定位#

QLoRA 精度与梯度路径

1.1 QLoRA 解决的显存瓶颈#

LoRA 冻结预训练模型,只为低秩适配器保留梯度和优化器状态。它显著压缩了“可训练参数态”,但没有自动压缩“冻结底座权重态”。

若底座含 PP 个参数,普通 16-bit LoRA 至少要常驻约 2P2P Byte 的底座权重。粗略看:

名义参数规模16-bit 权重主体4-bit 权重主体
7B约 14 GB约 3.5 GB
13B约 26 GB约 6.5 GB
65B约 130 GB约 32.5 GB

表中使用十进制 GB,只计算权重码本主体,不包含量化 Scale、未量化模块、LoRA、激活值、临时缓冲、CUDA Context 和内存碎片,因此不能当作显卡容量建议。

QLoRA 解决的首要问题是:

底座虽然不更新,但仍是模型中最大的一组常驻参数,能否用低比特形式保存,同时让损失梯度继续穿过它并训练适配器?

QLoRA 原论文 给出的答案是:将预训练权重量化到 4-bit,前向和反向使用时再反量化到计算数据类型,只为 LoRA 参数计算并保存权重梯度。论文在特定 LLaMA 65B、单张 48GB GPU 和相应训练配置下完成了微调;这个结果证明了方法可行,不意味着任意 65B/70B 模型、序列长度和 Batch Size 都能复现同一容量。

QLoRA 没有消除的显存包括:

  • 激活值;
  • Attention 中间量;
  • LoRA 权重与梯度;
  • LoRA 优化器状态;
  • 量化元数据;
  • 反量化与算子工作区;
  • 日志、评估和生成阶段可能引入的额外缓存。

因此它主要压缩的是冻结模型权重驻留,不是训练显存的所有组成部分。

1.2 QLoRA 与普通 LoRA 的共同部分#

QLoRA 保留了普通 LoRA 的核心参数化:

Weff=Wbase+sBA,s=αrW_{\mathrm{eff}} = W_{\mathrm{base}} + sBA, \qquad s=\frac{\alpha}{r}

或采用行向量记号:

Y=XWbase+sXAB.Y = XW_{\mathrm{base}} + sXAB.

两种写法只是矩阵方向约定不同。本文沿用列向量记号:

WbaseRdout×din,ARr×din,BRdout×r.W_{\mathrm{base}} \in \mathbb{R}^{d_{\mathrm{out}}\times d_{\mathrm{in}}}, \quad A\in\mathbb{R}^{r\times d_{\mathrm{in}}}, \quad B\in\mathbb{R}^{d_{\mathrm{out}}\times r}.

普通 LoRA 与 QLoRA 的共同点包括:

  • 底座参数被冻结;
  • 低秩矩阵 A,BA,B 可训练;
  • 可训练参数量仍为 r(din+dout)r(d_{\mathrm{in}}+d_{\mathrm{out}})
  • 损失函数仍由 SFT、DPO、Reward Model 或 RL 等上层目标决定;
  • Adapter 可以单独保存并与同一底座配合加载;
  • Target Modules、Rank、Alpha 和 Dropout 仍是关键超参数。

差异集中在底座路径:

维度普通 LoRAQLoRA
冻结底座的常驻表示常见为 FP16/BF16常见为 NF4/FP4 4-bit
底座计算前处理直接读取高精度权重按需反量化
量化元数据Scale、码本、量化状态
量化误差无此项存在
主要额外依赖LoRA 实现4-bit Kernel 与量化后端

所以 QLoRA 不是替代 LoRA,而是:

QLoRA=LoRA 参数化+量化冻结底座+相应内存管理.\text{QLoRA} = \text{LoRA 参数化} + \text{量化冻结底座} + \text{相应内存管理}.

1.3 量化底座、LoRA 参数与计算精度的分工#

讨论 QLoRA 时至少要拆开四种“精度角色”:

角色典型配置负责什么
底座存储类型NF4 4-bit压缩冻结权重的长期驻留
计算类型BF16、FP16,部分实现也可 FP32反量化后的矩阵计算与中间结果
LoRA 参数类型论文常用 BF16;现代 PEFT 也可能默认保留/提升为 FP32保存可训练低秩参数
优化器状态类型32-bit、8-bit 或 Paged 变体保存一阶、二阶统计量

这些配置不是同一个开关。例如:

BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)

这里只明确了底座量化与底座线性层的计算类型,并没有单独规定:

  • LoRA 参数最终以什么 Dtype 保存;
  • Trainer 是否开启 BF16 自动混合精度;
  • 优化器状态使用 32-bit 还是 8-bit;
  • Norm、Embedding、LM Head 是否保持 FP32/BF16;
  • FSDP 的量化打包存储类型是什么。

当前 PEFT 的部分版本会为了稳定性把 FP16/BF16 Adapter 提升为 FP32;QLoRA 原始实现则在 BF16 配置下显式将 LoRA 层转为 BF16。工程文章必须报告实际运行时 Dtype,而不能只根据配置名推断。

1.4 QLoRA 是训练方案而非独立损失函数#

设训练数据为 D\mathcal{D},任务损失为 Ltask\mathcal{L}_{\text{task}}。QLoRA 优化问题可写为:

minA,B,ϕLtask(W^base,A,B,ϕ;D),\min_{A,B,\phi} \mathcal{L}_{\text{task}} \left( \widehat W_{\mathrm{base}}, A, B, \phi; \mathcal{D} \right),W^base=Dequant(Q4,S),Q4,S 冻结.\widehat W_{\mathrm{base}} = \operatorname{Dequant}(Q_4,\mathcal{S}), \qquad Q_4,\mathcal{S}\ \text{冻结}.

ϕ\phi 表示除 LoRA 外显式放开的少量参数,例如某个任务 Head 或新增 Token。损失可以是:

  • SFT 的 Token-level 交叉熵;
  • DPO 的 Pairwise Preference Loss;
  • Reward Model 的排序或回归损失;
  • PPO、GRPO 等策略更新目标;
  • Continued Pretraining 的语言模型似然目标。

因此以下表达不完整:

“模型用 QLoRA 算法训练。”

更可复现的表达应是:

“在 NF4 + Double Quantization 的冻结 4-bit 底座上,对全部 Transformer 线性层注入 Rank-16 LoRA,使用 BF16 Compute Dtype 和 SFT Response-only Cross-Entropy 训练。”

QLoRA 回答的是“用什么精度存底座、用什么参数更新路径适配模型”;任务损失回答的是“希望模型行为朝什么方向变化”。二者处于不同概念层。


2. QLoRA 的整体计算结构#

2.1 4-bit 冻结底座模型#

设原始预训练权重为:

W0Rdout×din.W_0 \in \mathbb{R}^{d_{\mathrm{out}}\times d_{\mathrm{in}}}.

量化后不再长期保存每个元素的 BF16/FP16 数值,而是保存:

Q(W0)=(Q4,S,C),\mathcal{Q}(W_0) = \left( Q_4,\mathcal{S},\mathcal{C} \right),

其中:

  • Q4Q_4:每个权重对应的 4-bit 索引,通常两个值打包到一个 Byte;
  • S\mathcal{S}:每个 Block 的 Scale 或其二次量化状态;
  • C\mathcal{C}:NF4/FP4 码本与恢复所需的其他状态。

模型实际使用的是近似权重:

W^0=D(Q4,S,C),\widehat W_0 = \mathcal{D} \left( Q_4,\mathcal{S},\mathcal{C} \right),

通常有:

W^0W0.\widehat W_0 \ne W_0.

量化误差定义为:

Eq=W0W^0.E_q = W_0-\widehat W_0.

“冻结”表示优化器不更新 Q4Q_4 与其对应的底座权重,不表示底座在反向图中完全消失。它仍参与前向,也参与对输入激活梯度的计算。

2.2 高精度可训练 LoRA 适配器#

QLoRA 在量化线性层旁增加 LoRA 分支:

ΔW=BA,rank(ΔW)r.\Delta W = BA, \qquad \operatorname{rank}(\Delta W)\le r.

完整前向为:

y=W^0x+sBAx.y = \widehat W_0x + sBAx.

这里的“高精度”是相对 4-bit 底座而言,典型是 BF16、FP16 或 FP32,不代表所有实现都固定使用同一种 Dtype。

LoRA 参数不应被误解为 4-bit:

  • 它们需要接收连续梯度更新;
  • 优化器需要保存足够精度的统计量;
  • 训练稳定性通常依赖 16-bit 或 32-bit 浮点表示;
  • 其参数量远小于底座,没必要用同样激进的 4-bit 压缩换取较小收益。

原始 QLoRA 论文在主要实验中使用 Rank r=64r=64α=16\alpha=16,并把 LoRA 加到所有 Transformer 线性层;这是一套经实验选择的配置,不是 QLoRA 定义要求。现代任务常从更小 Rank 开始,再通过消融确定容量。

2.3 前向传播中的按需反量化#

QLoRA 的计算可抽象为:

W^0(c)=Dequantc(Q4,S),\widehat W_0^{(c)} = \operatorname{Dequant}_{c} \left( Q_4,\mathcal{S} \right),ybase=MatMulc(W^0(c),x(c)),y_{\mathrm{base}} = \operatorname{MatMul}_{c} \left( \widehat W_0^{(c)},x^{(c)} \right),

其中上标 cc 表示 Compute Dtype,例如 BF16。

“按需反量化”不应被想象成:

  1. 先为整个模型创建一份完整 BF16 副本;
  2. 让该副本在整个训练过程中常驻;
  3. 再执行普通 BF16 模型。

实际 4-bit Kernel 会在使用某层权重时解包、恢复 Scale 并以计算类型参与算子。实现可能按 Tile 或 Kernel 内部融合处理,临时工作区和缓存取决于后端;核心是不长期驻留一份完整高精度底座副本

因此 QLoRA 仍有反量化成本:

  • 读取 4-bit 编码与元数据;
  • 解包 4-bit 值;
  • 二级恢复 Scale;
  • 查 NF4 码本并缩放;
  • 转成 Compute Dtype;
  • 执行矩阵乘法。

较小的权重带宽与额外的解码工作形成吞吐权衡,不能仅凭 Bit 数断言 QLoRA 一定更快。

2.4 反向传播中的梯度路径#

令:

δ=Ly.\delta = \frac{\partial\mathcal{L}}{\partial y}.

对于:

y=W^0x+sBAx,y = \widehat W_0x+sBAx,

LoRA 梯度为:

LB=sδ(Ax),\frac{\partial\mathcal{L}}{\partial B} = s\delta(Ax)^\top,LA=sBδx.\frac{\partial\mathcal{L}}{\partial A} = sB^\top\delta x^\top.

输入梯度为:

Lx=W^0δ+sABδ.\frac{\partial\mathcal{L}}{\partial x} = \widehat W_0^\top\delta + sA^\top B^\top\delta.

而冻结底座不需要保存:

LW0\frac{\partial\mathcal{L}}{\partial W_0}

用于优化器更新。实现上通常直接令量化底座参数:

param.requires_grad = False

关键区别是:

不计算或不保存底座的“权重更新梯度”,不等于不计算穿过底座的“输入梯度”。

较早层的 LoRA 参数依赖后续层传回的输入梯度。如果把冻结层当作 detach(),计算图会被截断,前面的 Adapter 无法学习。

2.5 权重存储精度与计算精度的区别#

可以用三个问题检查一段 QLoRA 描述是否准确:

  1. 硬盘或显存中长期保存什么? 4-bit 编码、Scale、码本和少量未量化参数。

  2. 矩阵乘法吃进去什么? 经反量化、转换到 Compute Dtype 的近似权重,以及同计算路径兼容的激活。

  3. 优化器更新什么? LoRA 和其他显式放开的可训练参数。

所以:

表述是否正确原因
“QLoRA 的底座是 4-bit 存储”指长期权重表示
“QLoRA 用 4-bit 梯度更新底座”底座冻结
“QLoRA 的矩阵乘法一定是原生 4-bit×4-bit”论文方案反量化到 BF16 计算
“4-bit 模型执行时完全不产生 16-bit 权重片段”使用时需反量化到计算类型
“Compute Dtype 改为 BF16 会把常驻底座变回 BF16”存储类型与计算类型分离

这一区分也是理解显存估算、性能和部署兼容性的基础。


3. 模型量化的基础知识#

3.1 浮点表示与低比特整数表示#

常见浮点数由符号、指数和尾数组成。以 FP32、FP16、BF16 为例:

类型总 Bit指数 Bit尾数 Bit主要特点
FP3232823范围与精度都高
FP1616510精度较高,但动态范围较窄
BF161687动态范围接近 FP32,尾数更短

低比特量化通常不再为每个权重保留完整浮点结构,而让少量 Bit 表示离散级别的索引。例如 4-bit 可编码:

24=162^4=16

个状态。

但“4-bit”只说明索引宽度,不说明 16 个状态具体对应什么:

  • INT4:通常对应均匀整数级别;
  • FP4:按某种符号、指数、尾数规则构造;
  • NF4:查找一个针对近似正态分布设计的非均匀码本;
  • 其他 4-bit 格式:可以有完全不同的码点与缩放规则。

因此不能把所有 4-bit 方案都写成“把 FP16 转成四位整数”。对 NF4,更准确的说法是:

每个 4-bit 值是一个码本索引;恢复时先查非均匀浮点码点,再乘对应 Block 的 Scale。

3.2 Scale、Zero Point 与量化区间#

通用仿射量化常写为:

q=clip(round(xs)+z,qmin,qmax),q = \operatorname{clip} \left( \operatorname{round} \left( \frac{x}{s} \right) + z, q_{\min}, q_{\max} \right),

反量化为:

x^=s(qz).\widehat x = s(q-z).

其中:

  • s>0s>0 是 Scale;
  • zz 是 Zero Point;
  • [qmin,qmax][q_{\min},q_{\max}] 是离散编码范围;
  • Clip 负责处理超出可表示区间的值。

若给定实数区间 [xmin,xmax][x_{\min},x_{\max}] 和整数范围 [qmin,qmax][q_{\min},q_{\max}],一种常见非对称配置是:

s=xmaxxminqmaxqmin,s = \frac{x_{\max}-x_{\min}} {q_{\max}-q_{\min}},zqminxmins.z \approx q_{\min} - \frac{x_{\min}}{s}.

但 NF4 的查表量化不是简单的等距仿射 INT4。对一个 Block w(b)w^{(b)},常见抽象是:

ab=maxiwi(b),a_b = \max_i \left| w_i^{(b)} \right|,ui=wi(b)ab[1,1],u_i = \frac{w_i^{(b)}}{a_b} \in[-1,1],ji=argminj{0,,15}uicj,j_i = \arg\min_{j\in\{0,\ldots,15\}} \left| u_i-c_j \right|,w^i(b)=abcji,\widehat w_i^{(b)} = a_b c_{j_i},

其中 cjc_j 是 NF4 码本值。

3.3 对称量化与非对称量化#

对称量化通常令可表示区间围绕零对称:

xminxmax,z=0 或采用隐式零点.x_{\min}\approx-x_{\max}, \qquad z=0 \ \text{或采用隐式零点}.

优点包括:

  • 公式与 Kernel 更简单;
  • 对零中心权重分布自然;
  • Scale 只由绝对最大值决定。

缺点是若数据明显偏向一侧,另一侧的编码范围可能浪费。

非对称量化让 xminx_{\min}xmaxx_{\max} 分别映射到编码端点,通常能更充分覆盖偏移分布,但需要 Zero Point 或等价偏移。

NF4 的“asymmetric”需要单独理解。QLoRA 论文为确保 0 可以被精确表示,把负半区和正半区的分位数集合分别构造,再合并并去掉一个重复零点,因此最终 16 个码点并非正负严格镜像。这不等于普通 Affine INT4 中学习了一个任意整数 Zero Point。

Double Quantization 的第二级 Scale 则会先减去均值,使正值 Scale 近似中心化,再使用对称量化。这说明同一训练方案的不同量化层级可以采用不同中心化策略。

3.4 Per-tensor、Per-channel 与 Block-wise Quantization#

量化粒度决定多少元素共享一组 Scale。

Per-tensor

整个张量共享一个 Scale:

W^=sQ.\widehat W = s\,Q.

元数据最少,但一个异常大值可能拉宽全张量范围,使其余元素利用不到足够离散级别。

Per-channel

每个输出通道或输入通道共享一个 Scale:

W^i,:=siQi,:.\widehat W_{i,:} = s_iQ_{i,:}.

能适应通道间范围差异,元数据多于 Per-tensor。

Block-wise

把展平或按某种布局排列的权重切成大小为 BB 的 Block,每块独立量化:

W{W(1),W(2),,W(n)},nPB.W \rightarrow \left\{ W^{(1)},W^{(2)},\ldots,W^{(n)} \right\}, \qquad n \approx \left\lceil \frac{P}{B} \right\rceil.

每个 Block 保存一个 Scale:

W^(b)=abC(Q(b)).\widehat W^{(b)} = a_b C \left( Q^{(b)} \right).

QLoRA 论文对底座权重使用 Block Size 64,对第一次量化产生的 Scale 再以 Block Size 256 做第二级量化。当前 bitsandbytes 的低层 API 支持多种 Block Size,但某个后端、设备或版本支持哪些值应以实际文档和运行检查为准。

3.5 Quantization Error 的来源#

量化误差:

ei=xix^ie_i = x_i-\widehat x_i

主要来自五类因素。

1. 舍入误差

连续值必须落到有限码点:

x^i=nearestCode(xi).\widehat x_i = \operatorname{nearestCode}(x_i).

2. 截断误差

超出量化区间的值被 Clip 到边界。

3. Scale 共享误差

一个 Block 内的异常值增大 aba_b,使大量普通值被压缩到码本中央的较少区域。

4. 分布失配

NF4 针对近似零中心正态权重设计。若某层或某个 Block 的实际分布偏斜、多峰、重尾或含结构性异常值,等概率正态码本不再是该经验分布的最优表示。

5. 元数据量化误差

Double Quantization 还会近似 Scale,本身引入第二级误差。

常用误差指标包括:

MSE=1Pi=1P(xix^i)2,\operatorname{MSE} = \frac{1}{P} \sum_{i=1}^{P} (x_i-\widehat x_i)^2,RelativeError=XX^FXF+ϵ.\operatorname{RelativeError} = \frac{\lVert X-\widehat X\rVert_F} {\lVert X\rVert_F+\epsilon}.

但更小的权重 MSE 不保证任务指标一定更高。量化误差经过多层非线性传播后,会受到层敏感性、输入分布、归一化和 LoRA 补偿能力共同影响,因此最终仍需做任务级评估。


4. NF4 数据类型的设计原理#

均匀量化与 NF4 Codebook

4.1 神经网络权重的近似正态分布假设#

QLoRA 对 NF4 的出发点是:很多预训练权重张量可以近似看作均值接近零、尺度不同的正态分布:

WN(0,σ2).W \sim \mathcal{N}(0,\sigma^2).

若不同张量或 Block 的主要差异只是尺度 σ\sigma,就可以先用绝对最大值等 Scale 把它们规范到:

[1,1],[-1,1],

再共享一套针对标准正态分布设计的离散码本。

这个假设应理解为工程近似,而不是神经网络权重的普遍定理:

  • 不同模型家族的权重分布不同;
  • Attention、MLP、Embedding 和 Head 可能具有不同统计形态;
  • 一个完整张量近似正态,不代表每个小 Block 都精确正态;
  • 训练后权重可能偏斜、重尾或多峰;
  • MoE Expert、归一化参数等特殊模块不一定满足同样假设。

QLoRA 论文在附录中用多种预训练模型的权重统计支持这一近似,并通过下游实验验证 NF4 的实际效果。正确结论是:

对论文研究的预训练模型权重分布,正态先验足以构造一套有效的通用 4-bit 码本。

不应扩写成:

所有神经网络权重都严格服从正态分布,因此 NF4 对任何张量都必然最优。

4.2 均匀量化与分位数量化的差异#

均匀量化在数值轴上放置等间隔码点。若归一化区间为 [1,1][-1,1],16 个码点可抽象为:

cjuniform=1+2j15,j=0,,15.c_j^{\text{uniform}} = -1 + \frac{2j}{15}, \qquad j=0,\ldots,15.

相邻码点间距固定:

cj+1uniformcjuniform=215.c_{j+1}^{\text{uniform}} - c_j^{\text{uniform}} = \frac{2}{15}.

若数据在数轴上均匀分布,这种配置自然;但正态分布的大多数概率质量集中在零附近,尾部样本较少。均匀码本会把相同数量的级别分配给稀疏尾部和稠密中心,中心区域的分辨率不足。

分位数量化的思路是让各量化 Bin 在目标分布下拥有近似相等的概率质量。设标准正态分布 CDF 为 Φ(x)\Phi(x),Quantile Function 为:

Φ1(p).\Phi^{-1}(p).

可以在概率轴上等距取点,再映射回数值轴。于是:

  • 零附近分布密,码点也更密;
  • 尾部分布稀,码点间距更大;
  • 各 Bin 预期接收相近数量的样本;
  • 固定 16 个状态更贴合正态权重的信息分布。

二者的差异不是“分位数量化没有误差”,而是把有限码点预算分配到更常出现的位置

4.3 NormalFloat 4-bit 的离散值设计#

QLoRA 论文先对理论标准正态分布构造分位数码点,再归一化到 [1,1][-1,1]。一种概念表达是:

c~i=12[Φ1(i2k+1)+Φ1(i+12k+1)],\widetilde c_i = \frac{1}{2} \left[ \Phi^{-1} \left( \frac{i}{2^k+1} \right) + \Phi^{-1} \left( \frac{i+1}{2^k+1} \right) \right],

然后做范围归一化:

ci=c~imaxjc~j.c_i = \frac{\widetilde c_i} {\max_j|\widetilde c_j|}.

直接对称构造会缺少精确的零码点。为让 Padding 或真实零值可以无误差表示,NF4 分别构造负半区与正半区,其中一侧多取一个值,合并两个集合并删除重复零。最终得到 16 个不等距码点。

当前 bitsandbytes 源码中的 NF4 查找表为:

-1.0000000, -0.6961928, -0.5250731, -0.3949175,
-0.2844414, -0.1847734, -0.0910500, 0.0000000,
0.0795803, 0.1609302, 0.2461123, 0.3379152,
0.4407098, 0.5626170, 0.7229568, 1.0000000

这些数值揭示三点:

  1. 相邻间距不相等;
  2. 零附近级别更密;
  3. 正负码点并非严格镜像。

NF4 不是“4-bit 浮点数”的 IEEE 式缩小版。4-bit 字段只是查表索引,没有固定的符号位、指数位、尾数位分解。

对一个 Block,编码过程可以写为:

ab=maxiwi,a_b = \max_i |w_i|,qi=argminjwiabcj,q_i = \arg\min_{j} \left| \frac{w_i}{a_b} -c_j \right|,

反量化为:

w^i=abcqi.\widehat w_i = a_bc_{q_i}.

4.4 NF4 对预训练权重信息的保留#

在固定 4-bit 预算下,NF4 的优势来自对正态分布概率质量的匹配。若 XN(0,1)X\sim\mathcal{N}(0,1),每个 Bin 的概率近似为:

Pr(XBj)116.\Pr(X\in\mathcal{B}_j) \approx \frac{1}{16}.

这意味着编码状态不会像均匀量化那样在概率意义上严重闲置。用信息熵直观解释,若 16 个编码状态使用概率接近相等,则:

H(Q)=j=116pjlog2pj4 bit.H(Q) = -\sum_{j=1}^{16}p_j\log_2p_j \approx 4\ \text{bit}.

这就是论文所称“information-theoretically optimal”的背景:在其分布假设和量化构造下,码本状态被充分利用。它不等于:

  • 对每个具体权重张量都实现最小 MSE;
  • 对任意任务都保证零精度损失;
  • 对非正态分布仍然最优;
  • 量化后的模型函数与原模型完全一致。

Block-wise Absmax 还承担了局部尺度适配。若每 64 个权重独立计算 aba_b,不同局部区域可以使用同一 NF4 码本而保留各自动态范围。这通常比全张量共享 Scale 更精确,但也增加 Scale 元数据。

QLoRA 论文的实验结果显示,在其比较的模型和基准上,NF4 的困惑度与零样本准确率总体优于所测 FP4/INT4 配置;在 LLaMA 指令微调实验中,NF4 + Double Quantization 的平均 MMLU 表现匹配 BF16 LoRA。这个结论应限定在论文实验设置内,不能外推为所有新架构、任务和量化 Kernel 的固定排名。

4.5 NF4 与 FP4、INT4 的比较安排#

三类 4-bit 表示可按“码点如何产生”比较:

类型4-bit 状态的含义码点间距主要先验
INT4整数级别,经 Scale/Zero Point 映射通常均匀数值范围
FP4符号、指数、尾数式编码或对应查表非均匀,强调动态范围浮点结构
NF4标准正态分位数码本索引非均匀,零附近更密近似正态权重

应控制相同条件再比较:

  • 相同模型与原始权重;
  • 相同量化粒度;
  • 相同 Block Size;
  • 相同 Compute Dtype;
  • 相同 Double Quantization 开关;
  • 相同 LoRA 配置;
  • 相同训练 Token、数据顺序与随机种子;
  • 相同评估和推理后端。

否则,“NF4 比 FP4 好”可能混入 Block Size、Kernel、LoRA Target Modules 或训练预算差异。

还应区分两类实验:

  1. 纯量化误差实验:不训练 Adapter,比较量化后模型的困惑度、零样本准确率和权重误差;
  2. 量化微调实验:在相同 LoRA 配置下训练,再比较最终任务效果。

LoRA 可能补偿部分量化误差,所以第二类结果不能反推出第一类量化器本身的误差排序。


5. Double Quantization 原理#

5.1 分块量化中的量化常数开销#

Block-wise Quantization 为每个 Block 保存一个 Scale。设:

  • 底座参数量为 PP
  • 第一层 Block Size 为 B1B_1
  • 每个 Scale 使用 bsb_s Bit。

Scale 数量约为:

NsPB1.N_s \approx \frac{P}{B_1}.

Scale 元数据平均到每个权重上的 Bit 数为:

ms=NsbsPbsB1.m_s = \frac{N_sb_s}{P} \approx \frac{b_s}{B_1}.

QLoRA 论文使用:

B1=64,bs=32.B_1=64, \qquad b_s=32.

因此:

ms=3264=0.5 bit/parameter.m_s = \frac{32}{64} = 0.5 \ \text{bit/parameter}.

主权重只有 4 bit 时,额外 0.5 bit 相当于权重码本主体的:

0.54=12.5%.\frac{0.5}{4} = 12.5\%.

小 Block 提高局部量化精度,却会增加 Scale 数量。这正是 Double Quantization 试图缓解的矛盾。

5.2 对量化常数进行二次量化#

第一次量化可写为:

WQ1(Q4,c2FP32).W \xrightarrow{\mathcal{Q}_1} \left( Q_4,c_2^{\mathrm{FP32}} \right).

Double Quantization 把第一层 Scale c2c_2 当作新的输入,再做一次量化:

c2FP32Q2(q2FP8,c1FP32).c_2^{\mathrm{FP32}} \xrightarrow{\mathcal{Q}_2} \left( q_2^{\mathrm{FP8}}, c_1^{\mathrm{FP32}} \right).

QLoRA 论文对第二级量化使用:

B2=256,B_2=256,

即每 256 个第一层 Scale 共享一个第二层 FP32 Scale。由于 c2c_2 为正值,论文先减去其均值:

c~2=c2mean(c2),\widetilde c_2 = c_2-\operatorname{mean}(c_2),

再对中心化后的数值做对称 8-bit 量化。恢复顺序是:

c^2=Dequant2(q2,c1),\widehat c_2 = \operatorname{Dequant}_2 \left( q_2,c_1 \right),W^=Dequant1(Q4,c^2).\widehat W = \operatorname{Dequant}_1 \left( Q_4,\widehat c_2 \right).

所以“Double”指 Scale 被再次量化,不是:

  • 把 4-bit 权重再压成 2-bit;
  • 对同一权重连续做两次 4-bit 舍入;
  • 量化 LoRA 参数;
  • 做两次矩阵乘法。

5.3 Double Quantization 的显存收益#

二次量化后,第一层 Scale 由 32 bit 降到 8 bit,其平均开销为:

8B1.\frac{8}{B_1}.

第二层每 B2B_2 个第一层 Scale 保存一个 32-bit Scale,平均到原权重为:

32B1B2.\frac{32}{B_1B_2}.

总元数据开销:

mDQ=8B1+32B1B2.m_{\mathrm{DQ}} = \frac{8}{B_1} + \frac{32}{B_1B_2}.

代入论文配置:

mDQ=864+3264×256=0.125+0.0019531250.127 bit/parameter.m_{\mathrm{DQ}} = \frac{8}{64} + \frac{32}{64\times256} = 0.125 + 0.001953125 \approx 0.127 \ \text{bit/parameter}.

相对未二次量化的 0.5 bit:

Δm=0.50.1270.373 bit/parameter.\Delta m = 0.5-0.127 \approx 0.373 \ \text{bit/parameter}.

对 65B 参数,理想化节省为:

65×109×0.37383.03×109 Byte,\frac{ 65\times10^9\times0.373 }{8} \approx 3.03\times10^9 \ \text{Byte},

即约 3.0 GB,或约 2.82 GiB。原论文把它概括为约 3 GB。

启用 DQ 后,权重码本主体与 Scale 的理论合计约为:

4+0.127=4.127 bit/parameter,4+0.127 = 4.127 \ \text{bit/parameter},

但实际还要加上:

  • 码本与 Offset;
  • Tensor Shape 和 QuantState;
  • 对齐与 Padding;
  • 未量化参数;
  • 框架对象和分配器开销。

5.4 二次量化带来的附加误差#

没有 DQ 时:

w^i=c2C(qi).\widehat w_i = c_2\,C(q_i).

有 DQ 时,Scale 本身被近似为:

c^2=c2+ec,\widehat c_2 = c_2+e_c,

于是:

w^iDQ=c^2C(qi)=c2C(qi)+ecC(qi).\widehat w_i^{\mathrm{DQ}} = \widehat c_2C(q_i) = c_2C(q_i) + e_cC(q_i).

第二项:

ecC(qi)e_cC(q_i)

就是二次量化带来的附加误差。

它通常较小的原因包括:

  • 第一层 Scale 数值范围比原权重更规则;
  • 第二层使用 8-bit,而不是再次使用 4-bit;
  • 先减均值改善对称量化利用率;
  • 第二层 Block Size 256,在元数据开销与局部范围之间折中。

但“论文实验未观察到明显性能退化”不等于数学上无误差。对边界敏感任务、小模型、特殊权重分布或不同后端,应单独消融:

NF4vs.NF4 + DQ.\text{NF4} \quad\text{vs.}\quad \text{NF4 + DQ}.

5.5 Block Size 对开销和误差的影响#

设第一层 Block Size 为 B1B_1。Scale 元数据近似与其成反比:

ms1B1.m_s \propto \frac{1}{B_1}.

更小的 B1B_1

  • 每块范围更局部;
  • 异常值影响的元素更少;
  • 通常量化误差更低;
  • Scale 数量更多;
  • 元数据与 Kernel 管理开销更高。

更大的 B1B_1

  • Scale 更少;
  • 元数据更低;
  • 一个异常值可能拉宽更多权重的范围;
  • 可能增加舍入误差。

第二级 Block Size B2B_2 也有类似权衡,但作用对象是 Scale:

mDQ=8B1+32B1B2.m_{\mathrm{DQ}} = \frac{8}{B_1} + \frac{32}{B_1B_2}.

增大 B2B_2 会进一步减少第二层 FP32 Scale,却可能让更多第一层 Scale 共享一个范围。

论文中的 B1=64,B2=256B_1=64,B_2=256 是其精度、Kernel 和内存目标下的选择。当前 bitsandbytes quantize_4bit 的默认 Block Size 为 64,但默认值和后端支持可能随版本变化。实验报告应保存:

  • bitsandbytes 版本;
  • Quant Type;
  • Block Size;
  • Double Quantization;
  • Quant Storage Dtype;
  • Compute Dtype。

6. QLoRA 的前向与反向传播#

6.1 4-bit 权重读取与反量化#

对某个线性层,设备中持有:

(Q4,q2,c1,μc,C),\left( Q_4, q_2, c_1, \mu_c, \mathcal{C} \right),

分别表示 4-bit 权重编码、8-bit 一级 Scale 编码、二级 FP32 Scale、Scale 均值偏移和 NF4 码本等状态。

Double Dequantization 可抽象为:

c^2=Dequant(c1,q2)+μc,\widehat c_2 = \operatorname{Dequant} \left( c_1,q_2 \right) + \mu_c,W^(c)=Dequant(c^2,Q4,C).\widehat W^{(c)} = \operatorname{Dequant} \left( \widehat c_2,Q_4,\mathcal{C} \right).

读取过程的关键成本不是只看算术次数,还包括:

  • 从显存读取打包权重;
  • 读取量化状态;
  • 解包 Nibble;
  • 码本查找;
  • 数据类型转换;
  • 将恢复后的 Tile 送入矩阵乘法。

一个优化良好的 Kernel 会尽量融合这些步骤并避免完整中间张量落入显存。文章或架构图应画成“按层/按 Tile 恢复并计算”,而不是在模型入口一次性解压整个底座。

6.2 BF16 或 FP16 下的矩阵计算#

令 Compute Dtype 为 cc,通常:

c{BF16,FP16,FP32}.c\in \{ \mathrm{BF16}, \mathrm{FP16}, \mathrm{FP32} \}.

底座分支可写为:

ybase(c)=MatMulc(W^(c),x(c)).y_{\mathrm{base}}^{(c)} = \operatorname{MatMul}_{c} \left( \widehat W^{(c)},x^{(c)} \right).

QLoRA 论文主要使用 BF16。BF16 与 FP32 具有相同的 8-bit 指数宽度,因此动态范围更接近 FP32;FP16 只有 5-bit 指数,溢出和下溢风险更高,但在不支持 BF16 的硬件上可能是现实选择。

Compute Dtype 不应和以下参数混淆:

  • dtype / 旧版本中的 torch_dtype:控制未量化模块或加载类型;
  • bnb_4bit_quant_storage:控制打包量化数据的承载 Dtype,尤其关系到 FSDP;
  • Trainer 的 bf16=True / fp16=True:控制混合精度训练上下文;
  • Adapter 的实际 Parameter Dtype。

可靠做法是训练前遍历参数和模块,实际打印 Dtype,而不是只相信配置对象。

6.3 LoRA 分支与底座分支的结果相加#

采用列向量约定:

z=Ax,z = Ax,ylora=sBz,y_{\mathrm{lora}} = sBz,y=ybase+ylora.y = y_{\mathrm{base}} + y_{\mathrm{lora}}.

若 LoRA Dropout 为 D()D(\cdot),训练态更准确地写为:

ylora=sBAD(x)y_{\mathrm{lora}} = sBA D(x)

或按具体实现位于 LoRA 输入分支上。推理态 Dropout 关闭。

两条分支相加前需要兼容 Dtype。实际实现可能:

  • 将输入临时提升到 Adapter Dtype;
  • 计算 LoRA 输出;
  • 再把结果转换回原输出 Dtype;
  • 最后完成加法。

因此当前 PEFT 若默认把 Adapter 保持为 FP32,不能简单写成“所有 QLoRA 算子都在 BF16”。更严谨的报告应分别列:

对象实际 Dtype
Params4bit 打包承载uint8、BF16 等
4-bit Quant TypeNF4
Base ComputeBF16
LoRA A/BFP32 或 BF16
Norm常见 FP32
LM Head / Embedding依配置与实现

6.4 梯度仅更新 LoRA 参数#

对损失 L\mathcal{L},优化器参数集合应为:

Θopt={A,B}Tϕ,\Theta_{\mathrm{opt}} = \left\{ A_\ell,B_\ell \right\}_{\ell\in\mathcal{T}} \cup \phi,

T\mathcal{T} 是注入 LoRA 的目标层集合,ϕ\phi 是显式放开的额外模块。

底座量化状态不属于优化器:

Q4,SΘopt.Q_4,\mathcal{S} \notin \Theta_{\mathrm{opt}}.

训练循环的逻辑是:

读取 4-bit 底座
→ 反量化并完成前向
→ 计算任务损失
→ 梯度穿过底座和 LoRA 分支
→ 为 LoRA / 额外模块累积梯度
→ 优化器仅更新这些可训练参数

应做三重检查:

trainable = [
(name, p.shape, p.dtype)
for name, p in model.named_parameters()
if p.requires_grad
]
optimizer_ids = {
id(p)
for group in optimizer.param_groups
for p in group["params"]
}
unexpected = [
name
for name, p in model.named_parameters()
if id(p) in optimizer_ids and not p.requires_grad
]

同时记录:

trainable ratio=PtrainablePreported total.\text{trainable ratio} = \frac{P_{\mathrm{trainable}}} {P_{\mathrm{reported\ total}}}.

对量化包装模型,某些库对总参数量的计数方式可能受打包表示影响,因此应同时报告可训练参数绝对值和实际匹配模块列表。

6.5 量化底座保持冻结的原因#

若直接更新 4-bit 码本索引,参数空间是离散的:

qi{0,1,,15}.q_i \in \{0,1,\ldots,15\}.

普通梯度下降产生连续更新:

qiqiηgi,q_i \leftarrow q_i-\eta g_i,

与离散索引约束不兼容。可以使用 Straight-Through Estimator、量化感知训练或维护高精度主权重等专门机制,但那已经是另一类训练方案。

QLoRA 选择冻结底座,带来三项直接收益:

  1. 不保存底座权重梯度;
  2. 不保存底座优化器状态;
  3. 不维护一份可更新的高精度 Master Weight。

如果底座可训练,为每个参数恢复梯度、Adam 状态和主权重,4-bit 常驻权重带来的大部分训练显存优势会被抵消。

冻结也简化了误差边界:训练始终围绕固定的量化近似底座:

W^0=W0Eq\widehat W_0 = W_0-E_q

学习低秩补偿:

Weff=W0Eq+sBA.W_{\mathrm{eff}} = W_0-E_q+sBA.

LoRA 可能同时学习任务增量和部分量化误差补偿,但标准随机/零初始化 LoRA 并不显式最小化 EqE_q。LoftQ 等量化感知初始化方法才专门把降低初始量化残差纳入设计;它们是 QLoRA 之上的扩展,不是原始 QLoRA 的必要组成。


7. Paged Optimizer 与显存峰值管理#

Paged Optimizer 与显存峰值

7.1 长序列训练中的瞬时显存峰值#

训练显存不是一条水平线。即使平均占用低于显卡容量,某一步也可能因为以下因素出现峰值:

  • 当前 Micro-batch 的序列特别长;
  • Padding 后的有效矩阵突然变大;
  • Gradient Checkpointing 在反向阶段重计算激活;
  • Attention 或 MLP Kernel 申请临时工作区;
  • 梯度同步或优化器更新与其他张量生命周期重叠;
  • 评估阶段开启生成和 KV Cache;
  • CUDA Allocator 无法及时复用碎片。

可以把瞬时占用写为:

M(t)=Mpersistent+Mactivation(t)+Mtemporary(t)+Moptimizer,resident(t).M(t) = M_{\mathrm{persistent}} + M_{\mathrm{activation}}(t) + M_{\mathrm{temporary}}(t) + M_{\mathrm{optimizer,resident}}(t).

只要某一时刻:

maxtM(t)>MVRAM,\max_t M(t) > M_{\mathrm{VRAM}},

就会 OOM。

Paged Optimizer 不直接压缩激活峰值。它做的是:当激活和临时张量需要更多 GPU 空间时,允许一部分优化器状态页迁往 CPU RAM,从而降低:

Moptimizer,resident(t).M_{\mathrm{optimizer,resident}}(t).

这是一种“为峰值让路”的机制。

7.2 Unified Memory 的分页机制#

CUDA Unified Memory 为 CPU 和 GPU 提供统一虚拟地址空间。一个 Managed Allocation 可以在程序中通过统一地址访问,但其物理页面仍可能位于:

  • GPU VRAM;
  • CPU System RAM。

驱动根据访问、Page Fault、Prefetch 和显存压力迁移页面。重要区别是:

统一地址空间不等于统一物理内存,也不等于 CPU RAM 与 GPU VRAM 具有相同带宽和延迟。

QLoRA 论文把它类比为传统操作系统的内存分页,但实际主要路径是:

GPU VRAMCPU RAM,\text{GPU VRAM} \longleftrightarrow \text{CPU RAM},

不是把优化器状态常规写入磁盘。

Unified Memory 可以支持显存 Oversubscription,但页面迁移仍受:

  • PCIe 或其他互连带宽;
  • Page Fault 开销;
  • CPU RAM 容量;
  • 驱动调度;
  • 访问局部性;
  • 多 GPU 拓扑。

7.3 优化器状态的迁移过程#

以 AdamW 为例,每个可训练参数通常有一阶矩和二阶矩:

mt=β1mt1+(1β1)gt,m_t = \beta_1m_{t-1} + (1-\beta_1)g_t,vt=β2vt1+(1β2)gt2.v_t = \beta_2v_{t-1} + (1-\beta_2)g_t^2.

Paged Optimizer 将这些状态分配在可分页 Managed Memory 中。概念流程是:

  1. 正常训练时,活跃页可以驻留 GPU;
  2. 显存压力增大时,一部分优化器页被驱逐到 CPU RAM;
  3. 前向和反向获得更多 GPU 空间;
  4. Optimizer Step 访问 mt,vtm_t,v_t 时,所需页面迁回或被 Prefetch;
  5. 更新完成后,页面继续由 Unified Memory 管理。

分页的对象主要是优化器状态,不是:

  • 每一步都把整个 4-bit 底座搬到 CPU;
  • 自动迁移所有激活;
  • 把 LoRA 从计算图中卸载;
  • 重新计算被 Checkpoint 的激活。

还要区分两个正交维度:

维度选项示例解决什么
状态精度AdamW 32-bit、AdamW 8-bit单个状态占多少 Bit
状态驻留普通、Paged压力下页面放在哪里

因此 paged_adamw_32bit 是“32-bit 状态 + 分页”,并不等于 8-bit Optimizer。

7.4 Paged Optimizer 的适用场景#

Paged Optimizer 更适合:

  • 模型静态状态已接近显存上限;
  • 大多数步骤能放下,但偶发长序列导致峰值;
  • Gradient Checkpointing 重计算阶段出现短时压力;
  • CPU RAM 足够容纳被迁出的状态;
  • 可以接受少量不稳定延迟以换取避免 OOM。

收益可能有限的场景:

  • LoRA Rank 很小,优化器状态本来就很少;
  • OOM 主要来自一个无法放下的超长 Micro-batch;
  • 每一步都长期超过显存容量;
  • CPU—GPU 互连很慢;
  • 页面持续来回迁移,形成 Thrashing;
  • 使用的设备后端不支持相应 Unified Memory/Paged Kernel。

如果单个 Micro-batch 的激活本身就超过 GPU 容量,分页少量 LoRA 优化器状态可能仍无法解决问题。此时更直接的手段是降低序列长度、Micro-batch Size,或使用更节省内存的 Attention 和模型分片。

7.5 分页带来的延迟与性能权衡#

页面迁移耗时可粗略写为:

TpageVmigrateBeffective+Tfault+Tsync,T_{\mathrm{page}} \approx \frac{V_{\mathrm{migrate}}} {B_{\mathrm{effective}}} + T_{\mathrm{fault}} + T_{\mathrm{sync}},

其中:

  • VmigrateV_{\mathrm{migrate}} 是每步迁移的数据量;
  • BeffectiveB_{\mathrm{effective}} 是有效互连带宽;
  • TfaultT_{\mathrm{fault}} 是 Page Fault 与驱动开销;
  • TsyncT_{\mathrm{sync}} 是同步等待。

bitsandbytes 官方说明曾用 PCIe 3.0 x16 做示例:若有效带宽只有理论 16 GB/s 的约一半,每步迁移 1 GB 的理想化开销约为:

116×0.5=0.125 s.\frac{1}{16\times0.5} = 0.125\ \text{s}.

这只是示例,不是所有机器的固定延迟。

QLoRA 论文在一个 65B、48GB GPU、Batch Size 16 的设置下报告 Paged Optimizer 与普通优化器速度相同,但也明确没有对各种分页情形做系统硬测量。更准确的结论是:

不触发或很少触发页面迁移时,分页可用较小性能代价吸收峰值;若迁移频繁,训练速度可能明显下降。

因此实验应同时记录:

  • 峰值 allocatedreserved
  • Step Time 的均值和 P95/P99;
  • 是否出现主机—设备迁移;
  • CPU RAM 峰值;
  • Paged 与 Non-paged 对照。

8. QLoRA 的显存占用分析#

QLoRA 显存组成与峰值

8.1 4-bit 底座权重占用#

设真正被 4-bit 量化的权重数为 PqP_q,则码本索引主体为:

Mcode=4Pq8=0.5Pq Byte.M_{\mathrm{code}} = \frac{4P_q}{8} = 0.5P_q \ \text{Byte}.

启用论文配置的 Double Quantization 后,理论 Scale 元数据为:

0.126953125 bit/parameter.0.126953125 \ \text{bit/parameter}.

于是量化权重与 Scale 近似合计:

Mbase,DQPq(4.126953125)8 Byte.M_{\mathrm{base,DQ}} \approx \frac{ P_q(4.126953125) }{8} \ \text{Byte}.

若为了建立数量级直觉,暂时假设名义参数都被量化:

名义规模BF16 权重主体NF4 + DQ 理论主体DQ 理论节省
7B14.00 GB3.61 GB相对无 DQ 约 0.33 GB
13B26.00 GB6.71 GB相对无 DQ 约 0.61 GB
65B130.00 GB33.53 GB相对无 DQ 约 3.03 GB

表中使用十进制 GB。65B 的 33.53 GB 约等于 31.23 GiB。

实际应使用 PqP_q,而不是总参数 PP。典型实现主要量化线性层,Norm、Bias、Embedding、LM Head 或模型特有参数可能保持其他 Dtype。

8.2 量化元数据与反量化缓冲区#

更完整的底座内存是:

Mquantized base=Mcode+Mscale1+Mscale2+Moffset+Mcodebook+Mpadding+Mobjects.M_{\mathrm{quantized\ base}} = M_{\mathrm{code}} + M_{\mathrm{scale1}} + M_{\mathrm{scale2}} + M_{\mathrm{offset}} + M_{\mathrm{codebook}} + M_{\mathrm{padding}} + M_{\mathrm{objects}}.

其中码本本身很小,但大量 Tensor 的 QuantState、Shape、对象头、对齐和尾块会累积。

运行时还需要:

Mdequant temp(t)+MGEMM workspace(t).M_{\mathrm{dequant\ temp}}(t) + M_{\mathrm{GEMM\ workspace}}(t).

高效 Kernel 通常按 Block 或 Tile 恢复权重,不让完整 BF16 底座长期驻留;但“没有完整常驻副本”不等于“反量化不占任何临时显存”。

评估模型静态占用时可查看:

model.get_memory_footprint()

但它主要统计模型参数与 Buffer,不能代替完整训练峰值测量。

8.3 LoRA 权重、梯度与优化器状态#

对目标矩阵:

WRdout×din,W \in \mathbb{R}^{d_{\mathrm{out}}\times d_{\mathrm{in}}},

LoRA 参数量为:

PLoRA=r(din+dout).P_{\mathrm{LoRA}} = r(d_{\mathrm{in}}+d_{\mathrm{out}}).

所有目标层相加:

PL=Tr(din,+dout,).P_L = \sum_{\ell\in\mathcal{T}} r_\ell \left( d_{\mathrm{in},\ell} + d_{\mathrm{out},\ell} \right).

LoRA 训练状态可抽象为:

MLPL(bparam+bgrad+bopt+bmaster),M_L \approx P_L \left( b_{\mathrm{param}} + b_{\mathrm{grad}} + b_{\mathrm{opt}} + b_{\mathrm{master}} \right),

其中各 bb 以 Byte/parameter 计。

例如:

  • FP32 Adapter:参数 4 Byte;
  • FP32 梯度:4 Byte;
  • FP32 Adam m,vm,v:合计 8 Byte;
  • 若有独立 Master Weight:再增加相应占用。

总计不能机械固定为 16 Byte,因为框架可能:

  • Adapter 用 BF16;
  • 梯度用 BF16/FP32;
  • 优化器状态用 8-bit;
  • 使用 Master Weight;
  • 对状态分片或分页;
  • 延迟创建 Optimizer State。

原论文的 Rank-64、All-linear 配置会比常见 Rank-8/16、Attention-only 配置产生更多 LoRA 状态。QLoRA 节省底座权重显存,不代表 Adapter 配置可以不做成本分析。

8.4 激活值、序列长度与 Batch Size#

激活显存受以下变量共同影响:

Mact=f(Bμ,S,H,L,Attention Kernel,Checkpointing,Packing),M_{\mathrm{act}} = f \left( B_{\mu}, S, H, L, \text{Attention Kernel}, \text{Checkpointing}, \text{Packing} \right),

其中:

  • BμB_{\mu}:每设备 Micro-batch Size;
  • SS:序列长度;
  • HH:Hidden Size;
  • LL:层数。

许多隐藏状态项近似随:

BμSHLB_{\mu}SHL

增长。朴素 Attention 还可能显式产生与:

BμnheadS2B_{\mu}n_{\mathrm{head}}S^2

相关的中间矩阵。FlashAttention/SDPA 可显著改变这部分工作区和 IO,但具体峰值依赖实现。

Gradient Accumulation 改变有效 Batch:

Beff=Bμ×NGPU×Gacc.B_{\mathrm{eff}} = B_{\mu} \times N_{\mathrm{GPU}} \times G_{\mathrm{acc}}.

它只有在降低 BμB_{\mu} 时才降低单步激活峰值。保持相同 Micro-batch、只提高 Accumulation Steps,不会让当前 Micro-batch 更省显存。

Gradient Checkpointing 则以额外重计算换取少保存激活:

更少 Saved Activations更多 Backward Recompute.\text{更少 Saved Activations} \Longleftrightarrow \text{更多 Backward Recompute}.

8.5 理论估算与实际显存差异#

完整峰值可写为:

MpeakM4bit base+Mquant metadata+Mnonquant+MLoRA params+MLoRA grads+Moptimizer,resident+Mactivations+Mtemporary+Mruntime.\begin{aligned} M_{\mathrm{peak}} \approx{}& M_{\mathrm{4bit\ base}} + M_{\mathrm{quant\ metadata}} + M_{\mathrm{nonquant}}\\ &+ M_{\mathrm{LoRA\ params}} + M_{\mathrm{LoRA\ grads}} + M_{\mathrm{optimizer,resident}}\\ &+ M_{\mathrm{activations}} + M_{\mathrm{temporary}} + M_{\mathrm{runtime}}. \end{aligned}

理论公式与 nvidia-smi 可能不一致,因为:

  • CUDA Context 和库加载占用;
  • Caching Allocator 预留但未使用的块;
  • Tensor Core/GEMM Workspace;
  • 通信 Bucket;
  • 延迟初始化的优化器状态;
  • Eval/Generate KV Cache;
  • 内存碎片;
  • 多进程各自持有模型副本。

建议用真实训练步测量:

torch.cuda.reset_peak_memory_stats()
# 至少完成 warmup、forward、backward 和 optimizer.step()
allocated_gib = torch.cuda.max_memory_allocated() / 2**30
reserved_gib = torch.cuda.max_memory_reserved() / 2**30
print({"allocated_GiB": allocated_gib, "reserved_GiB": reserved_gib})
print(torch.cuda.memory_summary())

至少报告:

  • 模型名与精确 Revision;
  • 每设备 Micro-batch;
  • 最大和实际 P50/P95 序列长度;
  • Gradient Accumulation;
  • Quant/Compute/Adapter Dtype;
  • Rank 和 Target Modules;
  • Checkpointing、Attention Kernel;
  • Optimizer;
  • 峰值 Allocated、Reserved 和吞吐。

9. QLoRA 的工程实现#

9.1 4-bit 模型加载配置#

下面是一份教学用单卡骨架。API 名称会随版本演进,运行前应固定并记录 torchtransformerspeftacceleratebitsandbytes 版本。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
MODEL_ID = "your-base-model"
use_bf16 = (
torch.cuda.is_available()
and torch.cuda.is_bf16_supported()
)
compute_dtype = torch.bfloat16 if use_bf16 else torch.float16
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=compute_dtype,
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=quant_config,
dtype=compute_dtype, # Transformers 5.x;许多 4.x 版本使用 torch_dtype
)
model.config.use_cache = False

几个边界:

  • load_in_4bit=True 不保证使用 NF4、DQ 和 BF16 Compute;
  • 当前 Transformers 5.x 使用 dtype=,旧版示例常见 torch_dtype=,不要盲目同时传入;
  • 普通训练不要把 device_map="auto" 当作 DDP;
  • 模型是否能被 bitsandbytes 替换,取决于架构中的模块类型与后端支持;
  • 不要对量化模型整体调用 model.half() 或随意转换 Params4bit

9.2 NF4、Double Quantization 与 Compute Dtype#

四个容易混淆的配置是:

参数含义论文式常见设置
bnb_4bit_quant_type4-bit 码本"nf4"
bnb_4bit_use_double_quant是否量化 ScaleTrue
bnb_4bit_compute_dtype反量化后计算类型torch.bfloat16
bnb_4bit_quant_storage打包 4-bit 数据的承载类型单卡常用默认;FSDP 需专门协调

当前部分版本的默认配置可能是 FP4、不开 DQ、FP32 Compute。于是:

BitsAndBytesConfig(load_in_4bit=True)

不能直接等同于论文使用的 NF4 QLoRA。

另外,Transformers 的 BitsAndBytesConfig 当前没有通用的:

bnb_4bit_block_size

参数。Block Size 属于 bitsandbytes 底层量化实现和后端细节;文章可以分析其理论作用,但不要提供不存在的高层配置项。

FSDP-QLoRA 需要额外关注:

bnb_4bit_quant_storage=torch.bfloat16

并让模型顶层 Dtype 与 Quant Storage Dtype 对齐,使 FSDP 能一致包装和分片模块。这个承载类型仍不改变每个逻辑权重只有 4-bit 编码的信息量。

9.3 K-bit Training 前的模型准备#

PEFT 提供:

from peft import prepare_model_for_kbit_training
model = prepare_model_for_kbit_training(
model,
use_gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False},
)

当前实现主要会:

  1. 冻结调用时已经存在的底座参数;
  2. 将若干非量化 FP16/BF16 参数提升到 FP32;
  3. 必要时为输入激活建立梯度路径;
  4. 启用 Gradient Checkpointing;
  5. 某些版本可清理设备 Cache。

它不会:

  • 把模型量化成 4-bit;
  • 自动注入 LoRA;
  • 自动选择任务损失;
  • 自动决定 Target Modules;
  • 自动构造 Dataset。

正确顺序是:

加载量化底座
→ prepare_model_for_kbit_training
→ 注入 LoRA
→ 检查可训练参数
→ 创建 Optimizer / Trainer

若先 get_peft_model 再调用准备函数,准备函数可能把已经存在的 Adapter 一并冻结。顺序错误可能让训练正常运行、Loss 也被计算,却没有有效可训练参数。

9.4 LoRA Target Modules 与超参数设置#

论文式 QLoRA 把 Adapter 加到所有 Transformer 线性层。当前 PEFT 可写为:

from peft import LoraConfig, TaskType, get_peft_model
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules="all-linear",
bias="none",
)
model = get_peft_model(model, lora_config)

这段代码中的 r=16,α=32r=16,\alpha=32 是起点示例,不是原论文复现值。原论文主要实验使用:

r=64,α=16,r=64, \qquad \alpha=16,

并进行 All-linear 注入。

较窄配置如:

target_modules=["q_proj", "k_proj", "v_proj", "o_proj"]

只覆盖 Attention 投影,参数更少,但不是同样容量的 All-linear QLoRA。

需要检查的架构差异包括:

  • LLaMA 类模型的 q_projgate_proj
  • GPT 类模型可能使用合并 QKV;
  • T5 的模块命名不同;
  • MoE Expert 权重可能直接是 nn.Parameter 而不是 nn.Linear
  • all-linear 通常不会让 Embedding 和输出 Head 自动可训练。

因此 Target Modules 消融应同时报告实际匹配数量,而不是只保存字符串配置。

9.5 可训练参数、数据类型与设备映射检查#

训练前至少执行:

import bitsandbytes as bnb
model.print_trainable_parameters()
trainable = []
for name, param in model.named_parameters():
if param.requires_grad:
assert param.is_floating_point()
trainable.append(
(name, tuple(param.shape), str(param.dtype), str(param.device))
)
linear4_count = sum(
isinstance(module, bnb.nn.Linear4bit)
for module in model.modules()
)
adapted = [
name
for name, module in model.named_modules()
if hasattr(module, "lora_A")
]
print("Linear4bit modules:", linear4_count)
print("Adapted modules:", len(adapted))
print("Trainable sample:", trainable[:20])
print("Device map:", getattr(model, "hf_device_map", None))

还应验证:

assert getattr(model, "is_loaded_in_4bit", False)
assert len(trainable) > 0
assert linear4_count > 0
assert len(adapted) > 0

分布式训练时:

  • DDP 通常每个进程持有一份量化底座;
  • device_map="auto" 是大模型推理分发/卸载机制,不是普通 DDP 训练策略;
  • 需要模型跨卡分片时,使用经过验证的 FSDP-QLoRA 或 DeepSpeed ZeRO-3 配置;
  • FSDP 的 Quant Storage Dtype、模型 Dtype 和 Wrap Policy 必须一致;
  • 不要把“多卡”笼统写成“自动切层就能训练”。

10. 训练稳定性与硬件配置#

10.1 BF16 与 FP16 的选择#

BF16 和 FP16 都占 16 bit,但指数—尾数分配不同:

类型指数 Bit尾数 Bit最大有限值数量级训练特点
FP1651010410^4尾数更细,动态范围较窄
BF1687103810^{38}动态范围接近 FP32

大模型训练中,BF16 通常更不容易因大值溢出,因此在硬件高效支持时常作为 QLoRA Compute Dtype 首选:

use_bf16 = (
torch.cuda.is_available()
and torch.cuda.is_bf16_supported()
)
compute_dtype = (
torch.bfloat16
if use_bf16
else torch.float16
)

Trainer 配置应与之对齐:

training_args = TrainingArguments(
...,
bf16=(compute_dtype == torch.bfloat16),
fp16=(compute_dtype == torch.float16),
)

几个边界:

  • BF16 动态范围更大,不代表每个运算误差都更小;
  • FP16 可能依赖 Loss Scaling;
  • 某些老设备、后端或 Kernel 不支持高效 BF16;
  • Compute Dtype、Adapter Parameter Dtype 和 Accumulation Dtype 可能不同;
  • 不应仅根据 GPU 名称猜测,需实际检查 Kernel 和框架支持。

若 FP16 出现 NaN,可用较小模型或短序列临时切换 FP32/BF16 做定位,但不要误以为把量化底座“改成 FP32 Compute”会消除所有数据、损失或优化问题。

10.2 学习率、Rank 与梯度裁剪#

LoRA 的有效增量为:

ΔWeff=αrBA.\Delta W_{\mathrm{eff}} = \frac{\alpha}{r}BA.

改变 Rank 时若保持 α\alpha 不变,缩放系数也会变化:

s=αr.s = \frac{\alpha}{r}.

因此 Rank 消融必须说明 Alpha 如何处理。否则“更大 Rank 的效果变化”同时包含:

  • 参数容量变化;
  • 初始化统计变化;
  • 缩放变化;
  • 梯度尺度变化。

学习率也应与模型规模、目标层、数据量和有效 Batch 联合调节。原始 QLoRA 仓库常见起点为:

7B/13B:2e-4
33B/65B:1e-4

这是论文特定 LLaMA、数据和 Rank-64 配置的搜索结果,不是通用推荐。更可靠的实验是:

LR×Rank×Target Modules×Compute Dtype.\text{LR} \times \text{Rank} \times \text{Target Modules} \times \text{Compute Dtype}.

梯度裁剪:

ggmin(1,τg2+ϵ)g \leftarrow g\cdot \min \left( 1, \frac{\tau}{\lVert g\rVert_2+\epsilon} \right)

可限制突发梯度。原始仓库使用 max_grad_norm=0.3;常见框架默认或其他任务也会使用 1.0。阈值应作为实验参数,不应把论文值写成 QLoRA 定义。

还应记录:

  • Warmup Ratio 或 Warmup Steps;
  • Scheduler;
  • Weight Decay;
  • LoRA Dropout;
  • 有效 Batch;
  • 总优化步与训练 Token。

10.3 Gradient Checkpointing 与 Flash Attention#

Gradient Checkpointing 与 FlashAttention 都不是 QLoRA 的定义组件,而是可以组合的正交优化。

Gradient Checkpointing

  • 前向时只保存部分边界激活;
  • 反向时重算中间激活;
  • 通常减少激活显存;
  • 通常增加计算量和 Step Time。

FlashAttention / SDPA

  • 改变 Attention 的 IO 与分块方式;
  • 避免显式长期物化完整 S×SS\times S Attention Matrix;
  • 可以降低工作区并提高吞吐;
  • 支持情况依赖模型、Dtype、GPU、PyTorch、CUDA 和 Kernel 版本。

稳妥的通用起点是:

model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=quant_config,
dtype=compute_dtype,
attn_implementation="sdpa",
)

在完整兼容性验证后再切换:

attn_implementation="flash_attention_2"

组合实验至少比较:

CheckpointingAttention关注指标
Eager/SDPA基线显存与吞吐
Eager/SDPA重计算收益
FlashAttentionAttention Kernel 收益
FlashAttention联合峰值和速度

不能只报告“能跑起来”,还应报告 Tokens/s、Step Time 和峰值显存。

10.4 单卡、多卡与设备切分策略#

单卡

  • 配置最简单;
  • 量化底座、Adapter 和激活都在一张卡;
  • Paged Optimizer 可吸收部分瞬时压力;
  • 仍需控制 Micro-batch 和序列长度。

DDP

每个进程通常持有一份完整量化底座:

Mbase,per GPUMquantized base.M_{\mathrm{base,per\ GPU}} \approx M_{\mathrm{quantized\ base}}.

DDP 提升数据并行吞吐,但不会把底座平均切到多张卡。若一张卡放不下模型,增加 DDP 卡数并不能自动解决。

FSDP-QLoRA / ZeRO-3

可以跨 GPU 分片参数、梯度和状态,但 4-bit 包装带来额外要求。bitsandbytes 的 FSDP-QLoRA 文档要求通过浮点 bnb_4bit_quant_storage 让 FSDP 能分片打包权重,并让它与模型顶层 Dtype 对齐:

bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_quant_storage=torch.bfloat16,
)

普通多卡训练应使用:

Terminal window
accelerate launch train.py

torchrun,而不是用 device_map="auto" 把层随意分散后交给普通 Trainer。

分布式组合高度版本敏感,必须保存:

  • Accelerate 配置;
  • FSDP/ZeRO Stage;
  • Wrap Policy;
  • use_orig_params 等关键开关;
  • Quant Storage Dtype;
  • 每进程 Device;
  • 软件版本与启动命令。

10.5 OOM、NaN 与吞吐下降的排查#

OOM 排查顺序

  1. 降低每设备 Micro-batch;
  2. 降低最大序列长度;
  3. 检查真实长度分布、Padding 和 Packing;
  4. 启用 Gradient Checkpointing;
  5. 使用兼容的 SDPA/FlashAttention;
  6. 关闭训练与评估中不需要的 use_cache
  7. 检查是否误用 device_map="auto" 或复制模型;
  8. 再考虑 Paged Optimizer、FSDP 或 ZeRO-3。

Gradient Accumulation 只有配合更小 Micro-batch 才降低单步峰值。

NaN 排查

for name, param in model.named_parameters():
if param.requires_grad and param.grad is not None:
if not torch.isfinite(param.grad).all():
print("non-finite grad:", name)

逐项检查:

  • 第一批输入、Labels 和 Loss Mask;
  • Loss 是否从第一步就非有限;
  • BF16/FP16 与 AMP 是否一致;
  • Adapter 是否为可训练浮点参数;
  • 学习率和 Warmup;
  • 梯度裁剪;
  • 某个目标层是否异常;
  • 模板和 Tokenizer 是否产生空监督。

logging_nan_inf_filter=True 只影响日志显示,不会修复坏梯度。

吞吐下降排查

  • Compute Dtype 是否意外为 FP32;
  • Checkpointing 是否造成预期重算;
  • Unified Memory 是否频繁迁页;
  • 是否发生 CPU/Disk Offload;
  • 序列长度和 Padding 是否上升;
  • FlashAttention 是否回退;
  • 数据加载是否成为瓶颈;
  • 多卡通信是否掩盖计算。

正确预期是:

QLoRA 的核心优势是降低显存门槛,不保证比 BF16 LoRA 或全参数训练拥有更高吞吐。


11. 实验与消融评估#

11.1 QLoRA、LoRA 与全参数微调对照#

三类方法对照必须统一任务目标、数据与训练预算:

方案底座表示更新参数主要差异
Full FTBF16/FP16全参数最大更新容量与训练状态
LoRABF16/FP16 冻结底座Adapter去掉底座梯度与优化器状态
QLoRANF4/FP4 冻结底座Adapter在 LoRA 上进一步压缩底座

公平比较至少固定:

  • 完全相同的初始模型 Revision;
  • Tokenizer 和 Chat Template;
  • 训练/验证切分;
  • 目标 Token Mask;
  • 总训练 Token 或优化步;
  • 有效 Batch;
  • 评估解码参数;
  • LoRA 与 QLoRA 的 Rank、Alpha、Target Modules;
  • 随机种子数量。

同时报告:

Effect×Memory×Throughput×Time.\text{Effect} \times \text{Memory} \times \text{Throughput} \times \text{Time}.

若固定 Epoch 而数据管线、Packing 或有效监督 Token 不同,训练预算并不相同。应报告:

  • 唯一样本数;
  • 输入 Token;
  • 有效监督 Token;
  • Optimizer Steps;
  • 实际计算 Token;
  • Wall-clock Time。

QLoRA 论文在若干模型和任务上观察到 NF4 QLoRA 匹配 BF16 LoRA/全参数基线;它没有建立“任何任务、模型和预算下都等价”的普遍定理。

11.2 NF4、FP4 与不同量化配置对照#

推荐最小矩阵:

Quant TypeDQCompute目的
NF4BF16论文式主配置
NF4BF16隔离 DQ
FP4BF16比较码本
FP4BF16纯 FP4 基线
NF4FP16比较 Compute Dtype
BF16 Base不适用BF16非量化 LoRA 基线

应在训练前先评估“只量化、不训练”的模型,以区分:

初始量化损失\text{初始量化损失}

与:

训练后的 Adapter 补偿能力.\text{训练后的 Adapter 补偿能力}.

QLoRA 原论文报告的代表性结果包括:

数据类型Pile Common Crawl Mean PPL,越低越好
INT434.34
FP4 E2M131.07
FP4 E3M029.48
NF4 + DQ27.41

以及其 LLaMA 7B–65B、Alpaca/FLAN v2 汇总:

配置Mean 5-shot MMLU
BF1653.0
FP452.2
NF4 + DQ53.1

这些数值用于理解论文证据,不应当成新模型上的预期分数。

11.3 Double Quantization 的收益消融#

DQ 消融至少测四项:

  1. 模型静态占用;
  2. 完整训练峰值;
  3. 吞吐;
  4. 任务效果。

理论节省:

ΔMDQPq×0.3730468758 Byte\Delta M_{\mathrm{DQ}} \approx \frac{ P_q\times0.373046875 }{8} \ \text{Byte}

只预测 Scale 元数据差异。真实测量还受:

  • 尾块与对齐;
  • 未量化模块;
  • Allocator 粒度;
  • Kernel Workspace;
  • 量化状态对象;
  • 模型是否真的把所有候选 Linear 替换为 4-bit。

误差消融可同时计算:

WW^FWF\frac{ \lVert W-\widehat W\rVert_F }{ \lVert W\rVert_F }

和任务指标。若权重误差略有变化而任务指标无显著差异,应报告置信区间或多随机种子结果,避免把噪声写成增益。

还可按层分析:

  • Attention Q/K/V/O;
  • Gate/Up/Down;
  • 早层与晚层;
  • 大 Scale 和重尾 Block。

这有助于判断 DQ 误差是否集中在特定模块。

11.4 Rank、目标层与计算精度消融#

一个可解释的因子实验可写为:

r{8,16,32,64},r \in \{8,16,32,64\},T{Q,V,Attention,All-linear},\mathcal{T} \in \{ \text{Q,V}, \text{Attention}, \text{All-linear} \},c{FP16,BF16}.c \in \{ \mathrm{FP16}, \mathrm{BF16} \}.

若全因子成本太高,至少采用分阶段设计:

  1. 固定 All-linear 与 BF16,筛 Rank;
  2. 固定候选 Rank,比较 Target Modules;
  3. 固定结构,比较 Compute Dtype;
  4. 对最优附近做多种子复验。

必须控制 Scaling:

s=αr.s = \frac{\alpha}{r}.

可以:

  • 固定 α/r\alpha/r
  • 固定 α\alpha
  • 使用 rsLoRA 的 α/r\alpha/\sqrt r

但三者回答不同问题,不能混在同一 Rank 曲线里。

Target Modules 改变的不只是参数量,也改变可表达更新的位置。All-linear 相比 Attention-only 更容易补偿 MLP 和量化误差,但显存、优化器状态和计算开销更高。

11.5 效果、显存、吞吐与训练时间联合评估#

一个完整结果表应包含:

类别指标示例
任务效果Accuracy、F1、Pass@k、Reward、Win Rate
语言建模Validation NLL、Perplexity
行为质量指令遵循、格式遵循、拒答、安全
能力保持通用基准、域外基准、遗忘
显存静态 Footprint、Peak Allocated、Peak Reserved
吞吐Tokens/s、Samples/s、Step Time
时间总训练时长、达到目标指标的时间
稳定性NaN 次数、OOM 次数、失败 Run、梯度范数

还应区分:

训练吞吐推理吞吐.\text{训练吞吐} \ne \text{推理吞吐}.

训练时 Adapter、梯度、Checkpointing 和 Optimizer 参与;推理时没有反向和优化器,但有 KV Cache、Batching 和解码 Kernel。

建议绘制 Pareto Frontier:

maximize Effectwhile minimizing{Peak VRAM,Wall Time}.\text{maximize Effect} \quad \text{while minimizing} \quad \{ \text{Peak VRAM}, \text{Wall Time} \}.

QLoRA 的价值通常不是在单一指标上必胜,而是在较低显存预算下进入原本不可运行的模型规模。


12. 部署边界、常见误区与局限#

12.1 4-bit 存储不代表 4-bit 矩阵计算#

Canonical QLoRA 的语义是:

WNF4按需反量化W^BF16/FP16MatMuly.W_{\mathrm{NF4}} \xrightarrow{\text{按需反量化}} \widehat W_{\mathrm{BF16/FP16}} \xrightarrow{\text{MatMul}} y.

现代 Kernel 可能把解包、反量化和 GEMM 融合在一起,不显式物化完整 W^\widehat W,但这仍不等于整个运算成为普通的:

INT4×INT4\mathrm{INT4}\times\mathrm{INT4}

矩阵乘法。

应该分别报告:

  • 逻辑 Quant Type;
  • 打包 Storage Dtype;
  • Compute Dtype;
  • Accumulation Dtype;
  • 输入激活 Dtype;
  • 输出 Dtype。

“4-bit 模型”只是一种不完整简称。对训练、推理和性能讨论,它至少还需要说明后端与计算精度。

12.2 QLoRA 训练量化与推理量化的区别#

QLoRA 的目标是让冻结量化底座上的 Adapter 训练可行。部署则关心:

  • 推理引擎支持什么权重格式;
  • 是否支持运行时 LoRA;
  • 是否支持 NF4;
  • 是否支持目标硬件上的高效 Kernel;
  • KV Cache 使用什么精度;
  • Batch 和并发下的吞吐。

训练产物通常是:

base_model_name_or_path
adapter_config.json
adapter_model.safetensors

它并不自动成为:

  • GGUF;
  • GPTQ;
  • AWQ;
  • 某个硬件厂商的 INT4;
  • 任意推理框架可直接运行的单文件模型。

可以选择两种部署形态:

  1. 量化底座 + 独立 Adapter 内存低,可切换任务,但需要运行时同时支持量化后端和 LoRA。

  2. 高精度合并模型 + 目标格式重新量化 部署形态统一,但需要额外合并内存和重新量化评估。

训练时 NF4 表现好,不代表目标推理引擎的 NF4 Kernel 一定最快。

12.3 Adapter 合并后的权重精度选择#

标准 LoRA 合并为:

Wmerged=Wbase+sBA.W_{\mathrm{merged}} = W_{\mathrm{base}} + sBA.

若直接向 4-bit 底座合并,概念上会发生:

Q4DequantW^+sBAWRequantQ4.Q_4 \xrightarrow{\text{Dequant}} \widehat W \xrightarrow{+sBA} W' \xrightarrow{\text{Requant}} Q'_4.

最后一步会再次舍入。因此当前 PEFT 对 4-bit Linear 合并会警告结果可能因 Round-off 改变。

若重视合并精度,推荐:

from peft import PeftModel
from transformers import AutoModelForCausalLM
dense_base = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
dtype=torch.bfloat16,
)
peft_model = PeftModel.from_pretrained(
dense_base,
ADAPTER_DIR,
)
merged_model = peft_model.merge_and_unload(
safe_merge=True,
)
merged_model.save_pretrained(OUTPUT_DIR)

注意接收返回值。随后根据目标推理引擎重新量化:

WmergedQdeployQdeploy.W_{\mathrm{merged}} \xrightarrow{\mathcal{Q}_{\mathrm{deploy}}} Q_{\mathrm{deploy}}.

并重新评估困惑度、任务指标和生成样例。直接保留未合并 Adapter 往往是低显存场景下更安全的首选。

12.4 量化误差与任务敏感性#

QLoRA 的有效权重是:

Weff=W0Eq+sBA.W_{\mathrm{eff}} = W_0-E_q+sBA.

若 LoRA 完全补偿量化误差与任务增量,需要:

sBAΔWtask+Eq.sBA \approx \Delta W_{\mathrm{task}}+E_q.

BABA 受 Rank 和 Target Modules 约束:

rank(BA)r.\operatorname{rank}(BA)\le r.

当以下情况出现时,量化误差更可能影响最终效果:

  • 小模型冗余较少;
  • 任务依赖细微 Logit 排序;
  • 数学、代码或结构化输出对单 Token 错误敏感;
  • 某些敏感层未注入 LoRA;
  • 权重分布与 NF4 假设明显不匹配;
  • Rank 太小;
  • 合并后再次量化;
  • 域外输入激活放大量化误差。

评估不应只看平均分。还应做:

  • 按任务类别切片;
  • 长上下文与短上下文切片;
  • 置信度与校准;
  • 格式严格任务;
  • 错误类型;
  • 最坏案例和回归样例。

NF4 的经验优势不消除任务敏感性。

12.5 硬件内核支持和推理兼容性限制#

QLoRA 的可用性取决于完整软件—硬件栈:

模型架构
× Transformers
× PEFT
× bitsandbytes
× PyTorch
× 驱动 / CUDA / ROCm / XPU
× GPU 架构
× Attention Kernel
× 分布式框架
× 推理引擎

当前 bitsandbytes 已扩展到多种后端,但不同功能、Dtype 和设备的成熟度不完全相同。部署前应检查官方兼容矩阵,而不是根据“支持 4-bit”推断:

  • NF4 训练一定支持;
  • Paged Optimizer 一定支持;
  • BF16 Kernel 一定高效;
  • FSDP-QLoRA 一定可用;
  • merge_and_unload 一定无舍入;
  • 推理框架一定能加载相同 QuantState。

常见兼容性问题包括:

  • 模型使用自定义 Linear 或参数布局,未被自动量化;
  • MoE Expert 不是标准 nn.Linear
  • 某些 Head/Embedding 保持高精度;
  • FlashAttention 与模型 Mask 不兼容;
  • 量化 Storage Dtype 与 FSDP Dtype 不一致;
  • Adapter 的 Base Revision 或 Tokenizer 不匹配;
  • 推理引擎只支持特定量化格式;
  • CPU Offload 或 Unified Memory 带来不可接受的延迟。

一次可靠部署至少保存:

  • 精确底座 ID 与 Revision;
  • Tokenizer 和 Chat Template;
  • Adapter 配置与权重;
  • Quantization Config;
  • 软件版本;
  • 合并/量化脚本;
  • 目标硬件;
  • 回归测试集与基准结果。

结语#

QLoRA 的核心不是“把训练改成 4-bit”,而是对不同状态进行精确分工:

  • 冻结底座以 NF4 等 4-bit 形式存储;
  • Scale 可以再用 Double Quantization 压缩;
  • 使用权重时按需恢复到 BF16/FP16 等计算类型;
  • 底座仍参与输入梯度传播,但不保存权重梯度;
  • LoRA 保持可训练浮点表示;
  • Paged Optimizer 在显存压力下迁移优化器状态页;
  • 激活、临时工作区和硬件 Kernel 仍决定真实峰值与吞吐。

一句准确的 QLoRA 定义是:

QLoRA 在冻结的 4-bit 量化预训练模型上,通过较高精度计算把梯度传播到可训练 LoRA 适配器,从而主要压缩底座权重驻留与全参数训练状态,而不改变上层任务损失。

使用 QLoRA 前,应回答六个问题:

  1. 底座哪些模块真正被量化;
  2. Quant Type、DQ、Compute Dtype 和 Adapter Dtype 分别是什么;
  3. LoRA 注入哪些层,Rank 与 Scaling 如何选择;
  4. OOM 来自底座、Adapter 状态还是激活峰值;
  5. 训练效果是否相对 BF16 LoRA 和全参数基线成立;
  6. 最终部署是保留 Adapter、合并,还是合并后重新量化。

当这六个问题都被记录时,QLoRA 才从“显存不够时勾选的 4-bit 开关”变成一套可解释、可测量、可复现的训练方案。


参考文献与官方资料#

  1. Dettmers et al. QLoRA: Efficient Finetuning of Quantized LLMs, NeurIPS 2023.
  2. Dettmers et al. QLoRA 正式论文 PDF.
  3. Dettmers et al. QLoRA 官方实现.
  4. Hu et al. LoRA: Low-Rank Adaptation of Large Language Models, ICLR 2022.
  5. bitsandbytes. NF4、FP4、Block-wise Quantization 与 Double Quantization 源码.
  6. bitsandbytes. Linear4bit 官方 API.
  7. bitsandbytes. Quantization Primitives 官方文档.
  8. bitsandbytes. Paged Optimizer 机制说明.
  9. bitsandbytes. AdamW 与 Paged AdamW API.
  10. bitsandbytes. Transformers、PEFT 与 Paged Optimizer 集成.
  11. bitsandbytes. FSDP-QLoRA 官方指南.
  12. bitsandbytes. 安装与硬件兼容性.
  13. Hugging Face Transformers. bitsandbytes 4-bit Quantization 指南.
  14. Hugging Face Transformers. Quantization Overview.
  15. Hugging Face PEFT. Quantization 与 K-bit Training 指南.
  16. Hugging Face PEFT. prepare_model_for_kbit_training 源码.
  17. Hugging Face PEFT. LoRA API 与 all-linear.
  18. Hugging Face PEFT. Adapter Dtype 与常见故障.
  19. Hugging Face PEFT. PEFT Checkpoint Format.
  20. Hugging Face PEFT. LoRA 合并与 merge_and_unload.
  21. Hugging Face PEFT. FSDP 分布式训练.
  22. Hugging Face PEFT. DeepSpeed 与 QLoRA.
  23. NVIDIA. CUDA Unified Memory Programming Guide.
  24. NVIDIA. CUDA Runtime API:Memory Management.
  25. PyTorch. torch.cuda.is_bf16_supported.
  26. PyTorch. Automatic Mixed Precision.
  27. PyTorch. Activation Checkpointing.
  28. Dao et al. FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness, NeurIPS 2022.
  29. Dao. FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning, 2023.
  30. FlashAttention Authors. FlashAttention 官方实现与硬件要求.
  31. Yoshida. NF4 Isn’t Information Theoretically Optimal, 2023.
  32. Li et al. LoftQ: LoRA-Fine-Tuning-Aware Quantization for Large Language Models, 2023.
  33. Hugging Face Accelerate. Big Model Inference 与 Device Map.
  34. vLLM. BitsAndBytes Quantization.
  35. vLLM. LoRA Adapter Runtime.
第 04 篇:QLoRA 原理
https://jupiter-ws.cn/posts/agent-algorithms/04-qlora-principles/
作者
Jupiter
发布于
2026-07-17
许可协议
CC BY-NC-SA 4.0