跳到主要内容

数据截至 (上游 commit 1416fa0cf215)

02 · GRPO 配方与可验证奖励函数

这一章讲什么: Open R1 的 GRPO 训练怎么跑起来(脚本、vLLM 拓扑、关键旋钮),然后逐个拆 rewards.py 的奖励函数——这是全仓库算法密度最高的文件,也是「RLVR(可验证奖励强化学习)怎么做奖励工程」的最小完整教材。


1. 它要解决的小问题

GRPO 的想法一句话:一道题让模型采 N 个答案,按组内相对好坏给优势,省掉 value model。但它有个前提——你得能自动判断「哪个答案好」。

这个「判断器」就是奖励函数。数学题可以符号判等、代码题可以真跑测试,但每个领域都要解决自己的坑:

  • 数学:答案写法千奇百怪(0.5 vs \frac{1}{2}),怎么判等价?
  • 格式:怎么逼模型守 <think>…</think><answer>…</answer> 的规矩,又不被钻空子?
  • 长度:RL 训着训着推理越来越长,怎么罚?
  • 代码:执行生成的代码,安全和并发怎么管?

rewards.py 就是这一串问题的答案集。

2. GRPO 脚本:薄壳里有什么

src/open_r1/grpo.pymain(grpo.py:35)与 sft.py 几乎同构,差别只在三处:

  1. 按名字取奖励函数get_reward_funcs(script_args)(grpo.py:88)把 YAML 里的名单翻译成函数列表;
  2. prompt 拼装make_conversation(grpo.py:91-101)把数据集的一列包成 system+user 消息列表——GRPO 只需要 prompt,答案是采样出来的;
  3. 清掉 messages(grpo.py:105-107):带推理轨迹的数据集若直接拿来做 RL,旧 messages 列会干扰,必须删掉。

RL 循环本体(采样、组内优势、PPO clip)全部在 trl 的 GRPOTrainer 里,本仓库零实现。

2.1 关键旋钮

以数学演示配方 recipes/DeepSeek-R1-Distill-Qwen-1.5B/grpo/config_demo.yaml 为例:

参数作用
num_generations16(:36)每题采 16 个答案——GRPO 的「组」,组内比较算优势
max_completion_length2048(:34)演示配方压短推理长度换速度
learning_rate1.0e-06(:24)RL 学习率比 SFT 低一个数量级起步
temperature0.7(:56)采样温度;太低组内无差异,优势信号消失
reward_funcs / reward_weightsaccuracy+format+tag_count,各 1.0(:45-52)奖励组合,见 §3
use_vllmtrue(:16)采样走 vLLM;单机用 colocate,多节点用 server 模式

2.2 拓扑:vLLM 和训练怎么摆

两种摆法,README 都有给(README.md:218-234):

单机(colocate) 多节点(server,train.slurm)
8 GPU: 训练进程里嵌 vLLM 节点 1..N: accelerate 训练
一条命令即可 节点 N+1: trl vllm-serve
(最后一台节点专职采样)

多节点版由 slurm/train.slurm:128-136 自动完成:检测到配置里 use_vllm: true 就把节点列表最后一台切出去起 trl vllm-serve,并把 --vllm_server_host 透传给训练脚本。「最后一台给 vLLM」是写死在脚本里的约定。

2.3 坑:蒸馏模型的 chat template 会弄坏格式奖励

README 的 WARNING(README.md:227-228):DeepSeek 蒸馏模型自带的 chat template 有两个问题——会把 <think> 块的内容删掉、且给 assistant 回复预填 <think> 开头。后者让模型「天生满足格式奖励的前半段」,格式奖励直接失效。

修复方式是在配方里覆盖整个 chat template——config_demo.yaml:8-9 的注释写明了两条修改意图,然后第 9 行塞进一整条定制 Jinja 模板。这是「奖励函数生效的前提是数据呈现方式正确」的典型案例。

3. 奖励注册表:11 个函数

入口 get_reward_funcssrc/open_r1/rewards.py:646-706):一张 REWARD_FUNCS_REGISTRY 字典,YAML 里的 reward_funcs 名单按名取函数。TRL 会把它们逐个调用并按 reward_weights 加权求和。

