KVCacheCoordinator + BlockPool : coordination et pool de blocs pour modèles hybrides
Responsabilités
KVCacheCoordinator est, dans vLLM v1, la couche de coordination au-dessus des multiples types de cache KV (full attention / sliding window / état Mamba / MLA, etc.). Chaque groupe de cache KV dispose d'un SingleTypeKVCacheManager (par ex. FullAttentionManager / SlidingWindowManager / RSWAManager) ; le coordinator répartit « allouer N tokens de blocs », « trouver le plus long préfixe en cache », « libérer une requête », « cacher un bloc rempli » en appelant le bon single-type manager selon le groupe. BlockPool est le pool de blocs physiques partagé : tous les groupes partagent le même ensemble de KVCacheBlock — seule la clé de hash porte un group_id pour les distinguer, donc un même bloc peut être caché simultanément par plusieurs groupes (par ex. full attention et sliding window pour le même token).
À la construction, KVCacheCoordinator.__init__ instancie un BlockPool, puis get_manager_for_kv_cache_spec crée un single-type manager par groupe(kv_cache_coordinator.py:91-120). La stratégie est choisie par la fabrique get_kv_cache_coordinator : sans prefix caching → KVCacheCoordinatorNoPrefixCache, un seul groupe → UnitaryKVCacheCoordinator, plusieurs groupes → HybridKVCacheCoordinator(kv_cache_coordinator.py:782-822). BlockPool maintient lui-même une FreeKVCacheBlockQueue (liste doublement chaînée) pour l'éviction LRU, et une table de hachage cached_block_hash_to_block pour l'index du prefix caching(block_pool.py:144-197).
Motivation de conception
Pourquoi scinder le coordinator en NoPrefix / Unitary / Hybrid ?
- Repli à vide :
KVCacheCoordinatorNoPrefixCachesupporte un nombre de groupes quelconque (y compris 0), n'implémente aucune interface liée au prefix caching —find_longest_cache_hitrenvoie directement(blocks vides, 0),get_num_common_prefix_blocksrenvoie toujours 0(kv_cache_coordinator.py:413-424). Quand le prefix caching est désactivé, on n'emprunte aucun chemin de cache. - Chemin rapide mono-groupe :
UnitaryKVCacheCoordinatorsupposehash_block_size == block_sizeet un seul groupe(kv_cache_coordinator.py:471-476) ;find_longest_cache_hitappelle directement le single manager sans fusion(kv_cache_coordinator.py:480-496) pour éviter l'overhead de fusion spec-group du mode hybrid. - Agrégation par spec en Hybrid :
HybridKVCacheCoordinator.verify_and_split_kv_cache_groupsfusionne les groupes de même spec en unSpecGroupet fait la recherche de cache hit conjointement(kv_cache_coordinator.py:560-588), en plaçant la full attention en tête pour fournir aux groupes suivants une borne supérieure serrée(kv_cache_coordinator.py:590-595). - BlockPool partagé : tous les groupes partagent
num_gpu_blocksblocs physiques ; les hashes des différents groupes sont préfixés viamake_block_hash_with_group_id(block_hash, group_id),get_cached_blockinterroge par liste de group_id(block_pool.py:199-224) ; un même hash dansBlockHashToBlockMappeut correspondre aux blocs de plusieurs groupes. - LRU + priorité aux blocs de queue :
FreeKVCacheBlockQueueest une liste doublement chaînée pour une suppression intermédiaire en O(1) ; à la libération, on réinsère en ordre inverse afin que le bloc tail soit évicté en premier(kv_cache_utils.py:179-197) ;free_blocksévite d'abord les blocs sans hash avant ceux qui en ont un(block_pool.py:622-635). - Sentinelle null_block : block_id=0 est le
null_block, utilisé parremove_skipped_blockspour remplacer les blocs hors sliding window ou preempted ; son ref_cnt n'est pas maintenu, il faut sauter à la libération(block_pool.py:188-192).
Fichiers clés
KVCacheCoordinator.__init__:61-128— classe abstraite, instancie BlockPool, les single-type managers, litVLLM_PREFIX_CACHE_RETENTION_INTERVAL.get_num_blocks_to_allocate:130-185— somme sur les single-type managers, chemin séparé pour la cross-attention.allocate_new_blocks:233-266— appellemanager.allocate_new_blockspar groupe, tokens encoder traités à part.cache_blocks:268-283— appellecache_blocksde chaque manager, avecretention_interval.remove_skipped_blocks:331-352— remplace les blocs hors sliding window par null_block ; R-SWA utilisenum_prompt_tokenspour décider du gap.KVCacheCoordinatorNoPrefixCache:377-424— utilisé quand le prefix caching est désactivé, tous les hit sont vides.UnitaryKVCacheCoordinator:427-496— chemin rapide mono-groupe.SpecGroup + HybridKVCacheCoordinator:499-588— NamedTupleSpecGroup+ agrégation des groupes par spec.find_longest_cache_hit_per_group:742-779— recherche indépendante par groupe, renvoie(blocks_per_group, hit_lengths_per_group).get_kv_cache_coordinator:782-822— fabrique : NoPrefix / Unitary / Hybrid.BlockPool.__init__:144-197— instancieFreeKVCacheBlockQueue,cached_block_hash_to_block,null_block.BlockPool.get_cached_block:199-224— recherche par liste de group_id sur le même hash, un miss → None.BlockPool.cache_full_blocks:226-263— hache les blocs remplis et les écrit danscached_block_hash_to_block,block_maskpermet de sauter les blocs qui ne seront jamais hit.BlockPool.get_new_blocks:542-572— prend N blocs en tête de free queue, evict les anciens hashes au passage.BlockPool.touch:597-612— ref_cnt +1 ; quand ref_cnt passe de 0 à 1, on le retire de la free queue.BlockPool.free_blocks:614-635— prepend_last pour les blocs sans hash (éviction prioritaire), append pour ceux avec hash (queue LRU).BlockPool.reset_prefix_cache:656-690— ne vide que lorsque seuls les null blocks sont occupés.KVCacheBlock:118-176— métadonnées de bloc :block_id/ref_cnt/_block_hash/ pointeurs de liste /is_null.FreeKVCacheBlockQueue:179-197— liste doublement chaînée, suppression intermédiaire en O(1), ordre LRU.
Flux de données
BlockPool.get_new_blocks est la véritable entrée d'allocation physique : il pop N blocs en tête de la free queue, evict un à un les anciens hashes, et met 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) Appelé par SingleTypeKVCacheManager.allocate_new_blocks(single_type_kv_cache_manager.py:279-311), qui append les nouveaux blocs à req_to_blocks[request_id]. La libération passe par BlockPool.free_blocks : on décrémente ref_cnt, et pour les blocs dont ref_cnt tombe à zéro, on les réinsère en queue de free queue en distinguant « avec hash / sans hash »(block_pool.py:614-635). Quand le prompt d'une requête remplit un bloc, cache_full_blocks hache le contenu et l'écrit dans cached_block_hash_to_block ; à la requête suivante avec le même préfixe, get_cached_block hit directement, et touch incrémente ref_cnt sans recalculer(block_pool.py:199-224)(block_pool.py:597-612).
Limites et échecs
- Dépassement du nombre de blocs libres :
get_new_blocksne pré-vérifie pas, lève directementValueError(block_pool.py:553-554) ; l'appelant (KVCacheManager.allocate_slots) doit d'abord calculer viaget_num_blocks_to_allocate(kv_cache_coordinator.py:130-185). - Hybrid ne supporte pas DCP / PCP :
assert dcp_world_size == 1 and pcp_world_size == 1(kv_cache_coordinator.py:556-557). - Unitary impose hash_block_size == block_size :
assert not enable_caching or hash_block_size == self.block_size(kv_cache_coordinator.py:471-473). - Hybrid nécessite au moins deux attention groups :
assert len(self.attention_groups) > 1(kv_cache_coordinator.py:586-588). - Un bloc ayant déjà un hash ne peut être reset :
set_block_hashutilise un assert pour empêcher les doublons(kv_cache_utils.py:153-157). reset_prefix_cacherefuse de nettoyer : si d'autres blocs que null block sont encore occupés, on log en warning et on renvoieFalse(block_pool.py:665-672).evict_blocksreçoit des block_id externes : les ids de blocs remontés par le KV connector hors plage déclenchent un assert(block_pool.py:647-652).- Validation de retention_interval :
_validate_prefix_cache_retention_intervalexige un entier positif multiple descheduler_block_size(kv_cache_coordinator.py:31-58).
Résumé
Coordinator + BlockPool constituent la couche physique sous KVCacheManager : le coordinator répartit vers les single-type managers selon la stratégie, BlockPool gère le pool de blocs physiques et l'index du prefix cache. Le résultat de l'allocation de blocs devient une liste de block_id, que la block table emballe en tensors consommables directement par les kernels d'attention.
Voir la documentation officielle : Documentation vLLM · README