verl · V1 主线重构
verl 源码 ↗
写给已经用过 verl 的后训练工作者

verl 主线 pipeline 大改:我该关心的三件事

主入口 main_ppo.py 已迁到 V1:TransferQueue 数据仓库 + AgentLoop 服务化 rollout + 可插拔 trainer 模式, 默认 trainer.use_v1=true。本文只回答三个你真正关心的问题——为什么改、老脚本还能不能跑、我的自由度提高了多少。

范围:V0.8.0 前后主线重构 关键 PR:#6067 · #6710 · #6823 入口命令:python -m verl.trainer.main_ppo(未变)

TL;DR · 一屏结论

  1. 为什么改:老主线假设「同步、定长、单轮」,为让 Agent(多轮/工具/变长/异步 off-policy)成为一等公民,把生成与训练解耦。
  2. 老脚本还能跑吗:能。入口命令没变,默认 use_v1=true + trainer_mode=sync ≈ 老同步 PPO;常规 GRPO/PPO 脚本基本原样可跑。想要异步只是加几行配置。
  3. 自由度:大幅提高。生成逻辑 / 采样策略 / 调度模式 / PPO 每个阶段都成了可插拔扩展点——多数定制不用再碰主循环。

Q1为什么要动主线?目的是什么?

一句话:老 pipeline 的三条隐含假设(同步、定长、单轮)挡住了 Agent 训练。大改就是把这三条拆掉。

吞吐
生成与训练可并行,慢样本(多轮/长)不再拖住整批 GPU
Agent
多轮对话 + 工具调用 + 变长/多输出轨迹成为一等公民
可插拔
同步↔异步、on↔off-policy、各阶段实现都变成可替换配置

老范式挡在哪 V0

  • 同步生成:trainer 阻塞等一整批 rollout 完成才继续。
  • 定长批次:一个大 tensor batch 在 trainer 内流转,假设样本形状规整。
  • 单轮为主:prompt→response 一来一回,rollout 与 trainer 共享进程/显存。
  • trainer 什么都管:worker、资源池、数据集、训练循环全塞进一个 RayPPOTrainer

Agent 训练要什么 V1

  • 变长 + 多输出:一条轨迹可能几十轮、带工具返回,每 session 多个 output。
  • 异步 / off-policy:允许用「稍旧权重」生成的轨迹继续训练,换取吞吐。
  • 服务化 rollout:推理引擎(vLLM/SGLang)作为独立 server,AgentLoop 像调 API 一样发多轮请求。
  • 解耦数据通道:生成侧与训练侧各自并行,中间要一个能存变长数据、带元数据的「仓库」。
图 A · 架构拓扑:V0 单体同步 → V1 解耦异步(记住这张图,后面都围绕它)
V0 · RayPPOTrainer(单体 · 同步) Dataloader Actor + Rollout(共卡) 同一进程 · 同步 generate PPO 更新 串行循环:① generate(阻塞整批)→ ② 训练 → ③ update → 下一轮 V1 · 生成 / 数据 / 训练 / 权重同步 四块解耦 Dataloader AgentLoopManager fire-and-forget · 每样本一 async 任务 LLM Servers(副本) vLLM / SGLang 发请求 TransferQueue KV 仓库 · {uid}_{sid}_{idx} · tags(staleness) 写入轨迹 PPOTrainer.step() actor / critic / ref · 读字段→算→写回 写回 Checkpoint Engine 权重同步
图 A:V0 把生成与训练锁死在同一个同步循环里;V1 让「生成侧」和「训练侧」各自并行,只通过中间的 TransferQueue 交换数据、通过 CheckpointEngine 异步回灌权重。
展开说明 ▸
  • 上半(V0):虚线大框代表「一个进程内的 RayPPOTrainer」。Dataloader → 共卡的 Actor+Rollout → PPO 更新,底部虚线箭头表示串行循环——必须等一整批 generate 阻塞完成才能训练,rollout 和训练抢同一批 GPU。
  • 下半(V1)· 生成侧(上排):Dataloader → AgentLoopManager(fire-and-forget,每条样本一个 async 任务)→ 一组 LLM Server 副本(vLLM/SGLang)。慢样本不再拖住快样本。
  • 中间深色块 = TransferQueue:唯一的数据交汇点。LLM Server 把轨迹「写入」,key 形如 {uid}_{sid}_{idx}(uid=原始 prompt,sid=第几条 session,idx=同一 loop 第几个 output),tag 里带 staleness 元数据。
  • 下排 = 训练侧PPOTrainer.step() 与 TransferQueue 之间是双向箭头(读字段 → 计算 → 写回),各阶段都在仓库里进出。
  • 右侧橙色虚线 = 权重同步:训练完的权重经 CheckpointEngine 异步回灌给 LLM Servers——这是生成侧与训练侧唯一的强耦合点,也正是三种 trainer_mode 的分水岭。

