一张卡也能微调大模型?LoRA+Adapter+Prefix+Prompt四种方案全讲透(附代码)
📢 本文是 「108张AI知识卡片·大模型通关手册」 系列第 11 篇。上一篇打开了 GPT 的"胸腔"看架构组件,这篇回到工程落地:大模型训练完了,怎么让它适配你的业务?全量微调太贵,PEFT(参数高效微调)用 1% 的参数量达到 90% 的效果——一张卡也能跑。
目录
- TL;DR 太长不看
- 一、LoRA:低秩适配,微调界的性价比之王
- 二、Adapter Tuning:层间插入小适配器
- 三、Prefix Tuning:给输入加一段可学习前缀
- 四、Prompt Tuning:极简Embedding,能跑就行
- 五、一张图串起PEFT选型链路
- 六、代码:LoRA 微调实战
- 写到最后
- 系列导航 & 持续更新
TL;DR 太长不看
⚡ 30 秒版:先记这 4 条,细节往下翻。
- 🔴 LoRA:低秩适配——在原模型旁加两个小矩阵(A×B),只训小矩阵不碰原模型,参数量<1%,效果接近全量微调,还能合并回原模型零推理延迟。
- 🟠 Adapter Tuning:层间插入小适配器——每个 Transformer 层加一个"旁路",只训适配器,但推理多了一步前向计算,有延迟开销。
- 🟡 Prefix Tuning:在输入前面加一段可学习的"虚拟前缀"——不改模型参数,只学前缀,但前缀占用了上下文窗口。
- 🟢 Prompt Tuning:比 Prefix 更轻量——只学一组 Embedding 向量,极简但性能也有限,适合数据量特别少的场景。
- 🎁 选型口诀:追求效果→LoRA,多任务切换→Adapter,不改参数→Prefix,数据极少→Prompt。
一、LoRA:低秩适配,微调界的性价比之王

全量微调 LLaMA-7B 要更新 70 亿参数——一张 24G 的 4090 根本装不下。LoRA 的思路是:原模型不动,旁边加两个小矩阵,只训小矩阵。参数量不到原模型的 1%,效果却接近全量微调——这就是"低秩适配"的魔法。
LoRA(Low-Rank Adaptation)解决的核心问题:全量微调太贵。大模型有几十亿参数,全量微调要更新所有参数,显存和算力需求巨大。LoRA 的核心洞察:模型微调时的参数变化是"低秩"的——不需要更新所有参数,只需要在一个低维子空间里调整就够了。
它到底在干嘛(机制层):LoRA 在原模型的权重矩阵 W 旁边加了一个"旁路":ΔW = A × B。A:形状为 d×r 的矩阵(d 是原模型维度,r 是 LoRA 秩,通常 r=4~64)。B:形状为 r×d 的矩阵。ΔW = A×B:形状为 d×d,和原权重 W 一样大,但只用了 2×d×r 个参数(r 远小于 d,所以参数量极小)。训练时:原权重 W 冻结(不更新),只训练 A 和 B。推理时:把 ΔW = A×B 直接加到 W 上(W’ = W + A×B),零额外推理延迟——因为矩阵加法是 O(1) 操作。关键参数 秩 r:r 越大,表达能力越强,但参数量也越多。实验表明 r=16 在大多数任务上已经足够,r=4 在简单任务上就够用。
你能感受到什么(体感层):全量微调 LLaMA-7B:更新 70 亿参数,需要 4×A100(80G),训练 3 小时。LoRA 微调 LLaMA-7B(r=16):只更新约 2000 万参数(0.3%),一张 4090(24G)就能跑,训练 30 分钟。效果对比:在大多数 NLP 任务上,LoRA(r=16)的效果和全量微调差距在 1-3% 以内——用 0.3% 的参数量换 97% 的效果,性价比极高。
| 全量微调 | LoRA (r=16) | |
|---|---|---|
| 可训练参数 | 7B | ~20M (0.3%) |
| 显存需求 | 4×A100 | 1×4090 |
| 训练时间 | 3h | 30min |
| 效果 | 基准 | -1~3% |
| 推理延迟 | 基准 | 零额外延迟 |
🎛️ 动手感受:不同秩 r 的效果-参数量权衡
操作:同一个微调任务,分别用 r=4/r=16/r=64/全量微调,对比效果和训练参数量。
你会看到:
- r=4:参数量最少(~5M),效果比全量微调差 3-5%,简单任务够用。
- r=16:参数量适中(~20M),效果差 1-3%,大多数任务的最佳选择。
- r=64:参数量较多(~80M),效果接近全量微调,但参数优势开始缩小。
- 全量微调:效果基准,但资源需求是 LoRA 的 10-20 倍。
- 变化说明了什么:r=16 是"效果-参数量"的甜点——再增大 r,参数量线性增长,但效果提升递减。
🤔 想一想
LoRA 的"低秩假设"在什么情况下会失效?如果微调任务和预训练任务差异很大(如从通用文本到代码生成),参数变化可能不是低秩的——此时 LoRA 的效果会明显低于全量微调。LoRA 适合"微调"(在预训练基础上小幅调整),不适合"迁移学习"(跨领域大幅调整)。
🔗 顺着他想:LoRA 在原模型旁加了旁路,Adapter Tuning 也加了旁路——但方式不同:LoRA 加在权重上,Adapter 加在层间。哪个更好?取决于你的场景。
二、Adapter Tuning:层间插入小适配器

