Artikel

Desaggregierte Vorfüllung und Dekodierung

Hochgeschwindigkeits-geometrische Partikel von Daten, die vorbeisausen

Um Ausgabe-Token aus einem Eingabe-Prompt zu generieren, wird die LLM-Inferenz in zwei Stufen unterteilt: Prefill und Decode. Prefill läuft auf den Eingabe-Token und füllt die KV-Caches, bevor sie in die Decode-Phase eintritt, die Token nacheinander generiert.

Während ein einzelner Decode-Schritt typischerweise einige Dutzend Millisekunden dauert, benötigt Prefill erheblich länger. Wenn Prefill und Decode auf denselben Geräten ausgeführt werden, wird die Decode-Leistung beeinträchtigt. In diesem Artikel erkunden wir eine etablierte Lösung in Form von disaggregiertem Prefill und Decode, indem wir sie auf separaten Geräten ausführen, um sowohl die Prefill-Durchsatzleistung als auch die Decode-Latenzzeiten zu maximieren.

Prefill vs Decode Leistung

In einer typischen LLM-Servicemotor wählt der Batch-Planer Anfragen aus, die in jedem Ausführungsschritt eines Modells verarbeitet werden sollen. Wenn man auf einem einzelnen Gerät oder Knoten arbeitet, werden sowohl Prefill- als auch Decode-Anfragen zusammen gebatchet. Die Kosten der Aufmerksamkeit, die sich entlang der Sequenzlänge kumulieren, steigen sowohl für Prefill als auch für Decode, proportional zur Länge der Einträge im KV-Cache (kv_len). Decode-Anfragen übermitteln normalerweise einen einzelnen Token (qo_len=1) mit minimalen Kosten durch andere Schichten, die unabhängig auf die Token einer Sequenz arbeiten. Prefill-Anfragen übermitteln Tausende oder Zehntausende von Token mit erheblichen Kosten durch dichte Schichten (großes qo_len).

Die Latenz eines Vorwärtslaufs wird stärker durch die Anzahl unabhängiger Token beeinflusst, die durch dichte Schichten laufen (qo_len) als durch die Anzahl von Token, die während der Aufmerksamkeit aus dem KV-Cache abgerufen werden (kv_len). Aufmerksamkeit kann sowohl über die Anzahl der Anfragen als auch das kv_len proportional zu den Sequenzlängen parallelisiert werden, um eine gute Auslastung zu erreichen. Prefill ist rechenintensiv: Da das qo_len hoch ist, können GEMM-Kerne ausreichend Blöcke entlang der M-Dimension zuweisen, um die Rechenkapazitäten moderner GPUs voll zu nutzen. Decode ist speichergebunden: Aufgrund typischerweise kleiner Batch-Größen ist die Anzahl der Eingaben entlang M normalerweise klein, ausreichend für nur einen Block. Während Split-K-GEMM-Kerne den SM-Auslastungsgrad für kleine Token-Batch-Größen verbessern können, bleiben die Caches und die Matrix-Multiplikationseinheiten typischerweise unterausgelastet.

Wenn sie zusammen gemischt werden, verursachen Batch-Anfragen, die Prefill enthalten, höhere Latenzen beim Vorwärtslauf, was sich negativ auf den Decode-Durchsatz der gesamten Instanz auswirkt. Während das Mischen von Prefill-Anfragen mit Decode-Anfragen oder der Einsatz von Chunked-Prefill die Decode-Leistung leicht verbessern kann, ist es schwierig, ausreichend Prefill-Durchsatz aufrechtzuerhalten, um genügend Anfragen auf einer Instanz zu verarbeiten, um den Decode-Durchsatz zu maximieren. Im Falle großer Modelle mit typischen Ausgabelängen muss Prefill häufig genug durchgeführt werden, sodass es die durchschnittliche Latenz erheblich beeinträchtigt und zu Stottern in der Ausgabe führt.

Diese Probleme können durch die Nutzung eines separaten Satzes von Knoten zur Durchführung von Prefill und Decode angegangen werden. Durch die Zuordnung eines Prefill-Knotens mit mehreren Decode-Knoten können ausreichend Anfragen für Prefill geplant werden, um den Durchsatz zu maximieren und eine ausreichend große Anzahl gleichzeitiger Anfragen auf den Decoder-Knoten zu erhalten, um auch den Decode-Durchsatz zu maximieren. Die Prefill-Knoten füllen die KV-Caches, die dann an die Decoder-Knoten übertragen werden. Da die Decoder nicht mehr für Prefill unterbrechen müssen, werden die Latenzen viel deterministischer, da die Gesamtwirkung des wachsenden kv_len der aktiven Anfragen viel weniger ausgeprägt ist. Die Kosten werden in einer Erhöhung der Zeit bis zum ersten Token (TTFT) bezahlt, da die Übertragung der KV-Caches über das Netzwerk einige zehn bis hundert Millisekunden dauern kann.

