SFT+GRPO两阶段训练实战:用ms-Swift调优大模型推理能力
2026/9/16 18:51:53 网站建设 项目流程

做LLM训练的朋友应该都有体会,从SFT到RL这个阶段,工程上的坑往往比算法本身还多。最近我用ms-Swift框架完整跑了一遍“SFT + GRPO”两阶段训练,把一个小参数模型从只会“背答案”调教到能按照奖励信号自主优化推理路径,中间踩了不少坑,也沉淀了一套可复用的策略。这篇文章把整个流程、参数选择、踩坑记录都整理出来,如果你正准备在自己的数据集上跑类似的pipeline,可以参考着抄作业。

ms-Swift是阿里开源的一套大模型训练与推理框架,对SFT、DPO、GRPO、KTO这些主流对齐算法都有现成支持,而且把vLLM推理加速、LoRA/QLoRA轻量训练、数据集管理这些工程细节都封装好了。我选它而不是直接手写训练脚本,核心原因就一个:在GRPO阶段,rollout推理和reward计算的工程链路非常繁琐,ms-Swift把这些都标准化了,我可以把精力放在数据和奖励设计上,而不是去调试分布式采样和生成缓存。

这篇文章适合谁看?如果你已经跑过基础的SFT微调,正想往RL方向迈一步,或者你试过RLHF但被PPO的critic模型、KL散度控制这些复杂组件劝退,那GRPO这条路值得试一下,而ms-Swift恰好把这条路铺平了大半。下面我按实际项目推进的顺序来讲。

1. 整体设计思路:为什么把SFT和GRPO放在一起做

1.1 先说清楚SFT和GRPO各自解决的问题

大模型从预训练到可用,通常会经历几个阶段:预训练、SFT、RLHF/GRPO。预训练阶段模型学的是“语言统计规律”,SFT阶段学的是“模仿人类回答”,而GRPO阶段则是让模型在奖励信号的引导下自己探索更好的回答方式。

很多人容易有一个误区:觉得SFT已经让模型会答题了,RL阶段可有可无。但实际跑过之后你会发现,SFT学到的模式是“有样学样”,数据里有什么它就学什么,边界被训练数据锁死了。比如我用通用SFT数据微调模型,它给出的数学推理步骤表面上很规范,但仔细看会发现中间步骤经常冗余甚至错误,只是最后答案碰巧对。GRPO的目的就是打破这层边界,让模型在奖励函数的反馈下,自己权衡哪些步骤是必要的、哪些推理路径更容易拿到高分。

SFT和GRPO不是二选一的关系,而是接力关系。SFT负责把模型从“会说话”教到“会答题”,GRPO负责从“会答题”教到“答得好”。少了SFT直接上GRPO,模型生成的都是些乱七八糟的文本,奖励信号根本无法有效传播;只有SFT没有GRPO,模型又缺乏自我优化的能力。两阶段协同,才是这套策略的核心逻辑。

1.2 ms-Swift框架选型背后的三个理由

我在选框架时其实调研过几个方案,包括HuggingFace TRL、OpenRLHF、ms-Swift。最终选定ms-Swift,有三个决定性因素。

第一个是GRPO的工程链路完整度。GRPO算法本身不复杂,但实现起来涉及策略模型采样、奖励计算、组内优势归一化、策略更新这么一长串流水线。ms-Swift把GRPO的rollout过程接入了vLLM,生成速度比原生HuggingFace实现快了好几倍,而且已经处理好了序列填充、左padding、生成掩码这些细节,省了我大量调试时间。

第二个是数据集转换的便利性。ms-Swift支持Alpaca格式和ShareGPT格式,我手上的数据稍作整理就能直接喂进去,不用为了满足某个框架特定的数据schema写一堆转换脚本。

第三个是算力友好度。小团队很难有足够的GPU去全参数跑RL。ms-Swift的LoRA/QLoRA支持很成熟,我可以只在adapter上做SFT和GRPO更新,显存占用比全参数微调低一个数量级。实测下来,在单张24GB显卡上跑7B模型的QLoRA+GRPO是可行的,这对没有大型GPU集群的团队非常关键。

