Prhub

#51084 [Profiler] Add Proton CUDA graph attribution for MRV2

原始 PR 作者 Luosuu 合并时间 2026-09-15 03:02 文件变更 8 提交数 6 评论 23 代码增减 +657 / -45

执行摘要

Proton 支持 CUDA Graph 归因,MRV2 剖析回放内核

PR body 明确指出原有限制:Proton profiling 此前必须禁用 CUDA graphs(--enforce-eager),因为 graph 回放的内核无法归因,"otherwise replay kernels would be silently omitted"。该 PR 将 vLLM 的 Proton 集成从 #48789 基础上扩展出 cudagraph attribution 能力,使默认开启 CUDA graphs 的服务也能得到可读的内核级 profile。作者还举出真实事故:periodic_flushing 后端与 wrapper 的 phase 所有权冲突会导致 SIGABRT,因此必须提前在配置层拒绝该组合。

值得精读,特别是三处设计:一是 VllmConfig._validate_profiler_config() 的校验时机选择(在所有解析/归一化之后),这是配置门控的最佳实践;二是 ProtonProfilerWrapper 用 phase API 分离 capture 活动与 profiling 数据的生命周期管理模式,可复用于其他异步采集后端;三是"单一 owner"原则如何驱动了 periodic_flushing 的互斥设计。建议后续跟进两个未解决的 Codex 边界(活跃期重捕获、encoder-only 判定),并考虑为 capture_cuda_graphs 增加活跃间隔保护。

讨论亮点

Codex 自动 review 与维护者人工 review 形成了几个关键交锋:

  • MRV1 上 attribution 是否静默失效:Codex 提出 P1,指 MRV1 下 _get_cudagraph_capture_context 早退会让 proton_graph_attribution=true 产出无归因的普通 profile,属误导性结果。Luosuu 先回复 "this is intended",最终通过 _validate_profiler_config 对 MRV1 + attribution 显式抛错,把静默行为变为硬性拒绝。

  • Triton 版本检查用 assert 不可靠:Codex 指出 python -O 下 assert 被移除,Triton 3.6 会被放行、直到调用 advance_phase 才失败;最终改为 _require_triton_version() 显式异常。

  • set_output_name 幂等性:Codex 指出重复 start_profile 会改变已激活间隔的输出前缀;最终改为 if self._active: return 并在 _start 时才落定 _active_output_path,配套 test_duplicate_start_preserves_output_prefix。

  • encoder-only 捕获遗漏:Codex 指出 decoder 无 graph 时 encoder 捕获不会被观察;最终通过 needs_cudagraph_capture() 同时纳入 encoder 的 has_cudagraph() 判定解决。

  • periodic_flushing 相位所有权冲突:PR body 说明 wrapper 清空 capture phase 后,Proton 原生 periodic 后端仍会尝试导出该 phase 并 abort;结论是"phase 导出与清理必须只有一个 owner",在配置层拒绝组合并把原 SIGABRT 复现改为配置错误退出。

  • 仍未解决的两个边界:Codex 提出活跃 profiling 间隔期间发生 graph 重捕获(如 elastic EP 重配置)会截断当前 phase;以及 encoder-only worker 在 cudagraph_mm_encoder=true、cudagraph_mode=NONE 时会被 has_cuda_graphs 误判。最终代码中未见对应处理,属于遗留边界。

实现拆解

实现按 5 个步骤拆解:

  1. 配置模型扩展(vllm/config/profiler.py):新增 proton_graph_attribution: bool = False 字段,并将它纳入 ProfilerConfig 的非默认选项校验;同时增加两条互斥约束:attribution 要求 proton_data='tree',且与 periodic_flushing(大小写不敏感匹配)互斥,原因是 wrapper 与 Proton 原生 phase 管理器都拥有 phase 导出权,重复导出会导致 abort。

  2. 配置解析时序修正(vllm/config/vllm.py):把原来内联在 VllmConfig.__post_init__ 的 Proton 校验拆成独立的 _validate_profiler_config(),并移动到 _validate_v2_model_runner() 之后执行,确保默认 cudagraph 模式、平台归一化、runner 选择都已解析完毕。新校验覆盖:非 CUDA 拒绝;attribution + MRV1 拒绝;decoder 或 encoder 任一启用 CUDA graphs 时强制要求 attribution;pcsampling 在启用 graphs 时拒绝(大小写不敏感)。

  3. Worker 侧捕获观察点(vllm/v1/worker/gpu_worker.py):新增 _get_cudagraph_capture_context(),在 compile_or_warm_up_model() 中把 capture_model() 包进 context manager。仅当 V2 runner 且 needs_cudagraph_capture() 为真时,通过 create_graph_capture_profiler() 创建捕获期 Proton wrapper;否则返回 nullcontext(),保证普通推理路径零开销。GPUModelRunner 与 MMEncoderModelRunner 都补充 needs_cudagraph_capture() 方法,统一暴露 decoder 的 cudagraph_manager.needs_capture() 与 encoder 的 encoder_runner.has_cudagraph() 判定。

  4. wrapper 的 session/phase 生命周期(vllm/profiler/wrapper.py):ProtonProfilerWrapper 新增 capture_cuda_graphs() context manager,首次捕获时以隐藏路径(.proton_cuda_graph_session_...)创建独立 session,捕获结束用 data.advance_phase() + data.clear() 丢弃捕获活动;start_profile/stop_profile 期间复用该 session,每个间隔推进一个新 phase 并通过 _write_graph_phase() 导出为独立的 rank-qualified 文件,shutdown() 才 finalize session 并清理隐藏文件;set_output_name() 在非活跃时期更新下一次输出的前缀。基类 WorkerProfiler 同步增加 has_cuda_graph_session 属性与 capture_cuda_graphs() 默认 no-op hook,供其他 backend 扩展。

  5. 测试与文档(tests/v1/worker/test_gpu_profiler.py、docs/contributing/profiling.md):新增约 393 行测试,覆盖 decoder/encoder × attribution 组合、默认 cudagraph 模式解析后校验、periodic_flushing 与 pcsampling 拒绝(含混合大小写)、MRV1 限制、Triton 3.6/3.7 版本门控、graph phase 写出与 interval 隔离、重复 start 幂等、导出失败后可恢复。文档重写 Proton 章节,给出 proton_graph_attribution 用法并说明 pcsampling/Chrome trace 仍需禁用 graphs。

