vLLM 中的 DeepSeek V4:高效的长上下文注意力机制
我们很高兴地宣布,vLLM 现在支持 DeepSeek V4 模型系列(deepseek-ai/DeepSeek-V4-Pro 和 deepseek-ai/DeepSeek-V4-Flash)。
这些模型采用了一种高效的长上下文注意力机制,专为处理高达一百万 Token 的任务而构建。虽然新的注意力设计初看可能显得复杂,但一旦系统地检查,其基本原理就十分清晰。
本文档分为三个部分:
- 在 vLLM 上部署 DeepSeek V4 的快速入门指南
- DeepSeek V4 新架构设计的第一性原理说明
- 我们在 vLLM 上实现该模型时的优化方案概览:混合 KV 缓存、算子融合和去中心化推理(Disaggregated serving)。
这代表了我们初步的模型支持版本,更多的优化正在积极进行中。我们希望以下的技术说明能帮助开源社区理解该注意力机制本身,以及我们目前实现决策背后的逻辑。
在 vLLM 上运行 DeepSeek V4
DeepSeek V4 包含两个模型:一个参数量为 1.6T 的 DeepSeek-V4-Pro,以及一个参数量为 285B 的 DeepSeek-V4-Flash。两个模型均支持高达 100 万 Token 的上下文,而 vLLM 对这种新注意力机制的实现旨在适配这一上下文长度。
DeepSeek-V4-Pro
此处我们强调一种针对测试和原型设计优化的单节点部署方案,包含诸如 FP4 索引器和 MTP 等可选优化。以下命令可在 8xB200 或 8xB300 上运行。
docker run --gpus all \
--ipc=host -p 8000:8000 \
-v ~/.cache/huggingface:/root/.cache/huggingface \
vllm/vllm-openai:deepseekv4-cu130 deepseek-ai/DeepSeek-V4-Pro \
--trust-remote-code \
--kv-cache-dtype fp8 \
--block-size 256 \
--enable-expert-parallel \
--data-parallel-size 8 \
--compilation-config '{"cudagraph_mode":"FULL_AND_PIECEWISE", "custom_ops":["all"]}' \
--attention_config.use_fp4_indexer_cache=True \
--tokenizer-mode deepseek_v4 \
--tool-call-parser deepseek_v4 \
--enable-auto-tool-choice \
--reasoning-parser deepseek_v4有关更多部署策略(包括去中心化推理/更多 GPU 架构),请参考 部署方案 (recipes)。
DeepSeek-V4-Flash
此处我们强调一种针对测试和原型设计优化的单节点部署方案,包含诸如 FP4 索引器和 MTP 等可选优化。以下命令可在 4xB200 或 4xB300 上运行。
docker run --gpus all \
--ipc=host -p 8000:8000 \
-v ~/.cache/huggingface:/root/.cache/huggingface \
vllm/vllm-openai:deepseekv4-cu130 deepseek-ai/DeepSeek-V4-Flash \
--trust-remote-code \
--kv-cache-dtype fp8 \
--block-size 256 \
--enable-expert-parallel \
--data-parallel-size 4 \
--compilation-config '{"cudagraph_mode":"FULL_AND_PIECEWISE", "custom_ops":["all"]}' \
--attention_config.use_fp4_indexer_cache=True \
--tokenizer-mode deepseek_v4 \
--tool-call-parser deepseek_v4 \
--enable-auto-tool-choice \
--reasoning-parser deepseek_v4有关更多部署策略(包括去中心化推理/更多 GPU 架构),请参考 部署方案 (recipes)。
DeepSeek V4 注意力机制解析
长上下文推理面临两个主要挑战:
- KV 缓存内存增长:KV 缓存随上下文长度呈线性增长。虽然 DeepSeek 类模型使用 多头潜在注意力 (MLA),它比标准的多头注意力 (MHA) 或多查询注意力 (MQA) 更节省内存,但在 GPU 内存容量有限的情况下,扩展到 100 万 Token 仍然十分困难。
- 注意力计算成本:对长上下文进行注意力计算非常昂贵。即使使用 DeepSeek 稀疏注意力 (DSA) 等先验技术,计算依然是一个重大的瓶颈。
为了应对这些挑战,DeepSeek 团队设计了一种新的注意力机制,旨在同时压缩 KV 缓存并减少注意力计算时间。
- 共享键值向量(节省 2 倍内存)。为了保证准确性,我们对注意力输出应用逆向 RoPE 操作。
- 跨多个 Token 压缩 KV 缓存(节省 4 倍到 128 倍内存)。在 DeepSeek V4 中,有两种实现方式:
c4a:将 KV 缓存压缩约 1/4。一个压缩后的 Token 是8 个未压缩 Token 的加权和,步长为 4。c128a:将 KV 缓存压缩约 1/128。一个压缩后的 Token 是128 个未压缩 Token 的加权和,步长为 128。
- DeepSeek 稀疏注意力(限制注意力计算成本)。即使在使用
c4a注意力压缩 KV 缓存后,百万 Token 的序列仍然有 25 万个压缩 Token。为了加速计算,我们可以使用 DeepSeek 稀疏注意力 (DSA),仅关注前-个压缩 Token。 - 保留局部性:短滑动窗口。DeepSeek V4 使用大小为 128 的滑动窗口来处理局部信息,作用于未压缩的 Token,以便查询(Query)Token 在到达压缩边界之前可以关注局部信息。
为了更好地阐述这种新机制,这里有一个关于 c4a 注意力处理 13 个 Token 的动画。了解上述细节后,c128a 的情况也应该很容易理解。请启动 交互式版本 以悬停在 Token 上并检查连接情况。

