☰
LLM 微调实战(3):LoRA 实战:rank、alpha 与显存开销的关系
2026/10/1 19:48:10 网站建设 项目流程

承接与场景

上篇把 5 万条客服指令洗到了可用状态,这一篇正式开训。训练框架选 PEFT 库里的 LoRA 几乎是默认决定——全量微调 7B 的显存账单多数团队付不起,而 LoRA 用不到 1% 的可训练参数就能追平甚至超过全量效果。但默认决定不等于明白决定:rank 设 8 还是 64?alpha 和 rank 到底什么关系?只挂 q、v 和挂全部线性层差多少?这三个旋钮各自管什么,本篇用两本账说清:一本是显存的算术账,一本是学习动力学的模拟账。案例设定为财经内容团队的报告分析模型。照例声明:实验为纯 Python 3.12 标准库确定性模拟,生产请用 PyTorch + PEFT。

LoRA 改的是矩阵的什么

LoRA 的假设一句话:微调对权重矩阵的"改动量"是低秩的。全量微调让 W(比如 4096×4096)里每个元素都动,LoRA 则冻结 W,只训练两个窄矩阵 A(r×4096)和 B(4096×r),前向变成 y = Wx + (α/r)·B·A·x。r 就是"改动量允许有多复杂"的预算:r=16 意味着这轮微调对每个权重矩阵的修正被限制在 16 维子空间里。论文验证过,指令微调造成的权重扰动有效秩远小于矩阵本身维度,所以小 r 就够用。由此带来三个工程事实:训练显存大降(只有 1% 参数需要梯度和优化器状态);推理零开销(α/r·BA 可以直接加回 W,合并后矩阵乘法维度不变,实验一验证合并的数值等价性);多任务友好(一个底座挂多个 LoRA 适配器,按请求切换,运维成本近似一个基座加一摞几十 MB 的小文件)。代价是:低秩假设不总能成立,任务需要的"改动方向"超过预算时,效果对 rank 敏感——这正是模拟实验要演示的。

实验一:把显存账算成表格

显存大头有四块:冻结底座权重、可训练参数的权重+梯度+优化器状态、激活值、以及碎片。按 bf16 权重 2 字节、可训练参数 16 字节(权重 2+梯度 2+Adam 的 master/m/v 各 4)、7B 类 GQA 结构逐层累加,梯度检查点下激活按"每层只存入口张量"估:

"""7B 模型训练显存账: 全量微调 vs 不同 rank/目标模块的 LoRA, 确定性算术。"""H,L,I=4096,32,11008# 隐藏维 / 层数 / FFN 中间维 (7B 类结构)KV_H,HEAD=8,128# GQA: 8 个 kv 头, 每头 128 维VOCAB=32000GB=1024**3MODULES={"q_proj":(H,H),"k_proj":(H,KV_H*HEAD),"v_proj":(H,KV_H*HEAD),"o_proj":(H,H),"gate_proj":(I,H),"up_proj":(I,H),"down_proj":(H,I),}per_layer=sum(a*bfora,binMODULES.values())total=per_layer*L+2*H*VOCAB# 加 embedding 与 lm_headprint("7B 类结构: 每层 %.3fM 参数, 全模型约 %.2fB"%(per_layer/1e6,total/1e9))BS,SEQ=4,2048act_ckpt=L*BS*SEQ*H*2*2# 梯度检查点: 每层只存入口激活act_full=act_ckpt*12# 不重计算: 层内中间激活全留存deflora_params(r,targets):returnsum(r*(a+b)forn,(a,b)inMODULES.items()ifnintargets)*L scenarios=[("全量微调",total,act_ckpt),("LoRA r=16 q,v",lora_params(16,{"q_proj","v_proj"}),act_ckpt),("LoRA r=16 全部线性层",lora_params(16,set(MODULES)),act_ckpt),("LoRA r=64 全部线性层",lora_params(64,set(MODULES)),act_ckpt),("LoRA r=16 全层 无检查点",lora_params(16,set(MODULES)),act_full),]print("%-22s 可训练参数 占比 显存合计GB 24G单卡"%"方案")forname,trainable,actinscenarios:mem=(total*2+trainable*16+act)/GBprint("%-22s %8.1fM %6.2f%% %9.1f %7s"%(name,trainable/1e6,100*trainable/total,mem,"可"ifmem<22else"否"))

运行输出:

