KVCacheManager: KV-Cache-Lebenszyklus pro Anfrage
Verantwortung
KVCacheManager ist die KV-Cache (KV-Cache)-Schnittstelle, die der Scheduler (Scheduler) sieht. Es kapselt Aktionen wie "dieser Anfrage N Blocks zuweisen", "die Anfrage ist fertig, Blocks zurückgeben" und "prüfen, wie viel Prefix-Cache (Prefix-Caching) diese Prompt trifft" in den drei High-Level-APIs allocate_slots / free / get_computed_blocks. Intern delegiert es die Koordination unterschiedlicher KV-Cache-Typen (Full Attention / Sliding Window / Mamba-Zustand etc.) vollständig an KVCacheCoordinator und das physische Block-Pool-Management an BlockPool. Der Scheduler erhält lediglich ein unveränderliches KVCacheBlocks-Ergebnis(kv_cache_manager.py:29-50), ohne wissen zu müssen, ob darunter ein einzelner Typ oder ein hybrides Modell liegt.
Beim Aufbau setzt KVCacheManager.__init__ über get_kv_cache_coordinator den Coordinator auf und hängt block_pool / num_kv_cache_groups / kv_cache_config an sich selbst an(kv_cache_manager.py:147-183). Der Wasserzeichen-Puffer (watermark_blocks = int(watermark * num_blocks)) ist die Reserve für bereits verplante Anfragen und wirkt sich nur auf Anfragen im Status WAITING / PREEMPTED aus, damit eine neu eintreffende Anfrage nicht sofort bereits halb abgearbeitete Anfragen verdrängt(kv_cache_manager.py:164-167)(kv_cache_manager.py:367-374). Der Manager hält selbst kaum Per-Request-Zustand vor; die gesamten req_to_blocks liegen in SingleTypeKVCacheManager.
Entwurfsmotivation
Warum wirkt der Manager so dünn und die eigentliche Logik sitzt vollständig im Coordinator?
- Aufgabenschichtung:
KVCacheManagerist für die "anfrageorientierte" Semantik zuständig (allocate_slots / free / get_computed_blocks),KVCacheCoordinatorfür die "an KV-Cache-Gruppen orientierte" Koordination (wie bei hybriden Modellen mehrere Gruppen gemeinsam zugewiesen werden), undBlockPoolfür das "physische Block-orientierte" Pooling. Jede Schicht kümmert sich um ihre eigenen Belange, der Manager fungiert als Klebstoff. KVCacheBlocksals stabile Schnittstelle:blocks[i][j]bezeichnet den j-ten Block der i-ten KV-Cache-Gruppe;get_block_ids()wandelt ihn in eintuple[list[int], ...]für das Attention-Backend um(kv_cache_manager.py:73-88).__add__erlaubt es, die Ergebnisse zweierallocate_slots-Aufrufe zusammenzufügen(kv_cache_manager.py:52-59);new_emptyliefert den Platzhalter für "kein Block".- Verzögerte Cache-Einreichung: Der
delay_cache_blocks=True-Zweig vonallocate_slotsüberspringt bei asynchronem P/D-Transfercache_blocks(kv_cache_manager.py:448-451). Erst wenn der entfernte KV tatsächlich eingetroffen ist, wird der Hash markiert – ein zu frühes Cachen würde zu falschen Treffern führen. - Wasserzeichen nur für wartende Anfragen:
has_scheduled_reqs and request.status in (WAITING, PREEMPTED)löst das Hinzufügen des Wasserzeichens aus; für bereits laufende Anfallen gilt die inkrementelle Zuweisung ohne Reserve(kv_cache_manager.py:367-374). - Admissionsprüfung für die gesamte Sequenz: Bei
full_sequence_must_fit=Truewird zuerst geschätzt, wie viele Blocks die gesamte Prompt benötigt; bei Überschreitung wird direktNonezurückgegeben, damit Chunked-Prefill eine Anfrage nicht erst aufnimmt und dann stecken bleibt(kv_cache_manager.py:376-391). reserved_blocksfür asynchrone KV-Connectoren: Bereits in-flight befindliche Prefill-Sequenzen hängen von bestimmten Blocks ab; ein neu eintreffender asynchroner Connector-Load darf diese nicht überbieten. Beirequired_blocks > available_blockswird die Zuweisung abgelehnt(kv_cache_manager.py:420-426).
Schlüsseldateien
KVCacheBlocks dataclass:29-50— unveränderliches Ergebnis auf Scheduler-Seite,blocks[i][j]= i-te Gruppe, j-ter Block.KVCacheBlocks.__add__:52-59— Verkettung zweier Zuweisungsergebnisse, häufig bei inkrementeller Zuweisung.KVCacheBlocks.get_block_ids:73-88— Umwandlung intuple[list[int], ...]für das Attention-Backend.KVCacheManager.__init__:114-183— Coordinator aufsetzen, watermark_blocks berechnen,empty_kv_cache_blockszwischenspeichern, um GC-Overhead zu vermeiden.get_computed_blocks:206-246— ruftcoordinator.find_longest_cache_hitauf, um den längsten Prefix-Treffer zu finden; gibt(KVCacheBlocks, num_new_computed_tokens)zurück.allocate_slots:248-343— dreistufig: übersprungene Blocks freigeben → Prefix-Token verarbeiten → Blocks für neue Token zuweisen; bei MisserfolgNone.full_sequence_must_fit:376-391— Admissionsprüfung: Anzahl der Blocks der gesamten Prompt + Wasserzeichen muss ≤ Anzahl freier Blocks sein.required_blocks gate:410-426—available = free - reserved; beirequired > availablewird die Zuweisung abgelehnt.allocate_new + cache_blocks:428-464— delegiert neue Blocks an den Coordinator und ruftcache_blocksauf, um den Hash einzureichen.free:466-474— ruftcoordinator.free(request_id)auf; gibt in umgekehrter Reihenfolge frei, damit der Tail-Block zuerst verdrängt wird.pop_blocks_for_free:495-506— nimmt die Per-Request-Buchung heraus, gibt sie aber nicht an BlockPool zurück; für P/D-Transfer.reset_prefix_cache:516-530— nach RLHF-Gewichtswechsel gesamten Cache invalidieren.
Datenfluss
In jedem Scheduling-Schritt ruft der Scheduler für jede Anfrage zuerst get_computed_blocks auf, um den Prefix-Treffer zu prüfen, und dann allocate_slots, um die Blocks für die neuen Token zuzuweisen. Nachfolgend die wenigen Zeilen, die allocate_slots nach Wasserzeichen- und Admissionsprüfung tatsächlich ausführt:
if (
new_computed_block_list is not self.empty_kv_cache_blocks.blocks
or num_external_computed_tokens > 0
):
# Append the new computed blocks to the request blocks until now to
# avoid the case where the new blocks cannot be allocated.
self.coordinator.allocate_new_computed_blocks(
request_id=request.request_id,
new_computed_blocks=new_computed_block_list,
num_local_computed_tokens=num_local_computed_tokens,
num_external_computed_tokens=num_external_computed_tokens,
)
new_blocks = self.coordinator.allocate_new_blocks(
request.request_id,
num_tokens_need_slot,
num_tokens_main_model,
num_encoder_tokens,
)
# P/D: delay caching blocks if we have to recv from
# remote. Update state for locally cached blocks.
if not self.enable_caching or delay_cache_blocks:
return self.create_kv_cache_blocks(new_blocks)(kv_cache_manager.py:428-451) Zuerst werden die getroffenen Prefix-Blocks absorbiert (ref_cnt wird erhöht), dann lässt der Coordinator für die neuen Token physische Blocks zuteilen. Sofern das Caching nicht verzögert wird, wird anschließend cache_blocks aufgerufen, um die gerade gefüllten Blocks per Hash in den Prefix-Cache aufzunehmen (siehe Coordinator und BlockPool). Das zurückgegebene KVCacheBlocks wird über get_block_ids in eine block_table umgewandelt und in der Blocktabelle für den Attention-Kernel abgelegt.
Grenzen und Fehler
num_new_tokens == 0und keine externen Token: löstValueErroraus. Der Aufrufer muss sicherstellen, dass entweder neue Token zu berechnen oder bereits berechnete externe Token zu speichern sind(kv_cache_manager.py:346-350).get_computed_blocksfehlt bei vollem Treffer ein Token:max_cache_hit_length = request.num_tokens - 1, da bei vollem Treffer der letzte Token noch einmal berechnet werden muss, um die Logits zu erhalten. Die Trefferlänge ist also aufprompt_length - 1begrenzt(kv_cache_manager.py:225-231).- Anfragen, die den Prefix-Cache überspringen: Bei
request.skip_reading_prefix_cache=Trueoderenable_caching=Falsegibtget_computed_blocksdirekt Leer zurück(kv_cache_manager.py:222-223), z. B. für Anfragen, die Prompt-Logprobs benötigen. reset_prefix_cacheabgelehnt: Wennnum_used_blocks != 1(nur Null-Block), sind noch Blocks nicht freigegeben, der Reset schlägt fehl und liefertFalse(kv_cache_manager.py:516-530).pop_blocks_for_freegibt nichts an den Pool zurück: Der Aufrufer muss selbst in umgekehrter Reihenfolgeblock_pool.free_blocksaufrufen, sonst lecken Blocks(kv_cache_manager.py:495-506).take_eventsannotiert Gruppen-Metadaten: EinBlockStored-Ereignis enthält nur group_idx; der Manager ergänzt überkv_cache_event_metadataden Kind und das Sliding-Window(kv_cache_manager.py:566-589).
Zusammenfassung
KVCacheManager ist die KV-Cache-Fassade, die der Scheduler sieht; die eigentliche Arbeit verrichten KVCacheCoordinator + BlockPool (siehe Coordinator und BlockPool). Die zugeteilten Block-IDs landen schließlich in der Blocktabelle, wo der Attention-Kernel sie zur Adressierung des physischen KV-Cache nutzt. Die Modellgewichte selbst werden in der DefaultModelLoader-Linie geladen und sind mit dieser Schicht nicht gekoppelt.