Zwei einfache Techniken, um sicherzustellen, dass Ihr LLM-Training maximale Performance erreicht.
von Bruce Fontaine und Pardis Beikzadeh
Das Trainieren oder Feintunen eines Foundation-Modells mit mehreren Milliarden Parametern erfordert erhebliche Rechenleistung, die sich oft über Hunderte bis Tausende von GPU-Trainingsstunden erstreckt. Das Optimieren der Trainingsperformance für ein LLM ist jedoch eine Herausforderung, und allgemeine Metriken wie die GPU-Auslastung zeigen möglicherweise nicht Ihre tatsächliche Auslastung. In diesem Blogbeitrag zeigen wir, wie Sie die Abstraktionsschichten durchbrechen und das Feintuning des neuesten Qwen 3.8 27B-Modells auf AI Runtime um das Sechsfache beschleunigen können. Durch systematische Performance-Optimierung reduzieren wir beim Feintuning eines festen Datensatzes die Zeit von 7 Stunden und 44 Minuten auf 1 Stunde und 17 Minuten – und das selbst bei einer bestehenden GPU-Auslastung von 98 %.
Wir bieten einen tiefen Einblick in die Techniken hinter diesen Verbesserungen: von der Messung der entscheidenden Faktoren über Implementierungsdetails bis hin zu den Design-Abwägungen bei jeder Optimierung.
Wenn Sie sofort loslegen möchten, springen Sie direkt zum Beispielcode auf AI Runtime und probieren Sie es aus!
Wir beginnen mit einer Standard-Trainingsschleife für Qwen 3.8 27B unter Verwendung der transformers-Bibliothek und des SFT-Trainingsdatensatzes HuggingFaceH4/ultrachat_200k.
Unser Baseline-Training des Modells über den gesamten Datensatz dauert 7 Stunden und 44 Minuten, und während des Trainings zeigt die GPU eine durchschnittliche Auslastung von 98 % an. Das Ergebnis ist ein Training mit 9.396 tok/s.
Diese GPU-Auslastung sieht hoch aus, aber wie bereits erwähnt, ist diese Zahl irreführend. Die Standardmetrik für die GPU-Auslastung misst nur den Prozentsatz der Zeit, in der die GPU irgendetwas getan hat. Sie sagt uns also nicht, wie gut das Modell die GPU genutzt hat, während sie aktiv war. Um das zu verstehen, müssen wir genauer hinsehen.
Eine Möglichkeit hierfür ist der Torch-Trace-Mechanismus. Wir können unsere Trainingsschleife wie folgt anpassen (und gleichzeitig die Anzahl der Schritte begrenzen, um eine schnelle Antwort zu erhalten):
Für einen Beispielschritt erhalten wir folgende Daten:
| Operation | Anzahl der Aufrufe | Durchschnittliche Zeit (Mikros) | Gesamtzeit (Millis) |
|---|---|---|---|
| aten::bmm | 31,105 | 50.5 | 1,571.80 |
| aten::mul | 46,878 | 16.2 | 758.9 |
| MmBackward0 | 497 | 1,111.4 | 552.4 |
| aten::copy_ | 69,444 | 7.2 | 500.6 |
| aten::add | 45,623 | 8.0 | 366.7 |
| aten::select | 68,645 | 3.4 | 230.5 |
| aten::exp | 9,504 | 21.7 | 206.5 |
| aten::sum | 16,194 | 12.2 | 196.8 |
| aten::empty | 40,709 | 4.7 | 190.5 |
| aten::as_strided | 218,214 | 0.8 | 173.3 |
Tabelle 1: CPU – 14,4 s mit 1.714.382 Aufrufen. Die Tabelle enthält die Top-Operationen nach Gesamtzeit.
| Operation | Anzahl der Aufrufe | Durchschnittliche Zeit (Mikros) | Gesamtzeit (Millis) |
|---|---|---|---|
| nvjet GEMM | 1923 | 1,163.1 | 2,236.8 |
| Elementwise floating point add | 42,657 | 51.1 | 2,180.5 |
| Elementwise floating point multiply | 35,012 | 24 | 841.8 |
| Elementwise floating point fill | 33,557 | 21.5 | 723.1 |
| Elementwise floating point copy | 59,282 | 11.8 | 700.7 |
| Memcpy DtoD | 5,585 | 90.5 | 505.2 |
| sm80_xmma GEMM | 31,105 | 20.9 | 651.3 |
Tabelle 2: GPU – 10,3 s mit 269.448 Operationen. Die Tabelle enthält die Top-Operationen nach Gesamtzeit
Aus diesen Tabellen erkennen wir einige grundlegende Probleme beim aktuellen Modell:
Außerdem haben wir ein Problem mit übermäßigem Padding im Batch: Der Eingabe-Tensor muss rechteckig sein, und jede Zeile wird bis zur maximalen Anzahl an Tokens für diesen Batch aufgefüllt (Padding). Wir zeigen einen Trick für LLMs, mit dem sich dies minimieren lässt.
In den folgenden Abschnitten werden wir tiefere Einblicke in die einzelnen technischen Themen geben und erklären, worum es sich handelt, welche Best Practices empfohlen werden und welche wesentlichen Abwägungen bei der Anwendung zu berücksichtigen sind.
Sie werden feststellen, dass einige Kernel sehr h äufig vorkommen! Insbesondere bei Qwen wird der Kernel für die Batch-Matrix-Multiplikation in einem einzigen Schritt über 30.000 Mal ausgeführt.
In diesem Fall schauen wir uns zuerst fusionierte Kernel an. Ein fusionierter Kernel fasst mehrere kleinere GPU-Kernel zu einer Einheit zusammen. Dies kann die Performance verbessern, indem kostenintensive Übertragungen zwischen dem High Bandwidth Memory einer GPU und ihrem noch schnelleren On-Die-Speicher vermieden werden. Außerdem ermöglicht es eine effizientere Skalierung und Auslastung der GPU-Kerne.
Bei so vielen zusätzlichen Kerneln funktioniert torch.compile zwar möglicherweise, aber die Ausführung nimmt viel Zeit in Anspruch. Die Reduzierung der reinen Anzahl an Ops durch manuell gefuste Kernel ist daher ein guter erster Schritt. Zudem sind bekannte Fusionen wie Flash Attention nichts, was torch.compile von selbst finden würde.
Die transformers-Bibliothek verwendet standardmäßig SDPA von PyTorch für die Standard-Attention-Layer. Es stellt sich jedoch heraus, dass viele dieser kleinen Operationen, die wir sehen, von den nicht-standardmäßigen Gated-Delta-Network-Layern stammen. Glücklicherweise gibt es für diese Layer zwei Kernel, causal-conv1d und flash-linear-attention, die entwickelt wurden, um viele dieser Operationen pro Layer zu fusionieren. Wie finden wir das heraus? Die Implementierung der transformers-Bibliothek von Qwen zeigt uns bereits, welche Kernel unterstützt werden:
Es gibt zwei grundlegende Methoden, um diese Kernel zu erhalten:
Wir empfehlen generell die erste Option, insbesondere in Kombination mit unserer Docker-Container-Unterstützung: Erstellen Sie Ihren Container mit allen installierten Kerneln zur Build-Zeit, um die Startzeit Ihres Jobs zu verkürzen.
Kernel Hub ist zwar praktisch, aber da Kernel oft explizit von den installierten Torch- und CUDA-Versionen abhängen, ist für Ihre spezifische Konfiguration möglicherweise kein passender Kernel verfügbar. In diesem Fall greift das transformers-Modell stillschweigend auf einen unoptimierten Kernel mit geringer Leistung zurück.
Nach der Installation der Python-Pakete flash-linear-attention und causal-conv1d konnten wir unsere Ende-zu-Ende-Laufzeit um das 3,7-fache auf 2 Stunden und 5 Minuten reduzieren. In den folgenden Tabellen sehen Sie die neuen (hervorgehobenen) Kernel, die an die Stelle der elementweisen Funktionen aus den vorherigen Tabellen treten. Ebenso wurde die Gesamtzahl der ausgeführten Operationen drastisch reduziert – um das 15-fache:
| Operation | Anzahl der Aufrufe | Durchschnittliche Zeit (µs) | Gesamtzeit (ms) |
|---|---|---|---|
| cudaMemsetAsync | 1.854 | 283,9 | 526,3 |
| ChunkGatedDeltaRuleFunction | 96 | 871,7 | 83,7 |
| aten::mm | 1.987 | 39,6 | 78,6 |
| MmBackward0 | 497 | 134,2 | 66,7 |
| ChunkGatedDeltaRuleFunctionBackward | 48 | 1.159,9 | 55,7 |
| DaoAILab::_causal_conv1d_fwd_cpp | 96 | 55,8 | 5,4 |
| CausalConv1dFn | 96 | 51,4 | 4,9 |
Tabelle 3: CPU – 3,9 s bei 118.316 Aufrufen. Die Tabelle enthält die Top-Operationen nach Gesamtzeit.
| Operation | Anzahl der Aufrufe | Durchschnittliche Zeit (µs) | Gesamtzeit (ms) |
|---|---|---|---|
| nvjet GEMM | 1.923 | 1.183,3 | 2.275,5 |
| Elementweise Gleitkomma-Multiplikation | 2184 | 128,1 | 279,9 |
| Elementweises Kopieren + Casten | 1255 | 101,1 | 126,9 |
| chunk_cat flash-linear-attention | 65 | 1.402,0 | 91,2 |
| flash_attention_3 forwards | 16 | 2.693,8 | 43,1 |
| chunk_gated_delta_rule_fwd | 144 | 255,6 | 36,8 |
| flash_attention_3 backwards | 32 | 965,6 | 30,9 |
Tabelle 4: GPU – 3,9 s bei 18.060 Aufrufen. Die Tabelle enthält die Top-Operationen nach Gesamtzeit.
Hinweis: Beim Training benötigen Sie sowohl einen Kernel für den Vorwärts- als auch für den Rückwärtsdurchlauf. Einige Kernel unterstützen jedoch nur den Vorwärtsdurchlauf, da sie lediglich zur Beschleunigung der Inferenz implementiert wurden.
Fazit
Unser Qwen-Modell ist jetzt ziemlich schnell, aber wir können noch schneller werden. Standardmäßig verwendet unser Batching-Setup rechteckige 2D-Tensoren mit 8.192 Tokens, da dies die größte Größe ist, die passt, ohne dass der Speicher ausgeht. Der Nachteil dieses Ansatzes besteht darin, dass wir durch Padding einen erheblichen Teil des Platzes im Tensor verschwenden.
Wenn Qwen nur Standard-Attention-Mechanismen verwenden würde, wäre NestedTensor von Torch gut unterstützt und würde helfen, diesen Aufwand zu reduzieren, indem das Padding vollständig entfernt wird. Allerdings unterstützt flash-linear-attention nur einen traditionelleren Ansatz mit gepackten 1D-Tensoren:

Aber wir können das nicht einfach willkürlich tun: Das einfache Übergeben des 1D-Tensors an das Modell mag für das Pretraining akzeptabel sein, aber für das Finetuning müssen wir sicherstellen, dass sich die Tokens in einem Beispiel nur auf Tokens desselben Beispiels beziehen. Um das zu beheben, müssen wir die korrekten position_ids des Beispiels an das Modell übergeben. Das Modell wiederum übergibt diese an die fusionierten Attention-Kernel, die dann die richtige Attention-Maske anwenden. Wenn Sie sich den Beispielcode hier ansehen, können Sie sehen, wie wir die erforderlichen Eingaben berechnen, damit dies funktioniert.
Mit dieser einfach aussehenden Änderung verkürzt sich unsere Trainingszeit von 2 Stunden 5 Minuten auf 1 Stunde und 17 Minuten.
Dafür gibt es zwei Gründe:
Kompromiss: Diese Art der Änderung macht die Eingabe-Pipeline komplexer und birgt ein hohes Risiko für Fehler, die sich auf die Modellqualität auswirken. In diesem Fall sind die Einsparungen von 36 % es jedoch wert. Es gibt weitere Techniken zur Reduzierung des Paddings, wie z. B. die Beibehaltung des ursprünglichen Batching-Mechanismus bei gleichzeitigem Sortieren aller Eingaben nach Länge, sodass das Padding minimiert wird. Diese Reihenfolge kann jedoch unerwartete Korrelationen in benachbarten Samples erzeugen und das Modell verfälschen.
Fazit: Beim Training von LLMs über einen Datensatz mit Eingaben, die kürzer als Ihre maximal trainierbaren Tokens sind (z. B. in diesem Fall 8.192), kann das effiziente Packen von Daten in Batches eine erhebliche Zeitersparnis bringen. Für diesen Datensatz konnten wir eine 1,6-fache Steigerung der Trainingsgeschwindigkeit erzielen, wodurch sich unsere Trainingszeit von 2 Stunden 5 Minuten auf 1 Stunde und 17 Minuten verkürzte.
In diesem Blogbeitrag haben wir Ihnen gezeigt, wie Sie Ihre Trainingseffizienz um das Sechsfache steigern können. Entscheidend waren (1) zuerst das Messen der relevanten Metriken – nicht nur der GPU-Auslastung, sondern auch der Bereiche, in denen Ihr Modell während des Trainings Zeit verbringt –, (2) das Optimieren und Fusionieren von Kerneln mithilfe bekannter Bibliotheken wie Flash Linear Attention und (3) die gezielte Anpassung der Zuführung von Datenbatches in das Modell.
Um dies im großen Maßstab umzusetzen, benötigen Sie eine Plattform, die einen performanten Weg zum Standard macht. In AIR sind viele dieser Leistungsverbesserungen bereits integriert. Wir liefern die neueste CUDA-Version und Flash Attention direkt in unseren Basisumgebungen mit, erfassen automatisch Kernmetriken wie GPU-Auslastung, -Temperatur und -Zustand für Sie und stellen sicher, dass Torch-Versionen (und die Profiler) auf dem neuesten Stand sind. Unser interner Agent, Genie Code, ist außerdem darauf abgestimmt, nach solchen Möglichkeiten zu suchen, um Ihr Training weiter zu optimieren. Darüber hinaus ist das Lesen von Daten aus Databricks durch unseren UCVolumeDataset optimiert.
Probieren Sie AIR doch einfach für Ihren nächsten Trainings-Workload aus! Bereit zum Ausprobieren? Im AI Runtime Cookbook finden Sie Details dazu, wie Sie dieses Modell selbst ausführen können.
(Dieser Blogbeitrag wurde mit KI-gestützten Tools übersetzt.) Originalbeitrag
Abonnieren Sie unseren Blog und erhalten Sie die neuesten Beiträge direkt in Ihren Posteingang.