7B 类结构: 每层 177.209M 参数, 全模型约 5.93B 方案 可训练参数 占比 显存合计GB 24G单卡 全量微调 5932.8M 100.00% 103.5 否 LoRA r=16 q,v 6.8M 0.11% 15.2 可 LoRA r=16 全部线性层 36.8M 0.62% 15.6 可 LoRA r=64 全部线性层 147.3M 2.48% 17.2 可 LoRA r=16 全层 无检查点 36.8M 0.62% 59.6 否

这张表的读法很重要。第一,rank 从 16 提到 64,可训练参数翻了四倍(0.62%→2.48%),显存只多 1.6GB——rank 几乎不是显存问题,底座才是(冻结的 5.93B bf16 权重独占 11GB)。想省显存,动 rank 不如动底座精度(下一篇 QLoRA 的主题)。第二,r=16 只挂 q,v 是 6.8M 参数、挂全部线性层是 36.8M——差五倍多,但显存只差 0.4GB,因为省下的那点优化器状态在 15GB 面前是零头。也就是说"挂哪些模块"的决定影响的是效果与训练速度,不是能不能跑得下。第三,关掉梯度检查点从 15.6GB 弹到 59.6GB——24G 单卡直接出局,这是所有低显存教程都默认开着 gradient_checkpointing 的原因。

实验二:rank 管容量,alpha 管步幅

第二个实验把 LoRA 的最小单元抽象出来:6×6 权重矩阵,冻结 W,用 s·B·A(s=α/r)去逼近一个满秩的理想增量 D=T−W,手写 SGD 训练 400 步。三个待验证的命题:挂旁路的前向和把 B·A 合并进 W 的前向数值等价;rank 决定"能逼近到什么程度"(容量),逼近不满时残差由秩瓶颈造成;alpha 只改变收敛速度,不改变最终能到的地方(在满容量前提下)。

"""LoRA 三件事手算: 合并等价、rank 学习容量、alpha 缩放 (纯 Python SGD)。"""importrandom DIM=6defmatmul(A,B):n,k,m=len(A),len(B),len(B[0])return[[sum(A[i][t]*B[t][j]fortinrange(k))forjinrange(m)]foriinrange(n)]deffro(A):returnsum(v*vforrowinAforvinrow)**0.5rng=random.Random(7)W=[[rng.gauss(0,1)for_inrange(DIM)]for_inrange(DIM)]# 基座权重T=[[rng.gauss(0,1)for_inrange(DIM)]for_inrange(DIM)]# 任务所需目标D=[[T[i][j]-W[i][j]forjinrange(DIM)]foriinrange(DIM)]# 理想权重增量(满秩)x=[rng.gauss(0,1)for_inrange(DIM)]deffit(r,alpha,steps=400,lr=0.05):s=alpha/r# LoRA 缩放: 前向为 W@x + s * B@A@xar=random.Random(100+r)A=[[ar.gauss(0,0.3)for_inrange(DIM)]for_inrange(r)]B=[[0.0]*rfor_inrange(DIM)]# B 零初始化: 起点严格等价于基座BA=[[0.0]*DIMfor_inrange(DIM)]for_inrange(steps):BA=matmul(B,A)E=[[s*BA[i][j]-D[i][j]forjinrange(DIM)]foriinrange(DIM)]foriinrange(DIM):fortinrange(r):B[i][t]-=lr*2*s*sum(E[i][j]*A[t][j]forjinrange(DIM))fortinrange(r):forjinrange(DIM):A[t][j]-=lr*2*s*sum(B[i][t]*E[i][j]foriinrange(DIM))returnBA,s BA,s=fit(2,2.0)y_side=[sum(W[i][j]*x[j]forjinrange(DIM))+s*sum(BA[i][j]*x[j]forjinrange(DIM))foriinrange(DIM)]y_merge=[sum((W[i][j]+s*BA[i][j])*x[j]forjinrange(DIM))foriinrange(DIM)]print("挂旁路前向 vs 合并进权重前向, 最大逐维偏差: %.2e (浮点噪声级=可无损合并)"%max(abs(a-b)fora,binzip(y_side,y_merge)))print("rank 决定逼近满秩增量的容量, 相对残差 ||sBA-D||/||D||:")forrin(1,2,3,6):BA,s=fit(r,2.0)res=fro([[s*BA[i][j]-D[i][j]forjinrange(DIM)]foriinrange(DIM)])print(" rank=%d -> %.4f"%(r,res/fro(D)))print("满容量 rank=6 下, alpha(=缩放 s 的分子)只改变收敛快慢:")foralphain(0.5,1.0,2.0,4.0):forstepsin(40,400):BA,s=fit(6,alpha,steps=steps)res=fro([[s*BA[i][j]-D[i][j]forjinrange(DIM)]foriinrange(DIM)])print(" alpha=%.1f 步数=%3d -> 残差 %.4f"%(alpha,steps,res/fro(D)))

