微调工程
第 17 课:微调工程
前面几课我们一直在"把模型跑起来"——切分、省显存、量化。这一课换个目标:把通用模型调成你自己的助手。但全量微调在显存上先撞墙(7B 要 112 GB),于是有了 LoRA(即冻结基座、只训一对低秩矩阵)。这一课把 LoRA 的显存账、三个旋钮、QLoRA(即把冻结的基座也用低精度存、再训一小对适配器)的三件套算清,最后回答一个比"怎么调"更重要的问题:什么时候该微调、什么时候该用检索、什么时候改提示词就够了。
② 用 ΔW = BA 算出 LoRA 把参数量压到了几分之一,并理解 rank / alpha / target_modules 各在调什么
③ 算清 QLoRA 三件套各省多少,并给出"该微调、该 RAG(即检索增强,把外部资料检索出来塞进上下文)、还是该改提示"的判断
前几课我们把模型"跑起来"了;从这一课开始,我们把它调成自己的。第一步不是讲方法,而是先把"为什么必须换方法"算成数字。
很多人的直觉是"微调要存模型权重,所以显存 ≈ 模型大小"——这个直觉错得很远。训练时每个参数要额外背两样东西:它的梯度 gradients,以及优化器为它维护的状态(Adam 有两份动量,再加一份高精度主权重)。三项加起来,一个参数在训练时要占 16 字节,而它本身只有 2 字节。这就像搬一次家:真正要带走的东西不多,但为了搬运,你得给每件家具都配一辆手推车和一个临时仓储位。
更自然的估法是只按权重算:7B 权重 7e9 × 2 B = 14 GB,一张 80 GB 卡看着绰绰有余;可一旦把梯度与优化器状态算进来,同一个模型要 7e9 × 16 B = 112 GB,比 80 GB 卡还多出 32 GB——「按权重估」这套更省事的算法,正是全量微调撞墙的第一个原因。
算出这笔账,才会明白"全量微调"三个字在显存上意味着什么。
| 项 | 每参数字节 | 占比 |
|---|---|---|
| 权重(bf16) | 2 | 12.5% |
| 梯度 gradients(bf16) | 2 | 12.5% |
| 优化器状态(fp32 master 4 + Adam m 4 + v 4) | 12 | 75% |
| 合计 | 16 | 100% |
| 模型 | 参数量 | 16 B 口径(本课) | 12 B/参数(本课反推口径,非论文原文) |
|---|---|---|---|
| 7B | 7e9 | 112 GB | 84 GB |
| 13B | 13e9 | 208 GB | 156 GB |
| 65B | 65e9 | 1,040 GB | 780 GB |
| 项 | 数值 | 说明 |
|---|---|---|
| 175B 单份权重量 | 175e9 × 2 B = 350 GB | 只算权重 |
| 10 个下游任务 | 350 × 10 = 3,500 GB | 每个任务都要一份完整副本 |
| 权重占训练显存比例 | 2 ÷ 16 = 12.5% | 训练贵、部署也贵 |
全量微调撞墙的根因有两层:训练时贵在状态(16 B/参数里 12 B 是优化器与 master),部署时贵在副本(每个任务一份完整权重)。所以任何"省"的办法都必须同时回答这两问——下一张卡片的 LoRA,正是从砍掉大部分参数的梯度与优化器状态入手。
上一张卡片算清了全量微调为什么贵。这一张回答 LoRA 的核心疑问:既然要学"这个任务带来的改变",为什么不直接学整个 ΔW?
关键在于"改变"这件事本身有多大。因为基座已经把语言能力学完了,微调要注入的往往只是少数几个方向上的调整——格式、语气、领域术语。这类改变不需要动用 4096 × 4096 那么大的自由度——就像调一张照片往往只用几个滑块(亮度、对比度、色温),而不是逐个像素去改;要注入的改变只落在少数几个方向上。用矩阵语言说,就是要求更新矩阵 ΔW 的秩很小。
直接学整个 ΔW 的代价是实打实的:一个 4096×4096 投影就有 16,777,216 个数,每个都要背梯度与优化器状态——这正是上一张卡片 112 GB 里最贵的那部分。低秩之所以「不丢信息」,不是因为压缩掉了精度,而是因为要注入的改变本来就只占少数几个方向;只有当任务本质上需要满秩改写时,低秩才会真的够不到——这条边界留到第 5 张卡片展开。
还有一个反直觉点:rank 不是越大越好。参数量随 rank 线性涨——同一个 4096×4096 投影,r=1 时 8,192 个、r=64 时 524,288 个(涨 64 倍);而且低秩约束本身就是一种正则,盲目加大 rank 等于放大自由度、在数据有限时更容易过拟合(LoRA 综述 arXiv:2407.11046 明确「higher is not always better」,过大的 rank 会造成性能与效率双重退化)。
把"只沿少数方向走"写成矩阵语言,就是令 ΔW 的秩很小——下面把数字算出来。
| 项 | 形状 | 参数量 |
|---|---|---|
| 全量更新 ΔW | 4096 × 4096 | 16,777,216 |
| 因子 | 形状 | 参数量(r = 8) |
|---|---|---|
| A(降维) | 8 × 4096 | 32,768 |
| B(升维) | 4096 × 8 | 32,768 |
| 合计 | — | 65,536 |
| rank r | ΔW = BA 参数量 | 相对全量 | 直觉 |
|---|---|---|---|
| 1 | 8,192 | 1/2048 | 只改一个方向 |
| 2 | 16,384 | 1/1024 | 论文里"对 175B 已够"的量级 |
| 8 | 65,536 | 1/256 | 实践最常见起点 |
| 16 | 131,072 | 1/128 | 任务更复杂时 |
| 64 | 524,288 | 1/32 | 接近全量的表达能力 |
LoRA 不是"近似地学一遍全量",而是换了一个受约束的参数空间:它假设"任务需要的改变落在少数方向上"。这个假设在指令微调上通常成立(所以能 256 倍地省),但它也是一个能力上界——改变如果本质上需要全秩,LoRA 就够不到(第 5 张卡片展开)。B 初始化为 0 这个细节也别忘了:它保证训练从"等于基座"出发,而不是从随机扰动出发。
上一张卡片把 LoRA 压缩到 256 倍;这一张回答一个更常见的困惑:为什么同样的 LoRA,别人调得动、你调不动。
因为LoRA 不是一个参数,而是三个旋钮的组合:rank(多大容量)、alpha(更新缩放多大)、target_modules(改哪些层)。这三者的影响并不均等——只调 rank 而忽略另外两个,很容易出现"rank 翻了几倍、效果却没变"的情况。可以把它类比成调音台:推子(alpha)决定音量大小,通道数(rank)决定能同时处理多少路信号,而"接哪几路输入"(target_modules)决定你改的是主唱还是伴奏。
下面把三个旋钮各自在调什么、以及影响排序算清楚。
| rank r | alpha | 实际缩放 α / r | 含义 |
|---|---|---|---|
| 8 | 16 | 2.0 | 常见基线(α = 2r) |
| 16 | 32 | 2.0 | rank 翻倍、缩放不变 |
| 16 | 16 | 1.0 | 更新更保守 |
| 8 | 8 | 1.0 | 小 rank 时更稳 |
| 覆盖范围(7B,r = 16) | 可训练参数 | 占 7B 比例 |
|---|---|---|
| 只加在 Q、V 投影 | 8.4M | 0.12% |
| Q、K、V、O 四个投影 | 16.8M | 0.24% |
| 再加 MLP(gate / up / down) | 40.0M | 0.57% |
| 旋钮 | 常见取值 | 作用 |
|---|---|---|
| lora_dropout | 0.05 ~ 0.1 | 防过拟合(小数据集尤其重要) |
| learning rate | 1e-4 ~ 2e-4 | 比全量微调大(可训参数少,容忍更大步长) |
| 训练轮数 | 1 ~ 3 轮 | 轮数一多,适配器就开始过拟合 |
三个旋钮里,target_modules(覆盖面)与 alpha/r(缩放)常比 rank(容量)更值得先调——这是工程经验,不是 LoRA 论文的定量结论(论文消融里只加 Wq、Wv 已可得最佳)。只加 Q、V 与加全部线性层,差距往往比 rank 从 8 调到 64 更大——这也是"别人 r=8 就够"的原因:他们把 LoRA 铺得更广。调参顺序建议:先把覆盖面和 α/r 定下来,再按验证集曲线决定要不要加 rank。
上一张卡片把 LoRA 调好了;这一张回到 65B 这个量级——为什么只训千分之几,还是训不动?
因为LoRA 省掉的只是"要更新的那部分",被冻结的基座仍然以 bf16 全量躺在显存里:65B × 2 B = 130 GB,光基座就超过任何单卡。更自然的想法是「可训练参数都压到千分之几了,显存不该再是问题」——但可训练的那部分只有约 3.2 GB,真正压在显存上的是这 130 GB 冻结基座。所以 QLoRA 的思路是:让冻结的那部分也用低精度存。这不是新算法,而是"4-bit 存储 + 16-bit 计算 + LoRA 训练"的组合——权重存成 NF4,用到时反量化回 bf16 参与计算,梯度(gradients)只回传给适配器,基座始终保持冻结。这就像搬家时把不用的家具先压缩打包(4-bit 存),真正要用时再拆开还原(反量化回 bf16)——省的是存储空间,不是搬运精度。
三件套各自解决一个问题,下面逐件算账。
| 项 | 65B 占用 | 说明 |
|---|---|---|
| 冻结基座(bf16) | 130 GB | 65e9 × 2 B |
| 可训练适配器(r=16,全线性) | 3.2 GB | 2.0e8 × 16 B |
| 合计 | 约 133.2 GB | 适配器占比很小,瓶颈全在基座 |
| 基座精度 | 每参数字节 | 65B 基座 | 相对 bf16 |
|---|---|---|---|
| bf16 | 2 | 130 GB | 1.0× |
| int8 | 1 | 65 GB | 2× |
| NF4 | 0.5 | 32.5 GB | 4× |
| 方案 | 常数开销 | 65B 常数占用 |
|---|---|---|
| 普通块量化(块 64、常数 FP32) | 32 / 64 = 0.5 bit/参数 | 65e9 × 0.5 ÷ 8 ≈ 4.06 GB |
| 双重量化(常数再量化为 FP8) | 8/64 + 32/(64×256) ≈ 0.127 bit/参数 | 65e9 × 0.127 ÷ 8 ≈ 1.03 GB |
| 节省 | 0.5 − 0.127 = 0.373 bit/参数 | 65e9 × 0.373 ÷ 8 ≈ 3.03 GB |
| 方案 | 7B | 65B |
|---|---|---|
| 全量微调(16 B/参数) | 112 GB | 1,040 GB |
| LoRA(基座 bf16) | 14 + 0.64 ≈ 14.6 GB | 130 + 3.2 ≈ 133.2 GB |
| QLoRA(基座 NF4 + 双重量化) | 3.5 + 0.11 + 0.64 ≈ 4.3 GB | 32.5 + 1.03 + 3.2 ≈ 36.7 GB |
QLoRA 的三件套分工很清楚:NF4 压基座(4×)、双重量化压常数(0.5 → 0.127 bit/参数)、分页优化器压峰值。它证明了一件重要的事——"4-bit 存储 + 16-bit 计算"可以在不牺牲训练质量的前提下把门槛降一个数量级(65B:>780 GB → <48 GB)。但它只解决"放不下";至于"该不该微调",是下一张卡片和最后一张卡片的问题。
上一张卡片解决了"放不下";这一张回答一个更容易被忽略的问题:LoRA 省下来的那些能力,是不是真的没代价?
因为LoRA 把更新限制在低秩子空间里,它就有一个天然的能力上界:如果某个任务需要的改变本质上不是低秩的,LoRA 再怎么调 rank 也够不到。这就像给房间重新刷墙——贴墙纸(低秩调整)很快,但要改承重结构(全秩改写)就不是贴纸能解决的了。Biderman 等的研究把这条界线画得很清楚:学"行为"(指令跟随、格式、语气)LoRA 够用;学"新知识"(代码、数学这类要在 token 规模上注入新知的继续预训练)全量更强。
更要紧的是第二半:LoRA 省的是"改变的幅度",换来的恰恰是"不容易忘"。所以它不是一个纯粹的妥协,而是学习与遗忘的交换。
| 场景 | LoRA(低秩) | 全量微调 |
|---|---|---|
| 指令微调(通用数据,如 Tülu-v2-mix) | 持平 | 持平 |
| 代码继续预训练(CPT) | 不如全量 | 更强 |
| 数学继续预训练(CPT) | 不如全量 | 更强 |
| 代码/数学的指令微调(IFT) | 低 rank 时落后 | 更强 |
| 条件 | 为什么输 | 对策 |
|---|---|---|
| rank 太小 | 容量不足,表达不了需要的改变 | 适度加大 rank 或扩大 target_modules |
| 数据量大 / 要学新知识 | 低秩约束成为瓶颈 | 改用全量微调或继续预训练 |
| 要改写全秩表示 | 低秩子空间根本够不到 | 提高 rank 或换方法(DoRA 等) |
| rank | 能学到的 | 对旧能力的遗忘 |
|---|---|---|
| 小(如 4 ~ 8) | 少 | 少 |
| 中(16 ~ 32) | 中 | 中 |
| 大(64+) | 多 | 多(但仍少于全量) |
LoRA 不是"更省的全量微调",而是一次明确的交换:用"只能学低秩改变"换来"参数量降三个数量级 + 更不容易遗忘"。判据一句话——任务是学行为(IFT)还是学新知识(CPT):前者 LoRA 通常够用甚至更好(还不容易忘);后者全量微调明显更强,LoRA 调 rank 也只是逼近它的代价。别把"LoRA 更省"误读成"LoRA 更好"。
前面五张卡片都在讲"怎么微调";这一张回答更前面的问题:到底该不该微调。
因为微调改变的是模型的行为(语气、格式、思维方式),而不是给它"查资料"的能力——所以"模型不知道我们公司的最新产品"这类问题,微调通常不是正确答案。这就像对待一个新员工:如果问题是"他不知道公司的最新制度",你应该给他一份手册(把资料放进上下文);如果问题是"他说话总是不符合客服规范",那才需要培训他的表达习惯(动权重)。把这两件事弄混,常见的代价是花几个月微调一个本该用检索解决的系统。
更自然的反应是「答不对就补训练」,但先做一次最便宜的判别:把正确的资料放进上下文,它能答对吗?能——那缺的是知识,这是一道检索题;不能——才轮到微调。跳过这一步,就会用一次训练去解决一个检索当天就能解决的问题。
下面把三条路摆开,并回答"微调之后怎么服务"。
| 手段 | 改什么 | 成本量级(估算) | 适合 |
|---|---|---|---|
| 提示(prompt) | 指令本身 | ≈ 0(只花人力) | 行为、格式、推理步骤 |
| RAG(检索增强) | 模型看到的 context | 中(建索引 + 检索链路) | 知识时效性、可溯源 |
| 微调(LoRA) | 权重里的行为 | 低(一次训练) | 稳定风格 / 固定格式 / 窄任务 |
| 症状 | 处方 | 为什么 |
|---|---|---|
| 不知道最新政策 / 老答错事实 | RAG | 缺的是知识,塞进 context 即可 |
| 格式总不稳定、要重试多次 | 微调 | 缺的是行为一致性 |
| 语气/品牌口径不统一 | 微调 | 风格是权重层面的习惯 |
| 既要最新知识、又要固定格式 | RAG + 微调 | 两者正交,可叠加 |
| 方式 | 做法 | 存储 / 适用 |
|---|---|---|
| 合并(merge) | Wmerged = W₀ + BA,形状不变 | 单任务,零额外推理延迟 |
| 多适配器共享基座 | 基座常驻,按请求切换适配器 | S-LoRA 报告可服务上千个适配器 |
这张卡片的决策顺序只有三步:① 先试提示;② 是"缺知识"就上 RAG(把资料塞进 context);③ 是"缺行为"才微调——两者都有就叠加。微调内部再选型:要稳定行为且资源有限 → LoRA;要学新知识/大数据 → 全量;要服务很多租户 → 多适配器共享基座。到这里,"训练与推理工程"这条线就只剩最后一环——把这些模型真正装配成一个能扛并发的推理引擎,那是下一课的主题。