Startseite / Artikel / Warum Round-Robin bei LLM-Flotten versagt und wie llm-d nach Cache und Warteschlange routet

Warum Round-Robin bei LLM-Flotten versagt und wie llm-d nach Cache und Warteschlange routet

Round-Robin-Lastverteilung verschwendet Präfix-Caches und ignoriert die Größe von Anfragen. Wie llm-d Replikas bewertet, Prefill von Decode trennt und wann Sie das überhaupt brauchen.

2438 Wörter

Ein großes Sprachmodell auf einer Maschine zu betreiben, ist ein weitgehend gelöstes Problem. Es für die ganze Welt bereitzustellen, bringt eine andere Klasse von Ärger mit sich. Dieser Artikel zeigt, was kaputtgeht, wenn viele GPU-Server hinter einem gewöhnlichen Load Balancer stehen, und was llm-d dagegen tut.

Die Grundlagen im Schnelldurchlauf

Ein paar Fakten aus den unteren Schichten erklären alles Weitere.

Beginnen wir mit dem Modell, das einfach eine große Datei mit Gewichten ist. GPUs sind das richtige Werkzeug, weil sich die Arithmetik so gut parallelisieren lässt. VRAM gibt es, weil System-RAM etwa 52-mal zu langsam ist, um diese Kerne zu versorgen. Jede Anfrage zerfällt in Prefill, rechengebunden und verantwortlich für die anfängliche Pause (Zeit bis zum ersten Token), und Decode, bandbreitengebunden und verantwortlich für den Stream (Zeit pro Ausgabetoken). Beim Decode kostet jedes Token ein komplettes erneutes Lesen des ganzen Modells.

Auf dieser Hardware sitzt eine Softwareschicht:

  • Der KV-Cache speichert, was Prefill berechnet hat, damit Decode es nie neu berechnen muss. Deshalb tritt die anfängliche Pause nur einmal auf.
  • Er belegt bei einem 8B-Modell etwa 128 KB pro Token. Wie viele Nutzer gleichzeitig Platz finden, begrenzt daher der Speicher für den Cache, nicht die rohe Rechenleistung. Rund 60 GB auf einer H100 fassen etwa 490.000 Token.
  • PagedAttention behandelt diesen Pool so, wie ein Betriebssystem virtuellen Speicher behandelt. Fragmentierung verschwindet, und ein Präfix, das mehrere Anfragen teilen, wird nur einmal gespeichert.
  • Präfix-Caching lässt Blöcke die Anfrage überdauern, die sie erzeugt hat, verschlüsselt über einen Hash ihres Inhalts. In Runde zehn eines Chats fällt etwa so viel Arbeit an wie in Runde zwei.
  • Continuous Batching teilt die Kosten für das Lesen der Gewichte auf alle Wartenden auf. Das KV-Lesen lässt sich nicht teilen, da es jeder Sequenz gehört und bei jedem Schritt wiederkehrt. Der Durchsatz kann um das Zehnfache oder mehr steigen, während die Latenz pro Nutzer mit der Kontextlänge wächst: etwa 4,8 ms pro Token allein und etwa 22 ms, wenn der Cache-Pool voll ist.
  • Tensor-, Pipeline- und Expertenparallelismus teilen ein Modell auf viele GPUs auf, wenn eine einzelne Karte es nicht fassen kann.

All diese Optimierungen haben einen gemeinsamen Haken: Jede davon ist Zustand eines einzelnen Servers.

F: Was läuft in einer Flotte schief?

Ein echter Dienst kann nicht auf einer einzigen Maschine leben. Die ganze Welt zu bedienen heißt viele Server, jeder mit eigenen GPUs und einer vollständigen Kopie des Modells, und davor ein Load Balancer:

[ your message ] ──▶ [ load balancer ] ──▶ [ Server 2 ]
                       round-robin      ──▶ [ Server 5 ]
                                        ──▶ [ Server 9 ]

Das ist das standardmäßigste, langweiligste und am besten verstandene Stück Infrastruktur überhaupt, und für Webdienste funktioniert es seit fünfundzwanzig Jahren tadellos. Beim LLM-Serving versagt es auf zwei verschiedene Arten.

Fehler 1: Der Cache wird verstreut

Runde 1 eines Gesprächs trifft ein, und Round-Robin schickt sie an Server 2. Dieser Server führt Prefill für den Verlauf aus, baut einen KV-Cache auf, streamt die Antwort und behält dank Präfix-Caching die Blöcke.

Dann kommt Runde 2, und Round-Robin leitet sie an Server 5. Diese Maschine hat das Gespräch nie gesehen, also wird der ganze Verlauf erneut von null an durch Prefill geschickt. Der brauchbare Cache auf Server 2 bleibt unberührt, bezahlt und ungenutzt, bis ihn etwas verdrängt.

