很多人的第一次大模型微调,是在OOM报错里结束的。环境装好了,数据也整理得像模像样,一按开始训练,显存爆了;换成小batch、开梯度检查点,勉强跑起来,训完一测,模型答非所问。这不是运气问题,也不是显卡问题,而是还没建立一套完整的"微调体系"框架。训练阶段划分、数据类型、PEFT参数高效微调,这三件事像三根柱子,缺一根都会塌。
这篇文章是这套微调体系的第一篇,目标是把三根柱子一次讲透:训练阶段到底怎么划分、每个阶段要配什么形态的数据;从FP32到INT4的各种数据类型怎么选、显存怎么精确估算;以及LoRA这类参数高效微调方法为什么能用很小的开销撬动接近全参微调的效果。最后我会给出一套可以直接对照执行的组合方案,再复盘一次真实的小型微调过程。
适合谁看?准备在自己显卡上做领域问答、私有知识库、输出格式改造或风格迁移的开发者,以及跑通过一次教程、但换数据换任务就各种翻车的朋友。这篇文章不会教你点哪个按钮,而是让你理解每一步背后的取舍。
1. 训练阶段划分:先搞清模型走到哪一步,再决定微调策略
1.1 从预训练到对齐:一条不能跳级的成长路径
大模型的能力不是一次性长出来的,基本沿着"预训练→持续预训练→监督微调→偏好对齐"这条链逐步叠加。我用一个不太严谨但很好记的类比:预训练像让模型读万卷书,解决"知识怎么存进参数";监督微调像带着模型做大量问答练习,解决"知识怎么按你的要求说出来";偏好对齐则像社会化的礼仪训练,解决"什么该说、什么不该说、怎么说得让人舒服"。
很多人一上来就说"我要微调模型",但实际上"微调"这个词至少对应着三种完全不同的事情:
- 想让模型懂得某个垂直领域的专业内容,比如病历、法条、设备手册,这是持续预训练/领域预训练。目标是给模型补充知识和词汇,数据是纯文本,训练目标是普通的下一个词预测。
- 想让模型学会"指令-回答"的交互格式、学会调用工具、学会业务里定义的输出结构,这是监督微调(SFT)。数据必须是"指令/上下文 + 标准回答"的对,训练时只对回答部分计算损失。
- 想让模型在多个候选回答里偏好某种价值观、语气或安全边界,这是偏好对齐,常用DPO这类方法,数据是"同一个问题下的好回答/差回答"对比。
这三个阶段的数据形态完全不同、损失设计不同、训练步数和学习率也不同。把它们混为一谈是最容易出现的第一类翻车:你明明想让模型输出JSON格式,却拿了一堆纯文本语料去做持续预训练——跑完会发现模型更"懂"你的领域了,但根本不会乖乖按格式输出。反过来,你想补充大量新事实,却只做几千条SFT,模型只会记住这几千条问题的表面映射,换个问法就露馅。
这里要特别强调:阶段判断是微调策略的入口。后面PEFT往哪挂、挂多大规模、显存够不够、要训练几轮,都取决于你在哪个阶段做哪种微调。没有明确阶段就急着进数据工程,后面全是白工。
1.2 阶段决定数据形态、损失计算与配比
每个阶段对数据的要求是完全不同的,关键是"模型从数据里学什么"。
预训练和持续预训练阶段,数据就是一长段一长段的纯文本,模型的任务是预测下一个token。这个阶段没有"标准答案"的概念,只要是通顺、信息密度高的文本都可以拿来训练。但领域预训练有个容易踩的点:只用领域数据会把模型"带偏",出现灾难性遗忘——模型学会了病历术语,却忘了怎么写通用回复。实践经验是领域语料和通用语料按7:3或8:2混合,用通用语料兜住模型的通用能力。
SFT阶段的数据形态则完全不同,一定是成对的:一段输入(指令、上下文、历史消息)+ 一段标准输出。这里有一个很多新手不知道的细节:损失计算要对回答部分做mask(屏蔽)。也就是说,指令部分虽然参与前向计算,但不参与反向传播的loss计算。在常见的训练框架里,做法是把指令部分的label置为-100(ignore index),只有回答部分保留真实的token id。
如果不做mask,会出现一个很诡异的症状:训练loss降得很低,模型却变成了"复读机"——它会把你问的问题原样背出来,因为它在训练时把"提出问题"这件事也当成了要预测的目标。这个坑我见过太多次,排查到最后往往就是label构造的问题。
SFT数据配比也有讲究。如果任务类型多,类别之间最好做平衡采样,不要让某个任务占掉80%的数据。每条样本的长度也要控制,长回答和短回答混在一起训练时,长样本会拖慢收敛,因为它们占的token多、对梯度贡献大。
还有个容易忽略的细节:数据质量远大于数据量。5000条精心构造、格式统一、覆盖各种边界情况的指令数据,效果通常好过5万条从网上抓来的质量参差不齐的数据。造数据时先手工写50条覆盖任务边角的情况,跑一个小训练看效果,再决定要不要扩量,这个策略能省下大量时间。
1.3 每个阶段的训练节奏差异很大
训练阶段的差异不仅体现在数据和loss上,训练节奏也完全不同。
持续预训练要学的是新知识和新词汇,需要更大的数据量、更多的步数。一个小规模的领域预训练,语料至少是千万token量级,训练1到2个epoch就够,不需要反复看同一批数据,否则模型会被语料中的噪声带偏。学习率建议在1e-5到5e-5之间,要小一点,因为模型在预训练阶段已经把参数磨得很稳定,步子太大容易破坏已有能力。
SFT的学习对象是交互方式,任务相对简单,几千到几十万条样本就能看到明显效果。稳一点的配置是训练2到3个epoch,超过3个epoch在绝大多数任务里都会开始过拟合。全参SFT的学习率在1e-5到2e-5,如果使用PEFT方法,学习率可以放到1e-4到5e-4,这个差异背后的原因在讲PEFT时会详细展开。
偏好对齐阶段的数据量通常更小,几百到几千条偏好对就能做一次DPO训练,学习率和epoch数都要保守,因为对齐是在一个已经很听话的模型上做"微调中的微调"。
2. 数据类型:显存账本上的每一行都能省,也都会偷走精度
2.1 四种主流精度的真实定位
讲完阶段,第二个绕不开的话题是数据类型。很多教程会直接告诉你"用BF16"或"用INT4",但不说为什么。实际上数据类型的选择,本质是在"显存占用"和"数值精度"之间做交易。
| 数据格式 | 每参数字节数 | 表示范围 | 精度特性 | 典型用途 |
|---|---|---|---|---|
| FP32 | 4字节 | 很大 | 精确,几乎无损 | 全参微调的master weight、优化器状态 |
| FP16 | 2字节 | 窄(约±65504) | 容易溢出/下溢 | 老显卡上的混合精度训练 |
| BF16 | 2字节 | 与FP32相同 | 尾数精度低但范围大 | 现代显卡训练与推理的主流选择 |
| INT8 | 1字节 | 有限 | 约数 | 推理量化 |
| INT4/NF4 | 0.5字节 | 有限 | 比较粗糙 | QLoRA底座、推理量化 |
用记账来类比:FP32是逐笔精确记账,一分钱都不会错;FP16是只记到百位,数额大了就爆表;BF16是账本范围很大,但金额只保留大概;INT4就是"约等于几万块"这样的粗估。
关键点在于,训练场景下我们关心的不只是精度,还有数值范围。FP16虽然精度比BF16高一点,但范围太小,训练中激活值和梯度稍大就溢出,稍小就变成0。BF16牺牲了一部分尾数精度,却换来了和FP32相同的指数范围,这让它在训练里反而更稳,也因此成了现代显卡上训练大模型的主流选择。
2.2 以7B模型为例,把显存账本算一遍
很多人对显存的理解停留在"模型多大就占多大",但训练时的显存开销远不止权重本身。以一个7B参数的模型为例,我用粗算账的方式拆解。
先看权重:7B参数用BF16存储是14GB,用INT4/NF4量化后大约3.5GB。这是基础值。
再看全参微调。使用Adam优化器时,每个参数在显存里至少要存:FP32的参数副本(4字节)、一阶动量(4字节)、二阶动量(4字节),加上梯度(2或4字节),以及前向计算用的低精度参数副本。粗算下来每参数16字节是常态。7B×16就是112GB,这还没算激活值。这就是为什么全参微调在消费级显卡上基本是死路,它根本不是优化技巧能解决的问题,是账算不过来。
而PEFT冻结底座后,账本完全变了:底座权重的优化器状态为0,不需要保存梯度的完整图(只要反向传到LoRA矩阵就停);可训练参数从7B降到几百万到几千万。数值上,如果LoRA只挂q/v两层,可训练参数量大约是8M量级,以FP32存储加优化器状态也就几百MB,小到可以忽略。所以显存的大头还是底座权重本身加激活值。
激活值这块经常被忽略,但它才是显存爆掉的常见真凶。激活值大小和batch size、序列长度、层数直接相关。以7B模型、序列长度2048、batch为1估算,即便开了梯度检查点,激活值也会占掉2到6GB;不开检查点,这个数字会成倍上涨。所以如果你显存快满了,优先检查序列长度和梯度检查点,而不是只想着换量化格式。
综合算下来:一块24GB显存的显卡,可以跑BF16底座加LoRA,batch能做到2到4;16GB显存用QLoRA比较稳妥;8GB显存跑4bit底座加低秩LoRA可以勉强跑,但速度会肉眼可见地慢。这些数字不是绝对精确,架构不同会有浮动,但数量级是对的。
2.3 混合精度不是可选项,而是默认配置
现在几乎所有大模型训练都在用混合精度,核心思想是:不同角色用不同精度。
权重和梯度在计算时用FP16或BF16,因为它们只是计算中间量,精度稍微损失一点不影响大局;优化器状态用FP32保存,因为Adam的动量更新量非常小,用低精度保存会导致每次更新都被截断,累积起来就学不动了。这就像记账时流水可以记个大概,但总账必须精确,不然月底对不上。
FP16训练还有一个配套机制叫损失缩放(loss scaling)。因为FP16范围小,前向计算出的loss可能本身就下溢,需要先乘一个放大系数,反向传播算出梯度后再除回去。老显卡上用FP16混精训练时,如果loss突然变NaN,优先检查这个。
新一代显卡上直接选BF16混精,省掉了大部分loss scaling的麻烦。不过要提醒一句:较老的显卡不支持BF16,这时候只能退回FP16方案。
2.4 精度相关的几个实操教训
我在精度和显存上踩过的坑,总结出来基本是这几条:
第一,不要粗暴地把整个模型转成FP16。有些模块,比如归一化层和输出层,保持原始精度反而更稳。用训练框架自带的自动dtype机制,或者显式指定"全局BF16,但某些模块保持BF16/FP32",都比手动model.half()靠谱。
第二,QLoRA的4bit底座确实省显存,但要注意哪些模块被量化了。有些库默认会把几乎所有线性层都转成4bit,包括输出层,这会让生成质量明显下降。实操里可以保留部分关键模块不量化,换取质量,代价只是多占用几个GB。
第三,显存不足不要第一时间怪PEFT不够高效,先查这三项:序列长度是不是定得太长,梯度检查点有没有开,有效batch是不是被梯度累积顶得虚高。我见过有人把序列长度从2048加到4096后直接OOM,全因为激活值翻倍。
3. PEFT参数高效微调体系:为什么冻结底座也能出效果
3.1 低秩假设:为什么只动一小撮参数就够了
PEFT全称是Parameter-Efficient Fine-Tuning,参数高效微调。这个"高效"不是妥协,而是一套有理论支撑的工程策略。
核心假设是低秩假设:大模型的参数空间虽然维度极高,但真正决定一个下游任务行为变化的方向,往往集中在一个低维子空间里。就像一条三维空间里的螺旋曲线,真正描述它只需要少数几个坐标轴,不需要把空间里每一维都展开。
基于这个假设,微调一个下游任务时,不需要在7B参数上全量更新权重,只需要学到"相对原始的增量变化",而这个增量本身可以表示成低秩矩阵。这就是PEFT能大幅降低训练开销的根本原因——它没有偷工减料,而是绕开了大量与任务无关的参数维度。
3.2 LoRA的原理、初始化细节与合并时机
LoRA(Low-Rank Adaptation)是PEFT家族里应用最广的方法。它的做法很直接:冻结原始权重矩阵W0,在旁边引入两个小矩阵A和B,让增量ΔW = BA,其中A的形状是d×r,B的形状是r×d,r远小于d。
前向计算变成 h = W0x + BAx。训练中只有A、B可学习,W0完全冻结。这样做的精妙之处有两个:
一是初始化方式。A用高斯分布初始化,B初始化为全零,这样训练开始时ΔW=0,模型的输出和原始模型完全一致,不会因为引入额外参数而突然出现"性格突变"。这为后续训练提供了一个非常平稳的起点。
二是推理合并。训练完成后,可以把 ΔW 合并回原始权重:W' = W0 + (α/r)BA。合并后不增加任何推理计算量,部署时和普通模型一样快。测试评估时建议先不要合并,分别加载底座和adapter对比,方便排查问题;确认效果后再合并发布。
公式里有个缩放系数α/r,这个设计很巧妙。它把"更新幅度"和"秩大小"解耦了:你可以用较大的r表达复杂任务,同时用合适的α控制更新强度,两者互不干扰。经验上α取r的2倍比较稳,即r=16时α=32。
3.3 QLoRA和其它PEFT家族成员怎么选
QLoRA是LoRA在工程上的极致省显存版本,原理上做了三件事:把底座权重用4bit的NF4格式量化(NF4是一种为4bit专门设计的正态分布友好的量化方式);再做一次双量化,把量化常数本身也压一遍;配合分页优化器把优化器状态在显存和内存之间搬移。这套组合能把7B模型的底座压到3.5GB以下,让它能在小显存上训练。
QLoRA的代价是训练速度会慢一些,因为每一步都需要在4bit底座和计算精度之间做反量化转换,而且4bit底座本身对精度有损失。我的建议是:小显存或快速跑通实验用QLoRA,效果验证通过后,如果追求最优质量,再用BF16底座加LoRA做一轮正式训练。
其它PEFT方法里,Prefix Tuning和Prompt Tuning是更轻量的方案,只在输入序列前加一小段可学习的向量,训练参数量甚至不到0.1%,适合快速原型和多任务切换;但如果任务复杂度高,效果通常不如LoRA。AdaLoRA则是让算法自动给不同权重矩阵分配不同秩,等于把调rank的功夫交给优化器,适合不想手动调参、数据量又不小的场景,缺点是训练开销略高。
| 方法 | 可训练参数占比 | 显存开销 | 每步速度 | 适用场景 |
|---|---|---|---|---|
| 全参微调 | 100% | 极高 | 慢 | 大服务器、超大数据量 |
| LoRA | 0.1%~1% | 较高 | 快 | 效果优先的各类微调 |
| QLoRA | 0.1%~1% | 低 | 较慢 | 小显存、快速实验 |
| Prefix/Prompt Tuning | <0.1% | 最低 | 最快 | 快速原型、少样本 |
| AdaLoRA | 0.1%~1% | 中 | 中 | 不想手动调rank |
3.4 目标模块、秩与学习率的经验取值
LoRA挂在哪层、用多大秩,直接决定效果。这块没有绝对标准,但有很成熟的经验区间。
目标模块的选择上,最早的研究报告说只挂注意力层的q和v就够了,因为attention里的key/value分布是目前任务最需要调整的地方。但实际做领域知识任务时,我经常把q/k/v/o都挂上,有时候也把MLP层的gate/up/down挂上,效果提升明显,显存只多占一点。建议顺序是:先只挂注意力的q/k/v/o跑一轮评估,不满意再把MLP也加上,观察变化再决定。
秩r的选择:任务简单、只是改回答格式或语气,r取8到16;需要学习大量领域知识或复杂推理风格,r取16到32;如果领域词汇非常多、新词频繁出现,可以把r推到32到64。但r不是越大越好,r过大时LoRA参数量变大,在小数据集上过拟合的风险显著上升,训练速度也会下降。很多人一上来就r=64,结果训练集表现好、测试集一塌糊涂,就是这个原因。
学习率方面,因为PEFT可训练参数量很少,欠拟合的风险大于过拟合,所以学习率可以比全参微调大一个数量级,取1e-4到5e-4,配合cosine衰减和2%到5%的warmup。LoRA矩阵本质上是"在原始权重旁做增量",更新步幅太小的话,这个增量根本学不上去。
有一个实操经验:当数据量小于5000条时,优先降低秩而不是增加训练轮数。增加轮数会加速过拟合,降低秩虽然损失一点表达能力,但配合适当的dropout(0.05到0.1),能显著提升泛化能力。
4. 把训练阶段、数据类型与PEFT组合起来:方案矩阵与实战工作流
4.1 从业务目标反推组合方案
单独讲完三根柱子,最后要解决的是"我到底该怎么选"。我把常见的业务目标、对应训练阶段、推荐方案整理成一张矩阵,可以直接对照:
| 业务目标 | 属于哪个阶段 | 推荐方案 |
|---|---|---|
| 模型不懂领域术语和内部知识 | 持续预训练 | 领域纯文本语料 + BF16底座 + LoRA(r=32~64),数据量大 |
| 让模型按固定格式/语气/流程回答 | SFT | 指令数据 + BF16或QLoRA + LoRA(r=8~32),几百到几万条 |
| 补知识同时改格式 | 先持续预训练,再做SFT | 分两个阶段训练,先学文本后学格式 |
| 让回答风格像某个作者或客服 | SFT | 风格化样本 + LoRA(r=8~16),数据量小 |
| 偏好/价值观对齐 | DPO | 偏好对数据 + LoRA(r=8),保守训练 |
这张表的核心逻辑是:先判断你要解决的是"知识问题""格式问题"还是"偏好问题",再选择阶段。知识问题用持续预训练,格式和交互问题用SFT,偏好问题用DPO。不要试图用一个阶段解决所有问题。
4.2 五步工作流:从目标到全量训练
我实际做微调时,固定走五步,每一步都有明确的退出条件。
第一步,定义目标并判断阶段。把业务需求翻译成一句话:"模型现在缺少什么?是不知道这些知识,还是知道但不会按格式说出来?"这一步如果判断错了,后面全错。
第二步,手工构造5到20条样例,直接测试基座模型。这一步往往能省下一整个微调周期。如果基座模型已经能把大部分样例回答对,只是个别地方不对,那就根本不需要大规模微调,或者只需要一个很小的LoRA。很多人跳过这步直接造十万条数据,最后发现问题是提示词没写对。
第三步,根据显存选择数据类型和PEFT方案。24GB显存优先BF16底座加LoRA;16GB以下用QLoRA。这个选择会影响后续所有配置,所以要在数据工程之前确定。
第四步,处理数据。核心是三点:统一的模板格式、回答部分做loss mask、清洗掉重复和格式错乱的样本。造好的数据一定要抽样检查,亲眼看几组tokenized前后的样本。
第五步,用500到1000条子集跑通一轮小实验,人工检查生成结果,确认方向正确后,再上全量数据。这一步能救命,它能暴露模板错误、数据格式问题,而不是让你在跑了两小时后才发现loss一直不降。
4.3 训练中该监控什么、怎么判断过拟合
很多人的监控习惯是盯着训练loss,但大模型微调里,loss下降和生成质量并不总是正相关。
持续预训练阶段,loss下降是有效的信号,因为目标就是预测下一个token,loss低说明模型确实学到了语料分布。但SFT阶段,loss下降到一定程度后会进入平台期,这时候继续盯着小数点后第四位毫无意义,真正要看的指标是:模型在固定测试集上的格式正确率、关键信息命中率、回答是否符合约束。
我推荐一个非常朴素但有效的评估方法:在数据准备阶段就留出20到50条覆盖任务边角的测试题,不参与训练,每隔几百步跑一次推理,直接对比训练前后的回答。人工看一眼胜过任何浮点指标。
过拟合也有很典型的表现:训练loss持续下降,但测试集上模型开始"过度服从"训练数据里碰巧出现过的措辞,或者回答变得机械、重复。这时候的应对不是加数据,而是降低秩、增大dropout、减少epoch,或者回到数据层面增加多样性。
4.4 高频翻车点清单
最后总结几个我在实战中反复见到的高频错误,按出现频率排序:
- label没有mask,prompt部分也参与loss计算,导致模型变成复读机。
- 拿SFT数据做持续预训练,或反之,阶段和数据不匹配。
- 老显卡不支持BF16,跑出来的结果全是NaN或loss飞了。
- 序列长度设置过长,激活值把显存打爆,又不肯开梯度检查点。
- 新增了领域词汇表,却没有在训练中让embedding层充分更新,新词全部变成乱码。
- 过早合并LoRA adapter进行测试,发现问题后误以为是模型效果不好,其实是合并时的精度问题。
- 数据重复率过高,模型学成了复读机,生成内容不断自我重复。
5. 一次完整的领域微调复盘:从评估基座到合并发布
5.1 任务背景与第一步评估
做一个具体的复盘,项目代号就叫"模拟项目X"。任务是:把客服工单里的用户描述转换成一个标准JSON结构,包含故障分类、紧急程度和处理建议。基座是一个7B量级的开源模型,显卡是一块24GB显存的消费级卡。
第一步评估就发现了关键信息:输入几条样例,基座模型能理解工单内容,但输出是长篇散文,完全没有JSON结构,分类逻辑也不符合业务定义。这个评估结果说明,模型的知识基本够用,缺的是输出格式和业务约束,属于SFT阶段,不需要做领域预训练。
这个判断很值钱。它把方案从"准备几千万token领域语料"简化成"准备几千条指令对",工作量差了一个数量级。
5.2 数据改造与训练配置
数据方面,手工构造了5000条"描述→JSON"的样本,覆盖20种故障类型和5级紧急程度,按8:2切分训练集和验证集。每条样本都用统一的对话模板封装,回答部分只保留JSON内容。
loss mask的构造是这个项目里的关键细节。示意代码如下:
# 伪代码:构造label并对prompt部分做mask def mask_prompt_labels(encodings, prompt_token_len): labels = encodings["input_ids"].copy() # 回答部分保留真实token id labels[prompt_token_len:] = encodings["input_ids"][prompt_token_len:] # prompt部分设为-100,不参与loss计算 labels[:prompt_token_len] = -100 return labels训练配置完全沿用前面讲的经验值:底座用BF16,LoRA挂q/k/v/o四组矩阵,r=16、alpha=32、dropout=0.05,学习率3e-4,cosine衰减,warmup占比3%,训练2个epoch,有效batch为64,序列长度2048,开启梯度检查点。显存占用约17GB,24GB的卡跑起来余量充足,大约三个多小时跑完。
5.3 训练效果与前后对比
训练结束后,用验证集做了三轮对比评估:
| 评估指标 | 基座模型 | 微调后模型 |
|---|---|---|
| JSON输出格式正确率 | 0% | 97% |
| 故障分类准确率 | 31% | 92% |
| 紧急程度判断准确率 | 22% | 88% |
| 平均回答长度 | 142字 | 38字(JSON) |
这个结果其实在意料之中:基座模型不是不会判断,是完全不知道要按JSON输出,所以格式正确率为0。微调的主要贡献是约束了输出结构,同时通过几千条业务样例让模型学到了业务侧的判断规则。
给这个复盘做个补充:微调后的模型偶尔还会在复杂工单上把"紧急程度"判断成相邻级别,这是SFT阶段数据里这类样本太少导致的。如果后续要优化,正确做法不是盲目加数据,而是专门补这类边界样本,或者考虑把紧急程度判断单独做成分类器,而不是依赖生成式模型。
5.4 这套方案还能往哪些方向扩展
复盘里有一些值得延伸的结论。如果换成16GB显存,把底座从BF16改成QLoRA的4bit加载,训练配置几乎不用调整,速度会慢一些,但能跑。如果业务里需要大量领域新词,比如罕见药品名、设备型号,那就得在SFT之前加一轮轻量的领域预训练,LoRA的秩也要相应调大。
另一个扩展方向是把这个SFT模型继续做DPO对齐——用一批"标准JSON"和"冗余散文回复"作为偏好对,让模型更坚定地输出结构化结果。这一步数据量只需要几百条,训练成本很低,但对稳定性的提升非常明显。
最后分享一个我个人的体会:这套体系里面,最容易让人失败的从来不是技术实现,而是阶段判断错误。数据、精度、PEFT方法都有成熟的路径可以查,唯独"你到底想让模型发生什么变化"这件事,必须自己先想清楚。每次翻车,先问一句"我是不是选错了阶段",往往比折腾超参数更有效。