vllm: kv cache
参考文献
vllm官方prefix缓存设计文档:https://github.com/vllm-project/vllm/blob/main/docs/design/prefix_caching.md
数据表现
原始计算轮数据:
FlashAttentionMetadata: (num_actual_tokens=997, max_query_len=997, query_start_loc=tensor([0, 997]), max_seq_len=997, seq_lens=tensor([997]), slot_mapping=tensor([16, 17, 18, 19, 20, ... , 1012)], block_table=tensor([1, 2, 3, 4, 5, 6, ... ,61, 62, 63, 0, ... , 0)])
BatchDescriptor: (num_tokens=8)
缓存轮数据:
FlashAttentionMetadata: (num_actual_tokens=5, max_query_len=5, query_start_loc=tensor([0, 5]), max_seq_len=997, seq_lens=tensor([997]), slot_mapping=tensor([2032, 2033, 2034, 2035, 2036, -1, -1, -1]), block_table=tensor([1, 2, 3, 4, 5, 6, ... , 61, 62, 127, 0, ... , 0)])
原理&概要设计
prefix使用block内存储的token ids作为hash的对象,以block为单位存储每个block的hash值,作为kv cache复用的检索对象。对于多模态数据而言,由于多模态数据使用的是相同token id的多模态占位符,所以附加embeding过程产生的多模态data hash值作为附加哈希值。
管理与复用
以Block为单位,对block内的token进行hash,通过hash进行管理和复用;
参与hash值计算的为三部分:
父哈希值:父哈希块的哈希值。
块内****词元:该块中包含的词元元组。包含精确词元的目的是降低潜在的哈希值碰撞风险。
附加哈希:用于确保该块唯一性的其他必要值,例如LoRA标识符、多模态输入哈希值(参见下方示例),以及用于在多租户环境中隔离缓存的随机盐值。
在多模态场景中,由于块内词元是mm_placeholder,为了区分图片,附加哈希会添加上多模态输入哈希值(如图片转image token时创建的hash值);
数据结构
vLLM v1 中的前缀缓存在 KV cache manager中实现。其基本构建块是 "Block" 数据类(简化版):
class KVCacheBlock:
# The block ID (immutable)
block_id: int
# The block hash (will be assigned when the block is full,
# and will be reset when the block is evicted).
block_hash: BlockHash
# The number of requests using this block now.
ref_cnt: int
# The pointers to form a doubly linked list for the free queue.
prev_free_block: "KVCacheBlock | None" = None
next_free_block: "KVCacheBlock | None" = None

详细设计
初始化
kv_cache由Scheduler创建和管理:self.kv_cache_manager = KVCacheManager
Block Allocation
1 New request
scheduler为新请求分配KV缓存块的工作流程:
scheduler调用kv_cache_manager.get_computed_blocks()来获取已计算完成的块序列。这是通过对request中的提示词进行哈希并查找缓存块来实现的。
scheduler调用kv_cache_manager.allocate_slots()。该函数执行以下步骤:
-
计算所需的新block数量,如果可用block不足则直接返回。
-
"标记"已计算的block。将已计算block的引用计数加一,如果该block未被其他请求使用,则将其从空闲队列中移除。这是为了避免这些已计算的block被淘汰。具体示例见下一节说明。
-
从空闲队列头部弹出块来分配新block。如果弹出的头block是已缓存的block,则会"淘汰"该block,从现在起其他请求将无法再复用该block。
-
如果分配的block已填满token,我们会立即将其加入缓存block,使该block能在同一批次中被其他request复用。
2 Running request
scheduler为正在运行的请求分配KV缓存块的工作流程:
-
scheduler调用
kv_cache_manager.allocate_slots()。该函数执行以下步骤:-
计算所需的新block数量,如果可用block不足则直接返回。
-
通过从空闲队列头部弹出块block分配新block。如果弹出的头block是已缓存的block,则会“淘汰”该block,从现在起其他请求将无法再复用该块block
-
将token id追加到现有块以及新块的插槽中。如果一个block已满,则将其加入缓存块进行缓存。
-
Eviction (LRU)
当空闲队列的头block(最近最少使用block)已被缓存时,我们必须淘汰该block以防止其被其他reques使用。具体而言,淘汰过程包含以下步骤:
-
从空闲队列头部弹出该block。这是即将被淘汰的LRU block。
-
从cache blocks中移除该block的ID。
-
移除该block的哈希值。
示例
在本示例中,我们假设block大小为4(每个block可缓存4个token),且KV缓存管理器中共有10个block。
时刻1:缓存为空,新请求到达。我们分配了4个块。其中3个块已满并被缓存。第四个块部分填充了3个令牌(容量为4)。

时刻2:请求0使第3块填满,并要求分配新块以继续解码。我们缓存第3块并分配第4块。

时刻3:请求1到达,包含14个提示令牌,其中前10个令牌与请求0相同。我们可以看到只有前2个块(8个令牌)命中缓存,因为第3个块只匹配了4个令牌中的2个。

时刻4:请求0完成并被释放。块2、3和4按逆序加入空闲队列(但块2和3仍处于缓存状态)。块0和1未被加入空闲队列,因为它们正被请求1使用。

时刻5:请求1完成并被释放。

时刻6:请求2到达,包含29个提示令牌,其中前12个令牌与请求0相同。请注意,虽然空闲队列中的块顺序为7 - 8 - 9 - 4 - 3 - 2 - 6 - 5 - 1 - 0,但缓存命中的块(即0、1、2)在分配前被标记并从队列中移除,因此空闲队列变为7 - 8 - 9 - 4 - 3 - 6 - 5。最终分配的块为:0(已缓存)、1(已缓存)、2(已缓存)、7、8、9、4、3(被淘汰)。

