MiracleFarms — AI Infrastructure in public
MiracleFarms › Fields

研究笔记 · Field Note

vLLM KV Cache 类型体系拆解:十种规格的公式推导与横向对比

基于 vLLM v1 源码,拆解 KVCacheSpec 继承体系中十种缓存类型的 page size 公式、内存增长模式与存储池分配逻辑。

题图

版本声明:本文分析基于 vLLM commit 87e31455b(2026-05-20);除非特别说明,以下描述均基于此版本。

一个标准 Llama 模型的 KV cache 每层每 token 需要 2 × num_kv_heads × head_size × dtype_size 字节——以 Llama-3-8B(GQA,8 个 KV head,head_size 128,BF16)计算,每 token 每层 4,096 字节。DeepSeek V3 的 MLA 把同样的信息压缩到 (kv_lora_rank + qk_rope_head_dim) × dtype_size = 576 × 2 = 1,152 字节,缩小约 3.6 倍。Jamba 的 Mamba 层根本不产生 KV 对,只维护固定大小的 SSM 状态。Mistral 的滑动窗口层只保留最近 4,096 个 token 的 KV 数据。

一个推理框架要同时管理这些截然不同的内存布局。vLLM v1 通过 KVCacheSpec 类型体系,把全部多样性压缩到一个问题上:一个 page 占多少字节。回答了这个问题,PagedAttention 的 block 分配、prefix caching、PD 传输就可以在同一套机制下运行,不需要为每种注意力机制写独立的内存管理器。

本文拆解这套类型体系的继承设计、各类型的 page size 公式推导、内存增长模式的分类,以及存储池 block 数量的计算方法。

一、类型体系:一个基类,十种规格

vLLM v1 的 KV cache 类型通过继承体系组织[1]。基类 KVCacheSpec 定义了所有类型必须回答的三个问题:page_size_bytes(一个 block 占多少显存)、max_memory_usage_bytes(单个请求最多消耗多少显存)、max_admission_blocks_per_request(单个请求最多占用多少个 block)。

继承层次如下:

KVCacheSpec (base)
├── AttentionSpec
│   ├── FullAttentionSpec
│   │   ├── MLAAttentionSpec
│   │   │   └── HiddenStateCacheSpec
│   │   ├── SinkFullAttentionSpec
│   │   └── TQFullAttentionSpec
│   ├── SlidingWindowSpec
│   │   └── SlidingWindowMLASpec
│   ├── ChunkedLocalAttentionSpec
│   ├── EncoderOnlyAttentionSpec
│   └── CrossAttentionSpec
├── MambaSpec
└── UniformTypeKVCacheSpecs (多层聚合)

这棵树的分叉点揭示了 KV cache 世界的分类逻辑。第一个分叉把 Attention 和 Mamba 分开——这是两种根本不同的序列建模机制[4],一种缓存 token-level KV 对,一种维护固定大小的状态向量。第二个分叉在 Attention 内部:FullAttention 保留所有历史 token,SlidingWindow 只保留最近的窗口,ChunkedLocal 按固定 chunk 大小截断,CrossAttention 缓存编码器输出。

UniformTypeKVCacheSpecs 不是第十一种缓存类型,而是多层聚合容器。当一个模型所有层的 KVCacheSpec 类型相同时,它把这些层打包成一个 group,存储池只需要计算一次 page_size × num_layers。混合模型(比如 Jamba 的 Attention + Mamba 交替层)会产生多个 group,每个 group 内部类型统一,group 之间可以不同。

二、Page Size 公式:差异的核心

page_size 是整个类型体系的核心量。所有下游计算——block 数量、可服务请求数、传输字节数——都由它决定。不同类型之间 page_size 的差异,直接决定了同一块显存能承载多少并发请求。

2.1 FullAttentionSpec:标准基线

标准全注意力为每个 token 缓存 K 和 V 两个张量,维度分别是 num_kv_heads × head_size_k 和 num_kv_heads × head_size_v。一个 block 包含 block_size 个 token,因此:

page_size = block_size × num_kv_heads × (head_size_k + head_size_v) × dtype_size

大多数模型里 head_size_k == head_size_v,公式简化为:

page_size = 2 × block_size × num_kv_heads × head_size × dtype_size

