执行摘要
- 一句话:支持多输出轨迹异步奖励评分
- 推荐动作:此 PR 值得精读,尤其是设计权衡部分(修改 reward manager 接口 vs 调用 score_batch)。它展示了如何在保持向后兼容的同时引入灵活的多输出支持,对理解异步奖励架构有帮助。
功能与动机
PR body 原话:When using the TransferQueue trainer with multi-turn agent loops, each prompt may produce a list of AgentLoopOutputs. Previously _compute_score was called only on the final output, constructing a batch of size 1. This change passes all outputs to _compute_score so reward managers can be extended to score intermediate turns, while the default behaviour (scoring only the last output via data[-1:]) is preserved for all existing reward managers. Also fixes a pre-existing bug in AgentLoopWorkerTQ._agent_loop_postprocess where _compute_teacher_logprobs was called with the raw output parameter (which may be a list[AgentLoopOutput]) instead of the resolved final_output.
实现拆解
- AgentLoopWorker._compute_score 接口重构:将签名从接收单个 output 和多个分离 tensor 改为接收
list[AgentLoopOutput] 和 kwargs。内部遍历所有输出,重新计算每个输出的 prompts、responses、input_ids、attention_mask、position_ids,并通过 pad_sequence 打包成批量 TensorDict,最后只将奖励赋值给 outputs[-1]。
- 同步训练器 _agent_loop_postprocess 简化:在
verl/trainer/main_ppo_sync.py 中,移除手动构造最终输出 tensors 的逻辑,直接调用 self._compute_score(outputs, kwargs=kwargs),并将 _compute_teacher_logprobs 的调用参数从原始 output 修正为 final_output。
- RewardLoopWorker.compute_score 放宽断言:移除
assert len(data)==1,允许传入多输出序列,并在使用蒸馏奖励模型时通过 data[-1:] 仅取最后一项。
- 所有内置奖励管理器 (naive, dapo, gdpo, limited, remote) 的 run_single 方法:将开头的
assert len(data)==1 替换为 data = data[-1:],保证默认行为只评分最后输出,同时为自定义管理器留下扩展空间。
- 测试与文档同步更新:修改
test_rate_limited_reward_manager_on_cpu.py 以适配新接口,并更新 docs/advance/reward_loop.rst 文档反映新的参数格式。
关键文件:
verl/experimental/agent_loop/agent_loop.py(模块 代理循环;类别 source;类型 core-logic;符号 _compute_score): 核心变更点:重构 _compute_score 以支持多输出批量评分,是本 PR 的主要实现。
verl/trainer/main_ppo_sync.py(模块 训练器;类别 source;类型 core-logic;符号 _agent_loop_postprocess): 同步训练器的 _agent_loop_postprocess 简化,移除手动构造 tensors 并修复 teacher logprobs 参数。
verl/experimental/reward_loop/reward_loop.py(模块 奖励循环;类别 source;类型 core-logic;符号 compute_score): RewardLoopWorker.compute_score 移除单数据断言,并确保蒸馏奖励模型只取最后一个输出。
verl/experimental/reward_loop/reward_manager/dapo.py(模块 奖励管理器;类别 source;类型 core-logic;符号 run_single): DAPO 奖励管理器移除 assert,改为 data[-1:] 以保证默认只评分最后输出。
verl/experimental/reward_loop/reward_manager/gdpo.py(模块 奖励管理器;类别 source;类型 core-logic;符号 run_single): GDPO 奖励管理器移除 assert,改为 data[-1:]。
verl/experimental/reward_loop/reward_manager/limited.py(模块 奖励管理器;类别 source;类型 core-logic;符号 run_single): Limited 奖励管理器移除 assert,改为 data[-1:]。
verl/experimental/reward_loop/reward_manager/naive.py(模块 奖励管理器;类别 source;类型 core-logic;符号 run_single): Naive 奖励管理器移除 assert,改为 data[-1:]。
verl/experimental/reward_loop/reward_manager/remote.py(模块 奖励管理器;类别 source;类型 core-logic;符号 run_single): Remote 奖励管理器移除 assert,改为 data[-1:]。
tests/experimental/reward_loop/test_rate_limited_reward_manager_on_cpu.py(模块 测试;类别 test;类型 test-coverage): 测试适配新版接口,使用 numpy 数组而非标量。
docs/advance/reward_loop.rst(模块 文档;类别 docs;类型 documentation): 文档更新反映新的接口参数。
关键符号:_compute_score, _agent_loop_postprocess, compute_score, run_single
关键源码片段
verl/experimental/agent_loop/agent_loop.py
核心变更点:重构 _compute_score 以支持多输出批量评分,是本 PR 的主要实现。
async def _compute_score(self, outputs: list[AgentLoopOutput], kwargs: dict) -> None:
# Compute reward score for all outputs; default reward manager uses last output only.
enable_async_reward = self.reward_loop_worker_handles is not None
final_output = outputs[-1]
if final_output.reward_score is None and enable_async_reward:
# Gather tensors from each output and pad into a batch
all_prompts, all_responses, all_input_ids, all_attention_mask, all_position_ids = [], [], [], [], []
for output in outputs:
prompts = torch.tensor(output.prompt_ids, dtype=torch.int64)
responses = torch.tensor(output.response_ids, dtype=torch.int64)
input_ids = torch.cat([prompts, responses], dim=0)
attention_mask = torch.ones_like(input_ids, dtype=torch.int64)
multi_modal_inputs = self._compute_multi_modal_inputs(output, input_ids)
position_ids = self._compute_position_ids(
input_ids.unsqueeze(0), attention_mask.unsqueeze(0), multi_modal_inputs
).squeeze(0)
all_prompts.append(prompts)
all_responses.append(responses)
all_input_ids.append(input_ids)
all_attention_mask.append(attention_mask)
all_position_ids.append(position_ids)
n = len(outputs)
data = TensorDict({
"prompts": torch.nn.utils.rnn.pad_sequence(all_prompts, batch_first=True, padding_value=0),
"responses": torch.nn.utils.rnn.pad_sequence(all_responses, batch_first=True, padding_value=0),
"attention_mask": torch.nn.utils.rnn.pad_sequence(all_attention_mask, batch_first=True, padding_value=0),
"input_ids": torch.nn.utils.rnn.pad_sequence(all_input_ids, batch_first=True, padding_value=0),
"position_ids": torch.nn.utils.rnn.pad_sequence(all_position_ids, batch_first=True, padding_value=0),
})
# Send batch to reward loop worker; reward manager selects last item
selected_reward_loop_worker_handle = random.choice(self.reward_loop_worker_handles)
result = await selected_reward_loop_worker_handle.compute_score.remote(data)
final_output.reward_score = result # assign score to final output
verl/trainer/main_ppo_sync.py
同步训练器的 _agent_loop_postprocess 简化,移除手动构造 tensors 并修复 teacher logprobs 参数。
async def _agent_loop_postprocess(
self, output: AgentLoopOutput | list[AgentLoopOutput], validate, **kwargs
) -> None:
"""Put agent loop outputs into TransferQueue."""
uid, session_id = kwargs["uid"], kwargs["session_id"]
outputs = output if isinstance(output, list) else [output]
if not outputs:
logger.warning(f"Empty output for prompt {uid}_{session_id}")
return
# Pass all outputs to _compute_score; default reward manager scores only the last.
await self._compute_score(outputs, kwargs=kwargs)
final_output = outputs[-1]
# TODO: Support output:list[AgentLoopOutput]
await self._compute_teacher_logprobs(
final_output,
prompt_ids=final_output.prompt_ids,
response_ids=final_output.response_ids,
validate=validate,
sample_kwargs=kwargs,
)
if final_output.reward_score is not None:
for output in outputs[:-1]:
output.reward_score = final_output.reward_score
output.extra_fields["reward_extra_info"] = final_output.extra_fields["reward_extra_info"]
# NOTE: agent loop may has multiple outputs, put each output into TransferQueue.
# key format: {uid}_{session_id}_{index}
keys, fields, tags = [], [], []
for i, output in enumerate(outputs):
prompts = torch.tensor(output.prompt_ids, dtype=torch.int64)
responses = torch.tensor(output.response_ids, dtype=torch.int64)
input_ids = torch.cat([prompts, responses], dim=0)
attention_mask = torch.ones_like(input_ids, dtype=torch.int64)
multi_modal_inputs = self._compute_multi_modal_inputs(output, input_ids)
position_ids = self._compute_position_ids(
input_ids.unsqueeze(0), attention_mask.unsqueeze(0), multi_modal_inputs
).squeeze(0)
keys.append(f"{uid}_{session_id}_{i}")
field = output.as_dict()
field.update(kwargs)
field.pop("multi_modal_data", None)
# ... remaining transfer logic
verl/experimental/reward_loop/reward_loop.py
RewardLoopWorker.compute_score 移除单数据断言,并确保蒸馏奖励模型只取最后一个输出。
async def compute_score(self, data: DataProto) -> dict:
if self.config.reward.custom_reward_function.path is not None:
# directly use user-customized reward function
return await self.reward_manager.run_single(data)
else:
if self.config.reward.reward_model.enable:
# we assume the rm is disrm
# genrm must set custom_reward_function
return await self.compute_score_disrm(data[-1:])
else:
return await self.reward_manager.run_single(data)
评论区精华
风险与影响
- 风险:
- 自定义奖励管理器兼容性风险:如果用户的外部奖励管理器仍保留
assert len(data)==1,使用多输出轨迹时会报错。建议用户在升级时检查自定义 reward manager 的 run_single 方法。
- 冗余计算:
_compute_score 中为每个输出重新计算 multi_modal_inputs 和 position_ids(这些在 _agent_loop_postprocess 中已算过),存在轻微性能浪费,但影响不大。
- 多输出 teacher logprobs 缺失:对于启用在线蒸馏的用户,中间轨迹的 teacher logprobs 未被计算,可能影响蒸馏效果。已在代码中标记 TODO。
- 影响:
- 用户影响:使用 TransferQueue trainer 和多轮 agent loop 的用户可直接受益,无需更改配置即可让奖励管理器接收完整轨迹;现有单输出训练流程完全向后兼容。
- 系统影响:涉及 agent_loop、trainer、reward_loop 及所有内置 reward manager,但每个文件改动量很小(+66/-60),测试覆盖通过。
- 团队影响:为后续扩展多轮奖励评分提供了清晰的接口基点,设计讨论(批量 vs 逐次)可作为团队尝参。
- 风险标记:自定义 reward manager 兼容性, 多输出 teacher logprobs 缺失, 冗余计算
关联脉络
参与讨论