Prhub

#50734 [Bugfix][Model] Fix Qwen3.5 MTP for text-only checkpoints

原始 PR 作者 efschu 合并时间 2026-08-10 19:04 文件变更 3 提交数 5 评论 8 代码增减 +55 / -7

执行摘要

修复 Qwen3.5 纯文本 checkpoint 的 MTP 投机解码配置断路

PR body 指出「Two gaps that keep --speculative-config '{"method":"mtp",...}' from working on Qwen3.5 checkpoints that ship only the text config」:缺口一是 #50210 把 qwen3_5_text / qwen3_5_moe_text 注册进 _CONFIG_REGISTRY 后,纯文本 checkpoint 可以走普通生成,但 SpeculativeConfig.hf_config_override 只匹配 qwen3_5 / qwen3_5_moe,MTP draft 架构永远无法解析;缺口二是 Qwen3_5MTP / Qwen3_5MoeMTP 继承 SupportsMultiModal 却未注册 processor,启动时触发「treated as multimodal but has no registered multimodal processor」的误导性告警(该部分在 review 中被作者修正为注册表已有的预期回退行为)。

值得精读。改动虽小,但 review 过程展示了如何避免过度修复:作者最初朝「补全 processor 注册」的方向修,审核者引导后回到注册表既有回退机制,并沉淀出针对 draft runner 的告警分级。核心看点:hf_config_override 的 model_type 匹配如何与 _CONFIG_REGISTRY 的扩展保持同步,以及 MultiModalRegistry 对「声明多模态但无 processor」模型的预期回退语义。

讨论亮点

review 中最有价值的交锋发生在「是否给 MTP 模型注册多模态 processor」上:

  • 审核者 DarkLight1337 质疑「Could you show the stack trace for this? Perhaps we could solve this inside the multi-modal registry itself instead of having to edit the model definition」,引导作者重新验证问题描述。
  • 作者 efschu 核实后承认「my registration in fact made things worse, flipping supports_multimodal_inputs() to True for the draft config and pushing a text-only drafter through the full multi-modal path」,撤销模型定义改动,并指出 supports_multimodal_inputs 本就会捕获缺失 processor 并回退文本模式。
  • 对于遗留的启动警告,DarkLight1337 拍板「Let's do it in this PR as well」,将告警静默并入本 PR。
  • 最后 reviewer 要求删除警告分支的新增测试:「Let's remove this test, the warning is basically by definition anyway」,作者在 5744b78 照做。

实现拆解

  1. 扩展 MTP 配置重写匹配(vllm/config/speculative.py):将 SpeculativeConfig.hf_config_override 中 Qwen3.5 分支的 model_type 匹配从 ("qwen3_5", "qwen3_5_moe") 扩展为四种类型,is_moe 判定同步纳入 qwen3_5_moe_text,确保纯文本 checkpoint 与多模态版本一样被重写为 qwen3_5_mtp 及对应 draft 架构(Qwen3_5MTP / Qwen3_5MoeMTP)。这是 --speculative-config 能解析出 draft 架构的入口修复。
  2. 静默 draft 模型的预期回退(vllm/multimodal/registry.py):MultiModalRegistry.supports_multimodal_inputs 捕获到缺失 processor 的 ValueError 时,仅对 runner_type != "draft" 的模型打印警告。MTP 类 draft 模型声明 SupportsMultiModal 是为了消费 target 模型合并后的 embeddings,从不运行自身 processor,文本模式是预期结果。该改动同时惠及 Exaone4_5_MTP、MiMoV2OmniMTP 等同类 draft 模型,返回行为不变。
  3. 新增 CPU-only 参数化测试(tests/models/test_qwen3_5_mtp_config.py):_mtp_config 构造携带 mtp_num_hidden_layers=1 的 PretrainedConfig,参数化覆盖四种 model_type 到期望 MTP 架构的映射,断言 n_predict == 1 且架构名正确。测试不下载 checkpoint,纯配置层验证。
  4. 撤销最初的多模态 processor 注册尝试(过程性):首个 commit 曾给 Qwen3_5MTP / Qwen3_5MoeMTP 注册 Qwen3VLMultiModalProcessor 三件套,review 证实注册反而让 supports_multimodal_inputs 返回 True、把纯文本 draft 模型推入完整多模态路径,遂在 9c56a8e 撤销,模型定义保持上游原样;另按 reviewer 意见删除了 tests/multimodal/test_registry.py 中针对警告分支的测试(警告与分支绑定,测试无额外信号)。
    无配置、schema 或部署配套改动。
