vLLM V1 的 KV Cache 管理分三层:
block_pool.py 对应论文 §4.2 KV Cache Manager 中的 Block Allocator——管理物理 block 的分配、释放、引用计数和 Prefix Cache 索引。它不关心"哪个请求用了哪个 block"(那是上层 kv_cache_manager 的事),只关心"物理 block 池子里还有多少空闲、谁能复用"。
文件里有两个类:
| 类 | 角色 | 对应 OS 概念 |
|---|---|---|
BlockHashToBlockMap | Prefix Cache 的索引(hash → block) | 文件系统的 inode cache / dentry cache |
BlockPool | 物理 block 池管理器 | OS 的物理页帧分配器(buddy allocator) |
三个核心数据结构:
| 数据结构 | 在哪里 | 作用 |
|---|---|---|
self.blocks: list[KVCacheBlock] | BlockPool.__init__ | 全局物理 block 数组,按 block_id 索引。初始化时一次性创建所有 block 对象。 |
self.free_block_queue: FreeKVCacheBlockQueue | BlockPool.__init__ | 空闲 block 的双向链表。分配时从左端取(popleft),释放时追加到右端(append)或左端(prepend)。 |
self.cached_block_hash_to_block: BlockHashToBlockMap | BlockPool.__init__ | hash → block 的映射。block 写满后注册进来,新请求来时先查这里看能否复用。 |
class BlockHashToBlockMap:
self._cache: dict[
BlockHashWithGroupId, # key = (block_hash, group_id)
KVCacheBlock | dict[int, KVCacheBlock] # value = 单个 block 或多个 block
]
key 是什么? (block_hash, group_id) 的组合。block_hash 基于 block 内 token id 序列计算,group_id 标识 KV cache 组(多头 attention 可以分组管理)。
value 为什么是 union 类型? 大多数情况下一个 hash 只对应一个 block(直接存 KVCacheBlock)。极少数情况下同一个 hash 有多个 block(存 dict[block_id, KVCacheBlock])。代码注释说得很清楚:这么做是为了减少 GC 开销——单个 block 时不创建 dict 对象。
| 方法 | 做什么 | 什么时候调用 |
|---|---|---|
get_one_block(key) | 按 hash 查找一个 block | 新请求到来,查 Prefix Cache 是否命中 |
insert(key, block) | 注册一个 block 到 hash map | block 写满后缓存进来 |
pop(key, block_id) | 按 hash + block_id 从 map 中移除 | 驱逐缓存 block 时 |
def __init__(self, num_gpu_blocks, enable_caching, hash_block_size, ...):
# 1. 创建全部物理 block(一次性预分配)
self.blocks = [KVCacheBlock(idx) for idx in range(num_gpu_blocks)]
# 2. 全部 block 放进空闲链表
self.free_block_queue = FreeKVCacheBlockQueue(self.blocks)
# 3. 创建空的 hash 索引
self.cached_block_hash_to_block = BlockHashToBlockMap()
# 4. 取一个 block 出来作为 null_block(占位用)
self.null_block = self.free_block_queue.popleft()
self.null_block.is_null = True
几个要点:
def get_new_blocks(self, num_blocks: int) -> list[KVCacheBlock]:
# 检查空闲数量
if num_blocks > self.get_num_free_blocks():
raise ValueError(...)
# 从空闲链表左端取 n 个
ret = self.free_block_queue.popleft_n(num_blocks)
for block in ret:
# 如果开启了缓存,取出的 block 可能还带着旧 hash → 驱逐它
if self.enable_caching:
self._maybe_evict_cached_block(block)
assert block.ref_cnt == 0
block.ref_cnt += 1 # 标记为"被使用"
return ret
核心逻辑:
_maybe_evict_cached_block 就是在这里做的"延迟清理"。
def free_blocks(self, ordered_blocks, prepend=False):
for block in blocks_list:
block.ref_cnt -= 1 # 引用计数减 1
# 只有 ref_cnt 真正到 0 的才放回空闲链表
freed_blocks = [
block for block in blocks_list
if block.ref_cnt == 0 and not block.is_null
]
if prepend:
self.free_block_queue.prepend_n(freed_blocks) # 放到队列前端(优先被重用)
else:
self.free_block_queue.append_n(freed_blocks) # 放到队列尾部(最后被重用)
核心逻辑:
prepend 参数控制优先级:prepend=True 放到队列前端,意味着会被优先重新分配(用于被抢占请求的 block——这些 block 不太可能被复用,优先回收)。def touch(self, blocks: Sequence[KVCacheBlock]):
for block in blocks:
if block.ref_cnt == 0 and not block.is_null:
# ref_cnt=0 说明在空闲链表里(eviction candidate)
# 命中了 → 从空闲链表移除(不能被驱逐了)
self.free_block_queue.remove(block)
block.ref_cnt += 1
场景:新请求到来,其前缀的 hash 在 Prefix Cache 中命中了。命中的 block 可能 ref_cnt=0(之前的请求已结束,block 在空闲链表里等待驱逐)。此时:
mark_page_accessed()。
这是 Prefix Cache 的写入路径。当一个 block 被 token 填满后,需要计算 hash 并注册到索引中。
def cache_full_blocks(self, request, blocks, num_cached_blocks,
num_full_blocks, block_size, kv_cache_group_id, ...):
# 只处理新变满的 block(跳过已经缓存的)
new_full_blocks = blocks[num_cached_blocks:num_full_blocks]
for i, blk in enumerate(new_full_blocks):
if blk.is_null: # 跳过空 block(SWA 等稀疏 attention 场景)
continue
block_hash = new_block_hashes[i]
# 给 block 打上 hash 标记
block_hash_with_group_id = make_block_hash_with_group_id(
block_hash, kv_cache_group_id)
blk.block_hash = block_hash_with_group_id
# 注册到 hash map
self.cached_block_hash_to_block.insert(
block_hash_with_group_id, blk)
核心逻辑:
[num_cached_blocks, num_full_blocks) 里的 block——已经 cache 过的不重复处理block_hash 属性cached_block_hash_to_block hash mapBlockStored 事件通知其他节点Request 对象创建时和 append token 时就算好了(request.block_hashes)。block_pool 只是消费这些已计算好的 hash。
def _maybe_evict_cached_block(self, block: KVCacheBlock) -> bool:
block_hash = block.block_hash
if block_hash is None:
return False # 没有 hash,不需要驱逐
# 从 hash map 中移除
if self.cached_block_hash_to_block.pop(
block_hash, block.block_id) is None:
return False # hash map 里找不到,不需要驱逐
block.reset_hash() # 清除 block 上的 hash 元数据
return True
调用时机:在 get_new_blocks 中,当一个空闲 block 被重新分配时。
完整的 Lazy Eviction 生命周期:
# 初始化时取出一个 block 作为 null_block
self.null_block = self.free_block_queue.popleft()
self.null_block.is_null = True
null_block 是什么? 一个"占位符"block,block_id=0。用于以下场景:
特殊规则:
is_null 的 block,不放回空闲链表| 论文概念(§4.2) | 代码对应 | 说明 |
|---|---|---|
| Physical KV block | KVCacheBlock 对象 | 定义在 kv_cache_utils.py,有 block_id / ref_cnt / block_hash 属性 |
| Block pool (全局物理 block 池) | BlockPool.blocks: list[KVCacheBlock] | init 时一次性创建 |
| Free block list | BlockPool.free_block_queue: FreeKVCacheBlockQueue | 双向链表,支持 O(1) 的 popleft / append / remove |
| Block allocate | BlockPool.get_new_blocks() | 从空闲链表左端取 |
| Block free | BlockPool.free_blocks() | ref_cnt-1,到 0 归还空闲链表 |
| Reference count | KVCacheBlock.ref_cnt | touch +1、free -1、get_new_blocks 分配时设为 1 |
| Prefix Cache 索引 | BlockPool.cached_block_hash_to_block: BlockHashToBlockMap | hash + group_id → block 的映射 |
| Cache hit (Prefix Cache 命中) | BlockPool.get_cached_block() + touch() | 查 hash map → 命中 → ref_cnt+1 + 从空闲链表移除 |
| Cache register (注册) | BlockPool.cache_full_blocks() | block 写满后算 hash 注册到 map |
| Eviction (驱逐) | BlockPool._maybe_evict_cached_block() | Lazy:释放时不删 hash,重新分配时才清理 |
| 论文没提到的 | null_block | 论文没讲,代码里为稀疏 attention 加的占位符 |
| 论文没提到的 | kv_event_queue | 分布式 KV Cache 事件通知(V1 新增) |
blocks(全局物理 block 数组),② free_block_queue(空闲 block 双向链表),③ cached_block_hash_to_block(hash → block 的 Prefix Cache 索引)。
get_new_blocks(分配,0→1)、touch(cache 命中,+1)。free_blocks(释放,-1,到 0 归还空闲链表)。
_maybe_evict_cached_block)。最坏情况不比立即清理差,最好情况省下一次 Prefill 重算。
kv_cache_manager.py 和 GPU 侧的 fused block copy kernel 里。block_pool 的 ref_cnt 机制是 CoW 能工作的前提条件。
touch 需要 O(1) 地从链表中间移除一个 block——当 Prefix Cache 命中时,命中的 block 可能在空闲链表的任意位置。Python list 的 remove 是 O(n),双向链表是 O(1)。
vllm/v1/core/block_pool.py(GitHub 链接)vllm/v1/core/kv_cache_utils.py(KVCacheBlock / FreeKVCacheBlockQueue 定义)这是 Stage 1 / Week 2 的第二篇,对应 AI Infra 转型学习计划"读 vLLM 源码"任务。