vLLM-05|MLA、Compressor、Indexer 与 SWA:Flash 注意力栈在 vLLM 里怎么落地

3 阅读6分钟

协议层把 prompt 编成 token ids 之后,计算进入 DeepseekV4ForCausalLM.forward。Flash 的注意力实现不是标准多头注意力(MHA)单路径:在 DeepSeek V2/V3 的 MLA(Multi-head Latent Attention)底座上,按层叠加滑动窗口 SWA、可选 Compressor、以及仅在部分层出现的 Indexer 与 FlashMLA 稀疏 kernel。本文回答:compress_ratio 为 1、4、128 时,一层 attention 内各组件的职责边界与调用顺序,以及 decode 时 SWA 与压缩 KV 如何一并送入 FlashMLA。每 token KV 占多少字节、block 如何分配属 KV 布局话题;融合 kernel 细节不在本文展开。

MLA 底座:V4 forward 仍从 latent KV 写 cache

MLA 将 Q/K/V 先经低秩投影;写入 cache 的并非逐 head 展开的完整 K/V,而是 latent 内容与 RoPE 位置信息分块 的紧凑表示。V4 Attention 入口在 models/deepseek_v4/attention.py 的 DeepseekV4Attention;NVIDIA + FlashMLA 路径上,model.py 的 _select_dsv4_attn_cls 选型为 nvidia/flashmla.py 中的 DeepseekV4FlashMLAAttention。无论类名如何,每层 forward 都在 MLA 底座上再挂 SWA / Compressor / Indexer 等可选支路。

compress_ratio:三种 layer type 的总表

每层 DeepseekV4Attention 单独读 HF config.compress_ratios[layer_id],实现里 max(1, …)  只作用于 attention(同层 MoE/FFN 不看该字段)。若 config 为 0 或未设压缩,回落为 1,即 swaonly(仅 SWA + MLA,无 Compressor/Indexer)。该值决定本层 attention 是否启用压缩与稀疏 indexer,并映射 FlashMLA 的 layer typesparse_swa.py 中 swaonly / c4a / c128a;tile scheduler 对三种 type 使用不同 plan,不可混用一份调度表):

compress_ratiolayer typeSWACompressorIndexer压缩 KV top-k 来源(decode)
≤ 1swaonly无压缩支路
4c4a有(overlap)C4A:indexer 写 buffer,decode 现算 global top-k
128c128a有(无 overlap)C128A:metadata 预计算 c128a_global_decode_topk_indices

同一步 decode 中,约 60 层 attention 按 type 分组;各 type 共享 对应 FlashMLASchedMeta——第一次 forward 该 type 时 planner 分配,同 type 后续层复用。修改 checkpoint 或 compress_ratios 时,KV spec、metadata builder 与 CUDA graph 捕获路径均可能变化,不宜仅视为「多几层压缩」。

构造阶段(结构简化自 attention.py``__init__):

self.compress_ratio = max(1, config.compress_ratios[layer_id])
self.swa_cache_layer = DeepseekV4SWACache(...)
if self.compress_ratio == 4:
    self.indexer = DeepseekV4Indexer(...)
if self.compress_ratio > 1:
    self.compressor = DeepseekCompressor(..., compress_ratio=self.compress_ratio, ...)

三种 cache 与一段历史的分工

排障时须区分三类存储,不可混称「KV cache」:

对象职责典型层
DeepseekV4SWACache最近 sliding_window 个 token 的 MLA KV;decode 必读每层
compressor state cache压缩前的 kv/score 中间态ratio > 1
压缩 KV cacheCompressor 输出经 RMSNorm、RoPE、FP8 quant 后的远程历史槽ratio > 1

SWA 覆盖近期窗口;Compressor 将更远历史压入压缩槽。C4A 与 C128A 对远程历史都是 sparse attention + top-k,不是 C128A 全读压缩池;差别在 谁算 top-k——C4A 另有 Indexer 从约 L/4 个压缩槽里动态筛,C128A 压缩槽约 L/128,top-k 在 metadata 预计算,故无 Indexer 模块。

attention_impl:一层内的调用顺序

attention_impl(带 @eager_break_during_capture)在 CUDA graph 捕获段之间 eager 执行 metadata 相关路径;GEMM 与 RMSNorm 等可进 graph。按调用顺序可表述为:

hidden_states
  → attn_gemm_parallel_execute → qr, kv, kv_score, indexer 侧 score/weights
  → fused_q_kv_rmsnorm
  → wq_b + fused KV insert(写入 SWA cache)
  → [ratio==4] indexer 与 compressor 并行
  → [ratio==128] 仅 compressor
  → forward_mqa → FlashMLA(SWA indices + 可选 compressed top-k)
  → _o_proj

写入 SWA cache 后,ratio=4 层并行运行 Indexer 与 Compressor;ratio=128 层仅运行 Compressor。二者完成后进入 forward_mqa,由 FlashMLA 执行 sparse prefill/decode。

Compressor:ratio 4 与 128 的差异

