1. 核心矛盾:Cache 格式 vs Prefill 格式
DeepSeek MLA 的独特之处在于:存储在 KV Cache 中的数据格式和Prefill 阶段注意力计算需要的数据格式是不一样的。这是 MLA 区别于 MHA/GQA/MQA 的根本工程挑战。
核心矛盾:Cache 存 576 维 (共享),Prefill 需要 40,960 维 (per-head)。当使用 Prefix Cache 命中已有缓存时,这些 576 维压缩数据需要在线解压缩 (on-demand decompression) 为全量 KV 才能参与 Prefill 的 FlashAttention 计算。
2. MLA KV Cache 的存储结构
2.1 每 token 的缓存内容
MLA 的 KV Cache 不像传统 MHA 那样存储每个 head 的 K 和 V,而是只存储联合压缩的 latent 向量:
2.2 FP8 量化存储 (vLLM)
vLLM 中对 MLA KV Cache 进一步应用了 FP8 量化:
| 字段 | 内容 | 字节数 | 精度 |
| c_t | 512 个 float8_e4m3 值 | 512 B | FP8 |
| scale factors | 4 个 float32 (per-group scale) | 16 B | FP32 |
| k_rope | 64 个 bfloat16 值 (不量化) | 128 B | BF16 |
| 总计 | 656 B/token | 混合 |
对比 MHA:标准 128-head MHA,每 token 存储 128 × (128+128) × 2 = 65,536 B (BF16)。MLA 仅需 656 B,约 100 倍压缩,这正是 MLA 的巨大优势。
3. 两条注意力路径:Non-Absorbed (Prefill) vs Absorbed (Decode)
关键洞察:Decode 走 Absorbed 路径时,直接以 c_t 参与计算(矩阵吸收让 W_UK 被吸进 Q 侧),无需解压缩。但 Prefill 走 Non-Absorbed 路径时,必须将 c_t 通过 W_UK/W_UV 解压回全量 K/V。Prefix Cache 命中的那些 token,其缓存里只有 c_t + k_rope,因此 prefill 阶段必须做在线解压缩。
4. vLLM 的实现方案 vLLM
4.1 核心代码架构
vLLM 的 MLA 注意力后端位于 vllm/v1/attention/backends/mla/common.py,核心类为 MLACommonBackend。
4.2 _forward_prefill 的两阶段设计
当一个请求同时包含 prefix-cached tokens (context) 和 new tokens (extend) 时,vLLM 把 prefill 拆分为两个阶段:
4.3 merge_attn_states: 合并两阶段结果
两个阶段各自产出 (output, lse),最后通过 merge_attn_states 进行数值稳定的加权合并:
def merge_attn_states(output_a, lse_a, output_b, lse_b):
max_lse = torch.max(lse_a, lse_b)
weight_a = torch.exp(lse_a - max_lse)
weight_b = torch.exp(lse_b - max_lse)
merged_output = (weight_a * output_a + weight_b * output_b) / (weight_a + weight_b)
return merged_output
数学等价性:这个合并操作保证了最终结果与直接对全序列做 attention 完全等价。利用了 FlashAttention 计算过程中的 log-sum-exp 统计量,避免了需要重新计算全局 softmax。
4.4 _compute_prefill_context 的分块处理
对于 prefix cached 的 context 部分,vLLM 使用 context chunk 机制来控制内存:
for chunk_idx in range(num_context_chunks):
cache_kv_c, cache_k_pe = _gather_cache(
kv_cache, block_table, chunk_start, chunk_len)
kv_full = cache_kv_c @ kv_b_proj
k_nope = kv_full[:, :n_heads*d_nope].reshape(chunk_len, n_heads, d_nope)
v = kv_full[:, n_heads*d_nope:].reshape(chunk_len, n_heads, d_v)
k = torch.cat([k_nope, cache_k_pe.expand(..., n_heads, ...)], dim=-1)
chunk_out, chunk_lse = flash_attention(q_for_context, k, v)
ctx_output, ctx_lse = merge_attn_states(
ctx_output, ctx_lse, chunk_out, chunk_lse)
为什么要分块?假设 prefix cache 命中了 32K token,一次性解压缩的显存开销为 32K × 128 heads × 320 dims × 2B ≈ 2.5 GB。分成 4K 的 chunk 则每次只需 ~320 MB,显存压力可控。
4.5 vLLM Paged KV Cache 对 MLA 的适配
| 特性 | 标准 MHA | MLA 模式 |
| 每 block 存储 | K_head [n_heads, d_k] + V_head [n_heads, d_v] | c_t [d_c=512] + k_rope [d_rope=64] |
| block size | 16 token/block | 16 token/block |
| 每 block 字节 | 16 × 128 × 256 × 2 = 1 MB (BF16) | 16 × 656 = ~10 KB (FP8 mixed) |
| Hash 键 | token IDs + layer + position | token IDs + layer + position (相同) |
| Prefix 匹配 | 直接使用匹配的 KV | 匹配后需在线解压缩才能给 prefill 用 |
5. SGLang 的实现方案 SGLang
5.1 RadixAttention 与 MLA KV Cache
SGLang 使用 RadixTree 数据结构来管理 prefix cache(称为 RadixAttention),比 vLLM 的 block hash 方案更高效地支持前缀共享:
5.2 MLA KV Buffer 格式
SGLang 中 MLA 的 KV 缓存以单一 buffer 存储:
kv_buffer: List[Tensor]
5.3 Chunked Prefix Cache 优化 (FlashAttention3)
SGLang 的核心创新是 Chunked Prefix Cache 优化,目前仅在 FlashAttention3 后端可用:
5.4 SGLang 支持的 MLA Attention 后端
| 后端 | Prefill | Decode | Chunked Prefix Cache | 适用 GPU |
| FlashAttention3 | Non-Absorbed | Absorbed | 支持 | Hopper+ |
| FlashInfer | Non-Absorbed | Absorbed | 不支持 | Ampere+ |
| FlashMLA | - | Absorbed | - | Hopper |
| CutlassMLA | - | Absorbed | - | Hopper+ |
| TRTLLM MLA | Non-Absorbed | Absorbed | 不支持 | Blackwell |
| Triton | Non-Absorbed | Absorbed | 不支持 | 通用 |
重要限制:Chunked Prefix Cache 优化目前仅在 FlashAttention3 后端可用。使用其他后端时,prefix cache 命中的 token 仍会走完整的重新计算路径而非缓存复用路径,或者采用更基础的处理方式。FA3 的 merge_states API 是实现此优化的关键。
6. 核心数据流全景图:Prefix Cache → Chunked Prefill → Attention Output
关键公式:解压缩的 GEMM 计算量 = prefix_len × d_c × n_heads × (d_nope + d_v) = 8K × 512 × 128 × 256 ≈ 135 GFLOP。虽然这是额外开销,但相比完全重新计算 8K token 的全部 Transformer 层(约 8K × 隐藏层宽度 × 参数量 的量级),prefix cache + 在线解压的方案仍然是巨大的节省。
7. 性能对比与工程取舍
7.1 Prefix Cache 收益分析
| 场景 | 无 Prefix Cache | 有 Prefix Cache | 加速比 |
| 8K system prompt + 2K query | 全部 10K token 做 prefill | 仅 2K prefill + 8K 解压 | ~3-5x |
| 32K 长文档 + 512 query | 全部 32.5K token 做 prefill | 仅 512 prefill + 32K 解压 | ~5-10x |
| 重复 system prompt (多用户) | 每个用户都重算 | 所有用户共享 cache | 线性增长 |
7.2 解压缩的额外开销
与标准 MHA 的 prefix cache "零开销命中" 不同,MLA 的 prefix cache 命中后仍有解压缩开销:
7.3 vLLM vs SGLang 实现对比
| 特性 | vLLM | SGLang |
| Prefix Cache 结构 | Paged Block + Hash Table | RadixTree (RadixAttention) |
| KV Cache 格式 | FP8 c_t [512] + BF16 k_rope [64] = 656B | 混合精度 buffer [576 dims] |
| Prefill 解压方式 | context chunk loop + kv_b_proj GEMM | chunk 切分 + MHA attn + merge |
| Attention 合并 | merge_attn_states (lse-based) | merge_attn_states (lse-based) |
| 最佳后端 | FlashInfer / FA4 for MLA | FlashAttention3 (chunked prefix cache) |
| Decode 路径 | Absorbed (FlashMLA / PagedAttn) | Absorbed (FlashMLA / CutlassMLA / FlashInfer) |
| 吞吐提升 | ~3-5x (prefix cache 场景) | ~7x (综合优化后) |
8. 总结
一句话总结:MLA 的 Prefix Cache 存的是压缩 latent (c_t + k_rope = 576 dims),但 Prefill 需要全量 KV (40,960 dims)。解决方案是在线解压缩 + 分块处理 + attention state 合并。
关键技术要点
| # | 要点 | 说明 |
| 1 | Cache 只存压缩表示 | c_t [512] + k_rope [64] = 576 dims,所有 128 heads 共享 |
| 2 | Prefill 必须解压 | 通过 kv_b_proj (= [W_UK; W_UV]) 将 c_t 投影回 per-head K/V |
| 3 | 分块处理控制显存 | 长 prefix 分成 chunk(如 4K token/chunk),每次只解压一个 chunk |
| 4 | LSE-based 合并 | 各 chunk 结果通过 log-sum-exp 加权合并,保证数学等价性 |
| 5 | Decode 无此问题 | Absorbed 路径直接用 c_t,矩阵吸收消除了解压需求 |
| 6 | 总体仍然值得 | 解压 GEMM 的开销远小于完整 Transformer 重算的开销 |
工程启示:MLA 的 prefix cache 不是"免费"的——它以额外的解压 GEMM 为代价换取了 ~100x 的缓存空间节省和高效的前缀共享。这个 trade-off 在实际生产中(多用户共享 system prompt、长文档问答等场景)收益非常显著。
MLA Prefix Cache & Chunked Prefill 深度解析 | 基于 vLLM / SGLang 源码分析 | 2025