LoRA 适配器家族 — 从原版到新秀

📅 2026/7/23 23:04:17 👁️ 阅读次数 📝 编程学习
LoRA 适配器家族 — 从原版到新秀

1. 原版 LoRA:一切的起点

先花一分钟复习原版 LoRA,后面所有变体都是在这个基础上改的。

全参数微调的问题是:一个 7B 模型,全量训练要 7B × 4 bytes × 各种优化器状态 ≈ 56GB+ 显存。普通人根本跑不动。LoRA 的洞察是:微调时的权重更新矩阵 ΔW 虽然是满秩的,但它可以被压缩成一个低秩矩阵,因为微调不需要改那么多自由度。

ΔW = A × B     where   A ∈ ℝd×r, B ∈ ℝr×k,   r ≪ min(d, k)

前向传播时:h = W₀x + BAx,W₀ 冻住不动,只训 A 和 B。r=16 时,参数量只有原来的 16/4096 ≈ 0.4%。

# 原版 LoRA 的核心逻辑(伪代码)
class LoRALinear(nn.Module):def __init__(self, in_dim, out_dim, r=16, alpha=32):self.W = frozen(original_weight)          # 冻住的原始权重self.A = nn.Linear(in_dim, r, bias=False)  # 降维矩阵self.B = nn.Linear(r, out_dim, bias=False) # 升维矩阵self.scale = alpha / r                     # 缩放因子nn.init.kaiming_uniform_(self.A.weight)nn.init.zeros_(self.B.weight)              # B 初始化为零def forward(self, x):return self.W(x) + self.scale * self.B(self.A(x))

原版的三个设定(后面变体都会动它们)

① 低秩分解 r

所有层共用同一个 rank。r=8 省参但弱,r=64 强但参数多。原版 r=8~16 是甜点。

② 初始化方式

A 用 Kaiming 均匀分布随机初始化,B 初始化为零矩阵。这样一开始 ΔW=0,模型行为就是原始模型。

③ 统一学习率

A 和 B 用同一个 lr。缩放因子 alpha/r 作为等效学习率的调节器。

一句话记住 LoRA:冻住原模型,只训两个小矩阵。推理时 BA 可以直接合并回 W,不增加推理延迟。

2. QLoRA:量化 + LoRA,省到极致

省显存   2023, Dettmers et al.

QLoRA 的思路简单粗暴:LoRA 已经让可训参数很少了,但冻结的原始参数还是占显存(7B 模型 bf16 = 14GB)。那把这些冻结参数压缩成 4-bit,不就更省了吗?

W₀ 存在显存里:   bf16 → NF4 (4-bit)   → 显存直接 ÷4

但有个问题:4-bit 精度太低,直接算前向传播误差大。QLoRA 的解决方案是双重量化(Double Quantization):不光量化权重本身,连量化的缩放因子也量化一次。

from transformers import BitsAndBytesConfig
from peft import LoraConfig, get_peft_model# QLoRA 配置(这就是你 DPO 脚本里用的)
bnb_config = BitsAndBytesConfig(load_in_4bit=True,                    # 4-bit 量化基座bnb_4bit_quant_type="nf4",           # NormalFloat4,比普通 INT4 更好bnb_4bit_compute_dtype=torch.bfloat16, # 反量化回 bf16 计算,保持精度bnb_4bit_use_double_quant=True,       # 双重量化,再省 0.4 bit/param
)model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b",quantization_config=bnb_config,device_map="auto",
)lora_config = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj", "v_proj"])
model = get_peft_model(model, lora_config)

效果和代价

  全参数 bf16 LoRA bf16 QLoRA NF4
7B 模型显存 ~56 GB ~16 GB ~6 GB
可训参数量 7B ~4M (r=16) ~4M (r=16)
训练速度 基准 ~1.5× 快 ~0.8×(反量化有开销)
效果 最佳 接近全参数 和 LoRA 几乎无差别
一句话:QLoRA 让你在单张 24GB 显卡上微调 65B 模型。论文验证了 NF4 量化 + LoRA 的效果和全参数 bf16 微调差距在 1% 以内。这就是为什么你看到的大部分开源微调都用 QLoRA——不是不想用全参数,是实在跑不动。

3. DoRA:把权重拆成方向和大小

提效果   2024, Liu et al. (ICLR 2024 Oral)

原版 LoRA 把 ΔW 整个压成低秩。DoRA 说:别着急压,先把权重矩阵拆成"方向"和"大小"两部分——方向部分用 LoRA 更新,大小部分直接训一个标量。

