Direkt zum Hauptinhalt

LLM-Inferenz-Performance-Engineering: Bewährte Methoden


Diesen Beitrag teilen
LLM Inference Performance Engineering: Best Practices

In diesem Blogbeitrag teilt das Engineering-Team von MosaicML Best Practices für die Nutzung gängiger Open-Source-Modelle für große Sprachmodelle (LLMs) für den Produktionseinsatz. Wir geben auch Richtlinien für die Bereitstellung von Inferenzdiensten, die auf diesen Modellen basieren, um Benutzern bei der Auswahl von Modellen und Bereitstellungshardware zu helfen. Wir haben mit mehreren PyTorch-basierten Backends in der Produktion gearbeitet; diese Richtlinien basieren auf unseren Erfahrungen mit FasterTransformers, vLLM, NVIDIA's bald erscheinendem TensorRT-LLM und anderen.

Grundlegendes zur LLM-Textgenerierung

Large Language Models (LLMs) generieren Text in einem zweistufigen Prozess: "Prefill", bei dem die Token in der Eingabeaufforderung parallel verarbeitet werden, und "Decodierung", bei dem Text Token für Token autoregressiv generiert wird. Jedes generierte Token wird an die Eingabe angehängt und wieder in das Modell eingespeist, um das nächste Token zu generieren. Die Generierung stoppt, wenn das LLM ein spezielles Stopptoken ausgibt oder wenn eine benutzerdefinierte Bedingung erfüllt ist (z. B. eine maximale Anzahl von Token generiert wurde). Wenn Sie mehr über die Verwendung von Decoder-Blöcken durch LLMs erfahren möchten, lesen Sie diesen Blogbeitrag.

Token können Wörter oder Wortteile sein; die genauen Regeln für die Aufteilung von Text in Token sind von Modell zu Modell unterschiedlich. Sie können beispielsweise vergleichen, wie Llama-Modelle Text tokenisieren und wie OpenAI-Modelle Text tokenisieren. Obwohl LLM-Inferenzanbieter oft über die Leistung in tokenbasierten Metriken (z. B. Token/Sekunde) sprechen, sind diese Zahlen aufgrund dieser Unterschiede nicht immer über Modelltypen hinweg vergleichbar. Ein konkretes Beispiel: Das Team von Anyscale hat herausgefunden, dass die Llama 2-Tokenisierung 19 % länger ist als die ChatGPT-Tokenisierung (aber immer noch viel geringere Gesamtkosten verursacht). Und Forscher bei HuggingFace fanden auch heraus, dass Llama 2 ~20 % mehr Token benötigte, um über die gleiche Textmenge wie GPT-4 zu trainieren.

Wichtige Metriken für LLM-Serving

Wie genau sollen wir also über die Inferenzgeschwindigkeit nachdenken?

Unser Team verwendet vier Schlüsselmetriken für LLM-Serving:

  1. Time To First Token (TTFT): Wie schnell Benutzer die Ausgabe des Modells sehen, nachdem sie ihre Abfrage eingegeben haben. Niedrige Wartezeiten für eine Antwort sind bei Echtzeitinteraktionen unerlässlich, bei Offline-Workloads jedoch weniger wichtig. Diese Metrik wird durch die Zeit bestimmt, die benötigt wird, um die Eingabeaufforderung zu verarbeiten und dann das erste Ausgabetoken zu generieren.
  2. Time Per Output Token (TPOT): Zeit zum Generieren eines Ausgabetokens für jeden Benutzer, der unser System abfragt. Diese Metrik entspricht der Art und Weise, wie jeder Benutzer die "Geschwindigkeit" des Modells wahrnimmt. Beispielsweise würde eine TPOT von 100 Millisekunden/Token 10 Token pro Sekunde und Benutzer oder ~450 Wörter pro Minute entsprechen, was schneller ist, als eine typische Person lesen kann.
  3. Latenz: Die Gesamtzeit, die das Modell benötigt, um die vollständige Antwort für einen Benutzer zu generieren. Die Gesamtlatenz der Antwort kann anhand der beiden vorherigen Metriken berechnet werden: Latenz = (TTFT) + (TPOT) * (die Anzahl der zu generierenden Token).
  4. Durchsatz: Die Anzahl der Ausgabetoken pro Sekunde, die ein Inferenzserver über alle Benutzer und Anfragen hinweg generieren kann.