以 Llama-3-8B(GQA,num_kv_heads = 8,head_size = 128,BF16,block_size = 16)为例:

page_size = 2 × 16 × 8 × 128 × 2 = 65,536 bytes = 64 KB / layer

GQA 将 num_kv_heads 从完整的 num_attention_heads(如 32)压缩到更少的 key-value head 数(如 8),page_size 线性缩小。这也是 GQA 成为主流的直接工程原因——它在 KV cache 层面切掉了 4 倍显存开销。

GPU 上的实际 tensor 形状因后端而异。FlashAttention 将 K 和 V 合并存储为 (2, num_blocks, block_size, num_kv_heads, head_size),维度 0 的 2 代表 K 和 V。FlashInfer 则将 block 放在第一维,K 和 V 拆成独立 tensor。这种布局差异不影响 page_size 计算,但会影响后面的传输粒度。

2.2 MLAAttentionSpec:低秩压缩

DeepSeek V2/V3 的 MLA(Multi-head Latent Attention)[3]将所有 head 的 KV 信息压缩到一个低秩向量,取代了独立的 per-head KV 存储。在 KVCacheSpec 层面,MLA 的 num_kv_heads 固定为 1,head_size 等于 kv_lora_rank + qk_rope_head_dim:

page_size = storage_block_size × 1 × (kv_lora_rank + qk_rope_head_dim) × dtype_size

其中 storage_block_size = block_size / compress_ratio。DeepSeek V3 的典型参数是 kv_lora_rank = 512,qk_rope_head_dim = 64,因此 head_size = 576。

MLA 的 FP8 量化版本进一步压缩,采用定制化的字节布局。DeepSeek V3.2 每 token 固定 656 字节(包含 FP8 数据和 block scale):

page_size(V3.2 FP8) = block_size × 656

DeepSeek V4 引入 compress_ratio 后每 token 降到 584 字节:

page_size(V4 FP8) = (block_size / compress_ratio) × 584

同等配置下,MLA 每层 page_size 通常只有标准 MHA 的十分之一到五分之一。这个差距直接决定了 DeepSeek 系列在长上下文推理时的内存效率优势。

2.3 量化变体

量化不改变 page_size 的结构,但改变有效 dtype_size。三种量化模式各有不同的存储开销。

Per-token-head 量化(FP8 或 INT8 per-token-head)在标准 page_size 基础上,为每个 token-head 组合额外存储一个 float32 scale,K 和 V 各一份:

page_size(per_token_head) = standard_page_size + 2 × block_size × num_kv_heads × 4

NVFP4 量化把每个 head_size 维度映射为 h/2 + h/16——前半是 FP4 数据(每两个元素打包为一个字节),后者是 FP8 block scale(每 16 个元素一个 scale):

nvfp4_dim(h) = h/2 + h/16
page_size(NVFP4) = block_size × num_kv_heads × (nvfp4_dim(h_k) + nvfp4_dim(h_v)) × dtype_size

TurboQuant 使用自定义的 slot 大小替代标准的 head_size 计算:

page_size(TQ) = block_size × num_kv_heads × tq_slot_size

2.4 其他类型

SlidingWindowSpec 和 ChunkedLocalAttentionSpec 的 page_size 公式与 FullAttentionSpec 完全相同——它们在单 block 存储层面没有差异,区别体现在下一节的内存增长模式上。

SinkFullAttentionSpec 同样继承 FullAttention 的 page_size,额外管理 sink_len 个 token 的 block(attention sink 始终保留在缓存中,不被驱逐)。

CrossAttentionSpec(如 Whisper 的编码器-解码器架构)page_size 与 FullAttention 相同,但最大内存受限于编码器长度:max_memory = ⌈max_encoder_len / block_size⌉ × page_size。

MambaSpec 与 attention 系列完全不同。Mamba 缓存的是 conv 状态和 SSM 状态,page_size 由各状态张量的维度之和决定[4]:

page_size(Mamba) = Σᵢ (∏ shapes[i]) × dtype_size[i]
                 = conv_state_size × conv_dtype_size + ssm_state_size × ssm_dtype_size

在混合模型中,page_size_padded 参数可将 Mamba 的 page 大小对齐到 Attention 层的 page 大小,让 block allocator 维护单一 free list。

