🦙 第3章 微调 Llama 2 大模型
📄 一句话概括

微调要决定的第一件事不是超参,是动多少参数。 全参微调要存梯度和优化器状态,显存是权重的十几倍; LoRA 只在旁边加一条低秩通道,可训参数不到百分之一。 这一节把两笔账算清楚,再说清 LoRA 为什么能成立——答案在权重矩阵的奇异值谱上。

📝 本章和前面几章的关系

上两节备好了数据和模型,这一节开训。 梯度怎么回传,43 课 深度学习入门教室 里讲过; 这一节讲的是当参数量到了 67 亿,同一套梯度下降会遇到什么——不是算法变了,是账变了。

一、全参微调:算的不是参数,是那十二个字节

一个参数在训练时要占四份内存:权重本身两字节、梯度两字节、AdamW 的一阶矩四字节、二阶矩四字节。 加起来十二字节。 67 亿参数乘十二字节,是七十多 GB,还要加上 fp32 的主权重副本和激活。 所以 7B 全参微调要一百 GB 级别的显存——不是模型大,是优化器状态大。 省显存的办法有几种:混合精度(主权重用 fp32、计算用 fp16)、梯度累积(小批多次,凑出大批)、 梯度检查点(不存中间激活,反向时重算)。

梯度检查点这一项值得单独说:它用算力换显存,省多少、慢多少都跟模型和批大小有关,没有通用数字。 要用就自己量一遍,别信别人的百分比。

二、LoRA:不改原矩阵,只加一条修正通道

LoRA 的思路是:微调时权重的变化量是低秩的。 既然变化量秩低,就不用存整个大矩阵,只存两个瘦长的矩阵 B 和 A,让它们的乘积去近似那个变化。 前向传播变成「原矩阵的输出 + B 乘 A 的输出再乘一个缩放系数」。 缩放系数是 alpha 除以秩,调它等于调这条通道的强度。 初始化有个讲究:B 全零、A 随机。 这样训练开始时修正项是零,模型行为跟基座完全一致,不会一上来就把已经学好的东西打乱。

演示里的账:秩取 8,一层只训 45 万个参数,三十二层合计 0.15 亿,占全参的 0.23%——比全参少 441 倍。 所以 LoRA 的产物是几十 MB,而基座是十几 GB。 这就是社区里传「一个 LoRA」的原因:一份基座配多个 LoRA,一个业务一个文件。

三、低秩为什么够用:去看奇异值谱

演示里做了两组对比。一组是谱衰减的矩阵——前几个奇异值大、后面迅速变小; 另一组是谱平坦的,所有奇异值差不多大。 秩取 8 去近似:谱衰减的那组误差 5.9%,谱平坦的那组误差 93.5%。 低秩近似对前者几乎无损,对后者基本等于扔掉。 真实权重的谱是衰减的:少数几个方向承载大部分能量。 这就是「低秩修正」能成立的前提,也是 LoRA 有效的原因。

反过来推:如果一个任务要改的东西特别「满」——比如换一整套知识、换一种语言,秩就得往上涨, 涨到一定程度全参微调反而划算。

四、超参:LoRA 的学习率要比全参高一个量级

全参微调的学习率常见在万分之一到十万分之二;LoRA 常见在万分之一到万分之二。 差一个量级,原因是 LoRA 训的是新加的小矩阵,它们从零开始,需要更大的步子。 轮数通常一到三轮。指令微调的数据量小、重复训很容易过拟合,表现是训练损失一直降、验证损失开始涨。 批大小受显存限制时用梯度累积:批大小 8、累积 4 步,等效批大小 32。 注意学习率要跟着等效批大小调,否则等于偷偷改了步长。

预热步数用总步数的百分之三左右,让优化器先小步走,避免一上来就把权重推飞。

五、训练时盯什么

只看损失不够。要同时看三个:训练损失、验证损失、梯度范数。 训练损失平滑下降、验证损失跟着降,是正常;训练降验证涨,是过拟合,该停。 两个都不降,先查学习率和数据里的空答案。 梯度范数突然飙高,多半是某条数据特别长或者有异常值。这时候该去看数据,不是去调梯度裁剪。

