verl 强化学习奖励函数实战指南:从 RewardManager 机制到自定义打分函数实现
【免费下载链接】verlverl/HybridFlow: A Flexible and Efficient RL Post-Training Framework项目地址: https://gitcode.com/GitHub_Trending/ve/verl
本文聚焦 verl(HybridFlow)框架中数据集奖励函数(Reward Function)的设计与实现:从RewardManager的执行机制、DataProto输入约定,到 GSM8k/MATH 等预实现打分函数的内部原理,再到通过custom_reward_function配置自定义奖励函数的完整实操。读完本文,你将能够为任意数据集编写、注册并接入自己的奖励函数,让 PPO/GRPO 等后训练流程正确计算每条响应的 reward。
一、奖励函数在 verl 后训练流程中的定位
verl 是一个面向大模型强化学习后训练(RL Post-Training)的框架。在 PPO 等在线策略训练中,模型生成响应后,训练循环必须为每条响应计算一个数值奖励(reward)用于优势估计与策略更新。verl 的做法是:每个数据集要么配套实现一个奖励函数,要么接入一个奖励模型(Reward Model)来打分。
仓库中的奖励函数实现集中在verl/utils/reward_score目录,官方当前已预实现:
- GSM8k:基于字符串匹配的规则打分(见 verl/utils/reward_score/gsm8k.py);
- MATH:解析
\boxed{}答案并做数学表达式规范化等价比较(见 verl/utils/reward_score/math_reward.py); - 此外还内置了
math_dapo、prime_math(Numina 系列)、geo3k、search_r1_like_qa_em、prime_code(代码类)与sandbox_fusion(沙箱执行)等更多打分实现,统一由default_compute_score按data_source分发(见 verl/utils/reward_score/init.py)。
而对于 RLHF 类数据集(如full_hh_rlhf),verl 采用Reward Model打分;代码生成类数据集(如 APPS)则通过SandBox 沙箱执行验证正确性。
二、RewardManager:奖励计算的统一入口
在 PPO 后训练脚本入口 verl/trainer/main_ppo.py 中,verl 实现了RewardManager来调用预实现的奖励函数为每条响应计算分数。其核心工作流为:
- 接收一个
DataProto作为输入; - 取出其中的响应 token 与 ground truth;
- 对响应 token 做 detokenize,得到响应字符串;
- 将响应字符串与 ground truth 字符串交给
compute_score_fn计算每条响应的分数。
2.1 DataProto 输入约定
RewardManager的__call__接收的DataProto中需要包含以下字段:
| 字段 | 说明 |
|---|---|
input_ids、attention_mask | 经过 chat_template 处理后的输入,包含 prompt 与 response 两部分 |
responses | 模型生成的响应 token |
ground_truth | 当前 prompt 的标准答案字符串,存放在DataProto的non_tensor_batch中,需在数据预处理阶段写入 parquet 文件 |
data_source | 当前 prompt 所属数据集名称,同样存放在non_tensor_batch中,用于分发到对应的打分函数 |
以仓库内置的NaiveRewardManager(见 verl/experimental/reward_loop/reward_manager/naive.py)为例,其run_single方法展示了完整的数据读取逻辑:
data_source = data_item.non_tensor_batch["data_source"] ground_truth = data_item.non_tensor_batch["reward_model"]["ground_truth"] extra_info = data_item.non_tensor_batch.get("extra_info", {}) response_str = await self.loop.run_in_executor( None, lambda: self.tokenizer.decode(valid_response_ids, skip_special_tokens=True) ) result = await self.loop.run_in_executor( None, lambda: self.compute_score( data_source=data_source, solution_str=response_str, ground_truth=ground_truth, extra_info=extra_info, ), )几点实现细节值得注意:
- 对多序列输出,
NaiveRewardManager只取最后一个序列计算奖励(data = data[-1:]),并通过attention_mask截取有效响应长度后再解码; ground_truth存放在non_tensor_batch["reward_model"]["ground_truth"]中,extra_info还可携带num_turns、rollout_reward_scores、工具调用附加字段等信息传入奖励函数;- 打分函数返回值既可以是纯数字,也可以是包含
score键的字典(此时其余键会作为reward_extra_info透传,用于记录acc等额外指标)。
三、预实现奖励函数解析
3.1 GSM8k:字符串匹配 + 格式约束
GSM8k 的打分逻辑(见 verl/utils/reward_score/gsm8k.py)采用"强格式约束"设计:强制模型在四个####之后输出最终答案,再通过字符串匹配与 ground truth 比对。评分规则:
- 答案完全正确:得 1 分;
- 格式正确(能提取出答案但与 ground truth 不符):得 0.1 分(
format_score=0.0的默认值在不同调用场景下可配置); - 格式不正确(无法提取答案):得 0 分。
核心的答案提取函数extract_solution(gsm8k.pyL20-L49)支持两种模式:
- strict(默认):用正则
#### (\-?[0-9\.\,]+)匹配####后的数字,取最后一个作为最终答案,同时剔除了,与$符号——这种模式同时检验了模型的输出格式; - flexible:直接取字符串中最后一个合法的数字,不要求
####格式。
源码中还包含一个实用优化:当solution_str超过 300 个字符时,只对末尾 300 字符做正则匹配(_SOLUTION_CLIP_CHARS = 300),因为数学题的最终答案通常位于末尾,可以显著加速长字符串的正则扫描。
3.2 MATH:boxed 答案解析 + 数学表达式规范化
MATH 的打分实现(见 verl/utils/reward_score/math_reward.py)沿用了 lm-evaluation-harness 中hendrycks_math的做法,核心流程为:
last_boxed_only_string:定位响应中最后一个\boxed{...}(兼容\fbox与\boxed写法),提取括号内内容;remove_boxed:剥掉\boxed外壳拿到答案字符串;is_equiv:通过strip_string对两侧字符串做大量规范化后比较是否等价。
strip_string的处理极其细致,包括但不限于:去除换行、\left/\right、美元符号、百分号与单位;把\tfrac/\dfrac统一为\frac;把\sqrt3修正为\sqrt{3};把a/b改写为\frac{a}{b};把0.5归一为\frac{1}{2}等等。正是这一套规范化,使得\frac{1}{2}与1/2、0.5等不同写法都能被正确判定为等价答案。
3.3 更多内置打分器与按数据集分发
所有内置打分器统一由 verl/utils/reward_score/init.py 中的default_compute_score按data_source字符串分发(L44-L107)。当前支持的主要数据集别名包括:
openai/gsm8k→gsm8k.compute_score;lighteval/MATH、DigitalLearningGmbH/MATH-lighteval、HuggingFaceH4/MATH-500→math_reward.compute_score(源码注释提示可手动pip install math-verify后切换为math_verify.compute_score以提升精度);math_dapo、math、math_dapo_reasoning及aime前缀 →math_dapo.compute_score;numina_*系列 →prime_math.compute_score;codecontests、apps、codeforces、taco→ 优先走sandbox_fusion.compute_score(需要配置沙箱 URL),否则回退prime_code.compute_score;hiyouga/geometry3k→geo3k.compute_score,其实现将格式奖励与答案正确性奖励加权组合(见 verl/utils/reward_score/geo3k.py);searchR1_*系列 →search_r1_like_qa_em.compute_score。
对于未覆盖的data_source,会直接抛出NotImplementedError——这也提示我们:遇到新数据集时,正确姿势是编写自定义奖励函数(见下一节)。
四、自定义奖励函数:完整实操
4.1 通过配置指定自定义函数
verl 支持在独立的 Python 文件中实现自定义奖励函数,并通过配置项指定,无需修改框架源码。相关配置位于 verl/trainer/config/reward/reward.yaml:
# configs for the reward computation _target_: verl.workers.config.RewardConfig # 启动的 reward manager 数量,用于并行化奖励计算 num_workers: 8 # 自定义奖励函数定义 custom_reward_function: # 存放自定义奖励函数的文件路径 path: null # 文件中奖励函数的名称,默认是 'compute_score' name: compute_score其中RewardConfig的数据结构定义可参考 verl/workers/config/reward.py(num_workers默认 8,另有reward_manager、reward_model、sandbox_fusion三个子配置块)。完整配置项的逐条说明可进一步查阅配置文档(原文档指引参见config-explain-page,仓库内对应示例说明见 docs/examples/config.rst)。
4.2 函数签名约定
自定义奖励函数的参数签名固定为:
def my_reward_fn(data_source, solution_str, ground_truth, extra_info=None): return len(solution_str) / 100参数含义:
| 参数 | 说明 |
|---|---|
data_source | 数据集名称,可用于在同一个函数内区分不同数据集的打分逻辑 |
solution_str | 模型生成的响应文本(detokenize 之后) |
ground_truth | 标准答案字符串 |
extra_info | 附加信息(可选),可包含num_turns、工具调用字段等 |
返回值应为数值奖励;如果返回字典,则需包含score键作为奖励值,其余键会作为reward_extra_info记录。
4.3 场景一:单函数测试
如果只测试一个自定义奖励函数,最简单的做法是:把函数命名为compute_score,配置中只填写custom_reward_function.path,name留空(默认值即为compute_score)。
4.4 场景二:多函数对比测试
当需要对比多个奖励函数时,可在同一个文件中实现多个函数,通过修改custom_reward_function.name切换,而无需改动path:
# my_reward.py def compute_score(data_source, solution_str, ground_truth, extra_info=None): return 1.0 if solution_str == ground_truth else 0.0 def length_based_score(data_source, solution_str, ground_truth, extra_info=None): return len(solution_str) / 100 def keyword_score(data_source, solution_str, ground_truth, extra_info=None): return 0.5 if "answer:" in solution_str.lower() else 0.0然后分别为每次试验设置:
custom_reward_function: path: /path/to/my_reward.py name: length_based_score # 每次试验只改这里这种方式特别适合在脚本中批量跑多组奖励函数对比试验。
4.5 源码层面的加载机制
自定义函数并非简单地import,而是通过动态加载机制完成(见 verl/trainer/ppo/reward.py 中的get_custom_reward_fn,L50-L86):
- 从
config.reward.custom_reward_function读取path与name,若未配置path则返回None(此时回退到预实现函数); - 通过
verl.utils.import_utils.load_extern_object(module_path, object_name)动态导入; - 配置中若提供了
reward_kwargs,会与调用时的 kwargs 合并(以配置为准)后partial包装; - 若自定义函数是
async协程函数,会被自动识别并走异步调用路径,便于接入远程/异步奖励源。
在load_reward_manager(同文件 L111-L157)中,若未配置自定义函数且配置了sandbox_fusion.url,则会自动用default_compute_score结合沙箱 URL、并发信号量(默认max_concurrent=64)与内存上限(默认1024MB)构造打分函数,用于代码类数据集的沙箱执行验证。
五、数据预处理与端到端接入
要确保奖励函数正常工作,数据预处理阶段必须把data_source与ground_truth写入non_tensor_batch。仓库提供了现成的数据预处理脚本作为参考,例如:
- examples/data_preprocess/gsm8k.py:构造 GSM8k 训练/测试 parquet,写入 prompt 与标准答案;
- examples/data_preprocess/math_dataset.py:MATH 数据集同理;
- examples/data_preprocess/full_hh_rlhf.py:RLHF 数据集示例,可配合 Reward Model 打分。
接入流程可归纳为:数据预处理写入ground_truth/data_source→ 训练配置指定custom_reward_function.path(或使用预实现函数)→ PPO 入口的RewardManager自动完成 detokenize 与打分。跑通后可参考 examples/grpo_trainer/run_qwen3_8b_fsdp.sh 等启动脚本,结合实际数据集验证奖励是否符合预期。
六、小结
verl 的奖励函数体系设计清晰、扩展成本低:预实现函数覆盖 GSM8k、MATH、代码、几何等多类数据集,RewardManager统一调度,custom_reward_function提供零侵入的自定义入口。对开发者而言,接入一个新数据集只需三步:预处理出ground_truth与data_source、按(data_source, solution_str, ground_truth, extra_info)签名编写打分函数、在配置中指定path与name。若需多函数对比试验,仅需在脚本中切换name即可,无需改动任何框架代码。
【免费下载链接】verlverl/HybridFlow: A Flexible and Efficient RL Post-Training Framework项目地址: https://gitcode.com/GitHub_Trending/ve/verl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考