RolloutWorkflow 参考#
本文档描述 RolloutWorkflow 抽象,它是 AReaL 强化学习流水线中实现 Rollout 生成的核心接口。
注意:
本页面向希望深入理解代码库的开发者。对于代理 RL 训练,请使用 Agentic RL Guide 中描述的高级 API。
遗留模式:直接子类化
RolloutWorkflow被视为遗留方式,不应主动使用。对于新的代理 RL 工作流,请使用带有async def run()的代理工作流模式。
概述#
RolloutWorkflow 定义如何从输入数据生成训练轨迹。它封装了以下逻辑:
对提示进行分词并准备模型输入
调用推理引擎生成回复
为生成的输出计算奖励
将结果打包成用于训练的张量字典
接口#
from areal.api.workflow_api import RolloutWorkflow
class RolloutWorkflow(ABC):
@abstractmethod
async def arun_episode(
self, engine: InferenceEngine, data: dict[str, Any]
) -> dict[str, Any] | None | dict[str, InteractionWithTokenLogpReward]:
"""运行单个工作流 episode。"""
...
参数#
参数 |
类型 |
描述 |
|---|---|---|
|
|
用于生成模型回复的推理引擎 |
|
|
数据加载器的单个样本 |
返回类型#
arun_episode 方法支持三种返回类型:
返回类型 |
描述 |
|---|---|
|
用于训练的标准张量格式 |
|
Token 级别的交互(自动转换为张量);由高级 |
|
拒绝的轨迹,排除在训练之外 |
张量字典格式#
返回张量字典时,预期包含以下字段:
字段 |
形状 |
类型 |
必需 |
描述 |
|---|---|---|---|---|
|
|
int32 |
是 |
Token ID(提示 + 生成内容) |
|
|
bool |
是 |
有效 token 掩码 |
|
|
int32 |
否 |
生成内容 token 掩码(1 = 训练) |
|
|
float32 |
否 |
每个 token 的对数概率 |
|
|
float32 |
否 |
每序列奖励 |
|
|
int32 |
否 |
生成 token 时的权重版本 |
返回值示例:
return {
"input_ids": torch.tensor([[1, 2, 3, 4, 5]], dtype=torch.int32),
"attention_mask": torch.ones(1, 5, dtype=torch.bool),
"loss_mask": torch.tensor([[0, 0, 1, 1, 1]], dtype=torch.int32),
"logprobs": torch.tensor([[0.0, 0.0, -0.5, -0.3, -0.2]], dtype=torch.float32),
"rewards": torch.tensor([1.0], dtype=torch.float32),
"versions": torch.tensor([[0, 0, 1, 1, 1]], dtype=torch.int32),
}
工作流上下文#
在 arun_episode 中,通过 workflow_context 模块访问执行上下文。每个工作流实例有其自己的隔离上下文:
from areal.infra import workflow_context
async def arun_episode(self, engine, data):
# 获取当前执行上下文
ctx = workflow_context.get()
# 检查是否在评估模式运行
if ctx.is_eval:
# 使用不同参数进行评估
...
# 获取用于日志的任务 ID
task_id = ctx.task_id
# 根据模式获取统计作用域("rollout" 或 "eval-rollout")
scope = workflow_context.stat_scope()
轨迹转储#
当 InferenceEngineConfig.dump_to_file=True 时,轨迹自动保存到磁盘用于调试和分析。
配置#
rollout:
dump_to_file: true
fileroot: "/path/to/logs"
tokenizer_path: "model/tokenizer" # 文本解码必需
输出位置#
轨迹保存到:
{fileroot}/{experiment_name}/{trial_name}/[rollout|eval-rollout]/{version}/{task_id}.jsonl
示例:
/tmp/areal/my_exp/trial1/rollout/5/42.jsonl
输出格式#
JSONL 文件的每一行包含:
{
"task_id": 42,
"sample_idx": 0,
"seqlen": 512,
"prompt_len": 128,
"head_version": 5,
"tail_version": 6,
"version_rle": [[5, 100], [6, 200]],
"reward": 1.0,
"prompt": "<|im_start|>user\nWhat is 2+2?<|im_end|>\n<|im_start|>assistant\n",
"completion": "The answer is 4.<|im_end|>"
}
字段说明:
字段 |
说明 |
|---|---|
|
Batch 任务标识符 |
|
Batch 内的样本索引 |
|
有效序列长度 |
|
第一个生成 token 的位置( |
|
Per-sample 最小模型版本,仅统计 |
|
Per-sample 最大模型版本,仅统计 |
|
RLE 编码的 per-token 版本序列(仅输出 token),例如 |
|
该样本的 reward 值 |
|
解码后的 prompt 文本 |
|
解码后的 completion 文本 |
|
(仅多轮) 分段列表 |
目录命名: {version} 目录以 batch 内的全局最大版本(global_tail)命名。 同一目录内的单条 record 的
tail_version 可能 <= 目录名。
分组 Rollout#
分组 Rollout 对每个输入提示多次运行相同工作流,生成多样化回复用于训练。这对于像 GRPO 这样受益于每个提示多个样本的算法很有用。
配置#
提交 Rollout 时设置 group_size:
engine.submit(
data=sample,
workflow=MyWorkflow,
workflow_kwargs={...},
group_size=4, # 每个输入运行工作流 4 次
)
或通过 CLI:
rollout:
group_size: 4
工作原理#
当 group_size > 1 时,工作流被包装在 GroupedRolloutWorkflow 中:
包装器使用
asyncio.gather并发运行arun_episodegroup_size次根据类型合并结果:
张量字典:沿批次维度连接
InteractionWithTokenLogpReward 字典:合并为单个字典
每个 slot 可用时返回正常结果类型,不可用时返回
None。None有意保持不透明;原因分类与重试策略仍由 producer 负责。包装器只等待最初提交的 slots,既不会重试不可用 slot,也不会复制可用结果。
可用 slots 会各保留一次并连接;实际数量会在 reward 和 advantage normalization 中继续作为 prompt-group 边界。
设置
reward_normalization=True时,group 通过最小大小检查后,只对可用 rollout 的 interaction rewards 进行归一化。drop_incomplete_group=True仍要求所有原始 slot 成功;若保留的 rollout 缺少某一行的 reward,整个 group 会被丢弃。min_usable_group_size默认为1。当 RL trainer 的 reward 或 advantage normalization 使用 group statistics(该统计量至少需要两个观测值)时,才会将其设为2;singleton 目标 group (n_samples: 1)本身即是完整的,因此下限保持为1。设置actor.min_usable_group_size可覆盖该推导值;使用 group statistics 时,低于2的显式值会被拒绝。低于下限的 group 返回None,异步 collector 随后会接收另一个已就绪的 prompt group。采用 batch-relative PPO 或 REINFORCE 时则会保留可用的 singleton。v1 每次
arun_episode调用对应一个逻辑 rollout。上下文压缩或agent.export_style: individual可以导出多行,group 不会因此中止。Collector 在轨迹的rollout_group字段中保存RolloutGroup,记录每个 rollout 的连续行数和可选 reward 参考值。Batch 合并将其移入TrajBatchMeta,拆分时再恢复。可用 group 大小按逻辑 rollout 计数,n_samples: 1也遵循该规则。Group 和 batch reward normalization 都对每个逻辑 rollout 使用一个参考值。若同一 rollout 各行 reward 不同,workflow 必须在张量字典中或导出的
InteractionWithTokenLogpReward上 提供有限标量rollout_reward;同一 rollout 中所有显式参考值必须一致。省略时,只能从相等的 行 reward 取得参考值。每一行保留自己的 reward,并使用相同的平移和缩放;leave-one-out 基线排除整个逻辑 rollout。当算出的参考值标准差不大于 normalization epsilon 时,保留平移并使用除数1。内置 v1 agent workflow 为individual导出显式提供最终 reward 作为参考值,同时保留各行折扣后的 reward 和已提供的参考值。自定义 workflow 必须为不同行 reward 声明自己的参考值,不会自动猜测最终值、总和或均值。内置按行长度计算的 overlong penalty 不改变显式参考值;actor 的 reward bias、scaling 和 clipping 同时作用于行 reward 与参考值。未提供显式参考值时,施加长度惩罚后的行 reward 仍须相等。Advantage normalization 保持现有 masked token 统计及按 token 的 leave-one-out 行为,逻辑计数仅用于 singleton 回退。GAE 仍分别在各行上计算。
PPO 系列 actor loss 默认仍按全局 token 加权。因此,有更多有效 response tokens 的 partial group 会比更小或更短的 group 获得更高 loss weight。这是保持向后兼容的现有 estimator,并不表示各 prompt 隐式等权。
Grouped rollout 会导出 target_slot_count、usable_slot_count、
trainable_slot_count、fully_masked_group、singleton_slot_group、
pre_filter_usable_slot_yield 和 pre_filter_trainable_slot_yield。这些指标统计最初的 rollout
调用;should_accept_fn 运行后的最终 accepted 和 rejected 数量仍由 collector metrics 提供。PPO 训练会另外报告物理
usable group size 与有效 token 的 loss-weight 分布,其中包括按 size 区分的 group_loss_weight_size_<N>
指标。
输出形状#
当 group_size=4、工作流返回 [1, seq_len] 张量且 4 个 slot 均可用时,分组输出的形状为
[4, seq_len]。如果接受的是不完整 group,则第一维是实际可用的 slot 数量。
实现#
来自 areal/infra/remote_inf_engine.py:
class GroupedRolloutWorkflow(RolloutWorkflow):
async def arun_episode(self, engine, data):
# 并发运行 N 次
results = await asyncio.gather(
*[self.workflow.arun_episode(engine, data)
for _ in range(self.group_size)]
)
# 正常结果表示可用,None 表示不可用
valid_results = [r for r in results if r is not None]
if len(valid_results) < self.min_usable_group_size:
return None
# 根据结果类型合并
if all_interaction_dicts(valid_results):
return merge_dicts(valid_results)
else:
return concat_padded_tensors(valid_results)
实现自定义工作流#
创建自定义工作流:
子类化
RolloutWorkflow:
from areal.api.workflow_api import RolloutWorkflow
class MyWorkflow(RolloutWorkflow):
def __init__(self, tokenizer, gconfig, **kwargs):
self.tokenizer = tokenizer
self.gconfig = gconfig
async def arun_episode(self, engine, data):
# 1. 准备输入
input_ids = self.tokenizer.encode(data["prompt"])
# 2. 生成回复
req = ModelRequest(
rid=uuid.uuid4().hex,
input_ids=input_ids,
gconfig=self.gconfig,
tokenizer=self.tokenizer,
)
resp = await engine.agenerate(req)
# 3. 计算奖励
reward = self.compute_reward(resp, data)
# 4. 返回张量字典(或 None 拒绝)
if reward < 0:
return None
return self.build_tensor_dict(resp, reward)
向训练器注册:
trainer.train(
workflow=MyWorkflow,
workflow_kwargs={
"tokenizer": tokenizer,
"gconfig": config.gconfig,
},
)
工作流解析#
工作流可以通过多种方式指定:
格式 |
示例 |
描述 |
|---|---|---|
实例 |
|
预实例化的工作流 |
类 |
|
类(需要 kwargs) |
字符串路径 |
|
动态导入 |
代理工作流 |
任何带 |
带代理支持封装 |
训练系统自动将这些解析为 RolloutWorkflow 实例。
另见#
Agentic RL Tutorial - 使用代理框架训练
Adding Custom Workflows - 分步指南