W = m · (W₀ + B·A) / ||W₀ + B·A||

m: 大小(magnitude),逐通道的标量
W₀ + BA: 方向(direction),单位化后只改方向

这个设计的直觉来自对全参数微调的分析:论文发现全参数微调时,权重的方向变化很大,但大小变化很小。LoRA 同时改了方向和大小(耦合在一起),而 DoRA 把两者解耦,各管各的——效果更好。

# DoRA 的核心逻辑(简化版)
class DoRALinear(nn.Module):def __init__(self, in_dim, out_dim, r=16):self.W = frozen(original_weight)self.A = nn.Linear(in_dim, r, bias=False)self.B = nn.Linear(r, out_dim, bias=False)self.m = nn.Parameter(torch.ones(1, out_dim))  # ★ 新增:大小向量def forward(self, x):delta_W = self.B(self.A(x))           # LoRA 更新merged_W = self.W + delta_W           # 合并后权重# 拆成方向 + 大小direction = F.normalize(merged_W, dim=1)  # 单位化 = 纯方向return self.m * direction @ x             # ★ 大小 × 方向

和 LoRA 的关键区别

  LoRA DoRA
更新方式 ΔW = BA,直接加到 W₀ 先拆成方向+大小,方向和大小各更新各的
可训参数 A, B A, B, m(多了极少参数)
效果 基准 普遍 +0.5~1.5 个点(NLU/NLG 任务)
兼容性 可以叠 QLoRA(DoRA + 4bit)
类比:LoRA = 直接改一把尺子的刻度。DoRA = 把尺子拆成"方向"(刻度的排列方式)和"长度"(尺子本身多长),分别调整。改方向比改长度对测量结果的影响大得多,所以 LoRA 的效率浪费在了改一些不重要的"大小"变化上。
一句话:DoRA 是 LoRA 目前效果最好的升级版。多几个参数,涨 0.5~1.5 个点。如果你追求极致效果,DoRA 是首选。PEFT 库已原生支持,LoraConfig(use_dora=True) 一行搞定。

4. AdaLoRA:重要的层给更多 rank

提效果   2023, Zhang et al.

原版 LoRA 一个很蠢的地方:所有层都用同一个 rank。但实际上不是每层都同等重要——有些层对最终任务贡献大,该多分参数;有些层贡献小,r=2 就够了。

AdaLoRA 的做法:把总参数量预算固定,然后自动把预算分配给更重要的层。它会动态评估每个权重矩阵的 SVD 奇异值,大的奇异值对应的方向重要,保留;小的奇异值不重要,裁剪掉。

# AdaLoRA 的核心思路(简化)
# 不是 A×B,而是 P×Λ×Q —— 可微的 SVD 形式
class AdaLoRALinear(nn.Module):def __init__(self, in_dim, out_dim, init_r=16):# P: 左奇异向量,Λ: 奇异值(对角),Q: 右奇异向量self.P = nn.Parameter(torch.randn(out_dim, init_r))self.Q = nn.Parameter(torch.randn(init_r, in_dim))self.Lambda = nn.Parameter(torch.ones(init_r))  # ★ 可学习的奇异值def forward(self, x):# 根据奇异值大小动态分配 rankdelta_W = self.P @ torch.diag(self.Lambda) @ self.Qreturn frozen_W(x) + delta_W @ x

关键机制:训练过程中,对奇异值 Λ 加 L1 正则,逼着不重要的奇异值趋向零。然后定期把接近零的奇异值删掉,释放 rank 预算给别的层。

和 LoRA 的区别

  LoRA AdaLoRA
rank 分配 所有层统一 r 自动分配,重要的层 r 大
参数形式 A × B P × Λ × Q(可微 SVD)
总参数量 固定 固定(预算一样,只是分配方式不同)
额外开销 有(奇异值正则化 + 周期性剪枝)
一句话:AdaLoRA 在同等参数量下效果比 LoRA 好,因为钱花在刀刃上了。但实现复杂度高,训练有额外开销。适合那种"我就要在同参数预算下榨出每一滴性能"的场景。

5. LoRA+:A 和 B 用不同的学习率

收敛更快   2024, Hayou et al.

这篇论文发现了一个非常微妙的东西:原版 LoRA 里 A 和 B 用同一个学习率,会导致训练初期 B 更新太慢。