KV Messenger

Bei Perplexity basiert unsere Implementierung für disaggregiertes Prefill und Decode auf einem KV-Messenger, der mit dem LLM-Motor interagiert, um den Transfer von KV-Caches von Prefill-Knoten zu den Decoder-Knoten über ein Netzwerk zu orchestrieren. Auf der Prefill-Seite akzeptiert der Messenger Anfragen von Decoder-Knoten, übergibt sie an den Batch-Planer und verfolgt die Ausführung des Vorwärtslaufs, um KV-Caches mit möglichst geringer Latenz zu versenden. Auf der Decoder-Seite blockiert der Messenger nach der Zuweisung von unverzichtbaren Seiten die Planung der Anfrage für Decode, bis er über die Fertigstellung der KV-Cache- und Decoder-Kontextübertragungen benachrichtigt wird.

Das Disaggregieren von Prefill erfordert Hochdurchsatz- und Niedriglatenzverbindungen, daher ist unsere Implementierung für RDMA maßgeschneidert und unterstützt sowohl EFA- als auch ConnectX-Netzwerkschnittstellenkarten (NICs). Der KV-Messenger basiert auf libfabric und verwendet unsere fabric-lib-Wrapper, um höherwertige Niedriglatenz-Abstraktionen über die Remote-Direct-Memory-Access (RDMA)-Primitive bereitzustellen. Er implementiert effiziente Seiten- und Metadatenübertragungen sowie Niedriglatenz-Signalisierung. Im Hintergrund koordiniert fabric-lib eine GPU und ihre direkt verbundenen NICs, um Daten vom Prefill-Knoten zum Decoder-Knoten zu kopieren.

Nach dem Empfang weist der Prefill-Knoten ein entsprechendes Set von Quell-KV-Seiten zu und plant die Anfrage für Prefill mit seinem lokalen Motor. Um die Latenz zu minimieren, warten die Übertragungen nicht auf den Vorwärtslauf: Stattdessen werden KV-Seitenkopien unmittelbar nach dem Modell initiiert, das KV-Cache-Einträge für einzelne Schichten zum KV-Cache hinzufügt. Da Prefill-Anfragen in Blöcke aufgeteilt werden können, benachrichtigt der Batch-Planer den KV-Messenger über die derzeit geplanten Blöcke vor der Ausführung. Um CUDA-Grafiken zu unterstützen während der Fähigkeit, Schichten zu verfolgen, hält der Messenger einen dedizierten Thread, der einen Zähler überwacht, der nach der Ausgabprojektion der Aufmerksamkeit inkrementiert wird. Der Zähler wird nur auf dem Hauptknoten in einer geshardeten Umgebung geführt: Obwohl die KV-Cache-Einträge nach dem Anhängen und vor der Aufmerksamkeit gültig sind, wird die Ausgabprojektion über die Ränge reduziert und synchronisiert implizit diese. Sobald eine Änderung des Zählers beobachtet wird, wird der Messenger benachrichtigt und ruft fabric-lib auf, um die Übertragung einer Schicht zu initiieren.

Nach der Übertragung des letzten Blocks werden alle zusätzlichen Metadaten ebenfalls kopiert: Spekulatives Decoding oder MTP erfordern, dass Logits und versteckte Zustände zum Decoder verschoben werden. Diese Kopien werden ebenfalls über RDMA durchgeführt, zu und von vorab zugewiesenen Puffern.

Nach Abschluss aller ausstehenden Übertragungen des letzten Blocks deallokiert der Prefill-Knoten die KV-Seiten und schließt die Anfrage ab. Der Decoder-Knoten wird nicht explizit benachrichtigt: Stattdessen verwendet er direkte Zähler, um die Anzahl der abgeschlossenen Operationen zu verfolgen. Die Anzahl der RDMA-Operationen auf der Prefill-Seite ist proportional zur Anzahl der übertragenen Seiten. Nach Abschluss der bekannten Anzahl von Seiten- und Kontextkopien ruft fabric-lib den KV-Messenger auf, um anzuzeigen, dass eine Anfrage für das Decoding bereit ist. Der Messenger deallokiert jeden Kontext und übergibt die Anfrage an den LLM-Motor.

Gesplitte KV-Cache-Übertragungen

