Prhub

#56160 [Bugfix][MLA] Read sparse model settings from text config

原始 PR 作者 andylolu2 合并时间 2026-09-12 02:02 文件变更 11 提交数 4 评论 16 代码增减 +32 / -22

执行摘要

统一稀疏 MLA 配置至 hf_text_config,修复复合模型初始化失败

PR body 明确指出:复合(composite)和多模态模型将稀疏 MLA 设置放在嵌套的 text config 中,通用基础设施从顶层 HF config 读取会“make backend initialization, workspace sizing, or JIT warmup fail even though the model is configured correctly”。ModelConfig.hf_text_config 是纯文本与复合配置的统一归一化访问器,因此需要把所有公共读取路径从 hf_config 切换到 hf_text_config,同时保留 DeepSeek-v4 特定实现与 sparse_swa.py 不动的边界。

值得精读。这是一个典型的“配置读取契约漂移”修复:改动虽小(+32/-22),但确立了 hf_text_config 作为复合模型配置归一化入口的规范。重点看三处:一是 test_sparse_mla_backends.py 如何通过 del hf_config.index_topk 强制 fixtures 走复合路径以预防回归;二是 PR 刻意保留 DeepSeek-v4 特定代码的设计边界;三是 warmup key 生成对 getattr 默认值与 index_kpool 去重的处理方式。

讨论亮点
  1. NVIDIA/AMD shard 失败根因:作者在 PR body 的 Reviewer guide 中说明,三个预先存在的 warmup fixtures(test_indexer_deepseek_v4_slot_mapping.py)仍然只提供 hf_config,是 NVIDIA 和 AMD attention shard 失败的共同原因;latest update 修正后全部通过。
  2. HF 配置变更提示:NickLucche 评论 "hf change detected @hmellor",提醒合并者注意该 PR 触碰 HF config 读取路径,hmellor 随后 approve 并合并。
  3. 设计边界:PR 明确保留 DeepSeek-v4 特定代码与 sparse_swa.py 不变,因为这些实现有意消费各自的 flat model config,而不走通用复合路径。

实现拆解

步骤 1:核心元数据与 workspace 分配(sparse_mla_attention.py)

  • SparseMLACommonMetadataBuilder.__init__ 中 self.topk_tokens 从 vllm_config.model_config.hf_config.index_topk 改为 hf_text_config.index_topk。
  • 静态方法 determine_chunked_prefill_workspace_size 同步改读 hf_text_config,确保 chunked-prefill workspace 行数与压缩后 token 数一致,DCP 各 rank 的 gather 缓冲大小正确。
  • 这两处是初始化期的硬依赖:读不到字段会直接 AttributeError,导致整个模型初始化失败,因此是本次修复的核心。

步骤 2:跨平台后端归一化(flashattn_mla_sparse.py / rocm_aiter_mla_sparse.py / xpu_mla_sparse.py)

  • FlashAttnMLASparseBackend.supports_combination 的“是否存在 index_topk”校验从 hf_config 改为 hf_text_config,避免复合模型被后端选择器误判为不支持而拒绝启动。
  • ROCm AITER 与 XPU 的 metadata builder __init__ 中 topk_tokens 初始化同步切换,保证各平台在复合配置下都能得到真实的 top-k 值。

步骤 3:JIT warmup 与内核分派(indexer.py / compressor_utils.py / sparse_utils.py / dcp_indexer_cutedsl.py)

  • indexer.py 的 prefill indexer warmup key 生成改用 hf_text_config 读取 compress_ratios 与 index_kpool,并用 getattr 保留缺失时的默认值。
  • compressor_utils.py 的压缩 slot-mapping 内核 warmup 比率读取同样切换。
  • sparse_utils.py 的索引转换 warmup 的 num_topk_tokens 与 dcp_indexer_cutedsl.py 两个 DCP 内核类的 get_warmup_keys 中的 index_topk 一并切换。
  • 该步骤保证 warmup key 与运行时实际 dispatch 参数一致,避免编译缓存 miss 导致的首请求运行时 JIT 编译。

