DeepSeek V4 稀疏注意力完全解剖:1M context 从 84GiB 压到 9.6GiB 是怎么做到的

26 分钟阅读

DeepSeek V4 稀疏注意力完全解剖:1M context 从 84GiB 压到 9.6GiB 是怎么做到的

前情说明:AI Infra 综述系列讲了推理引擎六大主线,其中"Attention 机制革命"是最重最难的一条。本篇是技术深度篇 D1,专门挖 DeepSeek V4 的稀疏注意力——从数学原理到 KV cache 布局,再到 kernel fusion 和多 stream 编排,一层不落地讲清楚。 素材主要来自 vLLM Blog《DeepSeek V4 in vLLM: Efficient Long-context Attention》(2026-04-24),一篇非常良心的深度技术文。所有数字、公式、架构决策都来自这篇官方 blog + 附录数学推导。


起手:不可思议的数字

先把这些数字放一起,就能明白为什么这个东西值得单开一篇深挖:

模型 Context KV cache/序列 KV cache/token/layer
DeepSeek V3.2 1M 83.9 GiB 1408 bytes
DeepSeek V4 (bf16) 1M 9.62 GiB 平均 ~160 bytes
DeepSeek V4 (fp4 indexer + fp8 attn) 1M ~4.8 GiB 平均 ~80 bytes

8.7 倍 KV cache 压缩。不是靠"减少 layer 数"或者"缩小 hidden dim"这类粗暴手段,是靠在 attention 机制上做四层组合优化。

更重要的是——质量几乎没有损失。DeepSeek V4-Pro(1.6T)和 V4-Flash(285B)都是当前 SOTA 之一。

这就是这一代大模型的关键秘密——软硬件 co-design 已经不够,还需要模型架构本身为长上下文 + 高效推理从头设计。DeepSeek V4 是当前最完整的示范。


第一层:Long-context 的两座大山

任何 1M+ context 模型都要同时解决两个爆炸问题:

问题 A:KV cache 内存爆炸

标准 attention 的 KV cache 是 O(N × H × D × L):

  • N = context 长度
  • H = KV head 数
  • D = head dim
  • L = layer 数

1M context × 61 layers × 每 token 每 layer 1408 bytes = 83.9 GiB 单请求。B300 那 288GB HBM 都只能装 3 个并发用户。根本不能商用。

问题 B:Attention 计算爆炸

Attention 是 O(N²)——1M query × 1M key = 1 万亿次点积。即使 KV cache 装得下,算也算不动。

DeepSeek V4 对这两座山分别下药:

  • 对 KV 内存爆炸:三层压缩(Shared KV + 多粒度压缩 + 稀疏 attention 只读 top-K)
  • 对计算爆炸:DSA 稀疏 attention + 短滑动窗口

第二层:四大机制拆解

机制一:Shared KV — 让 K 和 V 共用一份存储

标准做法:K 和 V 是两个独立 tensor,各存一份。

DeepSeek V4 做法:K 和 V 用同一份 tensor——立即省一半 KV cache。

天真到爆的问题:K 和 V 的物理含义不同——K 用来算 attention weight(跟 Q 做点积),V 是被加权求和的实际信息。逼它们相等等于说"用来查找的地址和被查找的内容一样"——这在数学上没道理。

但 DeepSeek 这么做了,还需要一个补偿操作——Inverse RoPE——保证 attention 输出的平移不变性(translation invariance)。

深潜:为什么必须做 Inverse RoPE

这是全文数学最硬的一段,慢慢来。(vLLM blog 附录里有推导,我给你翻译成人话)。

先回忆 RoPE (Rotary Position Embedding) 的性质:

  • 对位置 m 的 query 应用旋转 R_m,对位置 n 的 key 应用 R_n
  • 关键性质:R_m · R_n^T = R_(m-n)(旋转矩阵的差等于位置差的旋转)

标准 attention(K 和 V 独立时):

