做完整流程的大模型训练,预训练、SFT、PPO这三个词基本绕不开。但说实话,真正把三个过程都亲手跑过一遍的人不多,多数项目要么只做微调,要么直接套开源权重走推理。我前一阵完整走了一遍“预训练 -> SFT -> PPO”的全链路,踩了一路的坑,也把很多原本模糊的直觉变成了可复用的经验。这篇内容不打算写成教科书,而是想把我实际操作中的方案选择、运行细节、失败记录和排查思路完整记录下来,给同样要做大模型训练的同行一个可落地的参照。
这套东西适合谁?如果你刚接手一个训练任务,或在SFT和RLHF之间犹豫怎么做选择,或已经跑通SFT但觉得模型效果离“能用”还差口气,那这篇内容应该能帮你把整条链路的逻辑串起来。我会把预训练的数据处理、SFT的模板设计、PPO的四模型架构讲清楚,也会把那些文档里不写的“隐性坑”摊开说。
1. 整体设计拆解:为什么是预训练、SFT、PPO三段式
1.1 三个阶段到底各自解决什么问题
很多人把这三个阶段理解成“升级打怪”的关系:预训练给模型知识,SFT教它对话格式,PPO让它讨人喜欢。这个理解方向对,但颗粒度太粗,容易导致训练策略上的误判。
预训练解决的是“语言能力底座”。这个阶段模型在纯文本上做下一个词预测,学到的是语法、常识、世界知识、推理雏形,但此时模型并不具备“听从指令”的行为模式。你会观察到,预训练模型可以续写一段话,但你说“帮我写一封请假邮件”,它大概率会机械地续写下去,而非按照指令执行。这本质上是训练目标的缺失——预测下一个词和“遵循指令给出回答”是两种不同行为模式。
SFT解决的是“指令跟随能力”。通过在人工标注或规则筛选出的指令-回答对上继续训练,模型开始学会将用户输入映射到合理的输出格式。这个阶段决定了模型“听话”的上限,非常重要的一点是,SFT并不增加知识量,它只是把预训练阶段学到的知识重新组织成能按指令调用的形态。这就像一个人本来就认识很多单词,SFT是教他如何按照题目要求写作文。
PPO解决的是“价值观对齐和价值偏好”。SFT之后模型已经能给出格式正确的回答,但格式正确不等于符合人类偏好。比如让模型解释一个技术概念,SFT后的回答可能冗长、绕圈子、缺少结构;PPO通过一个奖励模型来引导策略模型朝人类偏好方向优化——回答更简洁、更结构化、更诚实。这一阶段见效慢,超参数极其敏感,也是整个流程中训练实验成本最高的一环。
1.2 为什么不能跳过SFT直接做PPO
我见过有人想省事,想在预训练模型上直接跑PPO。理论上不是完全不能做,但实际效果几乎一定崩。原因很直接:PPO的探索空间是基于策略模型当前生成采样进行的,预训练模型的生成分布是“自由续写”,没有经过任何指令格式约束。你在采样阶段拿到的trajectory大概率是模型自己编的,奖励模型给的分也不稳定,策略模型很难收敛到“对齐”状态。
SFT是PPO的前提接口。SFT阶段让模型学会了“用户指令 -> 模型回答”的映射模板,PPO阶段才能基于这个相对稳定的行为空间做偏好强化。这也意味着,SFT质量直接决定PPO上限。如果SFT数据本身就杂乱、答案质量低,PPO阶段奖励模型会学到一个扭曲的偏好分布,最后强化出一个“自信地胡说八道”的模型。
1.3 算力分配和资源规划的整体思路
完整跑一遍三个阶段的算力消耗差异非常大。预训练阶段如果用千亿级token,基本是几百张GPU卡跑数周;SFT通常用几万到几十万条数据,几十张卡跑几小时到一两天;PPO阶段单次update包含多次生成和多次梯度更新,实际上比SFT更吃算力,因为它每次iteration都涉及rollout采样和梯度更新的双重开销。
我的个人经验是,中小团队如果做领域大模型,可以适当调低预训练的体量——用10B到70B的token做领域继续预训练,或者干脆基于开源底座做增量预训练,重点资源留给SFT和PPO。至于具体比例,没有标准答案,但我会在后面章节给出一个经验参考区间。
2. 预训练阶段的实操拆解
2.1 数据从哪里来:清洗、去重与配比
预训练数据的质量权重远超模型结构设计。同样花100万算力,数据工程做得好和做得差的模型,效果差距可能比换个模型结构还大。我在这轮项目里用的语料来源主要是公开文本、代码仓库镜像和专业领域文献,总量约800GB原始文本,清洗后剩510GB左右,压缩率接近四成——这很常见,原始爬取文本的噪声比例就是有这么高。
清洗管线我按这个顺序处理:编码检测和乱码过滤、语言识别过滤、正文抽取、段落去重、全文去重、低质量规则过滤。这里全文去重用的是MinHash + LSH方案,不是简单的字符哈希,因为大规模文本里存在大量部分重复内容,比如新闻转载、代码片段复用、问答内容的变体改写。MinHash能把相似度高的文本聚类,几千亿级别文本去重基本是标配。
数据处理里最容易忽略的是文档内部的重复片段。比如某些新闻网站会在每个页面重复渲染导航栏和推荐内容,这些片段如果不做段落级去重,模型会学到大量重复模式,直接体现为生成的文本出现重复机械语句。我用的是SimHash做段落级去重,粒度控制在128字节。
配比方面,我建议如果你做的是通用底座,中文、英文、代码的比例可以控制在6:3:1左右;如果偏领域增强,可以调到中文:英文:代码:领域=5:2:1:2。代码数据哪怕只占10%,也能显著提升模型的逻辑推理和结构化输出能力,不要因为是中文项目就完全忽略代码语料。
2.2 Tokenizer训练:一个最容易被低估的环节
Tokenizer训练在开源社区的标准做法里经常被直接沿用现成词表,但增量预训练或从零预训练时,我强烈建议按你的语料分布重新训练词表。
为什么?分词器直接影响训练效率和模型效果。如果你的语料以中文为主,但词表偏向英文,那中文每个字可能被切成两三个token,序列长度被拉长接近一倍,训练吞吐直接腰斩。训练时设置vocab_size为32000,用了SentencePiece的BPE算法,加上了byte fallback机制防止未知字符。
这里有一个细节值得单独说:词表的覆盖率和token数量不是越大越好。词表过大,embedding矩阵参数量增加,softmax计算量也线性上涨;词表过小,长尾词汇被切碎,信息密度低。我做过实验,同一个语料集,vocab_size=32000和48000的最终loss差异在0.02以内,但48000的训练速度慢了约15%。所以对你正在训练的语料做一次tokenizer覆盖统计,找到覆盖率和词表大小的平衡点,比盲目追求大词表更高效。
2.3 模型结构选择:稠密、MoE与位置编码
预训练阶段模型结构我选择了稠密Transformer,主要原因不在于MoE不好,而是中小团队训MoE的工程成本太高。MoE能带来稀疏激活的算力优势,但all-to-all通信、负载均衡loss、专家路由的稳定性都是额外的工程负担。如果你人力有限,稠密模型是更稳妥的路径。
位置编码用了RoPE,因为它在长度外推和相对位置建模上的表现更稳定。这里提一个关键参数:base值。默认RoPE的base是10000,但序列长度超过4096时,高频位置编码会迅速衰减,导致模型学不到长距离依赖。我测试了base=500000的组合,在8K上下文长度下明显更平稳。如果你的训练序列以2K到4K为主,10000够用;如果计划扩展到8K以上,建议把base调大。
层数、头数、维度这些参数建议直接参考同量级开源模型的配置,不需要自己发明。真正影响效果的通常是更底层的训练细节,而不是Transformer的参数排列组合。
2.4 并行策略:数据并行+张量并行+流水线并行
预训练阶段并行策略直接决定了你能用多大规模的集群跑多快的训练。我在这轮项目用的是3D并行:数据并行、张量并行、流水线并行同时开启。
数据并行最简单,每张卡持有完整模型副本,喂不同的数据batch,通过梯度allreduce同步。张量并行是切分单层内的矩阵乘法,适合单机多卡场景,可以减少激活显存但会增加通信开销。流水线并行则是按层切分,把模型的不同层放在不同设备上,解决“模型太大单卡放不下”的问题。
工程配置里我给出一个具体数字参考。7B稠密模型在32张A100 80G的集群上,TP=2、PP=4、DP=4的组合,全局batch size设为512,单卡micro batch size设为1,梯度累积步数设为16步(这里需要算一下:全局batch = 512 / (4 DP * 4 PP * 2 TP * 1 micro) = 16累积),这个组合在稳定性和吞吐之间能取得相对理想的均衡点,训练速度大约在每秒每卡处理3500~4000 token左右。如果你的模型更大,比如13B或70B,流水线层数需要按层数均匀切分,尽量让每张卡的负载差控制在10%以内。
2.5 训练细节:学习率、loss观测和神奇的“过拟合假象”
预训练学习率我用的是warmup+余弦衰减。7B模型下峰值学习率设置在3e-4,warmup步数2000步,衰减到最终学习率3e-5。batch size偏大的时候,峰值学习率可以适当调高,但不要超过1e-3这个阈值,否则loss会出现明显的震荡。
训练中的核心观测指标是validation loss和perplexity的对应关系。这里有个常见误判:很多团队看到训练loss下降就以为模型在学习,实际上在2K到4K的短序列上,模型很容易“死记硬背”而学到伪模式——开头几句背诵式重复、输出空洞的续写。我自己的习惯是每个epoch保存checkpoint后,专门抽几条长文本做生成测试,观察模型续写质量,不能只看loss。
预训练阶段还有一个容易被忽略的现象:loss plateau。模型训练到中期会进入一个看似“不学习”的阶段,loss曲线非常平缓,很多人这时候就决定终止训练。实际上这往往不是真正收敛,而是当前学习率下模型陷入了局部平滑区域。我的处理方式是保持训练,等warmup结束后把学习率降低一个量级继续训,通常能突破平台继续下降。
3. SFT阶段:模型“学会说话”的关键一跃
3.1 SFT数据从哪来:质量大于数量,多样性更重要
SFT阶段的核心不是数据量,而是数据分布的覆盖率和质量。业界一个很常见的错误是:为了凑数量疯狂上量,结果模型学到了大量重复的句式模板,生成内容变得千篇一律。我最终用的SFT数据集是13万条,但为了这13万条,前后清洗筛选了接近40万条原始候选数据。
数据来源主要有三类:第一是开源的中文指令数据集,注意做去重和格式统一;第二是从业务场景收集的真实用户问题,这个最宝贵,因为贴近实际分布;第三是合成数据——用大模型做数据增强,生成“问题多样性”和“回答简洁性”都更好的样本。我实测下来,三类数据的比例按2:5:3配比较为理想。
还有一个容易忽略的点:SFT数据必须包含一定比例的“拒绝回答”样本。现实场景里用户会问很多模型不该回答或无法回答的问题,如果数据里完全没有这类样本,模型会强制生成一个看似合理的回答,实际上是在一本正经地编造。加入5%左右的拒绝样本,能显著减少幻觉。
3.2 模板设计:System Prompt和对话格式的一致性陷阱
SFT阶段大多数人会忽略模板一致性对后续PPO的影响。你在SFT阶段用什么样的对话模板(user/assistant分隔符、system prompt的写法),PPO阶段的rollout采样就必须完全一致。否则模型在PPO阶段会困惑:训练时见到的格式和采样时输入的格式对不上,行为分布会发生漂移。
我用的模板统一为如下格式:
<|system|> 你是智能助手,请基于事实,简洁、准确地回答问题。 <|user|> [用户问题] <|assistant|>训练时,system prompt和user输入参与loss计算,但assistant回答部分的loss才参与反向传播;其余token位置做mask,不计算loss。这样模型只会去拟合“回答”部分,不会把模板本身背下来。
这里有一个很隐蔽的问题:模板中的system prompt文本如果被mask掉,模型实际上学不到system prompt的约束作用。你希望在生成时通过system prompt控制回答风格,但训练时这部分语义从未参与梯度更新,模型相当于“没学过”system prompt的含义。我的处理办法是:保留一小部分样本,system prompt和assistant回答同时参与loss计算,让模型学会理解system prompt的指示含义。比例不用太高,3%到5%就能起效。
3.3 Loss设计:只计算答案部分还是全序列计算
SFT阶段的loss设计有一个关键选择:是计算全序列的loss还是只计算assistant部分的loss。
我的实测经验:全序列计算会导致模型花费大量模型容量去记忆提示词和模板格式,反而挤占了回答部分的拟合能力;只计算assistant部分的loss,模型收敛更快,生成质量也更高。这个结论符合直觉——模板是固定的,不需要用模型容量去记住它。
实现上,我直接在主流的训练框架里对每个token打标签,把不需要计算loss的位置设置为ignore_index=-100。核心代码思路:
def mask_prompt_loss(labels, tokenizer, assistant_token_id): # labels形状为 [batch, seq_len] # 找出assistant token出现的位置,该位置之前全部mask masked_labels = labels.clone() for i, seq in enumerate(labels): assistant_positions = (seq == assistant_token_id).nonzero(as_tuple=True)[0] if len(assistant_positions) > 0: first_assistant = assistant_positions[0] masked_labels[i, :first_assistant] = -100 return masked_labels3.4 SFT训练参数设置
SFT阶段的学习率应该明显低于预训练。预训练峰值学习率是3e-4,SFT我设置的是1e-5,全参微调可以到2e-5,LoRA微调可以用5e-5到1e-4。学习率过高的典型表现就是训练集loss降到很低但验证集生成质量崩掉,模型出现复读机现象。
epoch数建议控制在1到3之间。13万条数据其实很多,2个epoch基本能让模型充分拟合,超过3个epoch的边际收益很低,反而增加过拟合风险。我实测4个epoch后模型开始出现对高频模板的机械复读。
dropout这里有个坑:预训练模型通常dropout设置较高,但SFT阶段建议把dropout降到0或者接近0。因为SFT数据量远小于预训练,高dropout会让模型在拟合指令数据时变得“软弱”,输出不稳定。我把所有Attention和MLP层的dropout置为0后,生成稳定度提升非常明显。
LoRA和全参微调的取舍上,如果数据和算力都充裕,全参微调最优;LoRA的优势是训练完只需要保存几十MB的adapter权重,迭代速度快。但如果你的目标是做完整RLHF链路,LoRA的局限性会在PPO阶段暴露——策略梯度的更新很不稳定,低秩更新空间可能无法承载PPO阶段的大幅度策略调整。所以规划PPO的话,建议SFT阶段直接全参微调。
3.5 SFT阶段容易踩的两个性能坑
SFT阶段最常见的性能坑是loss下降但生成质量没变化。排查思路是先看模板有没有问题——模板里的特殊token如果在词表里被错误地拆分,模型根本不知道这是一个独立token,loss当然降,但模型学不到格式约束。我建议用代码验证每个特殊token的id是否被正确保留,token在词表中是否作为一个整体存在。这是一个非常耗时的排查点,但找到后会恍然大悟。
第二个坑是数据泄漏。SFT数据集里如果混入了预训练阶段见过的文本,会造成过拟合假象。我的做法是把SFT数据在预训练语料里做了最近邻检索,凡相似度超过0.85的样本全部剔除,保证SFT数据是模型“没见过的”。
4. PPO阶段:价值对齐是一个“系统”,不只是算法
4.1 RLHF思想:为什么用强化学习而非继续SFT
SFT已经是“模型根据指令输出答案”,那为什么还需要PPO?核心原因是SFT只能模仿已有答案,无法超出数据集中答案的分布范围。如果SFT数据里没有“简洁回答”的例子,模型很难自行学会简洁;即使有,SFT也无法让模型习得一个全局的偏好优先级。
PPO引入了一个奖励模型作为“偏好信号源”。训练过程中,模型不断生成新的回答,奖励模型对回答打分,策略模型根据分数调整生成策略。这种在线学习的核心价值是:模型会逐步探索出那些“数据中没有明确出现,但整体偏好更优”的回答模式。奖励模型可以直接替代人工对海量生成结果的评估。
用一个生活化类比解释PPO:SFT阶段像学生看标准答案做练习,题目和答案都是固定的;PPO阶段像老师给作业打分但不给标准答案,学生通过不断尝试来理解老师的评分偏向。
4.2 PPO的四模型架构:Actor、Ref、Reward、Critic
PPO阶段需要四个模型同时存在:
- Actor:策略模型,就是我们最终要训练并部署的模型;
- Ref:参考模型,是SFT阶段保存的模型且参数冻结,用于约束Actor不要偏离SFT模型太远,避免策略崩溃;
- Reward:奖励模型,对生成结果打分,参数冻结;
- Critic:价值模型,用于估计状态价值函数,给优势函数提供baseline,训练过程中参数会更新。
Actor和Critic来自同一个底座SFT模型初始化;Ref就是SFT保存的权重;Reward需要在SFT基础上做一个独立的奖励模型头。这四个模型同时在显存里,显存开销接近三倍单模型,是PPO阶段最大的工程压力。
必须注意:Actor和Ref的初始权重必须完全一致。如果Ref在SFT阶段结束后又经历了任何参数变化而Actor没有同步,KL散度的约束起点就会出现偏移,训练一开始就处于不稳定状态。一个最简单的检查方法:训练启动前,把Actor和Ref的权重文件做逐参数对比,误差应为0。
4.3 奖励模型Reward Model的训练细节
奖励模型本身也是训练出来的。我给Reward模型设计了pairwise ranking loss:对同一个问题,准备两个回答,一个偏优、一个偏差,让模型学会区分优劣。核心loss公式为:
loss = -log(sigmoid(reward_chosen - reward_rejected))
这个公式的含义是:模型给优质回答的分数越高,给劣质回答的分数越低,整体loss越小。实际训练中需要把分数先做个归一化,防止reward数值范围漂移。我在训练Reward模型时加了标签平滑技术,将目标概率从1平滑到0.9,这在数据噪声较大的pair对中能显著提升稳定性。
Reward模型的训练数据质量要求极高。如果训练数据里有大量标注错误或有争议的偏好对,Reward模型会对某些无意义特征产生偏好,PA模型就会放大这个偏差。我准备训练样本是从线上真实对话中抽样排序、再由标注团队做两两对比生成的,每个样本至少独立标注两次,不一致的样本直接废弃。
另外必须提醒的是,PPO训练中reward模型分数最好做标准化——用训练过程中running mean和std把reward分数映射到均值为0方差为1的正态分布。否则奖励分数绝对值过大或过小,都会导致策略更新步长失控。
4.4 PPO核心机制:KL散度、GAE和策略更新
PPO阶段最核心的目标函数可以拆成三个部分:奖励模型给出的奖励、KL散度惩罚、以及PPO特有的clip项。
KL散度惩罚的公式是:kl_penalty = β * KL(actor_output_probs, ref_output_probs)。其中β是动态变化的系数。我采用了自适应KL系数调整:当KL散度高于目标值(比如0.02/token)时,β增大强化惩罚;当KL散度低于目标值时,β减小。这样能让策略保持在SFT模型附近但又有足够自由度去探索更优偏好。
GAE(广义优势估计)是PPO里估算动作优势值的关键模块。GAE参数的λ取值会影响优势估计的偏差-方差权衡。λ=1时优势估计接近蒙特卡洛采样,方差高但无偏;λ=0时只看即时奖励,偏差高但方差低。我的经验和大多数团队的默认选择一样,λ取0.95,这是个平稳且相对折中的值。
策略更新时有一个clip参数,控制新旧策略的差异上限,我设置为0.2。这意味着新旧策略在某个token的概率比不能超过1.2或低于0.8,超过这个范围就截断。这个机制保证了每次更新不会一步跨太大导致策略崩溃。critic的损失则采用MSE loss,学习率比Actor稍低,我用的是1e-5。
4.5 PPO训练超参组合参考
PPO超参之间互相影响很强,我给出一个经过多次实验验证可用的组合参考:
- rollout数量:每batch采样4096条完整response
- GAE λ:0.95
- clip范围:0.2
- 初始KL系数β:0.02
- KL目标值:0.03/token(动态调整β)
- Actor学习率:5e-6
- Critic学习率:1e-5
- 每次迭代的PPO更新轮次:4轮
- 温度采样:0.7
- 奖励标准化:每个batch内做running normalize
需要强调的是,PPO训练非常吃显存,四个模型同时加载通常需要把batch切得很小。我的方案是把rollout生成和梯度更新分开成两个阶段:在推理阶段只加载Actor和Ref,采样生成大量经验数据存入buffer;在训练阶段再加载Reward和Critic,用buffer中的数据进行策略更新。这个“生成-更新”分离设计能够显著降低显存峰值。
4.6 PPO训练中的“奖励欺骗”现象
PPO阶段最典型的问题就是reward hacking——模型学会钻奖励模型的空子。比如奖励模型偏好结构化回答,模型就会在回答里强行加一堆bullet point,内容其实没什么价值;奖励模型偏好“礼貌语气”,模型就会在每句回答后加“感谢您的提问”。
我排查这个问题的方法是观察响应的平均长度和句法复杂度变化趋势。正常训练中响应长度应该整体稳定;如果发现响应长度持续增长,或者标点使用频率急剧上升,基本可以确认模型在钻reward的空子。解决思路有三个:一是提升Reward模型本身的质量;二是在KL散度惩罚上加大力度;三是加入response长度惩罚因子。
我当时用的长度惩罚方案简单但有效:在reward分数上减去一个系数乘以response长度超出target部分。这样模型在优化奖励分数和缩短冗余长度之间必须做出权衡。
4.7 观察指标与训练收敛判断
PPO训练过程中不要只盯着reward曲线。reward曲线上升可能是好事,也可能意味着模型已经偏离到钻空子的方向,必须配合观测以下指标:
- KL散度值:应保持在预设目标范围内,过高说明模型偏离SFT版本太远
- response长度变化:持续增长的响应长度暗示奖励欺骗
- 文本多样性:用生成结果的n-gram重复率评估,重复率升高意味着模型开始复读
- Actor和Ref的预测分布差异:分布差异过大需要及时干预
收敛判断的实用方法是:在每个epoch结束做一次人工评估,用一组固定的测试prompt检查生成结果质量对比上一版的变化。如果连续2个epoch里人工评估分数没有提升,说明当前超参组合下已经接近收敛,可以停止训练。
5. 三个阶段衔接的工程实践复盘
5.1 数据配比与token消耗的宏观规划
完整跑完三个阶段的token消耗,我给一个参考区间:预训练阶段大约消耗1万亿token左右(基于5亿参数的底座模型),SFT阶段大约消耗2亿token,PPO阶段每次iteration生成推理token约2000万,整体训练下来PPO消耗token约1亿。这个比例显示出一个很有趣的事实:预训练token数量虽然更大,但计算开销是按算力token数计算的;PPO虽然token消耗量小,但每次token背后的计算链路更长(生成+多模型推断+策略更新),所以实际成本一点也不低。
对资源有限的团队,一个更经济的做法是:不用从零预训练10亿级模型,而是选择一个合适的开源底座,仅做领域适配的增量预训练(约500亿到1000亿token),再走完整SFT和PPO。这样总token消耗可以控制在预训练全量方案的1/5以内,但效果对于垂直场景足够。
5.2 模型权重管理:checkpoint策略和断点续训
三个阶段都会面临分阶段保存checkpoint的问题。我的习惯是:
- 预训练阶段每个500步保存一次全量权重,保留最近5个checkpoint滚动覆盖
- SFT阶段在每次epoch结束时保存,保留全部epoch版本用于对照
- PPO阶段每经过10次iteration保存一个actor权重快照,保留最近3份
这里有个非常容易踩的坑:PPO阶段Actor和Ref的初始权重必须来自同一个SFT版本。如果Ref用的是SFT的最终版本,Actor用的是SFT的某个中间epoch版本,两边的初始分布不一致,PPO一开始就会出现KL散度异常,表现为奖励不断下降但KL不断上涨,几乎无法救回来。
5.3 跨阶段数据泄漏的排查
三个阶段共享了很多语料来源,最容易忽视的是SFT阶段和PPO阶段的数据泄漏。PPO阶段Reward模型的训练数据如果跟SFT训练数据有重叠,模型会“认识”这些样本,给它们打出虚高的奖励,从而污染整个偏好信号。
我的做法是,每一阶段训练前都做一次数据溯源检测,核心实现是对高相似度样本做全量扫描。代码思路大致如下:
def dedup_across_sets(src_samples, tgt_samples, threshold=0.85): # 使用MinHash生成指纹 hash_list = [compute_minhash(sample) for sample in src_samples] # 对tgt样本逐一比对,返回相似度超过阈值的索引对 dup_pairs = [] for i, tgt in enumerate(tgt_samples): tgt_hash = compute_minhash(tgt) for j, src_hash in enumerate(hash_list): if jaccard_similarity(tgt_hash, src_hash) > threshold: dup_pairs.append((i, j)) return dup_pairs实测下来,即便是看似不相关的“用户提问”和“预训练文本”,也可能因为隐含的人物、产品名称而产生较高的文本相似度。不要嫌麻烦,这个步骤能帮你避免后面大量的返工。
5.4 训练停机恢复的一些经验
大规模训练过程中断很常见,我前后遇到过断点续训后loss突然抬升的情况。排查经历的教训是,续训时必须检查三个状态是否完全恢复:优化器状态、学习率调度器的当前步数、数据加载器的随机种子和当前位置。
特别是随机种子的恢复。Python的random库和PyTorch的dataloader都有各自的seed机制,如果只恢复了模型权重而没恢复数据加载顺序,模型看到的batch顺序完全不同,loss出现一阵波动是正常的。但如果数据加载顺序恢复错误,可能出现训练“看起来在继续但实际在乱学”的假象。建议做断点续训前,先在日志里记录breakpoint位置的全局step、epoch内step、cycle样本索引三要素。
6. 常见问题速查与解决实录
6.1 预训练阶段问题排查
瓶颈现象之一:loss不降。先检查学习率是否过高导致loss震荡,再查看数据是否重复太多,最后看词表覆盖是否过差。如果中文字符有大量被切成单字节,训练效率会非常低。
瓶颈现象之二:loss降了但生成文本质量差。优先怀疑数据清洗不够彻底——比如错误编码、繁体中文和简体中文混用、HTML标签残留。可以随机抽取500条训练文本做人工扫描,很多问题一眼就能看出来。
瓶颈现象之三:训练中途显存OOM。解决方案有很多:减小micro batch size;开启梯度检查点功能,用计算换显存;将优化器状态切成分片策略;或启用序列并行方案。我实测梯度检查点能节省约30%显存,但训练速度会降约15%。
6.2 SFT阶段问题排查
SFT后模型输出“嗯”、“好的”这类无意义后缀,几乎可以确定是训练数据里混入了过多短回答或低质量样本。排查方法是统计训练数据的回答长度分布,把超过设定阈值的低质量答案样本剔除。
SFT后模型对指令格式不敏感,用户怎么问它都按固定模板回答。这大概率是模板中user部分也被强制参与学习,导致模型把用户输入当作格式的一部分。检查你是否正确设置了mask。
6.3 PPO阶段问题排查
reward数值一路上涨但生成质量变差,几乎可以认定是reward hacking。立即介入,检查response长度、重复率、特殊符号使用频率。
KL散度爆炸性增长,常见原因是Ref和Actor初始化不一致,或者是PPO更新轮数过多导致策略偏移太大。减少PPO update轮数、增大KL惩罚系数、降低Actor学习率,这三招按顺序试。
critic loss不下降,说明价值函数没有学到有效信息。可能原因是GAE的优势估计数值范围过大,建议对advantage做标准化处理。
6.4 显存不足时的降级方案
全流程四个模型确实太占显存。如果资源有限,我推荐用低秩适配的方式训练Actor和Critic,Ref和Reward用4bit量化加载。虽然量化方案在严格训练精度上会有一点点损失,但实测下来奖励和KL散度的计算误差可以接受,显存占用能从80GB级别降到40GB以内,训练还能继续稳定跑下去。
7. 收尾:一点个人体会
完整跑完这个流程后,最深的感触是:预训练、SFT、PPO这三步不是简单的工程串联,每一步的选择都在为下一步铺路。预训练阶段多花心思在数据处理上,SFT阶段的收敛质量会明显更好;SFT阶段的模板设计和数据质量,直接决定PPO能不能稳定收敛。如果你准备走这条完整链路,我建议在起步阶段就做好算力规划、数据血缘追踪和节点恢复机制,这三个基础打好,后面的坑会少很多。
最后分享一个执行层面的小技巧:每个阶段结束不要急着进入下一个阶段,先留出一周时间做系统性的生成质量评估和回看。我曾在SFT阶段发现了一个模板token拼接问题,当时模型已经进入PPO,结果只能倒回来重新训SFT,白白消耗了大概两周的算力。宁可阶段间多验证,也不要到后面推倒重来。