执行摘要
- 一句话:新增 profiler 收尾钩子,重构 torch 与 rollout 全链路 profiling 采集
- 推荐动作:值得精读。三个设计决策值得借鉴:① finish hook 采用“每步 relocate + 末尾一次性执行命令”,避免重复上传累积目录;② torch profiler 的 schedule 粒度从 RL step 改为 update mini-batch,并关闭
ProfilerStep 行来去噪;③ rollout profiler 的 ranks 按全局 GPU rank 映射到 replica,并在 trace 重定位时过滤 tp 同伴,保证只露出用户请求的 rank。建议同时关注 reviewer 对 rollout profiling 必要性的质疑,评估是否应默认关闭并以 Prometheus 替代。
功能与动机
PR body 明确提出目标:add profiler post hook, fix behaviors and allow comprehensive torch profiler。此前 torch profiler 的调度粒度、trace 文件布局和 rollout 引擎的 profiler 产物收集都不完整,用户需要手动从 Ray 固定目录或 per-replica 子目录寻找 trace。本 PR 通过 POST hook 把"收集/上传"自动化,并让 discrete 与 continuous 两种模式都能得到可归属到 role/step 的 Chrome trace。Reviewer 的质疑也反映了需求边界:rollout 引擎是否需要在 RL 框架内做细粒度 profile,还是应由 Prometheus 承担。
实现拆解
- 扩展配置契约(
verl/utils/profiler/config.py):ProfilerConfig 新增 relocate_results、finish_hook_cmd、finish_hook_all_ranks、finish_hook_ranks 字段,并在 union/intersect 中传播这些字段;TorchProfilerScheduleConfig 语义从“step”改为“update mini-batch”,文档明确离散/连续两种模式下 schedule 的取舍;新增 rollout_trace_dir、rollout_profiler_global_ranks、relocate_rollout_traces 等辅助函数,统一 rollout 引擎 trace 目录推导与重定位逻辑。
- 实现 DistProfiler 收尾钩子(
verl/utils/profiler/profile.py):stop(run_command=True) 每次 stop 都调用 _run_finish_hook——每步搬迁 nsys 报告等框架固定目录产物到 save_path,但用户 finish_hook_cmd 只在最后一个 profiled step 执行一次,避免重复上传累积目录;_run_finish_command 通过 /bin/sh -c 执行并导出 VERL_PROFILE_* 环境变量,异常只告警不打断训练。
- 修复 torch profiler 行为(
verl/utils/profiler/torch_profile.py):get_torch_profiler 不再按 role 建子目录,全部 trace 落在 save_path,文件名通过 build_trace_basename 嵌入 role 与 profile_step;CPU activity 始终开启以保证 record_function 阶段标记可见;有 schedule 时设置 prof.record_steps = False 消除 ProfilerStep#<n> 噪声;discrete update 阶段用 _scheduled_mini_batch_range 反推窗口覆盖的 mini-batch 区间并写入 _mb 后缀。
- 打通 trainer 与 rollout 引擎(
verl/trainer/ppo/ray_trainer.py、v1/trainer_base.py、vllm_async_server.py、sglang/trtllm_async_server.py):_stop_profiling 在最后一个 profiled step 传 run_command=True;trainer 同时启停 _rollout_server_managers;vLLM 侧 _should_profile 只对选中 replica 配置引擎 profiler,stop_profile 后调用 relocate_rollout_traces 把 per-replica 子目录中的 trace 按全局 rank 过滤迁移;shared worker group(actor_rollout_wg 与 ref/critic 同对象)只 profile 一次。
- 测试与文档配套:新增
tests/workers/test_engine_workers_mini_batch_trace_on_cpu.py(mini-batch 标注与 step 次数)、tests/trainer/ppo/v1/test_rollout_profiling_on_cpu.py(rollout 引擎驱动与去重)、tests/trainer/test_constants_ppo_on_cpu.py(torch 覆盖 nvtx 注入);大幅扩充 test_server_profiler.py、test_torch_profile.py、test_nvtx_profile.py;同步更新 docs/perf、examples/profile 与 .github/workflows。
关键文件:
verl/utils/profiler/config.py(模块 配置层;类别 source;类型 dependency-wiring;符号 to_torch_kwargs, rollout_trace_dir, build_vllm_profiler_args, rollout_profiler_global_ranks): profiler 配置契约的核心变更点:新增 finish_hook/relocate 字段、schedule 语义改为 mini-batch 粒度,并新增 rollout trace 目录与重定位辅助函数。
verl/utils/profiler/profile.py(模块 分析核心;类别 source;类型 dependency-wiring;符号 _hook_print, stop, run_finish_hook, _run_finish_hook): finish hook 与 relocate 流程的落实地:stop() 改为每步 relocate、末尾一次性执行用户命令,并新增 build_rollout_dist_profiler。
verl/utils/profiler/torch_profile.py(模块 分析调度;类别 source;类型 core-logic;符号 _scheduled_mini_batch_range, _resolve_continuous_schedule_kwargs, _flush_partial_window): torch profiler 的核心行为修复:schedule 按 mini-batch 推进、关闭 ProfilerStep 行、trace 文件名带 role/step、CPU activity 常开。
verl/workers/rollout/vllm_rollout/vllm_async_server.py(模块 引擎集成;类别 source;类型 core-logic;符号 start_profile, _should_profile): rollout 引擎侧集成落地:_should_profile 决定是否驱动引擎 profiler,stop 后重定位 trace 并按全局 rank 过滤。
verl/trainer/ppo/ray_trainer.py(模块 训练器;类别 source;类型 core-logic;符号 _stop_profiling): trainer 侧收尾逻辑:_stop_profiling 决定最后一个 profiled step 才执行 finish 命令。
tests/trainer/ppo/v1/test_rollout_profiling_on_cpu.py(模块 测试;类别 test;类型 test-coverage;符号 _StubTrainer, on_step_end, on_sample_end, _trainer): 新增的关键测试:验证 trainer 驱动 rollout 引擎 profiler、连续模式不重启、shared worker group 只 profile 一次。
tests/workers/test_engine_workers_mini_batch_trace_on_cpu.py(模块 测试;类别 test;类型 test-coverage;符号 _engine, _worker, _record_names, fake_record_function): 新增的关键测试:验证更新循环每个 mini-batch 命名与 profiler.step 次数,前向-only 阶段不推进 profiler。
关键符号:DistProfiler.stop, DistProfiler.run_finish_hook, DistProfiler._run_finish_hook, DistProfiler._run_finish_command, build_rollout_dist_profiler, get_torch_profiler, Profiler._scheduled_mini_batch_range, Profiler._resolve_continuous_schedule_kwargs, Profiler._flush_partial_window, relocate_rollout_traces, rollout_profiler_global_ranks, rollout_trace_dir, vLLMHttpServer._should_profile, vLLMHttpServer.start_profile, vLLMHttpServer.stop_profile, NsightSystemsProfiler.relocate_results, PPOTrainer._stop_profiling, constants_ppo._uses_torch_profiler
关键源码片段
verl/utils/profiler/torch_profile.py
torch profiler 的核心行为修复:schedule 按 mini-batch 推进、关闭 ProfilerStep 行、trace 文件名带 role/step、CPU activity 常开。
# verl/utils/profiler/torch_profile.py
# schedule 下 trace 文件会是一个 mini-batch 窗口而不是整个 stage,
# 依据 prof.step_num 反推当前窗口覆盖的 mini-batch 区间用于文件名。
# 该逻辑在 get_torch_profiler 的闭包内,schedule 为传入的调度参数字典。
def _scheduled_mini_batch_range(step_num: int) -> tuple[int, int]:
# step() 每个 mini-batch 推进一次,因此窗口最后一个 mini-batch 是 step_num - 1;
# 窗口起点由 schedule 反推,而不是用 active 估算,因为 stop() 可能
# 在窗口未满时 flush(update loop 提前结束)。
skip_first = int(schedule.get("skip_first", 0) or 0)
wait = int(schedule.get("wait", 0) or 0)
warmup = int(schedule.get("warmup", 0) or 0)
active = max(int(schedule.get("active", 1) or 1), 1)
last_mb = max(step_num - 1, 0)
cycle_len = wait + warmup + active
cycle = max(last_mb - skip_first, 0) // cycle_len if cycle_len else 0
# 该 cycle 内真正开始记录的位置:跳过 skipped、idle 与 warmup
first_mb = skip_first + cycle * cycle_len + wait + warmup
return min(first_mb, last_mb), last_mb
def _trace_handler(prof):
idx = handler_state["count"]
handler_state["count"] += 1
suffix = ""
if schedule and name_mini_batch_window:
step_num = getattr(prof, "step_num", None)
if isinstance(step_num, int):
first_mb, last_mb = _scheduled_mini_batch_range(step_num)
suffix = f"_mb{first_mb}" if first_mb == last_mb else f"_mb{first_mb}-{last_mb}"
else:
suffix = f"_part{idx}"
elif idx:
suffix = f"_part{idx}"
out_path = os.path.join(save_path, f"{base_file_name}{suffix}.json.gz")
prof.export_chrome_trace(out_path)
# 所有 trace 直接落在 save_path:role 已进入文件名,不再建子目录,
# 避免一个 step 的 trace 散落在多个兄弟目录里,也避免 finish_hook_cmd
# 只看到 save_path 时漏掉部分产物。
# CPU activity 始终开启:verl 用 record_function 标记每个 stage,
# 这些标记是 CPU 侧事件,纯 device trace 无法归属到 stage。
activities = [torch.profiler.ProfilerActivity.CPU]
if not contents or "cuda" in contents:
activities.append(torch.profiler.ProfilerActivity.CUDA)
# 有 schedule 时 prof.step() 每 mini-batch 推进一次,torch 默认会给每个
# step 边界打 ProfilerStep#<n> 行,但 verl 的 step() 只代表一个 update
# mini-batch,这些行只会增加噪声,因此关闭记录但保留调度与保存逻辑。
if schedule:
prof.record_steps = False
verl/workers/rollout/vllm_rollout/vllm_async_server.py
rollout 引擎侧集成落地:_should_profile 决定是否驱动引擎 profiler,stop 后重定位 trace 并按全局 rank 过滤。
# verl/workers/rollout/vllm_rollout/vllm_async_server.py
# rollout profiler 的 ranks 是全局 GPU rank(与训练角色一致),
# 需要映射到持有这些 GPU 的 replica,再决定是否驱动该 replica 的引擎。
def _should_profile(self) -> bool:
"""当前 replica 是否驱动引擎 profiler。"""
return (
self.profiler_controller.check_enable()
and self.profiler_controller.check_this_rank()
and self.profiler_controller.is_discrete_mode()
)
async def start_profile(self, **kwargs):
if self.node_rank != 0:
return
if self._should_profile():
await self.engine.start_profile(**kwargs)
async def stop_profile(self):
if self.node_rank != 0:
return
if self._should_profile():
await self.engine.stop_profile()
# 引擎把 trace 写在 per-replica 子目录里,stop 后按需搬到 save_path,
# 让训练 worker 的 end-of-run 上传一次带走全部 trace;
# 引擎自身不再执行 finish 命令,避免与训练 worker 重复上传同一目录。
relocate_rollout_traces(
self.profiler_controller.config,
self.replica_rank,
self.replica_world_size,
self.profiler_keep_global_ranks,
)
评论区精华
唯一一条 review 评论由 wuxibin89 在 verl/workers/rollout/vllm_rollout/vllm_async_server.py 第 184 行提出:"Do we really need to profile rollout(vllm/sglang) in RL framework? Maybe prometheus metrics is a better option." 这是对"在 RL 框架内细粒度 profile 推理引擎"必要性的设计质疑,建议优先使用 Prometheus 指标。PR 最终仍被 APPROVED,实现保留了 rollout 引擎 profiler 并新增 trace 重定位来收敛产物——说明维护者认可开发期细粒度 profile 与线上监控是不同场景;但评论区没有作者回复,该设计权衡未被完全消解。
- 是否需要在 RL 框架内 profile rollout(vLLM/SGLang) (design): PR 最终 APPROVED 并合入,实现保留 rollout 引擎 profiler 但增加 relocate_results 收敛产物,表明维护者认可开发期 profiling 的价值;作者未在评论区回复,该设计争议未被完全消解。
风险与影响
- 风险:
- 跨模块配置契约变更:
ProfilerConfig 新增字段依赖 union/intersect 传播,若其他调用方直接构造配置而未携带新字段,多角色合并时 finish_hook 配置可能丢失;已有测试覆盖但需排查存量构造点。
- rollout 引擎路径:
vllm_async_server.py 中 _should_profile 只在选中 replica 上配置引擎 profiler,build_vllm_profiler_args 还按 _VLLM_VERSION >= 0.13.0 切换 legacy env 与 CLI 参数,两种通道的兼容性风险较高。
- finish_hook 执行:
_run_finish_command 在每个选中 rank 跑 /bin/sh -c,命令超时或误写可能挂住训练结尾;代码是 best-effort(异常不打断),但输出流式打印仍可能阻塞。
- trace 布局变化:角色从子目录改入文件名,依赖旧路径的脚本会失效;
relocate_rollout_traces 按全局 rank 过滤时对未知命名模式采用保守保留策略,不会丢文件但可能多出 tp 同伴的 trace。
- schedule 语义变化:continuous 模式下 schedule 前移会丢弃 active 窗口之前的 log-prob/rollout 阶段,用户需要理解
skip_first/wait/warmup 的新含义。
- 影响:对用户:profiler 配置方式新增 relocate_results 与 finish_hook_* 字段,trace 输出路径与命名变化,依赖旧布局的脚本需更新;对开启 profiling 的任务,trainer 会额外启停 rollout 引擎 profiler 并做 trace 迁移,带来可接受的 IO/调度开销。对系统:profiler 成为跨 trainer、worker、rollout 引擎的统一生命周期管理,trace 收集自动化后更利于大规模训练的性能复盘。对团队:profiler 子系统 API 规范化,后续 nsys/rollout 等工具的产物收集可复用同一套 relocate/finish_hook 机制。
- 风险标记:跨模块配置契约变更, rollout 引擎路径改造, 新配置字段传播风险, shell 命令执行, trace 布局变化影响脚本
关联脉络
- PR #7422 [rollout] fix: preserve dummy load_format in disaggregated rollout: 同改 vllm_async_server.py、trtllm_async_server.py 与 sglang_rollout 启动链路,与本 PR 的 rollout 引擎 profiler 配置改造处于同一功能线。
- PR #7434 [vllm] fix: vllm always need to resume weights before weight sync: 同改 engine_workers.py 与 vLLM 权重/生命周期逻辑,本 PR 也在 engine_workers.py 中调整了 profiler.step 与 mini_batch 标注。
- PR #7413 [sglang] fix: lora sglang e2e: 同改 async_sglang_server.py 与 sglang_rollout 引擎链路,均属于 rollout 引擎侧启动与行为修复的连续演进。
参与讨论