更新 @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。

r = 4 / 16 / 64(都是 all-linear、α = 2r 即 scaling 2、lr 1e-4)与全量的对照(Qwen2.5-0.5B,no_robots 800 条,80 步 × batch 4 × 512,CPU;训练前验证回复 loss 2.4936、普通文本 loss 2.8748)
配置 可训练参数 训练 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)。

target_modules:只挂 attention 对全部线性层,与”参数同样翻 4 倍”的 r = 64 并排(Qwen2.5-0.5B,no_robots 800 条,80 步 × batch 4 × 512,CPU;训练前验证回复 loss 2.4936、普通文本 loss 2.8748)
配置 可训练参数 训练 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}\)。

r = 64 上三种缩放:α = 128(scaling 2)、α = 32(scaling 0.5)、α = 32 + rsLoRA(scaling 4)(Qwen2.5-0.5B,no_robots 800 条,80 步 × batch 4 × 512,CPU;训练前验证回复 loss 2.4936、普通文本 loss 2.8748)
配置 可训练参数 训练 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}\):

LoRA r = 16 all-linear 的 lr 扫描:1e-5 / 1e-4 / 1e-3,与全量 lr 1e-5 并排(Qwen2.5-0.5B,no_robots 800 条,80 步 × batch 4 × 512,CPU;训练前验证回复 loss 2.4936、普通文本 loss 2.8748)
配置 可训练参数 训练 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

四条训练 loss 曲线:lr 1e-5 的 LoRA 明显落后,1e-4 与 1e-3 的 LoRA 和全量 1e-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\):

LoRA+(B 的 lr 是 A 的 4 倍)对普通 LoRA,其余配置相同(Qwen2.5-0.5B,no_robots 800 条,80 步 × batch 4 × 512,CPU;训练前验证回复 loss 2.4936、普通文本 loss 2.8748)
配置 可训练参数 训练 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\) 或数据来选初始方向:

LoRA 的初始化家族
方法 \(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):

PiSSA 初始化(init_lora_weights=”pissa_niter_4”)对默认初始化,其余配置相同(Qwen2.5-0.5B,no_robots 800 条,80 步 × batch 4 × 512,CPU;训练前验证回复 loss 2.4936、普通文本 loss 2.8748)
配置 可训练参数 训练 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 上大矩阵的范数计算相对更贵)。

DoRA(use_dora=True)对普通 LoRA,其余配置相同(Qwen2.5-0.5B,no_robots 800 条,80 步 × batch 4 × 512,CPU;训练前验证回复 loss 2.4936、普通文本 loss 2.8748)
配置 可训练参数 训练 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):

QLoRA NF4 与 FP32 LoRA 的同配置对照(变化均相对训练前的 FP32 底座 2.4936 / 2.8748)
配置 训练 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 十三种配置并排

十三种配置并排(Qwen2.5-0.5B,no_robots 800 条,80 步 × batch 4 × 512,8 线程 CPU;训练前验证回复 loss 2.4936、普通文本 loss 2.8748。QLoRA 一行的”普通文本 loss 变化”里含底座本身的 NF4 量化误差,见第八节)
配置 可训练参数 训练 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

十三种配置的验证回复 loss 与遗忘并排:验证 loss 上 r=16 all-linear、r=64、DoRA 与全量挤在 2.394 附近,遗忘随 scaling、lr、r 单调变大

9.2 按任务与资源选

任务与资源 → LoRA 配置
场景 建议 依据
默认起点(指令 / 格式 / 风格 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%),精度几乎不掉。

十一、自测

  1. 从 \(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\)。

  2. 同样 8.8M 可训练参数的预算,A 方案 \(r = 16\) 挂全部七个矩阵,B 方案 \(r = 64\) 只挂 q k v o。哪个好?为什么?

    答案

    A。\(r = 64\) attention-only 是 8.65M,参数量相近,但第三节的对照显示挂全部线性层的收益大于加 \(r\)(QLoRA 论文的同一结论):MLP 承载大部分变换,只改注意力改不到。

  3. 有人用 lr \(10^{-5}\) 训 LoRA 80 步说”LoRA 不 work”。最可能的原因?该怎么验证?

    答案

    lr 太小——LoRA 要从 \(B = 0\) 长出 \(\Delta W\),需要比全量大 10–20 倍的 lr。验证:看训练 loss 曲线是否几乎不降;把 lr 提到 \(10^{-4}\) 再跑同样步数对比。

  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\))。

  5. 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 / 参数。

  6. 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 为什么学不会。

这篇对你有用?

本文由 arganzheng 创作,采用 CC BY 4.0 许可协议。在保留原文作者、署名以及完整原文链接(https://arganzheng.life/lora-hyperparameters-rank-targets-alpha-lr-and-variants.html)的前提下,欢迎各种形式的转载、翻译或商业引用。


COMMENTS

评论存放在 GitHub Discussions, 用 GitHub 账号登录即可发表,支持 Markdown。 想针对正文某句话说?选中那段文字,点浮出的「评论」即可划线评论;觉得哪里写错了,发表时勾上「同时提交 Issue」。 有人回复你时 GitHub 会按你的通知设置发邮件,不用守在这里。

×