Unser Ziel? Die schnellste Zeit bis zum ersten Token, der höchste Durchsatz und die schnellste Zeit pro Ausgabetoken. Mit anderen Worten, wir möchten, dass unsere Modelle so schnell wie möglich Text für so viele Benutzer wie möglich generieren.

Insbesondere gibt es einen Kompromiss zwischen Durchsatz und Zeit pro Ausgabetoken: Wenn wir 16 Benutzerabfragen gleichzeitig verarbeiten, haben wir einen höheren Durchsatz im Vergleich zur sequenziellen Ausführung der Abfragen, aber wir benötigen länger, um Ausgabetoken für jeden Benutzer zu generieren.

Wenn Sie allgemeine Ziele für die Inferenzlatenz haben, finden Sie hier einige nützliche Heuristiken für die Bewertung von Modellen:

  • Die Ausgabelänge dominiert die Gesamtantwortlatenz: Für die durchschnittliche Latenz können Sie normalerweise einfach Ihre erwartete/maximale Ausgabetokenlänge nehmen und sie mit einer durchschnittlichen Zeit pro Ausgabetoken für das Modell multiplizieren.
  • Die Eingabelänge ist für die Leistung nicht signifikant, aber wichtig für die Hardwareanforderungen: Das Hinzufügen von 512 Eingabetoken erhöht die Latenz weniger als die Erzeugung von 8 zusätzlichen Ausgabetoken in den MPT-Modellen. Die Notwendigkeit, lange Eingaben zu unterstützen, kann jedoch die Bereitstellung von Modellen erschweren. Wir empfehlen beispielsweise die Verwendung des A100-80GB (oder neuer), um MPT-7B mit seiner maximalen Kontextlänge von 2048 Token bereitzustellen.
  • Die Gesamtlatenz skaliert sublinear mit der Modellgröße: Auf derselben Hardware sind größere Modelle langsamer, aber das Geschwindigkeitsverhältnis entspricht nicht unbedingt dem Parameteranzahlverhältnis. Die MPT-30B-Latenz ist ~2,5x so hoch wie die MPT-7B-Latenz. Die Llama2-70B-Latenz ist ~2x so hoch wie die Llama2-13B-Latenz.

Wir werden oft von potenziellen Kunden gebeten, eine durchschnittliche Inferenzlatenz anzugeben. Wir empfehlen, dass Sie, bevor Sie sich auf bestimmte Latenzziele festlegen ("wir benötigen weniger als 20 ms pro Token"), einige Zeit damit verbringen, Ihre erwarteten Eingabe- und gewünschten Ausgabelängen zu charakterisieren.

Herausforderungen bei der LLM-Inferenz

Die Optimierung der LLM-Inferenz profitiert von allgemeinen Techniken wie:

  • Operator Fusion: Das Kombinieren verschiedener benachbarter Operatoren führt oft zu einer besseren Latenz.
  • Quantisierung: Aktivierungen und Gewichte werden komprimiert, um eine kleinere Anzahl von Bits zu verwenden.
  • Komprimierung: Sparsity oder Distillation.
  • Parallelisierung: Tensor-Parallelität über mehrere Geräte oder Pipeline-Parallelität für größere Modelle.