EncoderOnlyAttentionSpec(BERT 类模型)不需要 KV cache,max_memory 返回 0。

2.5 Page Size 横向对比

类型 page_size 公式 关键变量 相对大小
FullAttention B × H_kv × (d_k + d_v) × s num_kv_heads 基线
SlidingWindow 同 FullAttention num_kv_heads = 基线
ChunkedLocal 同 FullAttention num_kv_heads = 基线
SinkFull 同 FullAttention num_kv_heads = 基线
CrossAttention 同 FullAttention num_kv_heads = 基线
MLA B' × 1 × d_latent × s kv_lora_rank ≈ 基线/5~10
SlidingWindowMLA 同 MLA kv_lora_rank ≈ 基线/5~10
TurboQuant B × H_kv × tq_slot tq_slot_size < 基线
Mamba Σᵢ(∏ shapes[i]) × s[i] conv+ssm dim 独立
EncoderOnly 0 — 不分配

B = block_size, H_kv = num_kv_heads, d_k/d_v = head_size, s = dtype_size, B’ = storage_block_size, d_latent = kv_lora_rank + qk_rope_head_dim

这张表揭示了一个结构性事实:page_size 的差异主要来自两个维度——head 压缩方式(MLA vs MHA vs GQA)和量化策略。SlidingWindow、ChunkedLocal、SinkFull 这些变体在 page_size 层面与 FullAttention 完全相同,它们的真正差异体现在内存增长模式上。

三、内存增长的三条曲线

page_size 决定单个 block 的代价,内存增长模式决定一个请求随上下文增长需要多少个 block。这两者相乘,才是真正的内存占用。所有 KV cache 类型的内存增长归入三种模式。

3.1 线性增长

FullAttention、MLA、Mamba(“all”) 模式下,每个新 token 都需要被缓存,所需 block 数随上下文线性增长:

blocks(L) = ⌈L / B⌉
memory(L) = ⌈L / B⌉ × page_size × num_layers

其中 L 是上下文长度,B 是 block_size。对一个 32 层、每层 page_size = 64 KB 的模型,每 1,000 个 token 的 KV cache 增量约为:

⌈1000/16⌉ × 64 KB × 32 = 63 × 64 KB × 32 ≈ 129 MB

线性增长是最”昂贵”的模式,也是长上下文推理的主要内存瓶颈。

当启用上下文并行(DCP × PCP)时,max_model_len 按并行度等分,每个 rank 只持有 ⌈max_model_len / (dcp × pcp)⌉ 长度的 KV cache,公式中的 L 上界相应缩小。

3.2 有界增长

SlidingWindow 和 ChunkedLocal 的内存增长存在天花板。请求刚开始时行为与线性增长一致,一旦上下文长度超过窗口大小,旧 block 被释放,内存停止增长。

SlidingWindow 的 block 上界:

max_blocks = ⌈min(sliding_window - 1 + max_batch_tokens, max_model_len) / B⌉ + 1

ChunkedLocal 的 block 上界:

max_blocks = ⌈min(chunk_size + max_batch_tokens, max_model_len) / B⌉

以 Mistral 的 sliding_window = 4096、block_size = 16 为例,单个请求最多消耗约 257 个 block。即使上下文增长到 128K token,内存占用也不会超过这个上界。这就是滑动窗口架构能支持超长上下文的根本原因——显存代价有界。

两者的区别在于滑动粒度:SlidingWindow 以 token 为单位滑动,每一步丢弃最旧的 token;ChunkedLocal 以 chunk 为粒度截断,在 chunk 边界处重新开始。上界公式结构相似,差异在于起始项——sliding_window - 1 对应 chunk_size。

3.3 恒定占用

三种类型的内存占用与上下文长度无关:

Mamba(“none”):SSM 状态是固定维度的向量,每个请求只需 1 个 block。

CrossAttention:编码器输出在整个解码过程中固定不变,block 数取决于编码器长度。

EncoderOnly:BERT 类模型不需要解码端 KV cache,分配量为 0。

memory(Mamba "none") = page_size × (1 + num_speculative_blocks)
memory(Mamba "align") = page_size × (2 + num_speculative_blocks)
memory(CrossAttn) = ⌈encoder_len / B⌉ × page_size