注册名函数判定方式值域出处/思想
accuracyaccuracy_rewardmath-verify 符号判等0/1 或 None数学 RLVR 标配
formatformat_reward严格正则0/1R1 格式
tag_counttag_count_reward逐标签计数0~1(步进 0.25)改自 willccbb 的 gist
reasoning_stepsreasoning_steps_reward数「Step N:」等模式0~1启发式
lengthlen_reward批内长度相对比较-0.5~0.5Kimi 1.5 报告
cosineget_cosine_scaled_reward长度余弦缩放可配
repetition_penaltyget_repetition_penalty_rewardn-gram 重复率≤0demystify-long-cot
codecode_reward沙箱跑测试,通过率0~1 或 NonePrimeIntellect 数据格式
binary_codebinary_code_reward通过率阈值化0/1 或 None
ioi_code / cf_codeioi_code_reward / cf_code_rewardPiston/Morph 跑竞赛测试分数IOI/Codeforces 规则
code_formatget_code_format_reward正则+代码块标记0/1
soft_overlong_punishmentget_soft_overlong_punishmenttoken 数分段惩罚-1~0DAPO 论文 Eq.13

默认组合是 ["accuracy", "format", "tag_count"]src/open_r1/configs.py:234-239)。下面挑四类讲原理。

4. 数学奖励:符号判等,且「判不了就跳过」

4.1 思路

0.5\frac{1}{2}50\% 是同一个答案。字符串比较不可用,得上符号数学:把两边的 LaTeX 解析成表达式,问 sympy 是否等价。open-r1 用的是 math-verify 库(setup.py:60 钉死 0.5.2)。

4.2 真实实现

accuracy_reward(rewards.py:40-82)逐条做三件事:

  1. 先解析 gold 答案(parse(sol, extraction_mode="first_match"),:45-48);
  2. 再解析模型输出,抽取配置里 boxed_match_priority=0 保证优先认 \boxed{}(:51-69);
  3. verify(gold_parsed, answer_parsed) 转 float 得 0/1(:72)。

关键设计是 None 协议(rewards.py:70-79):gold 解析不出、或 verify 抛异常时返回 None 而不是 0。None 让 trl 把这条样本从这批奖励里剔除——脏数据不产生梯度,而不是错误地惩罚模型。这是 RL 数据卫生里很容易被忽视的一手。

4.3 配套:格式奖励的两个粒度

  • format_reward(rewards.py:85-90):一条严格正则 ^<think>\n.*?\n</think>\n<answer>\n.*?\n</answer>$,全对才得 1——硬门槛
  • tag_count_reward(rewards.py:93-112):四个标签各值 0.25 的软梯度——训练早期模型还不会守格式时,给一个「部分对也给分」的平滑信号,避免硬门槛下奖励恒为 0、学不到东西。

5. 长度奖励家族:三种罚「想得太多」的姿势

RL 训推理模型有个通病:推理长度膨胀。仓库收了三种对策,思想各不相同。

函数机制特点
len_reward(rewards.py:132-202)批内比较:正确答案里越短分越高(0.5 - (len-min)/(max-min),:193);错误答案只罚不奖(:195-198)Kimi 1.5 报告的思路;相对的——全批同长时退化为全 0(:187-189)
get_cosine_scaled_reward(rewards.py:205-282)余弦调度:对的越短越接近 1,错的越长罚越轻(:266-277)绝对的,带上下界参数;鼓励「短而对」,宽容「长而错」
get_soft_overlong_punishment(rewards.py:620-643)DAPO 式软惩罚:长度在「缓存区」内线性扣分,超长直接 -1(:635-640)只罚不奖,防止模型靠无限拉长碰运气

读这三个函数能直接看到 RL 奖励设计的一个核心张力:绝对标尺稳定但要想好边界,相对标尺自适应但依赖批内分布

6. 代码奖励:把测试通过率变成奖励

6.1 思路

代码题天然可验证:跑测试用例数通过率。难点在工程——在训练循环里执行模型生成的不可信代码,需要沙箱、超时、并发控制,还要扛住外部服务的速率限制。

