Prhub

#56312 [MRV1] Scope breakable cudagraphs to the piecewise path only

原始 PR 作者 njhill 合并时间 2026-09-12 03:55 文件变更 4 提交数 4 评论 13 代码增减 +165 / -17

执行摘要

Breakable CUDA 图仅接管 PIECEWISE 路径,FULL 恢复标准捕获

PR body 明确指出:VLLM_USE_BREAKABLE_CUDAGRAPH=1 no longer overrides FULL cudagraphs in ModelRunnerV1——在 cudagraph_mode=FULL_AND_PIECEWISE 下,decode 分发改用标准 CUDAGraphWrapper(FULL),breakable 捕获只需替换 PIECEWISE,从而对齐 MRV2 的 use_breakable_cg 门控。此前 breakable 作为实验性路径会整条劫持成熟稳定的 FULL 捕获,既偏离 MRV2 行为,也让 decode 主路径承担了不必要的风险。此外 AndreaasKaratzas 在 review 中强调要避免 #55095 中“选择 piecewise 却没有捕获者”的回归,本次包裹策略调整正是围绕这一目标展开。

值得精读。两个设计决策尤其值得关注:(1)wrapper 的 runtime_mode 门控如何同时服务 MRV1(严格匹配)与 MRV2(默认 match-any),一份代码两种契约;(2)load_model 中“内层 breakable 先装、外层 FULL 再套”的独立 if 结构,如何在不显式依赖 mode 枚举的情况下吸收 drafter 与后期模式升级两类边界情况。结合 review 评论与 #55095 阅读,可以完整理解 vLLM 当前 CUDA Graph 捕获层的演进脉络。

讨论亮点

两条 review 评论均来自合并者 AndreasKaratzas,指出的都是包裹时机边界问题,最终都被合并版本吸收:

  • drafter 在 FULL target 下的 PIECEWISE 分发(正确性,已解决):initialize_cudagraph_keys() 会把 FULL target 下的 drafter 映射为 PIECEWISE,若按中间版本(has_piecewise_cudagraphs)跳过 breakable 安装,drafter 将因 breakable 关闭编译而完全 eager 运行。合并版本对 drafter.model 无条件包裹 runtime_mode=PIECEWISE 的 breakable wrapper,规避该问题。
  • FULL→FULL_AND_PIECEWISE 后期升级(设计,已解决):显式 attention splitting_ops 时,FULL 可能在解析 UNIFORM_BATCH 后端后被升级为 FULL_AND_PIECEWISE,若安装已过则“选中 piecewise 却无捕获者”——即 #55095 想避免的回归。合并版本把 elif 改为独立 if 并保持 breakable 条件为 != NONE,保证 wrapper 在模式升级前就已就位。
  • njhill 收到评论后回复 “Hopefully I have addressed them now”,随后 AndreaasKaratzas 给出 APPROVED 并确认 CI(含 AMD nightly)通过,仅剩一个已知 AMD 失败。

实现拆解

