首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >18-长尾rollout为什么agentRL容易让GPU空转

18-长尾rollout为什么agentRL容易让GPU空转

作者头像
anzhsoft
发布2026-07-13 11:12:46
发布2026-07-13 11:12:46
420
举报

平均吞吐解释不了 agent RL 的等待;真正支配 step 的,常常是最慢的少数轨迹。

上一篇把工具、sandbox 和环境反馈接回了训练信号。本文继续看这个设计的代价:一旦 rollout 不再是单轮补全,而是长 prompt、长 response、多轮工具调用、sandbox 等待和异步 server 的组合,一轮 RL step 的耗时就不再由平均样本决定,而经常由最慢的少数轨迹决定。

本文的核心判断是:agent RL 的 GPU 空转不是单个后端“不够快”的问题,而是 rollout 阶段出现了 step 级长尾。短请求先结束、长请求还在跑;工具和 sandbox 可能在 GPU 外等待;trainer 又要等整批 generate_sequences()合并成 DataProto 后才能进入 reward、logprob 和 update。于是,生成服务的局部并发并不能自动消除训练 step 的全局等待。

先看这个长尾在一轮 step 里的位置。读这张图时注意:每条样本的 rollout 都可以异步跑,但 trainer 进入 reward/update 之前,仍然要拿到整批输出。

一轮 rollout 中的长尾等待

源码路径正是这样:RayPPOTrainer.fit()gentimer 里调用 async_rollout_manager.generate_sequences(),随后让 rollout replicas sleep,并把生成结果 union 回训练 batch;之后才进入 reward、old/ref logprob、advantage、critic/actor update(verl/trainer/ppo/ray_trainer.py:1373-1418verl/trainer/ppo/ray_trainer.py:1426-1557)。

1. batch 内异步不等于 step 无等待

AgentLoopWorker.generate_sequences()对 batch 中每条样本创建一个 asyncio.create_task(),然后 await asyncio.gather(*tasks),最后 _postprocess()合并成 DataProto(verl/experimental/agent_loop/agent_loop.py:450-542)。这说明 batch 内的 agent loops 可以并发运行,但整批输出仍然要等所有 task 结束。

下面这张图把这个控制流展开。它补的是:异步降低了队头阻塞,但并没有取消“整批合并”的 step 边界。

AgentLoopWorker 的并发与汇总边界

这种结构很适合说明 agent RL 的长尾:同一个 batch 里,有的样本一轮生成后结束,有的样本要多次 tool call;有的 response 很短,有的直到 response_length上限;有的工具瞬间返回,有的工具或 sandbox 需要等待外部执行。短样本完成后,训练主循环仍然不能进入后续阶段,因为 _postprocess()需要整批结果。

这不是说异步没有价值。异步 server 和 batch 内并发可以提高请求吞吐,也能让工具等待期间其他请求继续推进。问题在于,如果训练算法仍以 step 为单位消费整批 DataProto,那么长尾轨迹会把“服务层并发”重新转化成“训练层等待”。

2. load balancer 分摊请求,但多轮需要 sticky session

rollout 服务层并不是无调度。LLMServerManager会启动多个 rollout replicas,再创建全局 load balancer;LLMServerClient.generate()每次请求先 acquire server,结束后 release(verl/workers/rollout/llm_server.py:222-354verl/workers/rollout/llm_server.py:169-220)。

下面这张图展示 server routing 的逻辑。看图时注意:首次请求按 least in-flight 选 server,多轮会用 sticky session 保持同一 request_id 到同一 server。

请求路由与 sticky session

GlobalRequestLoadBalancer的注释很直接:它同时做 sticky session 和 least-loaded selection。没有 sticky session 时,选择 in-flight 最少的 server;已有 request_id 时,优先回到同一 server,这对多轮 chat completion 和 prefix caching 有意义(verl/workers/rollout/llm_server.py:43-91)。LLMServerClient.generate()则会用外层 request_id acquire server,但给 server 端每一轮生成传入新的 request id(verl/workers/rollout/llm_server.py:178-220)。