换句话说,V1 把老的「大 trainer」拆成了三个各司其职的组件: 生成侧AgentLoopManager + LLM Server)、 数据桥TransferQueue)、 训练侧PPOTrainer + CheckpointEngine)。这也解释了为什么后面两问的答案都很干净——组件解耦了,你能单独替换其中任意一块。

Q2老脚本还能跑吗?怎么快速适配?启动脚本变了啥?

先给定心丸:入口命令没变——还是 python3 -m verl.trainer.main_ppo ...。 默认 trainer.use_v1=true + trainer.v1.trainer_mode=sync,行为≈老的同步 PPO。 仓库里 examples/grpo_trainer/*.sh 这些脚本压根没写 use_v1 / trainer_mode,照样跑。 要的话临时退回旧实现:trainer.use_v1=false(走 main_ppo_v0.py),但它 deprecated、计划 v0.9.0 删除,别长期依赖。

启动脚本新增的旋钮(不写就用默认,多数人只需关心前两行)

配置项默认作用
trainer.use_v1true走 V1 主线;false 回退旧 main_ppo_v0.py(将删)
trainer.v1.trainer_modesyncsync / colocate_async / separate_async
trainer.v1.sampler.max_off_policy_threshold8一条轨迹最多允许跨几个权重版本(异步时的陈旧度闸门)
trainer.v1.sampler.max_off_policy_strategydrop超阈值怎么办:drop 丢弃 / wait 等待
trainer.v1.colocate_async.num_warmup_batches1异步前预热多少批生成
trainer.v1.separate_async.num_warmup_batches4分离异步的预热批数
trainer.v1.separate_async.parameter_sync_step4每几步把新权重同步给 rollout
actor_rollout_ref.rollout.agent.default_agent_loopsingle_turn_agent默认用哪种 agent loop(单轮/工具/自定义)

同一个脚本:同步照跑;想开异步只加两行

# 常规同步(等价旧行为,无需任何改动)
python3 -m verl.trainer.main_ppo \
    algorithm.adv_estimator=grpo \
    data.train_files=... actor_rollout_ref.model.path=... \
    trainer.n_gpus_per_node=8 trainer.nnodes=1
# ↑ 默认已是 trainer.use_v1=true, trainer.v1.trainer_mode=sync

# 想开异步(共卡 + partial rollout):追加两行即可
    trainer.v1.trainer_mode=colocate_async \
    trainer.v1.colocate_async.num_warmup_batches=1

迁移清单:哪些老用法会炸 / 要改

那我该选哪种 trainer_mode?

trainer_modetrain / rollout 布局partial rollout权重同步时机选它当你…
sync colocate(共享显存) 关闭 每步:sample 后 sleep,train 后 update 单轮/短轨迹、要稳要复现(默认,零改动)
colocate_async colocate 开启 每步 update + resume_generation;带 warmup 共卡想提吞吐、容忍轻度 off-policy
separate_async 物理分卡 + 空闲切换 开启 parameter_sync_step 周期同步 大规模 / 长 agent 轨迹、要最高吞吐
图 B · 时间线视角:从「生成/训练不重叠」到「两组卡全程并行」
时间 → sync 共卡·串行 生成 训练 生成 训练 生成 生成与训练完全不重叠 · 最 on-policy · partial rollout 关闭 colocate _async 共卡·重叠 生成(partial rollout,可续跑) 训练 生成在后台持续 + warmup 批次 · 与训练时间上重叠 · 轻度 off-policy separate _async 分卡·并行 Rollout pool 持续流式生成(永不停) 持续训练(攒够一批就更新) 两组 GPU 全程并行 · 按 parameter_sync_step 周期同步权重 · 吞吐最高 / off-policy 最强
生成(rollout) 训练(train) 权重同步
图 B:从上到下,生成与训练的「重叠程度」逐级加深——sync 完全串行,colocate_async 在同一批卡上时间重叠,separate_async 用两组卡彻底并行。
展开说明 ▸
  • sync(共卡·串行):橙块「生成」与深块「训练」首尾相接、绝不重叠;每次训练后(橙色虚线)才同步权重。最稳、最贴近 on-policy,但生成时训练卡闲着、反之亦然。
  • colocate_async(共卡·重叠):同一批卡,开 partial rollout——生成在后台持续、可跨步续跑(上排橙块基本连续),训练块(下排深块)与生成时间重叠,空闲被填上,代价是轻度 off-policy。
  • separate_async(分卡·并行):Rollout pool 与 Train pool 是两组物理分开的 GPU,各自连续长条、全程并行;橙色竖虚线是每隔 parameter_sync_step 的权重同步。吞吐最高,但一个 batch 里可能混着多个权重版本的样本,需要 decoupled-PPO 修正。
  • 关键洞察:三行的主流程代码完全相同,差异只在「权重何时同步」——即 on_sample_end / on_step_end 两个钩子里怎么调 CheckpointEngine。

共卡 vs 分卡:GPU 到底怎么分?

一个容易被忽略、但直接影响你申请多少卡的点:三种模式里真正改变 GPU 分配的只有 separate_asyncsynccolocate_async 用的是同一批卡、同样数量,差别只在调度时机(上一节的钩子)。

图 D · GPU 归属:共卡(1 个池)vs 分卡(2 个池)
共卡 · sync / colocate_async(1 个池) 训练 GPU 池 trainer.n_gpus_per_node × nnodes Actor / Critic / Ref 训练 ⇅ 同一批卡时分复用 ⇅ Rollout 引擎(hybrid) rollout sleep ↔ resume:让位 / 收回显存 分卡 · separate_async(2 个池) 训练 GPU 池 trainer.n_gpus_per_node × nnodes Actor/Critic/Ref + hybrid(空闲兼职) 独立 Rollout 池 rollout.n_gpus_per_node × rollout.nnodes 持续流式生成(永不停) 两组卡全程并行;训练空闲时 hybrid 兼职生成 按 parameter_sync_step 周期同步权重
图 D:共卡两种模式只有一个 GPU 池,rollout 作为 hybrid 引擎和训练时分复用同一批卡;separate_async 额外开一个由 rollout.* 配置决定大小的独立 rollout 池,两组卡真正同时跑。
模式GPU 池rollout 放哪决定卡数的配置
sync / colocate_async 1 个(共卡) 训练卡上 hybrid,时分复用 trainer.n_gpus_per_node × nnodes
separate_async 2 个(分卡) 独立 rollout 池 + 训练卡空闲兼职 训练池同上 + rollout.n_gpus_per_node × rollout.nnodes

为什么 separate_async 有这三条硬约束?

这三条不是随意限制,全部由「rollout 跑在另一组卡上、且永不停机」这一点直接推导出来。

硬约束根因:为什么必须这样
train_batch_size == ppo_mini_batch_size 全异步要求每个训练步恰好推进一个策略版本——一次 sample() → 一次梯度更新 → 同步一次权重。这样 global_steps 才是忠实的「策略版本时钟」,陈旧度 (global_steps − 生成时的 step) / parameter_sync_step 才算得准。若一批被拆成多个 mini-batch 做多次 SGD,一个 step 内策略就跳了好几个版本,异步 staleness 记账失真、解耦 PPO 的「行为策略 ↔ 近端策略」对应关系也乱了。所以强制一批 = 一个 mini-batch = 一次更新。
checkpoint_engine.backend != naive naive(即 ColocatedCheckpointEngine)只做「同卡 colocatedall_gather」,依赖 actor 与 rollout 在同一批卡 / 同一 torch.distributed 通信域——这正是共卡模式的玩法。分卡后 rollout 在另一组卡、还不停机,必须换成能跨设备 / 跨节点把权重传过去的后端:nccl/hccl(all_gather + broadcast)、nixl/mooncake(p2p),它们本就是为「actor/rollout 分离集群」设计的。
colocate reward 不支持(需 enable_resource_pool=true colocate reward 是靠「rollout 睡觉、腾出显存」这个窗口来临时加载 reward model 打分的。separate_async 的独立 rollout 卡从不暂停释放显存,压根没有这个窗口,所以 reward model 必须自己占一组独立卡(standalone reward 池)。而共卡两种模式能让 rollout sleep 腾显存,才允许 colocate reward。

看懂异步:一条「塞 1 取 1」的流水线,预热负责起步

V1 主循环本质是生产者 / 消费者流水线:每个 step 里 _add_batch_to_generate()(生产者,fire-and-forget,非阻塞)塞入 1 批 prompt;replay_buffer.sample()(消费者,阻塞轮询)等到「完成的轨迹」够 1 批才取走(还优先取最旧的以压 staleness)。生产与消费解耦——这步塞进去的,不一定是这步消费的那批。

图 E · 塞 1 取 1 的流水线:预热灌满在途队列,让生产者领先消费者
新权重回灌 rollout(on_step_end · update_weights) Dataloader 塞1批/步 生成中 生成中 快完成 已完成·待取 在途队列(rollout)· 预热把它灌到 num_warmup_batches 批深 取1批 训练 step sample 阻塞等待 生产者(塞1)持续领先消费者(取1)一段提前量 → 生成与训练重叠;提前量=0 就退化成同步
图 E:把生成想象成一条传送带——预热在训练开始前先塞入 num_warmup_batches 批,把带子填满;此后每步「塞 1、取 1」维持这个提前量,训练一结束就有完成的轨迹可取,rollout 也不空转。
展开:colocate_async 一个 step 的解剖 ▸
  • ① 塞入下一批 prompt(非阻塞,rollout 后台开跑)。
  • sample() 取走够一批的完成轨迹(靠提前量基本不空等)。
  • on_sample_endabort_replicas() 中止未完成请求(partial rollout 把半截轨迹存下来)+ sleep_replicas()(rollout 交出显存/KV cache)。
  • ④ 训练各阶段:此刻共享的卡归训练独占。
  • on_step_endupdate_weights()(推新权重)+ resume_generation_replicas()(唤醒 rollout,带新权重接着跑那些被 abort 的半截轨迹)。
  • 为什么共卡也能「重叠」:不是同一瞬间同卡并行,而是 partial rollout 让长轨迹跨 step 分段续跑(于是 trajectory_spans > 1、跨权重版本)+ 提前量让队列不空。代价是这步样本多由上一版权重生成 → 轻度 off-policy。
  • 对比 sync:partial rollout 关闭、无预热、无 resume——每步阻塞生成一整批全新样本 → sleep → 训练 → update,严格串行、完全 on-policy。
模式num_warmup_batches 默认为什么是这个深度
sync无(0)刻意不重叠:生成完这批立刻训练这批,最 on-policy
colocate_async1共卡只能浅浅重叠一步,约 1 步 off-policy
separate_async4(+ parameter_sync_step=4分卡全程并行,流水线更深、容忍更强 staleness

一句话:预热深度 ≈ 流水线深度 ≈ off-policy staleness 预算。预热建立提前量、partial rollout 让长轨迹跨步续跑,两者合起来才让共卡的 colocate_async 在同一批卡上把生成与训练错峰重叠。

快速选型:sync 跑通(几乎零改动);发现生成把卡闲置严重、且能接受轻度 off-policy,再上 colocate_async;只有当轨迹很长 / 规模很大、且愿意配非 naive 权重后端时,才上 separate_async

Q3训练流程的自由度提高了多少?能方便改哪些?

V1 把「生成逻辑 / 采样策略 / 调度模式 / PPO 每个阶段」都做成了可插拔扩展点。 绝大多数定制,你只需写一个子类 + 一行配置,不用碰主循环

你想改的东西怎么接入入口 / 配置
生成 / 多轮 / 工具逻辑写一个 AgentLoop 子类@register + rollout.agent.agent_loop_config_path
整个 rollout 调度器自定义 AgentLoopManagerrollout.agent.agent_loop_manager_class
采样 / off-policy 策略自定义 ReplayBuffer 子类trainer.v1.sampler.custom_sampler.{path,name}
调度模式(同步/异步布局)注册新 trainer_mode@register_trainer + 生命周期钩子
PPO 单个阶段(adv / logprob / …)override PPOTrainer._compute_*子类 + register_trainer
off-policy 修正(IS / decoupled)配置 rollout_correctionalgorithm.rollout_correction.*
reward(规则 / 模型)RewardLoopManager / reward serverreward.reward_model.*

PPO 每个阶段都是独立方法——step() 里按顺序调用,想改哪步就在 trainer 子类里覆盖对应方法,其它步骤不受影响:

# verl/trainer/ppo/v1/trainer_base.py :: PPOTrainer.step()
_add_batch_to_generate → replay_buffer.sample → [_compute_reward_colocate]
  → _balance_batch → _compute_old_log_prob → [_compute_ref_log_prob]
  → [_compute_values] → _compute_advantage → [_update_critic] → _update_actor
# 生命周期钩子(三种 trainer_mode 就靠它们区分):
on_train_begin / on_step_begin / on_sample_begin / on_sample_end / on_step_end

下面三张卡片是最常见的三类定制,点开看最小代码骨架(基于当前源码 API,实验阶段接口可能微调)。

🔁
改「生成逻辑」· 写一个 AgentLoop(多轮 / 工具 / 自定义采样)
最常见的 Agent 定制入口:继承 AgentLoopBase,在 run() 里自由多轮/调工具,返回 AgentLoopOutput,框架自动写入 TransferQueue。

内置参考实现:single_turn_agent_loop.py(单轮)、tool_agent_loop.py(ReAct 多轮 + 工具)。你只需实现 run()

# my_agent_loop.py
from typing import Any
from verl.experimental.agent_loop.agent_loop import (
    AgentLoopBase, AgentLoopOutput, register,
)

@register("my_agent")                     # 注册名,供 config / 数据集按名选用
class MyAgentLoop(AgentLoopBase):
    async def run(self, sampling_params: dict[str, Any], **kwargs) -> AgentLoopOutput:
        messages = list(kwargs["raw_prompt"])
        prompt_ids = await self.apply_chat_template(messages)

        # 多轮就在这里 while 循环:生成 → 解析工具调用 → 追加观测 → 再生成
        out = await self.server_manager.generate(          # 向 LLM Server 发请求
            request_id=uuid, prompt_ids=prompt_ids, sampling_params=sampling_params,
        )

        return AgentLoopOutput(
            prompt_ids=prompt_ids,
            response_ids=out.token_ids,
            response_mask=[1] * len(out.token_ids),   # 工具返回段可置 0,不参与 loss
            num_turns=2,
        )

接入(Hydra 注入,无需改主代码):

# my_agents.yaml
- name: my_agent
  _target_: my_agent_loop.MyAgentLoop
actor_rollout_ref.rollout.agent.agent_loop_config_path=my_agents.yaml \
actor_rollout_ref.rollout.agent.default_agent_loop=my_agent
🎛️
改「采样 / off-policy 策略」· 自定义 ReplayBuffer
想按难度/优先级/陈旧度挑样本?继承 ReplayBuffer 覆盖 sample(),一行配置注入。
# my_sampler.py
from verl.trainer.ppo.v1.replay_buffer import ReplayBuffer

class MyReplayBuffer(ReplayBuffer):
    def sample(self, global_steps: int, partition_id: str, batch_size: int):
        # 自定义优先级 / 难度课程 / staleness 筛选
        batch, metrics = super().sample(global_steps, partition_id, batch_size)
        return batch, metrics

接入:

trainer.v1.sampler.custom_sampler.path=my_sampler.py \
trainer.v1.sampler.custom_sampler.name=MyReplayBuffer \
trainer.v1.sampler.max_off_policy_threshold=8      # 内建陈旧度闸门仍可叠加
⚙️
改「调度 / 权重同步时机」· 注册一个新 trainer_mode
主循环固定在基类;你只写生命周期钩子,就能定义一套新的「同步 vs 异步」策略。
# my_trainer.py
from verl.trainer.ppo.v1 import PPOTrainer, register_trainer

@register_trainer("my_mode")
class MyTrainer(PPOTrainer):
    def on_sample_end(self):
        self.checkpoint_manager.sleep_replicas()          # 采样完让出显存
    def on_step_end(self):
        if self.global_steps % 2 == 0:                     # 例:每 2 步才同步一次权重
            self.checkpoint_manager.update_weights(self.global_steps)

接入:trainer.v1.trainer_mode=my_mode(确保该模块被 import 以触发注册)。想改某个 PPO 阶段,同理在这里 override _compute_advantage 等方法即可。

番外 · 放到 slime / AReaL 里看:这套设计不是孤例

如果你也看过 slime(清华 / 智谱)或 AReaL(蚂蚁 / 清华 IIIS),会发现三者在解同一道题—— 解耦生成与训练、加一个数据桥、异步同步权重、控制 staleness。差异主要在抽象哲学解耦彻底程度

不是巧合,是互相借鉴:verl 依赖的 TransferQueue==0.1.8 是可复用的独立组件(Ascend 开源); verl 的异步 off-policy 修正直接指向 AReaL 论文——trainer_separate_async.py 里写着 # TODO: Support Decoupled PPO: https://arxiv.org/abs/2505.24298。三者设计是收敛的。
图 C · 共同骨架:三家框架都长成同一个样子
权重同步(异步回灌新策略) 生成 推理引擎 · 流式产出轨迹 数据桥 缓冲 · 过滤 · 复用 训练 攒够一批 → 更新策略 写入 取用 verl LLM Server + AgentLoop TransferQueue PPOTrainer(可插拔模式) slime SGLang + sgl-router Data Buffer Megatron 循环(裸写) AReaL rollout workers(可中断) Replay Buffer trainer workers(全异步)
图 C:三家框架都是「生成 → 数据桥 → 训练 → 权重同步回生成」这同一个闭环,差别只在每个盒子的具体实现与解耦彻底程度。
展开说明 ▸
  • 左盒「生成」:verl 用 LLM Server + AgentLoop 客户端;slime 用 SGLang + sgl-router;AReaL 用可中断的 rollout workers。
  • 中盒「数据桥」(深色):解耦生成与训练的缓冲层。verl = TransferQueue,slime = Data Buffer,AReaL = Replay Buffer,角色完全对应。
  • 右盒「训练」:verl 是可插拔的 PPOTrainer,slime 刻意裸写训练循环,AReaL 是全异步 trainer workers。
  • 顶部橙色虚线 = 权重同步:训练更新后把新策略异步回灌给生成侧——所有 online RL 框架的共同命脉。
  • 结论:verl V1 不是另起炉灶,而是把这套业界收敛的骨架,用「结构化可插拔 + 服务化 Agent + 可复用 TransferQueue」重新组织了一遍。
维度verl V1slimeAReaL
数据桥 TransferQueue Data Buffer Replay Buffer(用一次)
解耦 / 布局 sync / colocate_async / separate_async 三档 一个 --colocate 开关 完全分离集群(fully async 优先)
off-policy 控制 max_off_policy_threshold + rollout_correction over-sample + 部分轨迹回收(APRIL) max_head_offpolicyness + decoupled PPO
trainer 抽象哲学 结构化基类 + 注册表 + 钩子 刻意不封装,循环裸露在 train.py algorithm-first(AReaL-lite)
🔥
slime · 三模块 + Data Buffer,轻量到不要 trainer 类
Megatron + SGLang + Data Buffer;靠 Ray 的 ray.get 位置切同步/异步;训练循环裸露在入口脚本。

抽象哲学:极简。slime 明确不用 trainer 类包裹,训练循环直接暴露在 train.py;靠移动 ray.get 的位置切同步/异步,靠一个 --colocate 决定共卡/分卡。与 verl「结构化基类 + 注册表 + 钩子」正好相反——一个偏灵活裸写,一个偏工程可插拔。

长尾优化(APRIL):over-sample(要 32 发 64)→ 够了 abort 其余 → 半截轨迹存 buffer、下轮续跑。对应 verl 的 partial rollout + min/max_global_steps 跨版本追踪。

给 verl 用户的启发:slime 的裸训练循环提醒我们,verl 的 trainer 抽象是「为多模式可插拔」买的单——好处是 Q3 里那些扩展点很规整,代价是初次读主流程更重。

来源: THUDM/slime ↗ · 介绍博客 ↗

AReaL · 「完全异步」的极致 + decoupled PPO(verl 直接引用)
生成/训练分到不同 GPU 集群,rollout 持续流式、trainer 攒够一批就更新;用 max_head_offpolicyness + 解耦 PPO 稳住 off-policy。

fully asynchronous。rollout worker 不等待、持续流式;trainer 攒够一批就更新,更新后同步权重回 rollout。一个 batch 可能混着不同模型版本的样本。论文报告相比同步系统最高 2.77× 加速且性能不降。

两把稳定器:rollout.max_head_offpolicyness(=0 退化为同步,异步常用 2–8)+ actor.use_decoupled_loss(解耦行为策略 π_behav 与近端策略 π_prox)。

与 verl 的血缘:verl separate_async 的 TODO 直指这篇;verl 的 rollout_correction(token-IS / seq-IS / geo-RS 等 decoupled_* 预设)与 bypass_mode,本质就是这套修正在 verl 的落地。

来源: inclusionAI/AReaL ↗ · 论文 arXiv:2505.24298 ↗

一句话行动指南