变更入口是 MRV1 模型装配路径 vllm/v1/worker/gpu_model_runner.py::load_model(),配套修改两个 wrapper 实现与单元测试,分四步:

  1. 为 BreakableCUDAGraphWrapper 增加 runtime_mode 门控(vllm/compilation/breakable_cudagraph.py)
    - __init__ 新增可选参数 runtime_mode: CUDAGraphMode | None = None 并存入 self.runtime_mode。
    - __call__ 中,当 cudagraph_runtime_mode == NONE,或显式设置了 runtime_mode 且与当前分发模式不匹配时,直接 eager 调用 self.runnable;仅匹配时才走 _capture/_replay。
    - 默认 None 保留旧的 match-any 行为,保证 MRV2 调用点(不传 runtime_mode)零行为变化;同时允许 MRV1 将 breakable 严格限定到 PIECEWISE。

  2. 重构 load_model 的包裹顺序(vllm/v1/worker/gpu_model_runner.py)
    - breakable 分支条件保持 cudagraph_mode != NONE(而非中间版本收窄为 has_piecewise_cudagraphs()),但包裹时显式传 runtime_mode=PIECEWISE;drafter.model 同样无条件包裹。
    - 将原先的 elif 改为独立 if:当 cudagraph_mode.has_full_cudagraphs() 时,外层再套 CUDAGraphWrapper(..., runtime_mode=FULL)。
    - 这样在 FULL_AND_PIECEWISE 下形成 CUDAGraphWrapper(FULL) -> BreakableCUDAGraphWrapper(PIECEWISE) 嵌套;在纯 FULL 下,主模型的 FULL 由外层标准 wrapper 捕获,而 drafter(只分发 PIECEWISE 或 NONE)仍由内层 breakable 捕获,避免 drafter eager 运行;后期 FULL -> FULL_AND_PIECEWISE 升级(_check_and_update_cudagraph_mode)也能被已安装的 breakable 覆盖。

  3. unwrap() 递归穿透(vllm/compilation/breakable_cudagraph.py 与 vllm/compilation/cuda_graph.py)
    - 两个 wrapper 的 unwrap() 均改为:若 runnable 自身带 unwrap,则递归调用其 unwrap(),否则返回 runnable;保证嵌套后 get_model()/unwrap() 仍能直达原始模型。

  4. 单元测试配套(tests/v1/cudagraph/test_breakable_cudagraph.py,+131)
    - 新增 _mock_vllm_config() 与 4 个测试:runtime_mode 不匹配时 fall through(断言 eager 调用且 entries 为空)、匹配时触发 _capture、默认 None 匹配任意非 NONE 模式(注释明确 MRV2 依赖此行为)、嵌套 wrapper 下 unwrap() 直达原始 model。
    - 测试通过 patch.object(BreakableCUDAGraphWrapper, "_capture") 隔离真实捕获,验证的是 dispatch 契约而非 CUDA 行为,可在无 GPU 环境运行。

文件 模块 状态 重要度
vllm/v1/worker/gpu_model_runner.py 模型加载 modified 6.4
vllm/compilation/breakable_cudagraph.py 编译层 modified 6.34
vllm/compilation/cuda_graph.py 编译层 modified 5.23
tests/v1/cudagraph/test_breakable_cudagraph.py 单元测试 modified 6.82

关键符号

load_model BreakableCUDAGraphWrapper.__init__ BreakableCUDAGraphWrapper.__call__ BreakableCUDAGraphWrapper.unwrap CUDAGraphWrapper.unwrap

关键源码片段

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

MRV1 模型装配入口,包裹策略从 elif 改为独立 if,形成 FULL 标准 wrapper 在外、breakable(PIECEWISE) 在内的嵌套结构,是本 PR 行为变更的落点。

        # wrap the model with full cudagraph wrapper if needed.
        cudagraph_mode = self.compilation_config.cudagraph_mode
        assert cudagraph_mode is not None
        if (
            is_breakable_cudagraph_enabled()
            and cudagraph_mode != CUDAGraphMode.NONE
            and not self.parallel_config.use_ubatching
        ):
            # 限定 PIECEWISE 分发:FULL 由下方标准 wrapper 处理,互不影响。
            # PIECEWISE 分发也可能在包裹之后才出现(例如 FULL target 模式
            # 下的 drafter,或 _check_and_update_cudagraph_mode 后期把
            # FULL 升级为 FULL_AND_PIECEWISE),所以这里无条件先装 breakable。
            self.model = BreakableCUDAGraphWrapper(
                self.model, self.vllm_config, runtime_mode=CUDAGraphMode.PIECEWISE
            )
            drafter = getattr(self, "drafter", None)
            if drafter is not None and hasattr(drafter, "model"):
                # drafter 只会以 PIECEWISE 或 NONE 分发,统一交给 breakable 捕获。
                drafter.model = BreakableCUDAGraphWrapper(
                    drafter.model,
                    self.vllm_config,
                    runtime_mode=CUDAGraphMode.PIECEWISE,
                )
