Prhub

#37524 [Hardware][GPU] Profiler config additional to increase it scope and annotation details

原始 PR 作者 devalshahamd 合并时间 2026-07-18 04:39 文件变更 4 提交数 25 评论 17 代码增减 +240 / -26

执行摘要

新增 graph capture 阶段 profiler 和详细 trace annotation 配置

依据 PR body,目的是提高 profiling scope,为 attention 操作提供更详细的 trace 信息,便于分析不同 batch size 下的算子形状。Review 中 rasmith 询问 graph capture 期间 profiling 的价值,作者以 batch size 12 的 captured graph 为例说明可以获取操作的具体 shape。

建议仔细阅读 gpu_model_runner.pycapture_model_warmup_and_capture 的改动,理解如何将 profiler 注入 capture 流程。gpu_worker.py 中 detailed annotation 的 roofline metric 计算方式值得参考。整体设计清晰,类型提示方面的讨论(AbstractContextManager vs ContextManager)也值得关注。

讨论亮点
  1. rasmith 质疑 graph capture profiling 的价值:作者解释了不同 batch size 的 captured graph trace 可以揭示操作的 shape 信息,有助于性能诊断。rasmith 后续建议创建 RFC(已创建 #45069)。
  2. dllehr-amd 建议使用 info_once:避免在每次 graph capture 时重复输出日志,作者已修改。
  3. dllehr-amd 建议确保 bool 类型:避免字符串非空判断导致的意外行为,作者将 capture_torch_profiler 改为 bool 类型。
  4. gemini-code-assist[bot] 指出类型提示不准确profiler 参数可能为 nullcontext,建议使用 typing.ContextManager[Any]。最终代码使用了 AbstractContextManager[Any] | None 并保留了 None 检查。

实现拆解

  1. 配置层 (vllm/config/profiler.py):在 ProfilerConfig 中添加 capture_torch_profiler(bool)和 detailed_trace_annotation(bool)字段,并在 _validate_profiler_config 中增加 capture_torch_profiler 仅当 profiler 为 'torch' 时的校验。
  2. Graph capture profiler 集成 (vllm/v1/worker/gpu_model_runner.py):在 capture_model 中,根据 capture_torch_profiler 和 local_rank==0 的条件,创建 torch.profiler.profile 实例或 nullcontext,将 profiler 对象通过 _capture_cudagraphs 传递给 _warmup_and_capture。在 _warmup_and_capture 中,将 profiler 作为上下文管理器包裹在 _dummy_run 外,并添加 torch.profiler.record_function 标记。
  3. 详细 trace annotation (vllm/v1/worker/gpu_worker.py):在 annotate_profile 中,当 detailed_trace_annotation 启用时,计算每个请求的 seq_len、query_len,并按 context/generation 阶段汇总 sq、sk、sqsq、sqsk 指标,构造详细 annotation 字符串;否则保持原有简单格式。
  4. 测试 (tests/v1/worker/test_gpu_profiler.py):新增 TestAnnotateProfile 测试类(test_simple_format_mixedtest_detailed_format_mixed),验证 annotation 字符串格式正确;新增 test_profiler_entered_during_capture 测试,验证 profiler 在 _warmup_and_capture 中被正确作为上下文管理器进入和退出。
文件 模块 状态 重要度
vllm/config/profiler.py 配置层 modified 6.54
vllm/v1/worker/gpu_model_runner.py 执行器 modified 7.53
vllm/v1/worker/gpu_worker.py 工作进程 modified 7.15
tests/v1/worker/test_gpu_profiler.py 测试 modified 7.06

关键符号

capture_model _warmup_and_capture annotate_profile _validate_profiler_config

关键源码片段

vllm/v1/worker/gpu_model_runner.py core-logic

核心变更:在 capture_model 中条件创建 profiler,并传递到 capture 流程。

# vllm/v1/worker/gpu_model_runner.py ( 变更后 )@instrument(span_name="Capture model")
def capture_model(self) -> int:
    # ... 省略前面的代码
    set_cudagraph_capturing_enabled(True)
​
    # Setup torch profiler for graph capture traces (conditional)
    from vllm.distributed.parallel_state import get_world_group
    local_rank = get_world_group().local_rank
    enable_profiler = (
        local_rank == 0
    ) and self.vllm_config.profiler_config.capture_torch_profiler
    if enable_profiler:
        trace_dir = (
            self.vllm_config.profiler_config.torch_profiler_dir + "/capture_traces"
        )
        profiler = torch.profiler.profile(
            activities=[
                torch.profiler.ProfilerActivity.CPU,
                torch.profiler.ProfilerActivity.CUDA,
            ],
            record_shapes=True,
            profile_memory=True,
            with_stack=True,
            on_trace_ready=torch.profiler.tensorboard_trace_handler(
                trace_dir,
                worker_name=f"graph_capture_rank_{local_rank}",
                use_gzip=True,
            ),
        )
        logger.info_once(
            "Rank %d: Torch profiler enabled for CUDA graph capture, "
            "traces will be saved to: %s",
            local_rank, trace_dir,
        )
    else:
        profiler = nullcontext() # 或 nullcontext() 当不启用时
​
    with self._freeze_gc(), graph_capture(device=self.device):
        # ... 原来的 capture 流程
        for runtime_mode, batch_descs in self.cudagraph_dispatcher.get_capture_descs():
            self._capture_cudagraphs(
                batch_descriptors=batch_descs,
                cudagraph_runtime_mode=runtime_mode,
                profiler=profiler, # 传递 profiler
            )
        # ... encoder graph capture
​
    # ... 后续代码# _warmup_and_capture 接收 profiler 参数
def _warmup_and_capture(
    self,
    desc: BatchDescriptor,
    cudagraph_runtime_mode: CUDAGraphMode,
    profile_seq_lens: int | None = None,
    allow_microbatching: bool = False,
    num_warmups: int | None = None,
    profiler: AbstractContextManager[Any] | None = None,
):
    if profiler is None:
        profiler = nullcontext()
    # ... warmup 和 capture 逻辑
    with (
        profiler, # 作为上下文管理器,期间 profiling 处于活跃状态
        torch.profiler.record_function(
            f"capture_{desc.num_tokens}_{cudagraph_runtime_mode.name}"
        ),
    ):
        self._dummy_run(
            desc.num_tokens,
            cudagraph_runtime_mode=cudagraph_runtime_mode,
            uniform_decode=desc.uniform,
            # ... 其他参数
        )
vllm/v1/worker/gpu_worker.py core-logic

annotate_profile 方法扩展,支持详细 roofline 注释。

# vllm/v1/worker/gpu_worker.py ( 变更后 )def annotate_profile(self, scheduler_output):
    if not self.profiler:
        return nullcontext()
    self.profiler.step()
    iteration_details = compute_iteration_details(scheduler_output)
​
    if self.vllm_config.profiler_config.detailed_trace_annotation:
        # 计算 roofline 指标
        ctx_seq_len_sum = ctx_qq_compute = ctx_qk_compute = 0
        gen_seq_len_sum = gen_qq_compute = gen_qk_compute = 0
        total_scheduled_tokens = 0
        # 构建 num_computed_tokens 映射
        new_req_ids = {r.req_id for r in scheduler_output.scheduled_new_reqs}
        num_computed_tokens_ids = {
            r.req_id: r.num_computed_tokens
            for r in scheduler_output.scheduled_new_reqs
        }
        for req_id, nc in zip(
            scheduler_output.scheduled_cached_reqs.req_ids,
            scheduler_output.scheduled_cached_reqs.num_computed_tokens,
        ):
            num_computed_tokens_ids[req_id] = nc
​
        for req_id, num_tokens in scheduler_output.num_scheduled_tokens.items():
            query_len = num_tokens
            total_scheduled_tokens += query_len
            seq_len = num_computed_tokens_ids.get(req_id, 0) + query_len
            if (
                scheduler_output.scheduled_cached_reqs.is_context_phase(req_id)
                or req_id in new_req_ids
            ):
                ctx_seq_len_sum += seq_len
                ctx_qq_compute += query_len * query_len
                ctx_qk_compute += query_len * seq_len
            else:
                gen_seq_len_sum += seq_len
                gen_qq_compute += query_len * query_len
                gen_qk_compute += query_len * seq_len
        annotation = (
            f"execute_{total_scheduled_tokens}"
            f"_context_{iteration_details.num_ctx_requests}"
            f"(sq{iteration_details.num_ctx_tokens}"
            f"sk{ctx_seq_len_sum}"
            f"sqsq{ctx_qq_compute}"
            f"sqsk{ctx_qk_compute})"
            f"_generation_{iteration_details.num_generation_requests}"
            f"(sq{iteration_details.num_generation_tokens}"
            f"sk{gen_seq_len_sum}"
            f"sqsq{gen_qq_compute}"
            f"sqsk{gen_qk_compute})"
        )
    else:
        annotation = (
            f"execute_context_{iteration_details.num_ctx_requests}"
            f"({iteration_details.num_ctx_tokens})"
            f"_generation_{iteration_details.num_generation_requests}"
            f"({iteration_details.num_generation_tokens})"
        )
    return self.profiler.annotate_context_manager(annotation)
tests/v1/worker/test_gpu_profiler.py test-coverage

新增针对两个配置的单元测试,验证 annotation 格式和 profiler 上下文管理。

# tests/v1/worker/test_gpu_profiler.py ( 新增部分 )class TestAnnotateProfile:
    """Tests for Worker.annotate_profile() annotation string formatting."""
​
    def _annotate(self, detailed: bool) -> str:
        worker = MagicMock()
        worker.vllm_config.profiler_config.detailed_trace_annotation = detailed
        worker.profiler = MagicMock()
​
        ctx_req = MagicMock(req_id="ctx1", num_computed_tokens=0)
        cached = CachedRequestData(
            req_ids=["gen1"],
            resumed_req_ids=set(),
            new_token_ids=[],
            all_token_ids={},
            new_block_ids=[],
            num_computed_tokens=[10],
            num_output_tokens=[1],
        )
        sched = MagicMock(
            scheduled_new_reqs=[ctx_req],
            scheduled_cached_reqs=cached,
            num_scheduled_tokens={"ctx1": 4, "gen1": 1},
        )
​
        Worker.annotate_profile(worker, sched)
        return worker.profiler.annotate_context_manager.call_args[0][0]
​
    def test_simple_format_mixed(self):
        assert self._annotate(detailed=False) == (
            "execute_context_1(4)_generation_1(1)"
        )
​
    def test_detailed_format_mixed(self):
        # ctx1: sq=4, sk=4, sqsq=16, sqsk=16 | gen1: sq=1, sk=11, sqsq=1, sqsk=11 | bs=5
        assert self._annotate(detailed=True) == (
            "execute_5_context_1(sq4sk4sqsq16sqsk16)_generation_1(sq1sk11sqsq1sqsk11)"
        )
​
​
def test_profiler_entered_during_capture():
    """Verifies profiler is entered as context manager in _warmup_and_capture."""
    runner = MagicMock()
    runner.compilation_config.cudagraph_num_of_warmups = 0
    mock_profiler = MagicMock()
​
    GPUModelRunner._warmup_and_capture(
        runner,
        desc=MagicMock(num_tokens=4, uniform=True),
        cudagraph_runtime_mode=CUDAGraphMode.FULL,
        profiler=mock_profiler,
    )
​
    mock_profiler.__enter__.assert_called_once()
    mock_profiler.__exit__.assert_called_once()

评论区精华

Graph capture profiling 的价值 设计

rasmith 提问:graph capture 阶段的 profiling 有什么用处?devalshahamd 解释:不同 batch size 的 captured graph 可以获取操作 shape 信息,有助于性能分析。

结论:作者认可了价值,并进一步创建了 RFC issue #45069。 · 已解决

变量命名和类型安全性 正确性

dllehr-amd 建议确保 capture_torch_profiler 是 bool 类型,避免字符串非空判断;同时建议使用 info_once 减少日志。devalshahamd 采纳了建议。

结论:字段改为 bool,日志改为 info_once。 · 已解决

类型提示不准确 style

gemini-code-assist[bot] 指出 profiler 参数类型不准确,可能为 nullcontext,建议使用 ContextManager。

结论:最终代码使用 `AbstractContextManager[Any] | None`,并保留 None 检查,部分采纳。 · partially resolved

风险与影响

新配置均默认关闭,不影响现有默认行为。风险点:

  • capture_torch_profiler 启用后会在 GPU 上产生额外 profiling 开销,但仅在 graph capture 阶段,不影响运行时。
  • 路径相关:torch_profiler_dir 必须为有效路径,且需有写入权限,否则可能导致启动失败。
  • 测试覆盖:新增测试覆盖了 annotation 格式和 profiler 上下文管理,但未覆盖复杂的多 rank 场景。
  • 用户:可获得更丰富的 profiling 信息,可选择开启 capture 阶段 trace 和详细 roofline annotation。配置向后兼容。
  • 系统:graph capture 时若开启 profiler 会增加少量时间(trace 序列化),但仅发生在启动阶段。运行时 annotation 字符串长度增加,但对性能影响可忽略。
  • 团队:提升了 profiling 诊断能力,便于定位 attention 算子瓶颈。设计模式(profiler 作为上下文参数传递)可供其他阶段复用。
新配置默认关闭 profiler 路径有效性需验证 graph capture 阶段 overhead 仅影响启动 缺少多 rank 环境下的集成测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论