Mamba 的三种模式(”all”、”align”、”none”)覆盖了从全量缓存到零增长的完整谱系。”all” 模式让 Mamba 层也按 token 追加状态,行为等同于 Attention 的线性增长;”none” 模式每个请求只保留一个 page 的最新状态;”align” 介于两者之间,保留 2 个 page 用于状态对齐。

3.4 增长模式汇总

类型 增长模式 单请求 block 数 上界
FullAttention 线性 ⌈L/B⌉ ⌈max_model_len/B⌉
MLA 线性 ⌈L/B⌉ ⌈max_model_len/B⌉
SlidingWindow 有界 min(⌈L/B⌉, max) ⌈(sw-1+batch)/B⌉+1
SlidingWindowMLA 有界 min(⌈L/B⌉, max) ⌈(sw-1+batch)/B⌉+1
ChunkedLocal 有界 min(⌈L/B⌉, max) ⌈(chunk+batch)/B⌉
Mamba(“all”) 线性 ⌈L/B⌉ ⌈max_model_len/B⌉
Mamba(“align”) 恒定 2 + spec —
Mamba(“none”) 恒定 1 + spec —
CrossAttention 恒定 ⌈enc_len/B⌉ 固定
EncoderOnly — 0 0

L = context_len, B = block_size, sw = sliding_window, spec = num_speculative_blocks

这张表和上一节的 page_size 表结合起来,构成了 vLLM 做调度决策的完整信息:调度器在 admit 请求前,可以精确计算该请求在最坏情况下需要多少显存。这就是 KVCacheSpec 类型体系的工程价值——让调度器拥有精确的预测能力。

四、存储池容量:从可用显存到 Block 数

可用显存确定后,存储池需要计算能分配多少个 block。这个计算因模型架构而异。

4.1 可用显存计算

存储池的输入是模型加载完成后的剩余显存:

available_memory = total_gpu_memory × gpu_memory_utilization - model_weights - other_memory

以 80 GB A100、gpu_memory_utilization = 0.9、模型权重 15 GB、其他开销 1 GB 为例:

available = 80 × 0.9 - 15 - 1 = 56 GB

可通过 num_gpu_blocks_override 参数手动覆盖自动计算结果。

4.2 单一类型模型

当所有层使用相同的 KVCacheSpec(最常见的场景),公式直接:

num_blocks = ⌊available_memory / (page_size × num_layers)⌋

续接上例,32 层、每层 page_size = 64 KB:

num_blocks = ⌊56 × 1024² / (64 × 1024 × 32)⌋ ≈ 28,672 blocks

每个请求 128K context 需要 ⌈131072 / 16⌉ = 8,192 个 block,这张卡最多同时服务约 3.5 个 128K 请求,或约 229 个 2K 请求。

当所有层的 spec 可以合并为一个 UniformTypeKVCacheSpecs 时,公式等价但写法更紧凑:

num_blocks = ⌊available_memory / total_page_size⌋

其中 total_page_size 是所有层 page_size 的加和。

4.3 混合模型

混合模型(如 Jamba 的 Attention + Mamba 交替层)有多个 KVCacheSpec group[1]。存储池按最大 group 的层数分配,page_size 统一到对齐值:

group_size = max(各 group 层数)
num_blocks = ⌊available_memory / (unified_page_size × group_size)⌋

Mamba 层的 page_size_padded 就是用来和 Attention 层对齐 page 大小的机制,让 block allocator 维护单一 free list,避免多套 page_size 带来的碎片化。

4.4 DeepSeek V4 的特殊分配

DeepSeek V4 混合使用 FullMLA 和 SlidingWindowMLA,两种层的 page_size 不同。vLLM 对 V4 采用专门的分配策略,按 page_size 分桶后计算:

layer_tuple_bytes = Σ(各唯一 page_size)
num_layer_tuples = max(同一 page_size 的层数)
num_blocks = ⌊available_memory / (layer_tuple_bytes × num_layer_tuples)⌋

这种分配方式确保不同 page_size 的层共享相同数量的 block,简化了 block manager 的管理逻辑。

五、传输粒度:PD 分离与 CPU Offload