Wenn der Prefill und der Decoder auf Tensor-Parallelismus (TP) angewiesen sind und die KV-Caches identisch sharden oder replizieren, koordiniert ein einzelner Transfermotor mehrere Geräte, um die Seiten aller Replikate zu senden und zu empfangen. Um einen einzigen Messenger und Transfermotor verwenden zu können, trotz der Tatsache, dass der Executor des Modells über mehrere Geräte und Prozesse repliziert ist, werden cuMem und cuMemImportFromShareableHandle verwendet, um den Gerätespeicher zuzuweisen, der die KV-Caches unterstützt, und um ihn in den Hauptprozess zu mappen. Der Transfermotor inspiziert die Topologie des Knotens, um die NICs und die CPUs im nächsten NUMA-Knoten zu finden, die für die Übertragung jedes der KV-Cache-Slices verwendet werden sollen.

Wenn die Quelle und das Ziel identisch geshardet sind, sind Übertragungen trivial, da es eine Eins-zu-eins-Zuordnung von den Geräten und Seiten der Quelle und des Ziels gibt. In dieser Situation hilft das Sharding implizit bei den Übertragungslatenzen: Durch den Einsatz von mehr GPUs können mehr zugehörige NICs eingesetzt werden, um sich der vollen Bandbreitenauslastung zu nähern. Wenn jedoch ein Missverhältnis besteht, muss der Transfermotor die Seiten je nach dem Verhältnis zwischen den Source- und Ziel-Slices teilen oder rekonstruieren.

Wenn der Prefill den KV-Cache auf mehr Geräte aufteilt, werden vollständige Seiten auf dem Decoder rekonstruiert, indem die entsprechenden Hälften aus den Prefill-Geräten gesendet werden. Wenn der Decoder mehr Shards hat, erhält er Seiten von mehreren Quellen. Der Decoder muss das Sharding-Schema des Prefill-Knotens kennen, um die Anzahl der RDMA-Schreibvorgänge berechnen zu können, die er zu erhalten erwartet. Wenn eine Replikation involviert ist, gruppiert der Prefill-Knoten die Geräte in Replikat-Sets, die den vollständigen KV-Cache innerhalb von sich selbst replizieren. Den Zielreplikat-Sets wird zufällig eines der Quellsets zugewiesen, um alle verfügbaren Geräte zu nutzen, um RDMA-Schreibvorgänge zu initiierten.

Gesplitte Übertragungen erfordern eine leichte Anpassung der KV-Caches. Standardmäßig basiert FlashInfer auf dem NHD-Layout, das die Token innerhalb einer Seite innerhalb der Köpfe anordnet. Da Caches am ehesten entlang der Anzahl der Aufmerksamkeit-Köpfe geshardet werden, erstellt dies eine Diskontinuität innerhalb des Kopfes. RDMA-Übertragungen unterstützen nicht implizit gestaffelte Schreibvorgänge, wobei eine Operation pro Kopf erforderlich ist, um die Übertragung durchzuführen. Stattdessen organisieren wir zur Reduzierung der Anzahl der Interaktionen mit libfabric die KV-Caches im HND-Layout, das die Kopfdimension vor der Anzahl der Token platziert. Dies gewährleistet Kontinuität, sodass eine Seite mit einem einzigen Schreibvorgang kopiert werden kann.

Spekulatives Decoding

Spekulatives Decoding erfordert leichte Anpassungen an disaggregierten Prefill-Decode. In unserer Implementierung dürfen Prefiller-Knoten keine Token abtasten. Da die Sonar-Modelle von Perplexity strukturierten Output unterstützen, möchten wir die Komplexität der Synchronisierung der Schema-Prozessor-Implementierungen über Prefiller und Decoder vermeiden. In den MTP- und spekulativen Decoding-Mechanismen umfasst das Prefill des Entwurfmodells bis zu dem letzten Token das Abtasten von Token aus dem Zielmodell.

Um diese Probleme zu umgehen, umfasst Prefill nicht das letzte Token der Eingabefolge. Stattdessen werden versteckte Zustände oder Logits von Prefill vor dem letzten Token übertragen und als Decode-Token im nächsten Schritt auf dem Decoder behandelt. Während dies die Latenzen leicht erhöht, da ein vollständiger Decode-Schritt nach Prefill durchgeführt werden muss, um das erste Token auszugeben, wird die Komplexität der Implementierung erheblich reduziert.

Disaggregierte Bereitstellungen

Wir haben mehrere disaggregierte Konfigurationen mit verschiedenen Modellen bereitgestellt oder damit experimentiert, um entweder Produktionsverkehr oder In-House-Bewertungsaufgaben zu unterstützen. Basierend auf der Größe und dem Aufmerksamkeit Mechanismus von Modellen, wählen wir geeignete Sharding-Schemata für Prefill- und Decoder-Knoten, um GPUs optimal zu nutzen.