步骤 4:测试与回归配套(3 个测试文件)

  • test_sparse_mla_backends.py:把 decode / prefill / DCP 三组 fixtures 改成“稀疏参数仅存在于 hf_text_config”的复合形态,并显式 del model_config.hf_config.index_topk,防止回归到顶层读取。
  • test_indexer_deepseek_v4_slot_mapping.py:修复三个只提供 hf_config 的 warmup 测试,改为 hf_text_config;这是 NVIDIA 与 AMD attention shard 失败的共同根因。
  • test_dspark_noncausal_sparse_mla.py:同步调整 dspark 非因果稀疏 MLA 测试的配置结构。
  • CI 结果:所有 NVIDIA/AMD attention shards 通过;剩余失败均被评估为与 PR 无关(parser 缺 builder、Rust 磁盘不足、Intel HF cache 缺失)。
文件 模块 状态 重要度
vllm/model_executor/layers/attention/sparse_mla_attention.py 稀疏 MLA modified 5.68
vllm/model_executor/kernels/attention/dsa/dcp_indexer_cutedsl.py DCP 内核 modified 5.48
vllm/v1/attention/backends/mla/indexer.py 索引器 modified 5.47
vllm/v1/attention/backends/mla/flashattn_mla_sparse.py FA 后端 modified 5.23
vllm/v1/attention/backends/mla/compressor_utils.py 压缩器 modified 5.19
vllm/v1/attention/backends/mla/rocm_aiter_mla_sparse.py ROCm 后端 modified 4.69
vllm/v1/attention/backends/mla/sparse_utils.py 稀疏工具 modified 4.69
vllm/v1/attention/backends/mla/xpu_mla_sparse.py XPU 后端 modified 4.69
tests/v1/attention/test_sparse_mla_backends.py 测试覆盖 modified 4.27
tests/v1/attention/test_indexer_deepseek_v4_slot_mapping.py 测试覆盖 modified 4.12
tests/v1/attention/test_dspark_noncausal_sparse_mla.py 测试覆盖 modified 3.79

关键符号

SparseMLACommonMetadataBuilder.__init__ SparseMLACommonMetadataBuilder.determine_chunked_prefill_workspace_size FlashAttnMLASparseBackend.supports_combination ROCMAiterMLASparseMetadataBuilder.__init__ XPUMLASparseMetadataBuilder.__init__ CompressedSlotMappingKernel.get_warmup_keys get_warmup_keys (prefill indexer) get_warmup_keys (IndexConversionKernel) get_warmup_keys (DCP Triton indexer) get_warmup_keys (DCP CuTeDSL gather)

关键源码片段

vllm/model_executor/layers/attention/sparse_mla_attention.py data-contract

稀疏 MLA 公共元数据 builder 的核心文件:topk_tokens 初始化与 chunked-prefill workspace 大小计算都依赖 index_topk,是复合模型初始化失败的源头。

class SparseMLACommonMetadataBuilder(AttentionMetadataBuilder[T]):
    metadata_cls: type[T]
    require_uniform_decodes: ClassVar[bool] = False
​
    def __init__(self, kv_cache_spec, layer_names, vllm_config, device) -> None:
        super().__init__(kv_cache_spec, layer_names, vllm_config, device)
        self.vllm_config = vllm_config
        self.device = device
        self.model_config = vllm_config.model_config
        self.mla_dims = get_mla_dims(self.model_config)
        # 关键修复:复合 / 多模态模型的稀疏参数只存在于嵌套的 text config 中,
        # 顶层 hf_config 读不到 index_topk,会导致后端初始化或 workspace 分配失败。
        # hf_text_config 是 ModelConfig 提供的归一化访问器,
        # 对纯文本模型语义上等价于 hf_config。
        self.topk_tokens: int = vllm_config.model_config.hf_text_config.index_topk
​
        self.req_id_per_token_buffer = torch.empty(
            (vllm_config.scheduler_config.max_num_batched_tokens,),
            dtype=torch.int32,
            device=device,
        )
        parallel_config = vllm_config.parallel_config
        self.use_pcp = parallel_config.prefill_context_parallel_size > 1
        try:
            self.dcp_world_size = get_dcp_group().world_size
        except AssertionError:
            # DCP 可能未在测试中初始化,回退为单 rank。
            self.dcp_world_size = 1
        self.cp_kv_cache_interleave_size = parallel_config.cp_kv_cache_interleave_size
        self.dcp_local_block_size = self.cp_kv_cache_interleave_size
        self.dcp_virtual_block_size = self.dcp_local_block_size * self.dcp_world_size