compressor.py 的 DeepseekCompressor 在 compress_ratio in [4, 128] 时 assert。forward 先将 kv/score 写入 compressor state cache,再经融合 kernel 压缩 → RMSNorm → RoPE → FP8 quant → 写入与 MLA 共用的压缩 KV cache。RoPE 位置使用 (positions // compress_ratio) * compress_ratio,与普通 token 位置不同。

ratio=4 时 overlap=True coff = 1 + overlap = 2(实现里由 compress_ratio == 4 推出,非独立超参);ratio=128 时 overlap=Falsecoff=1。重叠的是 相邻两次压缩所 gather 的 token 区间,不是 SWA 窗口,也不是 GPU 多流 overlap。

无 overlap 的分块(便于对照) ——若每 4 token 切一刀、边界不共享,形状如下:

token:  [0 1 2 3] | [4 5 6 7] | [8 9 10 11]
压缩槽:    槽0    |    槽1    |     槽2

C4A(ratio=4,overlap) :稳态下 每 4 个 token 仍出 1 个压缩槽,但融合 kernel 每次从 state cache gather 连续 8 个 token(1+overlap)×4),对 kv/score 做 softmax 加权再压成 1 槽;相邻槽因此 共享中间 4 个 token

token:   0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
         └──── 槽0:用 0~7 ────┘
                  └──── 槽1:用 4~11 ────┘
                           └──── 槽2:用 8~15 ────┘
         槽0 与 槽1 重叠:4,5,6,7;槽1 与 槽2 重叠:8,9,10,11

序列开头第一个边界(position=3)时窗口可能不足 8,kernel 只对 已有 token 做 gather(pos >= 0 掩码)。C128(ratio=128,无 overlap)  则是每 128 token 无重叠切一块,例如 token 0~127 → 槽0,128~255 → 槽1。

物理 layout 上,fp8_ds_mla 需 576B 对齐;FlashMLA 文档记载 V4 每 token KV 为 584B(448B NoPE fp8 + 128B RoPE bf16 + 8B scale)——字节级推导与 scheduler spec 如何落地,在 KV 布局专题中展开,本篇只建立与 Compressor 输出的对应关系。

Indexer、C4A 与 C128A 的 top-k 分支

DeepseekV4Indexer 定义在 attention.py 内(非独立 indexer.py 模块),内部含小型 DeepseekCompressor(head_dim=128)与 SparseAttnIndexer,向 topk_indices_buffer 写入索引;index_topk 来自 HF config。

FlashMLA 稀疏后端为 v1/attention/backends/mla/flashmla_sparse.py 的 FlashMLASparseBackend;KV 格式为 fp8_ds_mla。decode 时在 DeepseekV4FlashMLAAttention._forward_decode 中分支:

  • C4A(ratio=4) :依据 indexer 维护的 buffer 现算 global top-k;
  • C128A(ratio=128) :读取 metadata 中预计算的 c128a_global_decode_topk_indices

易误解处:无 Indexer ≠ 不做 top-k。C128A 仍只 attend 压缩侧 top-k 槽;因候选规模约为 C4A 的 1/32(128÷4),用 metadata 预算 indices 即可,不必每层再跑 Indexer 打分。

metadata 预计算 指:进 FlashMLA 前,metadata builder 已写好 该读哪 k 个压缩槽(字段 c128a_global_decode_topk_indices),同 type 层共享 FlashMLASchedMeta;不是 kernel 里对「全部 attention 候选」再扫一遍表。候选只是 压缩 KV 池(约 L/128 槽),SWA 窗口仍走另一路 indices——metadata 存的是 已选好的 k 个槽号,不是全长 Q×K 矩阵。

最终 flash_mla_with_kvcache 同时接收 SWA 侧 indices 与压缩侧 indices(extra_k_cache / extra_indices_in_kvcache),在 SWA 窗口与 top-k 压缩槽上完成 sparse attention,而非对全长序列做 dense attention。机制上,算力随 SWA window 与 top-k 规模增长,而非随 max_model_len 线性膨胀;具体 latency 取决于层型混布与 batch,本文不作 profiling 结论。

eval 常见 --attention_config.use_fp4_indexer_cache=True 仅影响 Indexer 的 K cache 是否走 MXFP4(DeepseekV4Indexer.use_fp4_indexer_cache),不改变 SWA cache 的 fp8_ds_mla 布局。

SWA cache 与 tile scheduler

DeepseekV4SWACache 的 block_size 为 64(与 C4A 物理 page 对齐)。sparse_swa.py 将 compress_ratio 映射为 layer type 并选择 tile_sched_swaonly / tile_sched_c4a / tile_sched_c128a。FlashMLA tile scheduler 不能为不同 type 共用同一份 plan,故 forward 按 type 分组调度。

排障与阅读顺序

压缩与稀疏以效率换可观测性:attention 路径排障须先限定故障域——SWA insert 是否正常、compressor state 是否更新、压缩 KV 是否写入、C4A 的 indexer buffer 或 C128A 的 metadata top-k 是否与 FlashMLA 输入一致。阅读源码建议顺序:attention.py 的 attention_impl → compressor.py → nvidia/flashmla.py 的 _forward_decode(C4A / C128A 分支)→ sparse_swa.py 的 layer type 映射。

收尾

Flash 注意力栈在 vLLM 中的落地可概括为:MLA 写 latent KV;SWA 管近期窗口;Compressor 管远程历史的压缩槽;C4A 层以 Indexer 做 per-layer top-k,C128A 层以 metadata 预计算 top-k;FlashMLA 在 decode 时合并 SWA 与压缩两路 indices。ratio=1 的层无 Compressor、Indexer 及压缩 KV 支路,仅保留 SWA。