In einer zufällig verteilten Flotte liegt die Trefferquote des Präfix-Caches nahe null. Die wertvollste Optimierung im ganzen Stack wird auf der Routing-Ebene weggeworfen.

Fehler 2: Jede Anfrage sieht gleich teuer aus

Round-Robin geht davon aus, dass jede Anfrage ungefähr gleich viel kostet, an Zeit wie an Speicher. Diese Annahme stammt aus dem gewöhnlichen Web-Serving, wo sie nah genug an der Wahrheit liegt. Eine Profilseite abrufen, ein Template rendern und eine indizierte Abfrage ausführen ist jeweils in Dutzenden Millisekunden erledigt und belegt dabei ein paar Kilobyte. Wenn alle Anfragen so klein und gleichförmig sind, spielt kaum eine Rolle, welcher Server welche bekommt, und Verbindungen zu zählen ist eine gute Art, die Last zu verteilen.

Ein LLM bricht beide Hälften dieser Annahme gleichzeitig:

  • ein Nutzer tippt eine kurze Frage mit etwa 40 Token
  • ein zweiter Nutzer lädt ein 60-seitiges PDF mit rund 50.000 Token hoch

Das sind drei Größenordnungen Unterschied bei der Rechenleistung. Da der KV-Cache mit den Token wächst, unterscheidet sich der Speicher um etwa denselben Faktor: Eine Anfrage belegt ein rund 1.250-mal größeres Stück GPU-Speicher als die andere, und das viel länger.

Round-Robin gibt jeder von ihnen einen einzigen Slot. Schlimmer noch, Anfragen stellen sich hinter die an, die zuerst kam. Eine Maschine kann unter drei riesigen Prefills begraben sein, während ihre Nachbarin leerläuft, und der Balancer, der blind rotiert oder bestenfalls offene Verbindungen zählt, sieht eine perfekt gleichmäßige Verteilung.

Laut der llm-d-Dokumentation ignorieren generische Load Balancer Prompt-Struktur, Tokenanzahl, Warteschlangentiefe und GPU-Last. In Flottengröße ist das keine kleine Ineffizienz. Sie überdeckt alles, was sonst optimiert wurde.

F: Was würde ein LLM-bewusster Router tun?

Er würde aufhören zu fragen, wer an der Reihe ist, und stattdessen fragen, welche Replika genau zu dieser Anfrage im Moment passt.

Genau dieses Problem nimmt llm-d ins Visier. Es ist ein verteilter Inferenz-Stack, nativ für Kubernetes gebaut, ein CNCF-Sandbox-Projekt, gestartet von Red Hat, Google Cloud, IBM Research, CoreWeave und NVIDIA. vLLM dient als Engine, und llm-d koordiniert viele davon.

Anfragen kommen über ein Inference Gateway herein, bestehend aus einem Proxy und einem Endpoint Picker (EPP), dem Teil, der entscheidet, wohin jede Anfrage geht. Der EPP bewertet jede Kandidaten-Replika anhand von Signalen, die einem normalen Load Balancer fehlen:

  • Präfix-Cache-Überschneidung: welcher Anteil dieses Prompts auf der Replika bereits im Cache liegt
  • Warteschlangentiefe: wie viel Arbeit dort bereits wartet
  • KV-Cache-Auslastung: ob sie fast voll ist und gleich mit dem Verdrängen beginnt
  • Vorhergesagte Latenz: ein optionales XGBoost-Modell in llm-d, das schätzt, wie lange diese Anfrage dort dauern würde

Ein Beispiel mit drei Replikas zeigt die Abwägungen. Replika B ist völlig untätig, hält aber nichts Relevantes, ihre Wahl erzwingt also ein komplettes kaltes Prefill. Replika C deckt 88 % des Präfixes ab, aber 31 Anfragen stehen davor an, man verliert also mehr Zeit mit Warten, als man spart. Replika A deckt 92 % ab und hat nur 2 in der Warteschlange, sie gewinnt. Ein Round-Robin-Balancer kann dieses Urteil nicht fällen, weil es davon abhängt, was jeder Server enthält.

F: Woher weiß der Router, was im Cache liegt?

Es gibt zwei Ansätze, und sie tauschen Genauigkeit gegen Einfachheit.

Näherungsweise. Der Router zerlegt jeden Prompt in Blöcke fester Größe und verkettet darüber rollierende Hashes. In einem LRU-Index im Speicher merkt er sich, welche Präfix-Hashes er an welchen Pod geschickt hat. Dieser Ansatz ist sehr leichtgewichtig und braucht weder Tokenizer noch zusätzlichen Dienst. Der Nachteil: Er errät den Serverzustand aus seinen eigenen Aufzeichnungen, und die Schätzung driftet ab, sobald eine Replika Blöcke verdrängt, ohne den Router zu informieren.