再盯两个工程指标:每秒处理的 token 数(吞吐)、显存峰值。 吞吐掉一半通常是数据管道卡住了,不是模型的问题。

六、动手 Demo:LoRA 到底省了多少参数,秩卡住了什么

按 Llama 2 7B 的真实维度算:隐藏维 4096、注意力四路都是 4096×4096、前馈是 11008 那三块,三十二层。 然后算四个秩下的 LoRA 参数量,再看「秩到底卡住了什么」——用奇异值分解真算一遍低秩近似的误差。

D, F, L = 4096, 11008, 32

# 全参:注意力四路 + 前馈三块,每层这么多
per_layer = 4 * D * D + 3 * D * F
print(per_layer * L / 1e8, '亿')        # 64.76 亿

# LoRA:每个挂载模块加两个瘦矩阵 B(d x r)、A(r x d)
for r in (4, 8, 16, 64):
    one = 2 * D * r * 4                 # 挂 q、k、v、o 四路
    print(r, one, one * L / 1e8)

# 秩卡住了什么:SVD 看前 r 个奇异值能还原多少
U_, s, Vt = np.linalg.svd(Wm, full_matrices=False)
Wr = (U_[:, :r] * s[:r]) @ Vt[:r]
err = np.linalg.norm(Wm - Wr) / np.linalg.norm(Wm)

实跑输出:

--------------------------------------------------------------------
LoRA 到底省了多少参数,秩卡住了什么
--------------------------------------------------------------------


  ① 全参微调要训多少
  --------------------------------------------------------------------
  7B 一层:注意力 6711 万,前馈 13527 万,合计 20238 万。
  三十二层合计 64.76 亿参数要训——每一个都要存梯度和优化器状态。
  ! 按 fp16 算,光梯度就 12.1 GiB,加上 AdamW 的两个矩再翻倍。

  ② LoRA 只训两个小矩阵
  --------------------------------------------------------------------
| 秩 | 一层要训 | 三十二层合计 | 占全参的比例 |
|---|---:|---:|---:|
| r = 4 | 229376 | 0.07 亿 | 0.113% |
| r = 8 | 458752 | 0.15 亿 | 0.227% |
| r = 16 | 917504 | 0.29 亿 | 0.453% |
| r = 64 | 3670016 | 1.17 亿 | 1.813% |

  秩取 8 时,可训参数占全参的 0.23%——比全参少了 441 倍。
  ! 这就是 LoRA 的全部魔法:不改原来的矩阵,只在旁边加一条低秩的「修正通道」。

  ③ 为什么叫低秩,秩卡住了什么
  --------------------------------------------------------------------
  B 是 64 × 8,A 是 8 × 64,乘出来是 64 × 64,但秩最多 8。
  奇异值前八个:8.041、6.846、6.366、5.461、5.123、4.849、4.458、3.892
  第九个到第十一个:9.83e-16、7.90e-16、7.53e-16
  ! 从第九个起全是零(数值上是 1e-16 量级的噪声)。
  这就是「秩为 8」的意思:这个矩阵只能用八个方向来描述,多一个都没有。

  ④ 秩不够会怎样:两种矩阵,两种命运
  --------------------------------------------------------------------
| 秩 | 谱衰减的矩阵 | 谱平坦的矩阵 |
|---|---:|---:|
| r = 1 | 37.8% | 99.2% |
| r = 2 | 22.3% | 98.4% |
| r = 4 | 11.8% | 96.8% |
| r = 8 | 5.9% | 93.5% |
| r = 16 | 2.8% | 86.6% |
| r = 32 | 1.2% | 70.7% |

  秩 8 去近似一个谱衰减的矩阵,误差只有 5.9%;去近似一个谱平坦的,误差 93.5%。
  真实权重的奇异值谱是衰减的——少数几个方向承载大部分能量。这就是「低秩修正」能成立的前提。
  ! 所以 LoRA 有效不是因为它聪明,是因为权重矩阵本来就没那么「满」。
  反过来,如果一个任务要改的东西特别「满」(比如换一整套知识),秩就得往上涨,
  那时候全参微调反而划算。

  ⑤ 挂在哪几层
  --------------------------------------------------------------------