这种高效的注意力设计带来了显著的 KV 缓存节省。使用 bf16 KV 缓存时,DeepSeek V4 在 100 万上下文下每序列仅占用 9.62 GiB 的 KV 缓存。这比 61 层 DeepSeek V3.2 风格堆栈的 83.9 GiB 估计值小了约 8.7 倍。实际上,我们对索引器缓存使用 fp4,对注意力缓存使用 fp8,这使得 KV 缓存大小相比 bf16 估计值进一步减少了约 2 倍!
关于算术计算和数学解释的更多细节,请参考附录。
vLLM 对 DeepSeek V4 的实现
尽管结构上实现了节省,但该注意力机制仍具有内在复杂性,而在 vLLM 中高效实现这些节省是一个系统问题,涉及多个实现挑战:
- 类似于 DeepSeek V3.2 模型,注意力算子在预填充(Prefill)阶段使用 bfloat16 KV 缓存,在解码(Decode)阶段部分使用 Token 维度的 fp8。
- 模型混合使用了
c4a和c128a注意力,部分注意力层仅使用滑动窗口来获取局部信息而无压缩。异构的注意力类型使得 KV 缓存管理变得极其复杂。 - 当对多个序列进行批处理(Batching)时,它们在 KV 缓存压缩边界方面可能处于不同状态。
- 模型原生支持 fp4 MoE 权重,这在 vLLM 中需要特殊处理。
除了注意力机制本身,还有其他几项更新,包括 流形约束超连接 (Manifold-Constrained Hyper-Connections) 等架构变化以及 MoE 模块的一些修改。这些内容不在本文讨论范围内,因为它们是较简单的模型更改,易于适配。
vLLM 通过两个方面的优化解决了这些挑战:内存管理和算子效率。
保持 KV 缓存内存的紧凑性
vLLM 的 KV 缓存内存分配器必须将多种 KV 状态紧凑地打包在 GPU 内存中,同时仍需与前缀缓存(Prefix caching)、预填充/解码分离、CUDA 图以及 vLLM 其他服务路径协同工作。以下三个设计选择确保了这一点。
(1) 单一逻辑块大小
不同的层以不同的速率进行压缩(c4a 为 1/4,c128a 为 1/128,SWA 为 1/1)。一个显而易见的设计是围绕压缩后条目的整数倍来调整每层的块大小。但这样每层都有自己的页面布局,分配器必须分别处理它们。
相反,我们将所有压缩层的逻辑块固定为 256 个原生 Token 位置。一个 c4a 块物理上持有 256 / 4 = 64 个压缩条目,而一个 c128a 块持有 256 / 128 = 2 个。分配一个块总是意味着预留请求上下文的下 256 个原生位置,无论属于哪一层。槽位映射、调度器核算和前缀命中检测都可以使用相同的单元,而无需根据 compress_ratio 进行分支判断。
(2) 作为滑动窗口的压缩器状态
每个压缩器层还为每个请求维护一个小的滚动残差(Residual):C4 为 8 个 Token(重叠)的部分状态,C128 为 128 个 Token 的部分状态。最初的自然设计是将该残差保存在每个请求的侧边缓冲区(Side buffer)中。这在隔离环境下有效,但一旦需要与服务栈的其他部分交互,就会变得尴尬。
使用侧边缓冲区,前缀缓存需要在每个可缓存边界快照滚动状态,将其与前缀哈希一起键入,并在命中时恢复。去中心化预填充需要第二条传输路径,将残差从预填充工作节点发送到解码工作节点。每个要求本身都可管理,但合在一起则增加了维护状态管理的负担。
vLLM 通过将压缩器状态视为滑动窗口 KV 来避免这种情况。运行时不变量是一致的:每个请求固定大小,随解码进度推进,窗口外的状态要么被丢弃,要么通过缓存处理。因此,我们将压缩器状态注册在滑动窗口 KV 缓存规范下,设定 sliding_window = coff * compress_ratio(C4 为 8,C128 为 128),并将其置于与混合 KV 缓存管理器相同的 SWA 风格块中。
这使得多个服务特性可以复用相同的抽象:
- 前缀缓存 复用正常的块语义。缓存命中落在 KV 缓存块边界(上述 256 位置单元),该边界的压缩器状态已经是正确的移交点。
- 去中心化预填充 将压缩器状态视为 SWA 状态。仅传输窗口内的块,既保留了传输大小节省,又无需引入单独的残差特定传输路径。
- CUDA 图 和 MTP 遵循与 SWA 相同的集成模式,同时保持特定于压缩器状态的元数据和实现细节。
(3) 统一页面大小
上述两点选择尚不足够。C4 索引器块、c128a KV 块和 c4a 压缩器状态块仍然具有不同的页面大小(每个块的字节数不同)。如果每种缓存类型都有自己的块池,我们最终会陷入试图消除的跨池碎片化问题。
幸运的是,每种缓存类型的页面大小是 block_size * compress_ratio * per_entry_size 的乘积,这三个因子都在我们的控制之下。如果仔细选择,不同类型的缓存可以坍缩成少数几个页面大小桶(Page-size buckets),每个桶由一个共享块池支持。
在我们的实现中,整个五路缓存栈适配为三个页面大小。每个池在加载时大小已定,分配变成了简单的桶查找。运行时无需重新分区,无需按类型核算,也无需缓存类型间的碎片化。
- 最大桶:
c4a主 KV、SWA KV、c4a压缩器状态、c128a压缩器状态。 - 中型桶: C4 索引器 KV、C4 索引器压缩器状态。
- 最小桶:
c128a主 KV。
保持 GPU 高负载
内存布局仅是运行时的故事一半;另一半是保持 GPU 计算负载饱和。
vLLM 集成了 FlashMLA 和 FlashInfer,它们提供了优化的注意力机制和 MoE 算子。但该模型需要许多小的、主要是内存受限的算子。我们需要避免额外的算子启动和 HBM 往返,否则会拖慢完整的解码路径。
(1) 算子融合 (Kernel Fusion)
我们部署了三种融合来减少内存往返。在下图中,这些表现为算子组周围的彩色轮廓。
- 压缩器 + RMSNorm + RoPE + 缓存插入。 压缩后,压缩后的 K 立即通过 RMSNorm、RoPE,并插入到随后的注意力 KV 缓存中(用于主注意力或索引器)。由于这些阶段几乎完全是按元素操作的,我们将它们融合成一个算子。我们保留单独的算子用于索引器 K 缓存和主注意力 K 缓存,以便并行策略可以针对每个头维度进行调整。总体而言,相比未融合的基准,我们实现了约 1.4-3 倍的加速。
- 逆向 RoPE + fp8 量化。 在主注意力之后,输出通过逆向 RoPE,然后进入
o_lora投影的 fp8 批处理矩阵乘法。融合两者避免了连续的 HBM 往返,提高了算术强度,相比未融合版本加速约 2-3 倍。 - 融合 Q 归一化 + KV RoPE + K 插入。 在主注意力之前,我们需要为压缩路径和滑动窗口路径插入 KV 缓存。压缩路径已由第一个融合覆盖,剩下的就是对查询和未压缩 SWA 键的逐元素操作。我们将这些工作水平融合到一个具有静态
warpID分派的单一算子中:每个 Warp 独立工作于 Q 头或 K 头,无需 Warp 间通信。这相比原始未融合算子带来了 10-20 倍的加速。
我们还复用了 DeepSeek V3.2 工作中的融合,包括 Q RoPE + 量化 + 权重乘法,以及注意力开始时在 QK 投影之后进行的 QK 归一化水平融合。
(2) 多流处理 (Multi-stream)
主注意力之前的操作高度可并行化。它们分为三部分:索引器计算、主注意力 KV 压缩和滑动窗口 Token 插入。在初始投影之后,这些分支几乎是独立的,因此我们在 CUDA 流之间重叠执行它们。同一图可以从第二个角度解读:蓝色带标记默认流,琥珀色带标记索引器流。
- 对于没有索引器的
c128a层,我们并行运行主 KV 压缩与 SWA Token 插入。 - 对于
c4a层,我们在其自己的流上并行运行完整索引器流水线与主 KV 压缩和 SWA Token 插入(后两者相对于彼此保持串行)。
通过这些重叠执行,我们在低 Batch 大小下观察到端到端延迟降低了 5-6%,这是一个有用的信号,表明解码路径减少了 GPU 利用不足的时间。
此外,我们像处理所有其他模型一样,使用 CUDA 图来减少解码路径上的启动开销。
有关完整实现,请查看 PR。
后续计划
我们正积极进行以下优化,以进一步提升 vLLM 上 DeepSeek V4 的性能:
- DeepGEMM MegaMoE 算子
- 分页预填充(Paged prefill)算子
当前实现主要针对 NVIDIA GPU,包括 Hopper 和 Blackwell 架构。这些加速器的部署方案可在 我们的方案网站 上找到。借助 vLLM 的可扩展插件系统,硬件供应商可以直接添加对模型的支持。例如,vllm-ascend 和 vllm-mlu 均独立支持 DeepSeek V4。
致谢
感谢 DeepSeek 团队开源 DeepSeek V4,并感谢 DeepSeek 领导层对 vLLM 的信任与支持!该模型支持得益于 Inferact Inc. 的贡献,该公司旨在将 vLLM 打造成世界级 AI 推理引擎,并通过降低推理成本和提高速度来加速 AI 进步。
附录:DeepSeek V4 注意力机制背后的数学原理
为何共享键值(Key & Value)时需要逆向 RoPE
给定位置处的查询 Token
是一个正交矩阵,即
给定一组位于位置
对于位于位置
注意力输出为(为简洁起见,省略了缩放因子等部分细节)
注意力输出的一个优良属性是其平移不变性。任何依赖于位置的因子,即
如果我们共享键向量和数值向量,注意力输出将为
现在输出通过旋转矩阵直接携带了绝对位置信息
这样,输出通过旋转矩阵仅携带相对位置信息
类似的讨论也可在 https://kexue.fm/archives/10862 中找到。
实现细节:确切的位置范围与因果条件
处理压缩 KV 缓存时必须小心。对于每个压缩索引
对于 c4a,第
对于 c128a,索引
为了因果性,我们需要确保位于位置c4a)或c128a)。
实现细节:c4a 和 c128a 中 k 的具体数值
对于 DeepSeek V4 中的 c4a 注意力,默认值为c128a 注意力,默认值为
c128a 注意力具有更大的压缩率。在 100 万 Token 的上下文中,它最多拥有 8k 个压缩 Token。8k 个 Token 对注意力计算来说不是问题,因此我们可以在 c128a 压缩 Token 上简单地使用完整注意力。实现上,我们仍然可以将 c128a 注意力建模为一个稀疏注意力问题,其前-
实现细节:为何需要短滑动窗口
使用 c128a 时,位于位置
实现 8.7 倍节省的算术计算推导
对于 1M 上下文的序列
带 bf16 KV 缓存的 DeepSeek V3.2
- 每层每 Token 的 MLA 缓存
字节。 - 每层每 Token 的索引器缓存
字节。 - 每层每 Token 的总缓存状态
字节。 - 在 1,048,576 个 Token 时
每层 GiB。 - 超过 61 层:约
GiB。
61 层带 bf16 KV 缓存的 DeepSeek V4
- 每个共享 KV 缓存条目存储
字节。 - 每个
c4a索引器缓存条目存储字节。 c4a层:共享 KV 缓存字节加上索引器缓存 字节,总计约 MiB。 c128a层MiB。 - 30 个
c4a层和 31 个c128a层总计:约GiB。