output_m = Σ_n softmax((R_m · q)^T (R_n · k_n)) · v_n
        = Σ_n softmax(q^T · R_(m-n) · k_n) · v_n     ← 只依赖相对位置 (m-n)

平移不变——所有 token 一起挪 100 个位置,输出不变。这是好性质。

现在 DeepSeek V4 让 K = V:

output_m = Σ_n softmax(q^T · R_(m-n) · k_n) · (R_n · k_n)
                                             └─ V 也带 RoPE,因为它就是 K

注意最后那个 R_n——它是绝对位置(不是相对位置)。这个 R_n 让输出带上了绝对位置信息——违反了平移不变性。

补救:对 output 应用 inverse RoPE:

output_m' = R_m^{-1} · output_m
         = Σ_n softmax(q^T · R_(m-n) · k_n) · R_(n-m) · k_n
                                              └─ 又变成相对位置!

平移不变性恢复了——R_(n-m) 只依赖 n-m。

代价:每次 attention 输出后要做一次 R_m^{-1} 旋转。计算量微不足道(元素级操作),但代码里必须显式加。vLLM 的实现里,Inverse RoPE 跟后面的 fp8 quant fused 成一个 kernel(2-3x 加速)。

机制二:多粒度 KV 压缩 — c4a 和 c128a

核心思想:几个连续的 token 的 KV 合并成一个。

具体两种压缩率:

c4a(compress ratio ≈ 1/4):

原始 8 个 token:  t0  t1  t2  t3  t4  t5  t6  t7
压缩规则:        每 4 个 token 打一个包 (stride=4),包内含 8 个 token 的加权和
                └────────────┬─────────────┘    重叠区
                             └────────────┬─────────────┘
                                          ...

压缩后 tokens:  T0(=t0..t7 加权)  T1(=t4..t11 加权)  ...
  • 每 8 个原始 token 压成 1 个 compressed token
  • Stride 4 → 相邻两个 compressed token 有 4 token 重叠
  • 有效压缩率约为 1/4(因为重叠)

c128a(compress ratio ≈ 1/128):

  • 每 128 个原始 token 压成 1 个(stride 128,无重叠)
  • 极致压缩,但保留信息更粗糙

DeepSeek V4 的 61 层用不同压缩策略:

  • 一部分 c4a 层(保留局部细节)
  • 一部分 c128a 层(保留全局结构)
  • 少量纯 SWA 层(只保留 128 token 局部窗口)

这是 DeepSeek 的深度洞察——不同 layer 的 attention 承担不同粒度的信息聚合职责,所以压缩率应该不同。近处细节靠 c4a,远处大结构靠 c128a。

机制三:DeepSeek Sparse Attention (DSA) — 只关注 top-K

问题:c4a 压缩后,1M context 还有 250K 个 compressed token。做 attention 还是要算 Q·K^T over 250K 长度——依然巨大。

解法:Top-K 稀疏 attention——每个 query 只跟 K 个"最相关"的 compressed token 算 attention,其他直接忽略。

具体 K 值:

  • c4a:K = 512(1M context 上从 250K 里选 512,稀疏率 1/500)
  • c128a:K = 8192(1M context 上从 8K 里选 8K,等于全 attention——因为 8K 不算大)

Top-K 怎么选? 这里需要一个"廉价的相关性打分器"——Indexer。

Indexer 机制:一个极小的辅助 attention,专门用来给所有 compressed token 打分,然后 top-K 送到 main attention。因为 indexer 本身很轻(用 fp4 存),所以它的开销可控。

这就是 DSA 的名字来源——Deepseek Sparse Attention。它是 top-K 稀疏 attention 加上一个 indexer 层来选 top-K。

机制四:Short Sliding Window (SWA) — 保留局部细节

问题:c128a 有个致命 bug——第一个 compressed token 覆盖 position 0-127,但 query 在 position 5 时按照 causality 不能看到 position 5 之后的信息,也就是不能看 c128a 的第一个 compressed token。query 早期完全无 KV 可看。

