微调 Qwen 模型,从 LoRA 到 RLHF,中间要过的关卡比大部分人想象得多。我这两个月前前后后帮几个团队排查微调故障,见了不少"玄学问题":loss 一路向下但生成一塌糊涂、训练没报错推理却乱答、RLHF 之后模型反而比 SFT 阶段更笨。这些问题很少出自单一原因,但绝大多数都有迹可循。这篇文章我把高频故障按流程拆开,从数据、LoRA 参数、训练稳定性、RLHF 到最后的合并与推理验证,都过一遍,适合正在跑 Qwen 微调、或者准备从 SFT 往 RLHF 走又担心掉坑里的人。
1. 先算一笔账:Qwen微调全流程最容易翻车的六个区段
先明确我们说的"LoRA + RLHF 全流程"到底指什么。一般落地时是这样的链路:先用 LoRA 对基座模型做有监督微调(SFT),让模型学会指令跟随和对话格式;然后训练奖励模型(RM),给候选回复打分;最后用 PPO 这类强化学习算法让策略模型朝高奖励方向继续更新。每一步的输出都是下一步的输入,所以问题会跨阶段传染,这是排查时最大的难点。
按我接触到的案例,故障根本不是平均分布的。最容易翻车的区段我用一张表列出来:
| 区段 | 典型症状 | 根因方向 |
|---|---|---|
| 数据准备 | loss下降但对话逻辑乱、回复复制用户输入 | 聊天模板、标签掩码、截断策略 |
| LoRA参数配置 | 收敛慢、过拟合、和全参微调差距大 | rank、alpha、目标模块、学习率 |
| SFT训练过程 | OOM、NaN、loss反复震荡 | 显存规划、混合精度、数据顺序 |
| 奖励模型 | 训练准确率虚高、验证集排序不稳 | 数据泄露、样本对构造、epoch过多 |
| PPO强化学习 | KL爆涨、回复重复、输出退化 | KL系数、clip范围、优势计算 |
| 合并与推理 | 合并后效果漂移、量化后明显变笨 | 精度类型、量化方式、采样参数 |
这张表的核心价值不是让你背下"哪段会出错",而是提醒你排查时先做区段隔离。比如 loss 一直不降,先别急着调学习率,先去看数据模板是不是把<|im_end|>之类的结束符给裁了。同一个症状在不同区段里,病因完全不同,乱调参数只会让问题更难定位。
我的习惯是:出现异常时先问"当前证据落在哪个区段",再深挖。如果症状跨界,优先查上游。SFT 质量差,后面奖励模型怎么训练都救不回来。RLHF 不是用来修复 SFT 缺陷的,它只能放大或修正部分偏好。把这个认知摆正,能省掉一大半无效调参时间。
2. 数据侧的隐性故障:模板错位、标签泄漏与截断灾难
数据是"微调后变笨"的最大根源,但训练日志通常不会报错,只能靠特征反推。我按出现频率从高到低讲三个问题。
2.1 聊天模板没有正确加载
Qwen 系列的分词器内置了聊天模板,正确做法是让分词器自己把对话拼成 token 序列。但很多人图省事,自己用字符串拼接<|im_start|>user\n...,结果往往是特殊 token 被切成碎片,或者角色标记丢了一半。训练时模型确实能学进去一些,但生成阶段如果模板拼法和训练时不一致,效果立刻就崩。
检查方法很直接:把一条训练样本用分词器重新编码再解码,肉眼确认特殊 token 位置。我一般用类似这样的代码快速验证:
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("./qwen_base") sample = [ {"role": "user", "content": "你好"}, {"role": "assistant", "content": "你好,有什么可以帮你?"} ] ids = tokenizer.apply_chat_template(sample, tokenize=True) print(tokenizer.decode(ids))如果发现<|im_end|>前面没有换行、或者角色标记里混进了半个 token,说明模板拼写有问题。这个步骤只要在正式训练前做一次,就能避免后面大量的无效复现。
2.2 标签掩码错位导致模型变成"复读机"
多轮对话里通常只希望模型学 assistant 的回复,用户输入部分的标签要设成 -100 掩掉。如果掩码没做对,模型会把用户说的每句话也当标准答案去拟合,生成阶段就会不断复读用户刚才的提问,看起来像在礼貌回应,实际完全没效果。
排查方法我在另一个项目里用过很多次:从 DataLoader 里打印一个 batch 的input_ids和labels,确认非 -100 的位置正好覆盖 assistant 回复的 token 区间。如果用户输入区间也出现非 -100 标签,掩码逻辑就错了。这个问题不会让 loss 变高,所以很多人根本察觉不到,只能靠抽样检查发现。
2.3 截断策略把结尾裁没了
固定max_length截断长对话时,很容易把末尾 EOS 和最后一句回复直接砍掉。模型在训练中反复看到"有头无尾"的样本,生成时就容易话说到一半突然停下,或者不输出结束符。
一个比较实用的策略是:以最后一条 assistant 回复为保底,优先保留句末完整片段,而不是保留开头。确实需要保留开头场景的,单独做一条短样本,不要和长样本混在一起统一截断。数据侧的排查逻辑很朴素:先怀疑数据,再用可视化确认,修正后跑短实验验证。不要一上来就调 LoRA 参数,数据错的时候,参数怎么调都只是延缓问题暴露。
3. LoRA 参数疑难杂症:rank、alpha 与目标模块的匹配问题
LoRA 本身的数学很简单:对权重矩阵 W 加一个低秩增量 BA,训练时只更新 B 和 A。但正因为简单,很多人容易陷入"随便设参数"的误区。参数之间是联动的,单独调某一个通常没用。
3.1 rank 不是越大越好
rank 控制增量矩阵的表达能力。对 Qwen 7B 到 14B 这类规模的模型,rank 8 到 64 是比较常见的区间。数据量大、任务复杂时可以到 128,但更大的 rank 意味着更多可训练参数,也更容易过拟合。
判断当前 rank 合不合适有个笨但有效的办法:固定其他参数,分别用 rank 8、16、32、64 跑一轮短 SFT,对比验证集指标。如果 rank 翻倍后指标几乎不变,说明任务简单,用更小的 rank 还能提升稳定性;如果从 8 到 16 提升明显,到 32 又回落,说明 16 附近已经接近最优。这个实验成本不高,但能省下后面调 alpha 的大量时间。
3.2 alpha 与 rank 的比例要看成对参数
alpha 控制增量矩阵的缩放强度,常见起步值是 alpha = 2 × rank。但决定实际效果的不是 alpha 本身,而是 alpha 和 rank 的比值在当前学习率下产生的有效更新量。
我自己用的调试方法是记录训练前后权重增量的 L2 范数,和原始权重范数对比。如果增量范数大于原始权重的千分之一,说明更新太猛;低于万分之一,又可能欠拟合。把抽象参数变成可量化的数值,比反复试 alpha 高效得多。顺带一提,这个检查同样适用于诊断"为什么我的 LoRA 训练看起来像没训"——很多情况下就是 alpha 设太小,增量被压制在噪声水平。
3.3 目标模块选择失败
只把 LoRA 挂在 q_proj 和 v_proj 上是很多默认配置的做法,但 Qwen 的模型结构里 attention 有 q/k/v/o 四类投影,MLP 还有 gate/up/down 三类。对指令跟随类任务,只在 q/v 上做低秩适配往往不够。
更合理的做法是把 attention 的四个投影都加上,或者把 MLP 的 gate_proj 和 up_proj 也纳入 LoRA。可训练参数会多一些,但表达空间更完整。需要特别说明的是,目标模块选择没有普适最优解,关键是你实验时保持"同一时间只变一个变量"。我在一个对话任务里试过只加 q/v 和全部投影的对比,同样是 rank 16,全部投影的方案在评测集上的指令遵循率高了约 5 个点,但训练时间也涨了 30% 左右,这个取舍要按任务来。
3.4 学习率、权重衰减与预热步数
LoRA 的学习率一般比全参微调高一个数量级,常见在 1e-5 到 2e-4 之间。但很多人从全参微调的习惯里带过来 weight_decay=0.01,对 LoRA 来说这个值偏高,会把低秩增量压得太小。我一般会先关掉权重衰减跑一个短实验,确认效果后再决定是否加小权重。
在具体项目里,我会做 1 到 3 步的参数网格:学习率一个变量,rank 和 alpha 一组变量,目标模块一个变量,每轮只动一组。跑完对比后,再组合最优项。这个流程看着慢,实际比同时调五个参数然后陷入玄学快得多。
4. 训练中断与损失异常:从训练日志判断真实病因
训练阶段的问题通常分三类:显存不够、数值异常、loss 行为异常。每类都有相对清晰的排查路径。
4.1 OOM 不只是 batch size 的问题
LoRA 微调看似省显存,但优化器状态一样占地方。如果 batch size 稍微调大就 OOM,优先查这几项:梯度检查点是否开启、优化器状态是否被正确切分、基座模型是否保持冻结但忘关梯度。
一个容易被忽略的点是序列长度。数据里的长样本会让单条样本的显存消耗成倍上升。与其把 batch size 调到 1,不如先统计数据集长度分布,把超长样本单独处理,而不是全部喂进模型。梯度累积能缓解显存不足,但解决不了"单序列长度爆炸"的问题。同样一批数据,把max_length从 2048 降到 1024,单卡能容纳的 batch size 可能直接翻倍,训练速度反而更快。
4.2 NaN 与数值不稳定的定位顺序
训练到几十步就 NaN,常见原因有几类:学习率太大、数据里出现异常 token、混合精度下某些层溢出。我的排查顺序是这样:先看日志里 loss 趋势,是逐渐变大还是突然跳变;然后定位到第一个 loss 异常步,把那一步的数据单独拿出来跑前向;如果复现,把对应样本从训练集剔掉再试,大多能锁定脏数据。
如果在 fp16 下必现、bf16 下不现,那基本就是精度问题。Qwen 这类模型在部分层上对精度敏感,必要时可以给敏感层单独保留 fp32,或者直接切换成 bf16。这个选择要考虑硬件支持情况,不是所有卡都能无缝跑 bf16。
4.3 loss 不下降或反复震荡
这种情况最常见的根因反而在数据侧:标签掩码错误导致模型同时学习用户输入和助手回复,或者对话模板在部分样本上不统一。另一个常见原因是 shuffle 没开,模型在训练初期反复看到同一批样本,loss 短暂下降后开始震荡。我建议先把数据里的 label 分布可视化一遍,再去调学习率,顺序反了容易越调越乱。
4.4 loss 降了但生成依旧烂
这种"安静翻车"最麻烦。loss 下降只代表模型在拟合训练分布,不代表它学到了你要的行为。检查顺序:先确认验证集 loss 是否也同步下降,如果不是,多半过拟合;再检查生成时用的模板和训练时是否完全一致;最后把几条训练样本的生成结果拉出来和训练标签对比,看模型是否只是在机械记忆。
我见过一个项目,SFT 的 loss 一路降到 0.15 左右,但生成结果大量重复。最后发现训练数据的回复长度分布严重偏向短句,模型学会了用最短路径压 loss。加大数据多样性、调整回复长度上限之后,问题立刻缓解。这类问题在训练日志里完全看不到异常,只能靠抽样生成来判断。
5. RLHF 阶段的两段式坑:奖励模型与 PPO 的不稳定源头
进入 RLHF 前,先要建立一条认知:RLHF 不是 SFT 的简单升级,它引入了一个新的学习信号——奖励分数。奖励模型一旦过拟合或者出现偏好捷径,策略优化的方向就会被整体带偏。
5.1 奖励模型训练里的隐蔽问题
奖励模型最常见的坑是过拟合。训练 RM 时验证集准确率可能在第二个 epoch 就接近 90%,看起来很好,但 RM 学到的往往是"哪句话更长、哪句话格式更正式"这类表面偏好,而不是真正的质量排序。这个现象业内很常见,对策也基本一致:RM 的训练 epoch 尽量控制在 1 到 3 个以内,并且定期用一个小型人工校验集做排序准确率测试。如果人工校验准确率明显低于训练集准确率,就要警惕过拟合。
另一个问题是样本对构造质量。如果同一个 query 下两个回复的差异不够大,RM 就需要从近乎相同的内容里找差别,很容易学到噪声。我在构造训练对时会先过滤掉长度差太小、token 编辑距离过近的样本对,再进训练。数据质量差,再复杂的模型结构也扛不住。
5.2 PPO 训练中的三个危险信号
PPO 训练过程中,我常盯三个指标:KL 散度、策略模型的输出熵、reward 的移动平均。
- KL 散度突然爆涨,说明策略模型跑得太快、离参考模型越来越远。对策是提高 KL 系数或者缩小学习率。
- 输出熵持续下降,说明模型开始塌缩到少数重复模式,常见原因是训练步数偏多或者采样温度过低。
- reward 不断上升但生成质量肉眼下降,基本可以断定是奖励黑客,模型发现了 RM 的偏好捷径。比如它学会只要回复里包含某些高频词,分数就会变高,内容本身却毫无意义。
如果在日志里同时看到这几个信号,就该停下来检查,而不是等 loss 爆掉再处理。跑 PPO 之前,把参考模型用 eval 模式冻结,把生成回复的采样参数固定,这两件小事能排除不少干扰。
5.3 "RLHF 后模型变笨"的两种情形
很多人在 RLHF 训练后觉得模型变傻了。这里要区分两种情形:一种是模型变得式化,偏好单一风格,这通常是 KL 控制不足,模型离参考模型太远;另一种是模型彻底退化,开始胡言乱语,这大概率是奖励模型出了问题,而不是 PPO 算法本身的问题。遇到这种情况,我建议先不做任何调参,直接把训练早期保存的策略模型拉出来生成一批回复看看质量。如果早期模型正常,根因就在训练中段之后,重点检查 reward 是否被某个固定特征支配。
6. 合并、量化与推理验证:最后一公里仍然会翻车
微调不是训练完就结束,从 adapter 合并到最终推理,中间还有几个很容易出问题的基础环节。
6.1 adapter 合并时的精度一致性
LoRA 训练结束后,把 adapter 权重合并回基座模型是常见操作。这个环节最容易被忽略的是精度类型。如果基座权重是 fp16,adapter 权重训练时是 bf16,合并时没有统一精度,最终模型的 logits 可能和训练时对不上,表现自然漂移。
我习惯在合并后做一个 logit 一致性检查:取几条固定输入,分别用"合并后的模型"和"基座 + adapter 的未合并模式"跑一遍前向,对比输出概率分布。差异在合理误差范围内才算合并成功。这个检查只需要几分钟,但能避免把合并引入的偏差误判成微调效果不好。
6.2 量化方式与效果损耗
用 bitsandbytes 做 4bit 或 8bit 推理是很常见的做法。但量化后模型变笨,不一定是量化本身的问题,更常见的原因是动态量化没有校准步,或者某些层对量化特别敏感。如果模型只是偶尔答得差,可以先试 8bit;如果任务对生成质量要求高,4bit 尽量只用于快速验证而不是最终交付。
量化后的评估不要只看一两条回复,要在一个小评测集上跑完整对比。我有一套固定做法:准备 50 到 100 条覆盖各场景的输入,分别用 fp16、8bit、4bit 跑生成,记录可接受率。这个结果比任何单条样例都有说服力。
6.3 推理参数与评测口径
生成阶段的温度、top_p、重复惩罚对最终效果影响很大。很多人拿训练时的采样参数直接做评测,结果把温度过高导致的随机性误判成模型没学好。评测最好用较低的温度,甚至在确定性任务里直接贪心解码。对话和创意任务再适当调高温度,两套参数分开用。
最终评估建议结合自动化指标和人工抽检。自动化指标能快速发现问题,但质量类任务最终还是要人工看。我在项目里通常先让模型批量生成,自动筛一遍明显重复或空回复,再抽 10% 到 20% 做人工打分,效率和可靠性比较平衡。
7. 一份可以抄走的故障排查清单
最后,我把日常排查用的顺序和检查点汇总成一张清单。这套清单不依赖特定框架版本,适合把 Qwen 从 SFT 推向 RLHF 时做分阶段自查。
7.1 快速定位三步法
- 先复现。选一条出问题的输入,固定随机种子重跑推理,确认问题能稳定出现。
- 再隔离变量。把训练数据、LoRA 参数、训练配置、奖励模型、推理参数当作独立变量,一次只动一个。
- 最后对照。用一个小规模参考集记录每轮改动前后的指标变化,不要凭感觉判断。
7.2 分阶段自查项
- 数据格式:训练样本是否统一使用分词器的聊天模板,特殊 token 与角色标记是否正常。
- 标签掩码:用户输入是否已掩掉,assistant 回复区间是否完整,EOS 是否保留。
- LoRA 参数:rank 与 alpha 比例是否合理,目标模块是否覆盖 attention 和 MLP,增量范数与原始权重的比值是否落在万分之一到千分之一之间。
- 训练配置:梯度检查点是否开启,shuffle 是否正确设置,优化器状态与 batch size 是否匹配显存。
- 训练日志:loss 是否被验证集同步验证,是否出现 NaN,生成样本是否出现重复。
- RLHF 阶段:RM 验证集排序准确率是否可信,KL 散度是否受控,reward 是否存在单一特征捷径。
- 合并与推理:adapter 合并后是否做 logit 一致性检查,量化选择是否经过小数据集评测验证。
这张清单看起来繁琐,但真遇到问题时能省下大量时间。我自己接手新项目时,会在正式训练前把 7 项全部过一遍,任何一项不过,都会先停下来处理而不是硬跑。最后说句实在话,微调这件事,规律比套路重要,数据比参数重要,复现比猜测重要。只要坚持每次只动一个变量,你遇到的大部分"玄学问题"都会变成"已知问题"和"可解决的问题"。