Prhub

#7408 [perf] feat: add profiler post hook, fix behaviors and allow comprehensive torch profiler

原始 PR 作者 ETOgaosion 合并时间 2026-08-19 12:59 文件变更 43 提交数 18 评论 1 代码增减 +3025 / -316

执行摘要

新增 profiler 收尾钩子,重构 torch 与 rollout 全链路 profiling 采集

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 承担。

值得精读。三个设计决策值得借鉴:① 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 替代。

讨论亮点

唯一一条 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 与线上监控是不同场景;但评论区没有作者回复,该设计权衡未被完全消解。

实现拆解

  1. 扩展配置契约verl/utils/profiler/config.py):ProfilerConfig 新增 relocate_resultsfinish_hook_cmdfinish_hook_all_ranksfinish_hook_ranks 字段,并在 union/intersect 中传播这些字段;TorchProfilerScheduleConfig 语义从“step”改为“update mini-batch”,文档明确离散/连续两种模式下 schedule 的取舍;新增 rollout_trace_dirrollout_profiler_global_ranksrelocate_rollout_traces 等辅助函数,统一 rollout 引擎 trace 目录推导与重定位逻辑。
  2. 实现 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_* 环境变量,异常只告警不打断训练。
  3. 修复 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 后缀。
  4. 打通 trainer 与 rollout 引擎verl/trainer/ppo/ray_trainer.pyv1/trainer_base.pyvllm_async_server.pysglang/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 一次。
  5. 测试与文档配套:新增 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.pytest_torch_profile.pytest_nvtx_profile.py;同步更新 docs/perfexamples/profile.github/workflows
文件 模块 状态 重要度
verl/utils/profiler/config.py 配置层 modified 8.84
verl/utils/profiler/profile.py 分析核心 modified 8.57
verl/utils/profiler/torch_profile.py 分析调度 modified 8.41
verl/workers/rollout/vllm_rollout/vllm_async_server.py 引擎集成 modified 7.48
verl/trainer/ppo/ray_trainer.py 训练器 modified 6.95
tests/trainer/ppo/v1/test_rollout_profiling_on_cpu.py 测试 added 7.26
tests/workers/test_engine_workers_mini_batch_trace_on_cpu.py 测试 added 7.38

关键符号

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 core-logic

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_mbdef _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 core-logic

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,
        )

评论区精华

是否需要在 RL 框架内 profile rollout(vLLM/SGLang) 设计

wuxibin89 在 vllm_async_server.py 第 184 行评论:"Do we really need to profile rollout(vllm/sglang) in RL framework? Maybe prometheus metrics is a better option." 意指细粒度引擎 profiling 可能超出 RL 框架职责,Prometheus 指标更合适。

结论:PR 最终 APPROVED 并合入,实现保留 rollout 引擎 profiler 但增加 relocate_results 收敛产物,表明维护者认可开发期 profiling 的价值;作者未在评论区回复,该设计争议未被完全消解。 · approved

风险与影响

  1. 跨模块配置契约变更ProfilerConfig 新增字段依赖 union/intersect 传播,若其他调用方直接构造配置而未携带新字段,多角色合并时 finish_hook 配置可能丢失;已有测试覆盖但需排查存量构造点。
  2. rollout 引擎路径vllm_async_server.py_should_profile 只在选中 replica 上配置引擎 profiler,build_vllm_profiler_args 还按 _VLLM_VERSION >= 0.13.0 切换 legacy env 与 CLI 参数,两种通道的兼容性风险较高。
  3. finish_hook 执行_run_finish_command 在每个选中 rank 跑 /bin/sh -c,命令超时或误写可能挂住训练结尾;代码是 best-effort(异常不打断),但输出流式打印仍可能阻塞。
  4. trace 布局变化:角色从子目录改入文件名,依赖旧路径的脚本会失效;relocate_rollout_traces 按全局 rank 过滤时对未知命名模式采用保守保留策略,不会丢文件但可能多出 tp 同伴的 trace。
  5. schedule 语义变化:continuous 模式下 schedule 前移会丢弃 active 窗口之前的 log-prob/rollout 阶段,用户需要理解 skip_first/wait/warmup 的新含义。

对用户:profiler 配置方式新增 relocate_resultsfinish_hook_* 字段,trace 输出路径与命名变化,依赖旧布局的脚本需更新;对开启 profiling 的任务,trainer 会额外启停 rollout 引擎 profiler 并做 trace 迁移,带来可接受的 IO/调度开销。对系统:profiler 成为跨 trainer、worker、rollout 引擎的统一生命周期管理,trace 收集自动化后更利于大规模训练的性能复盘。对团队:profiler 子系统 API 规范化,后续 nsys/rollout 等工具的产物收集可复用同一套 relocate/finish_hook 机制。

跨模块配置契约变更 rollout 引擎路径改造 新配置字段传播风险 shell 命令执行 trace 布局变化影响脚本

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论