文件 模块 状态 重要度
vllm/profiler/wrapper.py 剖析器 modified 8.67
tests/v1/worker/test_gpu_profiler.py 剖析器测试 modified 7.52
vllm/config/vllm.py 配置校验 modified 7.13
vllm/v1/worker/gpu_worker.py GPU 执行 modified 6.65
vllm/config/profiler.py 配置校验 modified 5.83
vllm/v1/worker/gpu/model_runner.py 模型运行器 modified 5.78
vllm/v1/worker/mm_encoder_model_runner.py 编码器运行器 modified 4.95
docs/contributing/profiling.md 开发文档 modified 2.85

关键符号

WorkerProfiler.capture_cuda_graphs WorkerProfiler.has_cuda_graph_session ProtonProfilerWrapper.capture_cuda_graphs ProtonProfilerWrapper._write_graph_phase ProtonProfilerWrapper._finalize_session ProtonProfilerWrapper.set_output_name ProtonProfilerWrapper.shutdown create_graph_capture_profiler VllmConfig._validate_profiler_config GPUWorker._get_cudagraph_capture_context GPUModelRunner.needs_cudagraph_capture MMEncoderModelRunner.needs_cudagraph_capture

关键源码片段

vllm/config/vllm.py dependency-wiring

配置校验核心:把 Proton 校验从 __post_init__ 内联逻辑抽为独立 _validate_profiler_config(),并移到 runner 选择与 cudagraph 模式解析之后执行,是 attribution 约束(MRV1 拒绝、graphs 必须开 attribution、pcsampling 互斥)的落点。

# VllmConfig 中独立的 Proton 配置校验。
# 关键点:必须在 graph 默认值、平台归一化与 runner 选择之后执行,
# 才能捕获默认 cudagraph_mode 解析出来的组合。
​
    def _validate_profiler_config(self) -> None:
        if self.profiler_config.profiler != "proton":
            return
​
        from vllm.platforms import current_platform
​
        if not current_platform.is_cuda():
            raise ValueError("The Proton profiler currently supports NVIDIA CUDA only")
        if (
            self.profiler_config.proton_graph_attribution
            and not self.use_v2_model_runner
        ):
            # MRV1 无法保留捕获期 session,直接拒绝避免产出
            # 无归因的误导性 profile。
            raise ValueError("Proton CUDA graph attribution requires the V2 model runner.")
​
        has_cuda_graphs = (
            self.compilation_config.cudagraph_mode != CUDAGraphMode.NONE
            or (
                self.compilation_config.cudagraph_mm_encoder
                and not (self.model_config and self.model_config.enforce_eager)
            )
        )
        if not has_cuda_graphs:
            return
        mode = self.profiler_config.proton_mode
        if mode and mode.split(":", 1)[0].lower() == "pcsampling":
            # PC sampling 会同步 CUDA 上下文,与 graph 回放互斥。
            raise ValueError(
                "Proton PC sampling requires CUDA graphs to be disabled. "
                "Use --enforce-eager."
            )
        if not self.profiler_config.proton_graph_attribution:
            # 有 graph 而不开 attribution,回放内核会被静默遗漏。
            raise ValueError(
                "Proton profiling with CUDA graphs requires "
                "proton_graph_attribution=True to capture replayed kernels. "
                "Enable attribution or use --enforce-eager."
            )

评论区精华

MRV1 上 attribution 静默失效 正确性

Codex P1:VLLM_USE_V2_MODEL_RUNNER=0 时 `_get_cudagraph_capture_context` 早退,`proton_graph_attribution=true` 会产出无归因的普通 profile。Luosuu 回复 "this is intended"。

