Prhub

#50417 [Bugfix][Model Runner V2] Restore multimodal draft capability detection

原始 PR 作者 TQCB 合并时间 2026-08-04 05:18 文件变更 12 提交数 8 评论 10 代码增减 +317 / -58

执行摘要

恢复 MRV2 多模态 draft 能力检测

MRV2 重构后,supports_mm_inputs 在 speculator 的 init 阶段基于 draft_model_config 判断,而 draft 配置常被扁平化为纯文本,导致能消费外部多模态 embeddings 的 draft(Eagle/Inkling MTP)在目标模型为多模态时被误判为不支持。PR body 明确指出目标:Separate the target model ability to produce multimodal embeddings from the drafter ability to consume them。讨论中 TQCB 也说明 supports_multimodal 反射对多数 draft 模型不成立、dummy embed_input_ids 试调用是 ad hoc 方案,需要一个显式声明的能力契约。

值得精读。该 PR 展示了如何用『显式协议 + TypeIs 类型守卫』替代运行时试调用探测,把跨模块能力判定收敛到基类 load_model(),并借机删除了 Inkling hack;review 中关于反射 vs 显式声明的讨论(NickLucche、DarkLight1337、TQCB、njhill)有实际工程借鉴价值。建议 MRV2 / spec decode 维护者重点 review:

1) supports_mm_inputs 判定口径的统一;
2) Eagle3 等未标记模型的边界行为;
3) 后续新增 draft 模型时协议声明的约束。

讨论亮点

核心交锋围绕『检测方式』展开:NickLucche 建议用 supports_multimodal 反射替代 dummy forward;TQCB 指出 supports_multimodal 依赖继承 SupportsMultiModal 或显式属性,多数 draft 模型不满足,希望有显式声明契约;DarkLight1337 支持独立协议并最终落实为 SupportsMultiModalEmbeddings。njhill 进一步要求把判定集中到基类 load_model、并据此删除 Inkling model_type hack(已采纳),最终 njhill APPROVED,并请 TheEpicDolphin/benchislett 关注最终结构。无未解决疑虑。

实现拆解

  1. 新增能力协议与类型守卫:在 vllm/model_executor/models/interfaces.py 中新增 SupportsMultiModalEmbeddings 协议,内含 supports_multimodal_embeddings: ClassVar[Literal[True]] 标志与 embed_input_ids 签名;原 SupportsMultiModal 改为继承该协议,保证既有多模态模型天然符合新契约。同时新增带 TypeIs 重载的 supports_multimodal_embeddings() 反射式判断函数(基于 getattr),并在 vllm/model_executor/models/__init__.py 导出,供 spec decode 路径统一使用。

  2. 集中能力判定到基类 load_modelvllm/v1/worker/gpu/spec_decode/speculator.pyDraftModelSpeculator.__init__supports_mm_inputs 默认置 False,判定逻辑移入 load_model():目标端用 MULTIMODAL_REGISTRY.supports_multimodal_inputs(self.vllm_config.model_config),draft 端用 supports_multimodal_embeddings(self.model),两者同时成立才启用多模态输入,否则发出 warning_once 并回退纯文本。这样“目标能否产出”与“draft 能否消费”两个维度彻底分离。

  3. 缓冲分配延迟到能力确定之后AutoRegressiveSpeculatorMultiModuleMTPSpeculator 各自覆写 load_model(),在 super().load_model() 之后按 supports_mm_inputs 分配 inputs_embeds(MTP 还分配 cached_draft_input_embeds);__init__ 中基于 draft_model_config 的旧判定与 Inkling 的 model_type == "inkling_mtp" hack 一并删除;_run_modelsupports_mm_inputs 分支增加 assert self.inputs_embeds is not None 兜底。

  4. draft 模型显式声明能力EagleLlama4ForCausalLMEagleMistralForCausalLMEagleMistralLarge3ForCausalLM 以及 AMD/NVIDIA 两平台的 InklingMTP 显式继承 SupportsMultiModalEmbeddings;纯签名式 draft(如 Eagle3LlamaForCausalLM)保持未标记。此步骤替换了原先“dummy embed_input_ids 试调用”的隐性探测。

  5. 统一旧路径判定vllm/v1/spec_decode/llm_base_proposer.py 中原先对 self.model.embed_input_ids(dummy_input_ids, ...)try/except 探测替换为 supports_multimodal_embeddings(self.model) 检查,保证 V1 两条 spec decode 路径(MRV2 与旧 proposer)判定口径一致。

  6. 测试配套tests/v1/worker/test_gpu_autoregressive_speculator.py 新增 _MultimodalDraftModel / _TextOnlyDraftModel Mock,覆盖“加载后才配置多模态能力”“有能力保持开启”“无能力降级并告警”“MTP 额外缓冲分配”四条路径,并用参数化用例固定各 Eagle/Inkling 模型的能力矩阵(Eagle3LlamaForCausalLM 预期为 False)。