这解释了一个现实约束:load balancer 能减少副本间负载不均,但它不能随意把同一条多轮轨迹拆到不同 server 上。对长工具链、多轮对话、长上下文样本来说,sticky session 是正确性和缓存友好的选择,同时也会让某些 server 更容易被长轨迹占住。

3. 长尾来自多种变量叠加

agent RL 的长尾不是一个变量造成的。下面这张图把主要来源分成四类:token 长度、turn 数、环境等待和后端容量。

长尾 rollout 的来源地图

第一类是 token 长度。vLLM、SGLang、TRT-LLM 的 async server 都会根据 prompt_length + response_length - len(prompt_ids)max_model_len计算本轮还能生成多少 token,防止越过上下文窗口(verl/workers/rollout/vllm_rollout/vllm_async_server.py:447-525verl/workers/rollout/sglang_rollout/async_sglang_server.py:496-565verl/workers/rollout/trtllm_rollout/trtllm_async_server.py:313-341)。prompt 越长,可用生成预算越小;response 越接近上限,请求占用 decode 时间越长。

第二类是 turn 数和工具等待。ToolAgentLoopmax_assistant_turnsmax_user_turnsmax_parallel_callsmax_tool_response_length等限制;每轮生成后如果解析出 tool calls,就进入工具处理,再把 observation 追加回上下文继续生成(verl/experimental/agent_loop/tool_agent_loop.py:103-113verl/experimental/agent_loop/tool_agent_loop.py:255-286)。这意味着一个样本的耗时可能是多次 LLM decode 加多次环境等待的总和。

第三类是后端容量和调度。rollout 配置里的 max_num_batched_tokensmax_num_seqsenable_chunked_prefillenable_prefix_cachingscheduling_policy都会影响服务端怎样接纳和调度请求(verl/trainer/config/rollout/rollout.yaml:69-88)。TRT-LLM 还把 max_batch_sizemax_num_tokens和 scheduler policy 写入 AsyncLLM 配置(verl/workers/rollout/trtllm_rollout/trtllm_async_server.py:220-289)。

第四类是 disaggregation 与网络传输。SGLang PD 路径里,prefill 会创建 bootstrap room,并 fan out 到本地 prefill 和远端 decode;源码注释里明确提到随机选择 decode peer 是为了避免 RL prompt length 的 heavy-tailed skew 造成系统性倾斜(verl/workers/rollout/sglang_rollout/async_sglang_server.py:507-535)。这说明长尾不是外部猜测,而是后端实现中已经需要处理的调度事实。

4. abort/resume 是长尾与权重同步之间的止损机制

长尾还会影响权重同步。RL 训练希望 rollout 使用新 actor 权重,但 async server 可能还有未完成请求。非 naive checkpoint engine 路径的做法是先 abort unfinished requests,再释放 KV cache、同步权重、恢复 KV cache,并 resume generation(verl/checkpoint_engine/base.py:447-492)。

下面这张图把这个止损机制画出来。它和前面的长尾图互补:前面解释为什么慢请求拖住 step,这里解释系统如何在权重同步前处理未完成请求。

长尾请求与权重同步的 abort/resume 路径

各 backend 都要实现对应能力。vLLM server 的 abort_all_requests()会 pause generation、abort in-flight requests、等待 drain,并可清 prefix/MM cache;之后 resume_generation()恢复接收新请求(verl/workers/rollout/vllm_rollout/vllm_async_server.py:647-720)。SGLang server 通过 pause_generation(mode="abort")continue_generation()处理(verl/workers/rollout/sglang_rollout/async_sglang_server.py:667-675)。TRT-LLM server 会清掉 _generation_allowed、pause generation、等待 engine idle,再 reset prefix cache;恢复时再 set event(verl/workers/rollout/trtllm_rollout/trtllm_async_server.py:388-401)。

这类机制不是为了优化平均延迟,而是为了在“继续等慢请求”和“推进权重同步”之间建立可控边界。代价是系统必须处理 aborted output、KV cache 清理、prefix cache 失效和 generation resume 的一致性。

5. 读长尾 rollout 时要避免两个误读