DeepSeek-R1

Mit DeepSeek haben wir sowohl Tensor-Parallel- (TP) als auch Daten-Parallel- (DP) Bereitstellungen in Betracht gezogen. Wie in früheren Blogbeiträgen diskutiert, bieten TP-Bereitstellungen bessere Latenz bei den Kosten von niedrigerem Durchsatz, erfordern mehr GPUs, um schweren Verkehr zu bedienen. DP-Bereitstellungen skalieren viel besser mit der Last, jedoch ist ihre maximale Leistung niedriger aufgrund der Kosten von Inter-Geräte- oder Inter-Knoten-Kommunikation.

DeepSeek basiert auf Multi-Head-Latenten-Aufmerksamkeit, die KV-Caches komprimiert. Da alle KV-Köpfe in einen einzelnen Latenten-Vektor komprimiert werden, kann TP die KV-Caches nicht sharden, da es stattdessen die latenten Vektoren auf allen Rängen replizieren muss. Sharding erfolgt nach der Dekompression, da jeder Rang aus derselben latenten Darstellung unterschiedliche Köpfe extrahieren kann. Folglich sind alle KV-Cache-Shards bei sowohl Prefill- als auch Decoder-Shards identisch.

Mit einem Intra-Knoten-TP-Setup sind sowohl Prefiller als auch Decoder identisch geshardet. Übertragungen werden von allen Rängen versendet, um alle verfügbaren NICs vollständig auszulasten. Jedoch mit einem DP-Bereitstellung, bei der die TP-Ranggröße niedriger ist oder jeder DP-Rang einem einzelnen GPU zugewiesen ist, kann jedes Prefill-Gerät, das eine replizierte Kopie des KV-Cache hält, sie versenden. Um Anfragen über alle verfügbaren NICs auszugleichen, wählen wir zufällig eine GPU und eine NIC, um den KV-Cache vom Prefiller zum Decoder zu senden.

Bei gemischtem Prefill-Decode hatte unsere R1-Bereitstellung Schwierigkeiten, konsistent mehr als 50 TPS zu überschreiten, aufgrund häufiger Prefill-Unterbrechungen in der Größenordnung von mehreren hundert Millisekunden. Im Gegensatz dazu, durch Trennung von Prefill, hatten wir eine Strafe von etwa 100 ms zu TTFT für jede Anfrage, aber ein einzelner Prefill-Knoten konnte konsistente Batch-Größen auf 3 Decoder-Knoten aufrechterhalten und eine Leistung von über 90 TPS erbringen, während eine Last von etwa 1 QPS pro Decoder-Knoten gehandhabt wurde. Bei Daten-parallelen Bereitstellungen war TPS leicht niedriger bei etwa 50, jedoch konnten die Instanzen eine Last von 1 QPS pro Rang mit 8 Rängen zu einem einzigen Knoten verarbeiten.

Qwen3-Coder

Dieses 480B-Modell verwendet gruppierte Abfrage-Aufmerksamkeit (Grouped-Query Attention, GQA), sodass Aufmerksamkeit leicht geshardet werden kann und von Tensor-Parallelismus profitierst, ohne Speicher für KV-Caches zu opfern. Folglich konnten wir das Modell auf 8 GPUs für sowohl Prefill als auch Decode sharden, indem wir etwa 3 Decoder-Knoten mit einem einzelnen Prefill-Knoten paarten. Da Aufmerksamkeit geshardet ist, verlassen wir uns auf das HND-KV-Cache-Layout, um Prefill- und Decoder-KV-Caches zu sharden und Prefill-Ränge mit Decoder-Rängen zu paaren, sodass alle NICs voll ausgelastet werden, um die Slices parallel zu übertragen.

Interessiert daran, die Zukunft unserer API-Plattform mitzugestalten? Wir stellen ein.

Treten Sie unserer Entwickler-Community bei, um über neue Releases, Funktionen und Updates stets auf dem Laufenden zu bleiben.

Interessiert daran, die Zukunft unserer API-Plattform mitzugestalten? Wir stellen ein.

Treten Sie unserer Entwickler-Community bei, um über neue Releases, Funktionen und Updates stets auf dem Laufenden zu bleiben.

Interessiert daran, die Zukunft unserer API-Plattform mitzugestalten? Wir stellen ein.

Treten Sie unserer Entwickler-Community bei, um über neue Releases, Funktionen und Updates stets auf dem Laufenden zu bleiben.