Prefill vs. Decode: Warum Token eins langsamer ist
Prefill ist rechengebunden und bestimmt die Zeit bis zum ersten Token. Decode ist speicherbandbreitengebunden und bestimmt Token pro Sekunde. Messen Sie beide Werte getrennt.
Prefill vs. Decode in einem Absatz
Prefill vs. Decode ist die zentrale Unterscheidung, die die meisten Latenzfragen zu einem selbst gehosteten LLM (Large Language Model) erklärt. Prefill liest den gesamten Prompt in einem Durchlauf und ist durch die Rechenleistung begrenzt. Decode schreibt die Antwort Token für Token und ist durch die Speicherbandbreite begrenzt. Die Zeit bis zum ersten Token ist ein Prefill-Wert. Die Token pro Sekunde sind ein Decode-Wert.
Beide Phasen laufen auf derselben GPU (Graphics Processing Unit), mit denselben Gewichten und innerhalb desselben Prozesses. Daher liegt es nahe, sie als eine einzige Workload zu betrachten. Tatsächlich verhalten sie sich wie zwei verschiedene Programme, die sich ein Gerät teilen. Wenn Sie sie getrennt betrachten, sind viele zunächst verwirrende Ergebnisse nicht mehr verwirrend.
Warum ist Prefill rechengebunden?
Beim Prefill wird der gesamte Prompt einmal durch jede Schicht geleitet. Ein Prompt mit 2,000 Tokens liefert für jede Matrixmultiplikation 2,000 Arbeitszeilen. Dadurch führt die GPU für jedes geladene Byte der Gewichte viele arithmetische Operationen aus. Dieses Verhältnis aus Rechenoperationen und bewegten Bytes wird arithmetische Intensität genannt. Beim Prefill ist sie hoch. Das Gerät arbeitet nahe an seiner Rechengrenze, während der Speicherbus noch Reserven hat.
Prefill erzeugt zwei Dinge: den KV-Cache (die Key- und Value-Tensoren) für jedes Prompt-Token und das erste Ausgabetoken. Die Ausgabe erreicht den Leser erst nach Abschluss dieses Durchlaufs. Deshalb liegen die Prefill-Zeit und die Zeit bis zum ersten Token (TTFT) nahezu gleich.
Die Kosten des Prefill steigen mit der Prompt-Länge. Der lineare Anteil entfällt auf die Matrixoperationen pro Schicht. Der quadratische Anteil entfällt auf die Attention. Dabei berücksichtigt jedes Token alle vorherigen Tokens. Bei langen Kontexten wird dieser Anteil relevant. Eine Verdopplung des Prompts verdoppelt die TTFT daher mindestens.
Sie können das in einer Minute prüfen. Senden Sie Ihrem Server zuerst einen Prompt mit 200 Tokens und anschließend einen Prompt mit 2,000 Tokens. Fordern Sie jeweils dieselbe Anzahl an Ausgabetokens an. Die TTFT steigt deutlich. Die Streaming-Geschwindigkeit nach dem ersten Token verändert sich dagegen kaum.
Warum ist Decode durch die Speicherbandbreite begrenzt?
Decode erzeugt pro Schritt ein Token. Für dieses einzelne Token muss die GPU jedes Gewicht des Modells aus dem Speicher lesen, jedes Gewicht für einige Operationen verwenden und anschließend verwerfen. Die arithmetische Intensität liegt nahe bei 1. Daher warten die Recheneinheiten die meiste Zeit.
Decode ist langsam, weil für jedes Token das gesamte Modell aus dem Speicher gelesen werden muss. Dadurch gibt der Speicherbus das Tempo vor, während die Recheneinheiten warten.
Damit lässt sich die Obergrenze für die Decode-Geschwindigkeit eines einzelnen Streams rechnerisch bestimmen. Teilen Sie die Speicherbandbreite durch den Speicherbedarf der Gewichte.
The data behind this chart
[
{
"device": "CPU, dual channel DDR5-5600",
"mem_bandwidth_gb_s": 90,
"decode_ceiling_tok_s": 6
},
{
"device": "NVIDIA A10G",
"mem_bandwidth_gb_s": 600,
"decode_ceiling_tok_s": 38
},
{
"device": "NVIDIA L40S",
"mem_bandwidth_gb_s": 864,
"decode_ceiling_tok_s": 54
},
{
"device": "NVIDIA RTX 4090",
"mem_bandwidth_gb_s": 1008,
"decode_ceiling_tok_s": 63
},
{
"device": "NVIDIA A100 80GB SXM",
"mem_bandwidth_gb_s": 2039,
"decode_ceiling_tok_s": 127
},
{
"device": "NVIDIA H100 SXM",
"mem_bandwidth_gb_s": 3350,
"decode_ceiling_tok_s": 209
}
]Die Spalte „Bandbreite“ enthält den vom jeweiligen Hersteller veröffentlichten Spezifikationswert. Die Spalte „Obergrenze“ ergibt sich aus diesem Wert geteilt durch 16 GB, dem Speicherbedarf eines Modells mit 8 Milliarden Parametern bei einer Präzision von 16 Bit. Es handelt sich um eine Berechnung, nicht um ein Benchmark-Ergebnis. Ihre gemessene Rate wird darunter liegen. Wie groß der Abstand ist, ist hilfreich zu wissen, weil sich daraus ableiten lässt, ob Sie Ihren Serving-Stack oder Ihre Hardware anpassen müssen.
Lesen Sie die 6 Zeilen der Reihe nach. Das Muster ist eindeutig. Eine CPU mit zweikanaligem DDR5 erreicht etwa 90 GB/s. Das begrenzt Decode bei diesem Modell auf ungefähr 6 Token pro Sekunde. Eine L40S erreicht ungefähr 54. Eine H100 SXM mit einer veröffentlichten Bandbreite von 3350 GB/s liegt bei ungefähr 209.
Das erklärt auch, warum Quantisierung der wichtigste einzelne Hebel für die Decode-Geschwindigkeit ist. Wenn Sie dasselbe Modell statt mit 16 Bit mit 8 Bit speichern, halbiert sich die pro Token gelesene Datenmenge. Dadurch verdoppelt sich die Obergrenze ungefähr. Sie haben keine zusätzliche Rechenleistung hinzugefügt. Sie bewegen weniger Daten im Speicher.
Wie messe ich jede Phase auf meinem eigenen Server?
Ollama liefert die Aufteilung im Response-Body zurück. Fordern Sie eine nicht gestreamte Completion an und lesen Sie die Zähler aus.
curl -s http://localhost:11434/api/generate -d '{
"model": "llama3.2",
"prompt": "Explain memory bandwidth in two sentences.",
"stream": false
}' | jq '{prompt_eval_count, prompt_eval_duration, eval_count, eval_duration}'Verwenden Sie einen Model-Tag, den Sie tatsächlich heruntergeladen haben. ollama list zeigt Ihnen die verfügbaren Tags. prompt_eval_count und prompt_eval_duration beziehen sich auf das Prefill: die Anzahl der Prompt-Tokens und die dafür benötigte Zeit. eval_count und eval_duration beziehen sich auf das Decoding. Die Dauer wird in Nanosekunden angegeben. Daher berechnen Sie die Decoding-Geschwindigkeit mit eval_count / eval_duration * 1e9 und die Prefill-Geschwindigkeit mit prompt_eval_count / prompt_eval_duration * 1e9. Die Prefill-Rate ist bei derselben Anfrage erwartungsgemäß deutlich höher als die Decoding-Rate. Dieser Unterschied erklärt alle weiteren Punkte in diesem Abschnitt.
Bei einem OpenAI-kompatiblen Server wie vLLM kann curl die Zeit bis zum ersten Byte für Sie messen.
curl -N -s -o /dev/null \
-w 'pretransfer %{time_pretransfer}s first_byte %{time_starttransfer}s\n' \
http://localhost:8000/v1/completions \
-H 'Content-Type: application/json' \
-d '{"model": "meta-llama/Llama-3.1-8B-Instruct", "prompt": "Explain memory bandwidth.", "max_tokens": 128, "stream": true}'time_starttransfer bezeichnet den Zeitpunkt, zu dem das erste Byte des Response-Bodys eingetroffen ist. Zusammen mit "stream": true umfasst dieser Wert die TTFT und den Verbindungsaufbau. Ziehen Sie time_pretransfer ab, um die Kosten für den Verbindungsaufbau zu entfernen. Führen Sie den Test zweimal aus und verwenden Sie das zweite Ergebnis, weil der erste Aufruf das erstmalige Laden des Modells umfassen kann.
vLLM veröffentlicht diese Aufteilung außerdem als Prometheus-Metriken unter /metrics. Mit curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency' erhalten Sie die Histogramme vllm:time_to_first_token_seconds und vllm:inter_token_latency_seconds. Ergänzen Sie vllm:num_requests_running und vllm:num_requests_waiting für die Warteschlangentiefe sowie vllm:kv_cache_usage_perc für die Cache-Auslastung. Diese fünf Namen bilden das gesamte Dashboard.
Unter Last treibt vllm bench serve --model <name> --num-prompts 200 --request-rate 4 den laufenden Server an und meldet die Zeit bis zum ersten Token sowie die Latenz pro ausgegebenem Token mit Perzentilen. Nur damit lässt sich erkennen, wie sich die beiden Phasen gegenseitig beeinflussen. Erstellen Sie vor jeder Optimierung eine saubere Baseline. Die Methode unter Tokens pro Sekunde bei einem lokalen LLM messen liefert einen Messwert, der auch nach einem Reboot vergleichbar bleibt.
Warum verzögert ein langer System-Prompt das erste Token, aber nicht die Streaming-Geschwindigkeit?
Weil der System-Prompt ausschließlich Prefill-Arbeit verursacht. Er wird einmal verarbeitet, im selben Durchlauf wie der restliche Prompt, bevor das erste Token erscheint. Danach liegt er nur noch als KV-Cache-Einträge vor, und der Decode-Schritt liest diese zusammen mit allen anderen Einträgen. Ein System-Prompt mit 3,000 Tokens erhöht daher bei jeder einzelnen Anfrage die TTFT, während die Tokens pro Sekunde nahezu unverändert bleiben.
Nahezu, aber nicht exakt. Diese zusätzlichen KV-Cache-Einträge werden bei jedem Decode-Schritt erneut gelesen. Ein sehr langer Prompt verlangsamt den Decode-Schritt daher geringfügig. Im nächsten Abschnitt wird das behandelt.
Die Lösung besteht darin, dasselbe Präfix nicht erneut zu berechnen. Ein Server mit Prefix-Caching behält den KV-Cache eines gemeinsamen Präfixes und verwendet ihn wieder. Bei der zweiten Anfrage mit demselben System-Prompt entfällt dadurch dieser Teil des Prefill-Schritts vollständig. vLLM bezeichnet dies als Automatic Prefix Caching. Prüfen Sie vllm serve --help für Ihre Version, da sich der Standardwert zwischen Releases geändert hat. Dieser KV-Cache im GPU-Speicher ist etwas anderes als der Prompt-Cache, den ein API-Anbieter abrechnet. Der Unterschied zwischen einem KV-Cache und einem Prompt-Cache ist lesenswert, bevor Sie einen der beiden optimieren.
Warum wird das Decoding langsamer, wenn der Kontext wächst?
Dafür gibt es zwei Gründe. Beide betreffen den KV-Cache.
Der erste Grund ist die Speicherbandbreite. Bei jedem Decoding-Schritt liest die Attention die Keys und Values aller vorherigen Tokens. Die Gewichte verursachen pro Token einen festen Aufwand. Der KV-Cache wächst dagegen mit jedem Token. Seine Größe lässt sich aus dem config.json des Modells berechnen: Bytes pro Token entsprechen 2 multipliziert mit num_hidden_layers, mit num_key_value_heads, mit der Head-Dimension (hidden_size geteilt durch num_attention_heads) und mit der Bytegröße eines Elements. Die führende 2 steht für jeweils einen Key und einen Value.
Bei einem verbreiteten Layout mit 8 Milliarden Parametern, 32 Layern, 8 Key- und Value-Heads unter GQA (Grouped Query Attention), einer Head-Dimension von 128 und einer Präzision von 16 Bit ergibt sich: 2 x 32 x 8 x 128 x 2 = 131,072 Bytes, also etwa 128 KiB pro Token. Eine Unterhaltung mit 8,000 Tokens belegt daher pro Anfrage ungefähr 1 GB KV-Cache.
Der zweite Grund ist die Kapazität. Dieser 1 GB große Bereich kann weder Gewichte noch den Kontext eines anderen Benutzers aufnehmen. Der Server legt die Größe seines KV-Pools einmal beim Start fest, bei vLLM über --gpu-memory-utilization. Wenn der Pool voll ist, warten neue Anfragen. Ein steigender Wert von vllm:num_requests_waiting, während vllm:kv_cache_usage_perc nahe bei 1 bleibt, ist das eindeutige Kennzeichen dieses Zustands. Manche Stacks unterbrechen eine laufende Anfrage und berechnen ihren Cache später erneut, anstatt sie in die Warteschlange einzureihen. Für den Benutzer wirkt das wie ein Stillstand mitten im Stream.
Ein langer Kontext verursacht Kosten an zwei Stellen: zunächst durch mehr Prefill-Arbeit und anschließend durch mehr gelesenen Speicher pro Token für den Rest der Antwort.
Warum verbessert Batching den Durchsatz und verschlechtert die Tail-Latenz?
Da Decode bandbreitengebunden ist, verursachen zusätzliche Anfragen auf der Rechenseite kaum Mehrkosten. Ein einmaliges Lesen der Gewichte kann für jede Sequenz im Batch ein Token erzeugen. Dadurch steigt der Gesamtdurchsatz mit der Batchgröße nahezu linear, bis entweder der KV-Pool erschöpft ist oder der Batch so groß wird, dass die Verarbeitung wieder rechengebunden ist. Continuous Batching erstellt den Batch nach jedem Schritt neu. Eine abgeschlossene Anfrage verlässt den Batch, und eine wartende Anfrage kommt hinzu, ohne auf die anderen Sequenzen zu warten.
Die Kosten zeigen sich in den Perzentilen. Das nächste Token jedes Benutzers wartet nun auf den langsamsten Teil eines gemeinsam genutzten Schritts. Dadurch bleibt p50, also der Median, akzeptabel, während sich p99, also die langsamste Anfrage von 100, verlängert. p99 fällt Benutzern auf, weil dieser Wert die Pause mitten in einem Satz bestimmt.
Prefill verschärft das Problem. Wenn während des laufenden Streams ein großer Prompt eintrifft, belegt er das Gerät für einen langen Schritt. Alle aktuell laufenden Sequenzen sehen dadurch eine Lücke. Chunked Prefill reduziert diesen Effekt erheblich, indem ein langer Prompt in Teilstücke zerlegt und jedes Teilstück in die Decode-Batches eingemischt wird. Seit August 2026 führt die vLLM V1 Engine dies standardmäßig durch und stellt die Balance über --max-num-batched-tokens ein. Die vLLM-Tuning-Dokumentation beschreibt den Zielkonflikt eindeutig: Kleinere Werte um 2048 verbessern die Inter-Token-Latenz (ITL), weil weniger Prefills die Decodes unterbrechen. Größere Werte verbessern TTFT, weil mehr Prefill-Tokens in einen Batch passen. Dieses einzelne Flag stellt das Verhältnis zwischen Prefill und Decode als veränderbaren Zahlenwert bereit. Ab welchem Punkt p99 nicht mehr akzeptabel ist, hängt von der Kapazität ab. Wie viele parallele Benutzer ein selbst gehostetes LLM bedienen kann wird anhand derselben Metriken ermittelt.
Warum ändert eine größere GPU manchmal nichts?
Weil „größer“ in der Regel mehr Rechenleistung bedeutet. Für Decode ist jedoch nicht die Rechenleistung der begrenzende Faktor.
Vergleichen Sie zwei Zeilen der obigen Tabelle. Die A100 80GB erreicht eine angegebene Speicherbandbreite von 2039 GB/s. Die L40S erreicht 864 GB/s. Die Decode-Obergrenze folgt genau diesem Verhältnis: 127 Tokens pro Sekunde gegenüber 54. Die RTX 4090 ist nach den meisten Maßstäben eine sehr schnelle Karte. Mit 1008 GB/s liegt ihre Obergrenze bei 63. Unabhängig von weiteren Unterschieden zwischen zwei Karten folgt Single-Stream-Decode der Speicherbandbreite im Datenblatt.
Es gibt daher zwei Möglichkeiten, Decode zu beschleunigen: Lesen Sie pro Token weniger Bytes, indem Sie die Gewichte quantisieren oder ein kleineres Modell verwenden, oder kaufen Sie mehr Speicherbandbreite. Prefill ist der umgekehrte Fall. Prefill benötigt Rechenleistung. Eine schnellere Karte verkürzt daher die TTFT bei langen Prompts tatsächlich. Wenn das Problem darin besteht, dass das erste Token vier Sekunden benötigt, kann bessere Hardware helfen. Wenn der Text jedoch langsam ausgegeben wird, hilft sie wahrscheinlich nicht.
Sollten Sie Prefill und Decode auf getrennten Workern ausführen?
Große Serving-Stacks machen genau das. Die Technik heißt Prefill-and-Decode-Disaggregation. Ein Worker-Pool führt nur Prefill aus, ein zweiter Pool nur Decode. Der vom ersten Pool aufgebaute KV-Cache wird über eine schnelle Interconnect-Verbindung an den zweiten Pool übertragen. Das funktioniert, weil die beiden Phasen unterschiedliche Hardware und eine unterschiedliche Planung benötigen. Prefill benötigt Rechenleistung und große Token-Batches. Decode benötigt Bandbreite und viele gleichzeitig ausgeführte Sequenzen. Durch die Aufteilung kann jeder Pool unabhängig skaliert werden. Außerdem verhindert sie, dass ein sehr großer Prompt alle aktiven Streams blockiert.
Auf einem einzelnen VPS (virtual private server) mit einer GPU lohnt sich das fast nie. Sie würden ein Gerät gegen sich selbst aufteilen. Außerdem würden Sie aus einer Pointer-Übergabe eine Netzwerkübertragung von mehreren Gigabyte Cache machen. Die Technik lohnt sich, sobald genügend Beschleuniger vorhanden sind, um jeder Phase ganze Maschinen zuzuweisen. Zusätzlich muss genügend gleichmäßiger Datenverkehr vorhanden sein, damit beide Pools ausgelastet bleiben. Unterhalb dieser Größenordnung bietet Chunked Prefill mit einem einzigen Flag fast dieselbe Isolation.
Was zu ändern ist, wenn die Messwerte schlecht sind
Wenn die TTFT zu hoch ist:
- Kürzen Sie den Prompt. Die Prefill-Kosten richten sich nach der Anzahl der Prompt-Tokens. Der System-Prompt wird bei jeder Anfrage erneut abgerechnet.
- Aktivieren Sie Prefix-Caching, damit ein wiederkehrendes Präfix nur einmal statt bei jeder Anfrage berechnet wird.
- Erhöhen Sie
--max-num-batched-tokens, damit mehr Prefill-Arbeit in jedem Schritt verarbeitet wird. - Prüfen Sie die Warteschlange, bevor Sie das Modell verantwortlich machen. Ein Wert von
vllm:num_requests_waitingüber null bedeutet, dass die Anfrage noch nicht gestartet wurde. Das ist ein Kapazitätsproblem.
Wenn die Tokens pro Sekunde zu niedrig sind:
- Quantisieren Sie die Gewichte. Weniger Bytes pro Gewicht bedeuten weniger zu lesende Bytes pro Token.
- Vergleichen Sie die veröffentlichte Speicherbandbreite Ihrer Karte mit der obigen Tabelle. Prüfen Sie, wie nahe Sie an der Obergrenze liegen.
- Verringern Sie
--max-num-batched-tokens, damit Prefills die Decodierung seltener unterbrechen. - Prüfen Sie die Kontextlänge. Bei einer auf mehrere tausend Tokens angewachsenen Unterhaltung wird bei jedem Schritt ein deutlich größerer KV-Cache gelesen.
Auch die Laufzeitumgebung ist hier relevant, weil Ollama und vLLM Prefill und Decodierung unterschiedlich planen und eine Einstellung, die bei der einen Laufzeitumgebung hilft, bei der anderen wirkungslos sein kann. Messen Sie zunächst beide Phasen. Ändern Sie anschließend jeweils nur eine Einstellung.
FAQ
Warum benötigt mein erstes Token Sekunden, während die übrigen schnell gestreamt werden?
Die Wartezeit entsteht beim Prefill, das Streaming beim Decode. Beim Prefill wird der gesamte Prompt in einem rechengebundenen Durchlauf verarbeitet, bevor eine Ausgabe vorliegt. Die Kosten steigen daher mit der Prompt-Länge. Beim Decode wird anschließend in jedem Schritt ein Token ausgegeben. Die Rate wird dabei durch die Speicherbandbreite bestimmt und hängt nahezu unabhängig von der Prompt-Länge ab. Ein langer System-Prompt bei jeder Anfrage ist die häufigste Ursache. Prefix-Caching verhindert, dass dieser Teil der Kosten wiederholt anfällt.
Verlangsamt ein längerer Prompt die Tokenrate pro Sekunde?
Ein wenig, und aus einem anderen Grund als bei TTFT. Jeder Decode-Schritt liest die Keys und Values aller vorherigen Tokens. Ein größerer KV-Cache bedeutet daher mehr gelesene Bytes pro Token. Bei einem üblichen Layout mit 8 Milliarden Parametern ist der Cache etwa 128 KiB pro Token groß. Bei einem Kontext mit 8,000 Tokens werden daher bei jedem Schritt ungefähr 1 GB Daten gelesen. Der größere Effekt eines langen Prompts betrifft weiterhin TTFT und nicht die Streaming-Geschwindigkeit.
Welche GPU-Spezifikation sagt die Decode-Geschwindigkeit voraus?
Die Speicherbandbreite. Teilen Sie die angegebene Bandbreite durch die Größe der im Speicher abgelegten Gewichte. Das ergibt die theoretische Rechenobergrenze für einen Stream. Eine Karte mit mehr Rechenleistung, aber derselben Bandbreite, streamt nicht schneller. Aus demselben Grund verdoppelt eine Quantisierung auf 8 Bit die Decode-Geschwindigkeit ungefähr: Sie halbiert die pro Token gelesene Datenmenge, ohne die Rechenleistung zu verändern.
Warum steigt der Durchsatz, wenn ich Benutzer hinzufüge, obwohl sich jeder Benutzer langsamer anfühlt?
Ein Lesen der Gewichte verarbeitet ein Token für jede Sequenz im Batch. Deshalb steigt die Gesamtzahl der Tokens pro Sekunde mit der Batch-Größe. Jedes einzelne Token wartet nun auf einen gemeinsam genutzten Verarbeitungsschritt. Dadurch steigt gleichzeitig die Latenz pro Benutzer. Überwachen Sie die Inter-Token-Latenz bei p99 statt der aggregierten Durchsatzzahl. Prüfen Sie außerdem vllm:num_requests_waiting, um festzustellen, ob Anfragen in der Warteschlange stehen, anstatt verarbeitet zu werden.