Skip to content

如何写奖励函数

奖励函数是 RLVR 的第一生产力。一个清晰、稳定、可调试的 reward function,常常比复杂算法 trick 更能决定训练能不能动起来。

这页原来哪里不够适合新手

原来的版本给了接口轮廓,但还缺:

  • 数据字段从 parquet 到 DataProto.non_tensor_batch 的对应关系。
  • naivedapobatch reward manager 对函数签名和返回值的差异。
  • dict 返回值如何进入训练指标和验证指标。
  • 当前源码和部分旧 docs 的不一致点。

数据字段:reward function 能拿到什么

examples/data_preprocess/gsm8k.py 为例,一条 parquet 样本里有:

python
{
    "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:

text
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_sourcescorer
openai/gsm8kverl/utils/reward_score/gsm8k.py
DigitalLearningGmbH/MATH-lightevalverl/utils/reward_score/math_reward.py
math_dapomathaime*verl/utils/reward_score/math_dapo.py
code 类数据prime_code 或 sandbox scorer

如果你的 parquet 里 data_source 写错了,默认 scorer 会直接 NotImplementedError,或者更糟:走到不匹配的 scorer,reward 全错。

单样本 reward 函数签名

naivedapo reward manager 都按单样本调用:

python
def compute_score(data_source, solution_str, ground_truth, extra_info=None, **kwargs):
    ...

一个推荐的最小返回:

python
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@Nbest@Nmaj@N 等指标。

批量 reward 函数签名

verl/workers/reward_manager/batch.py 需要批量接口:

python
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 为例:

  1. 用 tokenizer 解码 prompt 和 response。
  2. non_tensor_batch["reward_model"]["ground_truth"] 取标准答案。
  3. non_tensor_batch["data_source"] 取数据源。
  4. 调用 compute_score(data_source, solution_str, ground_truth, extra_info)
  5. 如果返回 dict,读取 score["score"] 作为 reward,并保存所有额外键。
  6. 把 reward 写到 response 最后一个有效 token:
text
reward_tensor[i, valid_response_length - 1] = reward

dapo.py 在此基础上多了 overlong penalty:如果 response 长度超过 max_response_length - overlong_buffer.len,按线性规则扣分,并可记录 overlong_rewardoverlong

自定义 reward 配置

本地 docs 中写法是:

bash
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.pathname 动态 import 函数,并把 reward_kwargs 合并到调用参数里。

如果你需要给 reward function 传额外参数,优先用:

bash
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,并返回 accpred。如果你把 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 条:

text
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.rst
  • docs/preparation/prepare_data.rst
  • docs/examples/config.rst
  • examples/data_preprocess/gsm8k.py
  • examples/data_preprocess/math_dataset.py
  • verl/utils/reward_score/__init__.py
  • verl/utils/reward_score/gsm8k.py
  • verl/utils/reward_score/math_reward.py
  • verl/utils/reward_score/math_dapo.py
  • verl/utils/reward_score/math_verify.py
  • verl/workers/reward_manager/naive.py
  • verl/workers/reward_manager/dapo.py
  • verl/workers/reward_manager/batch.py
  • verl/trainer/ppo/reward.py

面向源码阅读的 verl 学习文档。