解法:加一个 128-token 的短滑动窗口,作用在未压缩的原始 token 上。

Query at position m:
    Sees c4a/c128a compressed tokens (远处 & 中距离信息)
    +
    Sees uncompressed tokens in [m-128, m-1] (紧邻局部信息)

收益:保证 causality 的同时不丢局部细节。128 是个精心选的数字——够短所以 KV cache 开销小(每层多存 128 个未压缩 token),够长所以能覆盖到最近的 compressed token 之前的所有位置。


第三层:把这些机制塞进 vLLM 需要面对的三个工程难题

现在你懂了模型侧。真正的痛苦在推理引擎侧:

  • 异构 layer:61 层里有 c4a、c128a、SWA 三种 attention 类型,KV 存储方式各不相同
  • 动态状态:每个 request 在不同 layer 的压缩边界位置都不同(比如某个 request 在 c4a 层可能刚好在 stride 边界,在 c128a 层却在窗口中间)
  • 多种量化:Prefill 用 bf16 KV,decode 部分用 token-wise fp8,indexer 用 fp4

KV cache allocator 之前只需要管一种 KV 布局(PagedAttention 的 fixed-size block)。现在要同时管五种不同 page size 的 cache——vLLM 是怎么做到不爆炸的?

三个精妙的设计。

设计 1:统一的"逻辑 block size" — 256 个原始 token

天真做法:为每个 layer 单独设计 block size,比如 c4a 层的 block = 64 个 compressed tokens(对应 256 个原始 tokens),c128a 层的 block = 2 个 compressed tokens(对应 256 个原始 tokens)。这样 block 内的 compressed entry 数是"漂亮的整数"。

问题:每个 layer 有自己的 page 布局。Slot mapping、scheduler 记账、prefix hit 检测都要按 layer 分类处理——巨大的复杂度。

vLLM 的做法:逻辑 block size 统一为 256 个原始 token 位置,不管是什么压缩率:

  • c4a block 物理上放 256 / 4 = 64 个 compressed entries
  • c128a block 物理上放 256 / 128 = 2 个 compressed entries
  • SWA block 物理上放 256 个原始 tokens

关键收益:分配一个 block 永远等于占 256 个原始 token 位置——不管哪层。Scheduler 只需要按"原始 token 位置"记账,完全不用分支处理压缩率。

这就是抽象的力量——把物理布局的差异藏在"每个 block 装多少 entry"这个变量里,暴露给上层的接口保持完全一致。

设计 2:Compressor state 复用 SWA 的抽象

DeepSeek V4 每一个压缩 layer 都有一个"rolling residual"——它是当前尚未完全压缩的中间状态:

  • c4a 层需要保留最近 8 个原始 token 的部分聚合状态(用来产生下一个 compressed token)
  • c128a 层需要保留最近 128 个原始 token 的状态

天真做法:为每个 request 单独开一个 side buffer 存这个 residual。

问题一堆:

  • Prefix caching 需要在每个可缓存边界快照 residual 状态,附到 hash key 上
  • Disaggregated prefill 需要额外一条传输通道把 residual 从 prefill worker 传到 decode worker
  • CUDA Graph、MTP、metrics 都要分别处理这个额外状态

vLLM 的洞察:residual 的行为跟 sliding window KV 一模一样——

  • 都是每 request 固定大小
  • 都是随着 decode 前进而滚动更新
  • 窗口外的都可以丢弃或缓存

所以 vLLM 直接把 compressor state 注册为 sliding window 类型的 KV:

  • sliding_window = coff × compress_ratio(c4a 是 8,c128a 是 128)
  • 用 SWA-style block 存储,塞进同一个 hybrid KV manager
  • Prefix caching / Disagg prefill / CUDA Graph 全部免费复用 SWA 的处理逻辑

