TL;DR: Lange agentic Kontexte stoßen GLM-5.3 auf H200s an zwei Grenzen: KV-Cache-Speicher und Prefill-Geschwindigkeit. Wir adressieren beides auf 16 H200s, aufgeteilt in einen Prefill-Knoten und einen Decode-Knoten:
- Prefill Context Parallelism (PCP) teilt jeden Prompt nach Token statt nach Köpfen auf die GPUs auf, was weit besser zu GLM-5.3s sparse Attention passt als Tensor Parallelism.
- Decode Context Parallelism (DCP) shardet den KV-Cache der Prefill-Engine nach Token, sodass ein 1M-Kontext auf den Prefill-Knoten passt.
- Hybrid HiSparse erlaubt dem Decode, den KV im Host-Speicher zu halten, sodass eine 1M-Token-Anfrage passt.
- Auf AgentX liefert PCP8 × DCP8-Prefill mit Hybrid-HiSparse-Decode das 1,7–3,2-Fache der Pro-Nutzer-Geschwindigkeit einer datenparallelen Bereitstellung bei gleichem Durchsatz, und es ist die einzige getestete Konfiguration, die 1M-Token-Kontexte bedient.
Bedienung langer Kontexte in speicherbeschränkten Bereitstellungen
Agentic-Workloads haben lange Kontexte, die mit jedem Zug wachsen, was sowohl den GPU-Speicher als auch den Prefill belastet. Auf H200s belegen GLM-5.3s Gewichte den Großteil des Speichers jeder GPU, sodass wenig Raum für den KV-Cache dieser langen Kontexte bleibt. Und da jeder Zug weitere Tool-Ausgaben und Dateiinhalte zum Prefill hinzufügt, können unter Last die Prefill-Engines zum Engpass der gesamten Bereitstellung werden.
Teil 1 zeigte, wie Hybrid HiSparse lange Kontexte in einer aggregierten Bereitstellung auf einem einzelnen 8×-H200-Knoten passen lässt. Prefill/Decode-Disaggregation verlangt mehr als das. Auf dem Prefill-Knoten nutzen wir PCP, um lange Prompts durch Parallelisierung über die Token des Prompts zu beschleunigen. Bei MLA hält damit allerdings jeder PCP-Rang eine vollständige Kopie des KV-Cache, daher kombinieren wir es mit DCP, um diese Replikation zu beseitigen. Auf dem Decode-Knoten ermöglicht Hybrid HiSparse höhere Konkurrenz bei langen Kontexten. vLLMs neuer modularer Kern (Model Runner V2) macht die Kombination all dessen viel einfacher.
Warum nicht TP oder DP?
Zwei gängige Arten, GLM-5.3-Prefill auf einem 8×-H200-Knoten zu betreiben, sind DP8 und TP8:
- DP8 weist jeden Prompt einer einzelnen GPU zu, sodass seine Attention die anderen sieben nicht nutzen kann. Dies begrenzt die Time to First Token (TTFT) für lange Prompts, und der KV-Cache des Prompts muss in den Speicher dieser GPU passen.
- TP8 partitioniert die Attention-Köpfe auf die GPUs. Weil MLAs latenter KV von allen 64 Köpfen geteilt wird, speichert jede GPU den vollständigen Cache und liest ihn, um nur 8 Köpfe zu bedienen. Der Indexer, der jeden früheren Token bewertet, um die 2.048 auszuwählen, denen sparse MLA Aufmerksamkeit schenkt, ist gar nicht partitioniert, sodass seine quadratischen Kosten auf jeder GPU wiederholt werden.
Keines der beiden Schemata verteilt die Attention-Arbeit eines langen Prompts auf die acht GPUs, ohne zugleich seinen gesamten KV auf jeder GPU zu halten, die ihn bedient. Die nächsten beiden Abschnitte behandeln jede Hälfte. PCP teilt die Queries eines Prompts auf die GPUs auf, und DCP teilt seinen KV auf.
PCP: teile die Token, nicht die Köpfe
In unserer 8×-H200-Prefill-Engine teilt PCP jeden Prompt nach Token auf die acht GPUs auf: Jede GPU berechnet die Queries für ein Achtel des Prompts, mit allen 64 Köpfen. Eine naive Aufteilung wäre unausgewogen. Unter kausaler Attention beachtet ein Token jeden Token vor sich, daher würde die GPU am Ende des Prompts weit mehr Arbeit leisten als die am Anfang. PCP stattdessen schneidet den Prompt in 16 Chunks und paart sie: GPU nimmt Chunk vom Anfang und Chunk vom Ende, sodass jede GPU dieselbe Mischung aus billigen und teuren Token erhält (Abbildung 3 zeigt dies als V-förmiges Muster über die acht GPUs). Dies ist das lastausgeglichene Sharding aus Context Parallelism for Scalable Million-Token Inference. Schließlich wird der neue KV jeder GPU all-gathered, sodass jede Query weiterhin dem vollen Kontext Aufmerksamkeit schenken kann.
Abbildung 1 vergleicht beide Aufteilungen. PCP behebt beide Probleme. Jede GPU indexiert nur ihre eigenen Token, sodass der Indexer nicht mehr auf jeder GPU repliziert wird und jede GPU ein Achtel davon ausführt. Und jede aus dem Speicher gelesene KV-Zeile bedient nun alle 64 Köpfe statt 8, was sparse MLA von etwa 26 auf etwa 191 FLOPs pro verschobenem Byte anhebt,1 nahe dem Punkt, an dem der H200 in BF16 compute-bound wird.2
Das Nicht-Replizieren des Indexers macht sich vor allem bei langen Prefills bemerkbar. Die Kosten des Indexers wachsen quadratisch mit der Prompt-Länge, während die von sparse MLA nur linear wachsen, weil jede Anfrage auf eine feste Anzahl von 2,048 Tokens achtet. Zählt man die FLOPs für einen Prefill von Grund auf, überholt der Indexer sparse MLA unter TP8, sobald der Prompt etwa 32K Tokens erreicht; unter PCP8, wo keine GPU ihn repliziert, verschiebt sich der Schnittpunkt hinaus auf etwa 256K.
Zusammen machen diese Eigenschaften PCP hervorragend geeignet für das Prefillen langer Prompts. Aber PCP hat dennoch zwei Kostenpunkte. Jede GPU hält alle Attention-Gewichte, während die MoE-Experten über die acht GPUs mit EP8 verteilt bleiben. Und weil das neue KV bei jeder Schicht all-gathered wird, speichert am Ende trotzdem jede GPU den gesamten KV-Cache. Dieser replizierte Cache und nicht die Rechenleistung begrenzt die Kontextlänge, und genau das adressiert DCP.
DCP: Sharding auch des KV-Caches
Eine 1M-Token-GLM-5.3-Anfrage benötigt 53.4 GiB an FP8-KV,3 aber jede GPU bietet nur etwa 15 GiB Platz. Decode Context Parallelism (DCP) löst das, indem es den Cache selbst aufteilt: Die Tokens werden im Round-Robin-Verfahren auf die acht GPUs verteilt, sodass jede GPU jedes achte Token speichert, und dieselbe Anfrage benötigt dann nur noch 6.7 GiB (=53.4/8) pro GPU. Abbildung 2 vergleicht dies mit DP8 und TP8.
Hinweis: PCP teilt Queries; DCP teilt KV. Die Namen sind historisch bedingt. DCP wurde für Decode gebaut, wo jede Anfrage nur ein einzelnes Query-Token hat, daher ist der KV-Cache das Einzige, das sich aufzuteilen lohnt. PCP wurde für Prefill gebaut, wo die vielen Query-Tokens des Prompts die Arbeit ausmachen. Unser Setup betreibt beides in der Prefill-Engine.
Als Folge dieses Shardings befindet sich der größte Teil des KV, den die Queries einer GPU benötigen, nun auf anderen GPUs. Beim Decode sendet DCP die Queries zum KV: Jede GPU führt Attention über ihr eigenes Shard aus, und die Teilergebnisse werden mit ihrem log-sum-exp zusammengeführt. In dieser Prefill-Engine geschieht das Gegenteil: Bei jeder Schicht all-gathered jede GPU die Shards in eine temporäre Kopie mit vollem Kontext, führt Attention für ihre eigenen Queries aus und gibt die Kopie wieder frei. Nur der persistente Cache wird durch acht geteilt.
Abbildung 3 fasst alles zusammen. Durchlaufen Sie schrittweise eine Schicht von PCP8 × DCP8, um zu sehen, welches KV jede GPU speichert, welche Queries sie berechnet und was über den Interconnect übertragen wird. Wechseln Sie zu DP8, TP8 oder PCP8 zum Vergleich. Beachten Sie, dass die beiden Layouts nicht ausgerichtet sind: GPU speichert jedes achte Token, berechnet aber die Queries der Chunks und , weshalb jede Schicht das All-Gather benötigt.
Dieses All-Gather ist nicht kostenlos: Bei jeder Schicht wird der gesamte zwischengespeicherte Kontext erneut gesendet, sodass seine Kosten mit der Kontextlänge wachsen. Abbildung 4 isoliert dies, indem derselbe 2K-Token-Prefill-Chunk auf einem PCP8-Knoten mit und ohne DCP8 bei gleichem zwischengespeichertem Kontext ausgeführt wird. DCP8 fügt etwa 6% pro Schritt hinzu bis zu 64K Tokens Kontext, steigend auf 15% bei 248K. Das ist der Preis für einen 8× größeren KV-Pool. Dieser Overhead ließe sich reduzieren, indem das All-Gather jeder Schicht mit der Berechnung der vorherigen Schicht überlappt wird – eine Optimierung, an der wir derzeit arbeiten.
Jenseits von rund 250K Tokens kann PCP8 allein den KV-Cache nicht halten; dort ist DCP kein Overhead, sondern das, was lange Prompts überhaupt erst möglich macht. Zusammen bietet PCP8 × DCP8 der Prefill-Engine sowohl Geschwindigkeit als auch Raum.
Das Deployment
Wir betreiben es als Prefill/Decode (P/D)-disaggregiertes Deployment auf zwei 8× H200-Knoten. Beide Engines verwenden einen FP8-KV-Cache und MTP Speculative Decoding mit drei Draft-Tokens (MTP3):
- Prefill: eine PCP8 × DCP8 × EP8-Engine mit 512 GiB CPU-KV-Offload, damit spätere Turns ihr Prefix wiederverwenden können.
- Decode: acht DP-Ranks mit EP8 und Hybrid HiSparse aus Teil 1 (48 GiB Host-Pool + 16 GiB Offload pro Rank), sodass die Decode-Concurrency durch den Host-Speicher statt durch HBM begrenzt ist.
Ein Prefix-bewusster llm-d Router sendet jede Anfrage an die Prefill-Engine, der NIXL-Connector verschiebt ihren KV-Cache zu einem Decode-Rank, und der Decode-Rank streamt die Tokens zurück.
Wir vergleichen PCP8 × DCP8-Prefill mit DP8-Prefill (acht DP-Ranks mit EP8 und je 64 GiB CPU-KV-Offload, insgesamt dieselben 512 GiB) sowie Hybrid HiSparse mit reinem CPU-KV-Offloading auf dem Decode-Knoten, in beiden Fällen mit denselben 512 GiB Host-Speicher (8 × 48 GiB HiSparse-Pool + 8 × 16 GiB Offload für HiSparse). Jede Konfiguration verwendet dieselben 16 GPUs, denselben Host-Speicher und denselben vLLM-Build.
Ergebnisse
Wir haben den SemiAnalysis- AgentX Benchmark ausgeführt: Claude-Code-Traces mit Prefix-Reuse wiedergegeben, ein frischer Server pro Concurrency, Ein-Stunden-Läufe und MTP3 mit einer synthetischen Acceptance Length, fixiert bei 2.99, sodass jede Konfiguration denselben spekulativen Speedup erhält. Der Durchsatz ist logische tok/s pro GPU (einschließlich gecachter): Input- plus Output-Tokens, wobei prefix-gecachter Input gezählt wird, über alle 16 GPUs. Interaktivität ist AgentX' P90 E2E-normalisierte Output-Tokens/s pro Nutzer, die x-Achse von Abbildung 5.
Bis zu c32 ist das Deployment meist prefill-gebunden, daher macht der Prefiller den größeren Unterschied. Mit HiSparse-Decode in beiden Setups liefert PCP8 × DCP8-Prefill die 1.7–3.2-fache Interaktivität von DP8 bei gleichem Durchsatz und senkt die mediane TTFT von 1.5–5.2 s auf 0.8–0.9 s. Der Decoder macht bei dieser Last wenig aus: Bei c8 und c16 liegen die beiden PCP8 × DCP8-Konfigurationen wenige Prozent auseinander, und Offload-only-Decode liegt bei c8 leicht vorn.
Ab c32 wird das Deployment mehr decode-gebunden, und HiSparse-Decode setzt sich ab. Bei c64 verdoppelt sich der Durchsatz fast, auf 8,340 logische tok/s/GPU bei 29.6 tok/s/user, während jede andere Konfiguration auf 1–2 tok/s/user abfällt.
Bei 1M Kontext benötigt die Bereitstellung beides. Ein DP8-Prefill-Rang kann keine einzelne 1M-Token-Anfrage halten, und ein Offload-only-Decode-Rang kann es ebenfalls nicht.
All das zusammensetzbar machen
Ein Großteil der anspruchsvollen Arbeit in vLLM besteht darin, Funktionen wie diese zusammensetzbar zu machen. Jede davon ändert, wie der KV-Cache angelegt, geplant oder verschoben wird, und jede Kombination muss korrekt und schnell bleiben.
Bei P/D muss das KV-Layout zwischen den beiden Engines geändert werden. Die Prefill-Engine shardet das KV jeder Anfrage tokenweise über acht GPUs. Jeder Decode-Rang benötigt das gesamte KV, einschließlich des der MTP-Schicht, in seinem eigenen paged Layout. Der NIXL-Connector übernimmt diese Wiederzusammensetzung zwischen den beiden Knoten, und die Prefix-Wiederverwendung aus dem CPU-Offload muss daneben weiter funktionieren.
Jede hier gezeigte Konfiguration läuft auf Model Runner V2. Es hält modellspezifische Zustände aus dem gemeinsamen Ausführungspfad heraus und betreibt Speculative Decoding ohne CPU-GPU-Synchronisationen. Hybrid HiSparse baut darauf auf.
Ausprobieren
Alles läuft auf vLLM v0.32.0 oder später (das vllm/vllm-openai:v0.32.0 -Image). Die obigen Ergebnisse wurden auf vLLM main bei 9e38b4111agemessen. Das Reproduktions-Gist pinnt 2537629eb2. Die wichtigsten Flags der Prefill-Engine:
--prefill-context-parallel-size 8 \
--decode-context-parallel-size 8 \
--dcp-comm-backend ag_rs \
--enable-expert-parallel \
--attention-config '{"sparse_mla_force_mqa":true}'Ohne sparse_mla_force_mqanehmen Prefill-Batches, deren Prompts innerhalb des Top-k von 2,048 Tokens liegen, den dichten MHA-Pfad. Das Flag hält sie ebenfalls auf dem Sparse-MLA-Kernel (MQA), sodass jede KV-Zeile allen Heads dient.
Danksagungen
Diese Arbeit wurde von Roberto L. Castro (Red Hat), Simon Veitner (Red Hat), Matthew Bonanni (Red Hat), Lucas Wilkinson (Red Hat) und Nicolò Lucchesi (Mistral) durchgeführt. Wir danken Maroon Ayoub (Red Hat) und Robert Shaw (Red Hat) für das Stresstesten des Deployments, das Melden von Fehlern und das Beheben von Bugs.
Anmerkungen
Fußnoten
-
Pro Query liest Sparse MLA 2,048 ausgewählte KV-Zeilen mit jeweils 660 Bytes (656 FP8-KV im
fp8_ds_mla-Format von FlashMLA, plus einen 4-Byte-Index), etwa 1.35 MB. Jeder Head fügt 2,176 Bytes für sein BF16-Query und -Output hinzu und rechnet 2,176 FLOPs pro Zeile (QK über 576 Dimensionen, PV über 512). So erledigen 8 Heads 35.7 MFLOPs über 1.37 MB, etwa 26 FLOPs pro Byte, und 64 Heads erledigen 285 MFLOPs über 1.49 MB, etwa 191. ↩ -
Für den H200 SXM: 989 dense BF16 TFLOP/s (die 1,979 des Datenblatts setzen 2:4-Sparsity voraus) 4.8 TB/s 206 FLOPs pro Byte (NVIDIA H200 Datenblatt). ↩
-
Pro Token: 78 Layer × 656 B MLA-KV, 21 Indexer-Layer × 132 B Indexer-Keys (128 FP8-Werte plus ein 4-Byte-Scale) und 788 B für die MTP-Schicht, insgesamt also 54,728 B. Bei = 1,048,576 Tokens sind das 54,728 MiB 53.4 GiB. ↩