首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >26-RLHF吞吐瓶颈 在哪里

26-RLHF吞吐瓶颈 在哪里

作者头像
anzhsoft
发布2026-07-23 20:58:13
发布2026-07-23 20:58:13
660
举报

RLHF 的吞吐瓶颈不是固定在 rollout 或 update 其中一侧,而是在同步闭环里由最长窗口和互等关系共同决定。 从第25篇的时间账继续推进,回答 RLHF 吞吐下降时该如何归因。文章不把瓶颈简单归到 rollout 或 update,而是用 perf/throughput、 timing_s/gen、update_actor、old_log_prob、update_weights 和 agent loop 指标建立诊断路径,适合带着日志定位真实等待窗口

第 25 篇建立了一轮 RL step 的 profiling 账本:先看 perf/time_per_stepperf/throughput,再拆 timing_s/gentiming_s/old_log_probtiming_s/update_actortiming_s/update_weights。第 26 篇继续往前走一步:当吞吐下降时,怎样判断问题到底在 rollout generation,还是 actor update,或者在它们之间的同步边界?

本文的核心判断是:在 verl 的同步 PPO 主循环里,端到端吞吐由 total_tokens / (step_time * n_gpus)定义,但 step_time是一串阶段窗口的组合。gen代表 trainer 等 rollout 产样本;old_log_prob是训练侧重新跑 actor inference;update_actor是训练 engine 的 mini-batch/micro-batch update;update_weights是新权重回到 rollout 的同步。只有把这些窗口和资源状态一起读,才能判断“慢”属于 rollout、update、sync,还是批次形态带来的误导。

先看第 26 篇的诊断地图。读图时注意:吞吐公式在右侧很简单,但左侧的 step time 并不是单个模块时间;任何一个阶段变长,都会拉低端到端 tokens/s/GPU。

RLHF 吞吐瓶颈诊断地图