这个决策的伟大之处:它把一个"看起来完全不同的新东西"识别成"其实是老朋友的另一副面孔"——减少了整个系统需要维护的状态种类。

好的抽象就是这样——把 N 个新问题变成 1 个已解决的问题的变体。

设计 3:统一 page size 到三个桶

新问题:即使有了统一 block size,不同 cache 种类的物理字节数还是不同:

  • C4 indexer block:256 个原始 token × 1/4 × 每 indexer entry 大小 = X bytes
  • c128a KV block:256 × 1/128 × entry size = Y bytes
  • c4a compressor state:8 × entry size = Z bytes
  • 等等...

如果每个 cache 种类独立开一个 block pool,还是会有跨池碎片——pool A 空着但 pool B 满了,请求过来只能拒绝。

vLLM 的解法:精心选择每种 cache 的三个因子(block_size × compress_ratio × per_entry_size),让所有 cache 种类压到只有 3 种 page size:

  • 最大桶:c4a main KV / SWA KV / c4a compressor state / c128a compressor state
  • 中等桶:C4 indexer KV / C4 indexer compressor state
  • 最小桶:c128a main KV

同一个桶内的所有 cache 种类共享一个 block pool——分配是纯 O(1) 的 bucket lookup,零碎片。

这个设计很像 malloc allocator 的 size class——把大小相近的分配请求打到同一个 size class 的 pool。tcmalloc / jemalloc 这套思路搬到了 KV cache。


第四层:Kernel Fusion — 三大融合点

内存布局搞定了,性能还差半个 GPU——因为 DeepSeek V4 attention 涉及大量小的、mostly memory-bound 的 kernel。每个 kernel 都要一次 HBM 读+写往返,累加起来 decode 路径慢得可怜。

解决方案:Kernel Fusion(内核融合)——把多个小 kernel 融合成一个大 kernel,减少 HBM 往返。

vLLM 在 DeepSeek V4 上做了三处融合,每处收益都很惊人。

融合点 1:Compressor + RMSNorm + RoPE + Cache Insertion

流程(未融合时):

Compressor 层 → 输出 compressed K
    ↓ HBM 写入
    ↓ HBM 读出
RMSNorm 层    → 归一化后的 K
    ↓ HBM 写入
    ↓ HBM 读出
RoPE 层       → 加旋转位置的 K
    ↓ HBM 写入
    ↓ HBM 读出
KV Cache Insertion → 写入 KV cache
    ↓ HBM 写入

问题:4 次 HBM 读+写往返。每一步都是内存 bound,算力浪费。

融合后:一个大 kernel,一次读入、一次写出。中间状态全部在 shared memory / registers 里。

Kernel 分两个版本——一个给 indexer K cache,一个给 main-attention K cache——因为两者 head dim 不同,最优 parallelization 策略也不同。

收益:1.4-3x 加速。

融合点 2:Inverse RoPE + FP8 Quantization

流程(未融合时):

Main Attention 输出
    ↓
Inverse RoPE (前面讲的补偿)
    ↓ HBM 往返
FP8 Quant (量化到 fp8 供后续 o_lora projection 用)
    ↓ HBM 往返
FP8 Batched Matmul (o_lora projection)

融合后:Inverse RoPE + FP8 Quant 融合成一个 kernel,避免了中间 HBM 往返。

为什么这个融合特别有效? 因为它显著提升 arithmetic intensity——Inverse RoPE 本身是元素级操作(memory bound),FP8 quant 也是。融合后单次读入一次写出,把两次操作的算力全塞进一次 HBM 访问的时间里。

收益:2-3x 加速。

融合点 3:Fused Q Norm + KV RoPE + K Insert(水平融合)

Main attention 之前需要做几件事:

  • Query 上做 Q Norm
  • Uncompressed SWA 路径的 Key 上做 RoPE + KV insert
  • Compressed 路径的 K 已经在融合点 1 里处理过了

融合前的问题:这三个操作元素级、彼此独立——都在争抢 HBM 带宽。

