Skip to content

KVCacheCoordinator + BlockPool:混合模型的協調與塊池

源码版本v0.25.1

職責

KVCacheCoordinator 是 vLLM v1 在多 KV cache 類型(全注意力 / 滑窗 / Mamba 狀態 / MLA 等)之上的一層協調器。每個 KV cache group 配一個 SingleTypeKVCacheManager(如 FullAttentionManager / SlidingWindowManager / RSWAManager),coordinator 把"分配 N 個 token 的 block"、"找最長前綴命中"、"釋放請求"、"cache 已填滿的 block"這些動作按 group 拆開呼叫對應的 single-type manager。BlockPool 是共享的物理塊池子,所有 group 共用同一組 KVCacheBlock——只是 hash key 上附帶 group_id 區分,所以同一個塊可以同時被多個 group 快取(例如全注意力和滑窗的同一 token)。

構造時 KVCacheCoordinator.__init__ 直接 new 一個 BlockPool,然後 get_manager_for_kv_cache_spec 給每個 group 起一個 single-type manager(kv_cache_coordinator.py:91-120)。具體策略由 get_kv_cache_coordinator 工廠選擇:不開快取走 KVCacheCoordinatorNoPrefixCache,單 group 走 UnitaryKVCacheCoordinator,多 group 走 HybridKVCacheCoordinator(kv_cache_coordinator.py:782-822)。BlockPool 自己維護一個 FreeKVCacheBlockQueue(雙向鏈表)做 LRU 淘汰,一個 cached_block_hash_to_block 哈希表做前綴快取索引(block_pool.py:144-197)。

設計動機

為什麼要把 coordinator 分成 NoPrefix / Unitary / Hybrid 三種?

  • 空跑兜底:KVCacheCoordinatorNoPrefixCache 支持任意 group 數(包括 0),不實現任何前綴快取相關介面——find_longest_cache_hit 直接返回 (空 blocks, 0),get_num_common_prefix_blocks 返回全 0(kv_cache_coordinator.py:413-424)。這樣禁用 prefix caching 時不會走任何快取路徑。
  • 單 group 快路徑:UnitaryKVCacheCoordinator 假設 hash_block_size == block_size 且只有一個 group(kv_cache_coordinator.py:471-476),find_longest_cache_hit 直接調 single manager 不用合併(kv_cache_coordinator.py:480-496),避免 hybrid 那套 spec-group 合併開銷。
  • Hybrid 按 spec 聚合:HybridKVCacheCoordinator.verify_and_split_kv_cache_groups 把相同 spec 的多個 group 合成一個 SpecGroup,一起做 cache hit 查找(kv_cache_coordinator.py:560-588),並且把 full attention 排在最前面給後續 group 一個緊的上界(kv_cache_coordinator.py:590-595)。
  • 共享 BlockPool:所有 group 共用 num_gpu_blocks 個物理塊,不同 group 的快取 hash 用 make_block_hash_with_group_id(block_hash, group_id) 拼接,get_cached_block 按 group_id 列表分別查(block_pool.py:199-224),BlockHashToBlockMap 一個 hash 可對應多 group 的塊。
  • LRU + 尾塊優先:FreeKVCacheBlockQueue 用雙向鏈表實現 O(1) 中間刪除,free 時按反向順序塞回讓 tail block 先淘汰(kv_cache_utils.py:179-197);free_blocks 還把無 hash 的塊先於有 hash 的塊淘汰(block_pool.py:622-635)。
  • null_block 佔位:block_id=0 是 null_block,被 remove_skipped_blocks 用來替換滑窗外或被搶佔的塊,ref_cnt 不維護,釋放時要小心跳過(block_pool.py:188-192)。

關鍵檔案

資料流

BlockPool.get_new_blocks 是物理分配的真正入口——從 free queue 頭部彈出 N 個塊,逐個 evict 舊 hash、把 ref_cnt 設為 1:

python
def get_new_blocks(self, num_blocks: int) -> list[KVCacheBlock]:
    if num_blocks > self.get_num_free_blocks():
        raise ValueError(f"Cannot get {num_blocks} free blocks from the pool")

    ret: list[KVCacheBlock] = self.free_block_queue.popleft_n(num_blocks)

    if self.enable_caching:
        for block in ret:
            self._maybe_evict_cached_block(block)
            assert block.ref_cnt == 0
            block.ref_cnt += 1
            if self.metrics_collector:
                self.metrics_collector.on_block_allocated(block)
    else:
        for block in ret:
            assert block.ref_cnt == 0
            block.ref_cnt += 1
            if self.metrics_collector:
                self.metrics_collector.on_block_allocated(block)
    return ret

(block_pool.py:542-572)被 SingleTypeKVCacheManager.allocate_new_blocks 調(single_type_kv_cache_manager.py:279-311),後者把新塊 append 到 req_to_blocks[request_id]。釋放走 BlockPool.free_blocks,先 ref_cnt -1,ref_cnt 歸零的塊按"有 hash / 無 hash"分流塞回 free queue 尾部(block_pool.py:614-635)。當一條請求的 prompt 填滿一個 block 時,cache_full_blocks 把 block 的內容算 hash 並寫入 cached_block_hash_to_block,下一條相同前綴的請求過來 get_cached_block 直接命中、touch 把 ref_cnt +1 而不必重新算(block_pool.py:199-224)(block_pool.py:597-612)。

邊界與失敗

小結

Coordinator + BlockPool 構成了 KVCacheManager 之下的物理層:coordinator 按策略分發到 single-type managers,BlockPool 管物理塊池 + 前綴快取索引。塊分配的結果最終變成 block_id 列表,被 塊表 包裝成 attention kernel 能直接消費的張量。

對照官方資料:vLLM 文件 · README