第一,不要把 GPU 空转只归因于工具慢。工具等待确实会造成 CPU/外部系统等待,但长尾也可能来自 prompt length、response length、backend batch token 容量、prefix cache、pipeline/TP 配置、PD 传输和权重同步窗口。工具只是让这些变量更显性。

第二,不要以为加 rollout replicas 就能线性解决。更多 replicas 可以降低队列压力,但 batch 级 gather、sticky session、多轮上下文和 trainer step 边界仍然存在。如果最慢样本长期决定 step 时长,系统需要的是样本切分、异步策略、partial rollout、off-policy 容忍度或更细粒度的数据流,而不只是更多 server。

小结:第三组从 rollout 服务走到 rollout 调度

第三组的前几篇建立了 rollout 的服务形态、backend 抽象、显存状态机和 agent token 轨迹。本文补上最后一块:当 rollout 变成多轮环境交互,吞吐瓶颈会从“单次 generate 多快”变成“整批轨迹何时都能返回”。

放回系列地图,第三组已经覆盖了:

代码语言:javascript
复制
controller
-> workers/resources
-> rollout/serving engine
-> agent loop / tool environment
-> long-tail rollout scheduling
-> weight sync

下一组会转入训练引擎与分布式核心。因为 rollout 产出的长尾轨迹最终仍要进入 actor/critic update,而训练引擎如何切 batch、如何并行、如何同步权重,会决定这个训推闭环能不能持续跑下去。

本文源码索引

  • verl/trainer/ppo/ray_trainer.py:1373-1418:训练主循环中生成、sleep、合并 rollout 输出和记录 token 数的路径。
  • verl/trainer/ppo/ray_trainer.py:1426-1557:rollout 后进入 reward、old/ref logprob、advantage 和 actor/critic update。
  • verl/experimental/agent_loop/agent_loop.py:450-542:batch 内创建 agent loop tasks,并用 asyncio.gather()等待整批完成。
  • verl/experimental/agent_loop/agent_loop.py:579-729:单条 agent output 如何 padding 成训练字段。
  • verl/workers/rollout/llm_server.py:43-91:全局 load balancer 的 sticky session 和 least in-flight 选择。
  • verl/workers/rollout/llm_server.py:178-220LLMServerClient.generate()如何 acquire/release server,并向 server 发起生成。
  • verl/workers/rollout/llm_server.py:265-354LLMServerManager如何创建 rollout replicas 和 load balancer。
  • verl/trainer/config/rollout/rollout.yaml:69-88:rollout token 容量、序列容量、chunked prefill、prefix cache 和 scheduling policy。
  • verl/experimental/agent_loop/tool_agent_loop.py:103-113255-286:多轮、工具并发和工具处理对 rollout 耗时的影响。
  • verl/workers/rollout/vllm_rollout/vllm_async_server.py:447-525:vLLM async generate 的 max token 计算和请求提交。
  • verl/workers/rollout/sglang_rollout/async_sglang_server.py:496-565:SGLang async generate 的 max token 计算和 PD dispatch。
  • verl/workers/rollout/trtllm_rollout/trtllm_async_server.py:220-289313-341:TRT-LLM 容量配置和 async generate 参数。
  • verl/checkpoint_engine/base.py:447-492:非 naive 权重同步前后的 abort、release KV、resume generation 流程。
  • verl/workers/rollout/vllm_rollout/vllm_async_server.py:647-720:vLLM abort/resume generation。
  • verl/workers/rollout/sglang_rollout/async_sglang_server.py:667-675:SGLang abort/resume generation。
  • verl/workers/rollout/trtllm_rollout/trtllm_async_server.py:388-401:TRT-LLM abort/resume generation。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 训推工坊 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. batch 内异步不等于 step 无等待
  • 2. load balancer 分摊请求,但多轮需要 sticky session
  • 3. 长尾来自多种变量叠加
  • 4. abort/resume 是长尾与权重同步之间的止损机制
  • 5. 读长尾 rollout 时要避免两个误读
  • 小结:第三组从 rollout 服务走到 rollout 调度
  • 本文源码索引
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档