融合后:水平融合(horizontal fusion)——一个 kernel 同时并行处理 Q head 和 K head。

关键实现细节:static warpID dispatch:

  • 一个 warp 完全负责一个 Q head 或一个 K head
  • 不同 warp 之间无需通信——避免了 warp-level barrier / atomic
  • 每个 warp 独立跑自己的计算,static 分配决定谁做什么

为什么这个技巧收益特别大? 因为 GPU 上 warp 间通信是最贵的——每次同步都要花上百个 cycle。static warpID dispatch 完全消除了这个开销,同时保持了并行度。

收益:10-20x 加速(是这三个融合里收益最大的,因为原始 kernel 太浪费了)。


第五层:Multi-Stream 并行 —— 让不同分支并发跑

Kernel fusion 让每个 kernel 更快,但kernel 之间还是串行执行——如果能让多个独立分支同时跑,还能压 5-6%。

DeepSeek V4 Attention 的分支结构

对于 c4a 层(有 indexer),main attention 前的操作可以拆成三条独立分支:

  • 分支 A:Indexer 计算(给 DSA 打分选 top-K)
  • 分支 B:Main-attention KV compression(c4a 主路径)
  • 分支 C:Sliding-window token insertion(SWA 路径)

初始 projection 完成后,这三条分支互相独立——不共享中间状态。

CUDA Stream 编排

vLLM 用 两个 CUDA stream:

  • Default stream (blue band):跑分支 B + C
  • Indexer stream (amber band):跑分支 A

对于 c4a 层:indexer pipeline 完全并行跑在 amber stream 上,跟 blue stream 上的 KV compression + SWA insertion 并发。

对于 c128a 层(无 indexer):main KV compression 和 SWA token insertion 并发跑(在同一 stream 里也可能重叠)。

收益:低 batch size 下 5-6% 端到端延迟下降。

5-6% 听起来不多——但要知道这是在已经充分 kernel fusion 的基础上再挤出来的。这是"最后一公里"优化。

为什么低 batch 收益更明显?

高 batch 时 GPU 已经很饱和了,multi-stream 增益有限;低 batch 时 GPU 时不时空转,用 multi-stream 把不同 kernel 挤到同一时刻跑,显著提高利用率。

这跟前面讲过的 SGLang zero-overhead scheduler 是同一个哲学——GPU 永远不能停,能用的每一纳秒都要榨干。


第六层:CUDA Graph 收尾

最后一层——vLLM 对 DeepSeek V4 decode 路径整体做 CUDA Graph 捕获。

为什么需要 CUDA Graph?

CUDA API 有个隐性成本:每次 kernel launch 大约 5-10μs 的 CPU 开销。DeepSeek V4 一次 decode 涉及几十个 kernel(每层几个融合 kernel × 61 层),累加起来就是几百微秒的 launch overhead——对 10ms 级 decode 来说是5-10% 的成本。

CUDA Graph 一次捕获、多次 replay:

  • 第一次 decode 捕获整个操作图
  • 后续 decode 只需要"replay"这张图——launch overhead 几乎归零

关键前提:图的结构必须相同——batch size、sequence length(quantized 到某个 tier)都得固定。所以 vLLM 用 FULL_AND_PIECEWISE 模式,同时捕获多个尺寸的图(多个 tier),运行时选最贴合的 tier 用。

DeepSeek V4 的 config 里就有这一行:

--compilation-config '{"cudagraph_mode":"FULL_AND_PIECEWISE", "custom_ops":["all"]}'

全景图:从模型到 GPU 的一次 decode

把前面所有内容串起来——DeepSeek V4 从"query 到达"到"下一个 token 生成"完整的一次 decode 流程:

1. Scheduler 决定这一步跑哪些 request
   ↓
2. 分配 KV block(256 原始 token / block)
   - c4a layer: 64 compressed entries
   - c128a layer: 2 compressed entries
   - SWA layer: 256 uncompressed tokens
   - compressor state 走 SWA 抽象
   ↓