​
        # FULL_AND_PIECEWISE 下外层再套标准 FULL wrapper,形成
        # CUDAGraphWrapper(FULL) -> BreakableCUDAGraphWrapper(PIECEWISE) 的嵌套对。
        if (
            cudagraph_mode.has_full_cudagraphs()
            and not self.parallel_config.use_ubatching
        ):
            self.model = CUDAGraphWrapper(
                self.model, self.vllm_config, runtime_mode=CUDAGraphMode.FULL
            )
        elif self.parallel_config.use_ubatching:
            if cudagraph_mode.has_full_cudagraphs():
                self.model = UBatchWrapper(
                    self.model, self.vllm_config, CUDAGraphMode.FULL, self.device
                )
            else:
                self.model = UBatchWrapper(
                    self.model, self.vllm_config, CUDAGraphMode.NONE, self.device
                )
vllm/compilation/breakable_cudagraph.py core-logic

BreakableCUDAGraphWrapper 增加 runtime_mode 门控与递归 unwrap,实现 PIECEWISE 严格匹配与 MRV2 match-any 双契约。

class BreakableCUDAGraphWrapper:
    """Drop-in replacement for :class:`CUDAGraphWrapper`,使用可分段捕获。
​
    与 CUDAGraphWrapper 相同的分发契约:
      * 无 forward_context 时 eager 执行;
      * 运行时模式为 NONE 或与 runtime_mode 不匹配时 eager 执行;
      * 否则按 batch_descriptor 惰性捕获并按需回放。
​
    设置 runtime_mode 后,其他非 NONE 模式会 fall through 到 runnable,
    从而允许外层再套一个处理其他模式的 CUDAGraphWrapper(如 FULL)。
    """
​
    def __init__(
        self,
        runnable: Callable[..., Any],
        vllm_config: VllmConfig,
        runtime_mode: CUDAGraphMode | None = None,
    ) -> None:
        # runtime_mode=None 表示匹配任意非 NONE 分发模式(MRV2 依赖此默认值);
        # 条目仍按 BatchDescriptor 建键,其本身已编码 batch 形状与均匀性。
        self.runnable = runnable
        self.vllm_config = vllm_config
        self.runtime_mode = runtime_mode
        self.compilation_config = vllm_config.compilation_config
        self.graph_pool = current_platform.get_global_graph_pool()
        self.is_debugging_mode = envs.VLLM_LOGGING_LEVEL == "DEBUG"
        self.entries: dict[BatchDescriptor, _BreakableEntry] = {}
        BreakableCUDAGraphWrapper._all_instances.add(self)
        logger.info_once("Breakable CUDA graph enabled")
​
    def unwrap(self) -> Callable[..., Any]:
        # 递归穿透嵌套 wrapper,直达最底层的原始 runnable。
        runnable = self.runnable
        return runnable.unwrap() if hasattr(runnable, "unwrap") else runnable
​
    def __call__(self, *args: Any, **kwargs: Any) -> Any:
        if not is_forward_context_available():
            return self.runnable(*args, **kwargs)
​
        forward_context = get_forward_context()
        batch_descriptor = forward_context.batch_descriptor
        cudagraph_runtime_mode = forward_context.cudagraph_runtime_mode
​
        # 默认(runtime_mode=None)匹配任意非 NONE 模式;一旦显式设置
        # runtime_mode,其他模式由外层 wrapper(如 FULL 的 CUDAGraphWrapper)
        # 负责捕获,这里直接 eager 调用。
        if cudagraph_runtime_mode == CUDAGraphMode.NONE or (
            self.runtime_mode is not None
            and cudagraph_runtime_mode != self.runtime_mode
        ):
            return self.runnable(*args, **kwargs)
