Prhub

#55738 [Perf][GLM-5.3-Flash] Dense/masked-MHA sparse prefill for the NoPE (256, 0, 256) layout + skip the NoPE K concat

原始 PR 作者 JaredforReal 合并时间 2026-09-14 17:36 文件变更 6 提交数 10 评论 15 代码增减 +96 / -0

执行摘要

NoPE (256,0,256) 预填改走 dense/masked MHA,TTFT 最多降 8.5%

PR body 指出:主干上 GLM-5.3-Flash 的 MLA 布局对 FlashAttention prefill 后端与 masked-MHA 白名单都是“未知几何”,MLACommonImpl 会打印 No MLA prefill backend supports this model,于是“every prefill token goes through the per-token top-k MQA kernel”。作者 profiling 显示该 kernel 受 KV gather 的 HBM 带宽限制:16k token chunk 下 5.9 ms/layer,其中约 4.8 ms 是纯 HBM 流量;即使 8x2048 这种小 batch 也要 1.6 ms/layer,而 dense MHA 只需约 0.2 ms。目标是在不改 kernel 的前提下,把 NoPE 布局映射到已有 kernel 路径上以拿到端到端 TTFT 收益。

值得精读,但不必逐行复查 96 行 diff——重点看三个设计决策:(1) 为何 (256, 0, 256) 可以复用 (192, 64, 256) 的 kernel,以及 _is_masked_mha_available 中 qk_head_dim == 256 and v_head_dim == 256 时绕过 get_flash_attn_version(head_size=...) 的特殊分支;(2) 白名单式后端选择的边界与测试方法,这是 vLLM MLA 后端选择的标准范式;(3) _concat_k_nope_k_pe 用别名返回换 134 MB/layer 拷贝的取舍——建议同时确认 _compute_prefill_context 只把该 K 作为只读输入传递。若团队计划生产部署,另需注意 FA4 mask_mod 的 JIT 预热缺口(首请求约 7 s)和量化 KV 场景无收益这两点;并跟踪 #55385 合入后是否需要回删这里的 7 行重复注册。

讨论亮点

本轮讨论的核心是“新增硬编码白名单/快速路径必须配回归测试”。Copilot 在三个文件上分别留下 comment:mla_attention.py 的 NoPE 早返回“changes behavior from 'always allocate and copy' to returning the k_nope view directly”,要求补 shape 与数值等价测试;sparse_mla_attention.py 的新元组是“a hard-coded gate that controls whether masked MHA can ever be used”,需要 focused unit test 防止后续误删;flash_attn.py 的新维度三元组“there is no unit test covering this dimension triplet”。ZJY0516 直接确认了这一点(“@JaredforReal please add some unit tests, the copilot review is right”),作者随后在 commit 679cd72 一次性补齐 14 个用例,ZJY0516 最终 APPROVED。

另一条线索是措辞与命名:ZJY0516 对注释给出 suggestion,把 GLM-5.x NoPE layout 改为 GLM-5.3-Flash NoPE layout;最终落库的注释是 GLM5Next NoPE layout(commit 7c708a8 “name glm5next”)。claude[bot] 因 PR 来自 fork 自动 review 被禁用,未产生技术意见。

作者在 PR body 中主动披露了两个未解决/需后续处理的问题:一是 FA4MLAPrefillKernel.get_warmup_keys 不覆盖 mask_mod,每个新序列长度桶首个请求要 JIT 编译约 7 s,属于“pre-existing gap for every masked-MHA model”;二是 commit 1 与 #55385 的 7 行重复,且 64k 以上 prompts 的 −3.5% 收益“is the K-concat removal alone”。此外作者给出了 duplicate-work check(#55385 / #54951 / #55222 / #55543 / #53969 / #55277),确认 masked-MHA 白名单与 K-concat 短路无重叠。