Präzise. Hier bezieht der Router die exakte Tokenisierung vom Render-Endpoint von vLLM, während vLLM bei jedem Erzeugen oder Verdrängen eines Blocks KV-Cache-Events über ZMQ veröffentlicht. Der Router hält dann eine global korrekte Karte der Token-Blöcke und ihrer Standorte. Die Genauigkeit ist exakt, komplizierte Verdrängungsmuster und disaggregierte Deployments werden korrekt behandelt, allerdings sind mehr Komponenten beteiligt.

Der berichtete Gewinn, wenn man das richtig macht, liegt bei etwa 3-fach höherem Ausgabedurchsatz und 2-fach schnellerer Zeit bis zum ersten Token. Er kommt nicht von neuer Hardware, sondern daher, dass Anfragen an die Maschine gehen, die die Arbeit schon erledigt hat.

F: Ist es ein Problem, dass sich Prefill und Decode eine GPU teilen?

Ja, ein ernstes, und es folgt aus den Grundlagen oben. Prefill giert nach Rechenleistung und kommt in Schüben, Decode giert nach Bandbreite und läuft lange. Auf derselben GPU konkurrieren die beiden.

Nutzer spüren den Fehler direkt. Stellen Sie sich fünfzig Menschen mitten in ihren Antworten vor, die alle flüssig Token erhalten. Dann kommt ein Prompt mit fünfzigtausend Token zum Prefill, beansprucht die GPU für Hunderte Millisekunden für sich, und alle fünfzig Streams stocken. Eine schwere Anfrage verschlechtert es für alle, und der Batching-Scheduler kann nicht helfen, weil beide Aufgaben wirklich dasselbe Silizium brauchen.

Dazu kommt ein Missverhältnis in der Ressourcenform. Für Prefill dimensionierte GPUs haben zu viel Bandbreite, für Decode dimensionierte haben zu wenig FLOPs. Ein einzelner Pool kann nicht für beides richtig sein.

Die Lösung: zwei Pools für zwei gegensätzliche Aufgaben

Disaggregation teilt die Flotte in zwei:

  • Der Prefill-Pool ist auf Rechenleistung ausgelegt. Er liest Prompts, baut KV-Caches und reicht sie weiter.
  • Der Decode-Pool ist auf Speicherbandbreite ausgelegt. Er nimmt Caches entgegen und streamt Token in großen Batches, ohne Unterbrechungen durch Prefill.

Die beiden Pools müssen den KV-Cache austauschen, ihn also aus dem Speicher einer GPU in den einer anderen kopieren. NIXL, die NVIDIA Inference Xfer Library, erledigt das und bewegt Cache-Tensoren über RDMA oder InfiniBand, NVLink, NVMe oder TCP mit voller Leitungsgeschwindigkeit. Von dieser Transportschicht hängt die Praxistauglichkeit ab. Eine langsamere Übergabe würde mehr kosten als das Neuberechnen des Caches.

Der Lohn: Die beiden Pools skalieren unabhängig voneinander:

Workload Typische Beispiele Kapazität ergänzen bei
Lange Prompts, kurze Antworten Zusammenfassung, RAG, Code-Review Prefill-GPUs
Kurze Prompts, lange Antworten Chat, Agenten Decode-GPUs

llm-d berichtet allein durch diese Aufteilung von bis zu 70 % mehr Token pro Sekunde, und Disaggregation wird oft als sofortiger Gewinn von ~25 % in typischen Deployments beschrieben.

F: Was gibt es sonst noch auf Flottenebene?

Mit einem inferenzbewussten Router und spezialisierten Pools wird noch einiges mehr möglich.

Gestaffeltes KV-Offloading. VRAM ist das knappste Gut, also können gerade ungenutzte Cache-Blöcke in den CPU-RAM und dann auf eine lokale SSD wandern und zurückkehren, wenn eine passende Anfrage auftaucht. Diese Stufe ist viel langsamer als VRAM, schlägt aber ein wiederholtes Prefill mit großem Abstand. Für cache-lastige Workloads berichtet llm-d von bis zu 13,9-fachem Durchsatz durch hierarchisches Offloading.

SLO-bewusstes Autoscaling. Kubernetes skaliert standardmäßig nach GPU-Auslastung, und die bedeutet hier fast nichts, denn eine GPU kann zu 100 % mit verschwenderischer Neuberechnung beschäftigt sein. llm-d skaliert nach dem, was tatsächlich zugesagt wurde: Zielwerte für die Zeit bis zum ersten Token und die Latenz zwischen Token. Außerdem kann es Prefill- und Decode-Pool getrennt skalieren, was nur dank der Disaggregation möglich ist.