​
        # workspace 行数必须与压缩后的 token 数匹配,否则 DCP gather 会越界。
        self.chunked_prefill_workspace_size = (
            self.determine_chunked_prefill_workspace_size(vllm_config)
        )
        workspace_head_size = (
            self.mla_dims.kv_lora_rank + self.mla_dims.qk_rope_head_dim
        )
        workspace_rows = self.chunked_prefill_workspace_size
        if self.dcp_world_size > 1:
            # DCP 各 rank 的本地 KV shard 都会被 gather 进 workspace,
            # 因此需要额外 1/DCP 行数。
            assert self.chunked_prefill_workspace_size % self.dcp_world_size == 0
            workspace_rows += self.chunked_prefill_workspace_size // self.dcp_world_size
        self.chunked_prefill_workspace = torch.empty(
            (workspace_rows, workspace_head_size),
            dtype=self.model_config.dtype,
            device=device,
        )
        # ... 后续初始化 topk 掩码 workspace 与 DCP manager ...
​
    @staticmethod
    def determine_chunked_prefill_workspace_size(vllm_config: "VllmConfig") -> int:
        scheduler_config = vllm_config.scheduler_config
        cache_config = vllm_config.cache_config
        model_config = vllm_config.model_config
        # 同样从 hf_text_config 读取,保证 workspace 大小与运行时 top-k 一致。
        topk_tokens = model_config.hf_text_config.index_topk
​
        workspace_size = min(
            max(
                8 * model_config.max_model_len,
                4 * scheduler_config.max_num_seqs * cache_config.block_size,
            ),
            64 * 1024,
            scheduler_config.max_num_seqs * topk_tokens,
        )
        workspace_size = max(workspace_size, cache_config.block_size)
        if vllm_config.parallel_config.decode_context_parallel_size > 1:
            return align_mla_chunked_context_workspace_size(
                vllm_config, workspace_size
            )
        return workspace_size
vllm/model_executor/kernels/attention/dsa/dcp_indexer_cutedsl.py data-contract

DCP top-k 索引与 gather 内核的 warmup key 生成依赖 index_topk;复合配置下顶层读取会得到错误 key 甚至 AttributeError,导致 DCP 编译缓存失效。

    def get_warmup_keys(self, vllm_config: Any) -> list[CompileKey]:
        dcp_world_size = vllm_config.parallel_config.decode_context_parallel_size
        if dcp_world_size <= 1:
            return []
        cp_interleave = vllm_config.parallel_config.cp_kv_cache_interleave_size
        # 复合 / 多模态模型的 index_topk 只存在于嵌套 text config,
        # 改从归一化访问器 hf_text_config 读取,否则 warmup key 与运行时
        # dispatch 参数不一致,首请求会触发运行时 JIT 编译。
        topk = vllm_config.model_config.hf_text_config.index_topk
        if topk <= 0:
            return []
​
        try:
            from vllm.distributed.parallel_state import get_dcp_group
            dcp_rank = get_dcp_group().rank_in_group
        except Exception:
            # 测试环境可能没有初始化 DCP group,回退为 rank 0。
            dcp_rank = 0
​
        return self._trace_dispatch(self.dispatch)(
            dcp_rank=dcp_rank,
            dcp_world_size=dcp_world_size,
            cp_interleave=cp_interleave,
            has_row_starts=(False, True),
            topk=topk,
            block_size=512,
        )
vllm/v1/attention/backends/mla/indexer.py core-logic

prefill indexer 的 JIT warmup key 由 compress_ratios 与 index_kpool 推导,是复合模型下 warmup 失败的高发点;同时是 DeepSeek-V4.1 等新模型直接消费的文件。

    def get_warmup_keys(self, vllm_config: VllmConfig) -> list[CompileKey]:
        max_tokens = max(
            1, min(vllm_config.scheduler_config.max_num_batched_tokens, 8)
        )
        # 统一从 hf_text_config 读取:复合模型只把稀疏设置放在嵌套文本配置里,
        # 读不到时 warmup key 会 miss,导致运行时首次编译延迟。
        hf_text_config = vllm_config.model_config.hf_text_config
        parallel_config = vllm_config.parallel_config
        dcp_world = parallel_config.decode_context_parallel_size
        dcp_interleave = parallel_config.cp_kv_cache_interleave_size
        dcp_rank = get_dcp_group().rank_in_group if dcp_world > 1 else 0