文件 模块 状态 重要度
vllm/config/speculative.py 配置层 modified 6.26
vllm/multimodal/registry.py 多模态 modified 5.92
tests/models/test_qwen3_5_mtp_config.py 测试 added 5.94

关键符号

SpeculativeConfig.hf_config_override MultiModalRegistry.supports_multimodal_inputs test_mtp_override_recognizes_text_only_types _mtp_config

关键源码片段

vllm/config/speculative.py core-logic

核心修复文件:SpeculativeConfig.hf_config_override 原先只匹配 qwen3_5 / qwen3_5_moe,漏掉 #50210 注册的 qwen3_5_text / qwen3_5_moe_text 纯文本类型,导致文本 -only checkpoint 无法被重写为 qwen3_5_mtp draft 配置,MTP 投机解码无法解析 draft 架构。

# vllm/config/speculative.py - SpeculativeConfig.hf_config_override 中
# 针对 Qwen3.5 系列的分支(PR 50734 修改后)。
#
# #50210 让纯文本 checkpoint 以 qwen3_5_text / qwen3_5_moe_text 类型加载,
# 它们与多模态版本携带同样的 mtp_num_hidden_layers 字段,因此必须一起被
# 重写为 qwen3_5_mtp,否则 MTP 投机解码无法解析 draft 架构。
if hf_config.model_type in (
    'qwen3_5',
    'qwen3_5_moe',
    'qwen3_5_text',
    'qwen3_5_moe_text',
):
    # MoE 判定需覆盖 qwen3_5_moe_text,避免纯文本变体被误判成 dense 架构。
    is_moe = hf_config.model_type in ('qwen3_5_moe', 'qwen3_5_moe_text')
    hf_config.model_type = 'qwen3_5_mtp'
    n_predict = getattr(hf_config, 'mtp_num_hidden_layers', None)
    hf_config.update({
        'n_predict': n_predict,
        'architectures': ['Qwen3_5MoeMTP' if is_moe else 'Qwen3_5MTP'],
    })
vllm/multimodal/registry.py core-logic

修改 MultiModalRegistry.supports_multimodal_inputs,对 runner_type == 'draft' 的模型静默缺 processor 的预期回退,消除所有 MTP draft 模型(Qwen3_5MTP、Exaone4_5_MTP、MiMoV2OmniMTP)启动时的误导性警告;这是 review 中确认的正确设计方向。

# vllm/multimodal/registry.py - MultiModalRegistry.supports_multimodal_inputs
# 的异常回退分支(PR 50734 修改后)。
try:
    info = self._create_processing_info(model_config, tokenizer=None)
except ValueError:
    # 投机解码 draft 模型(Qwen3_5MTP、Exaone4_5_MTP、MiMoV2OmniMTP)
    # 声明 SupportsMultiModal 是为消费 target 模型合并好的 embeddings,
    # 自身从不运行多模态处理器。文本模式是它们的预期结果,而非误配置,
    # 所以对 runner_type == 'draft' 的模型不再打印警告。
    if model_config.runner_type != 'draft':
        logger.warning_once(
            'Model %s is treated as multimodal but has no registered '
            'multimodal processor; running in text-only mode.',
            model_config.model,
        )
    return False
tests/models/test_qwen3_5_mtp_config.py test-coverage

新增 CPU-only 参数化测试,验证四种 Qwen3.5 系列 model_type 到 MTP 架构的映射,直接覆盖本次修复的回归面;不下载 checkpoint,适合 CI 快速执行。