文件 模块 状态 重要度
vllm/model_executor/models/interfaces.py 模型接口 modified 8.21
vllm/v1/worker/gpu/spec_decode/speculator.py 推测解码 modified 6.54
vllm/v1/worker/gpu/spec_decode/autoregressive/speculator.py 自回归草稿 modified 6.58
vllm/v1/worker/gpu/spec_decode/multi_module_mtp/speculator.py 多模块 MTP modified 7.1
vllm/v1/spec_decode/llm_base_proposer.py 提案器 modified 6.0
tests/v1/worker/test_gpu_autoregressive_speculator.py 单元测试 modified 7.45
vllm/model_executor/models/mistral_eagle.py Eagle 模型 modified 6.24
vllm/models/inkling/amd/mtp.py Inkling 模型 modified 5.99

关键符号

SupportsMultiModalEmbeddings supports_multimodal_embeddings DraftModelSpeculator.load_model AutoRegressiveSpeculator.load_model MultiModuleMTPSpeculator.load_model LLMBaseProposer.load_model test_draft_model_multimodal_embedding_capability

关键源码片段

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

新增 SupportsMultiModalEmbeddings 协议与 supports_multimodal_embeddings 类型守卫,是本 PR 能力检测契约的核心,SupportsMultiModal 改为继承新协议。

# vllm/model_executor/models/interfaces.py
# 新增协议:声明模型可以合并外部多模态 embeddings
# 与 SupportsMultiModal 解耦:目标模型是否多模态由 MULTIMODAL_REGISTRY 判断,
# draft 模型能否消费外部 embeddings 则看是否声明本协议。
@runtime_checkable
class SupportsMultiModalEmbeddings(Protocol):
    """The interface for models that can merge external multimodal embeddings."""
​
    # 能力标志,配合 supports_multimodal_embeddings() 类型守卫使用
    supports_multimodal_embeddings: ClassVar[Literal[True]] = True
​
    # 签名与 SupportsMultiModal.embed_input_ids 保持一致
    def embed_input_ids(
        self,
        input_ids: Tensor,
        multimodal_embeddings: MultiModalEmbeddings | None = None,
        *,
        is_multimodal: Tensor | None = None,
    ) -> Tensor: ...
​
​
# SupportsMultiModal 继承新协议,保证所有多模态模型天然具备该能力
@runtime_checkable
class SupportsMultiModal(SupportsMultiModalEmbeddings, Protocol):
    ...
​
​
@overload
def supports_multimodal_embeddings(
    model: type[object],
) -> TypeIs[type[SupportsMultiModalEmbeddings]]: ...
​
​
@overload
def supports_multimodal_embeddings(
    model: object,
) -> TypeIs[SupportsMultiModalEmbeddings]: ...
​
​
def supports_multimodal_embeddings(
    model: type[object] | object,
) -> TypeIs[type[SupportsMultiModalEmbeddings]] | TypeIs[SupportsMultiModalEmbeddings]:
    # 与 supports_multimodal 同款反射模式,只检查能力标志
    return getattr(model, "supports_multimodal_embeddings", False)