原因:A 是随机初始化的,B 是零初始化的。反向传播时,B 的梯度包含了 A 的随机噪声,而 A 的梯度更直接。这就导致 B 学得比 A 慢——但 LoRA 的效果需要 A 和 B 配合才能体现。

LoRA+ 的解决方式简单到让人想翻白眼:给 B 一个比 A 大的学习率。

# 原版 LoRA:A 和 B 统一 lr
optimizer = AdamW([A.parameters(), B.parameters()], lr=1e-4)# LoRA+:B 的 lr 是 A 的 λ 倍(λ > 1)
optimizer = AdamW([{"params": A.parameters(), "lr": 1e-4},{"params": B.parameters(), "lr": 1e-4 * 16},  # ★ B 的 lr 大 16 倍
])

论文推荐的 λ = 2^4 = 16(即 B 的 lr 是 A 的 16 倍)。实验显示这个比例在各种任务上都比较稳。你也可以搜一下 λ,范围 4~32 差别不大。

一句话:LoRA+ 是"零成本改进"——不改模型结构,不增参数,不改初始化,只把 B 的 lr 调大。论文验证了它能快 2 倍收敛,最终效果也有微弱提升。PEFT 库已支持:LoraConfig(loraplus_lr_ratio=16)

6. PiSSA:用 SVD 初始化,少走弯路

收敛更快   2024, Meng et al.

原版 LoRA 的初始化是:A 随机,B 全零 → 一开始 ΔW=0,从原始模型开始学。听起来很合理——不要一上来就改了模型的行为。

但 PiSSA 说:你把原始权重的"主成分"提取出来当作 LoRA 的初始化,效果更好。因为微调本质就是"在原始权重的主方向上做微调"——那为什么不直接把主方向放在 LoRA 里,让原始权重变成残差呢?

# PiSSA 的初始化流程
import torchdef pissa_init(W, r):# 1. 对原始权重做 SVDU, S, Vt = torch.linalg.svd(W.float(), full_matrices=False)# 2. 取前 r 个奇异值对应的主成分作为 LoRA 的初始化A_init = torch.diag(torch.sqrt(S[:r])) @ Vt[:r, :]   # 右奇异向量B_init = U[:, :r] @ torch.diag(torch.sqrt(S[:r]))    # 左奇异向量# 3. 原始权重减去主成分,变成"残差"W_residual = W - B_init @ A_initreturn A_init, B_init, W_residual# 使用时
A.weight.data = A_init
B.weight.data = B_init
original_W = W_residual  # ★ 被减掉主成分后冻结

PiSSA vs 原版 LoRA 的初始化差异

  原版 LoRA PiSSA
A 初始化 Kaiming 随机 从 W 的 SVD 主成分来
B 初始化 全零 从 W 的 SVD 主成分来
ΔW 初始值 0(模型行为不变) ≈ W 的主成分(模型行为基本不变)
冻结的 W 完整 W₀ W₀ - 主成分(残差)
收敛速度 基准 快 2~5 倍
类比:原版 LoRA = 从空白的黑板开始写微调的内容。PiSSA = 先在黑板上把原始模型里最重要的那些内容抄上去当草稿,然后在此基础上修改。你本来就要往那个方向改,起点自然近得多。
一句话:PiSSA 不改结构,只改初始化。收敛速度 2~5 倍提升,最终效果和原版持平或略好。代价是初始化时多做一次 SVD(只做一次,不贵)。PEFT 库已支持:LoraConfig(init_lora_weights="pissa")。另外一个变体叫 MiLoRA(取 SVD 的 minor components 而不是 principal),思路相反但同样有效。

7. VeRA:极致省参,共享随机矩阵

极限省参   2024, Kopiczko et al.

前面所有 LoRA 变体的可训参数都是 O(d×r + r×k),随 rank r 线性增长。VeRA 想做到O(d + k)——几乎不随 rank 增长。

怎么做到的?不训 A 和 B 这两个大矩阵。而是用一对冻结的随机矩阵 + 两个很小的可训缩放向量。

ΔW = Λ_b · Bfrozen · Λ_d · Afrozen