结论:最终通过 `_validate_profiler_config` 对 MRV1 + attribution 显式抛 ValueError,把静默行为改为硬性拒绝。 · 已解决

PC sampling 需独立于 capture 入口校验 正确性

Codex P2:原实现只在 capture_cuda_graphs 内检查 pcsampling,attribution 关闭或 V1 runner 早退时会绕过该守卫。

结论:统一搬到 `_validate_profiler_config`,与 cudagraph_mode 组合独立校验,并覆盖混合大小写 mode 名。 · 已解决

Triton 版本检查用 assert 在 python -O 下失效 正确性

Codex P2:PYTHONOPTIMIZE=1 时 assert 被移除,Triton 3.6 会被放行,直到调用 phase API 才失败。

结论:改用 `_require_triton_version()` 显式 RuntimeError,Triton 3.6 下构造 wrapper 即拒绝。 · 已解决

重复 start_profile 破坏输出名幂等性 正确性

Codex P2:`set_output_name` 在重复 start 时会覆盖已激活间隔的输出前缀,或对运行中的 profiler 抛错。

结论:改为 `if self._active: return`,输出名在 `_start` 时才落定为 `_active_output_path`,并补幂等测试。 · 已解决

encoder-only CUDA graph 捕获未被归因 正确性

Codex P2:decoder 无 graph 时 `_get_cudagraph_capture_context` 早退,encoder 捕获(cudagraph_mm_encoder=true)不被 Proton 观察。

结论:新增 `needs_cudagraph_capture()` 合并 encoder 的 `has_cudagraph()` 判定,测试参数化 encoder_only 覆盖。 · 已解决

encoder-only worker 被误判为 has_cuda_graphs 正确性

Codex P2:`cudagraph_mode=NONE` + `cudagraph_mm_encoder=true` 时 `_validate_profiler_config` 仍要求 attribution,而 MMEncoderModelRunner 实际无 graph 可捕获。

结论:最终代码未见 `is_mm_encoder_only` 排除逻辑,属于遗留边界,可能导致 encoder-only 部署被过度约束。 · 待处理

活跃 profiling 期间 graph 重捕获截断数据 正确性

Codex P2:elastic EP 重配置触发 `warm_and_capture` 时会复用活跃 session,退出时推进并清空当前 phase,使进行中的 interval 丢失数据。

结论:最终代码未在 `capture_cuda_graphs` 中检查 `_running`,该边界未处理也未测试覆盖。 · 待处理

风险与影响

主要风险集中在 vllm/profiler/wrapper.py 的 phase/session 生命周期:

  • graph 重捕获截断:若在 start_profile 活跃期间发生 CUDA graph 重捕获(elastic_execute 的 warm_and_capture 路径),capture_cuda_graphs() 未检查 _running,会推进并清空当前 phase,导致 /stop_profile 产出空或截断的 profile。该边界未被测试覆盖。

  • 相位所有权契约脆弱:wrapper 假定自己是 phase 的唯一 owner,periodic_flushing 已在 vllm/config/profiler.py 拒绝,但后续新增 Proton 后端若再管理 phase,容易重蹈 SIGABRT 覆辙。

  • 版本门控:attribution 依赖 Triton >= 3.7 的 phase data API,依赖版本不满足时会在 wrapper 构造期报错;普通 eager profiling 保持 3.6 兼容,但有两条路径的版本分支需要持续维护。

  • encoder-only 误判:_validate_profiler_config 的 has_cuda_graphs 未排除 is_mm_encoder_only,encoder-only 部署可能被要求开启 attribution,尽管 MMEncoderModelRunner.capture_model() 恒返回 0。

  • 行为断裂:开启 CUDA graphs 且未开 attribution 的 Proton 老用法从"启动即报错"变为"更晚的配置校验报错",错误信息比原来更具体,但用户升级后仍需调整配置;MRV1 + attribution 则被硬性拒绝,无法静默降级。

对用户:性能工程工作流显著受益——此前 Proton 只能在 --enforce-eager 下使用,现在默认 CUDA graph 配置即可获得带内核归因的 Hatchet/MessagePack profile,且支持多个 start_profile/stop_profile 间隔独立导出。对系统:推理主路径不受影响,WorkerProfiler.capture_cuda_graphs() 默认返回 nullcontext(),仅 Proton + attribution 组合在捕获期多一次 session 维护。对团队:WorkerProfiler 抽象新增两个扩展点(has_cuda_graph_session、capture_cuda_graphs),为未来 profiling backend(如支持 periodic 生命周期的后端)提供了明确契约;同时 VllmConfig 校验时序的调整(移到 runner 选择之后)是一个值得全仓库复用的模式。影响范围主要集中在 profiler 工具链,不涉及模型执行、调度或 KV 缓存路径。

相位所有权冲突 Triton 版本门控 MRV1 不支持 graph 重捕获边界未覆盖 encoder-only 判定过严

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论