实现拆解

  1. 注册 NoPE 几何到 FlashAttention prefill 后端(vllm/v1/attention/backends/mla/prefill/flash_attn.py 的 FlashAttnPrefillBackend.supports_mla_dimensions,+7 行):向 MLADimensions 白名单追加 (qk_nope_head_dim=256, qk_rope_head_dim=0, v_head_dim=256)。因为 qk_head_dim=256+0 与 v_head_dim=256 恰好和已有 (192, 64, 256) 的 DeepSeek-V3.2 布局使用同一组 FA kernel,所以只是放行、无需新增 kernel 分支;效果是 ≤ index_topk(2048)的 prefill 从逐 token top-k MQA 切换到 dense MHA。这 7 行与 #55385 完全相同,被单独放在 commit b368243 以便对方合入后直接删除。

  2. 扩展 masked-MHA 允许名单(vllm/model_executor/layers/attention/sparse_mla_attention.py 的 _is_masked_mha_available,+2 行):在白名单元组中加入 (64, 512, 256, 0, 256)。该函数是三道门的组合:is_device_capability_family(100)(SM100 family)、几何逐字段精确匹配、以及 get_flash_attn_version() == 4 and not is_quantized_kv_cache(kv_cache_dtype)。影响面最大:2k 到现有 FLASHINFER_MLA_SPARSE 阈值(TP4 下 36k、TP8 下 64k)之间的 prefill 改走 FA4 masked MHA,是 8x32k −7.4% 与 32k 服务点 −4.4% TTFT 的主要来源;_masked_mha_workspace_fits 仍作为二次门控防止显存超限。

  3. NoPE K concat 短路(vllm/model_executor/layers/attention/mla_attention.py 的 MLACommonBaseImpl._concat_k_nope_k_pe,+4 行):在 k_pe.shape[-1] == 0 时直接 return k_nope,跳过 torch.empty 分配与两次拷贝——GLM-5.3-Flash 上 16k token chunk 每层约 134 MB。该函数被所有 MLA 模型(含带 RoPE 的 DeepSeek/GLM-5)共用,RoPE 分支行为完全不变;但函数契约从“总是返回新张量”变为“可能返回入参别名”,这是需要调用方复核的语义变化。

  4. 测试配套(三个文件、14 个新用例):tests/v1/attention/test_sparse_mla_backends.py 新增参数化的 test_is_masked_mha_available_model_dims(覆盖 deepseek_v32 / glm5 / glm53_flash_nope 三个正例与 quantized_kv、no_fa4、rope_320、wrong_heads 四个负例),并把 glm53_flash_nope_hd256_v256 加入 test_sparse_backend_prefill_correctness,用 SDPA 参考校验 dense MHA、masked MHA 与 masked MHA + chunked context 三种 batch;tests/v1/attention/test_mla_prefill_selector.py 新增 test_flash_attn_accepts_glm53_flash_nope_dimensions,通过内嵌 validate() 断言 (256, 0, 256) 通过而 (256, 64, 256) 仍被拒;tests/v1/attention/test_mla_backends.py 新增 test_concat_k_nope_k_pe_matches_torch_cat,用 rope/nope 参数化断言与 torch.cat 逐位相等,并用 data_ptr 断言 no-rope 分支确实返回 k_nope 本身。注意 correctness 用例带 torch.cuda.get_device_capability()[0] < 10 的 skip 条件,非 SM100 环境不会执行。

  5. 无配置、schema 或部署改动:本 PR 不新增 flag,路由开关就是白名单本身;唯一“部署面”的影响是 SM90 需要等 #55385 单独接线,量化 KV 场景仍走 top-k MQA。作者同时披露了 FA4 CuTeDSL mask_mod 变体与 Triton _scatter_topk_single_req_kernel 未进 get_warmup_keys 的 JIT 预热缺口,明确留作 follow-up。

文件 模块 状态 重要度
vllm/v1/attention/backends/mla/prefill/flash_attn.py 预填充后端 modified 5.92
vllm/model_executor/layers/attention/sparse_mla_attention.py 稀疏注意力 modified 5.48
vllm/model_executor/layers/attention/mla_attention.py 注意力基类 modified 5.68
tests/v1/attention/test_sparse_mla_backends.py 稀疏注意力 modified 5.41
tests/v1/attention/test_mla_prefill_selector.py 预填充选择 modified 5.62
tests/v1/attention/test_mla_backends.py 注意力基类 modified 5.09

关键符号

FlashAttnPrefillBackend.supports_mla_dimensions _is_masked_mha_available MLACommonBaseImpl._concat_k_nope_k_pe MLACommonBaseImpl._compute_prefill_context test_flash_attn_accepts_glm53_flash_nope_dimensions test_is_masked_mha_available_model_dims test_concat_k_nope_k_pe_matches_torch_cat test_sparse_backend_prefill_correctness

关键源码片段

vllm/v1/attention/backends/mla/prefill/flash_attn.py core-logic

