Skip to content

KVCacheCoordinator + BlockPool: coordinación y pool de bloques para modelos híbridos

源码版本v0.25.1

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): KVCacheCoordinatorNoPrefixCache admite cualquier número de grupos (incluido 0) y no implementa ninguna interfaz relacionada con la caché de prefijo: find_longest_cache_hit devuelve directamente (bloques vacíos, 0) y get_num_common_prefix_blocks devuelve 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: UnitaryKVCacheCoordinator asume hash_block_size == block_size y un único grupo (kv_cache_coordinator.py:471-476); find_longest_cache_hit invoca 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_groups fusiona varios grupos con la misma spec en un SpecGroup y 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_blocks bloques físicos; los hashes de distintos grupos se concatenan con make_block_hash_with_group_id(block_hash, group_id), get_cached_block consulta por la lista de group_id por separado (block_pool.py:199-224), y en BlockHashToBlockMap un mismo hash puede apuntar a bloques de varios grupos.
  • LRU + cola final prioritaria: FreeKVCacheBlockQueue usa 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ás free_blocks evicte primero los bloques sin hash y luego los que tienen hash (block_pool.py:622-635).
  • Marcador null_block: el block_id=0 es el null_block, usado por remove_skipped_blocks para sustituir los bloques fuera de la sliding window o los preempted; no mantiene ref_cnt y al liberarlo hay que tener cuidado de saltárselo (block_pool.py:188-192).

Archivos clave

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:

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) 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_blocks no prechequea, lanza directamente raise ValueError (block_pool.py:553-554); el llamador (KVCacheManager.allocate_slots) debe calcular primero con get_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_hash usa assert para evitar duplicados (kv_cache_utils.py:153-157).
  • reset_prefix_cache se niega a limpiar: si además del null block hay otros bloques ocupados, hace logger.warning + devuelve False (block_pool.py:665-672).
  • evict_blocks recibe 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_interval exige que un entero positivo sea múltiplo de scheduler_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.