代码实现
数据结构
1)block_size: 每个kv cache block的slots数量
slots数量:即一个block能够存放多少token
默认情况下,每个KV Cache Block包含16个slots(tokens)。可选值:有效的block size包括 1, 8, 16, 32, 64, 128, 256;标准CUDA attention后端只支持最大32的block size,更大的size(64+)仅用于MLA(Multi-head Latent Attention)后端。
2)block_num: kv cache block数量
vllm执行虚拟前向传播(Dummy Forward Pass),使用最大batch size和最大序列长度来模拟峰值内存使用。可以分配给kv blocks的内存由(memory*utilization-model_weights-peak_usage)计算得到。实现细节而言,使用max_model_len来计算单个request推理需要的最大显存,max_num_batched_tokens来计算
max_model_len: 限制单个请求中"输入tokens + 输出tokens"的总和上限。
max_num_seqs: 控制vLLM能够同时处理的最大独立请求(序列)数量;默认值128。
max_num_batched_tokens: 限制单个调度步骤(schedule step)中所有请求的token总数预算,在调度器中通过SchedulingBudget类管理;默认值2048。
实验示例:
Model=Qwen3-vl-2B【4.24 GiB】, gpu_memory_utilization=0.9, 一张图片(from采集数据)的token占用约1000 tokens。
max_model_len=4096,max_num_seqs=5,max_num_batched_tokens=2048;Available KV cache memory: 16.87 GiB,GPU KV cache size: 157,952 tokens;
max_model_len=8192,max_num_seqs=5,max_num_batched_tokens=2048;Available KV cache memory: 16.87 GiB,GPU KV cache size: 157,952 tokens;
max_model_len=4096,max_num_seqs=10,max_num_batched_tokens=2048;Available KV cache memory: 16.87 GiB,GPU KV cache size: 157,952 tokens;
max_model_len=4096,max_num_seqs=5,max_num_batched_tokens=4096;Available KV cache memory: 16.59 GiB,GPU KV cache size: 155,296 tokens;
结论:模型大小,gpu_memory_utilization为KV cache能够容纳tokens数量的主要影响项,max_num_batched_tokens为轻微影响项。
初始化
GPUModelRunner.initialize_kv_cache_tensors
创建kv_caches: [torch.Size([2, 8243, 16, 8, 128])] * layer_num;注:8243为block数量,16为block大小;
将kv_caches与self.compilation_config.static_forward_context, self.kv_caches绑定;
计算过程中
cache 使用的变量
attn_metadata
组织形式:dict,以模型层名为dict_key,每一个item是模型对应层使用的attn_metadata。
以两个req为例,拆解其中的重要变量如下:
1)seq_lens: 当前批次中每个req已经推理的token数量。
# req1 prefill
seq_lens=tensor([997])
# req1 decode,req2 prefill
seq_lens=tensor([998, 1001])
2)block_table: 当前推理需要访问的block
# req1 prefill
block_table = tensor([[1, 2, 3, 4, 5, 6, ...]])
# req1 decode,req2 prefill
block_table = tensor([[1, 2, 3, 4, 5, 6, ...],[64, 65, 66, 67, 68, 69, 70, 71, ...]])
- slot_mappings: 当前批次中每个 token 的 KV 值应该写入(或读取)物理显存的具体位置(slot)。
# req1 prefill(len=997)
slot_mappings = tensor([16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,...,1012])
# req1 decode,req2 prefill(len=1+1001)
slot_mappings = tensor([1013, 1024, 1025, ..., 2022, 2023, 2024])
变量的传递和使用
- set_forward_context
additional_kwargs = current_platform.set_additional_forward_context(
attn_metadata=attn_metadata,
vllm_config=vllm_config,
num_tokens=num_tokens,
)
forward_context = create_forward_context(
attn_metadata,
vllm_config,
slot_mapping,
additional_kwargs,
)
forward_context通过全局变量_forward_context(forward_context.py)传递给模型运行时
- 获取使用
forward_context = get_forward_context()
attn_metadata: AttentionMetadata = forward_context.attn_metadata
在 vllm/attention/layer.py 中,Attention.forward() 方法直接调用 get_forward_context
prefix cache
以单张图的prefix cache复用,展示复用的变量配置
- 不使用缓存:
# prefill
num_actual_tokens=997
block_table = tensor([[1, 2, 3, ..., 61,62,63]])
slot_mappings = tensor([16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,...,1011])
seq_lens=tensor([997])
- 使用缓存:
# prefill with prefix cache
num_actual_tokens=5
block_table = tensor([[1, 2, 3, ..., 61,62,64]])
slot_mappings = tensor([1024, 1025, 1026, 1027, 1028, -1, -1, -1])
seq_lens=tensor([997])
注:3个-1,代表的是3个mm占位符token:<|vision_start|>{placeholder}<|vision_end|>;
- 缓存轮后续推理
# decode1
num_actual_tokens=1
block_table = tensor([[1, 2, 3, ..., 61,62,64]])
slot_mappings = tensor([1029])
seq_lens=tensor([998])
# decode2
num_actual_tokens=1
block_table = tensor([[1, 2, 3, ..., 61,62,64]])
slot_mappings = tensor([1030])
seq_lens=tensor([999])
# decode3
num_actual_tokens=1
block_table = tensor([[1, 2, 3, ..., 61,62,64]])
slot_mappings = tensor([1031])
seq_lens=tensor([1000])
浙公网安备 33010602011771号