读完这篇,你能做什么
能计算LoRA参数量,核对零初始化首次梯度,训练低秩线性增量并验证合并输出。
学习理由、准备要求与知识点
为什么学它
可训练参数少了,基础模型仍要参与计算。看清低秩与量化各自的限制,才能选目标层、估算内存并验证权重合并。
先读这些内容
知识点清单
- 低秩矩阵分解
- A和B的形状
- alpha
/r缩放 - 冻结基础权重
- 零B初始化和梯度
- 适配器合并条件
- 量化存储与计算dtype
QLoRA的NF4与其他机制
运行前先确认Java17、PyTorch2.9.1 CPU;LoRA源码基线PEFT v0.17.1。实验训练原创线性层适配器;附带均匀量化误差演示,不是NF4或完整QLoRA,没有bitsandbytes/GPU运行。
01先弄懂它是什么
LoRA 的做法是保留原权重,再加一条只需训练少量参数的分支。普通线性层用 W 把长度 din 的输入 x 变成长度 dout 的输出,W 有 dout×din 个数。新增分支先用 A 把输入压到 r 维,再用 B 映回输出维,合起来算 Wx+sBAx;A 的形状是 [r,din],B 是 [dout,r],普通 LoRA 的 s=alpha/r。
例如 din=3、dout=2、r=1,新分支共需 3+2 个参数。一般就是 r(din+dout) 个,不必给 W 的每个位置都另设一个可训练改动;r 的选择限制了这条分支能表达的变化。
冻结基础权重 W,意味着训练时不计算它的参数梯度,也不更新它的优化器状态,但模型仍然要保存和使用 W。若更早的适配器需要梯度,反向信号还要通过基础层继续传回去,因此不能把整个基础网络直接放进 no_grad。LoRA 主要减少的是需要训练的状态;前向计算和中间激活不会按相同比例一起消失。
QLoRA 把两个办法放在一起:基础模型冻结,并用较少的位数编码保存;任务适配仍交给可训练的低秩分支。原论文还涉及 NF4 量化、对量化所需常数再压缩的双重量化,以及管理内存的分页优化器。这里的“四比特”说的是基础权重怎样存放,不等于所有乘法、适配器参数都用四比特计算。本文只把数值舍入到间距相同的网格上,演示这种离散化会带来误差;它没有实现 NF4,也没有验证完整 QLoRA 训练效果。
文中这几个词,先认一认
- 低秩增量
- 用两个较小矩阵 A、B 的乘积 BA 来表示权重的改动,相当于让新增分支先经过 r 维的中间层,再回到所需输出维度。改动矩阵的秩最多为 r,限制的是这次权重改动的结构,不是把原始输入样本永久变成 r 维。
- 缩放alpha
- 控制
LoRA新增分支影响有多大的设置。普通LoRA使用 alpha/r作为缩放,其他变体可能采用不同规则。 - 合并
- 把浮点增量
sBA加进原权重,推理时用一个线性矩阵完成计算。合并前要明确数据类型和量化如何处理。
02看看它怎么工作
常见线性 LoRA 会把 A 随机初始化,把 B 全设为零。这样初始 BA 为零,新增分支还没有改变基础层输出。第一次反向时,A 的梯度包含 B,通常为零;B 的梯度包含 A 和输入,可以非零。先更新 B 后,A 才能获得学习信号。若 A、B 都设为零,两边梯度都可能为零,分支就学不动。这个说明只针对普通线性分支和这里的初始化,不能推广到所有 PEFT 变体。
实验冻结一个二乘三基础矩阵,构造已知低秩目标增量,通过均方误差只更新A和B。先核对基础输出完全一致,再核对第一次梯度,最后训练并验证目标误差下降。训练结果只是这个合成线性问题可拟合的证据,不是模型已经获得通用指令能力。检查基础权重不变,比单看可训练参数数量更能发现误冻结。
合并等价来自线性代数:Wx+sB(Ax)等于(W+sBA)x。推理时关闭dropout,使用相同缩放与矩阵方向才能对照。浮点舍入会有小误差;量化基础权重需要明确反量化、合并、再量化的流程,重新量化又会引入新误差。训练过程中带dropout的分支不能简单用固定合并矩阵替代其随机行为。
先跑通一个最小例子
运行 experiment.py,在CPU拟合合成线性任务。最后几行只做均匀低位网格演示,不实现NF4,不加载预训练权重。
展开收起代码 · experiment.py
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import torch
torch.set_num_threads(1)
torch.manual_seed(30)
w = torch.randn(2, 3)
base_copy = w.clone()
a = torch.nn.Parameter(torch.randn(1, 3) * 0.1)
b = torch.nn.Parameter(torch.zeros(2, 1))
x = torch.randn(32, 3)
delta = torch.tensor([[0.4], [-0.2]]) @ torch.tensor([[1., -1., 0.5]])
target = x @ (w + delta).T
def forward():
return x @ w.T + (x @ a.T) @ b.T
assert torch.equal(forward(), x @ w.T)
optimizer = torch.optim.Adam([a, b], lr=0.08)
initial = (forward() - target).square().mean()
initial.backward()
# 零B使首次A梯度为零,但B仍可获得梯度
assert torch.count_nonzero(a.grad) == 0
assert torch.count_nonzero(b.grad) > 0
assert w.grad is None
optimizer.zero_grad(set_to_none=True)
for _ in range(160):
optimizer.zero_grad(set_to_none=True)
loss = (forward() - target).square().mean()
loss.backward()
optimizer.step()
final = (forward() - target).square().mean().detach()
assert final < initial.detach() * 0.001
assert torch.equal(w, base_copy)
# 线性分支在无dropout且同精度下可以合并
merged = w + b @ a
assert torch.allclose(forward(), x @ merged.T, atol=1e-6)
# 对称均匀15级网格可装入4bit编码,但它不是NF4
values = torch.linspace(-1., 1., 101)
scale = values.abs().max() / 7
quant = (values / scale).round().clamp(-7, 7)
recovered = quant * scale
assert (values - recovered).abs().max() <= scale / 2 + 1e-6
print(f"8 LoRA and uniform quantization checks passed; loss {initial.item():.6f} -> {final.item():.6f}")
核对运行结果八项检查通过:基础权重不变,首次A梯度零、B非零,合成目标误差下降,合并结果一致。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart LR
X["输入x: din"] --> W["冻结W: dout乘din"]
X --> A["可训练A: r乘din"]
A --> B["可训练B: dout乘r"]
B --> S["缩放alpha除r"]
W --> Y["两分支相加"]
S --> Y
Y --> L["损失反向仅更新适配器"]按顺序看,每一步做了什么
- 写清基础层输入输出维度、rank和缩放约定。
- 冻结W,随机初始化A并将B清零。
- 检查起点输出、首次梯度与基础权重不变。
- 训练后在推理模式下比较分支与合并结果,并单测量化误差。
03对照实现代码
PEFT v0.17.1 · LoraLayer.update_layer;reset_lora_parameters;Linear.forward;get_delta_weight
下例为原创无偏置、无dropout的普通LoRA教学模块,不覆盖PEFT的量化、DoRA、rsLoRA或多适配器分支。
import torch
class TinyLoRA(torch.nn.Module):
def __init__(self, weight, rank, alpha):
super().__init__()
# 基础权重保留为缓冲,明确不交给优化器
self.register_buffer("weight", weight.clone())
self.a = torch.nn.Parameter(torch.randn(rank, weight.shape[1]) * 0.01)
# B清零保证增量初始为零,A保留非零学习通道
self.b = torch.nn.Parameter(torch.zeros(weight.shape[0], rank))
self.scale = alpha / rank
def forward(self, x):
# 行向量批次要使用矩阵转置,与列向量公式一致
return x @ self.weight.T + self.scale * (x @ self.a.T) @ self.b.T
update_layer为普通线性层创建输入到r和r到输出的两个投影;默认缩放与rsLoRA分支不同,配置必须随适配器一起保存。
普通线性reset_lora_parameters将A按指定方式初始化,B置零;Embedding和特殊初始化路径不一定沿用相同布局。
未合并forward先运行基础层,再加入经过dropout、A、B与缩放的增量分支。
get_delta_weight使用B乘A形成增量,并处理权重方向与dtype;合并后仍要做数值及任务评测。
代码先调用谁,再调用谁
- 配置目标层并建立A、B与缩放
- 初始化适配器后forward叠加增量
- 推理合并通过
get_delta_weight生成权重增量
04动手写一个例子
低秩分支的矩阵向量计算与合并对照
给定W[dout][din]、A[r][din]、B[dout][r]、x[din]与alpha,计算Wx+(alpha/r)BAx;拒绝不匹配和非有限输入。
- 输入 1
W为2维单位矩阵,A=[[1,2]],B=[[3],[4]],x=[1,1],alpha=1- 输出 1
[10,13]
先验证矩阵的矩形形状和每个数值,避免维度偶然相同掩盖转置错误。
先计算中间向量Ax,长度只有r;再乘B得到低秩输出,避免为单次向量计算显式构建完整BA。
基础输出与缩放增量逐元素相加。与显式合并矩阵做测试,能同时核对顺序、缩放和维度。
为什么这样写是对的完成第k个低秩通道后,每个输出已累加恰好前k个B列对应的A投影贡献;基础分支保持不变。
分别计算W乘x、A乘x与B乘中间向量;不创建dout乘din的增量矩阵。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
public class Main {
static void matrix(double[][] m,int rows,int cols){
if(m.length!=rows)throw new IllegalArgumentException("rows");
for(double[] row:m){
if(row==null || row.length!=cols)throw new IllegalArgumentException("cols");
for(double value:row)if(!Double.isFinite(value))throw new IllegalArgumentException("value");
}
}
static double[] apply(double[][] w,double[][] a,double[][] b,double[] x,double alpha){
int in=x.length,out=w.length,r=a.length;
if(in==0 || out==0 || r==0 || !Double.isFinite(alpha))throw new IllegalArgumentException();
matrix(w,out,in);matrix(a,r,in);matrix(b,out,r);
for(double value:x)if(!Double.isFinite(value))throw new IllegalArgumentException();
double[] mid=new double[r],y=new double[out];
// 先计算低维投影,不显式构造完整增量矩阵
for(int k=0;k<r;k++)for(int j=0;j<in;j++)mid[k]+=a[k][j]*x[j];
for(int i=0;i<out;i++){
// 基础权重在计算中只读,不会被适配器修改
for(int j=0;j<in;j++)y[i]+=w[i][j]*x[j];
for(int k=0;k<r;k++)y[i]+=(alpha/r)*b[i][k]*mid[k];
// 有限输入仍可能算术溢出,结果必须再检查
if(!Double.isFinite(y[i]))throw new ArithmeticException("overflow");
}
return y;
}
static void check(boolean ok){if(!ok)throw new AssertionError();}
public static void main(String[] args){
double[][] w={{1,0},{0,1}},a={{1,2}},b={{3},{4}};
check(Arrays.equals(apply(w,a,b,new double[]{1,1},1),new double[]{10,13}));
check(Arrays.equals(apply(w,a,b,new double[]{1,1},0),new double[]{1,1}));
check(Arrays.equals(apply(w,a,new double[][]{{0},{0}},new double[]{1,1},1),new double[]{1,1}));
double[][] merged={{4,6},{4,9}};
check(Arrays.equals(apply(merged,a,new double[][]{{0},{0}},new double[]{1,1},1),
apply(w,a,b,new double[]{1,1},1)));
try{apply(w,a,new double[][]{{1,2},{3,4}},new double[]{1,1},1);throw new AssertionError();}
catch(IllegalArgumentException expected){}
try{apply(w,a,b,new double[]{Double.NaN,1},1);throw new AssertionError();}
catch(IllegalArgumentException expected){}
System.out.println("6 low-rank update checks passed");
}
}
这些特殊情况也要试一试
- rank、输入和输出维度都必须为正。
- alpha为零只保留基础分支,但仍验证输入协议。
- 有限数字累加也可能溢出,结果非有限时显式失败。
进阶选读 · 多个适配器与量化合并
场景同一基础模型挂多个任务适配器,希望减少推理分支又保持任务隔离。
做法保留适配器与基础版本配对信息,在浮点参考上验证各分支之和;如果再量化,重新测量输出误差和任务指标,不把不同rank的缩放直接忽略。
复杂度显式构建单个BA需要O(dout·din·r)计算与O(dout·din)增量空间;在线分支成本则随各rank之和增长。
好处与代价合并减少分支调用但损失快速切换便利,量化再合并可能增加误差且影响共享基础权重的复用。
05项目里怎么用
选择目标层应基于任务和容量,而不是只套一份层名列表。注意力投影、输出投影与前馈网络适配会有不同参数量和表达能力;层名需要在实际模型结构中核对。训练前打印可训练张量名称与数量,检查基础参数是否意外进入优化器,保存rank、alpha、dropout、目标层及基础模型版本。
LoRA 减少了需要训练的参数,也可能省下对应梯度和动量等优化器记录;但此时占内存最多的,可能变成了每层为反向计算保留的中间结果,也就是激活。长序列仍可能需要分批累积梯度,或少存中间结果、需要时重算。基础权重冻结后,前向计算和部分反向传递依然存在。比较内存时,还要分清权重的存储编码、运算时的数据类型 dtype、适配器的数据类型,并计入量化比例尺等附加信息,不能只算“参数个数乘四比特”。
评估需要覆盖基础能力保留、领域任务与格式遵循。rank增加并不必然提升泛化,低数据场景仍可能过拟合。部署合并前后先比较固定输入logits,再比较完整任务指标;保存未合并适配器便于回滚和排查。本文合成线性损失下降不能外推为真实语言模型质量或显存节省量。
容易踩的坑
- A和B同时清零使普通分支失去学习信号。
- 把基础网络整体
no_grad,截断更早适配器的梯度。 - 将简单均匀四比特量化称为NF4或完整
QLoRA。 - 合并时忘记alpha
/r、转置或量化再编码误差。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 全参数微调 | 更新空间最完整 | 梯度与优化器状态较大 | 资源充足且需要广泛适应 |
浮点LoRA | 实现和数值对照较容易 | 基础权重容量仍存在 | 低成本适配的首个基线 |
量化基础加LoRA | 进一步压缩基础存储 | 内核依赖与量化误差更复杂 | 已验证兼容性的容量受限训练 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01为什么新增参数是r(din+dout)?
展开参考答案与推导
A有r乘din个元素,B有dout乘r个元素,两者相加;不含可选偏置。
02B为零时第一次A梯度为何通常为零?
展开参考答案与推导
链式法则中传回A的梯度经过B转置,零B使该项为零;B的梯度仍可依赖非零A和输入。
03四比特权重是否说明全训练都用四比特?
展开参考答案与推导
不是;存储编码与反量化后的计算dtype、适配器和优化器状态精度不同。
换个情况,还会处理吗
01适配器训练损失完全不变先查什么?
展开参考答案与推导
检查可训练参数是否进优化器、梯度是否被no_grad截断、是否A和B同时为零,以及损失是否有有效目标。
02合并后输出不同如何定位?
展开参考答案与推导
先关闭dropout,使用同dtype浮点权重,核对矩阵方向与缩放,再单独检查反量化和再量化误差。
03LoRA仍然OOM怎么判断原因?
展开参考答案与推导
分别统计基础权重、适配器状态、激活与临时峰值;如果激活主导,应检查长度、微批次和重计算策略。
写代码试一试
01计算适配器参数数。
- 输入 / 条件
din=4096,dout=4096,r=8,无偏置- 输出 / 预期
65536
提示 把两个矩阵分别计数。
展开参考答案与推导
8×4096+4096×8=65536。
02计算低秩输出。
- 输入 / 条件
W=I,A=[1,2],B=[3,4]转置,x=[1,1],s=1- 输出 / 预期
[10,13]
提示 先计算Ax。
展开参考答案与推导
Ax=3,BAx=[9,12],加基础输出[1,1]。
03给出均匀量化误差检查。
- 输入 / 条件
网格步长s,数值均在未饱和范围- 输出 / 预期
舍入误差不大于s/2,容忍浮点误差
提示 最近网格点距离。
展开参考答案与推导
验证最大绝对误差小于s/2加数值容差;饱和截断时不能使用这个界。
读完之后,独立完成
课后作业:自己做一遍
本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能计算LoRA参数量,核对零初始化首次梯度,训练低秩线性增量并验证合并输出。
本篇重点加练
低秩参数化、缩放与合并定义了同一函数的不同表示,需要加强等价条件审计。
- 低秩矩阵分解
- alpha
/r缩放 - 适配器合并条件
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
输入维度 6、输出维度 4、rank=2、alpha=6。写出低秩前向各中间形状、参数量及缩放位置,给一个手算小输入。
- 提交内容
- 形状图及计算记录
- 怎样算完成
- 矩阵方向一致
- 额外参数与基础参数分开
练到的知识点低秩矩阵分解 · A和B的形状 · alpha/r缩放
- 作业 02原理问答
比较 A、B 同时为零、只 B 为零、两者随机三种初始化下的初始输出与梯度条件;用符号表达说明判断所依赖的上游梯度。
- 提交内容
- 条件分析表
- 怎样算完成
- 冻结参数范围明确
- 不假定上游梯度永不为零
练到的知识点冻结基础权重 · 零B初始化和梯度
- 作业 03原理问答
把 A 乘以非零常数 c、B 除以 c,分析前向增量与两因子梯度是否具有相同变化;明确缩放 alpha
/r保持不变。- 提交内容
- 前向与梯度条件推导
- 怎样算完成
- 参数化与函数分开
- c 的合法范围明确
练到的知识点低秩矩阵分解 · alpha/r缩放
综合应用
- 作业 04编程实践
实现 CPU 低秩线性层,在训练前后检查基础权重未变;保存适配器并重载,比较未合并与合并状态的输出。
- 提交内容
- 实现、保存文件与误差报告
- 怎样算完成
- 缩放配置随适配器保存
- 合并前置条件完整
练到的知识点A和B的形状 · alpha/r缩放 · 冻结基础权重 · 适配器合并条件
- 作业 05排错实验
故意遗漏适配器参数注册、加载错误 alpha、重复合并三种错误,构造能够分别发现问题的测试。
- 提交内容
- 故障代码与检测记录
- 怎样算完成
- 各故障有独立证据
- 加载后不仅检查形状
练到的知识点零B初始化和梯度 · 适配器合并条件
- 作业 06面试场景
评审“基础权重四比特,所以梯度、激活和优化器也各占四比特”的预算表;重建按状态分类的估算并列出未知项。
- 提交内容
- 预算修订及 dtype 表
- 怎样算完成
- 存储和计算类型分开
- NF4 不等同任意均匀量化
练到的知识点量化存储与计算dtype · QLoRA的NF4与其他机制
- 作业 07编程实践
在 CPU 上训练两个不同 rank 的小适配器,再把各自增量转成稠密矩阵,比较秩、输出与合并结果;使用不超过 6×6 的权重。
- 提交内容
- 小矩阵实验与表示对照
- 怎样算完成
- 原权重和增量分别保存
- 矩阵秩测定容差记录
练到的知识点低秩矩阵分解 · 适配器合并条件
- 作业 08排错实验
适配器保存文件缺少 alpha 或目标层标识。设计加载校验和故障样例,判断仅凭 A
/B形状能否安全合并。- 提交内容
- 缺字段测试与加载契约
- 怎样算完成
- 身份和缩放均核验
- 未知配置不静默猜测
练到的知识点alpha/r缩放 · 适配器合并条件
- 作业 09设计评审
要叠加两个同基础模型训练的适配器,自行定义叠加权重及是否保留独立参数。评审可合并性、rank 变化和任务质量需要的证据。
- 提交内容
- 组合方案及验收计划
- 怎样算完成
- 代数可合并与任务有效分开
- 基础权重身份一致
练到的知识点低秩矩阵分解 · 适配器合并条件
进阶挑战 · 掌握前置后选做
- 作业 10设计评审
用 CPU 小矩阵比较两个 rank 和两种自定义量化粒度的拟合误差;明确该实验与完整
QLoRA训练在机制及资源验证上的差距。- 提交内容
- 受控实验与边界说明
- 怎样算完成
- rank 和量化因素分别控制
- 未实现机制不宣称已验证
练到的知识点低秩矩阵分解 · 量化存储与计算dtype · QLoRA的NF4与其他机制
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 11思维拓展 · 反例构造
“同 rank 的两组
LoRA因子表达同一增量,就会按相同轨迹优化”是否成立?构造不同因子尺度但初始函数相同的 CPU 对照。- 提交内容
- 等价初值及更新轨迹
- 怎样算完成
- 优化器设置一致
- 函数等价与优化等价分别检验
练到的知识点低秩矩阵分解 · alpha/r缩放
- 作业 12思维拓展 · 开放探究
将多个低秩增量合成为一个更低 rank 近似,会引入什么可测误差?在小矩阵上分别评估权重误差和任务输出误差,不要求量化 GPU 工具链。
- 提交内容
- 近似实验与误差定义
- 怎样算完成
- 两类误差不混用
- 近似 rank 和数据分布明确
练到的知识点低秩矩阵分解 · 适配器合并条件
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
LoRA 原论文 (新标签页)↗
阅读低秩更新、缩放与初始化的基本机制。
- 深入参考文档英文
QLoRA 原论文 (新标签页)↗
区分NF4、双重量化和分页优化器,不把普通量化演示当完整复现。
- 深入参考文档英文
PEFT0.17.1 LoRA 层实现 (新标签页)↗
核对线性A
/B形状、初始化、默认缩放与合并实现。