Prhub

#54005 [Bugfix][Model] Fix K3 DSpark config for 96-head drafts

原始 PR 作者 zixi-qi 合并时间 2026-08-28 06:58 文件变更 2 提交数 1 评论 2 代码增减 +19 / -8

执行摘要

修复 K3 DSpark 96 头配置解析失败

Issue #53998 报告:加载 Kimi-K3 MLA DSpark 草稿 checkpoint 时,由于 K3DSparkConfig 继承 DeepseekV2Config,其 validate_architecture 中的 MHA invariant(hidden_size % num_attention_heads == 0)错误地拒绝了 Block5 checkpoint(7168 / 96)。MLA 的头维度是独立声明的,该不变量不适用。本 PR 通过切换基类为 DeepseekV3Config 解决此问题。

值得精读,特别是对于处理 MLA 配置继承和校验逻辑的开发者。本 PR 演示了如何通过选择正确的基类来解决配置校验问题。

讨论亮点

未发现实质性讨论。Claude bot 指出此 PR 来自 fork,自动审查被禁用,建议维护者手动请求审查。WoosukKwon 已批准。

实现拆解

  1. 修改基类:在 vllm/transformers_utils/configs/k3_dspark.py 中,将 K3DSparkConfig 的继承关系从 DeepseekV2Config 改为 DeepseekV3Config。这改变了配置校验逻辑,DeepseekV3Config 更符合 MLA 的几何结构,允许非整除的 hidden_size 与注意力头数。同时更新了注释中的 MoE 拓扑描述。
  2. 更新测试 fixture:在 tests/transformers_utils/test_dspark_mla_config.py 中,将默认的 num_attention_heads 和 num_key_value_heads 从 64 改为 96(对应 Block5)。
  3. 参数化测试:将 test_dspark_mla_uses_latent_kv_geometry 参数化,覆盖 64 头和 96 头两种配置,并验证 TP8 下的本地头数(8 和 12)。确保两种公开 checkpoint 均能正确加载。
文件 模块 状态 重要度
vllm/transformers_utils/configs/k3_dspark.py 配置解析 modified 5.97
tests/transformers_utils/test_dspark_mla_config.py 配置测试 modified 5.51

关键符号

K3DSparkConfig.__init__ test_dspark_mla_uses_latent_kv_geometry

关键源码片段

vllm/transformers_utils/configs/k3_dspark.py core-logic

核心修复文件,修改 K3DSparkConfig 的基类,解决配置校验问题。

# vllm/transformers_utils/configs/k3_dspark.py
from transformers import DeepseekV3Config # 改为 DeepseekV3Config,它适用于 MLA 几何
​
class K3DSparkConfig(DeepseekV3Config):
    """Configuration for a dense MLA DSpark draft model."""
​
    model_type = "k3_dspark"
    has_no_defaults_at_init = True
​
    def __init__(
        self,
        mla_use_nope: bool = False,
        mla_use_output_gate: bool = False,
        mla_use_qk_norm: bool = False,
        rope_theta: float = 50000.0,
        **kwargs,
    ) -> None:
        # DeepseekV3Config 同样默认 MoE 拓扑,这里清零相关字段以标识为 dense
        kwargs.setdefault("n_routed_experts", 0)
        kwargs.setdefault("n_shared_experts", 0)
        kwargs.setdefault("num_experts_per_tok", 0)
​
        # 处理 rope 参数,优先使用传入的 rope_parameters,否则设置默认值
        rope_parameters = kwargs.get("rope_parameters")
        if rope_parameters is None:
            kwargs["rope_parameters"] = {
                "rope_type": "default",
                "rope_theta": rope_theta,
            }
        else:
            rope_parameters = dict(rope_parameters)
            rope_parameters.setdefault("rope_type", "default")
            rope_parameters.setdefault("rope_theta", rope_theta)
            kwargs["rope_parameters"] = rope_parameters
​
        super().__init__(**kwargs)
        self.mla_use_nope = mla_use_nope
        self.mla_use_output_gate = mla_use_output_gate
        self.mla_use_qk_norm = mla_use_qk_norm
​
        # 检查是否有不支持的选项,如果设置则抛出错误
        unsupported = [
            name
            for name in (
                "mla_use_nope",
                "mla_use_output_gate",
                "mla_use_qk_norm",
                "dspark_bonus_anchor",
            )
            if getattr(self, name, False)
        ]
tests/transformers_utils/test_dspark_mla_config.py test-coverage

测试文件更新,覆盖 64 头和 96 头配置,验证修复。

# tests/transformers_utils/test_dspark_mla_config.py
# 测试函数参数化,覆盖两种头部数量布局
@pytest.mark.parametrize(
    ("num_attention_heads", "expected_local_heads"),
    [(64, 8), (96, 12)], # 64 头对应 released 版本,96 头对应 Block5
    ids=["released-64-head", "block5-96-head"],
)
def test_dspark_mla_uses_latent_kv_geometry(
    tmp_path, num_attention_heads, expected_local_heads
):
    draft_path = tmp_path / "draft"
    _write_dspark_config(
        draft_path,
        num_attention_heads=num_attention_heads,
        num_key_value_heads=num_attention_heads,
    )
    model_config = ModelConfig(
        model=str(draft_path),
        tokenizer_mode="skip",
        runner="draft",
        max_model_len=32768,
    )
​
    assert model_config.is_deepseek_mla
    assert model_config.use_mla
    assert model_config.get_head_size() == 576
    # 使用 external_launcher 跳过本地 GPU 数检查,便于在单 GPU 上测试 TP8
    parallel_config = ParallelConfig(
        tensor_parallel_size=8, distributed_executor_backend="external_launcher"
    )
    assert model_config.get_num_kv_heads(parallel_config) == 1
    assert model_config.get_num_attention_heads(parallel_config) == expected_local_heads
    assert model_config.get_num_experts() == 0

评论区精华

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

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

风险与影响

基类切换可能影响其他依赖 DeepseekV2Config 默认值的逻辑,但 K3DSparkConfig 已显式覆盖 MoE 相关字段并设置 has_no_defaults_at_init = True,风险较低。测试覆盖了两种头部布局,降低了回归风险。

影响范围限于 K3 DSpark 草稿模型的配置加载。此修复使公开的 96 头 checkpoint 可用,提升了对 Kimi-K3 DSpark 模型的支持。对现有 64 头用户的兼容性已通过测试验证。

配置继承变更 回归风险低

关联 Issue

#53998 [Bug]: MLA DSpark draft config rejected by inherited DeepseekV2Config.validate_architecture (MHA-only hidden_size % num_attention_heads check)

完整报告

参与讨论