10. LoRA 与 QLoRA:只训练千分之五的参数
上一篇把资料助手的 SFT 数据、模板和损失都准备好了,剩下一个现实问题:Llama-3.1-8B 全参数训练,光是权重、梯度和 AdamW 状态就要约 128.5 GB(按每参数 16 字节算,见 训练显存账本),一张 80 GB 的 H100 放不下,还没算激活。团队手里只有一两张卡,而且将来人事、报销、IT 三个场景各要一个版本,每个版本存一份 16 GB 的完整权重也很浪费。
LoRA(Low-Rank Adaptation,低秩适配)的做法是:原模型一个参数都不动,只在部分矩阵旁边加两个很小的矩阵来学习“改动量”。大模型专题的 训练与微调 用单个 4096×4096 矩阵简单比较过参数数目,这一篇换成真实模型的完整配置来算,并讲清它的计算方式、为什么这么少的参数够用、秩和目标模块怎么选、QLoRA 怎样把底座再压到 4 bit,以及什么时候 LoRA 不够、该上全参数。读完你应该能手算一个 LoRA 配置有多少可训练参数,并判断自己的任务适不适合用它。
原矩阵不动,旁边加一条支路
Transformer 里的大部分参数都在线性层里,一个线性层就是一次矩阵乘法 h = W₀x。全参数微调要学的是一个和 W₀ 一样大的改动 ΔW;LoRA 规定这个改动必须能写成两个瘦长矩阵的乘积:
text
h = W₀x + (α/r) · B(Ax)
W₀:原矩阵,形状 d_out × d_in,冻结不更新
A :形状 r × d_in,把输入从 d_in 维压到 r 维
B :形状 d_out × r,再从 r 维放回 d_out 维
r :秩(rank),远小于 d_in 和 d_out,常见 8 到 64
α :缩放系数(lora_alpha),与 r 一起决定支路输出的放大倍数以 Llama-3.1-8B 每层 FFN 的第一个投影 gate_proj 为例,它把 4096 维变成 14336 维:
原矩阵有 14336 × 4096 = 58,720,256 个参数;r = 16 时,A 有 16 × 4096 = 65,536 个,B 有 14336 × 16 = 229,376 个,合计 294,912 个,是原矩阵的 0.50%。
图里有一个容易忽略的细节:A 随机初始化,B 初始化为全 0。这样训练第 0 步时 BA = 0,模型输出与原模型逐位相同,训练从底座出发,而不是从一个被随机扰动过的模型出发。如果两个都随机初始化,第一步前向就已经偏离了底座,前几百步要先把这份噪声“学回去”。PEFT 的默认初始化就是这样设置的。
训练时只有 A 和 B 收到梯度、占用优化器状态;推理时可以把 (α/r)BA 直接加回 W₀,得到一个形状完全相同的矩阵,不增加任何计算。这是 LoRA 和早期“插入额外层”的适配器方法最大的区别。
为什么这么少的参数就够
一个 4096 × 4096 的矩阵最多能表达 4096 个相互独立的“方向”,LoRA 只允许改动落在 16 个方向上。这能行,依据来自 2021 年 LoRA 论文的一个假设:大模型在适配新任务时,权重改动的内在秩很低。打个比方,预训练模型已经学会了写各种文体,让它改成“公司制度问答”的口吻,需要调整的只是少数几个风格和格式维度,而不是重学语言。
论文在 GPT-3 175B 上做了验证:只给注意力的 W_q、W_v 加 LoRA,秩取 1 时在 WikiSQL 和 MultiNLI 上已经和秩 64 差不多;不同秩学到的子空间高度重叠,加大秩并没有学到更有用的方向。由此得到的收益也很可观:与用 Adam 全参数微调 GPT-3 175B 相比,可训练参数少了约 10,000 倍,训练显存从 1.2 TB 降到 350 GB(约 3 倍),每个任务的检查点从 350 GB 变成 35 MB,训练速度快约 25%。
论文自己也给了这个假设的边界:如果下游任务用的是预训练时没怎么见过的语言,小秩未必够,重新训练整个模型可能明显更好。“改动量低秩”是一个经验观察,任务离预训练分布越远,它越不成立。
手算 Llama-3.1-8B 的 LoRA 参数
LoRA 在一个 d_out × d_in 的矩阵上增加 r × (d_in + d_out) 个参数。Llama-3.1-8B 每层有 7 个线性层,因为用了 GQA(8 个 KV 头、每头 128 维),k_proj、v_proj 的输出只有 1024 维。r = 16 时逐个代入:
| 模块 | 形状(输出 × 输入) | LoRA 参数 = 16 × (输入 + 输出) |
|---|---|---|
| q_proj | 4096 × 4096 | 131,072 |
| k_proj | 1024 × 4096 | 81,920 |
| v_proj | 1024 × 4096 | 81,920 |
| o_proj | 4096 × 4096 | 131,072 |
| gate_proj | 14336 × 4096 | 294,912 |
| up_proj | 14336 × 4096 | 294,912 |
| down_proj | 4096 × 14336 | 294,912 |
| 每层合计 | 1,310,720 |
text
32 层 × 1,310,720 = 41,943,040 ≈ 4194 万
占全模型 80.3 亿参数的 41,943,040 ÷ 8,030,261,248 ≈ 0.52%
训练状态(每参数 16 字节):41,943,040 × 16 ≈ 0.67 GB
冻结的 BF16 底座:约 16.1 GB全参数训练的 128.5 GB 训练状态,变成了 16.1 GB 的只读底座加 0.67 GB 的 LoRA 状态,再加上激活,单张 80 GB 的卡就能训,24 GB 的消费级卡配合激活重计算和较短序列也有机会。要注意激活并不会因为 LoRA 而变小:前向仍然要走完整个模型,反向也要穿过冻结层把梯度传给前面的 A、B。长序列训练时,激活往往才是显存的大头。
秩、alpha 和目标模块怎么选
目标模块比秩更重要。 2023 年的 QLoRA 论文发现,按早期做法只给 q_proj、v_proj 加 LoRA,追不上全参数微调;给所有线性层都加上才能追平,而在这个前提下,秩 r 的取值对结果影响不大。现在的主流框架基本都默认或推荐覆盖全部线性层(PEFT 写 target_modules="all-linear",LLaMA-Factory 写 lora_target: all)。
秩常见取 8、16、32、64。行为和格式类的任务(资料助手的口径、条款编号)r = 8 或 16 通常够用;要学的东西越多(新领域术语、复杂推理风格),越值得试更大的秩。参数量和秩成正比,r 从 16 加到 64,可训练参数从 4194 万变成约 1.68 亿,仍然只占全模型的 2%。
alpha 和秩一起决定支路的放大倍数 α/r。LoRA 论文的设计意图是:先固定 α,之后改 r 时不必重新调学习率。实践中常见两种写法:α = r(倍数 1,PEFT 的默认 r = 8、α = 8 就是这样),或 α = 2r(倍数 2,上图的 α = 32 就是这一种)。改 α 的效果和改学习率差不多,所以两者不要同时大幅调整。
学习率要比全参数微调大一个数量级,常见起点是 1e-4(TRL 文档和 LLaMA-Factory 示例都用这个值),全参数一般是 1e-5 左右。原因是 B 从 0 出发,A 的初始值也很小,支路一开始的输出几乎为零;学习率太小时,几轮训练下来改动量还没长到能影响输出的程度,表现为验证指标几乎不动。反过来,LoRA 的学习率开到 1e-3 以上容易发散,loss 曲线会出现尖峰。
QLoRA:把冻结的底座压到 4 bit
LoRA 把训练状态压到了 0.67 GB,剩下的大头是 16.1 GB 的 BF16 底座。底座既然不更新,能不能存得更省?2023 年的 QLoRA 就是这样做的:把底座的线性层量化成 4 bit 存储,计算时临时还原成 BF16 再做矩阵乘法,LoRA 的 A、B 仍保持 16 位精度训练。它靠三项技术把精度损失和显存峰值控制住:
- NF4(4-bit NormalFloat):一种 4 bit 数据格式。预训练权重大致呈正态分布,NF4 的 16 个取值按正态分布的分位点排布,比均匀刻度的 INT4 更贴合权重的实际分布。每 64 个权重一块,共用一个缩放常数。
- 双重量化:每块的缩放常数本身是 32 位数,平摊到每个权重上是 32 ÷ 64 = 0.5 bit。QLoRA 再把这些常数量化成 8 bit,平摊开销降到约 0.127 bit。
- 分页优化器:训练长序列时,显存会出现短暂的尖峰。分页优化器借助 NVIDIA 的统一内存,在显存不够时把优化器状态临时换到 CPU 内存,用到时再换回来,避免直接报显存不足。
论文的标志性结果是在单张 48 GB 的 GPU 上微调 65B 模型(16 位全参数微调需要 780 GB 以上),并且在他们的评测中与 16 位微调效果相当。代入 Llama-3.1-8B 估算底座大小:
text
线性层:69.8 亿参数 × (4 + 0.127) bit ÷ 8 ≈ 3.60 GB
词嵌入和输出层(通常保留 BF16):2 × 128,256 × 4096 × 2 字节 ≈ 2.10 GB
底座合计 ≈ 5.7 GB(估算),对比 BF16 的 16.1 GB省下的显存是用时间换的:每次前向和反向都要先把 4 bit 权重反量化,同样的数据 QLoRA 通常比 BF16 底座上的 LoRA 慢。显存够用时直接用 LoRA,放不下时再用 QLoRA。量化格式本身的原理见 数值精度:量化。
训练完怎样上线:合并还是挂载
LoRA 训练的产物是一个几十到几百 MB 的适配器文件,它只有和训练时那个确切的底座(同一个仓库、同一个版本、同一个分词器)放在一起才有意义。上线有两种方式:
合并:把 (α/r)BA 加回 W₀,保存成一个普通模型。推理引擎不需要任何 LoRA 支持,速度与原模型相同,适合只有一个版本的场景。
挂载多个适配器:显存里只放一份底座,人事、报销、IT 三个场景各一个适配器,按请求选用。以 vLLM 为例,启动时加 --enable-lora 并用 --lora-modules 名称=路径 注册适配器,请求里把 model 字段写成适配器名称即可;同一批请求可以混用不同适配器,--max-loras 和 --max-lora-rank 控制同时加载的数量和最大秩。代价是每一步多算一条支路,延迟略高于合并后的模型。挂载适配器后,前缀缓存要把适配器标识也算进缓存键,否则不同适配器会错误共用 KV,这一点在推理系列的 前缀缓存 里提到过。
两个容易出错的地方:已经合并过的模型再挂一次同一个适配器,改动会被叠加两次,输出明显变怪;在 QLoRA 的 4 bit 底座上训练的适配器,合并时要先把底座反量化到 16 位再加,合并后的模型(或再次量化后的模型)要重新跑一遍评估,不能沿用训练时的分数。
LoRA 的几个变体
LoRA 之后出现了很多改进,这里只介绍两个已经进入主流库、可以直接开关的:
- rsLoRA(2023):指出
α/r的缩放在秩较大时会让支路输出偏小、学习变慢,建议改用α/√r。打算用 r = 64 以上时值得一试,PEFT 里设use_rslora=True。 - DoRA(2024):把预训练权重拆成“幅度”和“方向”两部分,方向用 LoRA 更新,幅度单独用一个可训练向量。论文在 LLaMA、LLaVA 等模型上报告稳定优于 LoRA,低秩时提升更明显。代价是训练开销比 LoRA 大,PEFT 文档建议推理前合并。PEFT 里设
use_dora=True。
变体的收益通常是几个百分点量级,换底座、清洗数据带来的变化往往比它大得多,先把基本配置做对再考虑它们。
LoRA 学得少,也忘得少
LoRA 并不是免费的全参数微调。2024 年的论文《LoRA Learns Less and Forgets Less》在代码和数学两个领域做了系统比较,既有约 10 万条样本的指令微调,也有 200 亿 token 的继续预训练。结论分两面:在目标领域上,常见配置的 LoRA 明显不如全参数微调;在目标领域之外,LoRA 更好地保留了底座原有的能力,遗忘比全参数少。他们还测量到,全参数微调学到的权重改动的秩,比常见 LoRA 配置高 10 到 100 倍,这在一定程度上解释了差距。
这给出了一个比较清楚的选择方式:
| 情况 | 更合适的做法 | 理由 |
|---|---|---|
| 改口径、格式、语气,数据几千到几万条 | LoRA | 改动本来就小,还能少忘 |
| 一个底座服务多个业务版本 | LoRA,按请求挂载 | 每个版本只多几十 MB |
| 显存只够放下 4 bit 底座 | QLoRA | 用速度换显存 |
| 大规模学新领域(新语言、大量专业语料、数十亿 token) | 全参数(或继续预训练) | 低秩改动装不下这么多新东西 |
| 要在代码、数学这类能力上整体提升,数据量很大 | 全参数 | 上述论文测到的差距正出现在这类任务 |
判断自己处于哪一行,可以做一次小实验:用同一份数据、同样的轮数,分别训 r = 16 和 r = 64 的 LoRA。如果大秩明显更好,说明任务需要的改动比较“宽”,值得试全参数;如果两者差不多,说明瓶颈在数据而不是参数量,继续加大秩或换全参数都没什么用。
LoRA 用 W₀ + (α/r)BA 把改动量限制在低秩空间里:B 初始化为 0 保证从底座出发,r = 16 覆盖全部线性层时 Llama-3.1-8B 只训练 0.52% 的参数,训练状态从 128.5 GB 降到 0.67 GB;QLoRA 再把冻结底座压到约 5.7 GB。它擅长改行为、少遗忘,但学不进大量新知识。想亲手跑一遍,看 实践:用 LoRA 微调资料助手,那里用 Qwen2.5-0.5B-Instruct 在本机实际跑了一遍。之后进入下一个问题:SFT 只教了“好回答长什么样”,怎样让模型学会在两个回答之间选更好的那个,见 偏好对齐。