Neben diesen Methoden gibt es viele wichtige Transformer-spezifische Optimierungen. Ein Paradebeispiel dafür ist KV (Key-Value)-Caching. Der Aufmerksamkeitsmechanismus in reinen Decoder-basierten Transformer-Modellen ist rechentechnisch ineffizient. Jedes Token achtet auf alle zuvor gesehenen Token und berechnet daher viele der gleichen Werte wie bei jedem neuen Token neu. Während beispielsweise das Nte Token generiert wird, achtet das (N-1)te Token auf das (N-2)te, (N-3)te … 1. Token. In ähnlicher Weise muss die Aufmerksamkeit für das Nte Token bei der Generierung des (N+1)ten Tokens erneut auf das (N-1)te, (N-2)te, (N-3)te, … 1. Token achten. KV-Caching, d. h. das Speichern von Zwischenschlüsseln/-werten für die Aufmerksamkeits-Layer, wird verwendet, um diese Ergebnisse für die spätere Wiederverwendung zu speichern und wiederholte Berechnungen zu vermeiden.

Die Speicherbandbreite ist entscheidend

Berechnungen in LLMs werden hauptsächlich von Matrix-Matrix-Multiplikationsoperationen dominiert; diese Operationen mit kleinen Dimensionen sind auf den meisten Hardwarekomponenten typischerweise durch die Speicherbandbreite begrenzt. Beim Generieren von Token auf autoregressive Weise ist eine der Aktivierungsmatrixdimensionen (definiert durch Batchgröße und Anzahl der Token in der Sequenz) bei kleinen Batchgrößen klein. Daher hängt die Geschwindigkeit davon ab, wie schnell wir Modellparameter vom GPU-Speicher in lokale Caches/Register laden können, und nicht davon, wie schnell wir mit geladenen Daten rechnen können. Die verfügbare und erreichte Speicherbandbreite in der Inferenzhardware ist ein besserer Indikator für die Geschwindigkeit der Tokengenerierung als ihre Spitzenrechenleistung.

Die Auslastung der Inferenzhardware ist in Bezug auf die Serving-Kosten sehr wichtig. GPUs sind teuer und wir müssen sie so viel wie möglich arbeiten lassen. Gemeinsame Inferenzdienste versprechen, die Kosten niedrig zu halten, indem sie Workloads von vielen Benutzern kombinieren, einzelne Lücken füllen und überlappende Anfragen zusammenfassen. Für große Modelle wie Llama2-70B erzielen wir nur bei großen Batchgrößen ein gutes Kosten-Leistungs-Verhältnis. Ein Inferenz-Serving-System, das mit großen Batchgrößen arbeiten kann, ist entscheidend für die Kosteneffizienz. Ein großer Batch bedeutet jedoch eine größere KV-Cache-Größe, was wiederum die Anzahl der GPUs erhöht, die zum Bereitstellen des Modells erforderlich sind. Hier gibt es ein Tauziehen, und Betreiber gemeinsam genutzter Dienste müssen einige Kostenkompromisse eingehen und Systemoptimierungen implementieren.

Modellbandbreitenauslastung (MBU)

Wie optimiert ist ein LLM-Inferenzserver?

Wie bereits kurz erläutert, wird die Inferenz für LLMs bei kleineren Batchgrößen – insbesondere zur Decodierungszeit – dadurch eingeschränkt, wie schnell wir Modellparameter vom Gerätespeicher zu den Recheneinheiten laden können. Die Speicherbandbreite bestimmt, wie schnell die Datenbewegung erfolgt. Um die Auslastung der zugrunde liegenden Hardware zu messen, führen wir eine neue Metrik namens Modellbandbreitenauslastung (MBU) ein. MBU ist definiert als (erreichte Speicherbandbreite) / (Spitzenspeicherbandbreite), wobei die erreichte Speicherbandbreite ((Gesamtmodellparametergröße + KV-Cache-Größe) / TPOT) ist.

Wenn beispielsweise ein 7B-Parameter, der mit 16-Bit-Genauigkeit ausgeführt wird, eine TPOT von 14 ms hat, werden 14 GB Parameter in 14 ms verschoben, was einer Bandbreitennutzung von 1 TB/s entspricht. Wenn die Spitzenbandbreite des Rechners 2 TB/s beträgt, laufen wir mit einer MBU von 50 %. Der Einfachheit halber ignoriert dieses Beispiel die KV-Cache-Größe, die für kleinere Batchgrößen und kürzere Sequenzlängen klein ist. MBU-Werte nahe 100 % implizieren, dass das Inferenzsystem die verfügbare Speicherbandbreite effektiv nutzt. MBU ist auch nützlich, um verschiedene Inferenzsysteme (Hardware + Software) auf normalisierte Weise zu vergleichen. MBU ergänzt die Metrik Model Flops Utilization (MFU; eingeführt in der PaLM-Veröffentlichung), die in rechengebundenen Umgebungen wichtig ist.

