Prhub

#49852 [MRV2][Multimodal] Enable encoder cuda graph for model runner v2

原始 PR 作者 Isotr0py 合并时间 2026-08-14 17:33 文件变更 4 提交数 14 评论 11 代码增减 +101 / -26

执行摘要

MRV2 多模态编码器启用 CUDA Graph 捕获与执行

MRV2 是 vLLM v1 的新一代 GPU model runner 架构,此前编码器前向只能走常规 embed_multimodal 调用,无法复用解码器已具备的 CUDA Graph 能力,导致视觉等编码器场景存在明显的 kernel 启动开销。PR body 的 purpose 明确为 "Enable encoder cuda graph on model runner v2",即补齐 MRV2 编码器的 graph 化能力,使其与解码器捕获流程对齐。

值得精读。重点看 encoder_runner.py 的双路径回退设计、model_states/interface.py 中 manager 生命周期与门控条件,以及 model_runner.py 中 encoder/decoder 捕获的解耦编排。后续可关注编码器捕获是否会被移出 dummy LoRA 上下文,以及 is_captured 状态机在配置变更场景下的行为。

讨论亮点
  • 捕获跳过日志不完整(model_runner.py:855):shen-shanshan 指出原日志只提示 cudagraph_mode,应同时说明 encoder 与 decoder 两套捕获条件;Isotr0py 在 commit 40f48ef 中完成修改,改为分别提示 cudagraph_mm_encodercudagraph_mode
  • is_captured() 每步调用开销疑问(encoder_runner.py:153):shen-shanshan 担心每个 step 迭代中调用 is_captured() 引入额外成本,建议缓存结果;Isotr0py 回应改为检查 graph_pool 是否有效,最终实现为一次 None 比较,开销可忽略。
  • 总体结论:shen-shanshan 最终给出 APPROVED(LGTM),两个讨论线程均以 resolved 收尾。

实现拆解

  1. 管理器构造与门控:在 vllm/v1/worker/gpu/model_states/interface.py 的 ModelState.init 中,引入 supports_encoder_cudagraph 接口判断与 cast(SupportsEncoderCudaGraph),当满足「非 enforce_eager + 配置 cudagraph_mm_encoder + 模型支持编码器图」时创建 EncoderCudaGraphManager,并将其注入 EncoderRunner;否则为 None。该改动把「是否启用」的控制点集中到模型状态层,对上层 ModelRunner 透明。
  2. 编码器执行分流:在 vllm/v1/worker/gpu/mm/encoder_runner.py 中,EncoderRunner 新增 cudagraph_manager 参数与 has_cudagraph()/capture()/clear() 方法;execute_mm_encoder 内按 modality 分组后,若 manager 已捕获(is_captured())且支持该 modality,则优先走 cg_manager.execute(mm_kwargs_batch),否则回退到 self.model.embed_multimodal。回退分支保证了 eager 模式或未捕获场景行为完全不变。
  3. 捕获编排解耦:在 vllm/v1/worker/gpu/model_runner.py 的 capture_model 中,将原先单一的 cudagraph_manager.needs_capture() 判断拆分为 capture_encoder 与 capture_decoder 两个独立条件,并在 maybe_setup_dummy_loras 上下文内先捕获编码器再捕获解码器;跳过日志同时提示 cudagraph_mm_encoder 与 cudagraph_mode 两个配置。shutdown 中对支持多模态输入的模型状态调用 encoder_runner.clear() 释放图资源。
  4. 捕获状态查询:在 vllm/v1/worker/encoder_cudagraph.py 中新增 is_captured(),以 graph_pool 是否为 None 表示是否已有活跃的捕获图池,供执行路径做低开销判断。
  5. 测试与配置配套:本 PR 未新增或修改测试文件,PR body 指定用 tests/models/multimodal/generation/test_vit_cudagraph.py 验证;功能开关为编译配置中的 cudagraph_mm_encoder,需在非 eager 且模型声明支持编码器图捕获时生效。
文件 模块 状态 重要度
vllm/v1/worker/gpu/mm/encoder_runner.py 编码器 modified 7.79
vllm/v1/worker/gpu/model_runner.py 模型运行 modified 7.0
vllm/v1/worker/gpu/model_states/interface.py 模型状态 modified 6.58
vllm/v1/worker/encoder_cudagraph.py 编码器图 modified 5.29

