1. 总览
BlockManagerPool 是调度侧的 KV cache 总管。它自己不是最底层的物理 block 分配器,而是:
- 对外实现
KVCacheManager接口; - 对内按
dp_rank持有多个BlockManager; - 根据配置选择普通 / 并发 / 复合 / XTensor allocator;
- 同时管理 sequence 级别的
single_block; - 启用 linear-state 时,管理
LinearStatePrefixCache; - 作为
HierarchyBlockManagerPool时,在 device KV cache 外再管理一套 host KV cache。
Scheduler / Request / Sequence
│ sequence 级 KV cache API
▼
┌───────────────────────┐
│ KVCacheManager │ 抽象接口
└───────────────────────┘
▲
┌─────────────────────────────────────────┐
│ BlockManagerPool │
│ - block_managers_[dp_rank] │
│ - single_block_managers_[dp_rank] │
│ - linear_state_prefix_caches_[dp_rank] │
└─────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────────────┐
│ BlockManager │ │ SingleBlockMgr │ │ LinearStatePrefixCache │
│ 抽象接口 │ │ 单 id/slot 池 │ │ linear-state checkpoint│
└────────────────┘ └────────────────┘ └────────────────────────┘
▲
┌──────────────┬──────────────────┬──────────────────────┐
│ BlockManager │ ConcurrentBlock │ CompositeBlockManager │
│ Impl 普通 │ ManagerImpl 加锁 │ 组合多个子 manager │
└──────────────┴──────────────────┴──────────────────────┘
│ │
▼ ▼
┌─────────────┐ ┌───────────────────────┐
│ PrefixCache │ │ BlockManagerImpl / │
│ 前缀缓存 │ │ SlidingWindowBlockMgr │
└─────────────┘ └───────────────────────┘
另有:HierarchyBlockManagerPool 继承 BlockManagerPool,
增加 host_block_managers_,做 H2D / D2G transfer。
2. 两层接口:KVCacheManager vs BlockManager
这俩名字很像,但层级不同。
2.1 KVCacheManager:面向 Sequence / Scheduler
接口粒度是"一个请求、一个 sequence":
allocate(Sequence* sequence)
deallocate(Sequence* sequence)
allocate_shared(Sequence* sequence)
cache(Sequence* sequence)
它关心的是:一个 sequence 需要多少 KV cache?能不能复用 prefix cache?结束后怎么回收?BlockManagerPool 就是它的主要实现类。
2.2 BlockManager:面向物理 block 分配
接口粒度是"给我 N 个 block id":
allocate(num_blocks)
deallocate(blocks)
allocate_shared(token_ids)
cache(token_ids, blocks)
free(block_id)
它关心的是:分出 N 个 block id;哪几个可以逻辑释放;prefix cache 命中了哪些;最后一个引用没了时把 id 放回 free list。
BlockManagerPool 是一个 KVCacheManager,内部持有多个 BlockManager。
3. Block:最底层资源句柄,不是 allocator
Block 表示一个物理 KV block 的句柄,里面有 id_ / size_ / ref_count_ / manager_ / hash_value_。关键点:它持有一个非 owning 的 BlockManager* manager_,最后一个引用消失时调用 manager_->free(id_)。
3.1 deallocate() 和 free() 不是一回事
deallocate(blocks)
= sequence 逻辑上不用这些 block 了
= 更新 used 统计
= 但 block id 不一定马上回到 free list
Block 析构 / 最后一个引用消失
= ref_count 归零
= 调 manager->free(id)
= 物理 id 真正回到 free list
为什么要这样?因为 prefix cache 可能也持有同一个 Block:
Sequence.blocks_ ────┐
▼
Block(id=10, ref_count=2)
▲
PrefixCache entry ────┘
sequence 结束后,它这边释放了,但 prefix cache 还拿着 block,物理 id 不能回收。只有 prefix cache 也 evict 之后 ref_count 归零,才真正 free(id)。
4. BlockManagerImpl:普通 allocator + PrefixCache
普通模式下真正分配 KV block 的类。内部主要有 prefix_cache_ / padding_block_ / free_blocks_ / num_free_blocks_ / num_used_blocks_。
4.1 初始化
构造时:① 若启用 prefix cache,创建 PrefixCache;② 初始化 free_blocks_;③ 预留 block id 0 作为 padding block。
physical block ids:
0 -> padding block,保留
1..N-1 -> 可分配给 sequence
4.2 分配、命中、插入
allocate(num_blocks)
-> has_enough_blocks() free 不够则尝试 prefix_cache_->evict()
-> 从 free_blocks_ 弹出 block id
-> 构造 Block(block_id, this)
allocate_shared(token_ids)
-> prefix_cache_->match(...) -> 返回一组共享 Block
cache(token_ids, blocks)
-> prefix_cache_->insert(...)
4.3 eviction 与 ref_count 的关系
allocate() -> has_enough_blocks() -> prefix_cache_->evict(n)
-> 删除 prefix cache 里的 Block handle
-> Block 析构 -> ref_count--
-> 若 ref_count==0,则 manager->free(id)
所以 prefix cache eviction 不直接操作 free list,而是通过 Block 的析构链路把 id 还给 allocator。
5. Pool 构造与分配流程
5.1 构造时怎么选具体 manager
每个 dp_rank 创建一个具体 BlockManager:
for each dp_rank:
if enable_xtensor: XTensorBlockManagerImpl
else if manager_types 非空: CompositeBlockManager
else if disagg_pd 或 kvcache_store: ConcurrentBlockManagerImpl
else: BlockManagerImpl
同时创建一个 SingleBlockManager
若 enable_linear_state: 创建一个 LinearStatePrefixCache
5.2 普通 allocate 主流程
allocate(sequence)
│
├─ get_dp_rank(sequence)
│
├─ allocate_single_block()
│ ├─ single_block_managers_[dp]->allocate(1)
│ └─ if linear_state: acquire_live_slot()
│
├─ is_composite?
│ └─ yes: CompositeBlockManager::allocate_for_sequence()
│
├─ started_empty?
│ └─ yes: allocate_shared() -> PrefixCache::match()
│
├─ calculate needed blocks
└─ BlockManagerImpl::allocate(additional) -> Sequence::add_kv_blocks()
5.3 get_dp_rank:sequence 分到哪个 dp manager
若 sequence 已有 dp_rank: 用已有
否则: 找 free blocks 最多的 manager,set_dp_rank
注意:linear-state 开启时,选择会跳过 single-block 已无空闲的 dp——dp 选择不只看 KV block,还要考虑 sequence 级 slot 是否够用。
6. SingleBlockManager:sequence 级单 id 资源
它也继承 BlockManager,但用途不同:
普通 KV block: 一个 Block 表示一块 KV cache,size = block_size
single block: 一个 Block 表示一个单独的 id/slot,block_size = 1
它自己没有 prefix cache(allocate_shared() 返回空、cache() no-op),也保留 id 0。sequence 里的三种资源来源:
Sequence
├─ kv_state().blocks_ -> 由 block_managers_[dp] 分配
├─ single_block_ -> 由 single_block_managers_[dp] 分配
└─ linear_state_slot_ -> 由 linear_state_prefix_caches_[dp] 内部 slots_ 分配
7. LinearStatePrefixCache:GDN recurrent state 缓存
Qwen3.5 / Qwen3.6 这类 GDN linear-state 模型的 scheduler 侧缓存。它管理的不是 KV block,而是 recurrent state slot。内部持有一个 SingleBlockManager slots_、一张 hash → CacheEntry 表和 LRU。
7.1 三种 slot 状态
0: padding slot,不分配
live slot: 当前运行 sequence 持有(sequence->linear_state_slot_)
pending checkpoint: 已 reserve,worker 稍后 copy 进去,暂不能 match
committed checkpoint: 已进入缓存表,可被 prefix hash 命中,通过 LRU evict
7.2 为什么 linear-state 会影响 KV prefix cache
KV prefix cache 命中了多少 block,不代表 linear-state 一定能恢复到对应边界。对 linear-state 模型,复用 KV block 的同时还必须能恢复 recurrent state,所以只能复用那些"对应 prefix hash 有 linear-state checkpoint"的 block 边界。
prompt blocks: block0 block1 block2 block3
hash0 hash1 hash2 hash3
KV PrefixCache 命中: block0..block3
LinearStatePrefixCache 有 checkpoint: hash0, hash1, hash3
▲ 最长连续安全边界到 block1
结果: 只复用 block0, block1;block2, block3 要 trim 掉
逻辑是遍历 shared block 边界,累加"prefix hash 在 linear-state cache 里 contains"的连续前缀,把安全边界之后的 shared block deallocate 掉。
8. deallocate 流程
deallocate(sequence)
│
├─ cache(sequence) -> PrefixCache::insert()
├─ block_managers_[dp]->deallocate(kv_blocks)
├─ deallocate_single_block() -> release_linear_state_slot() + single mgr deallocate
└─ sequence->reset()
-> Block handles 被释放 -> ref_count--
├─ prefix cache 还持有: 物理 block 不回收
└─ ref_count==0: Block::~Block() -> manager->free(id)
9. 其它变体
9.1 ConcurrentBlockManagerImpl:加锁版
继承 BlockManagerImpl,每个接口加 recursive_mutex,用于 disagg PD / kvcache store。用 recursive mutex 的原因:prefix cache eviction 可能在 allocator API 持锁期间释放 Block,而释放会回调 free(),需允许同线程重入。
9.2 CompositeBlockManager:一个 manager 包多个子 manager
按 manager_types 创建不同子 manager(如 BlockManagerImpl 或 SlidingWindowBlockManager)。与普通路径最大区别在 sequence 的 block 存储结构:
普通: Sequence.kv_state().blocks_ 一维 vector<Block>
Composite: Sequence.kv_state().composite_blocks_ 二维 vector<vector<Block>>
每个 sub-manager 一组
Composite 当前不支持 prefix cache:allocate 直接走 allocate_for_sequence()、deallocate 走 deallocate_sequence()、cache 跳过。
9.3 SlidingWindowBlockManager:Composite 的子 manager
继承 BlockManagerImpl,自己分配就是复用父类。真正的 sliding-window 行为(释放超出窗口的旧 SWA blocks)在 CompositeBlockManager::allocate_for_sequence() 里处理。
9.4 HierarchyBlockManagerPool:device 外再加 host KV cache
继承 BlockManagerPool,新增 host_block_managers_ / load_block_transfer_infos_(H2D copy plan)/ offload_block_pair_queues_(D2G copy plan)。allocate 先调基础版再安排 H2D transfer;deallocate 时先把 device blocks cache 到 prefix cache,再根据 host/device 情况安排 offload。
9.5 BlockTable 在这里对应什么
这一层没有独立的 BlockTable 类。最接近的是 Sequence::kv_state().blocks_(vector<Block>),表示 logical block index 到 physical block id 的映射。下游 kernel 需要 slot id 时按下式转换:
token position i
-> block index = i / block_size
-> block offset = i % block_size
-> physical id = blocks_[block index].id()
-> slot id = physical_id * block_size + block_offset
10. 职责总表
| 类 | 层级 | 作用 |
|---|---|---|
Block | 最底层句柄 | 持有 id / ref_count / manager 指针;最后一个引用消失时回调 manager->free(id) |
BlockManager | allocator 抽象接口 | 定义 allocate/deallocate/cache/free 等物理 block 操作 |
BlockManagerImpl | 普通 allocator | free list + prefix cache + padding block |
ConcurrentBlockManagerImpl | 并发 allocator | BlockManagerImpl 加锁版,用于 disagg / kvcache store |
SingleBlockManager | 单 slot allocator | 管 sequence single block 或 linear-state slot,id 0 保留,无 prefix cache |
LinearStatePrefixCache | linear-state cache | 管 GDN recurrent state live slot / checkpoint / LRU |
CompositeBlockManager | 复合 allocator | 一个 manager 包多个子 manager,用 composite_blocks_ |
SlidingWindowBlockManager | composite 子类 | SWA 子 manager,实际分配复用 BlockManagerImpl |
BlockManagerPool | scheduler 入口 | 实现 KVCacheManager,按 sequence 编排分配 / 共享 / 缓存 / 回收 |
HierarchyBlockManagerPool | 分层 pool | 继承 BlockManagerPool,增加 host block managers 与 H2D/D2G transfer |
10.1 一张最关键的生命周期图
请求进入 -> Sequence 创建
-> BlockManagerPool::allocate(sequence)
├─ 选 dp_rank
├─ 分配 single_block_
├─ 若 linear_state: 分配 linear_state_slot_
├─ 若 prefix cache: allocate_shared() -> PrefixCache::match()
└─ 分配缺少的 KV blocks
-> worker 使用 block table / slot ids 做推理
-> Sequence 结束
-> BlockManagerPool::deallocate(sequence)
├─ cache(sequence) -> PrefixCache::insert()
├─ deallocate() -> used 统计下降
├─ deallocate_single_block()
└─ sequence->reset() -> Block handles 释放 -> ref_count--
├─ prefix cache 还持有: 物理 block 不回收
└─ ref_count==0: Block::~Block() -> manager->free(id) -> id 回 free list