KVCacheCoordinator + BlockPool:混合モデルの協調とブロックプール
役割
KVCacheCoordinator は vLLM v1 が複数の KV cache タイプ (full attention / sliding window / Mamba 状態 / MLA など) の上に設けた協調レイヤーです。各 KV cache group に 1 つの SingleTypeKVCacheManager (例: FullAttentionManager / SlidingWindowManager / RSWAManager) を割り当て、coordinator は「N 個の token の block を割り当てる」「最長プレフィックスヒットを探す」「リクエストを解放する」「埋まった block をキャッシュする」といった動作を group ごとに分割して対応する single-type manager に振ります。BlockPool は共有の物理 block プールで、すべての group が同じ KVCacheBlock 群を使います。ただし hash key に group_id を付けて区別するため、同じ block を複数の group (例: full attention と sliding window の同じ token) が同時にキャッシュできます。
構築時 KVCacheCoordinator.__init__ は 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 ハッシュテーブルでプレフィックスキャッシュ (prefix caching) のインデックスを管理します (block_pool.py:144-197)。
設計動機
なぜ coordinator を NoPrefix / Unitary / Hybrid の 3 種に分けるのか?
- 空実行のフォールバック:
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 が 1 つだけであることを前提とし (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 を 1 つのSpecGroupにまとめ、一緒に cache hit 探索をします (kv_cache_coordinator.py:560-588)。さらに full attention を先頭に並べ、後続 group にタイトな上界を与えます (kv_cache_coordinator.py:590-595)。 - 共有 BlockPool:すべての group が
num_gpu_blocks個の物理 block を共有します。異なる group のキャッシュ hash はmake_block_hash_with_group_id(block_hash, group_id)で連結し、get_cached_blockは group_id リストごとに検索します (block_pool.py:199-224)。BlockHashToBlockMapは 1 つの hash が複数 group の block に対応できます。 - LRU + tail block 優先:
FreeKVCacheBlockQueueは双方向リストで O(1) の途中削除を実現し、free 時には逆順に戻すことで tail block が先に淘汰されるようにします (kv_cache_utils.py:179-197)。free_blocksは hash のない block を hash 付きより先に淘汰します (block_pool.py:622-635)。 - null_block のプレースホルダー:block_id=0 は
null_blockで、remove_skipped_blocksが sliding window 外や preempt された block の置き換えに使います。ref_cnt は管理されず、解放時にはスキップするよう注意が必要です (block_pool.py:188-192)。
主要ファイル
KVCacheCoordinator.__init__:61-128— 抽象基底クラス。BlockPool と single-type managers を起動し、VLLM_PREFIX_CACHE_RETENTION_INTERVALを読みます。get_num_blocks_to_allocate:130-185— single-type managers を走査して合計、cross-attention は別経路。allocate_new_blocks:233-266— group ごとにmanager.allocate_new_blocksを呼び出し、encoder token は別処理。cache_blocks:268-283— 各 manager のcache_blocksを呼び出し、retention_intervalを考慮。remove_skipped_blocks:331-352— sliding window 外の block を null_block に置換、R-SWA はnum_prompt_tokensで gap を決定。KVCacheCoordinatorNoPrefixCache:377-424— プレフィックスキャッシュ無効時に使用、すべての hit は空を返します。UnitaryKVCacheCoordinator:427-496— 単一 group の高速パス。SpecGroup + HybridKVCacheCoordinator:499-588—SpecGroupNamedTuple + spec 単位での group 集約。find_longest_cache_hit_per_group:742-779— group ごとに独立して hit を探し、(blocks_per_group, hit_lengths_per_group)を返します。get_kv_cache_coordinator:782-822— ファクトリ:NoPrefix / Unitary / Hybrid の三択。BlockPool.__init__:144-197—FreeKVCacheBlockQueue、cached_block_hash_to_block、null_blockを起動。BlockPool.get_cached_block:199-224— group_id リストで同じ hash の block を探す。1 つでも miss すれば None を返します。BlockPool.cache_full_blocks:226-263— 埋まった block に hash を付けてcached_block_hash_to_blockに書き込み。block_maskで絶対にヒットしない block をスキップ可能。BlockPool.get_new_blocks:542-572— free queue の先頭から N 個取得し、ついでにキャッシュされた古い hash を evict。BlockPool.touch:597-612— ref_cnt +1、ref_cnt が 0 から 1 に上がるとき free queue から外します。BlockPool.free_blocks:614-635— hash なし block は prepend_last で優先淘汰、hash 付きは append (LRU 末尾)。BlockPool.reset_prefix_cache:656-690— null block だけが占めているときだけクリアを許可。KVCacheBlock:118-176— block メタデータ:block_id/ref_cnt/_block_hash/ リストポインタ /is_null。FreeKVCacheBlockQueue:179-197— 双方向リスト、O(1) の途中削除、LRU 順序。
データフロー
BlockPool.get_new_blocks が物理割り当ての真のエントリです。free queue の先頭から N 個の block をポップし、それぞれの古い hash を evict して ref_cnt を 1 に設定します:
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)、後者は新規 block を req_to_blocks[request_id] に append します。解放は BlockPool.free_blocks で行い、まず ref_cnt を -1 し、ref_cnt が 0 になった block は「hash あり / なし」で分けて free queue の末尾に戻します (block_pool.py:614-635)。リクエストの prompt が 1 つの 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)。
境界と失敗
- 空き block 数の超過:
get_new_blocksは事前チェックせず直接raise ValueErrorします (block_pool.py:553-554)。呼び出し側 (KVCacheManager.allocate_slots) はget_num_blocks_to_allocateで事前に計算しておく必要があります (kv_cache_coordinator.py:130-185)。 - Hybrid は DCP / PCP 非対応:
assert dcp_world_size == 1 and pcp_world_size == 1(kv_cache_coordinator.py:556-557)。 - Unitary は hash_block_size == block_size を強制:
assert not enable_caching or hash_block_size == self.block_size(kv_cache_coordinator.py:471-473)。 - Hybrid は最低 2 つの attention group:
assert len(self.attention_groups) > 1(kv_cache_coordinator.py:586-588)。 - block にすでに hash があるのに set しようとした:
set_block_hashは assert で重複を防ぎます (kv_cache_utils.py:153-157)。 reset_prefix_cacheのクリア拒否:null block 以外にまだ block が使われていればlogger.warning+Falseを返します (block_pool.py:665-672)。evict_blocksが外部 block_id を受け取る:KV connector が報告した block id が範囲外なら即座に assert します (block_pool.py:647-652)。- retention_interval のバリデーション:
_validate_prefix_cache_retention_intervalは正の整数でscheduler_block_sizeの倍数であることを要求します (kv_cache_coordinator.py:31-58)。
まとめ
Coordinator + BlockPool が KVCacheManager の下の物理層を構成します。coordinator は戦略に従って single-type managers に振り分け、BlockPool は物理 block プールとプレフィックスキャッシュのインデックスを管理します。block 割り当ての結果は最終的に block_id リストになり、ブロック表 が attention kernel が直接消費できるテンソルに包み込みます。