vLLM V1 的 KV Cache 管理由四个 Python 文件协作完成,共约 3200 行代码:
| 文件 | 行数 | 类 | 角色 |
|---|---|---|---|
| kv_cache_manager.py | 580 | KVCacheManager | 对外门面。Scheduler 通过它调用 KV Cache 功能。 |
| kv_cache_coordinator.py | 747 | KVCacheCoordinator / HybridKVCacheCoordinator | 协调多个 KV cache group。统一参数 + 分发给各 group 的 manager。 |
| single_type_kv_cache_manager.py | 1407 | SingleTypeKVCacheManager(ABC) / FullAttentionManager 等 | 承载最多业务逻辑。定义 allocate/free/find_cache_hit 等核心流程。 |
| block_pool.py | 528 | BlockPool | 物理 block 池。只管 block 的分配/释放/ref_count/hash 索引。 |
KVCacheManager 是 Scheduler 跟 KV Cache 子系统之间的唯一接口。它不包含分配逻辑,只做路由 + 状态记录。
| 方法 | Scheduler 什么时候调用 | 返回什么 |
|---|---|---|
allocate_slots(request, num_tokens) | schedule() 中,决定把一个请求加入 batch | 新分配的 KVCacheBlocks(block 列表)或 None(资源不够) |
free(request_id) | 请求完成后 | 无 |
get_num_common_prefix_blocks(request) | 评估请求能否复用 Prefix Cache | 命中前缀的 block 数 |
new_step_starts() | 每轮 schedule() 开头 | 无(重置内部计数器) |
self.req_to_blocks: dict[str, KVCacheBlocks]——每个请求的 block table。这是 Scheduler 需要知道的全部:请求 X 的 KV 数据在哪个 block 里。
一个模型可能有多个 KV cache group,比如:
每个 group 有自己独立的 SingleTypeKVCacheManager 和 BlockPool。Coordinator 负责:
| 职责 | 具体做法 |
|---|---|
| 统一参数 | hash_block_size 取各 group 的最小值(确保 hash 粒度在全部 group 上都有效);schedule_block_size 取各 group 的最大值 |
| 分发操作 | allocate / free / cache_hit 逐个 group 执行。每个 group 独立调用自己的 SingleTypeKVCacheManager |
| Prefix Cache 判定 | 在 HybridKVCacheCoordinator.find_longest_cache_hit 中:所有 group 都命中才算整体命中 |
| 回滚 | 某个 group 分配失败 → 之前已分配成功的 group 需要回滚 |
这是承载最多业务逻辑的文件(1407 行)。使用模板方法模式:基类定义流程骨架,子类 override 差异化部分。
| 方法 | 作用 | 为什么是抽象的 |
|---|---|---|
find_longest_cache_hit | 找最长命中前缀的 block 数 | 不同的 attention 类型(Full/Sliding Window/Chunked)有不同的前缀匹配策略 |
get_num_common_prefix_blocks | 两个请求之间共享前缀的 block 数 | 同上——前缀"公共"的定义因 attention 类型而异 |
| 方法 | 流程 |
|---|---|
get_num_blocks_to_allocate | 总需 block 数 - 已占用 block 数 - Prefix Cache 命中 block 数 = 要新分配的 block 数 |
allocate_new_computed_blocks | 调用 block_pool.get_new_blocks() 分配物理 block → touch 命中 block → cache_full_blocks 注册新满 block |
cache_blocks | 遍历新满 block → 给每个打上 hash → 注册到 block_pool 的 hash map |
free | 收集该请求所有 block → 调用 block_pool.free_blocks(ordered_blocks) |
所有子类中,FullAttentionManager(约 60 行)和 SlidingWindowManager 是最核心的两个。它们的差异就在 find_longest_cache_hit 的实现上。
前缀匹配策略:从左到右,逐个 block 检查 hash 是否命中块池缓存。
找到第一个 miss → 返回命中 block 数 = i。
原理:标准全注意力中,每个 token 依赖全部历史 context。
前缀的 KV 跟后面的 token 重新生成无关。
前缀匹配策略:从右到左,在 window 范围内逐个 block 匹配。
找到最长的连续命中 → 返回命中 block 数。
原理:滑动窗口注意力中,每个 token 只看固定窗口(如 4096)内的历史。
窗口外的历史 token 即使内容相同也不算命中——因为对新 token 没有影响。
| FullAttention | SlidingWindow | |
|---|---|---|
| 匹配方向 | 左→右 | 右→左(窗口内) |
| 命中范围 | 全部有效历史 | 仅窗口内有效 |
| 最长命中长度 | 更长 | 更短 |
| 适用模型 | LLaMA / GPT 系列 | Mistral / Gemma 等使用 SWA 的模型 |
Scheduler: 一个新请求进来,prompt 100 token
│
▼
KVCacheManager.prefill_allocation(request):
│
▼
KVCacheCoordinator:
│ ① hash_block_size 取 min → 统一 hash 粒度
│ ② 对每个 group: SingleTypeKVCacheManager.allocate_slots()
▼
SingleTypeKVCacheManager:
│ ① 算前缀命中: find_longest_cache_hit → touch 命中 block
│ ② 算要分配的: get_num_blocks_to_allocate
│ ③ 分配物理: block_pool.get_new_blocks()
│ ④ 建立映射: block_table[i] = physical_block
▼
BlockPool:
│ ① free_block_queue.popleft_n(n)
│ ② _maybe_evict_cached_block 清旧 hash
│ ③ ref_cnt = 1
Scheduler: RUNNING 请求生成 1 个新 token
│
▼
KVCacheManager 路由 → Coordinator 分发 → SingleTypeKVCacheManager.append_slots()
│
├── 最后一个 block 还有空位 → 直接填入,不需要新 block ⚡ 快路径
│
└── 最后一个 block 已满
├── 判断 free_blocks 是否够(扣除预留)
├── 够 → block_pool.get_new_blocks(1) → 加入 block_table
└── 不够 → 返回 None → Scheduler 走抢占
新请求 arrive → KVCacheManager → Coordinator → SingleTypeKVCacheManager
│
│ find_longest_cache_hit:
│ for each block (左→右 for FullAttention):
│ 调用 get_cached_block(hash) 查 block_pool 的 hash map
│ 命中 → 继续
│ miss → break,返回命中的 block 数
│
│ 命中 block = touch(blocks):
│ ref_cnt == 0 → 从 free_queue 移除(不能再被驱逐了)
│ ref_cnt += 1
│
│ 其余 token → 走路径一分配新 block
Scheduler: 请求完成
│
▼
KVCacheManager.free(request_id)
│
▼
Coordinator 分发 → SingleTypeKVCacheManager.free()
│
│ ① 收集该请求所有 block (block_table 里所有物理 block)
│ ② 调用 block_pool.free_blocks(blocks, prepend=False)
▼
BlockPool:
│ ① block.ref_cnt -= 1
│ ② ref_cnt == 0 → 加入 free_block_queue(append)
│ ③ hash 保留!← Lazy Eviction
| 论文 §4.2 概念 | 代码对应 |
|---|---|
| KV cache manager | KVCacheManager + KVCacheCoordinator + SingleTypeKVCacheManager |
| Block table(页表) | req_to_blocks[req_id]: KVCacheBlocks(包含物理 block 列表) |
| Block allocator | BlockPool + free_block_queue |
| Prefix Cache(hash → block 索引) | BlockPool.cached_block_hash_to_block |
| 按需分配 | append_slots 末尾追加,最后一个 block 满了才分配新 block |
| 论文没提到的 | Coordinator 的多 group 协调;7 种 attention 类型的子类设计 |
find_longest_cache_hit、get_num_common_prefix_blocks(前缀匹配策略因 attention 类型而异)。get_num_blocks_to_allocate、allocate_new_computed_blocks、cache_blocks、free(核心流程所有 attention 类型共享)。
get_cached_block 查 hash map → 命中;② touch:block ref_cnt + 1,如果在 free_queue 中则移除;③ 这些 block 写入新请求的 block_table,不需要重新计算 KV。
kv_cache_manager.py / kv_cache_coordinator.py / single_type_kv_cache_manager.py / block_pool.py这是 Stage 1 / Week 2 的第三篇,对应 AI Infra 转型学习计划"读 vLLM 源码"任务。