FlashAttnPrefillBackend.supports_mla_dimensions 新增 (256, 0, 256),是 MLA prefill 后端选择的入口白名单;不加这一项,NoPE 模型的 prefill 会直接落回 per-token top-k MQA。该 7 行与 #55385 重复,是后续 rebase/ 去重的关注点。

FlashAttnPrefillBackend.supports_mla_dimensions:NoPE 几何放行

class FlashAttnPrefillBackend(MLAPrefillBackend):
    """FlashAttention backend for MLA prefill."""
​
    @staticmethod
    def get_name() -> str:
        return "FLASH_ATTN"
​
    @classmethod
    def is_available(cls) -> bool:
        return is_flash_attn_varlen_func_available()
​
    @classmethod
    def supports_mla_dimensions(cls, mla_dimensions: MLADimensions) -> bool:
        # 白名单式精确匹配:几何差一个维度就整体拒绝,
        # 由 MLA prefill selector 记下 "supported MLA dimensions" 原因
        return mla_dimensions in [
            MLADimensions(qk_nope_head_dim=128, qk_rope_head_dim=64, v_head_dim=128),
            MLADimensions(qk_nope_head_dim=192, qk_rope_head_dim=64, v_head_dim=256),
            MLADimensions(qk_nope_head_dim=64, qk_rope_head_dim=64, v_head_dim=128),
            # 本 PR 新增:GLM5Next NoPE 布局 (qk_nope=256, qk_rope=0, v=256)。
            # qk_head_dim = 256 + 0 = 256、v_head_dim = 256,
            # 与上面的 (192, 64, 256) DeepSeek-V3.2 布局复用同一组 FA kernel,
            # 所以这里只是放行,不需要任何新的 kernel 分支。
            MLADimensions(qk_nope_head_dim=256, qk_rope_head_dim=0, v_head_dim=256),
        ]
vllm/model_executor/layers/attention/sparse_mla_attention.py core-logic

`_is_masked_mha_available` 白名单新增 (64, 512, 256, 0, 256),直接决定 2k-36k/64k 区间的 prefill 走 FA4 masked MHA 还是逐 token top-k MQA,是本 PR 端到端收益最大的改动;同时保留 SM100 family、FA4、非量化 KV 三重门控。

_is_masked_mha_available:三道门的几何白名单

def _is_masked_mha_available(
    num_heads_total: int,
    kv_lora_rank: int,
    qk_nope_head_dim: int,
    qk_rope_head_dim: int,
    v_head_dim: int,
    kv_cache_dtype: str,
) -> bool:
    """Check if masked MHA can ever fire for this model configuration."""
    # 第一道门:masked MHA 只在 SM100 family(FA4 / CuTeDSL)上存在
    if not current_platform.is_device_capability_family(100):
        return False
    model_dims = (
        num_heads_total,
        kv_lora_rank,
        qk_nope_head_dim,
        qk_rope_head_dim,
        v_head_dim,
    )
    # 第二道门:逐字段精确匹配的几何白名单;
    # head 数从 64 改成 128、或 rope 从 0 改成 64 都会退回 MQA 路径
    if model_dims not in (
        (128, 512, 128, 64, 128), # DeepSeek-V3.2
        (64, 512, 192, 64, 256), # GLM-5
        # 本 PR 新增:GLM-5.3-Flash NoPE;
        # qk_head_dim 256 与上面的 (192, 64, 256) 是同一组 kernel
        (64, 512, 256, 0, 256),
    ):
        return False
    qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
    if qk_head_dim == 256 and v_head_dim == 256:
        # 这条路径用连续 K/V,不依赖 paged-KV 特性,
        # 因此可以绕开通用 FA selector 对 head-dim 256 的禁用
        fa_version = get_flash_attn_version()
    else:
        fa_version = get_flash_attn_version(
            head_size=qk_head_dim, head_size_v=v_head_dim
        )
    # 第三道门:必须 FA4,且 KV cache 未被量化,否则仍走 top-k MQA
    return fa_version == 4 and not is_quantized_kv_cache(kv_cache_dtype)
vllm/model_executor/layers/attention/mla_attention.py core-logic

`MLACommonBaseImpl._concat_k_nope_k_pe` 增加 NoPE 早返回,省掉一次同尺寸分配 + 拷贝(16k chunk 约 134 MB/layer),但把函数契约变成“可能返回入参别名”,是三个源码改动中语义变化最需要复核的一处。

