LoRA(Low-Rank Adaptation)自从被提出以来,几乎是参数高效微调领域的默认选择。但随着模型规模从七十亿一路飙升到上千亿,社区很快发现传统LoRA也存在改进空间:可训练参数能不能再压一压?推理时几百个适配器能不能塞进一张卡?训练收敛慢、效果不及全量微调的问题能不能缓解?围绕这三个问题,VeRA、S-LoRA和LoRA+分别给出了各自的答案。本文将逐一拆解这三项技术的核心思路,并附上可直接运行的代码示例。

VeRA:用共享冻结矩阵把参数量再砍一个数量级
先回顾一下经典LoRA的做法。对于预训练权重矩阵 W,LoRA不直接修改它,而是引入两个低秩矩阵 A(形状为 r×d)和 B(形状为 d×r),前向传播时计算 Wx + BAx,训练时只更新 A 和 B。当秩 r 取 8 或 16 时,可训练参数已经比全量微调少了几个数量级,但VeRA的作者认为这还不够。
VeRA(Vector-based Random Matrix Adaptation)的关键洞察是:低秩矩阵本身甚至可以不训练。它为模型中所有被适配的层共享同一对随机初始化并冻结的矩阵 A 和 B,在每层只引入两个可训练的缩放向量 b 和 d,前向计算变为 Wx + Λd B Λb A x,其中 Λ 表示对角缩放。这样一来,参数量从原本的 2×d×r 下降到 2×d,对于大模型而言通常能压缩到LoRA的百分之一左右。
听起来有点反直觉:随机矩阵冻结不动,只靠两个对角向量就能学到东西?实验结果给出了肯定答案。在GLUE基准和指令微调任务上,VeRA用极少的参数达到了与LoRA接近的效果。其背后原因在于,共享的随机矩阵相当于把输入投影到一个固定的随机特征空间,而对角向量负责逐维度加权,这种结构类似随机特征映射,表达能力足以覆盖常见的适配需求。
在HuggingFace的PEFT库中启用VeRA非常简单:
from peft import VeraConfig, get_peft_model
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
config = VeraConfig(
r=256, # 共享矩阵的秩,通常取得比LoRA更大
target_modules=["q_proj", "v_proj"],
vera_dropout=0.05,
)
model = get_peft_model(model, config)
model.print_trainable_parameters()
# 输出示例:trainable params: 142,336 || all params: 6,742,609,920 || trainable%: 0.002需要注意的是,VeRA对秩的敏感度与LoRA不同,由于随机矩阵被冻结,通常需要更大的 r(如128或256)来保证随机特征空间的多样性。此外,当目标任务与预训练分布差异较大时,VeRA的效果可能略逊于LoRA,此时需要在参数量和精度之间做权衡。
S-LoRA:让一张GPU同时服务上千个适配器
S-LoRA解决的是另一个维度的问题:推理侧的多租户服务。想象一个平台场景,成千上万个用户各自用私有数据微调出了自己的LoRA适配器,请求到来时需要动态选择对应的适配器进行推理。如果每个适配器都独立加载,显存和切换开销会迅速失控。
S-LoRA(Serving Thousands of Concurrent LoRA Adapters)提出了三项关键技术。第一是统一分页(Unified Paging):把所有适配器的权重放入统一的内存池管理,按需在CPU和GPU之间换入换出,避免显存碎片。第二是SVD分解改造:将LoRA的 BA 计算拆分为针对非零奇异值的部分,使得不同秩的适配器可以共享计算内核。第三是动态张量级批处理:不同请求可以携带不同适配器,在同一个batch内完成混合计算。
工程实现上,S-LoRA基于vLLM和RadixAttention构建。核心思路是为每个适配器维护一个索引表,注意力计算时根据请求路由到对应的适配器分支。下面是一个简化版的多适配器调度示意:
import torch
class MultiAdapterRouter:
def __init__(self, base_model, adapters: dict):
self.base = base_model # 冻结的基座模型
self.adapters = adapters # {adapter_id: (A, B)}
@torch.no_grad()
def forward(self, x, adapter_ids):
# 先走基座模型的主干
base_out = self.base.backbone(x)
outs = []
# 按请求维度应用各自适配器的增量计算
for i, aid in enumerate(adapter_ids):
A, B = self.adapters[aid]
xi = x[i].unsqueeze(0)
outs.append((B @ A @ xi.transpose(-1, -2)).transpose(-1, -2))
return base_out + torch.cat(outs, dim=0)官方报告的数据相当惊人:在单张A100 GPU(80GB)上,S-LoRA可以同时服务多达2000个适配器,吞吐量比朴素方案提升数倍,且延迟增长非常有限。对于构建多租户大模型服务平台、AIGC应用后端来说,S-LoRA几乎是必选项。如果你的场景只涉及单个适配器,就没有必要引入这套复杂度,标准的vLLM加LoRA支持已经足够。
LoRA+:一个学习率技巧带来的收敛提升
LoRA+关注的是训练效果本身。实践中经常有人抱怨:LoRA微调出来的模型和全量微调相比总差一口气,尤其在复杂指令跟随任务上。LoRA+的作者从优化动力学的角度分析了原因:LoRA把 A 和 B 放在同一个参数组里,使用相同的学习率,但这两个矩阵的角色并不对称。
具体来说,初始化时通常 B 为零、A 采用高斯初始化,反向传播中 B 的梯度依赖于 A 的当前值,而 A 的梯度依赖于 B。在训练早期 B 接近零,导致 A 的有效更新量很小,形成所谓的慢学习现象。理论分析表明,要让 A 和 B 以匹配的速率学习,二者需要不同的学习率,且 B 的学习率应显著高于 A。
LoRA+的做法因此极其简单:给 B 矩阵设置一个更大的学习率,系数记为 λ(论文建议在 2 到 16 之间搜索)。在PEFT库中的实现方式如下:
from peft import LoraConfig, get_peft_model
config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
# 关键参数:B矩阵相对学习率倍数
loraplus_lr_ratio=8,
loraplus_lr_embedding=1e-6,
)
model = get_peft_model(base_model, config)
# 训练循环中记得使用分组学习率设置
from transformers import TrainingArguments
args = TrainingArguments(
output_dir="./output",
learning_rate=1e-4,
per_device_train_batch_size=8,
)实验显示,LoRA+在GLUE和指令微调任务上以相同的训练步数获得了更高的准确率,或者在达到相同效果时节省可观的计算量。它的好处是几乎零成本:不改模型结构、不增加参数、不改推理流程,只是优化器多了一个参数组。如果你正在用LoRA做微调且觉得效果不满意,LoRA+应该是第一个尝试的改进项。
三项技术怎么选:一张表看懂定位差异
这三种技术解决的是完全不同的问题,并不互斥。可以从三个维度来决策:训练参数预算、服务并发规模、收敛质量要求。
| 技术 | 核心思路 | 典型收益 | 适用场景 |
|---|---|---|---|
| VeRA | 共享冻结随机矩阵加对角缩放向量 | 参数量降至LoRA的约百分之一 | 极端显存受限、多任务需要大量轻量适配器 |
| S-LoRA | 统一分页与异构批处理 | 单卡服务数千适配器,高吞吐 | 多租户推理平台、AIGC后端服务 |
| LoRA+ | A与B矩阵差异化学习率 | 同算力下更高精度或更快收敛 | 任何LoRA训练任务的即插即用增强 |
| 经典LoRA | 低秩增量矩阵 | 平衡的效果与通用性 | 默认基线方案 |
实践中常见的组合方式是:训练阶段使用LoRA+提升适配器质量,如果显存极度紧张则改用VeRA;服务阶段如果适配器数量超过几十个,就引入S-LoRA的调度策略。三者分别作用于微调生命周期的不同环节,理解各自解决的问题比记住公式更重要。参数高效微调这条技术路线还在快速演化,但底层的取舍逻辑始终是显存、效果与吞吐三者之间的博弈,抓住这一点,后续出现的新方法也能很快看透其本质。