LoRA 把旁路加在权重矩阵上——Adapter Tuning 把旁路加在 Transformer 层之间。每个层插入一个小型 FFN(适配器),只训适配器不碰原模型。好处是多任务切换方便(换适配器就行),坏处是推理多了一步计算。
Adapter Tuning 解决的核心问题:多任务场景下,如何让一个模型服务多个任务。全量微调每个任务要存一份完整模型,太浪费。Adapter 的思路:原模型不动,每个任务训练一个小适配器——切换任务只需换适配器,模型只存一份。
它到底在干嘛(机制层):Adapter 在每个 Transformer 层的输出后插入一个小型 FFN。结构:下投影(d→r)→ 非线性激活(ReLU)→ 上投影(r→d)——本质是一个"瓶颈 FFN",和 LoRA 的 A×B 类似,但多了非线性激活。残差连接:Adapter 的输出加到原层输出上(y = x + Adapter(x)),保证不插入 Adapter 时模型行为不变。训练:原模型冻结,只训练 Adapter 的参数。多任务:每个任务一个 Adapter,推理时按任务加载对应 Adapter——模型主体只存一份,Adapter 体积很小(原模型的 1-5%)。和 LoRA 的关键区别:LoRA 是线性变换(A×B,无激活函数),Adapter 是非线性变换(有 ReLU)。非线性让 Adapter 表达能力更强,但也带来推理延迟——每个层多了一次 FFN 前向计算。
你能感受到什么(体感层):单任务:LoRA 和 Adapter 效果接近,但 LoRA 可以合并回原模型零延迟,Adapter 有额外延迟。多任务:3 个任务用全量微调→3 份完整模型(3×7B=21B 存储)。3 个任务用 Adapter→1 份模型+3 个小适配器(7B+3×0.2B=7.6B 存储),省 64% 存储。推理延迟:每个 Adapter 增加约 5-10% 的推理延迟——12 层 Transformer 每层都多一次 FFN,累积起来不可忽略。
🎛️ 动手感受:Adapter vs LoRA 的多任务切换
操作:同一个模型,分别用 LoRA 和 Adapter 做 3 个任务的微调,对比存储和切换成本。
你会看到:
- LoRA:每个任务存一对 A/B 矩阵,切换时重新加载 A/B 并合并到 W——切换需要几十毫秒。
- Adapter:每个任务存一个小 FFN,切换时加载对应 Adapter——切换也需几十毫秒,但推理多了延迟。
- 变化说明了什么:多任务场景两者存储优势相当,但 LoRA 推理零延迟,Adapter 有额外开销——单任务选 LoRA,多任务看延迟容忍度。
🤔 想一想
Adapter 的"层间插入"有个隐含假设:每层都需要适配。但有些任务可能只需要调整高层(语义层),低层(语法层)不需要动——此时给每层都加 Adapter 是浪费。更精细的方案是只给部分层加 Adapter,但"哪些层需要适配"需要实验确定,没有通用规则。
🔗 顺着他想:LoRA 改权重,Adapter 加层间——有没有不改模型参数的方案?Prefix Tuning 就是在输入端做文章,完全不碰模型。
三、Prefix Tuning:给输入加一段可学习前缀