当然它也有缺点:文档中关于RL部分的中文资料相对少,很多参数需要自己去读源码确认,这也是我写这篇文章的一个动机。

2. 数据准备与SFT微调实操

2.1 SFT样本到底要准备什么

聊到SFT,第一个绕不开的词就是“sft样本”。SFT样本的本质是“输入-期望输出”的配对数据。你给模型一个问题,它需要学会给出你标注的那个答案。但这里有个很容易被低估的点:样本质量比样本数量重要得多。

我在项目里准备SFT数据时,遵循了几个原则。第一,答案必须经过人工抽检。自动爬来的数据表面整齐,但经常有逻辑跳跃、计算错误,这些坏样本会被SFT忠实地学进去。第二,样本覆盖的题型分布要和真实应用场景一致。如果你做的是数学推理应用,却拿大量通用问答数据做SFT,模型会被带偏。第三,对于推理类任务,在SFT阶段就要把思维链写出来,哪怕是“简化版”思维链。因为GRPO阶段奖励函数判断的是完整回答,如果SFT阶段模型从来没学过“先推理再给答案”的格式,那GRPO阶段它连正确的格式都生成不出来,更别说优化内容了。

sft样本数量的选择也是个经验活。并不是样本越多越好。我用一个小参数量模型做过对照实验:2万条精选数据的效果明显好过10万条粗筛数据。原因在于SFT阶段模型容量有限,喂太多低质量数据会把attention容量浪费在冗余pattern上。对于7B模型做领域微调,起步建议1万到5万条高质量样本,重点观察loss收敛曲线和实际case效果,再决定是否扩充。

2.2 SFT微调的参数配置与实操记录

用ms-Swift跑SFT,核心命令简单到出人意料。这里贴一段我实际用过的LoRA微调配置:

CUDA_VISIBLE_DEVICES=0 swift sft \ --model Qwen/Qwen2.5-7B-Instruct \ --train_type lora \ --dataset ./data/sft_train.jsonl \ --torch_dtype bfloat16 \ --max_length 2048 \ --batch_size 4 \ --gradient_accumulation_steps 8 \ --learning_rate 2e-4 \ --lr_scheduler_type cosine \ --warmup_ratio 0.1 \ --num_train_epochs 3 \ --eval_steps 500 \ --save_steps 500 \ --output_dir output/sft_lora

有几个参数值得展开说一下。

LoRA的rank我选了16,alpha取32。对于7B模型,rank在8到32之间通常够用,太高会失去LoRA的低秩约束优势,太低则表达能力不够。alpha一般设成rank的1倍到2倍,我实测2倍效果稳定,在新领域数据上不容易过拟合。

学习率2e-4是ms-Swift LoRA任务的标准起点。如果你是全参数微调,学习率要降到1e-5左右;但LoRA因为只在低秩子空间更新,学习率可以激进一些。训练过程中我盯了一下loss曲线,SFT的loss在第1个epoch快速下降,第2个epoch之后进入平台期,第3个epoch其实已经开始轻微过拟合。所以跑LoRA微调时,epoch数不必贪多,3轮是性价比比较高的选择。

数据格式上,ms-Swift接受Alpaca格式的JSONL,每行一个样本:

{"system": "你是一个数学解题助手", "query": "一个三角形三边分别为3、4、5,求面积。", "response": "因为3^2+4^2=5^2,所以这是直角三角形,两条直角边为3和4,面积为1/2*3*4=6。"}

如果你的数据是ShareGPT格式(带多轮对话),ms-Swift也能直接处理。但有一类坑我遇到过:有些开源数据集中response字段包含大量换行和特殊字符,ms-Swift的tokenizer在处理这些内容时可能会产生意外的填充符。所以数据预处理阶段做一下文本清洗,把异常空白符、控制符统一处理掉,能省掉后面很多麻烦。

SFT训练完成后,不要急着进GRPO。先做一轮评估:用测试集跑几次推理,看模型回答是否已经具备基本格式,有没有出现重复、胡言乱语。我在项目里用ms-Swift自带的infer命令,配合vLLM做了快速批量推理,效果比单条调试高很多。