_concat_k_nope_k_pe:NoPE 早返回

    def _concat_k_nope_k_pe(
        self, k_nope: torch.Tensor, k_pe: torch.Tensor
    ) -> torch.Tensor:
        """
        Efficiently concatenate k_nope and k_pe tensors along the last dimension.
​
        This function avoids the performance penalty of torch.cat with expanded
        non-contiguous tensors by pre-allocating the output and using direct copies.
​
        Args:
            k_nope: Tensor of shape [..., nope_dim]
            k_pe: Tensor to broadcast and concatenate, typically shape [..., 1, pe_dim]
                or [..., pe_dim]
​
        Returns:
            Tensor of shape [..., nope_dim + pe_dim]
        """
        if k_pe.shape[-1] == 0:
            # 本 PR 新增:NoPE MLA 没有 RoPE 段,拼出来就等于 k_nope,
            # 直接返回可以省掉一次同尺寸分配 + 拷贝
            # (GLM-5.3-Flash 16k token chunk 约 134 MB/layer)。
            # 注意:返回值与入参共享底层存储(同一 data_ptr),
            # 调用方只能把它当只读张量使用。
            return k_nope
​
        k = torch.empty(
            (*k_nope.shape[:-1], k_nope.shape[-1] + k_pe.shape[-1]),
            dtype=k_nope.dtype,
            device=k_nope.device,
        )
​
        if self._use_flashinfer_concat_mla_k:
            torch.ops.vllm.flashinfer_concat_mla_k(k, k_nope, k_pe)
        else:
            # Fallback: Direct copies with efficient broadcasting
            k[..., : k_nope.shape[-1]] = k_nope
            k[..., k_nope.shape[-1] :] = k_pe
        return k

评论区精华

新增白名单 / 快速路径缺少 regression test 测试

Copilot 在三个文件上分别指出:`_concat_k_nope_k_pe` 的早返回把行为从 “always allocate and copy” 改成直接返回 `k_nope` 视图,建议补 pe_dim==0 的形状与数值回归测试;`_is_masked_mha_available` 的新元组是 “a hard-coded gate that controls whether masked MHA can ever be used”,需要 focused unit test;`(256, 0, 256)` 维度三元组没有 selector/validation 覆盖。ZJY0516 明确站队:“@JaredforReal please add some unit tests, the copilot review is right”。

结论:作者在 commit 679cd72 一次性补齐 14 个用例(selector 校验、allow-list 正负例、SDPA 参考、concat 等价 + data_ptr),ZJY0516 随后 APPROVED。 · 已解决

NoPE 早返回引入返回张量别名语义 正确性

Copilot 指出该早返回“changes behavior from 'always allocate and copy' to returning the k_nope view directly”,要求用回归测试防住未来重构。作者补的 `test_concat_k_nope_k_pe_matches_torch_cat` 用 `assert (k.data_ptr() == k_nope.data_ptr()) == (qk_rope_head_dim == 0)` 把别名行为固化,但并没有约束调用点只读。

结论:别名行为已被测试固定,视为有意设计;下游 `_compute_prefill_context` 的只读假设未在讨论中显式确认,属遗留确认项。 · partially-resolved

FA4 mask_mod 未纳入 JIT 预热 性能

作者在 PR body 的 Caveat 中说明:每个新的序列长度桶首个请求会 JIT 编译 FA4 CuTeDSL `mask_mod` 变体(`BlackwellFusedMultiHeadAttentionForward`)与 Triton `_scatter_topk_single_req_kernel`,各约 7 s,并强调 “`FA4MLAPrefillKernel.get_warmup_keys` does not cover mask_mod, so this is a pre-existing gap for every masked-MHA model”。

结论:被明确定位为既有缺口而非本 PR 引入;作者建议 “a warmup for the masked variants should be a follow-up before relying on this in production”,讨论中未给出跟踪 issue。 · unresolved

注释命名从 GLM-5.x 收紧到具体型号 style

ZJY0516 对 `flash_attn.py` 的注释给出 suggestion,把泛化的 `# GLM-5.x NoPE layout` 改成带具体型号的表述,避免日后歧义。

结论:最终落库注释为 `# GLM5Next NoPE layout: qk_head_dim 256 + 0 and v_head_dim 256 run the same kernels as the (192, 64, 256) DeepSeek-V3.2 layout.`(见 commit 7c708a8 “name glm5next”)。 · 已解决

与 #55385 的 7 行重复改动 设计