关键符号

EncoderRunner.has_cudagraph EncoderRunner.capture EncoderRunner.clear EncoderRunner.execute_mm_encoder ModelRunner.capture_model ModelRunner.shutdown ModelState.__init__ EncoderCudaGraphManager.is_captured

关键源码片段

vllm/v1/worker/gpu/mm/encoder_runner.py core-logic

执行分流的唯一核心:新增 cudagraph_manager 参数与 has_cudagraph/capture/clear 方法,并在 execute_mm_encoder 中实现 CUDA Graph 执行与 embed_multimodal 回退双路径,是本次功能落地的关键文件。

@torch.inference_mode()
def execute_mm_encoder(
    self, mm_kwargs: list[tuple[str, MultiModalKwargsItem]]
) -> list[torch.Tensor]:
    encoder_outputs: list[torch.Tensor] = []
    for modality, num_items, mm_kwargs_batch in group_and_batch_mm_kwargs(
        mm_kwargs, device=self.device, pin_memory=PIN_MEMORY
    ):
        # 优先尝试 CUDA Graph 执行路径:仅当 manager 已捕获且支持当前 modality 时才走图执行,
        # 否则回退到常规 embed_multimodal,保证 eager 或未捕获场景行为保持一致。
        cg_manager = self.cudagraph_manager
        cudagraph_output = (
            cg_manager.execute(mm_kwargs_batch)
            if cg_manager is not None
            and cg_manager.is_captured()
            and cg_manager.supports_modality(modality)
            else None
        )
        batch_outputs = (
            cudagraph_output
            if cudagraph_output is not None
            else self.model.embed_multimodal(**mm_kwargs_batch)
        )
        sanity_check_mm_encoder_outputs(batch_outputs, expected_num_items=num_items)
        encoder_outputs.extend(batch_outputs)
    return encoder_outputs@torch.inference_mode()
def capture(self) -> None:
    """在 graph_capture 上下文中捕获编码器 CUDA Graph。"""
    manager = self.cudagraph_manager
    assert manager is not None
​
    # graph_capture 提供并行状态下的图捕获上下文;
    # graph_pool_handle 返回平台级内存池,确保捕获的图地址可复用。
    from vllm.distributed.parallel_state import graph_capture
    from vllm.platforms import current_platform
​
    with graph_capture(device=self.device):
        manager.capture(graph_pool=current_platform.graph_pool_handle())
        torch.accelerator.synchronize()def clear(self) -> None:
    """释放编码器 CUDA Graph,避免残留图占用显存。"""
    if self.cudagraph_manager is not None:
        self.cudagraph_manager.clear()
vllm/v1/worker/gpu/model_runner.py data-contract

捕获编排主路径:capture_model 将 encoder/decoder 捕获条件解耦并调整跳过日志,shutdown 增加对编码器图的清理,直接决定功能是否在启动与关停阶段正确生效。

@torch.inference_mode()
def capture_model(self) -> int:
    if self.is_encoder_only:
        return 0
​
    assert self.cudagraph_manager is not None
    # 编码器与解码器的捕获条件相互独立:
    # 编码器需要模型支持 mm 输入且已挂载 cudagraph_manager;
    # 解码器仍由 cudagraph_manager.needs_capture() 决定。
    capture_encoder = (
        self.model_state.supports_mm_inputs
        and self.model_state.encoder_runner.has_cudagraph()
    )
    capture_decoder = self.cudagraph_manager.needs_capture()
    if not capture_encoder and not capture_decoder:
        logger.warning(
            "Skipping encoder and decoder CUDA graph capture. To enable "
            "encoder capture, ensure `cudagraph_mm_encoder` is enabled; "
            "to enable decoder capture, ensure `cudagraph_mode` is not `NONE`."
        )
        return 0
​
    compilation_counter.num_gpu_runner_capture_triggers += 1
    # 编码器捕获先于解码器捕获,统一放在 dummy LoRA 上下文内,
    # 确保捕获期间的权重状态一致。
    with self.maybe_setup_dummy_loras(self.lora_config):
        if capture_encoder:
            self.model_state.encoder_runner.capture()
