Skip to content

KVCacheCoordinator + BlockPool: Koordination und Block-Pool bei hybriden Modellen

源码版本v0.25.1

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: KVCacheCoordinatorNoPrefixCache unterstützt eine beliebige Gruppenanzahl (inkl. 0) und implementiert keine der Prefix-Cache-Schnittstellen – find_longest_cache_hit liefert direkt (leere Blocks, 0), get_num_common_prefix_blocks liefert überall 0(kv_cache_coordinator.py:413-424). So wird bei deaktiviertem Prefix-Caching kein Caching-Pfad berührt.
  • Schnellpfad für einzelne Gruppe: UnitaryKVCacheCoordinator nimmt an, dass hash_block_size == block_size und nur eine Gruppe existiert(kv_cache_coordinator.py:471-476). find_longest_cache_hit ruft 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_groups fasst mehrere Gruppen mit identischer Spec zu einer SpecGroup zusammen 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_blocks physische Blocks. Die Cache-Hashes verschiedener Gruppen werden über make_block_hash_with_group_id(block_hash, group_id) verkettet; get_cached_block sucht separat nach der group_id-Liste(block_pool.py:199-224). In BlockHashToBlockMap kann ein Hash auf Blocks mehrerer Gruppen verweisen.
  • LRU mit Tail-Block-Vorrang: FreeKVCacheBlockQueue wird 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_blocks verdrä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 von remove_skipped_blocks verwendet, 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

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:

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) 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_blocks prüft nicht im Voraus, sondern wirft direkt raise ValueError(block_pool.py:553-554). Der Aufrufer (KVCacheManager.allocate_slots) muss vorher über get_num_blocks_to_allocate korrekt 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_hash verhindert per assert eine Doppelbelegung(kv_cache_utils.py:153-157).
  • reset_prefix_cache lehnt das Leeren ab: Wenn außer dem Null-Block weitere Blocks belegt sind, erfolgt logger.warning und die Rückgabe False(block_pool.py:665-672).
  • evict_blocks mit 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_interval verlangt, dass positive ganze Zahlen ein Vielfaches von scheduler_block_size sind(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.

Siehe offizielle Dokumentation: vLLM 文档 · README.