Abbildung 1 zeigt eine bildliche Darstellung von MBU in einem Diagramm ähnlich einem Roofline-Diagramm. Die durchgezogene geneigte Linie des orangefarbenen Bereichs zeigt den maximal möglichen Durchsatz, wenn die Speicherbandbreite bei 100 % vollständig gesättigt ist. In der Realität ist die beobachtete Leistung jedoch bei niedrigen Batchgrößen (weißer Punkt) geringer als das Maximum – wie viel niedriger ist ein Maß für die MBU. Bei großen Batchgrößen (gelber Bereich) ist das System rechengebunden, und der erreichte Durchsatz als Bruchteil des maximal möglichen Durchsatzes wird als Model Flops Utilization (MFU) gemessen.

Modellbandbreitenauslastung
Abbildung 1: veranschaulicht MBU (Modellbandbreitenauslastung) und MFU (Modell-Flops-Auslastung). MBU und MFU sind Bruchteile der in speichergebundenen bzw. rechengebundenen Regionen erreichten Spitzenwerte.

MBU und MFU bestimmen, wie viel Spielraum noch vorhanden ist, um die Inferenzgeschwindigkeit auf einem bestimmten Hardware-Setup weiter zu erhöhen. Abbildung 2 zeigt die gemessene MBU für verschiedene Grade der Tensorparallelität mit unserem TensorRT-LLM-basierten Inferenzserver. Die maximale Speicherbandbreitenauslastung wird erreicht, wenn große zusammenhängende Speicherblöcke übertragen werden. Wenn kleinere Modelle wie MPT-7B auf mehrere GPUs verteilt werden, beobachten wir eine niedrigere MBU, da wir kleinere Speicherblöcke in jeder GPU verschieben.

Empirisch beobachtete MBU
Abbildung 2: Empirisch beobachtete MBU für verschiedene Grade der Tensorparallelität mit TensorRT-LLM auf A100-40G-GPUs. Anfragen: Sequenzen von 512 Eingabetoken mit einer Batchgröße von 1.

Abbildung 3 zeigt die empirisch beobachtete MBU für verschiedene Grade der Tensorparallelität und Batchgrößen auf den NVIDIA H100-GPUs. Die MBU nimmt mit zunehmender Batchgröße ab. Wenn wir jedoch GPUs skalieren, ist die relative Abnahme der MBU weniger signifikant. Es ist auch erwähnenswert, dass die Auswahl von Hardware mit größerer Speicherbandbreite die Leistung mit weniger GPUs steigern kann. Bei einer Batchgröße von 1 können wir eine höhere MBU von 60 % auf 2xH100-80GBs im Vergleich zu 55 % auf 4xA100-40GB-GPUs erzielen (Abbildung 2).

Tensor-Parallelitätsmodi
Abbildung 3: Empirisch beobachtete MBU für verschiedene Batchgrößen und Tensor-Parallelitätsmodi auf H100-80G-GPUs. Anfragen: Sequenzen von 512 Eingabetoken

Benchmarking-Ergebnisse

Latenz

Wir haben die Zeit bis zum ersten Token (TTFT) und die Zeit pro Ausgabetoken (TPOT) über verschiedene Grade der Tensorparallelität für MPT-7B- und Llama2-70B-Modelle gemessen. Mit zunehmender Länge der Eingabeaufforderungen beginnt die Zeit zum Generieren des ersten Tokens einen erheblichen Teil der Gesamtlatenz zu beanspruchen. Die Tensorparallelisierung über mehrere GPUs hilft, diese Latenz zu reduzieren.