Afrozen: 冻结的随机高斯矩阵(所有层共享同一对)
Bfrozen: 冻结的随机高斯矩阵(所有层共享同一对)
Λ_d, Λ_b: 两个可训的对角缩放矩阵(仅 O(d+k) 参数)
# VeRA 的核心逻辑
class VeRALinear(nn.Module):def __init__(self, in_dim, out_dim, r=256):  # r 可以设很大!self.W = frozen(original_weight)# 冻结的共享随机矩阵——所有 VeRA 层共用同一对!if not hasattr(VeRALinear, 'A_shared'):VeRALinear.A_shared = frozen(torch.randn(r, in_dim))VeRALinear.B_shared = frozen(torch.randn(out_dim, r))# 唯一可训的参数:两个极小的缩放向量self.Lambda_d = nn.Parameter(torch.ones(r))      # r 个参数self.Lambda_b = nn.Parameter(torch.ones(out_dim)) # out_dim 个参数def forward(self, x):# B_shared @ diag(Lambda_d) @ A_shared: 全是冻结的delta = VeRALinear.B_shared @ torch.diag(self.Lambda_d) @ VeRALinear.A_sharedreturn self.W(x) + torch.diag(self.Lambda_b) @ delta @ x

参数量对比

  LoRA (r=16) VeRA (r=256) 节约
每层参数量 d×r + r×k ≈ 128K d + k + r ≈ 10K ~92%
70B 模型总参数量 ~160M ~10M ~94%
可训参数占比 0.2% 0.01% 20×
代价:VeRA 虽然参数量极少,但共享随机矩阵意味着所有层的 ΔW 来自同一个低维子空间——表达能力受限。实验显示 VeRA 的效果比同 rank 的 LoRA 差 1~3 个点,但 r=256 的 VeRA 可以接近 r=16 的 LoRA。适合极度显存受限的场景(比如边缘设备)。
一句话:VeRA 把 LoRA 的参数效率推到极限——7B 模型只需 1MB 的可训参数。效果有折损,但换来了 94% 的参数节省。适合移动端、IoT 等极端场景。

8. 全家福对比 & 怎么选

一表看懂所有变体

方法 年份 核心改动 可训参数 效果 vs LoRA 收敛速度 PEFT 支持
LoRA 2021 低秩分解 ΔW=BA 基准 基准 基准
QLoRA 2023 4bit 量化冻结参数 同 LoRA 几乎持平 基准
DoRA 2024 权重分解为方向+大小 +0.01% +0.5~1.5 基准
AdaLoRA 2023 动态分配 rank 同 LoRA +0.3~0.8 略慢
LoRA+ 2024 A和B不同 lr 同 LoRA 微弱提升 ~2×
PiSSA 2024 SVD 初始化 同 LoRA 持平或微好 2~5×
VeRA 2024 共享冻结矩阵+缩放向量 −94% −1~3 个点 基准

怎么选——按你的需求对号入座

🏠 显存紧张

首选 QLoRA。4bit 量化让 7B 模型跑在 6GB 显存上。还不够就 QLoRA + DoRA 叠 buff。

🎯 追求效果(发论文)

DoRA 是目前效果天花板。加 LoRA+(B 的 lr 调大)可以更快收敛。不差显存的话 DoRA + 全参数 bf16。

⚡ 赶时间(快速迭代)

PiSSA 收敛快 2~5 倍 + LoRA+ 再快 2 倍,叠加起来可能快一个数量级。初期实验验证想法极快。

🔬 固定预算压榨性能

AdaLoRA。同等总参数量但按需分配,钱花在刀刃上。适合"我就这么多参数量了,帮我榨出来最优效果"。

📱 边缘设备 / 极致省参

VeRA。94% 参数节省,1MB 微调 7B 模型。效果有折损,但在端侧场景这点折损可以接受。

🛡 最保险(生产环境)

QLoRA(NF4 + 双重量化)。最成熟、坑最少、社区支持最全。DoRA 虽好但还新,可以观望一两个版本再上生产。

可以叠加的组合(不互斥)

很多变体可以叠加使用——它们改的不是同一个东西:

# 最强组合(如果你不差显存)
from peft import LoraConfigconfig = LoraConfig(r=16,lora_alpha=32,use_dora=True,              # DoRA: 方向+大小分解init_lora_weights="pissa",  # PiSSA: SVD 初始化loraplus_lr_ratio=16,       # LoRA+: B 的 lr 大 16 倍
)
# 再加 QLoRA 的 BitsAndBytesConfig 叠量化
注意:AdaLoRA 和 VeRA 不能和上面的叠加——它们的结构改动太大,不是"关掉就是原版 LoRA"那么简单。PEFT 库目前不支持 AdaLoRA+DoRA 这种组合。
总结:QLoRA 让你跑得动,DoRA 让你跑得好,PiSSA+LoRA+ 让你跑得快,VeRA 让你哪儿都能跑。