这张图的作用,是把第 26 篇和第 25 篇衔接起来:我们不从“rollout 一定慢”或“训练一定慢”出发,而是先把 perf/throughput拆回 timing_s/*。后文会依次解释 rollout-bound、update-bound 和 sync-bound 三种读法,并补上几个常见的误判边界。

1. 吞吐下降先回到 step time

verl 的 compute_throughout_metrics()把吞吐写得很直接:先从 batch.meta_info["global_token_num"]求总 token 数,再用 timing_raw["step"]和 GPU 数计算 perf/throughput。这意味着 throughput 下降只有两个直接原因:同样 GPU 数下,总 token 少了,或者 step time 长了。绝大多数瓶颈定位都要先回到第二项。

下面这张图把吞吐指标拆回 step time。读图时注意,perf/throughput是端到端指标,它本身不会告诉你是哪一段慢;真正的归因要看 timing_s/gentiming_s/update_actortiming_s/update_weights等明细。

从 throughput 回到 step time

源码上,trainer 在生成后写入 batch.meta_info["global_token_num"],最后调用 compute_timing_metrics()compute_throughout_metrics()统一产出指标(verl/trainer/ppo/ray_trainer.py:1417-14181639-1642)。compute_timing_metrics()给每个阶段输出 timing_s/{name}compute_throughout_metrics()则用 step作为分母(verl/trainer/ppo/metric_utils.py:271-346)。

所以第一条读法是:先不要比较不同实验的裸 throughput,而要同时看 token 数、response 长度、rollout.n、step time 和阶段明细。比如 response 变长会让 total tokens 增加,也会让 genold_log_probupdate_actor都变长;如果只看吞吐数字,很容易把正常的 token 结构变化误判成系统退化。

2. 同步 PPO 默认先生成,再更新,再同步权重

第 26 篇讨论的是当前同步 PPO 主路径,不是 fully async policy。这个边界很重要:在 RayPPOTrainer.fit()里,gen返回后,trainer 会调用 checkpoint_manager.sleep_replicas(),随后继续 reward、old logprob、adv、update actor,最后通过 checkpoint_manager.update_weights()把新权重送回 rollout。也就是说,rollout generation 和 actor update 在这条主路径上不是完全重叠的流水线。

下面这张图画的是资源时间线。读图时注意:rollout engine 活跃在 gen窗口;训练 worker 活跃在 logprob/update 窗口;update_weights把训练后的 actor 接回 rollout。这个串行结构决定了瓶颈不是“谁的 GPU 利用率高”,而是谁让下一段等得更久。

同步 PPO 的 rollout、update 与 sync 时间线

源码支持这个时间线:gen阶段调用 async_rollout_manager.generate_sequences(),紧接着让 rollout replicas sleep;后续 old_log_probupdate_actorupdate_weights都在同一个 step计时窗口内顺序执行(verl/trainer/ppo/ray_trainer.py:1373-1586)。CheckpointEngineManager.sleep_replicas()会让所有 rollout replicas sleep,update_weights()在 naive 路径直接更新 colocated worker,在非 naive 路径还会 abort、release KV cache、build process group、双侧 update、finalize、resume generation(verl/checkpoint_engine/base.py:410-492)。

这带来一个工程解释:如果 timing_s/gen很高,训练 worker 可能在等样本;如果 timing_s/update_actortiming_s/update_weights很高,rollout 侧可能在睡眠或等待新权重。第 29 篇会讨论 fully async 如何改变这种互等关系;第 26 篇先把同步路径读清楚。

3. rollout-bound 的证据不只是 timing_s/gen

timing_s/gen占 step time 的大头时,第一反应通常是“rollout 慢”。但第 25 篇已经说过,gen是一个外层等待窗口,它可能包含 LLM server generation、agent loop 工具调用、异步 reward、Ray worker 汇总、response 长尾和 sleep 时机。要判断是不是 rollout-bound,需要继续看 agent loop 的内部指标。

下面这张图给出 rollout-bound 的证据链。读图时注意:左边是外层 gen,右边是 agent loop manager 聚合出的 min/max/mean 和 slowest sample。只有这两层一致,才能更有把握地把慢点归到 rollout 侧。

rollout-bound 的证据链

在源码里,AgentLoopManager.generate_sequences()会把输入 batch 切给多个 agent loop worker,等待所有 worker 返回后 concat,再从每个输出的 meta_info["metrics"]中计算 agent_loop/generate_sequences/min|max|meantool_calls/min|max|meancompute_score/min|max|mean,并记录 slowest sample 的 prompt/response length(verl/experimental/agent_loop/agent_loop.py:1069-1126)。单轮 agent loop 对 server request 用 simple_timer("generate_sequences")计时;tool agent loop 还会单独记录 tool_callsverl/experimental/agent_loop/single_turn_agent_loop.py:57-70verl/experimental/agent_loop/tool_agent_loop.py:216-285)。

因此,rollout-bound 的典型证据不是一句“gen高”,而是:

  1. timing_s/gen / timing_s/step占比高。
  2. agent_loop/generate_sequences/maxagent_loop/slowest/response_length指向长尾样本。
  3. agent_loop/tool_calls/maxcompute_score/maxnum_preempted/max能解释多轮工具、reward 或推理调度压力。
  4. timing_s/update_actor相对稳定,说明训练 update 本身没有同步变慢。

如果只满足第一条,结论要保守。gen还可能被 controller 汇总、DataProto 合并、Ray 调度或 sleep/resume 影响;这些会在第 27、28 篇进一步拆。

4. update-bound 要同时看 update_actor和 MFU

另一种常见情况是 timing_s/update_actor占比高。这时瓶颈更接近训练 engine,但仍然不能只看总时间。_update_actor()会把 batch 转成 TensorDict,转成 no-padding 表示,写入 ppo_mini_batch_sizeppo_epochs、shuffle 和 loss 计算开关,再调用 actor_rollout_wg.update_actor()。worker 侧会进入 ActorRolloutRefWorker.update_actor(),再调用 TrainingWorker.train_mini_batch()train_batch()

下面这张图把 update-bound 的路径拆开。读图时注意:update_actor高可能来自算法层的 PPO epoch/mini-batch,也可能来自 engine 层的 micro-batch、dynamic batch size、sequence length、并行通信或 optimizer step。

update-bound 的 actor 更新路径

源码上,_update_actor()会把ppo_mini_batch_size乘以rollout.n,设置epochsmini_batch_sizecompute_loss=True,然后调用 actor worker group;返回 metrics 后把actor/mfu改名成perf/mfu/actorverl/trainer/ppo/ray_trainer.py:1205-1245)。worker 侧train_mini_batch()会按 DP rank 切 mini-batch,遍历 PPO epochs,逐个 mini-batch 调train_batch()train_batch()再把use_dynamic_bszmax_token_len_per_gpumicro_batch_size_per_gpu等执行参数注入 engine,并用engine.train_batch()完成训练(verl/workers/engine_workers.py:250-315329-382646-651)。

所以 update-bound 的证据链应该包含两类指标:第一是 timing_s/update_actortiming_per_token_ms/update_actor;第二是 perf/mfu/actor、显存指标、mini-batch/micro-batch 配置和 token 长度。如果 update_actor高但 MFU 也高,系统可能已经主要受计算量约束;如果 update_actor高但 MFU 低,就要怀疑 micro-batch 太碎、dynamic batch 装箱、并行通信、offload、数据搬运或 pipeline bubble。

5. old_log_probupdate_weights会伪装成两侧瓶颈

第 26 篇标题说 rollout generation vs actor update,但实际排查时还有两个阶段经常改变结论:old_log_probupdate_weights。前者会重新跑 actor inference,后者会把训练后的权重同步回 rollout。它们都不属于“新样本生成”或“optimizer 更新”本身,但都会拉长 step time。

下面这张图把这两个伪装点放在 gen/update 之间。读图时注意:old_log_prob更像训练侧 inference 成本,update_weights更像 train/serve 边界成本;如果忽略它们,就会把瓶颈错误归到 rollout 或 actor update。

old_log_prob 和 update_weights 改变瓶颈归因

_compute_old_log_prob()会把 batch 转 TensorDict、转 no-padding,调用 actor_rollout_wg.compute_log_prob(),再把 no-padding 结果还原成 padding,并返回 old_log_prob_mfu。主循环里它被记录为 timing_s/old_log_probperf/mfu/actor_inferverl/trainer/ppo/ray_trainer.py:1168-12031450-1465)。这说明 old logprob 慢时,不能简单说 rollout 慢;它使用的是 actor inference 路径。

update_weights的含义也不同。配置里 checkpoint engine 默认 backend 是 naive,同时还有 update_weights_bucket_megabytes等同步参数;非 naive 路径会显式处理 abort、release KV cache、build process group、trainer/rollout 双侧 update、resume KV cache 和 resume generation(verl/trainer/config/rollout/rollout.yaml:263-284verl/checkpoint_engine/base.py:448-492)。如果这个阶段高,下一步应该读第 24 篇的 checkpoint engine 路线,而不是继续调 rollout sampling 或 actor micro-batch。

6. 一张实用判定表

把上面的证据合在一起,可以得到一个保守但可执行的判定表。它不是自动诊断器,而是帮助读者把观测指标转成下一步源码路径。

观测模式

更可能的瓶颈

下一步看什么

timing_s/gen占比高,agent loop slowest 也高

rollout-bound

response length、tool calls、compute score、preemption、rollout backend profile

timing_s/update_actor占比高,perf/mfu/actor高

compute-bound update

训练 engine、并行维度、PPO epochs、token 数

timing_s/update_actor高但 MFU 低

execution-bound update

micro-batch、dynamic batch、通信、offload、数据搬运

timing_s/old_log_prob高

actor inference 重算成本

logprob micro-batch、actor inference MFU、bypass/rollout correction 分支

timing_s/update_weights高

weight-sync-bound

checkpoint engine backend、bucket、KV cache、abort/resume

perf/throughput低但各阶段比例稳定

workload shape 变化

total tokens、response length、rollout.n、batch balance

这张表的核心是把“慢在哪里”变成“下一步读哪里”。如果下一步要改配置,至少应该先明确改的是 rollout 并发、response 长度、actor mini/micro-batch、checkpoint backend,还是数据路径。否则,吞吐调优很容易变成互相抵消的参数试错。

小结:瓶颈是同步闭环里的最大等待窗口

第 26 篇回答的不是一个固定答案,而是一种判断方式:在同步 RLHF 训练里,rollout generation、actor update、old logprob 和 weight sync 都会进入同一个 step time;端到端 throughput 只是把这段时间除以 token 和 GPU 数。真正的瓶颈,是当前配置和 workload 下让闭环等待最久的窗口。

放回系列地图,第 25 篇建立 profiling 账本,第 26 篇把账本转成瓶颈诊断。接下来第 27 篇会继续拆一个更隐蔽的问题:就算 compute 侧看起来合理,DataProto、Ray object、controller 汇总和序列化也可能成为 data movement 的隐藏大头。

本文源码索引

  • verl/trainer/ppo/metric_utils.py:271-346timing_s/*、per-token timing 和 perf/throughput的计算方式。
  • verl/trainer/ppo/ray_trainer.py:1373-1642:同步 PPO 主循环中 genold_log_probupdate_actorupdate_weights的顺序和指标汇总。
  • verl/trainer/ppo/ray_trainer.py:1168-12031450-1465:old logprob 如何走 actor inference 路径,并记录 perf/mfu/actor_infer
  • verl/trainer/ppo/ray_trainer.py:1205-1245:actor update 如何注入 PPO mini-batch、epochs、shuffle,并产出 perf/mfu/actor
  • verl/workers/engine_workers.py:250-315329-382646-651:actor worker 如何进入 train_mini_batch()train_batch()和 engine 训练路径。
  • verl/workers/engine_workers.py:177-235:训练 worker 如何聚合 loss、显存和 MFU metrics。
  • verl/experimental/agent_loop/agent_loop.py:1069-1126:agent loop manager 如何聚合 generation/tool/reward timing 和 slowest sample。
  • verl/experimental/agent_loop/single_turn_agent_loop.py:57-70verl/experimental/agent_loop/tool_agent_loop.py:216-285:单轮和工具 agent loop 如何记录 generation 与 tool call 时间。
  • verl/checkpoint_engine/base.py:410-492:rollout sleep、非 naive weight sync、KV cache 释放和 resume generation 的同步生命周期。
  • verl/trainer/config/rollout/rollout.yaml:25-3169-79263-284:response length、rollout batching 和 checkpoint engine bucket 配置入口。
  • verl/trainer/config/actor/actor.yaml:10-33114-118:rollout.n、PPO mini-batch、dynamic batch 和 PPO epochs 的配置入口。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-21,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 吞吐下降先回到 step time
  • 2. 同步 PPO 默认先生成,再更新,再同步权重
  • 3. rollout-bound 的证据不只是 timing_s/gen
  • 4. update-bound 要同时看 update_actor和 MFU
  • 5. old_log_prob和 update_weights会伪装成两侧瓶颈
  • 6. 一张实用判定表
  • 小结:瓶颈是同步闭环里的最大等待窗口
  • 本文源码索引
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档