如何写奖励函数
奖励函数是 RLVR 的第一生产力。一个清晰、稳定、可调试的 reward function,常常比复杂算法 trick 更能决定训练能不能动起来。
这页原来哪里不够适合新手
原来的版本给了接口轮廓,但还缺:
- 数据字段从 parquet 到
DataProto.non_tensor_batch的对应关系。 naive、dapo、batchreward manager 对函数签名和返回值的差异。- dict 返回值如何进入训练指标和验证指标。
- 当前源码和部分旧 docs 的不一致点。
数据字段:reward function 能拿到什么
以 examples/data_preprocess/gsm8k.py 为例,一条 parquet 样本里有:
{
"data_source": "openai/gsm8k",
"prompt": [{"role": "user", "content": "..."}],
"reward_model": {"style": "rule", "ground_truth": "42"},
"extra_info": {"split": "train", "index": 0, "answer": "...", "question": "..."},
}训练时这些非 tensor 字段进入 DataProto.non_tensor_batch。reward manager 会解码 response,然后调用 scorer:
data_source -> 用来选择默认 scorer
solution_str -> 模型生成的 response 文本
ground_truth -> reward_model["ground_truth"]
extra_info -> 预处理脚本写入的辅助信息,另加 num_turns / rollout_reward_scores 等所以写 reward function 前,先检查数据预处理。ground_truth 的格式必须和 scorer 的解析逻辑一致。GSM8K 是裸数字,MATH 是从 \boxed{} 提取出的答案。
默认 scorer 如何分派
默认入口在 verl/utils/reward_score/__init__.py::default_compute_score():
data_source | scorer |
|---|---|
openai/gsm8k | verl/utils/reward_score/gsm8k.py |
DigitalLearningGmbH/MATH-lighteval | verl/utils/reward_score/math_reward.py |
math_dapo、math、aime* | verl/utils/reward_score/math_dapo.py |
| code 类数据 | prime_code 或 sandbox scorer |
如果你的 parquet 里 data_source 写错了,默认 scorer 会直接 NotImplementedError,或者更糟:走到不匹配的 scorer,reward 全错。
单样本 reward 函数签名
naive 和 dapo reward manager 都按单样本调用:
def compute_score(data_source, solution_str, ground_truth, extra_info=None, **kwargs):
...一个推荐的最小返回:
def compute_score(data_source, solution_str, ground_truth, extra_info=None, **kwargs):
pred = parse_answer(solution_str)
format_ok = pred is not None
acc = bool(format_ok and pred == ground_truth)
return {
"score": 1.0 if acc else 0.0,
"acc": acc,
"pred": pred or "[INVALID]",
"format_reward": 1.0 if format_ok else 0.0,
}返回可以是标量,也可以是 dict。dict 必须有 score,因为 reward manager 会用它写入 reward tensor。其他键会进入 reward_extra_info,随后写回 batch.non_tensor_batch,验证时可生成 val-core/{data_source}/acc/mean@N、best@N、maj@N 等指标。
批量 reward 函数签名
verl/workers/reward_manager/batch.py 需要批量接口:
def compute_score(
data_sources,
solution_strs,
ground_truths,
extra_infos,
**kwargs,
):
return [{"score": 1.0, "acc": True} for _ in solution_strs]它会一次解码一批 response,然后把每个结果写到对应样本最后一个有效 token。适合外部服务、批量 verifier 或较重的解析逻辑。
reward manager 做了什么
以 verl/workers/reward_manager/naive.py 为例:
- 用 tokenizer 解码 prompt 和 response。
- 从
non_tensor_batch["reward_model"]["ground_truth"]取标准答案。 - 从
non_tensor_batch["data_source"]取数据源。 - 调用
compute_score(data_source, solution_str, ground_truth, extra_info)。 - 如果返回 dict,读取
score["score"]作为 reward,并保存所有额外键。 - 把 reward 写到 response 最后一个有效 token:
reward_tensor[i, valid_response_length - 1] = rewarddapo.py 在此基础上多了 overlong penalty:如果 response 长度超过 max_response_length - overlong_buffer.len,按线性规则扣分,并可记录 overlong_reward、overlong。
自定义 reward 配置
本地 docs 中写法是:
reward.custom_reward_function.path=/path/to/my_reward.py \
reward.custom_reward_function.name=compute_score当前源码加载入口在 verl/trainer/ppo/reward.py::get_custom_reward_fn(),它会从配置里的 reward.custom_reward_function.path 和 name 动态 import 函数,并把 reward_kwargs 合并到调用参数里。
如果你需要给 reward function 传额外参数,优先用:
reward.custom_reward_function.reward_kwargs.some_threshold=0.5不要把超参硬编码到函数里。这样 wandb/console 日志和启动命令才可复现。
GSM8K 和 MATH 的具体坑
GSM8K。 verl/utils/reward_score/gsm8k.py 只在 response 最后 300 个字符里找最后一个 #### number。当前源码默认 format_score=0.0,所以格式正确但答案错误也是 0。部分旧文档写 0.1,以当前源码为准。
MATH。 math_reward.py 找最后一个 \boxed{} 并做字符串归一化;math_dapo.py 返回 dict,正确是 score=1.0,错误是 score=-1.0,并返回 acc 和 pred。如果你把 MATH prompt 写成“最后输出 ####”,默认 MATH scorer 就不适合。
Math-Verify。 math_verify.py 需要额外安装 math-verify,并用 subprocess + timeout 包住验证。适合更强的数学等价判断,但要关注超时率和进程池开销。
常见坑 checklist
data_source和 scorer 不匹配。ground_truth格式和 parser 不匹配。- response 没按要求输出
####或\boxed{},reward 长期全 0。 - parser 太宽,导致错误答案也拿分。
- parser 太严,导致正确答案因空格、逗号、单位、LaTeX 格式拿不到分。
- reward dict 没有
score键。 acc用字符串"true",验证聚合时不如 bool/float 稳。- 过长输出被截断,最后答案消失。
- reward function 打印太多,拖慢训练。
- 外部 reward 服务不稳定,导致 rollout 阶段耗时和 reward 分布一起抖。
调试最小动作
训练前先抽样打印 10 条:
data_source
prompt
response
ground_truth
parsed_pred
score
acc / format_reward训练中至少看:
critic/score/mean/max/min:原始 scorer 是否有信号。critic/rewards/mean/max/min:真正用于训练的 reward,可能已扣 KL。response_length/clip_ratio:答案是否被最大长度截断。- 验证指标里的
val-core/{data_source}/acc/mean@N:dict 返回了acc时最有用。
如果 reward 分布全 0 或全 1,先别调学习率。先看样例和 parser。
本节参考与延伸阅读
docs/preparation/reward_function.rstdocs/preparation/prepare_data.rstdocs/examples/config.rstexamples/data_preprocess/gsm8k.pyexamples/data_preprocess/math_dataset.pyverl/utils/reward_score/__init__.pyverl/utils/reward_score/gsm8k.pyverl/utils/reward_score/math_reward.pyverl/utils/reward_score/math_dapo.pyverl/utils/reward_score/math_verify.pyverl/workers/reward_manager/naive.pyverl/workers/reward_manager/dapo.pyverl/workers/reward_manager/batch.pyverl/trainer/ppo/reward.py