LoRA 改权重,Adapter 加层——Prefix Tuning 更"轻":不改模型任何参数,只在输入前面加一段可学习的"虚拟前缀"。模型看到这段前缀就知道"你要我做什么任务",自己调整行为——像给模型贴了一张便签。
Prefix Tuning 解决的核心问题:不想改模型参数,又想让模型适配新任务。LoRA 和 Adapter 都要改模型内部(权重或层间),Prefix Tuning 只改输入——在输入前面加一段可学习的 Token 序列,引导模型朝目标方向生成。
它到底在干嘛(机制层):Prefix Tuning 在输入序列前面拼接一段"虚拟前缀"(Virtual Prefix)。前缀是什么:一段长度为 P 的可学习 Token 序列(P 通常 10-200),每个 Token 是一个 Embedding 向量。前缀怎么学:训练时冻结模型所有参数,只优化前缀的 Embedding——通过反向传播更新前缀,让模型在前缀引导下生成正确输出。前缀怎么用:推理时把学好的前缀拼在输入前面,模型看到前缀就知道"这是翻译任务"或"这是摘要任务"。关键设计:前缀只加在 Key 和 Value 上(不是 Query),这样前缀只影响注意力计算,不直接参与输出——更稳定、效果更好。前缀长度 P:P 越长,引导能力越强,但占用的上下文窗口越多。P=200 在大多数任务上已经饱和。
你能感受到什么(体感层):无前缀:模型不知道你要它做什么——“请翻译"和"请摘要"可能给出类似的结果。有前缀:模型看到学好的前缀,自动切换到"翻译模式"或"摘要模式”——不需要在 Prompt 里写"请翻译"。代价:前缀占用了上下文窗口——P=200 意味着可用上下文少了 200 个 Token。对于 4K 上下文的模型,200 个前缀不算多;但对于长文档场景,每一段 Token 都很珍贵。
🎛️ 动手感受:不同前缀长度的引导效果
操作:同一个任务(如翻译),分别用 P=10/P=50/P=200 的前缀,对比生成质量。
你会看到:
- P=10:前缀太短,引导不够,模型有时"忘记"要翻译,开始自由生成。
- P=50:引导基本够用,翻译质量明显提升,但偶尔有格式不一致。
- P=200:引导充分,翻译质量稳定,格式一致——但上下文窗口被占 200 个 Token。
- 变化说明了什么:前缀长度是"引导强度-上下文占用"的旋钮——P=50 是大多数任务的甜点。
🤔 想一想
Prefix Tuning 的前缀是在 Embedding 空间优化的——它不是人类可读的文本,而是一组"魔法向量"。这意味着你无法像看 Prompt 一样"看懂"前缀在引导什么。前缀是"黑盒引导"——有效但不可解释,调试和迭代比文本 Prompt 困难得多。
🔗 顺着他想:Prefix Tuning 学的是一段连续的 Token 序列——能不能更简单,只学一组 Embedding?Prompt Tuning 就是极简版。
四、Prompt Tuning:极简Embedding,能跑就行

