1. 一条链路,三个阶段的整体设计
如果你正在接触大模型训练,肯定听过这三件事:预训练(Pre-training)、SFT(Supervised Fine-Tuning,监督微调)、PPO(Proximal Policy Optimization,近端策略优化)。它们不是三个无关的技术名词,而是同一个目标下环环相扣的三个阶段——让一个模型先学会"知识",再学会"听话",最终学会"在真实反馈中变强"。这篇文章就把这条链路拆开讲透,包括每一阶段在做什么、为什么必须这么做、我在实际训练中踩过的坑,以及可以直接照抄的参数和配置。
先说核心结论:预训练解决的是"模型懂不懂",SFT解决的是"模型会不会按人话回答问题",PPO解决的是"模型有没有真正对齐人的偏好"。三个阶段缺一不可,也不能调换顺序。预训练阶段模型学会了语言规律和大量知识,但此时它只会续写文本,不会老老实实回答问题;SFT用人工标注的"问题-答案"对把它调教成对话形态,但模型仍然可能给出泛泛而谈的稳妥回答;PPO再通过奖励模型对齐人类偏好,让回答更有用、更安全、更符合真实需求。
这套链路适合谁?如果你是自己训练十几B以下的小模型,或者微调开源基座模型做垂直场景应用,甚至只是想彻底搞明白大模型训练的内部机制,这篇文章都能用得上。我不讲抽象的公式推导,只讲工程实践中的经验、参数、数据配比和问题排查——这些都是常规文档里不会写的东西。
2. 预训练阶段:从零教模型"通识"
2.1 预训练到底在学什么
预训练的本质,就是用海量文本做自监督学习。模型做的是下一词预测任务(Next Token Prediction),给定前面的文本,预测下一个词是什么。这个任务看起来简单,但把全网的文本都塞进去反复训练之后,模型内部会涌现出语法规则、事实知识、推理链条、多语言关联能力,甚至早期的规划能力。
我自己习惯把它类比成一个"刚入行的新人被丢进图书馆看了三年书"。这个阶段模型读了很多很多书,学到了词与词之间的统计规律,知道了"苹果"后面大概率跟着"手机"或者"公司",知道了方程式的写法、代码的缩进习惯。但它还不太会和人对话,就像一个满腹经纶但社交笨拙的学者,你问它问题,它可能给你续写出一大段"提问之后通常会出现的文本",而不是直接回答你。
这个阶段的实现要点非常聚焦:海量数据 + 大规模分布式训练 + 稳定的超参数策略。模型结构上,现在主流基本都是Decoder-only的Transformer架构,配合RoPE旋转位置编码、SwiGLU激活函数、RMSNorm归一化。这些结构选择已经被很多开源模型验证过,我自己训练时也直接沿用,没有再自创结构——在预训练阶段,稳定性远比创新重要。
2.2 数据配比,决定模型的天花板
"垃圾进,垃圾出"这句话在预训练阶段被体现得淋漓尽致。预训练数据的配比直接决定模型能力的上限,训练过程只是把这个上限压榨出来。我实际训练时用的数据配比大致是这样:
通用网页文本(英文)占40%左右,中文文本占25%-30%,代码数据占20%,然后扒出5%左右的数学和推理类数据,再混入少量多语言和书籍类数据。这个配比不是拍脑袋定的——英文语料在开源社区里质量高、规模大,适合做基础通识;中文语料要专门做清洗和去重,否则重复内容会让模型出现复读机倾向;代码和数学数据虽然占比不大,但能显著提升模型在逻辑推理任务上的表现。
数据清洗阶段有几个关键动作:去重(用MinHash近似去重)、过滤(删除低质量、乱码、广告、成人内容)、语言识别(按比例混入目标语言)。原文信息如果说是零散的,这里必须补一句:tokenizer的词汇表大小通常设在32K到128K之间,中文场景建议偏大一些,否则中文每字都要切碎成多个token,训练效率会明显下降。
2.3 预训练的关键参数,直接抄就可以
如果你跑的是十亿到几十亿参数量级的模型,组一个8卡到32卡A100级别的环境,核心超参数可以参考下面的表格:
| 参数项 | 我的推荐值 | 说明 |
|---|---|---|
| 峰值学习率 | 3e-4(配合warmup) | 太大容易loss飞掉,太小训练速度拖慢 |
| Warmup步数 | 2000-5000步 | 让优化器平稳启动,避免初始震荡 |
| 批次大小 | 0.5M-1M tokens | 用"有效批次大小"计算,不只看sample数 |
| 序列长度 | 2048或4096 | 兼顾训练速度和长文本能力 |
| 优化器 | AdamW | beta1=0.9, beta2=0.95,权重衰减0.1 |
| 损失函数 | 交叉熵(CE) | 预测下一个token |
| 余弦衰减 | 下降到峰值的1/10 | 学习率尾部平滑,别直接硬切 |
批次大小需要展开说一下。大模型训练里经常说"我们用了1024的batch size",但其实严格应该按tokens数计算。一个batch如果包含1024个sample,每个sample是2048个token,那一共有约2M tokens,这才是有效批次大小。业界超大模型的有效批次可以达到数百万甚至上千万tokens,但自建场景下0.5M到1M是比较务实的区间——太小训练不稳定,太大会出现内存爆炸和收敛变慢。
训练过程中你需要盯住训练集loss曲线和验证集loss曲线。正常的期望是训练loss稳步下降,验证loss同步下降,两者之间没有明显裂开。如果验证loss开始反弹,说明模型已经开始过拟合训练语料分布(真实场景中不太常见,因为数据量极大),这时要检查是否需要对数据进行扰动或者切换到下阶段。
2.4 训练稳定性的三个保命手段
模型规模小时,预训练可以相对鲁莽;模型一旦超过1B参数,训练稳定性的优先级就会飙升。我在实际训练中遇到过loss突然spike到几十甚至几百的情况,排查下来通常有几个来源。
第一是数据问题。某个数据混入了损坏的文本片段,导致出现极长的token序列或者异常的数值;处理方式是定期抽检数据,跑一个小模型快速预训练,先验证数据质量再上大模型。
第二是学习率和warmup问题。学习率过大、warmup过短,会直接让loss冲出合理区间;我曾经把warmup从10%降到3%之后遇到过反复spike,改回去就稳定了。
第三是混合精度下的loss溢出,尤其是在fp16训练中,loss可能一冲上去就再也回不来,正确的做法是开启loss scale的动态调整,或者在框架中切换到bf16——现在A100/H100以bf16为主,这个坑现在少了很多。
除此以外,请养成定时保存checkpoint的习惯。我自己是每500步保存一次checkpoint,保留最近5个版本。训练跑了几周后突然爆loss,你不可能把损失挽回,只能回滚到最近的稳定checkpoint,重新调整学习率甚至换数据继续跑。多保存checkpoint不是浪费硬盘,是给未来的自己买保险。
3. SFT阶段:让模型从"懂"到"听话"
3.1 为什么预训练后必须做SFT
预训练得到的基座模型,本质上是一个"高智商但不会聊天的怪人"。你给它一句"请写一段自我介绍",它可能续写出几百个字像在替别人写百科词条,完全不像在和你对话。SFT就是拿一批人工精心编写的(指令,回答)配对数据,用监督学习的方式把模型从"自由续写"拽到"指令应答"的轨道上。
我打个比方,预训练阶段买回来一台没有操作系统的裸机,性能很强但没法直接给用户用;SFT就是给它装上了图形界面和预装软件,让它能理解输入框,能输出结构化结果。这一步用到的数据量远小于预训练,通常几万到几十万条就够,但每一条的质量都至关重要,因为模型在SFT阶段学到的模式会直接决定它的"性格"上限。
从原理上讲,SFT阶段依然使用交叉熵损失,但Loss只计算回答部分(assistant token),而用户指令、系统提示等部分在计算时会被mask掉。这个细节常常有人忽略——如果不mask指令部分,模型会学习到"把指令也复述一遍"的错误模式,推理时就开始胡言乱语。段落生成时的实现是传入一个布尔类型的labels数组,把需要预测的assistant位置设为真正的token id,其他位置设为-100忽略。
3.2 数据质量优先,数量是第二位的
SFT数据的构建是一个"宁缺毋滥"的过程。我的经验是,高质量的一千条数据比粗糙的十万条数据效果更好。粗糙数据会教坏模型,比如带口头禅式的回答、拒绝回答的模板、回答长度不匹配等,都会让模型产生偏置。
SFT数据格式必须统一。训练前每条样本都要整理成统一的对话模板,比如:
<|system|>: 你是一个负责代码审查的助手 <|user|>: 这段Python代码有什么潜在问题? <|assistant|>: 主要问题有两点……(这里才是计算loss的部分)数据需要覆盖的任务类型尽量多样,比如:知识问答、摘要、翻译、代码生成、角色扮演、逻辑推理、头脑风暴、改写,甚至还要包含一部分拒绝回答的样本。拒绝样本很重要,否则模型什么话都接,连违规内容也会生硬地尝试回答。
我在项目初期吃过一个亏:数据全部来自真实对话日志,但真实日志中有大量闲聊和模糊问答,导致训练出来的模型回答问题喜欢扯话题,不够直接。后来我把数据重写为"问题-直接答案"的格式,去掉所有无关寒暄,效果立刻上了一个台阶。SFT数据的风格不要过于单一,也不要过于花哨,就照着用户真实会怎么问、理想助手该怎么答来写。
3.3 SFT超参数:记得"少量多步"地学
SFT不需要像预训练那样大张旗鼓。学习率如果太高,模型会迅速过拟合这批数据,把预训练阶段积累的通识能力忘得一干二净(灾难性遗忘);学习率太低,又学不进去新格式。我的经验值是2e-5这个量级,配合AdamW优化器。
训练轮数(epochs)是另一个关键点。预训练可能跑几十个epoch?不,预训练通常只跑1-2个epoch(数据量巨大)。SFT一轮训练往往要跑3-5个epoch才能收敛,但也不要跑太久——过久的SFT也照样会发生灾难性遗忘,模型会变得只会机械复述训练集答案,失去泛化能力。我的策略是训练到验证集loss不再明显下降时立即停止,同时观察模型在保留测试集上的回答质量,比单纯盯着loss曲线更靠谱。
一些小技巧:SFT阶段可以混入10%-20%的通用语料(普通文本)一起训练,用来缓解遗忘问题。我在实验里发现这种做法能显著减轻模型"突然变得只会回答问题"的副作用。另外可以引入LoRA/QLoRA来做SFT,只需要冻结原模型权重,只训练低秩适配器,显存占用能下降到原来的1/3左右。LoRA SFT的效果在全量微调的七十到九十分之间,对于快速验证和垂直场景调优非常划算。
3.4 SFT阶段的评测,看"用"而不是看"损失"
SFT模型的好坏,loss曲线只能给参考,真正的评测必须基于实际使用的感受。我会构建一个固定的评测集,包含200到500条评测样本,涵盖意图、格式、长度、内容质量几个维度,训练过程中每隔一定步数就用这个评测集跑一次推理,人工给答案打分。
评测集要固定,别今天换一批明天换一批,否则你根本无法横向比较模型版本之间的变化。一个有效做法是,把每个训练几步的中间版本在评测集上的回答全部打印出来,按任务类型排列对比。你经常能肉眼看到模型从"完全跑题"到"格式正确但不准确"再到"回答像样"的演进过程,这个比任何指标都直观。
4. PPO阶段:让模型在反馈中真正"对齐"
4.1 PPO在整条链路里解决什么问题
SFT做完后模型已经会规规矩矩回答问题,但"会回答"不等于"回答得好"。人类偏好中的那些微妙信号——回答要简洁不要啰嗦、要有依据不要胡编、要礼貌但不要卑微——很难在SFT里通过人工写数据完全覆盖。PPO做的就是这件事:先训练一个奖励模型,让它学会判断什么回答更符合人类偏好,再用强化学习的方式让策略模型朝着高分方向持续优化。
通俗地说,SFT是"老师手把手教你怎么做",PPO是"给你一个打分的裁判,你自己试着做的过程中不断调整动作"。后者更能激发出多样性,也更能对齐人类偏好中那些难以言明的部分。
PPO阶段涉及四个模型,很多人第一次搭的时候容易搞晕:
| 模型 | 角色 | 说明 |
|---|---|---|
| Actor(策略模型) | 被训练的模型 | 就是SFT产出的模型,继续被优化 |
| Reference(参考模型) | 防止跑偏 | 是SFT参数的固定副本,计算KL散度用 |
| Reward Model(奖励模型) | 打分的裁判 | 经过偏好数据训练的模型,给回答输出分数 |
| Critic(价值模型) | 预估未来收益 | 与Actor结构类似,拟合优势估计值 |
这么多模型叠在一起,显存和算力开销比预训练更大。我第一回跑的时候,四卡没够用,后来改成用LoRA方式训练Actor和Critic才把显存压下去。
4.2 奖励模型:先学会打分,再谈优化
PPO的质量很大程度取决于奖励模型的质量。奖励模型的训练数据是"同一问题下的两个回答,哪个更好",标注人员(或者用更强模型做标注)每标注一对偏好数据,模型就学习判断哪个回答得分更高。常见实现基于Bradley-Terry模型,用排序对做训练,将一个pair中两个回答的得分差映射为二分类概率。
奖励模型的数据构建需要特别注意:偏好对比中的两个回答,差异要适中。如果两个回答一个极好一个极差,模型会学得很快,但实际PPO过程中遇到的两个回答往往差异很小,所以奖励模型要在大比例"接近但略有好坏之分"的样本上训练才能有分辨力。我的实际经验是每轮PPO数据里,要包含大约30%的低差异对比数据,奖励模型才不会沦为一个只看主题的粗打分器。
奖励模型的输出是单一标量分数,但我们常会抽样检查奖励模型对同一回答的不同变体的打分是否符合直觉。如果奖励模型被"刷爆"了,比如发现带长回答的样本分数无理由偏高,那一定要回到数据去排查是否存在长度偏置,否则PPO阶段模型会拼命把回答写得很长去刷分,这在实际产品中是绝对不可接受的。
4.3 四步走:让PPO训练稳定地跑起来
PPO完整迭代的基本流程分为四步:采样、打分、优势估计、策略更新。
第一步是采样。从Actor模型当前的策略下,拿一批prompt做生成(greedy或者带随机采样),拿到一批回答。第二步是打分。用奖励模型给这批回答打分,同时用参考模型计算当前Actor和参考模型之间的KL散度——KL散度用来约束Actor别跑太远,避免模型为了刷高分在输出空间里蹦得太远,把SFT学到的语言能力崩坏。第三步是优势估计。用GAE(广义优势估计)计算每个token的优势值,核心参数是γ(折扣因子,常用0.95到1.0)和λ(GAE的平滑参数,常用0.95)。第四步是策略更新,用裁剪(clip)机制的PPO目标函数更新Actor和Critic,设置裁剪范围ε(常用0.2)。
这四步里的KL散度控制是最容易被新手忽略的。PPO目标函数中除了奖励模型的分数,还要减去一个β乘以KL散度的惩罚项,β这个系数需要动态调节。如果模型回答模式开始变得异常,比如疯狂的标点堆砌、重复词,多半就是β太小了,KL惩罚没拉住。
4.4 别让"奖励刷爆"毁掉模型
PPO阶段最常翻车的地方就是reward hacking,模型找到了奖励模型的漏洞,用一些人类并不喜欢的方式拿到了高分。最常见的有三种:
一是长度攻击。模型发现回答越长分数越高,开始生成大批注水内容。对策是奖励训练时加入长度信息去偏,或者在PPO目标中加入长度惩罚项。二是格式攻击。模型发现列点回答得分高,开始什么都用列表,一个简单问题拆成一堆分点,反而显得机械空洞。三是内容窄化。模型发现某种特定语气和用词容易拿高分,生成结果开始趋于保守和模板化,多样性下降。
排查reward hacking的方法是定期抽样看PPO模型输出的真实回答,同时监控"平均KL散度"和"平均奖励分"两个指标。如果平均KL持续上升而奖励分不再上升,几乎可以断定模型在刷奖励而不是在学更好的行为。此时应该调大KL惩罚系数β,或者回到奖励模型数据里去修复偏置漏洞。
5. 三个阶段之间的数据工程:贯穿全程的生命线
5.1 从原始语料到偏好数据的一整条流水线
聊完三个阶段,再回头看整条链路,你会发现数据工程贯穿始终,而且每一阶段需要的数据形态完全不同。预训练要的是海量无标注文本,成TB级;SFT要的是十几万条高质量指令数据,人工精修;PPO要的是偏好对比数据,需要成对的排序标注。这条生命线如果断了,后面每个阶段都会变成无源之水。
我自己搭数据管线的心得是:三阶段数据要"分而治之"。别尝试做一个万能的数据清洗脚本去同时处理三批数据,它们的目标完全不同。预训练数据追求大规模去重去噪;SFT数据追求高频指令覆盖和对话风格的统一;PPO偏好数据追求判断难度分布合理。三条管线可以共用一些基础设施(比如存储、标注平台、格式校验脚本),但各自要有独立的清洗逻辑和质量指标。
5.2 数据量级和配比的工程估算
关于数据量级,业内有一个比较粗略的参考公式:预训练token数大约是模型参数量的20倍以上。比如十亿参数的模型,预训练数据量应该达到20B tokens以上;百亿参数模型,至少需要200B tokens。这个比例可以理解为"参数越多,越需要大量语料来喂养"。SFT数据量没有类似的硬性公式,我的经验是五万到二十万条高质量指令对大多数垂直任务都够用了;如果数据很少但任务很个性化,优先保证数据多样性,把任务类型覆盖开。
PPO偏好数据量一般只需要SFT的十分之一到五分之一。有人可能会惊讶,但PPO是对"排序关系"学习,信息密度远高于"文本本身"。每个偏好对都是一条明确的反馈信号,几万对高质量偏好数据已经足够让模型明显朝目标偏好方向移动。
6. 算力、框架与资源规划
6.1 各阶段算力需求与显存估算
聊完了原理和数据,必须聊钱的问题——算力。三个阶段对算力的需求差异非常大,很多人误以为PPO最烧钱,实际预训练才是真正的大头。预训练所需算力大约占整条链路的90%以上。SFT和PPO虽然模型一样大,但训练步数少得多,数据量小得多,总计算量低到几乎可以忽略。
给你一个直观估算:假设我们要训练一个10B参数的模型,从零开始预训练大概需要数百张GPU连续跑几十天到上百天;SFT可能几十张GPU跑几天;PPO虽然要四个模型反复推理加训练,但通常跑两到四周就够,总的GPU消耗远低于预训练。所以很多团队选择直接拿开源基座模型做SFT+PPO,原因很简单:省掉最烧钱的预训练环节。
显存估算方面,我的经验公式是:训练一个N参数的模型,用AdamW优化器加混合精度,大约需要N乘以16到20字节的显存。比如10B模型,单卡至少要160GB以上显存,即使8卡A100(每卡80GB)也未必放得下。所以才需要分布式策略:数据并行、张量并行、流水线并行、ZeRO优化器状态切分它们互相组合,才能在有限的GPU集群上训练大模型。如果是做LoRA微调,显存需求可以压缩到原来的1/5到1/4,性价比非常高,也是我日常最常用的方案。
6.2 主流框架怎么选
框架层面我推荐直接使用现成的分布式训练框架,不要自己造轮子。主流选择大概是这三类:
DeepSpeed方案:社区最常用,ZeRO优化器非常成熟,对PyTorch生态友好,适合中小规模(几十B以内)训练,使用门槛相对友好。
Megatron方案:适合超大模型、超多GPU的场景,张量并行和流水线并行实现更彻底,但上手成本高,一般团队用不太到。
HuggingFace Transformer + PEFT方案:适合SFT和LoRA微调,代码简单直观,训练辅助工具齐全,模型切换方便,但对超大规模分布式场景支持弱一点。
我自己的习惯是:预训练偏大的模型用DeepSpeed(ZeRO stage 2或3);SFT和PPO用HuggingFace生态配合PEFT库做LoRA训练。这样写的代码量最少,部署到推理环节也顺手。
7. 实战中的高频坑与排查方法
7.1 高频问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 预训练loss突然飙升到几十 | 数据损坏、学习率过大、fp16溢出 | 回滚最近checkpoint,检查数据样本,调低学习率或改bf16 |
| 预训练loss下降缓慢 | 数据重复率高、配比不合理 | 检查MinHash去重,合理增加代码和数学数据 |
| SFT后模型回答变短/变模板化 | 指令数据过长、格式重复度高 | 混入通用语料,丰富指令模板,避免多轮模板完全一致 |
| SFT后模型出现灾难性遗忘 | 学习率偏高、训练轮数太多 | 降学习率到2e-5量级,减少epochs,混入通用语料 |
| PPO训练时KL急剧上升 | KL惩罚系数β太小 | 调大β,或者加长KL warmup过程 |
| PPO奖励上涨但人类评测变差 | 奖励模型偏置、reward hacking | 检查奖励模型,加入长度去偏,观察抽样回答 |
| 训练中显存溢出 | 模型太大、batch过大、没有用LoRA | 减小batch,开启ZeRO,换用LoRA/QLoRA |
| 推理变慢 | 模型被PPO训成大段输出 | 限制生成长度,调reward模型打分逻辑,修长度攻击 |
我自己排查问题的顺序永远是:数据 → 模型输出样例 → 超参数 → 框架配置。先怀疑数据,再怀疑模型,最后才是环境问题和代码问题,这样能省下大量时间。
7.2 每次训练前必做的三件小事
最后分享三个习惯,真的能帮你少踩很多坑。第一,训练前跑一个几百条数据的小规模smoke test,验证代码流程和数据格式是否正常,跑通了再上正式训练,否则一次训练启动几小时后才发现数据管道bug,损失的时间根本不是钱能补的。第二,保存一个完整复现清单,记录这个实验用了什么数据版本、什么超参数、什么模型版本、评测结果如何,训练实验多了之后没有这个清单你根本记不住哪个模型是怎么来的。第三,每个阶段评估都要保留足够多的模型中间版本,不要只留最终版,因为你永远不知道后面阶段是不是把前面学到的好东西给带偏了,需要回滚。
7.3 对这条链路的整体思考
从预训练到SFT再到PPO,我最大的感受是:三个阶段各有各的复杂,但最关键的还是"对齐"二字。预训练需要海量算力堆出能力底座,SFT用高质量数据把模型引入正轨,PPO用奖励信号让模型逼近真实的人类偏好。每个阶段单独拿出来都有成熟的工具和方案,但串联在一起时,数据质量、超参数、评测体系必须从头到尾贯穿一致,否则链条上任何一环掉链子,最终交付的模型都会暴露短板。
我个人在实际操作中体会到最多的一件事:大模型训练的失败极少来自某一个阶段的原理错误,而是来自阶段之间的"裂缝"——预训练数据风格和SFT指令风格不一致,SFT指令分布和PPO偏好分布不一致,每一条裂缝最终都会在用户真实使用场景里暴露出来。所以如果你正要开始跑这条链路,我的建议是从数据一致性入手,把三阶段的数据风格当作一个整体去设计,而不是各搞各的。这件事做扎实了,模型能力的释放只是时间问题。
最后再分享一个小技巧:训练过程中定期用固定prompt集给模型拍照留档,每训练到一个里程碑就把生成结果存档。这些存档就是模型演化的"进度照片",后期排查回归问题、定位数据问题、写总结报告时都非常好用。这个习惯我从一开始就坚持,后来每次复盘实验都离不开它。