​
        assert batch_descriptor is not None
        entry = self.entries.get(batch_descriptor)
        if entry is None:
            entry = _BreakableEntry(batch_descriptor=batch_descriptor)
            self.entries[batch_descriptor] = entry
​
        if entry.capture is None:
            return self._capture(entry, args, kwargs)
        return self._replay(entry, args, kwargs)

评论区精华

FULL target 下 drafter 需要 breakable 包裹 正确性

AndreaasKaratzas 指出:initialize_cudagraph_keys() 会将 FULL target 模式下的 drafter 映射为 PIECEWISE,但中间版本条件(has_piecewise_cudagraphs)会跳过 breakable 安装;由于 breakable 同时关闭编译,drafter 将退化为 eager 运行。

结论:合并版本对 drafter.model 无条件安装 runtime_mode=PIECEWISE 的 BreakableCUDAGraphWrapper,保证 drafter 的 PIECEWISE 分发始终有捕获者。 · 已解决

FULL→FULL_AND_PIECEWISE 后期升级的包裹时机 设计

AndreaasKaratzas 提醒:breakable 启用且显式 attention splitting_ops 时,FULL 可能在 UNIFORM_BATCH 后端解析 attention 支持后被升级为 FULL_AND_PIECEWISE,届时若 breakable wrapper 已跳过安装,会出现“选择 piecewise 却无捕获者”,即 #55095 想避免的回归。

结论:合并版本将 elif 改为独立 if,且 breakable 条件保持 cudagraph_mode != NONE;wrapper 在模式升级前已安装,运行时按 runtime_mode=PIECEWISE 精确匹配,FULL 由外层标准 wrapper 捕获。 · 已解决

风险与影响

  1. 核心路径变更:load_model 是 MRV1 模型装配的必经之路,包裹顺序变化影响所有开启 VLLM_USE_BREAKABLE_CUDAGRAPH=1 的用户;FULL_AND_PIECEWISE 下新增双层嵌套使 dispatch 链路更长。
  2. 依赖 forward_context 的分发标注:breakable 是否捕获完全由 forward_context.cudagraph_runtime_mode 决定,任何调用方对模式标注不一致(如 NONE 与 PIECEWISE 混用)都会导致意外 eager 回退或捕获空转,形成隐性性能回归。
  3. 测试覆盖缺口:新增测试集中在 wrapper 单元层,缺少 FULL_AND_PIECEWISE 嵌套下的 e2e/集成测试(真实模型加载 + 捕获),模式升级路径 _check_and_update_cudagraph_mode 的行为只能靠代码阅读保证。
  4. unwrap() 的 hasattr 探测:若 runnable 恰好定义了同名 unwrap 属性且语义不同,递归会误触发;当前 wrapper 均为仓库内类,风险低但需留意第三方 runnable。
  5. 编译关闭的连带影响(评论提及 breakable 会关闭编译):纯 FULL 模式下 drafter 被 breakable 化后其编译同样被关闭,需要确认该场景的性能预期。
  • 用户影响:仅 MRV1 + 显式开启 VLLM_USE_BREAKABLE_CUDAGRAPH=1 的部署受影响,行为从“整条路径 breakable”变为“FULL 用标准捕获、PIECEWISE 用 breakable”,与 MRV2 的 use_breakable_cg 行为对齐;未开启该开关的用户零影响。
  • 系统影响:FULL_AND_PIECEWISE 下出现两层 wrapper 嵌套,捕获条目由两个 wrapper 分别持有,内存与 graph pool 使用略增;但消除了 breakable 劫持 FULL 捕获带来的正确性风险。
  • 团队影响:确立 wrapper 嵌套契约(runtime_mode 严格门控 + 递归 unwrap),为后续在单一模型上混合使用 FULL 与 breakable 捕获提供可复用模式;MRV2 采用默认 match-any,两条 runner 路径的行为在此收敛。
核心路径变更 嵌套 wrapper 契约 缺少 e2e 测试覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论