6.2 结构

code_reward (rewards.py:511)
│ 从回答里抠出 markdown 代码块 → extract_code (rewards.py:476)
│ 把「代码 + 测试用例」包成一段评测脚本(evaluation_script_template, :529-567)

get_provider → E2BProvider / MorphProvider (code_providers.py:339)
│ asyncio 信号量限并发(默认每进程 2,configs.py:286-291)

云沙箱执行 → stdout 最后一行 = 通过率 → float

评测脚本本身是个「进程套进程」的结构(rewards.py:529-567):外层脚本在沙箱里跑,内层用 subprocess.run(["python3","-c",code]) 逐测试用例执行模型代码,5 秒超时,逐行比对 stdout。通过率(0~1 的小数)打印出来,由外层 float(execution.text) 读回。

6.3 工程细节

  • 双超时带 margin:E2B 沙箱超时 30s、请求超时 28s、asyncio 兜底 32s(src/open_r1/utils/code_providers.py:141-144)——注释说明这些值是用 gold 答案实测 256 条标定出来的。任何一层超时都返回 0.0 而不是炸掉训练(:154-161)。
  • router 模式:云上跑大批量执行会被限流,于是提供 e2b_router.py/morph_router.py 在 CPU 节点起路由服务,所有训练作业共享一个入口(README.md:318-342;E2BProviderRoutedSandbox 分支在 code_providers.py:88-105)。
  • binary_code:把通过率按 0.99 阈值二值化(rewards.py:499-506)——部分通过的答案在竞赛里等于错,二值化更贴近真实判分。
  • 竞赛级奖励ioi_code_reward(rewards.py:367)/cf_code_reward(rewards.py:420)走自架 Piston 集群或 Morph,test_batch_size 参数做「先跑一批测试,挂一个就提前停」的短路(configs.py:276-281),给错答案省沙箱配额。

7. 配方怎么选奖励:三个真实例子

奖励组合是配方的灵魂,仓库里三份 GRPO 配方刚好展示三种取舍:

配方reward_funcs(权重)意图
数学演示(recipes/DeepSeek-R1-Distill-Qwen-1.5B/grpo/config_demo.yaml:45-52accuracy + format + tag_count(各 1.0)答案对 + 守格式,双管齐下
Python 代码(recipes/Qwen2.5-1.5B-Instruct/grpo/config_demo_code.yaml:43-48code(1.0) + format(0.1)沙箱通过率为主,格式只留个轻约束——权重 0.1 表示「别为了格式牺牲代码正确性」
Codeforces(recipes/Qwen2.5-Coder-7B-Instruct/grpo/config_codeforces.yaml:61-66cf_code(1.0) + code_format(0.1)竞赛判分为主,且要求答案里带 markdown 代码块标记

Codeforces 配方还透露了 RL 调参的「现代默认」:beta: 0.0(:21,关掉 KL 惩罚)、loss_type: dr_grposcale_rewards: false(:22-23)——向 Dr.GRPO/DAPO 论文指出的「去长度偏置」方向靠拢。

8. 代码地图

主题文件路径符号名
GRPO 训练主线src/open_r1/grpo.pymainmake_conversation
奖励注册表src/open_r1/rewards.pyget_reward_funcsREWARD_FUNCS_REGISTRY
数学判等src/open_r1/rewards.pyaccuracy_reward
格式奖励src/open_r1/rewards.pyformat_rewardtag_count_rewardget_code_format_reward
长度家族src/open_r1/rewards.pylen_rewardget_cosine_scaled_rewardget_soft_overlong_punishment
重复惩罚src/open_r1/rewards.pyget_repetition_penalty_reward
代码执行奖励src/open_r1/rewards.pycode_rewardbinary_code_rewardextract_code
竞赛奖励src/open_r1/rewards.pyioi_code_rewardcf_code_reward
沙箱 providersrc/open_r1/utils/code_providers.pyCodeExecutionProviderE2BProviderMorphProviderget_provider
奖励相关配置项src/open_r1/configs.pyGRPOScriptArguments
vLLM/训练节点切分slurm/train.slurm—(bash,128-136 行)