| 挂法 | 模块数 | 评价 |
|---|---|---|
| 只挂 q、v | 2 个模块 | 最省,效果打折 |
| 挂 q、k、v、o | 4 个模块 | 常用,性价比高 |
| 再加 gate、up、down | 7 个模块 | 效果最好,参数多一倍 |

  原论文的结论是挂 q 和 v 就有不错的效果,后来的实践发现全挂更稳。
  ! 这一项没有标准答案,跟任务和数据量有关。
  别信「最佳配置」,在自己的数据上跑两组对比,比看十篇博客管用。

①算的是全参微调的账:一层两亿多参数,三十二层 64.76 亿,每个都要存梯度和优化器状态。 按 fp16 算光梯度就 12.1 GiB,这还只是梯度,不含权重和优化器。 ②是 LoRA 的账。秩取 8,可训参数占全参的 0.23%,少 441 倍。 而且这个比例随秩线性涨——秩翻倍,参数翻倍,不像全参那样一涨就是几亿。 ③把「秩」这个词落地了:B 是 64×8、A 是 8×64,乘出来是 64×64,但秩最多 8。 奇异值列表里前八个是正常数,从第九个起全是 1e-16 量级的噪声。 这就是「秩为 8」的意思:这个矩阵只能用八个方向描述。 ④是整节最有信息量的一张表。同一个秩 8,谱衰减的矩阵误差 5.9%,谱平坦的 93.5%。 低秩近似能不能用,取决于被近似的矩阵「满不满」。 真实权重的谱是衰减的,所以 LoRA 能成立。这不是因为它聪明,是因为权重矩阵本来就没那么满。 ⑤是挂载位置。只挂 q、v 最省,效果打折;挂 q、k、v、o 常用;全挂效果最好、参数翻倍。 这一项没有标准答案,得在自己数据上跑两组对比。

30 秒实验

  1. 把②里的秩从 64 改成 128、256,看参数占比涨到多少。再想想:如果真的涨到 10%,LoRA 还有意义吗?
  2. 把⑤里的挂载模块从 4 路改成 7 路(加前馈三块),重算参数占比。 前馈的维度是 11008,加进去之后涨幅比你想的大吗?
  3. 把④里的谱衰减指数从 1.6 改成 0.5,重看秩 8 的误差。 这个实验说明「LoRA 有效」是有条件的——条件是什么?

常见误区

  1. 以为秩越大越好。秩越大参数越多、越容易过拟合,而收益很快到顶。 秩是个要试的超参,不是个要拉满的旋钮。
  2. 把全参微调的学习率照搬到 LoRA。 差一个量级,照搬的结果是损失几乎不动,你以为数据有问题,其实是步子太小。
  3. 只看训练损失降了就收工。 指令微调数据量小,一两轮就过拟合很常见,必须同时看验证损失, 否则保存下来的是「背下了训练集」的那个版本。
自测三问(点开对答案) 1. LoRA 训练时为什么把 B 初始化成全零? —— 这样训练开始时修正项 B 乘 A 是零,模型行为跟基座完全一致。 如果两个都随机初始化,一上来就给权重加了一个噪声修正,已经学好的能力会被打乱。 2. 「秩为 8」到底限制了什么? —— 限制了这个矩阵只能用八个方向来描述。 它能表达的变化量落在八个基向量张成的子空间里,超出这个子空间的部分无论训多久都学不到。 3. 什么情况下该放弃 LoRA 改全参微调? —— 当要改的东西特别「满」——换一整套知识、换语言、换任务形态,低秩近似误差很大, 秩得涨到接近满秩。那时候 LoRA 的参数和显存优势都没了,全参更划算。

训练跑完了,接下来最容易被跳过的一步是——判断它到底有没有变好。下一节讲怎么量。

相关笔记