CUDA_VISIBLE_DEVICES=0 swift infer \ --model output/sft_lora/merged \ --infer_backend vllm \ --max_new_tokens 1024

3. GRPO阶段的核心细节

3.1 GRPO和PPO比,到底优化了什么

GRPO全称是Group Relative Policy Optimization,是DeepSeek开源数学推理模型中验证过的强化学习算法。要理解GRPO,先得从PPO说起。

PPO是RLHF阶段最经典的策略优化算法。它需要四个模型:策略模型(被训练的那个)、参考模型(避免策略跑太远)、奖励模型(打分的)、以及一个critic模型(估计状态价值函数)。critic模型的引入是为了降低策略梯度的方差,但它本身也要训练,而且训练不稳定,内存开销也大。

GRPO做了一个关键简化:不要critic模型了,改用组内相对比较来计算优势。具体做法是,对同一个问题,让策略模型生成G个回答(比如8个),把这G个回答按奖励从高到低排序,然后计算每个回答相对于组内均值的优势值。回答比组内平均好,优势为正,梯度鼓励这类回答;比平均差,优势为负,梯度抑制这类回答。这样做的直觉是:单个奖励分数的绝对值有噪声、有偏,但同一问题下多个回答的相对好坏是非常可靠的学习信号。

这种设计带来的好处很直接:不需要训练critic模型,内存占用少了,训练稳定性反而更高了,因为相对排序不会像绝对分数那样容易受reward model的bias影响。GRPO非常适合参与式小规模团队使用,这也是我在这个项目里最终选择GRPO的原因。

3.2 奖励函数、超参与rollout配置

GRPO训练的关键,其实是奖励函数的设计。奖励函数决定了模型优化的方向,如果奖励函数有漏洞,模型会万里奔袭去钻漏洞,这是RL训练中最常见也最头疼的问题。

在这个项目里我做了两种奖励的混合:规则奖励(format reward)和模型奖励(judge reward)。规则奖励用程序判断回答是否满足指定格式要求,比如是否包含完整的“推理过程”和“最终答案”标记;答案是数学数值的话,可以直接校验和标准答案是否一致。模型奖励则用一个小的judge模型判断回答的推理步骤是否逻辑通顺、步骤是否冗余。

奖励分数我控制在0到2之间:格式正确+最终答案正确=2分,格式正确但答案错误=0.5分,格式错误=0分。注意一点,如果完全不给格式正确但答案错误的回答正的奖励,模型很快会趋向于只输出一两个token来碰运气,因为生成越短越不容易犯错。给0.5分是让模型明白:格式对但答案错,比格式错好一些,至少步骤是有价值的。

GRPO的超参设置上,我用的ms-Swift命令如下:

CUDA_VISIBLE_DEVICES=0 swift rlhf \ --rlhf_type grpo \ --model Qwen/Qwen2.5-7B-Instruct \ --adapters output/sft_lora \ --train_type lora \ --dataset ./data/grpo_train.jsonl \ --reward_funcs custom \ --custom_reward_func my_reward.py:compute_reward \ --num_generations 8 \ --max_new_tokens 1024 \ --temperature 0.9 \ --top_p 0.95 \ --max_length 2048 \ --batch_size 2 \ --gradient_accumulation_steps 8 \ --learning_rate 1e-5 \ --lr_scheduler_type cosine \ --warmup_ratio 0.05 \ --num_train_epochs 1 \ --eval_steps 200 \ --save_steps 200 \ --output_dir output/grpo_lora

几个关键参数我解释一下。

num_generations是组内采样数,也就是上面说到的G。这个值是GRPO训练中最重要的超参数。G太小,组内相对比较的统计噪声大;G太大,单条样本的采样时间线性增长。我实测8到10是比较均衡的选择。资源紧张时至少别低于6,低于6优势估计的方差会明显变大,训练曲线会比较抖。

temperature在rollout阶段设成0.9,因为需要模型有一定探索能力。采样温度过低会导致生成的G个回答高度雷同,组内排序失去意义;温度过高样本质量太差,奖励普遍很低,学习信号又会淹没在噪声里。0.9是我在多轮实验里验证过的适用范围。

