我到现在还记得第一次把微调目标从“整个模型”换成“两个低秩小矩阵”时的场景:某个文本分类任务,完整微调一个小型 Transformer 时,GPU 显存勉强够用,每跑一个 epoch 都要等半天,而且训练到第二轮之后,训练指标还在涨,验证集指标却开始原地踏步。后来我改成 LoRA 方案,可训练参数直接从百万量级掉到 1.6 万这个量级——接近 98% 的缩减。当时我第一反应是“这怕不是骗人的吧”,但训练结束后,模型在验证集上的表现几乎和全量微调持平,甚至在小数据上更稳。
这篇文章就围绕这件事彻底聊透:常规微调为什么贵、LoRA 凭什么能把参数砍掉这么多、从 100 万到 1.6 万这笔账到底怎么算,以及我在真实项目中调参和排坑的完整记录。
1. 先把问题看清楚:常规微调到底在为什么买单
很多人第一次接触 LoRA,注意力全被“98%”这个数字吸引,但如果不理解全量微调的成本结构,就理解不了这个数字的含金量。所以我先拆一拆,常规微调到底贵在哪里。
1.1 一张 768×768 矩阵的隐藏账单
Transformer 模型里最典型的权重矩阵就是自注意力的投影矩阵。假设隐藏维度 d=768,那么一个投影矩阵 W 的尺寸就是 768×768,总参数量是 589,824 个。看起来不多,但如果要更新两个这样的矩阵(比如 Q 和 V),就是 1,179,648 个参数——也就是标题里“100 万”的量级来源。
关键问题在于,训练时这些参数并不只是“存一份数值”那么简单。完整微调时,显存里至少要同时放这几样东西:
| 存储对象 | 单个 768×768 矩阵 | Q、V 两个矩阵合计 |
|---|---|---|
| 模型权重(fp32) | 2.36 MB | 4.72 MB |
| 梯度(fp32) | 2.36 MB | 4.72 MB |
| Adam 一阶动量 | 2.36 MB | 4.72 MB |
| Adam 二阶方差 | 2.36 MB | 4.72 MB |
| 合计 | 9.44 MB | 18.88 MB |
这只是注意力层里两个矩阵的账,还没有算 embedding、前馈网络、LayerNorm,也没有算前向/反向过程中的激活值。模型越大,优化器状态和梯度占用就越夸张。所以全量微调真正烧显存的地方,不只是“模型本身”,而是“为了更新模型而必须维护的那一整套状态”。
这也是 LoRA 第一个让我觉得实用的点:它把需要更新参数的数量级降下来之后,最直观的收益是优化器状态从“跟模型一样大”变成“几乎可以忽略”。
1.2 除了显存,全量微调还有三个隐性成本
显存是最容易感知的成本,但还不是全部。真正让全量微调在项目里越来越难用的,还有下面这三件事。
第一个是灾难性遗忘。下游任务的数据通常只有几万条,甚至几千条,而预训练模型是在海量语料上学的。你要让它的几百上千万参数去适配一个很小、分布很偏的任务,很容易把预训练阶段学到的通用能力冲掉。现象就是训练指标一路走高,验证指标却止步不前,或者换个领域测试就崩。
第二个是多任务部署。一个模型要服务多个任务时,全量微调就意味着每个任务保存一份完整的独立模型副本。100 万参数还只是“能接受”,到了亿级模型就变成存储和分发的大麻烦。LoRA 的 adapter 文件通常只有几百 KB,几十个任务就是几十个小文件,切换任务时加载 adapter 的成本几乎可以忽略。
第三个是迭代效率。全量微调时,每次实验改一个超参数都要重新训练一大片参数,实验周期被拖得很长。LoRA 的训练参数量小,收敛快,一轮实验的时间可以缩短到原来的几分之一,这在调参阶段特别有价值。
1.3 LoRA 不是在“压缩模型”,而是在“压缩更新量”
这里要先澄清一个最容易误解的地方:LoRA 没有把模型变小,也没有减少前向推理的计算量。模型原来的权重还在那里,还是那么多参数,推理时该算的矩阵乘法一个都不少。
LoRA 的思路是:原模型的权重全部冻结,在它旁边插入几个可训练的小矩阵。训练时只更新这些“旁路”小矩阵的参数,而不是更新整个模型。所以它减少的是“需要训练、需要存梯度、需要存优化器状态的参数数量”,而不是“模型总参数量”。
这个区别非常重要。如果你看到某个项目说“LoRA 把模型从 1B 压缩到 1M”,那是错的。准确的说法是“LoRA 把需要微调的参数量从 1B 压缩到 1M,模型本体仍然是 1B”。理解了这一点,后面所有参数账才算得明白。
2. LoRA 的核心原理:低秩假设与旁路注入
既然要聊原理,那就要把“低秩”和“旁路”这两个词讲透。它们其实是两个独立的设计,拼在一起才构成了 LoRA 的完整方案。
2.1 低秩假设:更新量没有那么多“有效信息”
想象一下:预训练模型在某个任务上做微调时,最终模型权重可以写成“原始权重加一个更新量”:
$$W' = W + \Delta W$$
其中 W 是预训练权重,ΔW 是微调过程中产生的变化量。在全量微调里,ΔW 是一个和 W 同尺寸的矩阵,比如 768×768。也就是说,全量微调要在 589,824 个维度上自由调整。
但 LoRA 提出的低秩假设是:这个 ΔW 虽然看起来很大,但它的“有效自由度”其实很低,可以用两个小矩阵的乘积近似,也就是:
$$\Delta W \approx B \times A$$
其中 A 的尺寸是 r×d,B 的尺寸是 d×r,而 r 远小于 d。这里的 r 就是 LoRA 里最重要的超参数,通常取 4、8、16 这样的值。数学上,当 r=8 时,两个小矩阵加起来只有 8×768 + 768×8 = 12,288 个参数,不到原来 589,824 的 2.1%。
为什么可以这样近似?可以打一个生活化的比方:全量微调相当于你给一本已经出版的书重新排版印刷,每个角落都可以改;而 LoRA 相当于在这本书上贴勘误条,只修改那些确实有问题的段落。大多数情况下,一本好书只需要少量勘误就够了,没必要整本重印。
这个假设并不总是成立,但它被大量实验验证过:在绝大多数下游任务上,r=8 的 LoRA 效果已经逼近全量微调,说明 ΔW 确实存在明显的低秩特性。
2.2 旁路结构:为什么是 A、B 两个矩阵
理解了低秩近似,下一个问题就是:为什么不直接用一个小矩阵去学习 ΔW,而是要用 A 和 B 两个矩阵相乘?
这里的关键在于“初始状态的一致性”。LoRA 在做的事情,是在已经训练好的模型上做增量适配,所以最理想的状态是:微调刚开始时,模型输出应该和预训练模型完全一致,然后随着训练逐步偏离,去适配下游任务。
如果只用一个矩阵 M 去替代 ΔW,那 M 一开始必须初始化为零。但一个全零矩阵在反向传播时梯度也是零,根本没法训练。而用 A、B 两个矩阵的方式,可以这样初始化:B 初始化为零矩阵,A 初始化为随机高斯分布。这样一开始 B×A 的结果正是零矩阵,ΔW=0,模型输出和预训练完全一致;训练开始后,A 有梯度,B 也会跟着更新,旁路逐渐“长”出任务相关的修正。
所以 A、B 两个矩阵不是冗余设计,而是为了让“零初始化”和“可训练”两个要求同时成立。
前向计算时,LoRA 做的事情可以写成:
$$h = Wx + \frac{\alpha}{r} \cdot BAx$$
其中 x 是输入,Wx 是原始路径的输出,BAx 是旁路修正,α/r 是缩放系数。这个缩放系数也很有讲究,后面调参部分我会专门讲。
2.3 和其他高效微调方案的区别
很多人会把 LoRA 和 Adapter、Prefix-Tuning 这类方法放在一起比较,但它们解决的问题域不同,设计取舍也不同。
Adapter 的做法是在 Transformer 层中间插入新的小网络,训练时只更新这些小网络。缺点是这些额外层串在主干路径上,推理时每一个样本都要多过几层网络,会带来实实在在的推理延迟。LoRA 则完全不同,它的旁路矩阵在训练结束后可以合并回原来的权重矩阵里,推理时就是一次普通的矩阵乘法,没有任何额外层,延迟几乎为零。
Prefix-Tuning 的做法是在输入侧拼接可学习的前缀向量。它对生成任务效果不错,但会改变输入序列的长度,对某些任务不太友好,而且调优经验在不同模型间迁移性差。LoRA 不改变输入输出的结构,只修改权重路径,所以天然适配各种任务类型。
这也是我在项目里优先选 LoRA 而不是其他方案的原因:训练完合并权重、推理零开销、任何 Transformer 结构都能用,调试思路也直观。它不是“唯一能用的方案”,但绝对是最省心、最通用的那一个。
3. 参数账本:从 100 万到 1.6 万的完整拆解
现在进入最硬核的部分:这笔账到底怎么算。这也是我觉得每个用 LoRA 的人都应该亲手算一遍的数字,只有这样你才能真正理解 98% 这个比例代表什么、不代表什么。
3.1 一张表看懂数量级对比
LoRA 的可训练参数计算规则很简单:每注入一个 d×d 的权重矩阵,需要新增两个低秩矩阵 A 和 B,参数量为 d×r + r×d = 2dr。所以总新增训练参数就是“注入的矩阵个数 × 2dr”。
全量微调同样范围内需要更新的参数则是“注入的矩阵个数 × d×d”。把这两个数放在一起,对比就出来了:
| 隐藏维度 | 注入范围 | 全量微调参数量 | LoRA r=4 | LoRA r=8 | LoRA r=16 | r=8 时的缩减比例 |
|---|---|---|---|---|---|---|
| 768 | Q、V 两个矩阵 | 1,179,648 | 12,288 | 24,576 | 49,152 | 97.92% |
| 768 | Q、K、V、O 四个矩阵 | 2,359,296 | 24,576 | 49,152 | 98,304 | 97.92% |
| 512 | Q、V 两个矩阵 | 524,288 | 8,192 | 16,384 | 32,768 | 96.88% |
注意看表格里的第三行配置:隐藏维度 512,注入 Q、V 两个矩阵,r=8,LoRA 训练参数正好是 16,384,也就是标题里“1.6 万”的直接来源。而完整微调这两个矩阵需要更新 524,288 个参数,约 52 万;如果把模型中少量新增的分类头、可训练的 bias 也算进去,整体更新量就迈入百万级别。所以“从 100 万到 1.6 万”并不是一个固定配置的唯一结果,而是不同规模模型下普遍存在的数量级对比——究竟是 96% 还是 98%,取决于你选多大维度、注入多少矩阵、r 取多少,但“百万对一万”这个量级差是稳定的。
再仔细看第四行和第五列的关系:全量微调参数量按“矩阵个数 × d²”增长,而 LoRA 按“矩阵个数 × 2dr”增长。d 越大,两者差距越悬殊。大模型之所以更需要 LoRA,不是因为它“比较好用”,而是因为 d 变大之后,全量微调的成本是平方级增长,LoRA 却只是线性增长。
3.2 98% 减掉的到底是什么
这里必须再强调一次:98%,减掉的是“训练时需要维护的可训练参数数量”,不是“模型的大小”,也不是“推理的耗时”。
以表格第一行为例,全量微调 Q、V 两个 768×768 矩阵需要更新 117.9 万个参数;LoRA r=8 时只用训练 24,576 个。这时候:
- 需要存储的梯度、Adam 动量、Adam 方差都随之降到原来的 2% 左右;
- 反向传播时只需要对这两个小矩阵计算梯度,而不是对两个大矩阵;
- 模型的总参数量还是那么大,前向计算还是原来的矩阵乘法,推理速度几乎没有变化。
有一种常见误解是“用了 LoRA 模型推理会变快”。如果不做任何后处理,推理速度和全量微调模型是一样的。只有当你把 LoRA 合并回原权重之后,如果原模型本身没变、计算量没变,速度自然没变。LoRA 带来的不是“运行更快”,而是“训练更省、部署更灵活”。
还有个容易忽略的细节:微调任务里如果新增了分类头,分类头的参数量无论全量微调还是 LoRA 都需要训练。但分类头通常只有几万参数,放在全量微调里不值一提,放在 LoRA 的几万参数里可能占比不小。用 PEFT 库时,print_trainable_parameters()打印的数字会包含分类头,所以对比时应只看 LoRA 相关参数,心里要有数。
3.3 不同 r 的选择对参数量的影响
既然 r 直接决定 LoRA 的参数量,那 r 的取值就值得单独说。从公式 2dr 可以看出:r 每增大一倍,LoRA 参数量也增大一倍。所以 r=16 的参数量是 r=8 的两倍,是 r=4 的四倍。
但这里有个很反直觉的点:r 增大带来的效果提升,远没有参数量提升那么显著。很多公开实验和个人实践都指向同一个结论:r=8 时效果已经接近全量微调,再往上加 r,边际收益非常有限,反而增加过拟合风险。LoRA 论文里的实验也展示过类似规律,不是说 r 越大越好,而是“够用就好”。
我的经验是:新任务先从 r=8 起步,把 baseline 跑出来,然后试一次 r=16 和 r=4,看验证集指标的方向。如果 r=4 和 r=8 差不多,说明任务简单,可以用 r=4 进一步压缩;如果 r=16 还有明显提升,说明任务复杂或者数据量大,可以继续往上加,甚至考虑分层的 rank 设置。后面调参部分我会展开说。
4. 实操落地:用 PEFT 把一个 LoRA 微调跑起来
原理说得再多,不跑一次等于白说。这一节我按实际项目里最快的路径,完整走一遍 LoRA 微调的代码流程。
4.1 环境准备:依赖其实很少
LoRA 微调需要的东西比全量微调少得多,核心就是 PyTorch 和 PEFT 库。PEFT 是围绕参数高效微调封装的工具库,封装了 LoRA 的注入、保存、合并等操作,省去手写低秩矩阵的麻烦。
pip install transformers peft datasets accelerate如果你想在消费级显卡上跑更大的模型,建议装一下bitsandbytes,后面聊 QLoRA 的时候会用到。transformers 版本建议 4.30 以上,PEFT 版本最好保持比较新,因为接口迭代过几轮,旧版本在target_modules的行为上有些差别。
显卡方面:几个亿参数的小模型在 16G 显存上跑 LoRA 基本无压力;更大规模的模型搭配 4bit 量化也能跑。实在没有 GPU,拿 CPU 跑一个极小模型的 LoRA 演示流程也是可以的,只是慢一点。
4.2 注入 LoRA 的完整代码
以文本分类任务为例。加载基础模型后,用 LoraConfig 声明 LoRA 参数,再用 get_peft_model 把 LoRA 结构嵌进模型:
from transformers import AutoModelForSequenceClassification, AutoTokenizer from peft import LoraConfig, TaskType, get_peft_model model_name = "your-base-model" # 替换成你实际使用的基础模型 model = AutoModelForSequenceClassification.from_pretrained( model_name, num_labels=2, ) config = LoraConfig( task_type=TaskType.SEQ_CLS, r=8, lora_alpha=16, target_modules=["query", "value"], lora_dropout=0.1, bias="none", ) model = get_peft_model(model, config) model.print_trainable_parameters()运行后,print_trainable_parameters()会输出一行类似这样的信息:
trainable params: 24,576 || all params: 110,063,874 || trainable%: 0.0223对于隐藏维度 768 的模型,注入 Q、V 两个矩阵、r=8 时,LoRA 训练参数正好是 24,576,占总参数的约万分之二。这个数字和你自己手算的公式完全对得上:2 个矩阵 × 2 × 8 × 768 = 24,576。
注意:
target_modules里的名字不是随便写的,它必须和模型内部模块的实际命名一致。BERT 系列里注意力投影层通常叫query、key、value;部分模型的命名是q_proj、v_proj这种带_proj后缀的。不确定就去打印模型结构或者查配置文档,别凭感觉填。
4.3 训练循环:唯一要注意的是学习率
LoRA 微调的训练代码和普通微调没有本质区别。我自己习惯直接写训练循环,不额外引入训练框架:
import torch from transformers import AdamW optimizer = AdamW(model.parameters(), lr=2e-4) for epoch in range(3): for batch in train_dataloader: model.train() outputs = model(**batch) loss = outputs.loss loss.backward() optimizer.step() optimizer.zero_grad()这里最值得注意的就是学习率。全量微调时我习惯用 2e-5 这种比较保守的数值;LoRA 可训练参数少,收敛路径不同,学习率可以放得更大,一般在 1e-4 到 5e-4 之间。我常用 2e-4 起步,效果已经很稳定。如果你发现 loss 降得太慢或训练震荡,优先考虑调整学习率,而不是去改 r。
训练轮次也不需要像全量微调那样多。LoRA 参数少,在小数据集上常常 2 到 4 个 epoch 就能收敛到不错的效果,再继续训练反而容易过拟合。
4.4 保存、加载与合并权重
训练完成后,LoRA 的产物不是一个完整模型,而是一个很小的 adapter:
# 保存 adapter model.save_pretrained("my-lora-adapter")这个目录下通常只有两个文件:adapter_config.json和adapter_model.safetensors。前者记录基础模型名、r、alpha、target_modules 等配置,后者就是训练出来的 A、B 矩阵。整个目录几百 KB 是常态,分发、下载、版本管理都很轻。
如果要部署到推理服务,最省事的方式是把 LoRA 合并回原模型权重,得到一个常规模型:
merged_model = model.merge_and_unload() merged_model.save_pretrained("merged-model")合并的原理就是把 W' = W + BA 这个加法真正做一遍,把旁路“揉”回原权重。合并之后,模型和普通微调模型没有任何区别,推理时不走 LoRA 分支,也就不存在额外计算量。
注意:合并操作会使模型权重变成“基础权重 + LoRA 权重”,这时候如果再继续训练,是在合并后的权重上做普通微调,LoRA 结构已经没了。想继续用 LoRA 训练,就保留未合并的 adapter,不要拿合并后的模型直接继续 LoRA 训练。
5. 调参实战:r、alpha、target_modules 的取舍
LoRA 超参数不多,但每个都值得认真调。这一节是我自己从几个项目里总结出来的经验,不一定适用于所有场景,但作为起点非常稳。
5.1 r 的选择:从 8 开始,往两边试探
r 控制的是低秩空间的维度。r 太小,A、B 矩阵能表达的信息不够,模型欠拟合;r 太大,参数量上升,过拟合风险增加,而且边际收益递减。
我实测下来的规律是:
- 数据量小、任务简单、基础模型能力已经很强:r=4 就够;
- 一般分类、抽取、指令微调任务:r=8 是甜点区,效果接近全量微调;
- 数据量大、任务复杂、领域差异大:r=16 或 r=32 可能有提升,但要在验证集上确认;
- r=64 以上很少带来明显收益,除非模型极其庞大、任务极其复杂。
真正做项目时,我不会只凭感觉猜。流程是先 r=8 跑一个 baseline,再跑 r=4 和 r=16 两组对比,看验证指标的方向。如果 4 和 8 差不多,说明任务确实简单,就选 4;如果 16 比 8 好很多,再往上试 32。这个流程多花不了多少时间,但能让你对任务的“复杂度”有清晰认知。
5.2 alpha 与 r 的关系:改 r 时别忘记比例
alpha 是 LoRA 分支的缩放系数。前向计算里,旁路的输出是 (alpha / r) 乘以 BAx,所以 alpha 与 r 的比例决定了 LoRA 分支对模型输出施加的影响强度。
理解起来可以这样想:alpha 相当于是 LoRA 分支的“音量”旋钮。alpha 相对于 r 越大,旁路修正对输出的影响就越强,训练步长也越激进;比例太小,则修正太温和,收敛慢。
常见的经验配置是 alpha 取 r 的 1 到 2 倍。我自己习惯用 alpha = 16、r = 8;如果换了 r,会保持 alpha/r 这个比例大致稳定,比如 r=16 时 alpha 取 32,r=4 时 alpha 取 8。这样改动 r 时,LoRA 分支的初始影响强度不会突然变化太多。
如果你观察到的现象是训练早期 loss 下降得飞快但是不稳定,试试把 alpha 调小或把学习率调小;如果 loss 一直很平、降不下去,检查 alpha 是不是太小了。
5.3 target_modules:先 Q 和 V,别一上来就全选
LoRA 注入到哪些模块,直接影响容量和效果。很多第一次用 LoRA 的人看到target_modules这个参数就懵:到底该选哪些?
说一个我的惯性做法:先从注意力层的 Q、V 两个矩阵开始。这不是拍脑袋,LoRA 论文里最常见的实验配置就是 Q、V 两个投影,大量复现实验也证明这个基线在多数任务上效果不错。它的好处是参数少、内存占用低、收敛快,适合作为对比基准。
需要更强效果时,按这个顺序逐步加入:
| 加入范围 | 大致效果影响 | 参数成本 |
|---|---|---|
| 只注入 V | 基线上限较低,适合极简单任务 | 最低 |
| Q、V | 多数任务的甜点配置 | 适中 |
| Q、K、V | 对需要建模 token 间关系的任务(如抽取)更友好 | 上升 50% |
| Q、K、V、O | 进一步接近全量微调 | 上升一倍 |
| 再加 FFN 层 | 极端情况下接近全量微调 | 逼近全量 |
我踩过的坑是:为了“效果保险”一开始就把所有线性层全选上,结果可训练参数涨到十几万,小数据集上没过几个 epoch 就过拟合。后来老老实实从 Q、V 开始,反而更早收敛、效果更稳。
判断依据很简单:打印trainable params,如果占比超过 1%,对于大多数任务来说已经偏重了。LoRA 的精神是“够用就好”,不是“全部注入”。
5.4 dropout、bias 与类别不平衡
- 数据量少、容易过拟合时,把
lora_dropout从默认的 0.0 提到 0.1。注意 LoRA 分支只在训练时启用 dropout,推理时没有额外开销。 bias一般设"none"。如果你的任务和预训练任务分布差异很大,比如领域文本、非标准格式输入,可以试试"lora_only",让偏置项也参与训练。虽然会增加一点参数,但有时能稳定收敛。- 分类任务的类别不平衡问题,LoRA 不背这个锅。该加 class weights 就加,该换 loss 就换,这些技巧和全量微调完全通用。
6. 常见问题与排查实录
最后这一节,我把真实项目里遇到过的 LoRA 问题按频率排了个序,每个都配了排查思路。这些问题在网上问得很多,但大部分靠文档里的只言片语很难一次定位。
6.1 训练 loss 不降或震荡
排查顺序应该是这样的:
- 确认模型真的只训练了 LoRA 参数。调用
print_trainable_parameters(),如果输出显示所有参数都可训练,说明get_peft_model没有生效或配置写错了。 - 把学习率从 2e-4 开始调整。LoRA 的学习率不能按全量微调的习惯来。我曾遇到一个任务,lr=5e-5 时 loss 几乎不动,换到 2e-4 后立刻正常。
- 检查 r 是否太小。r=1 的 LoRA 在稍复杂任务上很难拟合,可以试 r=8 看 loss 是否改善,以此判断容量瓶颈。
- 如果 loss 出现 NaN,多半是混合精度下 alpha 过大导致梯度爆炸,先调小 alhpa 或降低学习率。
6.2 推理时合并权重后效果变差
LoRA 的合并操作在数学上是严格的,但实际代码里有精度坑。主要问题是 fp16/bf16 下合并时的舍入误差,以及 transformers 版本之间模块名映射不一致。
我的建议是:训练完先保存 adapter,然后分别测试“不合并、用 adapter 模式推理”和“合并权重后推理”的指标。如果合并后变差、adapter 模式正常,再考虑用更高精度做合并,或者干脆推理时用 adapter 模式,不合并。
另外,adapter 配置文件里会记录基础模型标识,跨版本加载如果报错,多半是基础模型的版本或配置对不上,重新下载对应模型即可。
6.3 显存还是不够
很多人以为用了 LoRA 就一定能塞进小显存,其实不对。LoRA 省掉的是优化器状态和梯度的显存,但前向/反向过程的激活值依然和模型结构直接相关,这部分不会因为 LoRA 而消失。
如果显存还是紧张,按这个顺序叠加方案:
- 开启 gradient checkpointing,用少量显存换训练计算量;
- 开启 bf16 混合精度,进一步压缩显存;
- 用量化和 LoRA 结合,也就是 QLoRA 的思路:把基础模型量化到 4bit,冻结它,再在上面做 LoRA 训练。
QLoRA 的意义在于:基础模型的权重用 4bit 存储,显存占用远小于 fp16,而 LoRA 参数用较高精度训练,既保住效果又大幅降低门槛。现在很多个人开发者能拿消费级显卡跑几十亿参数的模型,靠的就是这套组合。
6.4 多任务、多 adapter 的踩坑笔记
LoRA 的一个隐藏优势是“一个基础模型,多套 adapter”。我做多任务时就是一个任务一个 adapter,推理时切换对应 adapter,基础模型只需要保留一份。但这里有两个坑必须提醒:
第一个是加载 adapter 时基础模型必须完全一致。包括模型架构、分词器、版本,任何对不上都会报错或出现行为漂移。adapter_config.json 里记录了基础模型标识,加载时对照一下。
第二个是不同任务之间不要共享同一个 adapter 去调。看着省事,实际会互相污染。正确姿势是每个任务独立训练、独立保存,需要时再切换。
最后说点实在的
参数从 100 万缩到 1.6 万这件事,真正的意义不是省那几 MB 存储,而是把“微调大模型”从一种需要专门资源的操作,变成了普通开发者用一块消费级显卡就能完成的日常操作。我个人的体会是:LoRA 最大的价值不在那 98% 的数字,而在它让“尝试”变得便宜了——你可以为一个小任务花十几分钟跑一个 adapter,不行就换参数再跑,这种迭代速度是全量微调给不了的。
如果这篇文章只留一句建议,我会说:新任务直接上 r=8、alpha=16、target_modules 选 query 和 value、学习率 2e-4,先把流程跑通,再根据验证集表现去调。很多调参经验不是读出来的,是跑出来的,先把第一步迈出去,后面都好说。