vllm/v1/worker/gpu/spec_decode/speculator.py core-logic

DraftModelSpeculator 基类集中能力判定(目标 vs draft),决定 supports_mm_inputs,所有 speculator 共用同一口径。

# vllm/v1/worker/gpu/spec_decode/speculator.py
# DraftModelSpeculator.load_model():加载 draft 模型后集中判定多模态能力
def load_model(self, target_model: nn.Module) -> None:
    target_attn_layer_names = set(
        get_layers_from_vllm_config(
            self.vllm_config,
            AttentionLayerBase, # type: ignore[type-abstract]
        ).keys()
    )
​
    self.model = self.load_draft_model(target_model, target_attn_layer_names)
    self._validate_local_argmax_reduction()
​
    all_attn_layers = set[str](
        get_layers_from_vllm_config(
            self.vllm_config,
            AttentionLayerBase, # type: ignore[type-abstract]
        ).keys()
    )
    self.draft_attn_layer_names = all_attn_layers - target_attn_layer_names
​
    # 能力判定拆成两个维度:
    # 1. 目标模型能否产出多模态输入(MULTIMODAL_REGISTRY)
    # 2. draft 模型能否消费外部多模态 embeddings(显式协议)
    # 只有两者同时成立才把 embeddings 传给 draft,否则回退纯文本。
    target_supports_mm = MULTIMODAL_REGISTRY.supports_multimodal_inputs(
        self.vllm_config.model_config
    )
    draft_supports_mm = supports_multimodal_embeddings(self.model)
    self.supports_mm_inputs = target_supports_mm and draft_supports_mm
    if target_supports_mm and not draft_supports_mm:
        logger.warning_once(
            "Draft model %s does not support external multimodal embeddings. "
            "Embeddings from the target model will not be passed to the "
            "drafter; using text-only draft inputs instead.",
            type(self.model).__name__,
        )
tests/v1/worker/test_gpu_autoregressive_speculator.py test-coverage

新增 Mock draft 模型与参数化能力矩阵测试,覆盖自动回归与 MTP 两类 speculator 的加载期能力配置。

# tests/v1/worker/test_gpu_autoregressive_speculator.py
# Mock draft 模型:声明支持多模态 embeddings,embed_input_ids 在加载期不应被调用
class _MultimodalDraftModel(torch.nn.Module):
    supports_multimodal_embeddings = True
​
    def embed_input_ids(self, input_ids, multimodal_embeddings=None, *, is_multimodal=None):
        raise AssertionError("embed_input_ids should not be called during loading")
​
​
# 文本型 draft:只带 embed_input_ids 签名但未声明能力,应触发回退
class _TextOnlyDraftModel(torch.nn.Module):
    def embed_input_ids(self, input_ids, multimodal_embeddings=None, *, is_multimodal=None):
        raise AssertionError("embed_input_ids should not be called during loading")
​
​
def test_load_model_disables_mm_support_for_text_only_drafter(monkeypatch):
    speculator = object.__new__(_TestSpeculator)
    speculator.supports_mm_inputs = False
    speculator.inputs_embeds = None
    speculator.vllm_config = SimpleNamespace(model_config=object())
    draft_model = _TextOnlyDraftModel()
    speculator.test_draft_model = draft_model
    warning_messages = []
    _mock_base_model_load(monkeypatch)
    monkeypatch.setattr(
        base_spec_module.MULTIMODAL_REGISTRY,
        "supports_multimodal_inputs",
        lambda model_config: True,
    )
    monkeypatch.setattr(
        base_spec_module.logger,
        "warning_once",
        lambda message, *args: warning_messages.append(message % args),
    )