学习率从SFT的2e-4降到1e-5,原因在于RL阶段策略不能大踏步更新。GRPO通过组内相对优势调整策略,如果学习率大,一个批次就会把策略推出安全区域,后面会遭遇生成质量崩溃,奖励越训越低。

4. SFT与GRPO的协同优化策略

4.1 两阶段如何衔接,checkpoint如何选

我见过不少人在SFT完成以后直接拿最后一个checkpoint进GRPO,这其实不一定是最优的。SFT阶段最后的checkpoint可能已经轻微过拟合了训练集,模型生成多样性降低,而这恰恰是GRPO阶段最忌讳的——多样性不足,组内采样的G个回答近乎相同,相对优势就失灵了。

我通常的做法是:在SFT训练过程中保存多个checkpoint,然后拿中间某个checkpoint做GRPO的起点。具体来看,如果SFT训练了3个epoch,我一般取第2个epoch结束时的checkpoint作为GRPO起点。这个checkpoint下模型已经学会了基本格式和推理风格,但对训练集还没有完全死记硬背,生成时保留了一定随机性。实测同样的GRPO配置,使用中间checkpoint的收敛速度比使用最后一个checkpoint快约20%。

另外一个容易被忽略的细节是:SFT阶段用的LoRA参数不要直接丢弃。进入GRPO时把SFT训练出的LoRA作为init adapter加载进来,而不是从基座模型从头开始。这样做的好处是,策略模型的初始分布更接近SFT收敛点,而不是基座模型分布,GRPO阶段策略更新的起点更优。

4.2 从SFT到GRPO的reward一致性设计

协同优化的另一个关键点是:SFT阶段和GRPO阶段的奖励/数据口径要保持一致性。

具体来说,SFT阶段你教给模型的输出格式,必须在GRPO阶段被奖励函数识别并奖励。如果SFT阶段要求模型用“解析:...结论:...”这种格式输出,但GRPO阶段的规则奖励只检查“推理:...答案:...”,那模型在RL阶段无论怎么探索,格式分都拿不到,学习信号基本归零。

我在项目中把prompt模板做成了全局常量,SFT数据生成、GRPO奖励检查、最终推理服务三处共用同一套格式定义。这样虽然是个笨办法,但能保证几个阶段的格式口径不会漂移。

另外我还养成了一个习惯:GRPO训练过程中定期存checkpoint,并且每50到100步用固定的验证集做一次生成采样,人工看十几条case。RL训练的loss曲线往往不能完全反映生成质量,尤其是reward funcs是自定义规则时,loss下降但实际回答变差的情况经常出现。宁可训练慢一点,也要保持人工抽检的频率。

4.3 显存与效率的工程化调优

小规模团队跑GRPO,显存是最现实的瓶颈。GRPO阶段比SFT多了一个rollout过程,策略模型生成G个回答时全部要走一遍前向,显存压力比SFT高出一倍以上。

我在工程上做了三个优化:

第一,rollout阶段用vLLM推理而不是直接调用模型生成。vLLM在batch生成上的吞吐量优势非常大。ms-Swift内部已经提供了vLLM backend,我只需要在训练命令中把生成相关参数配置好即可,不需要额外写推理服务。唯一要注意的是vLLM推理使用的GPU显存和训练使用的显存是独立的,如果两张显存都在同一张卡上,会让可用batch size缩水不少。我实测下来,把rollout和训练放在同一张24GB卡上,训练batch size基本顶不到4,所以有条件的尽量切分GPU,或者降低batch size换取稳定性。

第二,把模型量化为4bit做QLoRA训练。GRPO阶段使用NF4量化训练adapter,模型精度略降,但显存占用大幅减少。对于数学推理任务,4bit量化对最终性能的影响通常很小。

第三,控制max_new_tokens。GRPO阶段生成的回答不需要过长,把上限控制在1024以内。很多推理问题其实300到500个token就能完成,上限开太大只会让rollout变慢,并浪费显存缓存。