运行输出:

挂旁路前向 vs 合并进权重前向, 最大逐维偏差: 8.88e-16 (浮点噪声级=可无损合并) rank 决定逼近满秩增量的容量, 相对残差 ||sBA-D||/||D||: rank=1 -> 0.8143 rank=2 -> 0.6086 rank=3 -> 0.3856 rank=6 -> 0.0000 满容量 rank=6 下, alpha(=缩放 s 的分子)只改变收敛快慢: alpha=0.5 步数= 40 -> 残差 0.9701 alpha=0.5 步数=400 -> 残差 0.0977 alpha=1.0 步数= 40 -> 残差 0.6366 alpha=1.0 步数=400 -> 残差 0.0065 alpha=2.0 步数= 40 -> 残差 0.2226 alpha=2.0 步数=400 -> 残差 0.0000 alpha=4.0 步数= 40 -> 残差 0.1079 alpha=4.0 步数=400 -> 残差 0.0000

三段输出对应三段结论。偏差 8.88e-16 就是 float64 的舍入噪声,坐实了"合并等价":训练完把 s·B·A 加回 W,推理框架完全不需要知道 LoRA 存在,延迟零增加。中段是全篇最重要的一列数:理想增量 D 是满秩(6)的,rank=1 逼近完还剩 81% 的能量没学到,rank=6 才归零——残差不是训练不充分,是容量到顶,多跑一万步也不会变小。真实世界里 D 的"有效秩"由任务决定:改话术风格可能 4~8 就够,注入新判定流程可能需要 32~64。末段演示 alpha 的角色:满容量下 alpha 从 0.5 到 4.0,400 步后全部收敛到同一终点,差别只在 40 步时"走了多远"——alpha/r 这个缩放因子作用在梯度与更新量上,等效于给低秩分支调了个学习率倍率。PEFT 默认 alpha=16、r=8/16(s=1~2)是经验起点;调不动效果时优先怀疑 rank,其次才动 alpha,反过来做就是在用容量换步幅的错觉。

常见陷阱

  • rank 玄学递增:r=16 没效果就加到 128。先看数据是不是根本没到容量门槛(多数失败是数据问题),rank 翻倍对照实验只加一个变量,否则归因失效。
  • 改 alpha 当调参玩具:s=α/r,动了 alpha 等于动了有效学习率,和调度器叠加后 LR 已经不是你以为的那个 LR。规矩:一次实验只让 r 或 alpha 其一变化,另一个固定 s=2。
  • 只挂 q,v 的肌肉记忆:2021 年的论文设定,GQA 模型上 k_proj 变短后作用上升、FFN 承载大量领域知识,QLoRA 论文直接建议全线性层。显存账已证明全挂几乎不多花钱。
  • 推理忘合并或忘关 adapter:同一底座加载 LoRA 的方式(PEFT 动态加载 vs 合并导出)性能与显存不同,灰度时两条路径的数值输出要做一致性抽查。
  • 多 LoRA 叠乘幻觉:两个独立训练的 adapter 直接相加不等于联合训练(第七篇专讲合并的干扰问题),多任务先考虑分开训+路由。

落地清单

  • 预算表先行:底座 bf16 2 字节/参 + 检查点激活是地板,rank 与目标模块只在上层微调
  • 起点配置:r=16、alpha=32、目标模块=全部线性层、梯度检查点开
  • 对照实验纪律:rank 与 alpha 不同时动,评估用第六篇的固定评测集
  • 导出决策:单模型单用途→合并导出;一底座多租户→保留 adapter 动态加载
  • 记住容量与步幅分工:学不会→rank;学得慢→alpha/LR;两者症状在 loss 曲线上可区分

显存账里那个 11GB 的冻结底座才是真正的大石头。下一篇《LLM 微调实战(4):QLoRA 与 4bit 量化:单卡跑通 7B 微调》把底座压到 3.5GB,看看量化误差花在哪里、NF4 为什么比普通 int4 更适合权重分布。

参考来源

  • LoRA: Low-Rank Adaptation of Large Language Models:https://arxiv.org/abs/2106.09685
  • QLoRA: Efficient Finetuning of Quantized LLMs:https://arxiv.org/abs/2305.14314
  • LoRA Learns Less and Forgets Less:https://arxiv.org/abs/2405.09673
  • Hugging Face PEFT 文档:https://huggingface.co/docs/peft/index
  • ZeRO: Memory Optimizations Toward Training Trillion Parameter Models:https://arxiv.org/abs/1910.02054

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询