# tests/models/test_qwen3_5_mtp_config.py - CPU-only 参数化测试,
# 验证四种 Qwen3.5 系列 model_type 都能被重写为 MTP 架构。
@pytest.mark.parametrize(
    'model_type,expected_arch',
    [
        ('qwen3_5', 'Qwen3_5MTP'),
        ('qwen3_5_moe', 'Qwen3_5MoeMTP'),
        # 纯文本变体必须映射到相同的 MTP 架构。
        ('qwen3_5_text', 'Qwen3_5MTP'),
        ('qwen3_5_moe_text', 'Qwen3_5MoeMTP'),
    ],
)
def test_mtp_override_recognizes_text_only_types(model_type, expected_arch):
    cfg = SpeculativeConfig.hf_config_override(_mtp_config(model_type))
    assert cfg.model_type == 'qwen3_5_mtp'
    assert cfg.architectures == [expected_arch]
    assert cfg.n_predict == 1

评论区精华

是否应该在 MTP 模型上注册多模态 processor 设计

DarkLight1337 请作者给出堆栈并质疑是否应改模型定义,称或许应在多模态注册表内部解决。作者核实后承认:supports_multimodal_inputs 本就会捕获缺失 processor 并回退文本模式,注册 processor 反而使 supports_multimodal_inputs() 返回 True,把纯文本 draft 模型推入完整多模态路径,使问题更糟。

结论:撤销 Qwen3_5MTP / Qwen3_5MoeMTP 上的 processor 注册(commit 9c56a8e),模型定义保持上游状态;在注册表内对 draft runner 静默预期回退。 · 已解决

是否压制 draft 模型缺 processor 的启动警告 设计

作者提出遗留问题:MTP draft 模型每次启动都会触发「treated as multimodal but has no registered multimodal processor」警告,询问是否值得单独开 PR 在注册表侧静默。DarkLight1337 回复「Let's do it in this PR as well」,要求并入本 PR。

结论:supports_multimodal_inputs 对 runner_type == 'draft' 的配置跳过警告(commit a6b160b),返回行为不变。 · 已解决

删除针对警告分支的新增测试 测试

DarkLight1337 在 review 评论中要求删除 tests/multimodal/test_registry.py 中关于「draft 模型不警告」的新增测试,理由是警告本质上与分支绑定(by definition),测试不提供额外信号。

结论:commit 5744b78 删除该测试,最终 PR 不包含此文件改动;保留 tests/models/test_qwen3_5_mtp_config.py 的配置映射测试。 · 已解决

风险与影响

  1. registry 行为变化风险:vllm/multimodal/registry.py 对 runner_type == "draft" 的模型静默回退文本模式。若未来某 draft 模型确实依赖多模态输入处理(例如按图像特征解码),此静默可能掩盖配置错误、增加排查难度。当前已知三个 MTP draft 模型均只消费 target 模型的 embeddings,风险为理论性。
  2. 配置匹配为枚举式硬编码:vllm/config/speculative.py 的匹配元组为显式枚举,未来再新增文本-only 变体(类似 qwen3_5_text 的模式)若未同步更新,MTP 将再次失效;相邻的 intern_s2_preview 分支依赖 text_config.model_type 判定,同属易漏点。
  3. 测试覆盖取舍:警告分支无单元测试(reviewer 主动要求删除),仓库 CI 也未强制覆盖 --speculative-config 的端到端路径,MTP 解码验证依赖作者个人硬件;本次回归保障主要依靠 tests/models/test_qwen3_5_mtp_config.py 的配置映射测试。
  4. runner_type 语义依赖:静默逻辑依赖 ModelConfig.runner_type 属性在 api_server 与 engine 路径上语义一致,若未来 draft 模型以其他方式标记,需同步修正条件。

用户侧:Qwen3.5 文本-only checkpoint 现在可以正常启用 --speculative-config '{"method":"mtp",...}' 投机解码,此前该配置直接失效;所有 MTP draft 模型(Qwen3_5MTP、Exaone4_5_MTP、MiMoV2OmniMTP)启动时不再输出误导性警告。系统侧:注册表只有日志行为变化,返回语义不变,风险面小。团队侧:确立了「draft 模型声明 SupportsMultiModal 但不注册 processor 是特性而非缺陷」的设计约定,为后续多模态 draft 模型提供了可复用的处理模式。

draft 模型警告静默化 配置匹配为枚举式硬编码 警告分支无独立测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论