LoRA:大型语言模型的低秩适配器
几个月前,我盯着一行输出——“torch.cuda.OutOfMemoryError”,差点把3090从机箱里抠出来摔了!😡
我那个项目呢,就想微调一个7B的模型做文本分类。结果呢?全参数微调一跑,40GB显存打底,我那卡直接当场去世。连着两次OOM,第三次我怂了,开始翻参数高效微调(PEFT)的救急方案。
说真的,第一次看到LoRA论文标题里那个“Low-Rank”的时候,我脑子里全是“矩阵分解玄学”几个字。心里想:又搞这套?装神弄鬼的吧?
结果试了一次,你猜怎么着?
真香!真特么香!🔥
先别急着上手,我们得捋一捋,为什么LoRA能成为我们这群“显存贫困户”的救世主。
你看,在LoRA出世之前,你要微调一个大模型,只有两条路:
一条是全参数微调(SFT)——效果是牛,但代价呢?你得把整个模型翻来覆去训一遍。GPT-3 175B?好家伙,一个任务存175B权重,十个任务就是10×175B。光存储就上千GB了,更别训练时候的显存和电费了。而且还有坑:灾难性遗忘。刚练好新技能,老本事全忘了,跟狗熊掰苞米似的。
另一条是各种花里胡哨的Adapter——参数量是少了,但问题更大!你在Transformer里插了额外层,推理的时候根本没办法合并回原始权重。结果呢?延迟蹭蹭涨,线上服务叫苦连天,用户体验直接崩。
但!2021年,微软那帮人搞出了LoRA。
核心思路你听听,简单到离谱:
“权重更新的秩其实很低,所以我们不动原模型权重W,而是整两个小矩阵A和B,用他俩的乘积来近似W的变化量。”
公式不贴了,反正就是说——
训练的时候,原模型冻着不动,只练A和B这俩小玩意儿。推理的时候呢?把AB加回到W里,合并成新权重。额外推理延迟?零!零啊!等于白嫖!💥
我当时就一个反应:真的假的?凭什么敢假设更新量是低秩的?
论文直接甩了一堆硬核实验:在GPT-3 175B上,LoRA把可训练参数量降了一万倍,GPU内存需求砍掉三分之二,效果居然和全微调持平甚至还好!
而且这还没完,同一个基座模型,你想挂几个LoRA都行。切换任务?换个文件,说换就换,比翻书还快。
那你想想,这谁顶得住啊?
说到实操,我用的就是HuggingFace的PEFT库(0.6.1),配合transformers 4.35.0和bitsandbytes搞量化和混合精度。
配置LoraConfig那会儿,我可是踩了满脚的坑。一个一个说,免得你再摔一次。
rank(r):这就是控制低秩矩阵的“秩”,直接影响参数量。论文建议从8开始,大多数任务8就够用了。我从1试到64,8和16差别不大,但你敢把r设成1吗?效果直接掉坑里!r=64也没更好,因为我才一万多条数据,反而容易过拟合。有些人喜欢设128甚至更高,我劝你一句:除非你数据量大得离谱,不然就是白加算力,算了个寂寞。
lora_alpha:缩放因子,它和r一起决定每次更新的步子大小。源码里scaling = alpha / r。默认r=16、alpha=32,scaling就是2。alpha设大了,步子迈得太大容易扯着蛋。我试过alpha=128,好家伙,训练直接崩了,当场表演一个梯度爆炸。
target_modules:这个我吃过最大的亏!一开始我照着论文只选了q_proj和v_proj,结果呢?收敛慢得让人心碎。后来翻了源码才发现,很多开源模型的模块名根本不是那回事!比如DeepSeek,它有q_proj、k_proj、v_proj、o_proj,还有gate_proj、up_proj、down_proj那些前馈层。
想知道你的模型有哪些层?别猜,直接打印:
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("deepseek-ai/deepseek-llm-7b-chat")
for name, _ in model.named_modules():
if "proj" in name:
print(name)看到结果你就明白了,哪些该加进target_modules一目了然。我现在的习惯是:注意力模块的q、k、v、o全加上;前馈层,如果是生成任务就加上gate_proj和up_proj,效果提升肉眼可见。
bias:别的别碰,直接设"none"。LoRA核心就是训两个投影矩阵,你去动bias干嘛?别给自己找事。
lora_dropout:我设0.05,主要防小数据集过拟合。试过0.1吧,收敛慢得想骂人。还是0.05稳妥,你也记住了。
初始化这里还有个门道。论文里A用Kaiming初始化(随机高斯),B初始化为零。有人要问了:为什么不都设零?
听好,这是个反直觉的点:如果B也是零,那对A的梯度里含有B转置,等于零,A就永远学不会,直接凉凉。所以必须A随机、B为零——刚开始是零,但跑一个batch就打破平衡了。
实践出真知,我确认过。👇
训练完以后,LoRA最让人舒服的就是合并权重。
一句model.merge_and_unload()搞定所有。当然你也可以不合并,直接留LoRA分支用peft_model.generate()推理。但我习惯合并,因为部署时候就是一个完整的模型,不用再加载额外分支,省显存也省心。
后来呢?我又试了QLoRA。
先把基座模型量化到4bit,再加LoRA模块训练。显存直接降到8GB以下!我那老掉牙的3060都敢跑7B模型了!
当然要注意:量化模型加LoRA后,LoRA模块的梯度必须保持在FP16或FP32,不能用int4去算梯度——还好bitsandbytes会自动处理,但你要确认优化器没把LoRA权重也量化掉。效果嘛,跟纯LoRA比确实有一丢丢损失,但几乎看不出来,尤其分类这种简单任务,简直白给。
我总在网上看到有人吵:“LoRA到底能不能打平全参数微调?”
我的回答就一句话:大部分任务,尤其是中小规模数据集,LoRA完全够用。
只有那些极其复杂、需要模型从头学到全新能力的任务——比如代码生成、数学推理——全参数微调的上限可能才高那么一点。但差距呢,随着基座模型越来越大、越来越聪明,反而正在拉大!你可以用LoRA去微调100B甚至更大的模型,全参数?根本训不动。
那未来呢?LoRA不会消失,它只会越来越工具化。DoRA、AdaLoRA、MoRA这些变体已经在搞更精细的秩分配和权重解耦了。而且LoRA现在已经不只是语言模型的专利了,Stable Diffusion微调也用LoRA,多模态、视频模型全在套这个范式。
对我来说,未来两三年内,LoRA + 量化就是小团队和个人开发者做落地最主流的选择,没有之一。
好啦,如果你现在还对着终端的OOM发愁,别死撑了。
去GitHub把peft库拉下来,配个LoraConfig,跑一个epoch试试。
你就知道——
你之前被全参数微调折磨的日子,算是白过了。 🚀
读者评论 3