3. Decode forward(每层重复以下步骤,全部 CUDA Graph replay)
   ↓
   A. QKV Projection (Q RoPE + quant + weight multiply 融合)
   ↓
   B. Q Norm + KV RoPE + K Insert(水平融合,static warpID)
   ↓
   C. 多 stream 并行:
      Stream A (indexer): 计算 indexer scores → top-K selection
      Stream B (main):    KV compression → RMSNorm → RoPE → cache insert (Fusion 1)
      Stream C (SWA):     Sliding window token insertion
   ↓
   D. Main Attention(FlashMLA kernel,只 attend top-K compressed + local SWA)
   ↓
   E. Inverse RoPE + FP8 Quant (Fusion 2)
   ↓
   F. o_lora projection (fp8 batched matmul)
   ↓
   G. MoE Layer (FlashInfer / DeepGEMM MegaMoE kernel)
   ↓
   H. 下一层...
   ↓
4. 生成下一个 token → 返回 scheduler → 循环

每一个步骤都被优化到极致:

  • Kernel fusion 消除 HBM 往返
  • Multi-stream 消除 kernel 间空隙
  • CUDA Graph 消除 launch overhead
  • Static warpID 消除 warp 间通信
  • KV cache 布局消除跨类型 fragmentation

这就是"生产级"的含义——不是一个算法能跑,是堆叠十几层微观优化,让它跑得像单一模型那么高效。


尾声:这一课的三个启示

从 DeepSeek V4 这个案例,能提炼出三个通用规律——不只关于 attention,是关于整个 AI Infra:

启示 1:模型架构和推理引擎必须 co-design

过去:模型团队做模型,engine 团队做 engine,"能对上就行"。

现在:DeepSeek V4 的很多设计(shared KV / compressor sliding window / 三种压缩率 mixed layers)没有 vLLM 团队参与讨论根本落不了地。反过来 vLLM 的三种设计选择(unified logical block / SWA 抽象复用 / 三桶 page size)没有对模型架构深入理解就做不出来。

未来所有头部模型都会走这条路——设计模型时 engine 团队就在参与。这个门槛会淘汰绝大多数没有推理团队的模型公司。

启示 2:好的抽象胜过精妙的优化

vLLM 团队最厉害的不是三个 kernel fusion,是**"把 compressor state 识别为 SWA 变体"**这个抽象决策。

它把一个"新增 5 种 cache 类型"的问题变成"复用现有 SWA 抽象"的问题。整个 prefix caching / disagg prefill / CUDA graph / MTP 都免费兼容。

这是软件工程的最高境界——用抽象消灭复杂度,而不是用优化对抗复杂度。

启示 3:性能是"层层碾压"叠加出来的

DeepSeek V4 的 8.7x KV 压缩不是靠一个大招——是靠:

  • Shared KV (2x)
  • c4a 压缩 (4x)
  • c128a 压缩 (128x,部分层)
  • DSA top-K (计算量 500x 稀疏率)
  • FP8/FP4 量化 (2x)
  • ... 每一项都是独立的 2x-100x 提升

性能不是"找一个 silver bullet"——是"在每一层堆一个 2x,最后乘起来"。

这也是为什么 AI Infra 领域会长期需要专家——每一层的 2x 都需要深入理解才能拿到。做通用工具的人只能拿到某一层的收益,做深度优化的团队能把 10 层 2x 全拿到。


附:素材来源

主线

论文 & 参考

社区解读

姊妹篇


整理于 2026-07-31 深夜。原文技术细节归属 vLLM 团队与 DeepSeek 团队。数学推导部分基于官方 blog 附录 + 苏剑林公开讨论。中文表达可能有解读偏差,请以原文为准。

Zoe

Written by

Zoe

AI Infra Engineer · LLM Serving · GPU/RDMA · 造工具的偏执狂

评论