vLLM 源码精读:KV Cache 管理层 —— 从门面到 block_pool 的调用链

— KVCacheManager → KVCacheCoordinator → SingleTypeKVCacheManager → BlockPool
源码精读 AI Infra vLLM V1 KV Cache Stage 1 / Week 2
目录
  1. 1. 四层架构总览
  2. 2. KVCacheManager:对外门面
  3. 3. KVCacheCoordinator:多 group 协调者
  4. 4. SingleTypeKVCacheManager:抽象基类
  5. 5. FullAttention vs SlidingWindow:关键差异
  6. 6. 四条核心路径走读
  7. 7. 论文映射
  8. 8. ⭐ 高频卡点答疑
  9. 9. 参考资料

1. 四层架构总览

vLLM V1 的 KV Cache 管理由四个 Python 文件协作完成,共约 3200 行代码:

文件行数类角色
kv_cache_manager.py580KVCacheManager对外门面。Scheduler 通过它调用 KV Cache 功能。
kv_cache_coordinator.py747KVCacheCoordinator / HybridKVCacheCoordinator协调多个 KV cache group。统一参数 + 分发给各 group 的 manager。
single_type_kv_cache_manager.py1407SingleTypeKVCacheManager(ABC) / FullAttentionManager 等承载最多业务逻辑。定义 allocate/free/find_cache_hit 等核心流程。
block_pool.py528BlockPool物理 block 池。只管 block 的分配/释放/ref_count/hash 索引。
Scheduler │ ▼ KVCacheManager (门面,对外暴露 allocate_slots / free / get_num_common_prefix_blocks) │ ▼ KVCacheCoordinator (协调者,持有多个 SingleTypeKVCacheManager) │ ┌─── group_id=0: SingleTypeKVCacheManager ├──├─── group_id=1: SingleTypeKVCacheManager │ └─── ... ▼ SingleTypeKVCacheManager (抽象基类,模板方法模式) │ ├── FullAttentionManager (L292-310, override hit 策略) │ ├── SlidingWindowManager (L311-380) │ └── ... ▼ BlockPool (物理 block 池,分配/释放/ref_count/hash)
一句话
四层调用链:门面 → 协调 → 业务逻辑 → 物理池。越往下越接近硬件,越往上越接近业务。Scheduler 只管"要什么",不关心"怎么管 block"。

2. KVCacheManager:对外门面

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() 开头无(重置内部计数器)
设计要点
KVCacheManager 持有 self.req_to_blocks: dict[str, KVCacheBlocks]——每个请求的 block table。这是 Scheduler 需要知道的全部:请求 X 的 KV 数据在哪个 block 里。

3. KVCacheCoordinator:多 group 协调者

为什么需要 Coordinator

一个模型可能有多个 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 需要回滚
关键设计
Prefix Cache 命中的判定条件非常严格:任何一个 group 没命中,整个请求就不算命中。因为 attention 计算需要所有 group 的 KV 就绪,少一个都不行。

4. SingleTypeKVCacheManager:抽象基类

这是承载最多业务逻辑的文件(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)
模板方法模式的价值
FullAttention、SlidingWindow、Mamba 等 7 种 attention 类型共享同一套 allocate/append/free 流程,只在"前缀怎么匹配"这一个小点上分道扬镳。这是优秀的工程抽象。

5. FullAttention vs SlidingWindow:关键差异

所有子类中,FullAttentionManager(约 60 行)和 SlidingWindowManager 是最核心的两个。它们的差异就在 find_longest_cache_hit 的实现上。

FullAttentionManager

前缀匹配策略:从左到右,逐个 block 检查 hash 是否命中块池缓存。
找到第一个 miss → 返回命中 block 数 = i。

原理:标准全注意力中,每个 token 依赖全部历史 context。
前缀的 KV 跟后面的 token 重新生成无关。

SlidingWindowManager

前缀匹配策略:从右到左,在 window 范围内逐个 block 匹配。
找到最长的连续命中 → 返回命中 block 数。

原理:滑动窗口注意力中,每个 token 只看固定窗口(如 4096)内的历史。
窗口外的历史 token 即使内容相同也不算命中——因为对新 token 没有影响。
FullAttentionSlidingWindow
匹配方向左→右右→左(窗口内)
命中范围全部有效历史仅窗口内有效
最长命中长度更长更短
适用模型LLaMA / GPT 系列Mistral / Gemma 等使用 SWA 的模型

6. 四条核心路径走读

路径一:Prefill 阶段分配新 block

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

路径二:Decode 阶段追加 token

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

7. 论文映射

论文 §4.2 概念代码对应
KV cache managerKVCacheManager + KVCacheCoordinator + SingleTypeKVCacheManager
Block table(页表)req_to_blocks[req_id]: KVCacheBlocks(包含物理 block 列表)
Block allocatorBlockPool + free_block_queue
Prefix Cache(hash → block 索引)BlockPool.cached_block_hash_to_block
按需分配append_slots 末尾追加,最后一个 block 满了才分配新 block
论文没提到的Coordinator 的多 group 协调;7 种 attention 类型的子类设计

8. ⭐ 高频卡点答疑

Q1:KVCacheManager 跟 BlockPool 是什么关系?
KVCacheManager 是请求级管理器(管 block table、前缀命中判断),BlockPool 是物理级池子(管分配/释放/ref_count/hash 索引)。前者调后者,不直接操作物理 block。
Q2:Coordinator 为什么重要?什么场景下必要?
当模型有多个 KV cache group(混合 attention 类型、不同 block_size)时必须用。最重要的逻辑:所有 group 都命中才算 Prefix Cache 命中,因为 attention 需要全部 group 的 KV 就绪。
Q3:SingleTypeKVCacheManager 里哪些是抽象方法、哪些有默认实现?
抽象:find_longest_cache_hit、get_num_common_prefix_blocks(前缀匹配策略因 attention 类型而异)。
默认实现:get_num_blocks_to_allocate、allocate_new_computed_blocks、cache_blocks、free(核心流程所有 attention 类型共享)。
Q4:FullAttention 和 SlidingWindow 在前缀匹配上有什么差异?
FullAttention:左→右匹配,所有历史 token 都有效 → 命中最长前缀更长。
SlidingWindow:右→左在窗口内匹配,只窗口内历史有效 → 命中更短。
Q5:Decode 时 append_slots 的"快路径"是什么?
最后一个 block 还有空位 → 不申请新 block,直接写入。大多数 decode step 走的都是这条快路径(每 16 步才需要申请一次新 block)。
Q6:Prefix Cache 命中后,触发了哪些操作?
① get_cached_block 查 hash map → 命中;② touch:block ref_cnt + 1,如果在 free_queue 中则移除;③ 这些 block 写入新请求的 block_table,不需要重新计算 KV。

9. 参考资料


这是 Stage 1 / Week 2 的第三篇,对应 AI Infra 转型学习计划"读 vLLM 源码"任务。