Prhub

#53884 [Bugfix] Make Gemma4 MTP suppress_tokens masking CUDA-graph-safe

原始 PR 作者 lucianommartins 合并时间 2026-08-26 23:39 文件变更 1 提交数 1 评论 2 代码增减 +14 / -3

执行摘要

修复 Gemma4 MTP suppress_tokens 遮蔽导致 CUDA 图捕获失败

V2 模型运行器成为非 MoE 架构默认后,speculator 会将 drafter 的 prefill 流程(含 compute_logits)捕获进 FULL CUDA graphs。而 Gemma4MTP.compute_logits 原先使用 Python 列表索引(logits[:, ids] = -inf)进行遮蔽,这会产生未固定内存的 H2D 拷贝,在 CUDA 图捕获期间非法。对于 google/gemma-4-12B-it-assistant 这类带非空 suppress_tokens 的草稿模型,引擎初始化会确定性崩溃。

这个 PR 值得快速了解,特别是对关注 CUDA 图兼容性的开发者。它展示了如何处理在 CUDA 图捕获期间避免 H2D 拷贝的常见模式。建议阅读 gemma4_mtp.py 中的改动,并考虑在相关测试中增加覆盖率。

讨论亮点

该 PR 的 review 主要由 maintainer LucasWilkinson 审批,他给出了 LGTM 并感谢修复。Claude 的自动化 review 因 fork 而跳过。没有深入的代码讨论线程。

实现拆解

  1. 在 Gemma4MTP.init 中新增 self._suppress_idx 属性,初始化为 None,并添加注释说明这是为 CUDA 图安全设计的。
  2. 在 load_weights 中,在权重加载完成后(此时设备已真实,非 meta),如果存在 suppress_tokens,则用 torch.tensor 创建设备端 long 类型索引张量,设备从当前参数获取。
  3. 修改 compute_logits,将原来的 Python 列表索引遮蔽改为 logits.index_fill_(1, self._suppress_idx, -float('inf')),与 Gemma4ForConditionalGeneration 的既有模式对齐。
  4. 对于无 suppress_tokens 的草稿,_suppress_idx 保持 None,分支为 no-op,行为不变。
  5. 本 PR 未包含测试文件变更,但作者在 PR 描述中说明已通过单元测试(29/29)和回归测试验证。
文件 模块 状态 重要度
vllm/model_executor/models/gemma4_mtp.py 模型层 modified 6.58

关键符号

Gemma4MTP.compute_logits Gemma4MTP.load_weights

关键源码片段

vllm/model_executor/models/gemma4_mtp.py data-contract

核心修复文件:修改 compute_logits 和 load_weights 以支持 CUDA 图安全。

# gemma4_mtp.py 关键片段(整理后)
​
class Gemma4MTP(nn.Module):
    def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
        # ... 省略其他初始化 ...
        draft_cfg = speculative_config.draft_model_config
        gen_cfg = draft_cfg.try_get_generation_config()
        self._suppress_token_ids = gen_cfg.get("suppress_tokens") if gen_cfg else None
        # 在设备上物化,因为 compute_logits 在 V2 speculator 的 CUDA 图捕获期间运行,
        # 此时用 Python 列表索引会触发未固定的 H2D 拷贝(捕获期间非法)。
        self._suppress_idx: torch.Tensor | None = None
​
    def compute_logits(self, hidden_states: torch.Tensor, spec_step_idx: int = 0) -> torch.Tensor | None:
        if self.masked_embedding is not None:
            logits = self.masked_embedding(hidden_states, self._get_full_lm_head_weight())
        else:
            logits = self.logits_processor(self.lm_head, hidden_states)
        if logits is not None and self._suppress_idx is not None:
            # 使用 index_fill_ 替代 Python 列表索引,避免捕获期间 H2D 拷贝。
            logits.index_fill_(1, self._suppress_idx, -float("inf"))
        return logits
​
    def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
        self._stable_full_lm_head_weight = None
        loader = AutoWeightsLoader(self)
        loaded = loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
        if self._suppress_token_ids:
            # 在权重加载后物化索引,此时设备已确定(非 meta),且早于图捕获。
            self._suppress_idx = torch.tensor(
                self._suppress_token_ids,
                dtype=torch.long,
                device=next(self.parameters()).device,
            )
        return loaded

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

该修复针对 Gemma4 MTP 模型,尤其是带 suppress_tokens 的草稿模型。改动仅在 load_weights 阶段新增一次张量创建,compute_logits 的遮蔽逻辑改为 index_fill_,行为一致,风险较低。但需注意:

1) 若 load_weights 被多次调用(如权重更新),_suppress_idx 会重新创建,但逻辑上没问题;
2) 未直接添加测试文件,依赖作者的手动回归,可能缺乏自动化覆盖;
3) 该修复仅涵盖 Gemma4,其他模型若存在类似模式可能也会受影响。

影响范围局限于 Gemma4 MTP 模型路径,具体是带 suppress_tokens 的草稿模型(如 google/gemma-4-12B-it-assistant)。修复后,V2 运行器下这些模型可以正常进行 CUDA 图捕获,引擎初始化不再崩溃。对无 suppress_tokens 的模型无影响。对用户而言,消除了一个阻碍使用的 bug;对系统而言,增强了 CUDA 图路径的健壮性。对团队而言,提供了一个可复用的模式(在 load_weights 中物化索引)。

缺少自动化测试 特定模型路径 数据契约变更

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论