Day21 | GRPO 实战——从零跑通一个能解数学题的 reasoning 模型
苦猿的大模型日记 · Day21 · GRPO 实战——从零跑通一个能解数学题的 reasoning 模型-帮普通人把AI学进简历系列
前言:那个 reward 死活不涨的凌晨
我给你讲个事。
上个月某个周末,凌晨一点,我对着 SwanLab 上的一条平直红线发呆。
那条线叫reward/mean,理论上是 GRPO 训练里最重要的指标——它该是波动的、上升的、有生气的。可它从第 1 步到第 50 步,纹丝不动地贴在 -2.0 附近,像死人的心电图。
我调的是 Qwen3-8B,跑 GRPO,数据集是 DAPO-Math-17k。模型加载正常、reward 函数能编译、训练循环能跑——可模型就是不学。
第一反应,reward 函数写错了。我把check_answer拆开一行行看,没问题。
第二反应,数据集 ground truth 有问题。我把dataset[0]["solution"]打印出来,对得上。
第三反应——lr 太小。我把学习率从5e-6提到5e-5,reward 还是 -2.0 一条直线。
折腾到凌晨三点半,我才反应过来——
是<start_working_out><end_working_out><SOLUTION>这三个特殊 token 没加进 tokenizer。
apply_chat_template我调了,chat template 拼得也对,prompt 里清清楚楚写着"按<start_working_out>...<end_working_out><SOLUTION>...</SOLUTION>输出"——可模型压根没把这些尖括号字符串当成"特殊标记"。在它眼里<SOLUTION>跟"hello"一样是普通字符,于是输出时该写<SOLUTION>的位置它写成< solution ><SOLUTION ><SOL>……各种花样。
而我的 reward 函数靠正则严格提取<SOLUTION>标签——错一个字符都提取不到,check_answer直接返回 -2.0(格式错),check_numbers返回 -2.5(提取不到数值)。
所以 group 内 8 条回答 reward 全是 -2.0 / -2.5 这类负分,几乎没区分度,advantage 长期接近 0,梯度近乎为 0。模型什么都学不到。
那一刻我突然想明白一件事——
GRPO 难的根本不是算法,是 reward 函数和你模型实际输出能不能对上电波。
上一篇我讲 GRPO 理论的时候,把算法公式、group baseline、三大改造讲了一遍。今天这篇,我给你一把铲子——从零到一,跑通一个能解 GSM8K 数学题的 Qwen3-8B。
跑完之后你会发现,公式那张纸只占 10% 的工作量。剩下 90%——全是 chat template、reward 函数、reward 又不涨、KL 又爆炸这种工程脏活。
而面试官最爱问的,恰恰就是这 90%。
PART 01:为啥必须先 SFT 再 GRPO——R1 路线的真相
很多人对 DeepSeek-R1 有个误解——
"R1-Zero 不是直接对 base 模型跑 GRPO 就行吗?"
是的,那是 72B + 2048 张 H800 集群的暴力美学。你拿来跑 8B 单卡,百分之百不收敛。
为什么?因为 base 模型根本不会按<start_working_out>...<end_working_out><SOLUTION>...</SOLUTION>这种格式输出。它的输出是自然语言大白话。
而 GRPO 的 reward 函数靠正则提取<SOLUTION>标签——base 模型不说这种话,reward 函数全返回负的固定值(-2.0 / -2.5),group 内 8 条几乎没区分度。
advantage 长期接近 0,梯度近乎为 0——模型什么都学不到。
这就是为什么 self-llm 的官方教程、Unsloth 的官方 notebook,全都把训练分成两阶段:
SFT 阶段:教模型"说话的格式"
SFT 阶段的目的不是教模型会做题——这一点很关键。
目的是教模型按固定的格式说话:
<start_working_out> (思考过程) <end_working_out> <SOLUTION> (最终答案) </SOLUTION>只要模型稳定地输出这种格式,SFT 就成功了。会不会做题,那是 GRPO 阶段的事。
GRPO 阶段:教模型"做对题"
格式对了之后,reward 函数才能稳定提取答案、给奖励。这时候 GRPO 才开始有意义——模型通过试错学会"什么样的思考链能拿高分"。
数据集选择
- SFT 阶段:
unsloth/OpenMathReasoning(含完整 CoT 推理链,2025 reasoning 训练的事实标准) - GRPO 阶段:
open-r1/DAPO-Math-17k-Processed(DeepSeek 开源 R1 路线的官方数据集处理版)
两个数据集的设计目的不一样——SFT 用的是带完整推理链的"标准答案",GRPO 用的是只有最终答案的"题目"。前者教格式,后者逼推理。
SFT 配置——别跑多
trainer = SFTTrainer( model = model, processing_class = tokenizer, train_dataset = dataset, args = SFTConfig( per_device_train_batch_size = 1, gradient_accumulation_steps = 1, warmup_steps = 5, num_train_epochs = 2, # 别跑多,2 epoch 够学格式 learning_rate = 2e-4, # 比 GRPO 大一个量级 optim = "adamw_8bit", weight_decay = 0.01, lr_scheduler_type = "linear", report_to = "swanlab", ), )几个反直觉点:
num_train_epochs=2:很多人觉得 SFT 该跑 5-10 epoch 把 loss 压下去。这里不能多跑——多跑了模型 CoT 风格僵化,反而不利于 GRPO 阶段探索learning_rate=2e-4:比 GRPO 阶段大两个数量级。LoRA 训练有"快进快出"的特点,lr 小了学不动per_device_train_batch_size=1:8B 模型在单卡上的极限。靠gradient_accumulation_steps凑等效 batch
SFT 跑完,你应该能看到 loss 从 1.5 左右下降到 0.5 以下。这时候模型已经稳定按格式输出了,可以进 GRPO。
如果你 SFT loss 卡在 1.0 以上不下降——先回头查数据预处理,别急着进 GRPO。
PART 02:模型加载——Unsloth 怎么把 8B 塞进单卡
我上一篇讲 GRPO 理论的时候,硬件门槛写的是"4 卡 A100 80G 起步"。
那是没用 Unsloth 之前的乐观估计。2025 下半年开始,Unsloth 把 Qwen3-8B GRPO 的显存账砍到让人发指——
单卡 H100 80G,QLoRA 路线,能跑 Qwen3-8B GRPO,context 拉到 110K。
Unsloth 的三板斧
- Flash Attention 2/3:注意力层显存砍掉一半以上
- QLoRA 4bit 量化:8B 权重从 BF16 的 ~16GB 压到 ~5GB
gradient_checkpointing="unsloth":用算力换显存,反向传播时重算激活值
三板斧下来,80G 卡跑 GRPO 还能留出 30-40G 给 vLLM 采样——这是 GRPO 显存真正的吃大户。
加载代码——三个最容易踩的坑
下面这份是SFT 阶段的加载方式(GRPO 阶段要改load_in_4bit,下面坑①讲):
from unsloth import FastLanguageModel import torch max_seq_length = 2048 lora_rank = 32 model, tokenizer = FastLanguageModel.from_pretrained( model_name = "Qwen/Qwen3-8B", max_seq_length = max_seq_length, load_in_4bit = False, # 坑①:SFT 阶段 False / GRPO 阶段改 True fast_inference = True, # 坑② max_lora_rank = lora_rank, gpu_memory_utilization = 0.7, # 坑③ ) model = FastLanguageModel.get_peft_model( model, r = lora_rank, target_modules = [ "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj", ], lora_alpha = lora_rank * 2, use_gradient_checkpointing = "unsloth", random_state = 3407, )坑①load_in_4bit——SFT 和 GRPO 该填不一样的值:
- SFT 阶段可以
False——没有 vLLM 采样,FP16 权重 ~16G 在 80G 卡上还能挤得下 - GRPO 阶段必须改
True——vLLM 要吃掉 30-40G 显存采样 group 内 8 条回答,QLoRA 帮你把权重从 16G 砍到 5G,否则单卡必 OOM - 进 GRPO 前要重新
from_pretrained加载一次,别想直接复用 SFT 阶段的 model 对象
坑②fast_inference=True:
- 开vLLM 加速推理——GRPO 每个 step 要采样 8 条回答(group size G=8),不开 vLLM 你等到天荒地老
- Qwen3-8B + 当前 Unsloth 版本下,单卡训练必开
- 版本坑(容易翻车):Unsloth 新版本对部分新模型(如 Qwen3.5 系列)的 GRPO 实际上推荐
fast_inference=False走 Unsloth 原生 inference——因为 vLLM 对新模型支持有滞后。遇到 vLLM 报 "model not supported" 类错误时,第一反应是把fast_inference改False
坑③gpu_memory_utilization=0.7:
- vLLM 占显存的比例。太高训练 forward 会 OOM,太低采样慢
- 0.7 是 self-llm 教程的默认值,实测在 H100 80G 上稳定
LoRA 配置——r 不是越大越好
我跑 GRPO 试过r=8、r=32、r=64——
r=8:显存小,但 GSM8K 这种任务欠拟合r=32:甜点尺寸,GSM8K 已饱和r=64:显存翻倍,acc 没明显提升
target_modules必须全套投影矩阵都挂——q/k/v/o 注意力层 + gate/up/down 前馈层。只挂 q/v 是那种"能跑就行"的偷懒配置,reasoning 任务顶不住。
lora_alpha = lora_rank * 2是社区惯例——α 控制 LoRA 输出对原始权重的"扰动强度",2r 是个稳的经验值。
PART 03:reward 函数设计——GRPO 真正的技术含量
到这里,我们终于要碰到 GRPO 训练最核心的东西了——
reward 函数。
我上一篇讲理论的时候说过,GRPO 的算法不复杂,真正难的是 reward 函数。今天给你看 self-llm 教程的四件套——这是 2025 下半年到 2026 中文社区事实标准的写法。
4 个 reward 函数协同
self-llm 给的reward_funcs列表是这样的:
trainer = GRPOTrainer( model = model, reward_funcs = [ # match_format_exactly, # 注释掉了,理由后面讲 match_format_approximately, # 宽松格式奖励 check_answer, # 答案正确性 + proximity reward check_numbers, # 纯数值兜底 ], ... )4 个函数各司其职,最终 reward 是它们的加权和。一个一个看。
函数①:match_format_exactly(被注释掉了)
严格匹配<end_working_out><SOLUTION>...</SOLUTION>格式——二值奖励,命中 1 / 不命中 0。
self-llm 教程把这个函数注释掉了。为什么?
因为它的功能被"宽松版"完全覆盖。你写两个会冗余,而且严格匹配在前 100 步模型还没学会格式时全是 0——没梯度,反而拖累训练。
这是一个工程经验:reward 函数不是越多越好。
函数②:match_format_approximately(宽松格式)
def match_format_approximately(completions, **kwargs): scores = [] for completion in completions: score = 0 response = completion[0]["content"] # 每个标签该出现 1 次:命中 +0.5,多次出现 -1.0 score += 0.5 if response.count(reasoning_end) == 1 else -1.0 score += 0.5 if response.count(solution_start) == 1 else -1.0 score += 0.5 if response.count(solution_end) == 1 else -1.0 scores.append(score) return scores为什么需要"宽松"格式奖励?
一开始模型还没学会格式,严格奖励全是 0 没梯度——宽松奖励给"接近正确"的部分分。模型哪怕只出现了<SOLUTION>开标签,也给 0.5 分鼓励一下。
这是 reward shaping 的核心思想:给"对的"信号,也给"接近对的"信号。
函数③:check_answer(本篇最核心)
def check_answer(prompts, completions, answer, **kwargs): responses = [c[0]["content"] for c in completions] extracted = [ guess.group(1) if (guess := match_format.search(r)) else None for r in responses ] scores = [] for guess, true in zip(extracted, answer): if guess is None: scores.append(-2.0) # 格式错,扣分但别太狠 continue if guess == true: scores.append(5.0) # 完美命中 elif guess.strip() == true.strip(): scores.append(3.5) # 空格差异 else: # proximity reward——这是 R1 路线的精髓 try: ratio = float(guess) / float(true) if 0.9 <= ratio <= 1.1: scores.append(2.0) elif 0.8 <= ratio <= 1.2: scores.append(1.5) else: scores.append(-2.5) except: scores.append(-4.5) # 非数值,最狠的惩罚 return scores这是整篇 GRPO 训练里最该拆透的函数。
几个关键设计——
完全正确 +5.0 / 去空格正确 +3.5:这是基础奖励。
proximity reward(数值近似奖励):这是 R1 路线最精髓的设计——模型答案接近正确答案也给部分分:
- 答案在 ±10% 内:+2.0
- 答案在 ±20% 内:+1.5
- 答案差距大:-2.5
为什么 proximity 这么重要?
GRPO 的 group baseline 机制下——如果 group 内 8 条回答全错(hard 题模型完全不会做),reward 全是 -4.5 或 -2.5,advantage 全是 0,梯度直接消失,模型什么都学不到。
加了 proximity,group 内的 8 条错误回答开始有区分度——"差 5%"的拿 +2.0、"差 30%"的拿 -2.5、"完全跑偏"的拿 -4.5。哪怕全错,模型也能从"哪个错得更接近"里学到东西,梯度信号不至于塌缩。
这是 reasoning 任务用 proximity reward 的真正价值——它不是为了"鼓励差一点对",是为了"在 group 全错时保留梯度信号"。
格式错 -2.0 / 非数值 -4.5:负 reward 不可怕,可怕的是全 0。负 reward 至少给模型一个"别这么干"的信号。
函数④:check_numbers(数值兜底)
check_answer的孪生兄弟,专门处理<SOLUTION>标签内纯数字的情况,还兼容了"1,234"这种带逗号的数字。
guess = float(guess.strip().replace(",", ""))这种兜底看着不起眼,但 GSM8K 里大量答案是大数字——没有replace(",", ""),"1,234" 会直接抛异常进入except分支被打 -4.5 分,模型委屈死。
reward 函数的心法
写完这 4 个函数,你回头看——
reward 函数不是越复杂越好,也不是越严格越好。
self-llm 把严格匹配注释掉、宽松匹配留 3 个标签各 0.5 分、proximity reward 给"接近"的部分分——所有设计都在围绕一件事:让梯度信号密集起来。
GRPO 靠 group baseline 算 advantage。组内 reward 全相等时 advantage=0,梯度直接消失——这是 GRPO 最阴险的崩盘点。
你的 reward 函数必须保证——同一道题的 8 条采样回答里,reward 要有区分度。
PART 04:GRPOTrainer 配置——一个 step 里到底跑什么
reward 函数准备好,进 GRPOTrainer。
训练参数——每个超参为啥这么设
from trl import GRPOConfig, GRPOTrainer training_args = GRPOConfig( output_dir = "outputs", num_generations = 8, # group size G max_prompt_length = 512, max_completion_length = 1024, per_device_train_batch_size = 1, gradient_accumulation_steps = 1, learning_rate = 5e-6, # 比 SFT 小两个数量级 temperature = 0.9, # 采样多样性来源 beta = 0.04, # KL 系数,default loss_type = "dr_grpo", # DAPO/R1 路线推荐的 loss 变体 logging_steps = 5, optim = "adamw_8bit", weight_decay = 0.01, lr_scheduler_type = "linear", report_to = "swanlab", ) trainer = GRPOTrainer( model = model, processing_class = tokenizer, reward_funcs = [ match_format_approximately, check_answer, check_numbers, ], args = training_args, train_dataset = dataset, ) trainer.train()每个超参的"为什么"——
num_generations=8(group size G):
- 上一篇讲过,G 太小 baseline 不稳,G 太大显存爆
- G=8 是 DeepSeek 论文的默认值,单卡 H100 上稳跑
- 调高 G=16 的话,每个 prompt 要采样 16 条回答,vLLM 显存吃紧
max_prompt_length=512 / max_completion_length=1024:
- reasoning CoT 普遍 800+ token,
max_completion_length别压到 512 以下——硬截断会直接砍掉模型的<SOLUTION>标签,check_answer提取不到答案打 -2.0,形成"越长越被截断越被扣分"的恶性循环 - prompt 通常 200 token 内,512 留富余
per_device_train_batch_size=1:
- GRPO 显存敏感,单卡极限就是 1
- 别想着开 4——一个 step 直接 OOM
learning_rate=5e-6:
- 比 SFT 小两个数量级
- reasoning 的 reward 噪声大,lr 一大 reward 曲线直接发散
- 调到
1e-5试试可以,再大就崩
beta=0.04(KL 系数):
- TRL 默认值,控制 policy 漂离 reference 多远
- KL 一直稳定 < 0.1,可以调低到 0.01 让模型更激进探索
- KL 爆炸了,调高到 0.1 拉紧
loss_type="dr_grpo":
- DAPO/R1 路线推荐的 loss 变体(Dr. GRPO),去掉了 GRPO 原版一个偏差项
- 默认的
"grpo"也能跑,但dr_grpo在数学推理上效果略好
temperature=0.9:
- 太低(< 0.7)group 内 8 条回答全一样,reward 方差为 0,崩盘
- 太高(> 1.2)模型胡言乱语,格式都保不住
- 0.9 是 self-llm 教程的默认,实测稳
一个 step 究竟跑了什么
代码贴完了。给你看一个 step 内部干了啥——
- 采样 prompt:从数据集取 1 个数学题
- vLLM 并行采样 8 条回答:
temperature=0.9保证多样性,这步是显存大头 - reward 函数并行算分:每条回答得到一个总分(你前面写的 3 个函数加权和)
- 算 group advantage:
A_i = (reward_i - mean) / std - 一次前向算两个 logprob:LoRA 路线下的精髓——enable adapter 算 policy logprob,disable adapter 算 reference logprob,前向只用跑一次
- 算 KL 损失:
beta * KL(policy || reference),beta默认 0.04 - 反向传播 + 优化器 step:clipped objective + KL 一起进 loss,更新 LoRA 权重
注意第 5 步——LoRA 路线下不需要你单独加载 reference model。GRPOTrainer内部 disable adapter 就拿到 reference logprob,这是 LoRA 路线相对全量微调的隐藏优势(全量微调要额外存一份 reference 权重)。
这也是为什么前面强调gpu_memory_utilization=0.7——vLLM 采样和训练 forward 共享 GPU,留少了 OOM,留多了训练慢。
PART 05:训练循环监控——三大崩盘点怎么救
跑起来只是开始。真正决定你能不能跑出来的是看监控。
我跑 GRPO 踩过的三大崩盘点——一个一个给你拆。
崩盘点①:reward 死活不涨(贴在某个固定值)
症状:reward/mean从第 1 步到第 50 步,纹丝不动地贴在某个固定值上(0 附近、-2.0 附近、或者 -4.5 附近都见过)。
就是我开篇讲的那个故事。
大概率原因:
- chat template 没用
apply_chat_template,模型没按格式输出 - SFT 阶段没跑或没跑好,base 模型根本不会按格式说话
- reward 函数的正则和模型实际输出格式对不上
该动哪个旋钮:
第一步,打印前 5 条采样回答肉眼看——
for i, r in enumerate(responses[:5]): print(f"=== Response {i} ===") print(r)如果看到的是自然语言大白话、没有<SOLUTION>标签——回去修 SFT 或修 chat template。
崩盘点②:奖励信号塌缩
症状:reward/std为 0,reward/mean要么是 5 要么是 -4.5——group 内 8 条回答全对或全错。
为啥是崩盘:group baseline 算 advantage 时,组内 reward 全相等 → advantage=0 → 梯度为 0 → 模型什么都学不到。
大概率原因:
- 数据集太简单(模型已经会做,8 条回答全对)
- 数据集太难(模型一个都做不出,8 条回答全错)
temperature太低,采样出来的回答都长得一样
该动哪个旋钮:
- 换数据集难度分布——DAPO-Math-17k 已经按难度分层,直接用就行
temperature调高到 1.0-1.2——让 group 内有多样性
崩盘点③:KL 散度爆炸
症状:kl_loss指数级上升,模型输出开始乱码、出现非英文符号、答非所问。
大概率原因:
learning_rate太大——这是 90% 的情况beta(KL 系数)太小——GRPOConfig默认beta=0.04,太松会导致 policy 漂离 reference 太远- 极少数情况:LoRA adapter 的 target_modules 不全(漏挂了
gate/up/down_proj),导致 reference 和 policy 的 logprob 计算路径不一致
该动哪个旋钮:
- lr 减半,从
5e-6降到2e-6 - 把
beta从0.04调到0.1,拉紧 KL 这根绳 - 不要去手动加载/冻结 reference model——
GRPOTrainer内部用 LoRA 的 disable adapter 技巧算 reference logprob(前向一次,disable adapter 拿到 reference、enable adapter 拿到 policy),你手动加 reference_model 反而会和 trainer 打架报错
一个常被忽略的信号:completion_length
监控 4 条曲线里,最容易被忽略的是completion_length。
健康的 GRPO 训练,completion_length应该缓慢上升——模型学会写更长更细的 CoT,是 reasoning 训练有效的关键信号。
如果completion_length下降——模型在偷懒,写越来越短的回答来骗 reward。
这时候要检查 reward 函数——是不是有某个函数的奖励偏好短回答。比如你的 format reward 写成了"越短奖励越高",模型就会钻空子。
崩盘点对照表(建议收藏)
| 症状 | 大概率原因 | 该动哪个旋钮 |
|---|---|---|
| reward mean 死活不涨 | chat template 没对 / 特殊 token 没加进 tokenizer / SFT 没跑 | 打印采样肉眼看,回头修 SFT 或 tokenizer |
| reward std 长期 0 | group 内全对或全错 | 换数据难度 / 调高 temperature |
| KL 指数级上升 | lr 太大 / beta 太松 | lr 减半 / beta 从 0.04 调到 0.1 |
| completion_length 下降 | reward 函数偏好短回答 | 检查 reward 设计,加长度惩罚 |
| completion_length 持续顶到 max | max_completion_length太小,CoT 被截断 | 调大到 1536/2048;被截断的回答<SOLUTION>出不来,check_answer全打 -2.0,会形成"模型越写越长 → 越被截断 → reward 越负"的恶性循环 |
| loss 突然变 NaN | reward 数值过大 / lr 过大 | 加 reward clipping / lr 减一个数量级 |
| 显存 OOM | group 太大 / context 过长 | 降 num_generations 或 max_completion_length |
心法——
GRPO 调参的本质是同时管三个变量:数据难度分布 / reward 函数权重 / 采样温度。
但比 PPO 还是简单一截。因为这三个变量的因果关系清晰得多——不像 PPO 那样 lr / β / KL 互相纠缠。
PART 06:推理验证——把训好的 LoRA 跑一道真数学题
训完了,得验证。
保存 LoRA 权重
model.save_lora("grpo_saved_lora")LoRA 权重只有几十 MB——比 SFT 全量权重小两个数量级,可以随便拷。
加载 LoRA 推理
from vllm import SamplingParams # 切到推理模式——FastLanguageModel 底层把 LoRA adapter 挂到 vLLM 上 FastLanguageModel.for_inference(model) text = "What is the sqrt of 101?" messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": text}, ] sampling_params = SamplingParams( temperature = 0.8, top_p = 0.95, max_tokens = 2048, ) output = model.fast_generate( [tokenizer.apply_chat_template(messages, tokenize = False)], sampling_params = sampling_params, )[0].outputs[0].text注意max_tokens=2048——reasoning 模型推理时 CoT 经常写到 1500+ token,设小了<SOLUTION>标签出不来,你会以为模型坏了。
真实对比:SFT 后 vs GRPO 后
我跑过同一道 GSM8K 题,对比如下——
SFT 后的输出(短):
<start_working_out> 这道题要求小明有多少个苹果。 他原来有 5 个,妈妈又给了 3 个。 5 + 3 = 8 <end_working_out> <SOLUTION> 8 </SOLUTION>GRPO 后的输出(长):
<start_working_out> 这道题要求小明有多少个苹果。 他原来有 5 个,妈妈又给了 3 个。 5 + 3 = 8 等等,让我检查一下——题目里"妈妈又给了 3 个"是不是有别的意思? 重新读题:"妈妈又给了 3 个红苹果和 2 个绿苹果"。 哦,我漏了。3 + 2 = 5。 所以总共是 5 + 5 = 10。 让我再算一遍。 原有 5 + 红色 3 + 绿色 2 = 10。 <end_working_out> <SOLUTION> 10 </SOLUTION>GRPO 之后,模型的 CoT明显变长变细,会自我检查——"等等,让我重新算一遍"这种话是 reasoning 模型的典型特征。
别只看最终答案对不对
GSM8K accuracy 提升可能只有 5-10%——但你不能因此说 GRPO 没用。
reasoning 训练的 ROI 要看"思考过程"——CoT 长度上升、出现自我检查、能处理多步推理——这才是 reasoning 模型的价值。
如果你只看 accuracy 卡死,建议测一下AIME / MATH 这种高难度数据集,提升幅度会大得多。
导出 GGUF(可选)
model.save_pretrained_gguf("grpo_gguf", quantization_method = "q4_k_m")可以丢到 llama.cpp / Ollama 跑——这是我下一篇要讲的事,先挖坑。
结尾:GRPO 实战,门槛在工程不在算法
到这里,从 SFT 冷启动到 4 个 reward 函数,再到 GRPOTrainer 配置、监控崩盘点、推理验证——全链路跑通了。
我猜你已经发现一件事——
GRPO 真正难的地方,没有一处是算法。
group baseline 的公式你不用手写——TRLGRPOTrainer帮你算了。clipped objective 你不用手写——还是 TRL 帮你算了。
你真正花时间的地方是——
- chat template 该怎么拼
- reward 函数的正则能不能匹配上模型实际输出
- group 内 8 条采样有多样性还是全一样
- KL 在第 200 步突然爆炸怎么办
- vLLM 显存留 0.7 还是 0.6 才不 OOM
全是工程脏活。
这就是为什么我开篇那句话——"GRPO 难不在算法,难在 reward 函数和你模型实际输出能不能对上电波"。
简历视角——三层答案
面试官问你"跑没跑过 GRPO",你能答出三层——
- 跑过,能讲清楚 SFT 冷启动为什么必要——base 模型不会按格式输出,reward 函数提取不到答案
- reward 函数怎么设计——4 类 reward + proximity 数值近似让梯度信号密集
- 崩盘点怎么救——reward 不涨 / 信号塌缩 / KL 爆炸三大症状的诊断逻辑
这三层答下来,面试官知道你不是"看了篇博客就来面"的那种人——你是真在凌晨三点半对着 reward 不涨发过呆的人。
一点观察
国内中文社区 GRPO 实战教程现在主要靠self-llm / Unsloth / SwanLab三个项目撑着。
self-llm 是 datawhale 出的中文开源大模型食用指南,Unsloth 是 Daniel & Mike Han 维护的显存优化训练框架,SwanLab 是国产的训练追踪/可视化平台(对标 WandB,国产 LLM 圈用得最广)——这三个组合在 2025 下半年到 2026 基本是中文社区 GRPO 入门的事实标准栈。
三个月内能把这套跑通,简历上"GRPO 实战经验"这一栏就有底气了。
下一篇预告——
reasoning 训练这块到今天告一段落。下一步两个方向我都很想写——
A:多模态(CLIP / VLM / 多模态 RAG)B:评估(LM-Eval / 魔搭榜 / 自己搭一套评估 pipeline)
评论区投票吧,哪个票多先写哪个。
互动时间——
你跑 GRPO 卡在哪?是 reward 死活不涨,还是 KL 突然爆炸,还是 reward 函数写得想砸键盘?
评论区聊聊,下一篇我会挑几个典型问题在开头回复。
最后送你一句——
RL 训练的真相是:10% 的时间在想算法,90% 的时间在调 reward 函数。论文写得很优雅,工程现场全是脏活。
而面试官要的,恰恰是那个在脏活里爬出来的人。
— END —
苦猿 · 帮普通人把 AI 学进简历
