Wie eine GPU aufgebaut ist
Recheneinheiten, Warps, Tensor Cores und Speicherhierarchie: was tatsächlich passiert, wenn eine Matrixmultiplikation auf einer Grafikkarte ausgeführt wird.
Die Idee
Stellen Sie sich eine Fabrikhalle mit hundert Arbeitsgruppen vor. Jede Gruppe hat 32 Leute, die alle denselben Handgriff gleichzeitig ausführen, jeder an einem anderen Werkstück. Wer aus der Reihe tanzt, hält die ganze Gruppe auf.
Genau so arbeitet eine Grafikkarte. Diese Bauform erklärt beides: die enorme Leistung bei gleichförmigen Aufgaben und den völligen Einbruch bei verzweigungsreichem Code.
Die Ebenen
| Ebene | Umfang | Entspricht |
|---|---|---|
| Faden | eine Rechnung | ein Arbeiter |
| Warp | 32 Fäden im Gleichschritt | eine Gruppe |
| Block | bis zu 1.024 Fäden mit gemeinsamem Speicher | eine Schicht an einem Tisch |
| Recheneinheit (SM) | führt viele Blöcke aus | eine Werkbank |
| Karte | 60 bis 150 Recheneinheiten | die Halle |
Die Speicherhierarchie
Sie ist der eigentliche Schlüssel zur Leistung.
| Ebene | Größe | Latenz | Bandbreite |
|---|---|---|---|
| Register | 256 KB je Recheneinheit | ~1 Takt | sehr hoch |
| Shared Memory | 100 bis 228 KB je Recheneinheit | ~20 Takte | hoch |
| L2-Cache | 40 bis 100 MB | ~200 Takte | mittel |
| Kartenspeicher (HBM) | 24 bis 192 GB | ~400 Takte | 1 bis 8 TB/s |
| Hauptspeicher über PCIe | beliebig | ~10.000 Takte | 32 bis 64 GB/s |
Der Sprung von der vierten zur fünften Zeile ist der Grund, warum ein Modell, das nicht in den Kartenspeicher passt, nicht ein bisschen langsamer wird, sondern um den Faktor 30 bis 100.
Divergenz
Ein if in einem Kernel ist deshalb nicht kostenlos, sondern kostet im
schlimmsten Fall den Faktor 32. Praktisch schreibt man Kernel so, dass alle Fäden
eines Warps denselben Pfad nehmen, etwa durch Sortieren der Daten vor der
Verarbeitung.
Kachelung einer Matrixmultiplikation
Der naive Weg liest für jedes Ergebniselement eine ganze Zeile und eine ganze
Spalte aus dem Kartenspeicher. Bei n = 4096 sind das 2 · n³ = 1,4e11
Lesevorgänge. Die gekachelte Variante lädt Blöcke von T × T in den Shared
Memory und benutzt jedes geladene Element T Mal:
Bei T = 64 ist das ein Faktor 64 weniger Speicherverkehr. Genau diese
Optimierung steckt in cuBLAS und CUTLASS, und deshalb erreicht eine handgeschriebene
naive Matrixmultiplikation typischerweise unter 5 Prozent der Kartenleistung.
Tensor Cores und die passenden Formate
Tensor Cores rechnen Blöcke fester Größe, etwa 16 × 16 × 16, in einem Befehl. Damit sie greifen, müssen drei Bedingungen erfüllt sein:
- Das Datenformat passt:
float16,bfloat16,int8,fp8odertf32. Reinesfloat32nutzt sie nicht. - Die Matrixdimensionen sind Vielfache der Blockgröße, meist 8 oder 16. Eine Breite von 4095 statt 4096 kostet spürbar Leistung.
- Der Speicher ist passend ausgerichtet, was Bibliotheken üblicherweise sicherstellen.
Die zweite Bedingung erklärt, warum Modellbreiten in der Praxis immer glatte Zahlen wie 4096, 5120 oder 8192 sind.
Auslastung messen
Eine hohe Auslastung der Recheneinheiten ist nicht das Ziel; ein hoher Anteil tatsächlich genutzter Rechenleistung ist es. Die aussagekräftigen Größen sind der Anteil erreichter Spitzenleistung und der Anteil erreichter Speicherbandbreite. Liegt der erste bei 8 Prozent und der zweite bei 85 Prozent, ist die Aufgabe speichergebunden und jede weitere Rechenleistung nutzlos, siehe Speicher und Bandbreite.