​
        # compress_ratios 允许缺失(默认 1);index_kpool 作为额外压缩比候选,
        # 通过 dict.fromkeys 去重,保证所有 Triton 特化都被预热。
        compress_ratios = tuple(
            dict.fromkeys(
                max(1, int(ratio))
                for ratio in (
                    *(getattr(hf_text_config, "compress_ratios", None) or (1,)),
                    getattr(hf_text_config, "index_kpool", 1) or 1,
                )
            )
        )
        index_kpool = getattr(hf_text_config, "index_kpool", None)
        if index_kpool and index_kpool > 1 and index_kpool not in compress_ratios:
            compress_ratios = compress_ratios + (index_kpool,)
​
        return self._trace_dispatch(self.dispatch)(
            # 覆盖 Triton 的 divisible、exact-one 与通用 i32 标量特化类别。
            query_slice_start=(0, 1, 2),
            query_slice_stop=(1, 2 * max_tokens - 1, 2 * max_tokens),
            DCP_RANK=dcp_rank,
            DCP_WORLD=dcp_world,
            DCP_INTERLEAVE=dcp_interleave,
            BLOCK_SIZE=self.BLOCK_SIZE,
            COMPRESS_RATIO=list(compress_ratios),
            input_variant=(
                TritonPointerInputVariant.from_alignment(uncompressed_seq_lens=True),
                TritonPointerInputVariant.from_alignment(uncompressed_seq_lens=False),
            ),
        )

评论区精华

NVIDIA/AMD attention shard 失败与 warmup fixtures 修复 测试

PR body 说明:三个预先存在的 warmup 测试(test_indexer_deepseek_v4_slot_mapping.py)只提供 hf_config,导致 warmup key 读取与实现不一致,是 NVIDIA 和 AMD attention shard 失败的共同原因;latest update 中修正为 hf_text_config。

结论:修正后所有 NVIDIA/AMD attention shards 通过,包括此前失败的稀疏 MLA 测试。 · 已解决

CI 失败项评估 测试

作者报告剩余 CI 失败均与 PR 无关:parser 缺 DeepSeek-V4.1 test builder(已在 main 修复)、CPU shard 的 Rust 构建磁盘不足、Intel 缺 Hugging Face cache 文件。

结论:评估为无关失败;hmellor 重新触发 CI 后合并。 · 已解决

HF 配置变更提示 other

NickLucche 评论 "hf change detected @hmellor",提醒合并者该 PR 触碰了 HF config 相关读取路径。

结论:hmellor 随后 approve 并完成合并。 · 已解决

风险与影响

  1. 直接属性访问无兜底:sparse_mla_attention.py 与三个平台后端对 index_topk 都是直接属性访问而非 getattr 兜底;若某个纯文本模型或手工构造配置的 hf_text_config 缺失该字段,会以 AttributeError 显式失败。这符合“尽早暴露配置错误”的设计意图,但对旧模型配置的兼容性是潜在回归点。
  2. 平台后端三处同步修改:NVIDIA、ROCm、XPU 各自维护一份 metadata builder,本次同步修改 3 处,未来新增平台或后端时容易漏改同一读取点。
  3. JIT warmup 静默 miss:warmup key 生成错误不会直接报错,而是表现为运行期首次编译延迟与显存峰值,测试难以完整覆盖所有平台组合。
  4. 双配置源并存:DeepSeek-v4 特定代码仍读 flat config,与通用路径并存;若未来这些实现被迁移到通用链路,同类 bug 可能复现。

对用户:修复了复合/多模态稀疏 MLA 模型(如带视觉编码器的 DeepSeek 变体)在 CUDA、ROCm、XPU 上的初始化失败问题;纯文本模型因 hf_text_config 与 hf_config 语义一致,行为不变。对系统:chunked-prefill workspace 大小与 DCP gather 缓冲更准确,JIT warmup 覆盖更完整,减少运行期编译。对团队:确立了 hf_text_config 作为模型特定配置读取的归一化入口,后续新增稀疏 MLA 后端必须遵循同一访问模式;测试 fixtures 明确了复合配置的构造规范。

复合配置数据契约变更 多平台后端同步修改 JIT warmup 键依赖配置读取 直接属性访问无 getattr 兜底 双配置源并存(DeepSeek-v4 flat config)

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论