​
        if capture_decoder:
            self.cudagraph_manager.capture(
                self.model,
                self.model_state,
                self.input_buffers,
                self.intermediate_tensors,
                self.block_tables,
                self.attn_groups,
                self.kv_cache_config,
                has_lora=self.lora_config is not None,
                use_aux_hidden_state_outputs=self.use_aux_hidden_state_outputs,
                lora_capture_hook=create_lora_capture_hook(self.lora_config, self),
            )
            if self.speculator is not None:
                self.speculator.capture()
            # 自适应验证等解码器侧捕获后续逻辑保持原样 ...
    return cuda_graph_size
vllm/v1/worker/gpu/model_states/interface.py data-contract

数据契约与门控:ModelState.__init__ 决定 EncoderCudaGraphManager 是否创建并注入 EncoderRunner,是功能开关的集中控制点,影响所有多模态模型初始化路径。

if encoder_cache is not None:
    # 编码器 CUDA Graph 仅在三个条件同时满足时启用:
    # 1. 非 eager 模式;2. 配置 cudagraph_mm_encoder;3. 模型声明 supports_encoder_cudagraph。
    enable_encoder_cuda_graph = (
        not self.model_config.enforce_eager
        and vllm_config.compilation_config.cudagraph_mm_encoder
        and supports_encoder_cudagraph(model)
    )
    cudagraph_manager = (
        EncoderCudaGraphManager(
            vllm_config=vllm_config,
            device=device,
            dtype=self.dtype,
            model=cast(SupportsEncoderCudaGraph, model),
        )
        if enable_encoder_cuda_graph
        else None
    )
​
    self.encoder_cache = encoder_cache
    observability_config = vllm_config.observability_config
    self.encoder_runner = EncoderRunner(
        model=self.model,
        max_num_tokens=self.max_num_tokens,
        hidden_size=self.inputs_embeds_size,
        encoder_cache=encoder_cache,
        dtype=self.dtype,
        device=self.device,
        cudagraph_manager=cudagraph_manager,
        enable_timing=bool(
            observability_config
            and observability_config.enable_mm_processor_stats
        ),
    )

评论区精华

capture_model 日志未涵盖 encoder 捕获配置 documentation

shen-shanshan 指出捕获跳过日志应同时说明 encoder 与 decoder 的捕获条件,即 `cudagraph_mode` 与 `cudagraph_mm_encoder` 两个配置。

结论:Isotr0py 在 commit 40f48ef 中更新日志,分别提示两个配置。 · 已解决

execute_mm_encoder 每步调用 is_captured() 的开销 性能

shen-shanshan 担心每个 step 迭代中调用 is_captured() 可能引入额外成本,建议缓存结果;并确认 capture() 是否总在真实推理前完成。

结论:Isotr0py 改为直接检查 graph_pool 是否有效,最终 is_captured() 仅做一次 None 比较,开销可忽略。 · 已解决

风险与影响

  • 执行路径回归风险:encoder_runner.execute_mm_encoder 新增 CUDA Graph 分支,若图捕获的 token budget 与真实输入规模不匹配,需依赖 EncoderCudaGraphManager 内部的 padding 或回退处理;若图执行产生语义错误,sanity_check_mm_encoder_outputs 只能拦截数量不一致,无法拦截内容错误。
  • capture 阶段同步开销:EncoderRunner.capture 中包含 torch.accelerator.synchronize(),与 PR#43107 正在推进的 GPU<->CPU 同步点清理方向存在张力;捕获本身低频,影响有限。
  • shutdown 清理顺序:model_runner.shutdown 中在 free_before_shutdown 之前调用 encoder_runner.clear(),若图释放与模型权重生命周期存在隐式依赖,可能引入关停路径回归。
  • 测试覆盖缺口:本 PR 未新增测试文件,回归依赖已有 test_vit_cudagraph.py;门控条件组合(enforce_eager / cudagraph_mm_encoder / supports_encoder_cudagraph)覆盖不足。
  • 用户侧:多模态(视觉编码器)推理在 MRV2 下延迟降低,尤其图像 token 密集、batch 规模稳定的场景;显存占用因图池略微增加。
  • 系统侧:编码器执行路径从单一 embed_multimodal 变为「图执行 + 回退」双路径,新增 is_captured 状态依赖,执行语义取决于捕获是否成功。
  • 团队侧:MRV2 性能能力向 MRV1 看齐的关键一步,后续多模态编码器优化(如消除同步点)可在图路径上直接迭代。
核心路径变更 缺少测试覆盖 CUDA Graph 捕获失败风险 配置耦合

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论