KVCacheCoordinator + BlockPool: Koordination und Block-Pool bei hybriden Modellen
Verantwortung
KVCacheCoordinator ist eine Koordinationsschicht in vLLM v1 über mehreren KV-Cache-Typen (Full Attention / Sliding Window / Mamba-Zustand / MLA etc.). Jeder KV-Cache-Gruppe ist ein SingleTypeKVCacheManager zugeordnet (z. B. FullAttentionManager / SlidingWindowManager / RSWAManager). Der Coordinator zerlegt Aktionen wie "N Token Blocks zuweisen", "längsten Prefix-Treffer finden", "Anfrage freigeben", "gefüllte Blocks cachen" nach Gruppe und ruft jeweils den passenden Single-Type-Manager auf. BlockPool ist der gemeinsam genutzte physische Block-Pool; alle Gruppen verwenden dieselben KVCacheBlock-Instanzen – lediglich der Hash-Schlüssel trägt eine group_id, sodass derselbe Block gleichzeitig von mehreren Gruppen gecacht werden kann (z. B. Full Attention und Sliding Window für dasselbe Token).
Beim Aufbau erzeugt KVCacheCoordinator.__init__ direkt einen BlockPool und lässt dann get_manager_for_kv_cache_spec für jede Gruppe einen Single-Type-Manager anlegen(kv_cache_coordinator.py:91-120). Die konkrete Strategie wählt die Factory get_kv_cache_coordinator: ohne Caching KVCacheCoordinatorNoPrefixCache, einzelne Gruppe UnitaryKVCacheCoordinator, mehrere Gruppen HybridKVCacheCoordinator(kv_cache_coordinator.py:782-822). BlockPool selbst verwaltet eine FreeKVCacheBlockQueue (doppelt verkettete Liste) für die LRU-Verdrängung und eine Hashtabelle cached_block_hash_to_block als Prefix-Cache-Index(block_pool.py:144-197).
Entwurfsmotivation
Warum wird der Coordinator in NoPrefix / Unitary / Hybrid aufgeteilt?
- Notfall-Pfad ohne Caching:
KVCacheCoordinatorNoPrefixCacheunterstützt eine beliebige Gruppenanzahl (inkl. 0) und implementiert keine der Prefix-Cache-Schnittstellen –find_longest_cache_hitliefert direkt(leere Blocks, 0),get_num_common_prefix_blocksliefert überall 0(kv_cache_coordinator.py:413-424). So wird bei deaktiviertem Prefix-Caching kein Caching-Pfad berührt. - Schnellpfad für einzelne Gruppe:
UnitaryKVCacheCoordinatornimmt an, dasshash_block_size == block_sizeund nur eine Gruppe existiert(kv_cache_coordinator.py:471-476).find_longest_cache_hitruft direkt den Single-Manager auf, ohne Verschmelzung(kv_cache_coordinator.py:480-496), und spart sich den Spec-Group-Merge-Overhead von Hybrid. - Hybrid aggregiert nach Spec:
HybridKVCacheCoordinator.verify_and_split_kv_cache_groupsfasst mehrere Gruppen mit identischer Spec zu einerSpecGroupzusammen und führt die Cache-Hit-Suche gemeinsam aus(kv_cache_coordinator.py:560-588). Full Attention wird dabei nach vorne sortiert, um den nachfolgenden Gruppen eine enge obere Schranke zu geben(kv_cache_coordinator.py:590-595). - Gemeinsamer BlockPool: Alle Gruppen teilen sich
num_gpu_blocksphysische Blocks. Die Cache-Hashes verschiedener Gruppen werden übermake_block_hash_with_group_id(block_hash, group_id)verkettet;get_cached_blocksucht separat nach der group_id-Liste(block_pool.py:199-224). InBlockHashToBlockMapkann ein Hash auf Blocks mehrerer Gruppen verweisen. - LRU mit Tail-Block-Vorrang:
FreeKVCacheBlockQueuewird als doppelt verkettete Liste implementiert, um O(1)-Löschungen in der Mitte zu ermöglichen. Beim Freigeben werden Blocks in umgekehrter Reihenfolge eingefügt, sodass der Tail-Block zuerst verdrängt wird(kv_cache_utils.py:179-197).free_blocksverdrängt außerdem Blocks ohne Hash vor Blocks mit Hash(block_pool.py:622-635). - null_block-Platzhalter: block_id=0 ist der
null_block. Er wird vonremove_skipped_blocksverwendet, um Blocks außerhalb des Sliding Windows oder verdrängte Blocks zu ersetzen. ref_cnt wird nicht gepflegt; beim Freigeben muss er übersprungen werden(block_pool.py:188-192).
Schlüsseldateien
KVCacheCoordinator.__init__:61-128— abstrakte Basisklasse, legt BlockPool und Single-Type-Manager an, liestVLLM_PREFIX_CACHE_RETENTION_INTERVAL.get_num_blocks_to_allocate:130-185— Summierung über alle Single-Type-Manager; Cross-Attention geht einen separaten Pfad.allocate_new_blocks:233-266— ruft pro Gruppemanager.allocate_new_blocksauf, Encoder-Token separat behandelt.cache_blocks:268-283— ruftcache_blocksjedes Managers auf, mitretention_interval.remove_skipped_blocks:331-352— ersetzt Blocks außerhalb des Sliding Windows durch null_block; R-SWA nutztnum_prompt_tokensfür den Gap.KVCacheCoordinatorNoPrefixCache:377-424— wird bei deaktiviertem Prefix-Cache verwendet, alle Hits kommen leer zurück.UnitaryKVCacheCoordinator:427-496— Schnellpfad für einzelne Gruppe.SpecGroup + HybridKVCacheCoordinator:499-588—SpecGroup-NamedTuple + Aggregation von Gruppen nach Spec.find_longest_cache_hit_per_group:742-779— sucht pro Gruppe unabhängig den Hit, liefert(blocks_per_group, hit_lengths_per_group).get_kv_cache_coordinator:782-822— Factory: wählt eines aus NoPrefix / Unitary / Hybrid.BlockPool.__init__:144-197— legtFreeKVCacheBlockQueue,cached_block_hash_to_block,null_blockan.BlockPool.get_cached_block:199-224— sucht nach group_id-Liste nach Blocks mit gleichem Hash; bei einem Miss wird None geliefert.BlockPool.cache_full_blocks:226-263— gefüllte Blocks werden gehasht und incached_block_hash_to_blockeingetragen;block_maskkann Blocks überspringen, die nie getroffen werden.BlockPool.get_new_blocks:542-572— entnimmt der Free-Queue N Blocks am Kopf und verdrängt ggf. bereits gecachte alte Hashes.BlockPool.touch:597-612— ref_cnt +1; wenn ref_cnt von 0 auf 1 geht, wird der Block aus der Free-Queue entfernt.BlockPool.free_blocks:614-635— Blocks ohne Hash werden per prepend_last vorrangig verdrängt, Blocks mit Hash per append (LRU-Ende).BlockPool.reset_prefix_cache:656-690— erlaubt das Leeren nur, wenn ausschließlich der Null-Block belegt ist.KVCacheBlock:118-176— Block-Metadaten:block_id/ref_cnt/_block_hash/ Listenzeiger /is_null.FreeKVCacheBlockQueue:179-197— doppelt verkettete Liste, O(1)-Löschung in der Mitte, LRU-Reihenfolge.
Datenfluss
BlockPool.get_new_blocks ist der eigentliche Einstieg für die physische Zuweisung: N Blocks werden am Kopf der Free-Queue entnommen, jeder alte Hash verdrängt und ref_cnt auf 1 gesetzt:
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) Aufgerufen wird dies von SingleTypeKVCacheManager.allocate_new_blocks(single_type_kv_cache_manager.py:279-311), das die neuen Blocks an req_to_blocks[request_id] anhängt. Die Freigabe läuft über BlockPool.free_blocks: Zuerst wird ref_cnt um 1 verringert; wenn ref_cnt Null erreicht, werden die Blocks nach "mit Hash / ohne Hash" klassifiziert und am Ende der Free-Queue eingefügt(block_pool.py:614-635). Wenn die Prompt einer Anfrage einen Block füllt, berechnet cache_full_blocks den Hash des Blockinhalts und trägt ihn in cached_block_hash_to_block ein. Bei der nächsten Anfrage mit demselben Prefix trifft get_cached_block direkt zu und touch erhöht ref_cnt, ohne neu rechnen zu müssen(block_pool.py:199-224)(block_pool.py:597-612).
Grenzen und Fehler
- Anzahl freier Blocks überschritten:
get_new_blocksprüft nicht im Voraus, sondern wirft direktraise ValueError(block_pool.py:553-554). Der Aufrufer (KVCacheManager.allocate_slots) muss vorher überget_num_blocks_to_allocatekorrekt rechnen(kv_cache_coordinator.py:130-185). - Hybrid unterstützt kein DCP / PCP:
assert dcp_world_size == 1 and pcp_world_size == 1(kv_cache_coordinator.py:556-557). - Unitary erzwingt hash_block_size == block_size:
assert not enable_caching or hash_block_size == self.block_size(kv_cache_coordinator.py:471-473). - Hybrid benötigt mindestens zwei Attention-Gruppen:
assert len(self.attention_groups) > 1(kv_cache_coordinator.py:586-588). - Block hat bereits Hash und wird nochmal gesetzt:
set_block_hashverhindert per assert eine Doppelbelegung(kv_cache_utils.py:153-157). reset_prefix_cachelehnt das Leeren ab: Wenn außer dem Null-Block weitere Blocks belegt sind, erfolgtlogger.warningund die RückgabeFalse(block_pool.py:665-672).evict_blocksmit externer block_id: Vom KV-Connector gemeldete Block-IDs, die außerhalb des Bereichs liegen, werden sofort per assert abgelehnt(block_pool.py:647-652).- retention_interval-Validierung:
_validate_prefix_cache_retention_intervalverlangt, dass positive ganze Zahlen ein Vielfaches vonscheduler_block_sizesind(kv_cache_coordinator.py:31-58).
Zusammenfassung
Coordinator + BlockPool bilden die physische Schicht unterhalb des KVCacheManager: Der Coordinator verteilt strategisch auf die Single-Type-Manager, BlockPool verwaltet den physischen Block-Pool und den Prefix-Cache-Index. Das Ergebnis der Block-Zuweisung wird schließlich zu einer block_id-Liste und von der Blocktabelle in einen Tensor verpackt, den der Attention-Kernel direkt konsumieren kann.