作者在 PR body 与 commit message 中主动声明 commit `b368243` 的 7 行 “Same lines as in the SM90 wiring PR #55385; kept separate so it can be dropped once that lands”,并给出 duplicate-work check(#54951 / #55222 / #55543 / #53969 / #55277 均无重叠)。

结论:接受为临时重复:拆成独立 commit 便于 #55385 落地后直接删除,但合入顺序与 rebase 仍需协调。 · accepted-with-followup

风险与影响

  1. 核心路径内核切换(最高):2k–36k(TP4)/64k(TP8)区间的 prefill 由 per-token top-k MQA 改走 FA4 masked MHA,路由只由几何、FA4 版本和 KV 是否量化决定,任何白名单误扩展都会静默改变数值路径且不会报错。缓解:作者提供了 prompt-logprob A/B(4k/12k/30k/60k,mean|Δ| 0.06-0.12 对 main-vs-main 噪声底 0.04-0.11)、needle-in-a-haystack 检索和 gsm8k(93.10% vs main 93.33%);但 2x8192 数据点作者自己标注“~4% run-to-run noise floor, treat it as indicative only”。
  2. 返回张量别名语义:_concat_k_nope_k_pe 在 NoPE 下返回 k_nope 本身(同一 data_ptr),测试只固定了“数值等价 + 别名成立”,没有约束调用点只读。调用方是 MLACommonBaseImpl._compute_prefill_context 的 context loop,若后续有人在拿到返回值后做原地写,就会污染 k_nope 甚至上游张量;这是本 PR 里最难静态发现的风险。
  3. JIT 预热缺口被放大:FA4MLAPrefillKernel.get_warmup_keys 未覆盖 FA4 CuTeDSL BlackwellFusedMultiHeadAttentionForward 的 mask_mod 变体与 _scatter_topk_single_req_kernel,每个新序列长度桶首个请求各约 7 s 编译(jit_monitor 告警)。这是既有缺口,但本 PR 把更多流量导向 masked MHA 路径,生产首包延迟会被放大,需要 follow-up。
  4. 硬编码几何白名单的维护面:三组几何现在同时硬编码在 FlashAttnPrefillBackend.supports_mla_dimensions 与 _is_masked_mha_available 两处,且功能只在 is_device_capability_family(100) 生效;SM90 要等 #55385,SM120 要等 #53969,存在同一模型布局被多次逐点接线、遗漏一处就退化到 MQA 的长期成本。
  5. 兼容性与收益边界:量化 KV(is_quantized_kv_cache)和 FA3 仍被否决,因此 fp8_ds_mla 部署拿不到本 PR 的收益;_concat_k_nope_k_pe 是全局共享函数,契约变化影响所有 MLA 模型(尽管 RoPE 分支未变)。
  6. 与 #55385 的重复改动:commit b368243 的 7 行与 #55385 相同,两者先后合入会产生 rebase 冲突或冗余白名单项,需要合入顺序协调。

对用户:在 SM100(GB300/B200 等)上部署 GLM-5.3-Flash 且使用非量化 KV 时,prefill TTFT 8x2048 −8.5%、8x32k −7.4%、32k/256 c=16 场景 −4.4%,decode 吞吐与 TPOT 在 ±0.8% 内不变,gsm8k 精度无统计显著下降;量化 KV 部署与 SM90/SM120 用户不受影响也拿不到收益。对系统:MLACommonBaseImpl._concat_k_nope_k_pe 的契约由“总是新建张量”变为“NoPE 时返回入参别名”,这是全局共享代码的语义收窄,会影响所有 MLA 模型实现的调用约定;后端选择逻辑(白名单 + _use_masked_mha / _masked_mha_workspace_fits)的行为边界也随白名单扩展而改变,但都限制在 SM100 family。对团队:本 PR 固化了“用白名单把新模型几何映射到已有 kernel”的既有工程模式,配套测试的写法(allow-list 参数化正负例 + torch.cat 等价 + SDPA 参考)可直接复用到 #55385(SM90)和 #53969(SM120)等后续接线工作,但也意味着每上一个 NoPE/新几何模型都需要重复一次这种手工接线。

核心路径内核切换 硬编码维度白名单 NoPE 路径无 JIT 预热 返回张量别名语义 与 #55385 重复改动

关联 Issue

#53969 [Bugfix] Support NoPE models on FLASHINFER_MLA_SPARSE_SM120 and validate effective topk buffer width
#55385 [perf] wire FA and FlashMLA for sm90 GLM5Next NoPE SparseMLA

完整报告

参与讨论