5. 常见问题与排查技巧实录

5.1 训练不收敛、奖励异常这类典型问题

先列一个我在多次实验中遇到的高频问题速查表。

现象可能原因解决方法
奖励曲线整体不上升奖励函数本身无法区分好坏人工检查reward打分,确保满分和低分样本有区分度
奖励上升但回答质量变差模型在钻奖励函数漏洞增加format约束,补充“格式错误直接判0分”的规则
生成回答重复率过高temperature过低或SFT过拟合调高temperature到0.9以上,或换较早的SFT checkpoint
显存OOMrollout和训练共用显存降低batch size,使用4bit量化,缩小max_new_tokens
组内G个回答完全相同temperature太低或模型退化调高温度,检查是否加载错了checkpoint
Loss出现NaN学习率过高,或数据中存在过长的异常文本降低学习率,清洗数据,检查max_length截断

这里面最值得展开说的是“奖励上升但回答质量变差”。这是RL训练中最经典的奖励黑客(reward hacking)问题。我曾经在奖励函数里加了“推理步骤中出现了关键公式就给0.3分”的规则,结果模型很快学会了把所有公式都列一遍,甚至重复列出无关公式,推理逻辑依然一塌糊涂,但奖励分数却升高了。要解决这类问题,最有效的方法是把奖励函数做得更粗粒度,越细粒度的规则越容易被钻空子。宁可牺牲一点奖励信号的分辨率,也要保证信号本身不可作弊。

5.2 数据质量问题的排查经验

GRPO阶段用的数据其实比SFT阶段更讲究。SFT可以容忍一定程度的噪声,模型会从大量样本中学到共性。GRPO对数据质量非常敏感,因为奖励函数的计算依赖标准答案,如果数据里标准答案本身是错的,模型会学到“正确答案也被判错”的错误关联。

我在项目里做过一次比较惨痛的实验:有一批数学题的参考答案人选错误比例约为5%,结果GRPO训练中模型的最终答案准确率始终徘徊在70%左右上不去。排查了很久才发现是答案错了。所以进入GRPO之前,务必对训练数据做一轮标准答案抽检,建议抽检比例不低于10%。如果条件允许,用程序化方法做一次标准答案的一致性检查,比如数学题重新计算一遍,比单纯看文本更能发现错误。

另外一个数据相关的坑是prompt模板不一致。GRPO训练时,一个问题可能被采样多次,如果prompt模板里有随机变化的字段,比如“请回答这个问题:{question}”和“请解决这个数学题:{question}”混用,那么模型学习的输入分布就不稳定,奖励信号也会被分散。把prompt模板固定下来,即使有多个模板,也要按比例固定好,而不是每次都随机。

5.3 一点关于训练效率的个人建议

最后分享一个我在资源紧张时的做法:GRPO训练之前,先跑一个极短的调试版本,把num_generations临时降到4,步数设成10步,验证整个链路(rollout、reward计算、策略更新)能跑通,再上完整配置。这个调试版本只要几分钟,能省下正式训练后才发现配置错误的时间成本。我因为跳过这一步吃了两次亏,一次是自定义reward函数里有个除零错误,另一次是数据集的字段名和ms-Swift期望的不一致,都是几分钟就能在调试版本中发现的问题,放到正式训练里就要浪费几个小时。

还有一个容易被忽略的小技巧:保存的自定义reward函数路径必须写绝对路径,或者确保路径相对于运行目录正确。ms-Swift在加载自定义reward时对路径的解析比较严格,我最初用相对路径怎么都加载不成功,换成绝对路径一次通过。

我在实际跑完这套SFT+GRPO的pipeline之后,最大的体会是:GRPO算法本身的复杂度比PPO低很多,真正难的是奖励工程和数据工程。模型有没有推理能力,很大程度上在SFT阶段就已经定型了;GRPO只是把这个能力引导到“更符合奖励期望”的方向上。所以如果你准备在自己的项目里复现这套方案,我建议你先花大力气把SFT数据和奖励函数打磨好,再谈训练策略本身。数据对了,GRPO的训练曲线会给你惊喜。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询