KVCacheCoordinator + BlockPool: coordinación y pool de bloques para modelos híbridos
Responsabilidades
KVCacheCoordinator es la capa de coordinación de vLLM v1 por encima de múltiples tipos de caché KV (full attention / sliding window / estado Mamba / MLA, etc.). Cada grupo de caché KV tiene su SingleTypeKVCacheManager (por ejemplo FullAttentionManager / SlidingWindowManager / RSWAManager); el coordinator descompone acciones como "asignar N tokens en bloques", "encontrar la coincidencia de prefijo más larga", "liberar una petición" o "cachear bloques ya llenos" y las reparte entre los single-type managers correspondientes. BlockPool es el pool de bloques físicos compartido: todos los grupos comparten el mismo conjunto de KVCacheBlock, solo que la hash key lleva un group_id adicional, de modo que un mismo bloque puede estar cacheado por varios grupos a la vez (por ejemplo el mismo token para full attention y para sliding window).
Durante la construcción, KVCacheCoordinator.__init__ crea directamente un BlockPool y luego get_manager_for_kv_cache_spec instancia un single-type manager por cada grupo (kv_cache_coordinator.py:91-120). La política concreta la elige la factoría get_kv_cache_coordinator: sin caché se va por KVCacheCoordinatorNoPrefixCache, con un único grupo por UnitaryKVCacheCoordinator, y con múltiples grupos por HybridKVCacheCoordinator (kv_cache_coordinator.py:782-822). El BlockPool mantiene por su cuenta una FreeKVCacheBlockQueue (lista doblemente enlazada) para el reemplazo LRU, y una tabla hash cached_block_hash_to_block como índice de caché de prefijo (block_pool.py:144-197).
Motivación de diseño
¿Por qué dividir el coordinator en NoPrefix / Unitary / Hybrid?
- Ruta vacía (no-op):
KVCacheCoordinatorNoPrefixCacheadmite cualquier número de grupos (incluido 0) y no implementa ninguna interfaz relacionada con la caché de prefijo:find_longest_cache_hitdevuelve directamente(bloques vacíos, 0)yget_num_common_prefix_blocksdevuelve todo ceros (kv_cache_coordinator.py:413-424). Así, cuando se desactiva la caché de prefijo (prefix caching), no se recorre ninguna ruta de caché. - Camino rápido de un solo grupo:
UnitaryKVCacheCoordinatorasumehash_block_size == block_sizey un único grupo (kv_cache_coordinator.py:471-476);find_longest_cache_hitinvoca directamente al single manager sin merges (kv_cache_coordinator.py:480-496), evitando el coste de fusión spec-group del híbrido. - Hybrid agrega por spec:
HybridKVCacheCoordinator.verify_and_split_kv_cache_groupsfusiona varios grupos con la misma spec en unSpecGroupy busca hits de caché conjuntamente (kv_cache_coordinator.py:560-588); además coloca full attention el primero para dar a los grupos siguientes una cota superior ajustada (kv_cache_coordinator.py:590-595). - BlockPool compartido: todos los grupos comparten
num_gpu_blocksbloques físicos; los hashes de distintos grupos se concatenan conmake_block_hash_with_group_id(block_hash, group_id),get_cached_blockconsulta por la lista de group_id por separado (block_pool.py:199-224), y enBlockHashToBlockMapun mismo hash puede apuntar a bloques de varios grupos. - LRU + cola final prioritaria:
FreeKVCacheBlockQueueusa lista doblemente enlazada para borrar del medio en O(1); al liberar, los bloques se devuelven en orden inverso para que el tail block se evicte primero (kv_cache_utils.py:179-197); ademásfree_blocksevicte primero los bloques sin hash y luego los que tienen hash (block_pool.py:622-635). - Marcador null_block: el
block_id=0es elnull_block, usado porremove_skipped_blockspara sustituir los bloques fuera de la sliding window o los preempted; no mantieneref_cnty al liberarlo hay que tener cuidado de saltárselo (block_pool.py:188-192).
Archivos clave
KVCacheCoordinator.__init__:61-128— clase base abstracta; crea el BlockPool, levanta los single-type managers, leeVLLM_PREFIX_CACHE_RETENTION_INTERVAL.get_num_blocks_to_allocate:130-185— suma sobre los single-type managers; la cross-attention va por una ruta aparte.allocate_new_blocks:233-266— por grupo llama amanager.allocate_new_blocks; los tokens del encoder se tratan aparte.cache_blocks:268-283— invocacache_blocksde cada manager conretention_interval.remove_skipped_blocks:331-352— sustituye por null_block los bloques fuera de la sliding window; R-SWA usanum_prompt_tokenspara decidir el gap.KVCacheCoordinatorNoPrefixCache:377-424— usado cuando se desactiva la caché de prefijo; todos los hits devuelven vacío.UnitaryKVCacheCoordinator:427-496— camino rápido de un grupo.SpecGroup + HybridKVCacheCoordinator:499-588— NamedTupleSpecGroup+ agregación de grupos por spec.find_longest_cache_hit_per_group:742-779— consulta hits por grupo y devuelve(blocks_per_group, hit_lengths_per_group).get_kv_cache_coordinator:782-822— factoría: elige entre NoPrefix / Unitary / Hybrid.BlockPool.__init__:144-197— creaFreeKVCacheBlockQueue,cached_block_hash_to_blockynull_block.BlockPool.get_cached_block:199-224— busca bloques con la misma hash por lista de group_id; cualquier miss devuelve None.BlockPool.cache_full_blocks:226-263— los bloques llenos reciben hash y se escriben encached_block_hash_to_block; admiteblock_maskpara saltar bloques que nunca harán hit.BlockPool.get_new_blocks:542-572— saca N bloques de la cabeza de la free queue y evicte los hashes viejos al pasar.BlockPool.touch:597-612—ref_cnt += 1; cuando pasa de 0 a 1 se saca de la free queue.BlockPool.free_blocks:614-635— los bloques sin hash van aprepend_lastpara evicción prioritaria; los que tienen hash van aappend(cola LRU).BlockPool.reset_prefix_cache:656-690— solo permite vaciar cuando solo queda el null block.KVCacheBlock:118-176— metadata del bloque:block_id/ref_cnt/_block_hash/ punteros de lista /is_null.FreeKVCacheBlockQueue:179-197— lista doblemente enlazada; borrado del medio en O(1); orden LRU.
Flujo de datos
BlockPool.get_new_blocks es la verdadera entrada a la asignación física: saca N bloques de la cabeza de la free queue, evicte cada hash viejo y pone 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) Lo invoca SingleTypeKVCacheManager.allocate_new_blocks (single_type_kv_cache_manager.py:279-311), que a su vez appenda los nuevos bloques a req_to_blocks[request_id]. La liberación pasa por BlockPool.free_blocks: primero ref_cnt -= 1, y los bloques cuyo ref_cnt llega a cero se devuelven a la cola de la free queue divididos entre "con hash / sin hash" (block_pool.py:614-635). Cuando el prompt de una petición rellena un bloque, cache_full_blocks calcula el hash del contenido y lo escribe en cached_block_hash_to_block; la siguiente petición con el mismo prefijo hace hit directo en get_cached_block y un touch sube ref_cnt += 1 sin recalcular (block_pool.py:199-224) (block_pool.py:597-612).
Límites y fallos
- Más bloques que libres:
get_new_blocksno prechequea, lanza directamenteraise ValueError(block_pool.py:553-554); el llamador (KVCacheManager.allocate_slots) debe calcular primero conget_num_blocks_to_allocate(kv_cache_coordinator.py:130-185). - Hybrid no admite DCP / PCP:
assert dcp_world_size == 1 and pcp_world_size == 1(kv_cache_coordinator.py:556-557). - Unitary exige hash_block_size == block_size:
assert not enable_caching or hash_block_size == self.block_size(kv_cache_coordinator.py:471-473). - Hybrid exige al menos dos attention group:
assert len(self.attention_groups) > 1(kv_cache_coordinator.py:586-588). - set_block_hash sobre bloque con hash previo:
set_block_hashusa assert para evitar duplicados (kv_cache_utils.py:153-157). reset_prefix_cachese niega a limpiar: si además del null block hay otros bloques ocupados, hacelogger.warning+ devuelveFalse(block_pool.py:665-672).evict_blocksrecibe block_id externo: los ids que reporta el KV connector fuera de rango disparan un assert inmediato (block_pool.py:647-652).- Validación de retention_interval:
_validate_prefix_cache_retention_intervalexige que un entero positivo sea múltiplo descheduler_block_size(kv_cache_coordinator.py:31-58).
Resumen
Coordinator + BlockPool forman la capa física por debajo de KVCacheManager: el coordinator reparte a los single-type managers según la política, y BlockPool gestiona el pool de bloques físicos + el índice de caché de prefijo. El resultado de la asignación de bloques acaba siendo una lista de block_id que la tabla de bloques envuelve en un tensor que el kernel de attention puede consumir directamente.
Véase la documentación oficial: Documentación de vLLM · README.