Prefix Tuning 学一段前缀序列——Prompt Tuning 更极端:只学一组 Embedding 向量,连"序列"都不是。极简到什么程度?参数量可能只有几千个。但极简的代价是性能有限——数据量少时还行,数据量多时明显弱于 LoRA。
Prompt Tuning 解决的核心问题:在数据量极少的场景下,如何用最少的参数让模型适配新任务。Prompt Tuning 是 PEFT 家族里最轻量的方案——只学一组 Embedding,参数量可能只有几千到几万。
它到底在干嘛(机制层):Prompt Tuning 在输入的 Embedding 层加一组可学习的"软提示"(Soft Prompt)。和 Prefix Tuning 的区别:Prefix Tuning 的前缀是拼接在每一层的 Key/Value 上(影响每层注意力),Prompt Tuning 的软提示只加在输入层(只影响第一层)。结构:一组长度为 P 的 Embedding 向量,直接拼在输入 Embedding 前面——模型看到这些向量就像看到了"虚拟 Token"。训练:冻结模型所有参数,只优化这组 Embedding。参数量:P × d(P 是软提示长度,d 是 Embedding 维度)。例如 P=20, d=4096→参数量 81920——不到原模型的 0.001%。Prompt Tuning 的局限:只影响输入层,不像 Prefix Tuning 影响每层注意力——引导能力弱得多。模型规模小时效果差——在小于 10B 参数的模型上,Prompt Tuning 明显弱于 LoRA/Adapter。模型规模大时差距缩小——在 100B+ 参数的模型上,Prompt Tuning 和 LoRA 的效果差距缩小到 5% 以内(大模型本身够强,不需要太多引导)。
你能感受到什么(体感层):数据量 100 条:Prompt Tuning 和 LoRA 效果差距不大(数据太少,LoRA 也学不到多少)。数据量 1000 条:LoRA 开始拉开差距,Prompt Tuning 明显弱。数据量 10000 条:LoRA 效果接近全量微调,Prompt Tuning 差 10-15%。模型规模 100B+:Prompt Tuning 和 LoRA 差距缩小——大模型自己够强,软提示只需"点一下方向"。
| LoRA | Adapter | Prefix | Prompt | |
|---|---|---|---|---|
| 可训练参数 | ~20M | ~35M | ~800K | ~80K |
| 效果(7B模型) | 95% | 93% | 85% | 75% |
| 效果(100B模型) | 98% | 97% | 94% | 93% |
| 推理延迟 | 零 | +5-10% | 零 | 零 |
| 上下文占用 | 无 | 无 | +P tokens | +P tokens |
🎛️ 动手感受:Prompt Tuning 在不同模型规模上的表现
操作:同一个任务,分别在 7B 和 70B 模型上用 Prompt Tuning,对比效果。
你会看到:
- 7B 模型:Prompt Tuning 效果明显弱于 LoRA,差距 15-20%——小模型需要更强的引导。
- 70B 模型:Prompt Tuning 和 LoRA 差距缩小到 5% 以内——大模型自己够强,软提示够用。
- 变化说明了什么:Prompt Tuning 是"大模型的朋友"——模型越大,软提示的效果越好,因为模型本身的知识储备足够,只需轻轻引导。
🤔 想一想
Prompt Tuning 的"极简"在工程上有个隐藏优势:部署时只需存一组 Embedding 向量(几十 KB),而不是一份完整模型(几十 GB)。对于边缘设备或移动端场景,这个存储优势可能比效果差距更重要——用 5% 的效果损失换 1000 倍的存储节省,在资源受限场景下是值得的。
🔗 顺着他想:4 种 PEFT 方案各有侧重——怎么选?下一节一张图串起来。
五、一张图串起PEFT选型链路
4 种方案串成一条从"改权重"到"改输入"的 PEFT 选型链路:
PEFT选型决策树
│
├── 追求效果+效率? ──── LoRA
│ 参数少(0.3%)、效果好(95%)、零推理延迟
│
├── 需要多任务切换? ──── Adapter Tuning
│ 每任务一个适配器、切换方便、有推理延迟
│
├── 不想改模型参数? ──┬─ Prefix Tuning
│ │ 改输入、占上下文、引导较强
│ └─ Prompt Tuning
│ 极简Embedding、数据少时够用
│
└── 数据极少+模型大? ── Prompt Tuning
参数最少、大模型上效果接近LoRA
选型口诀:
- 效果优先→LoRA——性价比之王,大多数场景的首选。
- 多任务→Adapter——换适配器就行,模型只存一份。
- 不改参数→Prefix——在输入端做文章,模型完全不动。
- 数据极少→Prompt——极简方案,大模型上效果也不差。
六、代码:LoRA 微调实战
import torch
import torch.nn as nn
# ① LoRA 线性层
class LoRALinear(nn.Module):
def __init__(self, in_dim, out_dim, rank=16, alpha=32):
super().__init__()
self.original = nn.Linear(in_dim, out_dim, bias=False)
self.original.weight.requires_grad = False # 冻结原权重
self.lora_A = nn.Parameter(torch.randn(in_dim, rank) * 0.01)
self.lora_B = nn.Parameter(torch.zeros(rank, out_dim))
self.scaling = alpha / rank # 缩放因子
def forward(self, x):
# 原路径 + LoRA旁路
return self.original(x) + (x @ self.lora_A @ self.lora_B) * self.scaling
# ② 给Transformer层替换LoRA
def apply_lora_to_model(model, rank=16):
for name, module in model.named_modules():
if isinstance(module, nn.Linear) and "mlp" in name:
# 只替换MLP层的线性层(实际可按需选择)
parent_name = ".".join(name.split(".")[:-1])
child_name = name.split(".")[-1]
parent = model.get_submodule(parent_name)
lora_layer = LoRALinear(
module.in_features, module.out_features, rank=rank
)
lora_layer.original.weight.data = module.weight.data.clone()
setattr(parent, child_name, lora_layer)
return model
# ③ 合并LoRA权重(推理时零延迟)
def merge_lora_weights(model):
for name, module in model.named_modules():
if isinstance(module, LoRALinear):
# ΔW = A × B × scaling
delta_w = (module.lora_A @ module.lora_B).t() * module.scaling
module.original.weight.data += delta_w
# 删除LoRA旁路,只保留合并后的权重
setattr(module, "lora_A", None)
setattr(module, "lora_B", None)
# ④ 演示
dim, rank = 4096, 16
lora = LoRALinear(dim, dim, rank=rank)
x = torch.randn(1, 10, dim)
out = lora(x)
print(f"原模型参数: {dim*dim:,}")
print(f"LoRA参数: {2*dim*rank:,} ({2*dim*rank/(dim*dim)*100:.1f}%)")
print(f"输入: {x.shape} → 输出: {out.shape}")
这段代码实现了 LoRA 的核心逻辑:旁路加低秩矩阵(A×B)、冻结原权重、合并后零延迟推理。生产环境推荐直接用 HuggingFace PEFT 库:from peft import LoraConfig, get_peft_model——几行代码就能给任何 HuggingFace 模型加 LoRA。
写到最后
4 种 PEFT 方案,从 LoRA 的"改权重"到 Prompt Tuning 的"改输入",核心思路都是同一个:冻结大模型,只训小参数。LoRA 是性价比之王,Adapter 是多任务之友,Prefix 是不改参数的中间方案,Prompt 是极简主义的最后选择——没有银弹,只有最合适的。
下一篇我们讲大模型部署——模型微调好了,怎么从 Demo 上线到生产?部署、推理、量化、加速,一条链路走完。
如果你读下来觉得真有用:
- 👍 点个赞,让我知道 PEFT 选型对比这种写法值得继续;
- ⭐ 收藏起来,LoRA/Adapter/Prefix/Prompt 的对比表在微调选型时回来翻的概率很高;
- 💬 关注一下,下一篇"大模型部署篇"会讲部署/推理/量化/加速,关注了就不会错过。

有问题评论区直接说,我会逐条回。
系列导航 & 持续更新
📚 系列第 11 篇|上一篇:大模型架构篇——Transformer+Attention的心脏解剖图 |下一篇预告:大模型部署全流程——从Demo到生产
更多推荐

所有评论(0)