Im Gegensatz zum Modelltraining bietet die Skalierung auf mehr GPUs einen erheblichen abnehmenden Ertrag für die Inferenzlatenz. Z. B. verringert sich die Latenz für Llama2-70B beim Übergang von 4x zu 8x GPUs nur um das 0,7-fache bei kleinen Batchgrößen. Ein Grund dafür ist, dass eine höhere Parallelität eine niedrigere MBU aufweist (wie bereits erwähnt). Ein weiterer Grund ist, dass die Tensorparallelität Kommunikations-Overhead über einen GPU-Knoten hinweg verursacht.

  Zeit bis zum ersten Token (ms)
Modell 1xA100-40GB 2xA100-40GB 4xA100-40GB 8xA100-40GB
MPT-7B 46 (1x) 34 (0,73x) 26 (0,56x) -
Llama2-70B Passt nicht 154 (1x) 114 (0,74x)

Tabelle 1: Zeit bis zum ersten Token, wenn Eingabeanfragen 512 Token lang sind und die Batchgröße 1 beträgt. Größere Modelle wie Llama2 70B benötigen mindestens 4xA100-40B-GPUs, um in den Speicher zu passen

Bei größeren Batchgrößen führt eine höhere Tensorparallelität zu einer signifikanteren relativen Abnahme der Tokenlatenz. Abbildung 4 zeigt, wie sich die Zeit pro Ausgabetoken für MPT-7B ändert. Bei einer Batchgröße von 1 reduziert der Übergang von 2x zu 4x die Tokenlatenz nur um ~12 %. Bei einer Batchgröße von 16 ist die Latenz mit 4x 33 % niedriger als mit 2x. Dies stimmt mit unserer früheren Beobachtung überein, dass die relative Abnahme der MBU bei höheren Graden der Tensorparallelität für eine Batchgröße von 16 geringer ist als für eine Batchgröße von 1.

Erhöhung der Anzahl der GPUs
Abbildung 4: Zeit pro Ausgabetoken pro Benutzer, wenn wir MPT-7B über A100-40GB-GPUs skalieren. Die Latenz skaliert nicht linear mit der zunehmenden Anzahl von GPUs. Anfragen: Sequenzen von 128 Eingabe- und 64 Ausgabetoken

Abbildung 5 zeigt ähnliche Ergebnisse für Llama2-70B, mit der Ausnahme, dass die relative Verbesserung zwischen 4x und 8x weniger ausgeprägt ist. Wir vergleichen auch die GPU-Skalierung über zwei verschiedene Hardwarekomponenten hinweg. Da H100-80GB eine 2,15x höhere GPU-Speicherbandbreite im Vergleich zu A100-40GB aufweist, können wir sehen, dass die Latenz bei einer Batchgröße von 1 um 36 % und bei einer Batchgröße von 16 um 52 % für 4x-Systeme niedriger ist.

Mehrere GPUs
Abbildung 5: Zeit pro Ausgabetoken pro Benutzer, wenn wir Llama-v2-70B über mehrere GPUs skalieren (Eingabeanfragen: 512 Token Länge). Bitte beachten Sie, dass 1x40GB GPU-, 2x40GB- und 1x80GB-GPU-Zahlen hier fehlen, da Llama-v2-70B (in Float16) nicht auf diese Systeme passt.

Durchsatz

Wir können Durchsatz und Zeit pro Token gegeneinander abwägen, indem wir Anfragen zusammenfassen. Das Gruppieren von Abfragen während der GPU-Auswertung erhöht den Durchsatz im Vergleich zur sequenziellen Verarbeitung von Abfragen, aber jede Abfrage dauert länger (ohne Berücksichtigung von Warteschlangeneffekten).