KV cache 不只在本地 GPU 上存在。Prefill-Decode 分离架构需要跨节点传输 KV cache,长上下文场景需要 GPU-CPU offload。传输粒度决定了网络和总线带宽的利用效率。

5.1 NIXL PD 传输

NIXL 是 vLLM 的主要 PD 分离传输机制,通过 RDMA 实现跨节点 KV cache 传输。每次传输的最小单元是一个 block 的一个 layer 的 K 或 V 部分,具体粒度取决于 Attention 后端的 tensor 布局:

后端 单次传输大小 说明
FlashAttention page_size / layer K+V 连续存储,一次传完
FlashInfer page_size / layer / 2 K 和 V 拆成独立 tensor,分开传输
MLA page_size / layer K/V 已融合为一个 latent vector
Cross-layer block Σ_layers page_size 所有层打包为一次传输

以 Llama-3-8B + FlashInfer 后端为例,单次 RDMA 传输的最小单元:

min_transfer = block_size × num_kv_heads × head_size × dtype_size
             = 16 × 8 × 128 × 2 = 32,768 bytes = 32 KB  (per K or V, per layer, per block)

传输一个完整 block 的所有层需要 32 KB × 2(K+V) × 32(layers) = 2 MB。对于 128K context(8,192 blocks),总传输量约 16 GB——PD 分离架构对网络带宽的高要求由此而来。

5.2 CPU Offload

CPU offload 的传输粒度与 NIXL 类似,但走 PCIe 总线:

布局 单次传输大小
FlashInfer page_size / layer / block
FlashAttention page_size / layer / 2 / block(K/V 分开)

offload buffer 的大小通过 kv_offloading_size 参数配置(单位 GiB),决定了 CPU 端能暂存多少 block。

5.3 各 Connector 传输粒度对比

Connector 传输粒度 传输通道
NIXL per-block, per-layer, per-K/V RDMA
P2P NCCL per-block GPU 间 NCCL
Mooncake per-block 远程 KV 存储
LMCache per-block 外部缓存系统
HF3FS per-block 高性能文件系统
CPU Offload per-block, per-layer PCIe

所有 connector 的传输粒度都以 block 为基本单位,差异在于是否进一步拆分到 layer 和 K/V 维度。NIXL 是粒度最细的——细到单个 layer 的单个 K 或 V tensor,这使它能更灵活地利用 RDMA 的多路并发能力。

六、结语

vLLM 的 KVCacheSpec 类型体系将十种缓存规格统一到三个维度:page_size 决定单 block 代价,增长模式决定单请求的内存曲线,block 分配公式决定存储池容量。调度器通过这三个维度的组合,可以在 admit 请求前精确预测最坏情况下的显存占用[2]。

这套抽象的边界也很清晰:它只管理 block 级别的空间分配,不涉及 block 内部的计算 kernel 选择(那是 Attention backend 的职责),也不涉及 block 之间的依赖关系管理(那是 prefix caching 和 scheduler 的职责)。KVCacheSpec 回答的始终是同一个问题——这种类型的 cache,一个 page 要多少字节。

目前仍在演化的部分包括:DeepSeek V4 的 compress_ratio 机制、NVFP4 量化与 MLA 的组合、以及 Mamba 状态在混合模型中的对齐策略。


参考资料

[1] vLLM v1 Source Code — KVCacheSpec Interface

[2] Efficient Memory Management for Large Language Model Serving with PagedAttention

[3] DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model

[4] Mamba: Linear-Time Sequence Modeling with Selective State Spaces

版本对齐信息

材料 仓库 / 来源 版本 日期
KVCacheSpec 继承体系 vllm-project/vllm commit 87e31455b 2026-05-20
Page size 公式 vllm/v1/kv_cache_interface.py 同上 同上
Block 分配逻辑 vllm/v1/core/kv_cache_utils.py 同上 同上
NIXL 传输粒度 vllm/v1/kv_connector/v1/nixl/ 同上 同上

Discussion

留言

评论由 GitHub Discussions 承载,需要 GitHub 账号登录后发布。

滚动到这里时加载评论。

WeChat Official Account

微信公众号

MiracleFarms 微信公众号二维码

Miracle Farms

微信扫码关注,接收每日 AI Infra 早报。