直觉:为什么不能总是全参微调

微调(fine-tuning)就是在预训练模型基础上,用领域数据继续训练,让模型适配特定任务或风格。最直接的做法是全参微调(full fine-tuning):更新模型的每一个参数。问题在于成本。

一个 7B 模型,参数本身用 fp16 存就要约 14 GB。但训练时显存远不止于此——优化器(以最常见的 Adam 为例)要为每个参数额外保存一阶动量和二阶动量,再加上梯度本身。粗略地算这笔账:

1
2
3
4
5
参数(fp16)        2 bytes/参数
梯度(fp16) 2 bytes/参数
Adam 一阶动量 4 bytes/参数 (fp32)
Adam 二阶动量 4 bytes/参数 (fp32)
(混合精度下常另存一份 fp32 参数副本 4 bytes)

也就是说,每个参数训练时要占十几个 byte。7B 模型光这些状态就奔着上百 GB 去了,还没算激活值。这就是为什么全参微调动辄需要多卡 A100/H100。我们需要更省的办法。

机制:LoRA 为什么有效

LoRA(Low-Rank Adaptation)的出发点是一个经验观察:微调引起的权重变化 ΔW\Delta W 是"低秩"的——即模型适配新任务时,真正需要改变的方向数远小于权重矩阵的满秩。

设某个权重矩阵 W0Rd×kW_0 \in \mathbb{R}^{d \times k}。全参微调学的是 W0+ΔWW_0 + \Delta W。LoRA 不直接学 ΔW\Delta W,而是把它分解成两个瘦长矩阵的乘积:

ΔW=BA,BRd×r, ARr×k\Delta W = B A, \quad B \in \mathbb{R}^{d \times r},\ A \in \mathbb{R}^{r \times k}

其中秩 rmin(d,k)r \ll \min(d, k),典型取 8、16、32。前向计算变成:

h=W0x+αrBAxh = W_0 x + \frac{\alpha}{r} B A x

α\alpha 是缩放系数,控制 LoRA 分支的影响强度。训练时冻结 W0W_0,只更新 AABB

参数量对比立竿见影:原矩阵有 d×kd \times k 个参数,LoRA 只引入 r×(d+k)r \times (d + k) 个。若 d=k=4096d = k = 4096r=16r = 16,原本约 1677 万参数的矩阵,LoRA 只需约 13 万——不到 1%。

初始化有个小细节:AA 用随机高斯初始化,BB 初始化为零,于是训练开始时 BA=0BA = 0,模型行为与原始权重完全一致,从原点平滑出发。

1
2
3
4
5
6
7
8
9
10
11
12
13
class LoRALinear(nn.Module):
def __init__(self, base_linear, r=16, alpha=32):
super().__init__()
self.base = base_linear # 冻结
for p in self.base.parameters():
p.requires_grad = False
d_in, d_out = base_linear.in_features, base_linear.out_features
self.A = nn.Parameter(torch.randn(r, d_in) * 0.01)
self.B = nn.Parameter(torch.zeros(d_out, r)) # 零初始化
self.scale = alpha / r

def forward(self, x):
return self.base(x) + self.scale * (x @ self.A.T @ self.B.T)

LoRA 的另一个红利:梯度和优化器状态只对那不到 1% 的参数维护。前面那笔显存账里最贵的优化器状态,绝大部分被砍掉了。而且推理时可以把 BABA 合并回 W0W_0,不引入任何额外延迟。

QLoRA:把底座也压缩

LoRA 省掉了优化器状态,但冻结的底座 W0W_0 仍然要常驻显存。QLoRA 进一步:把冻结的底座量化到 4-bit

它的三个关键技术点:

  1. NF4(4-bit NormalFloat)量化:针对神经网络权重近似正态分布的特性设计的数据类型,比普通 int4 在同样位宽下信息保留更好。
  2. 双重量化(double quantization):连量化用的 scale 常数本身也再量化一次,进一步压缩。
  3. 分页优化器(paged optimizer):用类似操作系统换页的机制,在显存峰值时把优化器状态临时挪到内存,防 OOM。

关键在于:底座以 4-bit 存储,但前向计算时反量化回 bf16 参与运算,LoRA 适配器始终是高精度的。所以梯度流经的是 4-bit 底座 + 高精度 LoRA 分支。这让单张消费级显卡微调几十 B 的模型成为可能。

三者对比:怎么选

维度 全参微调 LoRA QLoRA
可训练参数 100% <1% <1%
底座精度 fp16/bf16 fp16/bf16 4-bit
显存占用 最高 最低
训练速度 最快(单步) 较慢(反量化开销)
效果上限 最高 接近全参 略低于 LoRA
多任务部署 需多份全模型 共享底座+切换适配器 同 LoRA

几条实践经验:

  • 数据量大、要改变模型底层能力(如换一种语言、注入大量新知识)时,全参更稳。 LoRA 的低秩假设在"大改"场景下会成为瓶颈。
  • 数据量中小、目标是风格对齐或任务适配时,LoRA 几乎是默认选择,效果接近全参而成本低一个数量级。
  • 显存是硬约束(单卡跑大模型)时上 QLoRA,用一点训练速度和精度换可行性。

踩坑

1. LoRA 别只挂在注意力的 Q、V 上。 早期实践常只对 attention 的部分投影加 LoRA,但把 LoRA 同时挂到所有线性层(含 MLP)通常效果更好,代价是可训练参数略增。

2. α\alpharr 的关系常被误解。 由于缩放是 α/r\alpha/r,单纯调大 rr 而不动 α\alpha,会同时稀释每个方向的有效学习率。调参时关注的是 α/r\alpha/r 这个比值。

3. QLoRA 的"省"是有代价的。 反量化在每次前向都要做,吞吐比纯 LoRA 低。如果显存够用,别无脑上 QLoRA。

4. 适配器合并要小心精度。 QLoRA 训练出的 LoRA 若直接合并回 4-bit 底座再量化,可能损失明显。生产部署常把底座反量化到 fp16 后再合并。

小结

微调的演进本质是一条省显存的路线:全参微调贵在优化器状态,LoRA 用低秩分解把可训练参数砍到 1% 以下、顺带省掉绝大部分优化器开销,QLoRA 再把冻结底座压到 4-bit。选型的核心问题只有两个——你要改变模型多深的能力,以及你有多少显存。前者决定 LoRA 够不够,后者决定要不要 QLoRA。