Breiter Expertenparallelismus. Bei Mixture-of-Experts-Modellen werden die Experten auf viele GPUs verteilt, sodass nur die arbeiten, an die ein Token geroutet wird. Das macht Modelle mit 671B Parametern überhaupt erst bereitstellbar, und llm-d nennt für diesen Pfad etwa 50.000 Token pro Sekunde Cluster-Durchsatz.

Flusskontrolle. Mandantenfähiges Serving braucht Fairness, damit ein Batch-Job eines Kunden den interaktiven Traffic eines anderen nicht aushungert. Prioritäten müssen am Gateway greifen, denn einer Anfrage, die bereits eine GPU erreicht hat, ist nicht mehr zu helfen.

Vorsicht bei den Zahlen. Alle diese Werte stammen aus den eigenen v0.7-Benchmarks des Projekts, gefahren auf „well-lit paths“ („gut ausgeleuchteten Pfaden“), also getesteten Referenz-Setups. Betrachten Sie sie als Obergrenzen unter guten, sauber abgestimmten Bedingungen und nicht als Versprechen. Echte Ergebnisse folgen der Form des Traffics. Teilen die Prompts keine Präfixe, bringt cache-bewusstes Routing nichts, daher lohnt es sich, zuerst einen echten Workload zu messen.

F: Braucht man das alles?

Am Anfang fast sicher nicht. Es gibt eine sinnvolle Reihenfolge, und Vorgreifen kostet mehr, als es spart. llm-d selbst beschreibt seine Funktionen als kombinierbar: Sie sollen einzeln eingeführt werden, wenn bestimmte Engpässe auftauchen, und nicht als Paket installiert.

Das gilt allgemein. Jede hier besprochene Schicht wurde geschaffen, um ein gemessenes Problem in der Schicht darunter zu lösen. Wer eine Schicht übernimmt, ohne ihr Problem zu haben, fügt nur Betriebsaufwand hinzu.

Messen Sie zuerst, denn der Engpass sitzt selten dort, wo man ihn vermutet.

Der ganze Stack als Kette von Konsequenzen

  1. Die GPU gibt es, weil ein Forward Pass aus Milliarden unabhängiger Multiplikationen besteht und eine CPU sie seriell abarbeitet.
  2. VRAM gibt es, weil System-RAM etwa 52-mal zu langsam ist, um Tausende Kerne zu versorgen.
  3. VRAM ist zudem klein, was Speicher zur knappen Ressource macht, die alle Schichten darüber begrenzt.
  4. vLLM gibt es, weil ein PyTorch-Skript allein kein Dienst ist.
  5. Den KV-Cache gibt es, weil ein erneutes Prefill für jedes Token vor jedem Wort stocken würde.
  6. PagedAttention gibt es, weil naive KV-Allokation den Großteil des Speichers verschwendet.
  7. Präfix-Caching gibt es, weil ein Chat in jeder Runde den ganzen Verlauf erneut sendet.
  8. Continuous Batching gibt es, weil ein einziges Lesen der Gewichte alle Wartenden bedienen sollte, nicht eine Person.
  9. Parallelismus gibt es, weil das Modell der Karte entwachsen ist.
  10. llm-d gibt es, weil Round-Robin-Balancing Punkt 7 wegwirft und Punkt 8 untergräbt, sobald ein zweiter Server dazukommt.

Was passiert, wenn Sie in einer Chat-App Enter drücken

Die Nachricht wird an den Verlauf angehängt und an ein Inferenz-Gateway geschickt. Ein Router hasht das Präfix, schaut nach, welche Replikas dieses Gespräch bereits halten, vergleicht ihre Warteschlangentiefen und den Cache-Druck und wählt eine aus.

Die gewählte Replika findet den Großteil des Verlaufs aus der letzten Runde im Cache, sodass nur die neueste Nachricht Prefill braucht. Ist die Flotte aufgeteilt, landet diese Arbeit auf einem GPU-Pool, der für Prefill reserviert ist, und der entstandene KV-Cache wandert dann per RDMA zu einem Decode-Pool.

Im Decode-Pool landet die Anfrage in einem Batch mit Dutzenden Fremden. Alle paar Millisekunden wird das ganze Modell genau einmal aus dem VRAM gelesen, und dieses eine Lesen liefert das nächste Token für alle zusammen. Der Cache wächst um 128 KB, der Batch wird neu gebaut, und der Zyklus wiederholt sich.

Ein paar Hundert Mal, und Wörter erscheinen auf dem Bildschirm.

Hinter dieser Antwort steckt eine Lösung, die unverhältnismäßig schwer zu bauen ist.

Wo Sie mehr lesen können

  • das llm-d-Repository mit den Referenzpfaden und den v0.7-Benchmark-Ergebnissen
  • die llm-d-Projektseite mit der Architekturdokumentation
  • die vLLM-Dokumentation mit den Designnotizen zum Präfix-Cache
  • das NIXL-Repository mit der KV-Transferbibliothek