系列 《LoRA 专题:SFT 的默认微调方式》 第 2 / 4 篇
系列总览 — 为什么这样组织、读它需要什么、读完能做什么
- 低秩假设:为什么两个瘦矩阵够用,以及它省了哪几本账
- 选参:r、target_modules、alpha、lr 与 QLoRA / DoRA / PiSSA 各让模型变成什么
- 工程:adapter 文件、合并、多 LoRA 服务与参考模型
- 系列总结与通关自测
上一篇:低秩假设:为什么两个瘦矩阵够用,以及它省了哪几本账下一篇:工程:adapter 文件、合并、多 LoRA 服务与参考模型
更新 @2026-09-30:实验用 peft 0.21.1、trl 1.14.1、transformers 5.17.0、bitsandbytes 0.50.2、PyTorch 2.14(CPU,8 线程),模型 Qwen2.5-0.5B(base),数据 HuggingFaceH4/no_robots。配套脚本
ai-learning-labs/lora/02_knobs.py,--list列出全部配置名;正文给出每种配置的数字。
LoraConfig(r=16, lora_alpha=32, target_modules="all-linear", lora_dropout=0.05) 加 learning_rate=2e-4——这一行出现在几乎每个 SFT 脚本里。本文把它拆成一个个旋钮,每个旋钮回到 \(W' = W + \frac{\alpha}{r} BA\) 里的位置,然后用同一份数据、同样 80 步的对照实验给一组数字:秩 \(r\)、挂哪些矩阵、\(\alpha\) 与 rsLoRA、学习率与 LoRA+、初始化家族(PiSSA / OLoRA / EVA / LoftQ)、DoRA、QLoRA。最后是全部十三种配置并排的一张表,和一张按任务与资源选配置的表。
一、实验设置:十三种配置,三个指标
所有配置共用:
| 项 | 值 |
|---|---|
| 模型 | Qwen/Qwen2.5-0.5B(base,494M) |
| 数据 | no_robots 训练集 800 条单轮样本;验证集 100 条 |
| 训练 | 80 步,batch 4 × 最长 512,AdamW,cosine,warmup 8 步,只对回复算 loss(completion_only_loss=True) |
| 指标一:验证回复 loss | 100 条验证样本的 assistant 回复 token 上的平均交叉熵。训练前 2.4936。越低越好 |
| 指标二:普通文本 loss 变化 | wikitext-2 测试集 60 段上的交叉熵相对训练前(2.8748)的变化。衡量遗忘,越接近 0 越好 |
| 指标三:每步秒数 | 8 线程 CPU 上的墙钟时间,只看相对值 |
基线是全量微调 lr \(10^{-5}\):训练 loss 2.409 → 2.182,验证回复 loss 2.3924,普通文本 loss +0.0213,6.7 s/步——与后训练第一篇在 Apple Silicon 上跑出的数字相同。LoRA 的默认配置(\(r = 16\)、全部线性层、\(\alpha = 32\)、lr \(10^{-4}\))是所有对照的另一个锚点。
80 步、800 条数据是一个刻意的小实验:它能分辨旋钮之间的差别,但绝对数字不代表”LoRA 能训到多好”。每一节末尾都标出哪些结论在大模型、长训练上会变。
二、秩 r:从 4 到 64 差多少
\(r\) 决定 \(A\)、\(B\) 的瘦边有多宽,也就是 \(\Delta W\) 最多有几个独立方向。可训练参数与 \(r\) 成正比:0.5B 全部线性层上 \(r = 4 / 16 / 64\) 是 2.2M / 8.8M / 35.2M。
| 配置 | 可训练参数 | 训练 loss(10 步 → 80 步) | 验证回复 loss | 普通文本 loss 变化 | s/步 |
|---|---|---|---|---|---|
| 全量 lr 1e-5 | 494.0M(100%) | 2.409 → 2.182 | 2.3924 | +0.0213 | 6.7 |
| LoRA r=4 全部线性层 lr 1e-4 | 2.2M(0.45%) | 2.436 → 2.211 | 2.4141 | -0.0007 | 6.4 |
| LoRA r=16 全部线性层 lr 1e-4 | 8.8M(1.78%) | 2.422 → 2.181 | 2.3943 | +0.0107 | 6.3 |
| LoRA r=64 alpha=128(缩放 2)lr 1e-4 | 35.2M(7.12%) | 2.413 → 2.184 | 2.3958 | +0.0436 | 8.7 |
读法:
- 从 4 到 16 收益明显(2.4141 → 2.3943),从 16 到 64 没有再改善(2.3958),\(r = 16\) 已经追到全量的 0.002 以内。\(r\) 的边际收益递减,与 Hu 等 2021 在 GPT-3 上的观察一致(\(r = 8\) 与 \(r = 64\) 学到的子空间前几个方向高度重合)。
- \(r\) 越大,遗忘也越大:普通文本 loss 变化 −0.0007 / +0.0107 / +0.0436,\(r = 64\) 已经是全量(+0.0213)的两倍。\(r\) 是一个容量旋钮,不是”越大越好”。
- 什么时候要大 \(r\):数据量大(万条以上)、任务离预训练分布远(新领域、新语言、代码/数学)、要注入知识。Biderman 等 2024 在代码继续预训练上 \(r = 256\) 仍不如全量;指令微调上 \(r = 16 \sim 64\) 已接近。
rank_pattern可以给不同矩阵不同的 \(r\)(如 MLP 大、attention 小);AdaLoRA(Zhang 等 2023)按重要性动态分配 \(r\)。两者都是”\(r\) 该花在哪”的问题,默认均匀分配在多数任务上够用。
三、target_modules:只挂 attention 还是全部线性层
Hu 等 2021 只给 \(W_q\)、\(W_v\) 挂 LoRA;Dettmers 等 2023(QLoRA)发现挂全部线性层比增大 \(r\) 更重要——只挂 attention 的 LoRA 即使 \(r\) 加大也追不上全量,挂全部线性层后 \(r\) 的影响反而变小。本实验的对照:\(r = 16\) 只挂 q k v o(2.2M)与挂全部七个(8.8M)。
| 配置 | 可训练参数 | 训练 loss(10 步 → 80 步) | 验证回复 loss | 普通文本 loss 变化 | s/步 |
|---|---|---|---|---|---|
| 全量 lr 1e-5 | 494.0M(100%) | 2.409 → 2.182 | 2.3924 | +0.0213 | 6.7 |
| LoRA r=16 attention lr 1e-4 | 2.2M(0.44%) | 2.433 → 2.206 | 2.4113 | +0.0018 | 5.0 |
| LoRA r=16 全部线性层 lr 1e-4 | 8.8M(1.78%) | 2.422 → 2.181 | 2.3943 | +0.0107 | 6.3 |
| LoRA r=64 alpha=128(缩放 2)lr 1e-4 | 35.2M(7.12%) | 2.413 → 2.184 | 2.3958 | +0.0436 | 8.7 |
读法:
- 从 attention 到全部线性层参数多了 4 倍,验证 loss 改善 0.017(2.4113 → 2.3943);把 \(r\) 从 16 提到 64 同样是参数翻 4 倍,却没有改善。参数应该花在挂哪里,不是花在 \(r\) 上。代价是遗忘从 +0.0018 到 +0.0107:只挂 attention 的模型改得最少。MLP 层承载了大部分”知识”与非线性变换,只改注意力等于只改”看哪里”不改”怎么算”。
target_modules="all-linear"在peft里展开为模型中所有nn.Linear减去输出层lm_head——输出层挂 LoRA 会让 adapter 与词表大小相关(151936 × 16),且改输出分布的效果差。- 想再省:k_proj、v_proj 在 GQA 模型上只有 896 × 128,挂不挂对参数量几乎没影响,不必特意排除;真要省就去掉 gate/up/down 中的一个,但没有证据说明哪一个更不重要。
- embedding 与 lm_head 不在
all-linear里(embedding 不是nn.Linear,lm_head 被排除)。加了新 token 时它们必须另外处理——第三篇第六节。
四、alpha 与 rsLoRA:缩放常数怎么随 r 变
4.1 α / r 是什么
前向是 \(W x + \frac{\alpha}{r} B A x\),\(\alpha / r\) 是一个不训练的常数(scaling)。它的作用是让改 \(r\) 时不必重调学习率:Adam 的更新量与梯度尺度无关,每步 \(B\)、\(A\) 的每个元素大约动 lr 那么多,\(B A x\) 的尺度随 \(r\) 增长(\(r\) 个方向的贡献相加),除以 \(r\) 把它拉回来。所以固定 \(\alpha\)、改 \(r\),大致能用同一个 lr。
三个例子:\(r = 16\)、\(\alpha = 32\) → scaling 2;\(r = 64\)、\(\alpha = 32\) → scaling 0.5;\(r = 64\)、\(\alpha = 128\) → scaling 2。社区惯例 \(\alpha = 2r\) 就是让 scaling 恒为 2——这等于说”\(\alpha / r\) 这个设计根本没起作用,我们手动固定了它”。
4.2 rsLoRA:α / √r
Kalajdzievski 2023 指出 \(\alpha / r\) 除得太狠:\(r\) 个随机方向相加,尺度按 \(\sqrt r\) 增长而不是 \(r\)(与随机游走同理)。除以 \(r\) 后,\(r\) 越大 adapter 的有效更新越小,大 \(r\) 的 LoRA 学得比小 \(r\) 还慢——这就是很多人”把 \(r\) 开到 256 也没变好”的原因。修法是 scaling 改成 \(\alpha / \sqrt r\)(use_rslora=True):\(r = 64\)、\(\alpha = 32\) → \(32 / 8 = 4\)。
本实验在 \(r = 64\) 上比三种缩放:\(\alpha = 128\)(scaling 2,\(\alpha = 2r\) 惯例)、\(\alpha = 32\)(scaling 0.5,”固定 \(\alpha\) 直接加大 \(r\)“)、\(\alpha = 32\) + rsLoRA(scaling 4)。lr 都是 \(10^{-4}\)。
| 配置 | 可训练参数 | 训练 loss(10 步 → 80 步) | 验证回复 loss | 普通文本 loss 变化 | s/步 |
|---|---|---|---|---|---|
| LoRA r=64 alpha=128(缩放 2)lr 1e-4 | 35.2M(7.12%) | 2.413 → 2.184 | 2.3958 | +0.0436 | 8.7 |
| LoRA r=64 alpha=32(缩放 0.5)lr 1e-4 | 35.2M(7.12%) | 2.422 → 2.182 | 2.3940 | +0.0087 | 6.1 |
| rsLoRA r=64 alpha=32(缩放 4)lr 1e-4 | 35.2M(7.12%) | 2.420 → 2.201 | 2.4135 | +0.0727 | 8.2 |
读法:
- scaling 与 lr 是同一个旋钮的两面:\(B A x\) 前面乘 2 与 lr 乘 2 在一阶上等价(Adam 下几乎精确等价)。三行的验证 loss 与遗忘随 scaling 单调变化:0.5 → 2 → 4 的普通文本 loss 变化是 +0.0087 → +0.0436 → +0.0727,scaling 4 的验证 loss 反而最差(2.4135)。与第五节 lr 1e-3 的那一行是同一件事。
- 在这个任务上 scaling 0.5 没有被”压掉”(2.3940,三者中最好)——因为 800 条 no_robots 需要的 \(\Delta W\) 很小,lr 1e-4 × scaling 2 已经在”够大”一侧。rsLoRA 论文(Kalajdzievski 2023)的收益出现在大 \(r\) 要学更多东西(数据多、任务远)的场合:那时固定 \(\alpha\) 的 \(1 / r\) 才真的把更新压掉。
- 实践建议:要么 \(\alpha = 2r\) 固定 scaling,要么
use_rslora=True固定 \(\alpha\);两者都可以,但改 \(r\) 时一定看一眼 scaling 变成了多少,把它当 lr 的一部分来调。
五、学习率与 LoRA+
5.1 为什么 LoRA 的 lr 比全量大一个量级
全量微调 lr 取 \(10^{-5}\) 量级,LoRA 取 \(10^{-4} \sim 2 \times 10^{-4}\)。原因有两个:\(B\) 从零开始、\(A\) 是随机方向,它们要从无到有长出一个 \(\Delta W\),而全量微调的每个参数只需在原值附近微调;\(\Delta W = s B A\) 对 \(B\) 的每一步更新的响应是 \(s \cdot \delta B \cdot A\),\(A\) 的元素是 \(O(1/\sqrt{d_{in}})\) 的,同样的 lr 落到 \(\Delta W\) 上比全量小。本实验比 \(10^{-5}\)、\(10^{-4}\)、\(10^{-3}\):
| 配置 | 可训练参数 | 训练 loss(10 步 → 80 步) | 验证回复 loss | 普通文本 loss 变化 | s/步 |
|---|---|---|---|---|---|
| 全量 lr 1e-5 | 494.0M(100%) | 2.409 → 2.182 | 2.3924 | +0.0213 | 6.7 |
| LoRA r=16 全部线性层 lr 1e-5 | 8.8M(1.78%) | 2.442 → 2.256 | 2.4475 | -0.0023 | 6.5 |
| LoRA r=16 全部线性层 lr 1e-4 | 8.8M(1.78%) | 2.422 → 2.181 | 2.3943 | +0.0107 | 6.3 |
| LoRA r=16 全部线性层 lr 1e-3 | 8.8M(1.78%) | 2.421 → 2.205 | 2.4215 | +0.0907 | 5.5 |
(Qwen2.5-0.5B,no_robots 800 条,80 步 × batch 4 × 512,CPU;训练前验证回复 loss 2.4936、普通文本 loss 2.8748)
读法:
- \(10^{-5}\):与全量同一个 lr,LoRA 只学到一半(验证 loss 降 0.046,\(10^{-4}\) 降 0.099),遗忘为零——不是不会学,是 80 步不够。
- \(10^{-3}\):验证 loss 比 \(10^{-4}\) 差(2.4215 对 2.3943),普通文本 loss 却涨了 +0.0907,是全量的 4 倍——LoRA 也会遗忘,只是 lr 合适时忘得少。”LoRA 忘得少”的前提是它学得也少。
- 合适区间 \(10^{-4} \sim 5 \times 10^{-4}\),与全量差 10–20 倍。
5.2 LoRA+:给 B 更大的 lr
Hayou 等 2024 从无穷宽极限的分析得出:\(A\) 与 \(B\) 的角色不对称——\(A\) 是 \(d_{in} \to r\) 的投影,\(B\) 是 \(r \to d_{out}\) 的展开,同一个 lr 下 \(B\) 的有效学习速度比 \(A\) 慢 \(O(d)\) 倍。修法是 \(B\) 的 lr 设成 \(A\) 的 \(\lambda\) 倍,\(\lambda\) 取 \(2^4\) 左右。peft.optimizers.create_loraplus_optimizer(model, AdamW, lr=1e-4, loraplus_lr_ratio=4) 做这件事。本实验 \(\lambda = 4\):
| 配置 | 可训练参数 | 训练 loss(10 步 → 80 步) | 验证回复 loss | 普通文本 loss 变化 | s/步 |
|---|---|---|---|---|---|
| LoRA r=16 全部线性层 lr 1e-4 | 8.8M(1.78%) | 2.422 → 2.181 | 2.3943 | +0.0107 | 6.3 |
| LoRA+ r=16 全部线性层 lr 1e-4(B ×4) | 8.8M(1.78%) | 2.414 → 2.182 | 2.3998 | +0.0455 | 5.7 |
读法:LoRA+ 在本实验没有收益——前 10 步训练 loss 略低(2.414 对 2.422),80 步后验证 loss 略差(2.3998 对 2.3943),遗忘多了 4 倍(+0.0455 对 +0.0107)。论文(Hayou 等 2024)报告的是 1–2% 的效果与最多 2 倍的收敛速度,出现在大模型、长训练上。它本质上等价于把整体 lr 调大并偏向 \(B\),与 5.1 节的 lr 扫描不是独立的旋钮:lr 已经在”够大”一侧时再给 \(B\) 乘 4 只会多遗忘。代价几乎为零(两个 param group),可以试,但要与 lr 一起调。
5.3 dropout、weight decay、bias
lora_dropout:只作用在 LoRA 分支的输入上(lora_B(lora_A(dropout(x)))),冻结的 \(W x\) 不受影响。数据少(千条以下)时 0.05–0.1 有用,数据多时设 0 并省一次 dropout kernel。本实验数据 800 条、80 步,设 0 以免引入随机性。weight_decay:作用在 \(A\)、\(B\) 上,把 \(\Delta W\) 往零拉。通常 0 或很小;LoRA 本身就是强正则,再加 weight decay 收益不明显。bias="none":LoRA 默认不训 bias。Qwen2.5 的 q k v 有 bias,bias="lora_only"可以把挂了 LoRA 的层的 bias 一起训,参数量可忽略,效果差别也可忽略。
六、初始化家族:PiSSA、OLoRA、EVA、LoftQ、CorDA
第一篇说了默认初始化 \(B = 0\)、\(A\) 随机的理由:训练开始时 \(W' = W\),而梯度不为零。它的缺点是 \(A\) 的随机方向与 \(W\) 无关,前几十步要先”找方向”。一族方法用 \(W\) 或数据来选初始方向:
| 方法 | \(A_0\)、\(B_0\) 怎么来 | 怎么保持 \(W' = W\) | 额外代价 | init_lora_weights= |
|---|---|---|---|---|
| 默认 | \(A\) Kaiming 均匀、\(B = 0\) | \(B = 0\) | 无 | True |
| Gaussian | \(A \sim \mathcal{N}(0, 1/r)\)、\(B = 0\) | \(B = 0\) | 无 | "gaussian" |
| PiSSA(Meng 等 2024) | \(W\) 的前 \(r\) 个奇异方向:\(B_0 A_0 = U_r S_r V_r^\top\) | 改底座 \(W_{res} = W - B_0 A_0\) | 每个矩阵一次 SVD(pissa_niter_4 用随机 SVD 近似);发布 adapter 要转换 |
"pissa" / "pissa_niter_4" |
| OLoRA(Büyükakyüz 2024) | \(W\) 的 QR 分解取前 \(r\) 列 | 改底座 | 一次 QR;要转换 | "olora" |
| EVA(Paischer 等 2024) | 用一小批数据的激活做 SVD,把 \(A\) 初始化成激活的主方向,并按解释方差重新分配各层的 \(r\) | \(B = 0\) | 训前跑几十个 batch 的前向 | "eva" + eva_config |
| LoftQ(Li 等 2023) | 对着量化误差初始化:\(B_0 A_0 \approx W - Q(W)\),交替迭代 | 底座本来就是量化的 | 迭代 SVD;只对 QLoRA 有意义 | "loftq" + loftq_config |
| CorDA(Yang 等 2024) | 用数据协方差加权的 SVD,选任务相关(或无关,用于保知识)的方向 | 改底座 | 前向 + SVD;要转换 | "corda" + corda_config |
它们分两类:改底座的(PiSSA、OLoRA、CorDA:训的是 \(W_{res}\) 上的 adapter,第三篇1.3 节讲怎么把它转成对原始 \(W\) 的普通 adapter)与不改底座的(EVA:只是选了更好的 \(A\),adapter 与普通 LoRA 完全兼容)。本实验跑 PiSSA(\(r = 16\),pissa_niter_4):
| 配置 | 可训练参数 | 训练 loss(10 步 → 80 步) | 验证回复 loss | 普通文本 loss 变化 | s/步 |
|---|---|---|---|---|---|
| LoRA r=16 全部线性层 lr 1e-4 | 8.8M(1.78%) | 2.422 → 2.181 | 2.3943 | +0.0107 | 6.3 |
| PiSSA r=16 全部线性层 lr 1e-4 | 8.8M(1.78%) | 2.417 → 2.207 | 2.4152 | +0.0526 | 5.6 |
读法:PiSSA 让 adapter 从 \(W\) 最主要的方向出发,论文(Meng 等 2024)在 GSM8K 等任务上报告比 LoRA 收敛更快、高几个点。本实验里它更差:验证 loss 2.4152 对 2.3943,遗忘 +0.0526 对 +0.0107,前期训练 loss 也看不出更快。原因就在它的设计里:它训的正是 \(W\) 的主奇异方向,也就是预训练模型最重要的那部分,学一个小任务时首先动的是通用能力,遗忘的风险相应更大——CorDA 的”知识保留模式”(选任务无关的方向)就是冲着这一点来的。EVA 在不改底座的前提下拿到类似的收敛加速,是更保守的选择。
七、DoRA:把幅度与方向分开
Liu 等 2024 观察到全量微调时权重的幅度(每列的范数)与方向的变化模式与 LoRA 不同:全量微调常常是大方向变化配小幅度变化,LoRA 则两者正相关。DoRA 把 \(W\) 分解成 \(W = m \cdot \frac{V}{\lVert V \rVert_c}\)(\(m\) 是 \(d_{out}\) 维的列范数向量,\(V / \lVert V \rVert_c\) 是单位方向),只对方向部分挂 LoRA、幅度 \(m\) 单独训:
\[W' = m \odot \frac{W + \frac{\alpha}{r} B A}{\lVert W + \frac{\alpha}{r} B A \rVert_c}\]代价:每个矩阵多 \(d_{out}\) 个参数(可忽略,0.5B 上 8.8M → 9.1M);每步要算 \(W + BA\) 的列范数,这需要显式构造 \(d_{out} \times d_{in}\) 的 \(BA\)——正是第一篇说 LoRA 刻意避免的事。所以 DoRA 一步比 LoRA 慢(本实验 CPU 上慢 27%;GPU 上大矩阵的范数计算相对更贵)。
| 配置 | 可训练参数 | 训练 loss(10 步 → 80 步) | 验证回复 loss | 普通文本 loss 变化 | s/步 |
|---|---|---|---|---|---|
| LoRA r=16 全部线性层 lr 1e-4 | 8.8M(1.78%) | 2.422 → 2.181 | 2.3943 | +0.0107 | 6.3 |
| DoRA r=16 全部线性层 lr 1e-4 | 9.1M(1.84%) | 2.422 → 2.181 | 2.3942 | +0.0113 | 8.0 |
读法:\(r = 16\) 上 DoRA 与 LoRA 的验证 loss、遗忘都相同(2.3942 对 2.3943,+0.0113 对 +0.0107)。这与论文(Liu 等 2024)一致:DoRA 在小 \(r\) 上收益最明显(\(r = 4\) 时补上大部分与全量的差距),\(r = 16\) 以上差别缩小;训练慢 (本实验 8.0 对 6.3 s/步,慢 27%),合并后推理与 LoRA 一样零开销(merge_and_unload 会把 \(m\) 乘回去)。适合”\(r\) 必须很小”的场景(adapter 数量极多或显存极紧)。
八、QLoRA:把冻结权重量化到 4 bit
第一篇的账:8B 模型 LoRA 训练状态 15.5 GiB,其中 15 GiB 是冻结的 BF16 权重。QLoRA(Dettmers 等 2023)把这一块量化到 4 bit,LoRA 部分不变,8B 的训练状态降到 5 GiB 左右,65B 可以在一张 48 GB 的卡上微调。它由三件事组成:
8.1 NF4:按正态分布分位数取的 16 个格点
普通 INT4 把 \([-1, 1]\) 均分成 16 格;权重是近似正态分布的,均分会浪费两端的格点。NF4 用标准正态分布的 16 个分位数做格点(对称、含 0):
-1.0000 -0.6962 -0.5251 -0.3949 -0.2844 -0.1848 -0.0911 0.0000
0.0796 0.1609 0.2461 0.3379 0.4407 0.5626 0.7230 1.0000
量化一个块:每 64 个权重一块,取块内绝对值最大者 \(m\) 作缩放,\(w / m\) 落到最近的格点,存 4 bit 的格点编号;反量化时 \(\hat w = \text{格点} \times m\)。一个手算:块内最大绝对值 \(m = 0.08\),权重 \(w = 0.02\):\(w / m = 0.25\),最近的格点是 0.2461(编号 10),反量化 \(0.2461 \times 0.08 = 0.01969\),误差 \(3 \times 10^{-4}\)。
8.2 双重量化:把缩放系数也量化
每 64 个权重一个 FP32 的 \(m\),是每参数 \(32 / 64 = 0.5\) bit 的额外开销。双重量化把每 256 个 \(m\) 再做一次 8 bit 量化(块内共用一个 FP32 的二级缩放),开销变成 \(8 / 64 + 32 / (64 \times 256) = 0.127\) bit / 参数。8B 模型省 \(8 \times 10^9 \times 0.373 / 8 = 0.37\) GB。
8.3 分页优化器与计算精度
分页优化器(paged optimizer)用 CUDA 统一内存把 Adam 状态放在可以换出到 CPU 的页上,防止长序列 batch 的激活峰值把显存打爆——它解决的是激活值那本账的尖峰,与量化无关。计算时每个线性层先把 NF4 块反量化成 BF16(bnb_4bit_compute_dtype)再做矩阵乘:权重存 4 bit,算的时候仍是 16 bit,反量化每步都要做,所以 QLoRA 一步比 LoRA 慢 30–50%。
8.4 实测
BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True) 加载 Qwen2.5-0.5B,内存占用 430 MB(BF16 是 988 MB;embedding 与 lm_head 不量化,所以不到四分之一)。
在它上面挂 r=16 全部线性层的 LoRA,与 FP32 底座的同一配置并排(同数据、同 80 步、同 lr 1e-4):
| 配置 | 训练 loss 首→末 | 验证回复 loss | 普通文本 loss 变化 | 每步 |
|---|---|---|---|---|
| LoRA r=16 全部线性层,FP32 底座 | 2.422 → 2.181 | 2.3943 | +0.0107 | 6.3 s |
| QLoRA NF4 r=16 全部线性层 | 2.514 → 2.262 | 2.4663 | +0.1415 | 6.7 s |
训练 loss 从第一步起就高 0.09(2.514 对 2.422)——那是 NF4 底座自己的量化误差,0.5B 这么小的模型被 4-bit 伤得不轻;80 步后 adapter 把差距缩到 0.07,验证回复 loss 2.4663 仍比 FP32 的 2.3943 差得多。表里的”+0.1415”是相对 FP32 底座算的,其中大部分是底座量化本身、不是 adapter 带来的遗忘:NF4 底座训练前的验证回复 loss 是 2.5780,比 FP32 高 0.084(第三篇第三节),QLoRA 的 adapter 在它上面拿到了 −0.112,比 FP32 LoRA 的 −0.099 还多一点。每步慢 6%(CPU 上反量化走的是 kernels 包的 CPU kernel;GPU 上论文报的是慢 30% 左右)。
读法:QLoRA 的精度损失来自底座量化误差,adapter 一定程度上会补偿它(它是对着量化后的底座学的);论文在 Vicuna 评测上 4-bit 追平 16-bit 全量。代价是每步慢、训练前要有 bitsandbytes 的 kernel(CPU 上要 kernels 包并联网取一次)。上线时的底座精度选择与失配问题在第三篇第三节。
九、全表与选择表
9.1 十三种配置并排
| 配置 | 可训练参数 | 训练 loss(10 步 → 80 步) | 验证回复 loss | 普通文本 loss 变化 | s/步 |
|---|---|---|---|---|---|
| 全量 lr 1e-5 | 494.0M(100%) | 2.409 → 2.182 | 2.3924 | +0.0213 | 6.7 |
| LoRA r=16 attention lr 1e-4 | 2.2M(0.44%) | 2.433 → 2.206 | 2.4113 | +0.0018 | 5.0 |
| LoRA r=16 全部线性层 lr 1e-4 | 8.8M(1.78%) | 2.422 → 2.181 | 2.3943 | +0.0107 | 6.3 |
| LoRA r=4 全部线性层 lr 1e-4 | 2.2M(0.45%) | 2.436 → 2.211 | 2.4141 | -0.0007 | 6.4 |
| LoRA r=64 alpha=128(缩放 2)lr 1e-4 | 35.2M(7.12%) | 2.413 → 2.184 | 2.3958 | +0.0436 | 8.7 |
| LoRA r=64 alpha=32(缩放 0.5)lr 1e-4 | 35.2M(7.12%) | 2.422 → 2.182 | 2.3940 | +0.0087 | 6.1 |
| rsLoRA r=64 alpha=32(缩放 4)lr 1e-4 | 35.2M(7.12%) | 2.420 → 2.201 | 2.4135 | +0.0727 | 8.2 |
| LoRA r=16 全部线性层 lr 1e-5 | 8.8M(1.78%) | 2.442 → 2.256 | 2.4475 | -0.0023 | 6.5 |
| LoRA r=16 全部线性层 lr 1e-3 | 8.8M(1.78%) | 2.421 → 2.205 | 2.4215 | +0.0907 | 5.5 |
| DoRA r=16 全部线性层 lr 1e-4 | 9.1M(1.84%) | 2.422 → 2.181 | 2.3942 | +0.0113 | 8.0 |
| PiSSA r=16 全部线性层 lr 1e-4 | 8.8M(1.78%) | 2.417 → 2.207 | 2.4152 | +0.0526 | 5.6 |
| LoRA+ r=16 全部线性层 lr 1e-4(B ×4) | 8.8M(1.78%) | 2.414 → 2.182 | 2.3998 | +0.0455 | 5.7 |
| QLoRA NF4 r=16 全部线性层 lr 1e-4 | 8.8M(1.78%) | 2.514 → 2.262 | 2.4663 | +0.1415 | 6.7 |
9.2 按任务与资源选
| 场景 | 建议 | 依据 |
|---|---|---|
| 默认起点(指令 / 格式 / 风格 SFT,千到万条数据) | \(r = 16\)、all-linear、\(\alpha = 32\)、lr \(1 \sim 2 \times 10^{-4}\)、dropout 0–0.05 |
第二、三、五节:与全量差距最小、遗忘最少的性价比点 |
| 数据多(10 万条以上)或领域远 | \(r = 64 \sim 128\)、use_rslora=True 或 \(\alpha = 2r\)、lr 不变 |
第二、四节:要学得多时容量要跟上,scaling 不能被 \(1 / r\) 压掉 |
| 注入知识、继续预训练、代码/数学大幅提升 | 全量;或 \(r \geq 256\) + all-linear 并接受差距 | 第一篇第四、六节:改动本身高秩 |
| 显存放不下 BF16 底座 | QLoRA(NF4 + 双重量化)+ 上面的 LoRA 配置;可加 LoftQ 初始化 | 第八节:只动冻结权重那本账 |
| 必须很小的 \(r\)(几百个 adapter 同服) | \(r = 4 \sim 8\) + use_dora=True |
第七节:小 \(r\) 上 DoRA 收益最大 |
| 只有几百条数据、怕过拟合 | \(r = 8\)、dropout 0.1、步数少、lr \(10^{-4}\) | 第五节:LoRA 也会遗忘 |
| 收敛慢、步数预算紧、数据多 | 先调 lr 再试 EVA 初始化或 LoRA+ \(\lambda = 4 \sim 16\) PiSSA 要盯遗忘 |
第五、六节:小任务上它们不比调 lr 更有效 |
| 加了新 token | 任何配置 + trainable_token_indices |
第三篇第六节 |
| 只服务一个模型、显存充足 | 全量微调 | LoRA 的收益都用不上 |
十、本文小结
- \(r\) 是容量:4 → 16 收益大,16 → 64 在小任务上没有收益,遗忘随之增大;知识注入要么大 \(r\) 要么全量。
all-linear比只挂 attention 重要,比加 \(r\) 重要;lm_head与 embedding 不在其中。- scaling \(= \alpha / r\) 与 lr 是一个旋钮的两面:缩放越大学得越猛、忘得越多。固定 \(\alpha\) 加大 \(r\) 会把更新压掉(要学得多时才显出来);用 \(\alpha = 2r\) 或 rsLoRA,并把 scaling 当 lr 的一部分调。
- LoRA 的 lr 比全量大 10–20 倍;太大会遗忘——LoRA 忘得少的前提是学得也少。LoRA+ 给 \(B\) 更大的 lr,等价于 lr 再调大,本实验没有收益。
- 初始化:PiSSA / OLoRA / CorDA 改底座、发布要转换,动的是主方向、遗忘更大(本实验 PiSSA 比默认差);EVA 不改底座;LoftQ 只对 QLoRA。
- DoRA 幅度方向分开训,小 \(r\) 收益大、\(r = 16\) 上与 LoRA 相同,每步慢 27%。
- QLoRA = NF4 + 双重量化 + 分页优化器,只动冻结权重那本账,每步慢(文献常见 30–50%),精度几乎不掉。
十一、自测
-
从 \(r = 16\)、\(\alpha = 32\) 改成 \(r = 128\),\(\alpha\) 不变、lr 不变。scaling 从多少变成多少?会发生什么?两种修法各是什么?
答案
scaling 从 2 变成 0.25,\(\Delta W\) 的有效更新被压掉 8 倍,一阶上等价于 lr 缩小 8 倍;步数紧、要学的东西多时大 \(r\) 反而学得更慢(第四节里小任务 80 步内看不出来,不代表没发生)。修法一:\(\alpha = 256\)(\(\alpha = 2r\));修法二:
use_rslora=True,scaling 变成 \(32 / \sqrt{128} = 2.83\)。 -
同样 8.8M 可训练参数的预算,A 方案 \(r = 16\) 挂全部七个矩阵,B 方案 \(r = 64\) 只挂 q k v o。哪个好?为什么?
答案
A。\(r = 64\) attention-only 是 8.65M,参数量相近,但第三节的对照显示挂全部线性层的收益大于加 \(r\)(QLoRA 论文的同一结论):MLP 承载大部分变换,只改注意力改不到。
-
有人用 lr \(10^{-5}\) 训 LoRA 80 步说”LoRA 不 work”。最可能的原因?该怎么验证?
答案
lr 太小——LoRA 要从 \(B = 0\) 长出 \(\Delta W\),需要比全量大 10–20 倍的 lr。验证:看训练 loss 曲线是否几乎不降;把 lr 提到 \(10^{-4}\) 再跑同样步数对比。
-
PiSSA 训完直接
save_pretrained发布 adapter,用户装到 Hugging Face 上的原始 Qwen2.5-0.5B 上。会怎样?答案
错。PiSSA 的 adapter 是对着改过的 \(W_{res} = W - B_0 A_0\) 学的;装到原始 \(W\) 上等于多加了一份 \(B_0 A_0\)(\(W\) 的主奇异成分),模型会明显变坏。要用
path_initial_model_for_weight_conversion转成对原始 \(W\) 的普通 adapter(秩变 \(2r\))。 -
NF4 一个块 64 个权重,绝对值最大 0.12;某个权重是 \(-0.05\)。它被量化成哪个格点?反量化后是多少?双重量化后每参数的存储是多少 bit?
答案
\(-0.05 / 0.12 = -0.4167\),最近的格点是 \(-0.3949\)(比 \(-0.5251\) 近),反量化 \(-0.3949 \times 0.12 = -0.0474\),误差 0.0026。存储 \(4 + 0.127 = 4.127\) bit / 参数。
-
QLoRA 训练一步比 LoRA 慢,慢在哪?DoRA 一步也比 LoRA 慢,慢在哪?两者哪个更慢、为什么?
答案
QLoRA 慢在每个线性层前向与反向都要把 NF4 块反量化成 BF16(带宽操作,一个额外 kernel),约慢 30–50%。DoRA 慢在每步要显式构造 \(W + BA\) 并算列范数,是一个 \(d_{out} \times d_{in}\) 的完整矩阵操作,相当于每个矩阵多算一次 \(BA\) 的展开,通常比 QLoRA 的反量化更贵。
下一篇
配置定了、训完了,手里是一个几十 MB 的 adapter 目录。第三篇回答它怎么存、怎么装回去、合并前后差多少、换量化底座差多少、多个 adapter 怎么切换与合成、一个底座服务几十个客户的账怎么算、DPO 为什么不用再放参考模型、新 token 为什么学不会。
系列 《LoRA 专题:SFT 的默认微调方式》 第 2 / 4 篇
系列总览 — 为什么这样组织、读它需要什么、读完能做什么
- 低秩假设:为什么两个瘦矩阵够用,以及它省了哪几本账
- 选参:r、target_modules、alpha、lr 与 QLoRA / DoRA / PiSSA 各让模型变成什么
- 工程:adapter 文件、合并、多 LoRA 服务与参考模型
- 系列总结与通关自测
上一篇:低秩假设:为什么两个瘦矩阵够用,以及它省了哪几本账下一篇:工程:adapter 文件、合并、多 LoRA 服务与参考模型
本文由 arganzheng 创作,采用 CC BY 4.0 许可协议。在保留原文作者、署名以及完整原文链接(https://arganzheng.life/lora-hyperparameters-rank-targets-alpha-lr-and-variants.html)的前提下,欢迎各种形式的转载、翻译或商业引用。
COMMENTS
评论存放在 GitHub Discussions, 用 GitHub 账号登录即可发表,支持 Markdown。 想针对正文某句话说?选中那段文字,点浮出的「评论」即可划线评论;觉得哪里写错了,发表时勾上「同时提交 Issue」。 有人回复你时 GitHub 会按你的通知设置发邮件,不用守在这里。