​
    speculator.load_model(torch.nn.Module())
​
    assert not speculator.supports_mm_inputs
    # 关键断言:目标多模态但 draft 不声明能力时,给出可定位的降级告警
    assert warning_messages == [
        "Draft model _TextOnlyDraftModel does not support external multimodal "
        "embeddings. Embeddings from the target model will not be passed to the "
        "drafter; using text-only draft inputs instead."
    ]

评论区精华

能力检测用反射还是显式声明 设计

NickLucche 建议用 supports_multimodal 反射并省去 dummy forward;TQCB 解释 supports_multimodal 依赖继承 SupportsMultiModal 或显式属性,多数 draft 模型不满足,倾向显式声明契约。

结论:最终采用显式 SupportsMultiModalEmbeddings 协议 + supports_multimodal_embeddings 类型守卫。 · 已解决

独立协议还是复用 SupportsMultiModal 设计

DarkLight1337 认为应单独协议并显式继承,而不是在 SupportsMultiModal 上加 flag;TQCB 担忧与 SupportsMultiModal 的继承冲突。

结论:新增独立 SupportsMultiModalEmbeddings 协议,SupportsMultiModal 改为继承它。 · 已解决

Inkling hack 的移除 设计

njhill 建议在 multi_module_mtp/speculator.py 也应用新协议,从而移除 Inkling model_type hack。

结论:已采纳,load_model 集中判定后按需分配缓冲,Inkling hack 删除。 · 已解决

能力判定与缓冲区分配的时机 设计

njhill 建议把 supports_mm_inputs 逻辑全部移入 load_model,避免不必要的缓冲分配。

结论:判定与 inputs_embeds/cached_draft_input_embeds 分配均移到 load_model,_run_model 增加断言。 · 已解决

风险与影响

  1. 契约遗漏风险:新增 draft 模型若未继承 SupportsMultiModalEmbeddings,即使其 embed_input_ids 实际支持外部 embeddings,也会被静默降级为文本模式(仅 warning_once 提示),多模态加速收益丢失而非报错;PR 也明确 Eagle3LlamaForCausalLM 等纯签名式 draft 保持未标记,属于有意为之,但后续若其实现真正消费 embeddings 会产生行为回归。
  2. 缓冲分配时序变化:inputs_embeds/cached_draft_input_embeds__init__ 移到 load_model,任何绕过 super().load_model() 的测试或子类路径会拿到 None;已在 _run_modelassert 暴露问题。
  3. 判定口径一致性:llm_base_proposer.py 去掉 try/except 试调用后,判定完全依赖模型是否声明协议;若某模型误声明协议但未实现 embed_input_ids 语义,会直接进入多模态分支并报错。
  4. 验证缺口:PR 声明未跑 model evals,模型数学未变,但能力开关翻转对真实多模态推理质量的影响缺少端到端验证。

影响范围集中在 V1 + 推测解码路径:MRV2 的 AutoRegressive(Eagle)与 MultiModule MTP(Inkling)两条 speculator 的模型加载流程,以及旧路径 llm_base_proposer.py。对用户而言,修复使多模态目标模型 + 支持 embeddings 的 draft 组合恢复预期行为(Eagle Llama4/Mistral/Mistral Large 3、Inkling MTP),文本 draft 组合的回退路径更明确且带告警;对系统而言,去掉构造期的无条件能力推断与 Inkling hack,减少无效缓冲分配,加载期多一次协议检查(可忽略)。对团队而言,模型接口新增标准契约,后续新增 draft 模型需显式声明,扩展方式更规范;但 SupportsMultiModal 的继承结构变化要求所有多模态模型实现 embed_input_ids 签名,属于轻微接口约束收紧。整体影响面为中高,核心路径 + 契约级别变更,但无模型数学改动。

核心路径变更 契约变更 静默降级为文本模式 draft 模型需同步声明能力 缺少模型级评估

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论