xLLM Notes / notes / kv-cache-block-management
Knowledge · Memory Management

KV Cache Block 管理架构

谁是入口、谁真正分配物理 block、谁只是资源句柄、谁负责缓存 / 分层 / 特殊模式。一张从 BlockManagerPoolBlock 的分层地图,含 prefix cache、composite、hierarchy 与 linear-state slot。

Note Block Manager Prefix Cache 内存管理

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_。关键点:它持有一个非 owningBlockManager* 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 cacheallocate_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
与已发布报告呼应:save / restore / 数据契约的完整机制,见配套 note Linear-State Prefix Cache 内部机制 及设计报告 Qwen3.5 Linear-State Prefix Cache(PR #1839)

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)
顺序不能乱:先 cache,再 deallocate,再 reset sequence。因为 prefix cache 插入需要 sequence 里的 token 和 block,不能先 reset。

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(如 BlockManagerImplSlidingWindowBlockManager)。与普通路径最大区别在 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)
BlockManagerallocator 抽象接口定义 allocate/deallocate/cache/free 等物理 block 操作
BlockManagerImpl普通 allocatorfree list + prefix cache + padding block
ConcurrentBlockManagerImpl并发 allocatorBlockManagerImpl 加锁版,用于 disagg / kvcache store
SingleBlockManager单 slot allocator管 sequence single block 或 linear-state slot,id 0 保留,无 prefix cache
LinearStatePrefixCachelinear-state cache管 GDN recurrent state live slot / checkpoint / LRU
CompositeBlockManager复合 allocator一个 manager 包多个子 manager,用 composite_blocks_
SlidingWindowBlockManagercomposite 子类SWA 子 manager,实际分配复用 BlockManagerImpl
BlockManagerPoolscheduler 入口实现 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