Es gibt einige gängige Techniken zum Batching von Inferenzanfragen:

  • Statisches Batching: Der Client packt mehrere Eingabeaufforderungen in Anfragen, und eine Antwort wird zurückgegeben, nachdem alle Sequenzen im Batch abgeschlossen wurden. Unsere Inferenzserver unterstützen dies, erfordern es aber nicht.
  • Dynamisches Batching: Eingabeaufforderungen werden im laufenden Betrieb innerhalb des Servers zusammengefasst. In der Regel ist diese Methode schlechter als statisches Batching, kann aber nahezu optimal sein, wenn die Antworten kurz oder von einheitlicher Länge sind. Funktioniert nicht gut, wenn Anfragen unterschiedliche Parameter haben.
  • Kontinuierliches Batching: Die Idee, Anfragen beim Eintreffen zusammenzufassen, wurde in diesem hervorragenden Artikel vorgestellt und ist derzeit die SOTA-Methode. Anstatt darauf zu warten, dass alle Sequenzen in einem Batch abgeschlossen sind, werden Sequenzen auf Iterationsebene zusammengefasst. Es kann einen 10- bis 20-mal besseren Durchsatz als dynamisches Batching erzielen.

LLM-Serving
Abbildung 6: Verschiedene Arten von Batching mit LLM-Serving. Batching ist eine effektive Möglichkeit, die Effizienz der Inferenz zu verbessern.

Kontinuierliches Batching ist normalerweise der beste Ansatz für gemeinsam genutzte Dienste, aber es gibt Situationen, in denen die anderen beiden besser sein könnten. In Umgebungen mit niedriger QPS kann dynamisches Batching kontinuierliches Batching übertreffen. Es ist manchmal einfacher, GPU-Optimierungen auf niedriger Ebene in einem einfacheren Batching-Framework zu implementieren. Für Offline-Batch-Inferenz-Workloads kann statisches Batching erheblichen Overhead vermeiden und einen besseren Durchsatz erzielen.

Batchgröße

Wie gut Batching funktioniert, hängt stark vom Anfragestream ab. Aber wir können eine Obergrenze für seine Leistung erhalten, indem wir statisches Batching mit einheitlichen Anfragen benchmarken.

  Batchgröße
Hardware 1 4 8 16 32 64 128
1 x A10 0,4 (1x) 1,4 (3,5x) 2,3 (6x) 3,5 (9x) OOM-Fehler (Out of Memory)
2 x A10 0,8 2,5 4,0 7,0 8,0  
1 x A100 0,9 (1x) 3,2 (3,5x) 5,3 (6x) 8,0 (9x) 10,5 (12x) 12,5 (14x)  
2 x A100 1,3 3,0 5,5 9,5 14,5 17,0 22,0
4 x A100 1,7 6,2 11,5 18,0 25,0 33,0 36,5

Tabelle 2: Spitzen-MPT-7B-Durchsatz (req/s) mit statischem Batching und einem FasterTransformers-basierten Backend. Anfragen: 512 Eingabe- und 64 Ausgabetoken. Bei größeren Eingaben liegt die OOM-Grenze bei kleineren Batchgrößen.

Latenz-Kompromiss

Die Anfrage-Latenz nimmt mit der Batchgröße zu. Wenn wir beispielsweise mit einer NVIDIA A100-GPU den Durchsatz mit einer Batchgröße von 64 maximieren, erhöht sich die Latenz um das 4-fache, während sich der Durchsatz um das 14-fache erhöht. Gemeinsame Inferenzdienste wählen in der Regel eine ausgewogene Batchgröße. Benutzer, die ihre eigenen Modelle hosten, sollten den entsprechenden Latenz-/Durchsatz-Kompromiss für ihre Anwendungen festlegen. In einigen Anwendungen, wie z. B. Chatbots, hat eine niedrige Latenz für schnelle Antworten oberste Priorität. In anderen Anwendungen, wie z. B. der Batch-Verarbeitung unstrukturierter PDFs, möchten wir möglicherweise die Latenz für die Verarbeitung eines einzelnen Dokuments opfern, um alle parallel schnell zu verarbeiten.

Abbildung 7 zeigt die Durchsatz-vs.-Latenz-Kurve für das 7B-Modell. Jede Linie auf dieser Kurve wird erhalten, indem die Batchgröße von 1 auf 256 erhöht wird. Dies ist nützlich, um zu bestimmen, wie groß wi