24 Punkte von GN⁺ 2025-06-25 | 1 Kommentare | Auf WhatsApp teilen
  • GPUs sind bei der Rechengeschwindigkeit dem Speicherzugriff weit überlegen, daher wird die Speicherhierarchie zum Performance-Flaschenhals
  • Je nach Arithmetic Intensity (AI) werden Berechnungen in memory-bound oder compute-bound eingeteilt; der Schwellenwert der A100-GPU liegt bei etwa 13 FLOPs/Byte
  • Zu den wichtigsten Strategien der Performance-Optimierung gehören Fusion und Tiling; Fusion reduziert unnötige Speicher-Roundtrips, Tiling maximiert die Datenwiederverwendung
  • Für das Schreiben hochperformanter Kernel ist es wichtig, die strukturellen Eigenschaften der GPU-Hardware zu verstehen, etwa Synchronisierung, Coalesced Load und das Vermeiden von Bankkonflikten
  • Weitere Aspekte wie Occupancy, Minimierung von Thread-Divergenz und Quantisierung haben erheblichen Einfluss auf die tatsächliche Performance

Compute- und Speicherhierarchie von GPUs

  • GPUs haben im Allgemeinen eine deutlich höhere arithmetische Verarbeitungsleistung als Speicherbandbreite
  • Zum Beispiel erreicht die NVIDIA A100 etwa 19,5 TFLOPS (32-Bit-Gleitkomma), während die Speicherbandbreite bei rund 1,5 TB/s liegt
  • Während 4 Byte Daten gelesen werden, können Dutzende Rechenoperationen ausgeführt werden, daher ist Datenbewegung der Performance-Flaschenhals
  • Global Memory (VRAM) ist ein langsamer Off-Chip-Speicher, in dem alle Daten liegen, während der Streaming Multiprocessor (SM) das Rechnen übernimmt
  • Jeder SM besitzt einen schnellen On-Chip-Shared Memory (SRAM), der als vom Programm direkt verwalteter Cache genutzt werden kann
  • Threads sind die kleinste Ausführungseinheit, und jeder Thread besitzt einen eigenen Registersatz
  • 32 Threads bilden einen Warp, und ein Block ist ein Grid aus Threads, das auf demselben SM ausgeführt wird

Performance-Bereiche: memory-bound vs. compute-bound

  • Die Performance eines Kernels ist entweder memory-bound (durch Datentransferrate begrenzt) oder compute-bound (durch die Rechenleistung des SM begrenzt)
  • Arithmetic Intensity (AI) ist definiert als Total FLOPs / Total Bytes Accessed und ist eine wichtige Kennzahl
  • Das Roofline-Modell stellt die tatsächlich erzielbare Performance eines Kernels in einem Diagramm mit AI auf der x-Achse und FLOPS/s auf der y-Achse dar
    • Ist die AI niedrig und damit memory-bound, liegt die Performance auf der Diagonalen (durch die Speicherbandbreite begrenzt)
    • Ist die AI hoch und damit compute-bound, liegt sie auf der Horizontalen (durch die maximale Rechenleistung begrenzt)
  • Der Ridge Point der A100 ist 19,5 TFLOPS / 1,5 TB/s ≈ 13 FLOPs/Byte
  • Wird die AI erhöht, steigt die Performance, und der Kernel kann den compute-bound-Bereich erreichen

Strategien zur Erhöhung der Arithmetic Intensity

  • Einfaches Modell: Ein Thread berechnet genau ein C[i,j] → AI = 0,25 (sehr niedrig, memory-bound)
  • Selbst wenn ein Thread ein 2x2-Tile berechnet, ist AI = 0,5 (immer noch niedrig)
  • Um die AI zu erhöhen, müssen mehrere Threads blockweise große Tiles in den Shared Memory laden, um die Datenwiederverwendung zu maximieren
  • Durch die Zusammenarbeit der Threads innerhalb eines Blocks kann AI > 13 erreicht werden, wodurch der compute-bound-Bereich möglich wird

Overhead-bound-Zustand

  • Beim Zuweisen von Arbeit von der CPU (Host) an die GPU kann Overhead entstehen
  • Sind GPU-Kernel zu klein oder zu zahlreich, wartet die GPU auf neue Arbeit
  • Moderne Frameworks führen asynchrone Ausführung ein und queueen Command-Streams vorab, um den Overhead zu minimieren

Zwei zentrale Strategien zur Performance-Steigerung: Fusion und Tiling

Operator Fusion

  • Bei einfachen Operationsketten, z. B. y = relu(x + 1), verursacht jede Operation als separater Kernel Speicherverkehr zum und vom Global Memory
  • Fusion fasst mehrere Operationen in einem einzigen Kernel zusammen, speichert Zwischenwerte nicht im Global Memory, sondern verarbeitet sie in Registern und schreibt nur das Endergebnis zurück
  • Beispiele: JIT-Compiler wie Triton oder torch.compile Inductor automatisieren dies

Tiling

  • Bei komplexeren Operationen wie Matrixmultiplikation ist die AI im Single-Thread-Modell niedrig
  • Nach dem Aufteilen in blockweise Tiles laden alle Threads im Block gemeinsam Datentiles in den Shared Memory und ermöglichen so umfangreiche Datenwiederverwendung
  • Die Berechnung folgt dem dreistufigen Muster: "Load (Global Memory -> Shared Memory) - Synchronize - Compute"

Coalesced Load und Vektorisierung

  • Beim Verschieben von Daten aus dem Global Memory in den Shared Memory ist Coalesced Access wichtig (32 Threads eines Warps greifen auf zusammenhängende 128-Byte-Bereiche zu)
  • Durch Vektorisierung, z. B. mit float4, können mehrere Datenwerte auf einmal geladen werden, was Hardware-Ressourcen spart und die Speicherbandbreitennutzung maximiert
  • Daten-Alignment ist zwingend nötig; der K-Wert in Byte innerhalb einer Matrix sollte für Effizienz ein Vielfaches von 4 sein

Shared-Memory-Bänke und Bankkonflikte

  • Shared Memory besteht aus 32 unabhängigen Bänken, daher sollten die 32 Threads eines Warps jeweils auf unterschiedliche Bänke zugreifen, um konfliktfrei zu arbeiten
  • Zeilenweiser Zugriff verursacht keine Konflikte, spaltenweiser Zugriff dagegen schon (Zugriff auf dieselbe Bank)
  • Für das B-Tile wird beim Laden eine Strategie vom Typ "Load and Transpose" verwendet, sodass es transponiert im Shared Memory gespeichert wird und bei der Berechnung überwiegend zeilenweise Zugriffe erfolgen, um Bankkonflikte zu vermeiden

Muster für schnelle On-Chip-Berechnungen

Grundstrategie 1: Ein Thread berechnet einen Output

  • Unter der Begrenzung BLOCK_DIM=32 liegt die maximale AI bei 8, daher ist der compute-bound-Bereich nicht erreichbar

Strategie 2: Ein Thread berechnet mehrere Outputs

  • Bei BLOCK_DIM=16 und TILE_DIM=64 berechnet ein Thread 4x4 Outputs → AI = 16
  • Da AI > 13 ist, lässt sich auf einer A100 compute-bound-Performance erreichen
  • Effiziente Berechnung ist mit vektorisierten Loads wie float4 aus dem Shared Memory möglich

Praktische Grenzen des Tiling: Tile-Quantisierung

  • Ist die Matrixgröße kein Vielfaches der Tile-Größe, berechnen Randblöcke Bereiche, die größer als nötig sind (unnötige Berechnung), und werden gepolstert
  • Threads am Rand verhindern mit Guard-Bedingungen unnötige Speicherzugriffe, aber die Berechnungsschleife läuft identisch weiter, wodurch nutzlose Operationen entstehen (z. B. C += A * 0)

Weitere Elemente des Performance-Tunings

Occupancy und Verbergen von Latenz

  • Wenn ein Warp etwa auf Speicherzugriffe lange warten muss, schaltet der SM sofort auf einen anderen Warp um und reduziert so Leerlaufzeit (Latency Hiding)
  • Werden mehrere Thread Blocks gleichzeitig zugewiesen, minimiert hohe Occupancy die Wartezeit
  • Werden Block- oder Tile-Größen zu groß, sinkt die Zahl residenter Blocks, was die Occupancy reduziert und die Performance verschlechtert

Minimierung von Thread-Divergenz

  • Kommt es innerhalb eines Warps zu if-else-Verzweigungen, werden beide Pfade nacheinander ausgeführt, wodurch die effektive Performance ungefähr halbiert wird
  • Branchless Code mit min, max usw. ist nötig, um Divergenz zu minimieren

Quantisierung

  • Wird die Präzision von FP32 auf FP16/BFP16 reduziert, verdoppeln sich sowohl die bewegte Datenmenge pro Speichertransfer als auch die Menge verarbeitbarer Daten
  • Auf einer A100 kann FP16-Rechenleistung 312 TFLOPS erreichen (gegenüber 19,5 TFLOPS bei FP32 relativ bis zu 16-fache Performance)
  • Quantisierung kann im Roofline-Modell gleichzeitig eine Verschiebung nach rechts (Speichereffizienz) und nach oben (maximale Rechenleistung) bewirken

Gesamtzusammenfassung

  • Die grundlegende Grenze der GPU-Performance ergibt sich aus dem Ungleichgewicht zwischen Speicherbandbreite und On-Chip-Rechenleistung
  • Performance-Steigerung wird durch maximale Datenwiederverwendung (Tiling) und minimale Zwischenspeicher-Traffic-Kosten (Fusion) erreicht
  • Um hochperformante Kernel zu schreiben und zu optimieren, müssen die Eigenschaften der Hardware-Struktur verstanden werden, darunter Warps, Bänke, Coalesced Access und Synchronisierung
  • In der Praxis beeinflussen zusätzliche Faktoren wie Occupancy, Minimierung von Divergenz und Quantisierung die tatsächliche Geschwindigkeit direkt
  • Das Design hochperformanter GPU-Berechnungen erfordert eine kombinierte Betrachtung aus theoretischer AI-Steigerung, Nutzung von Hardware-Eigenschaften sowie dem Umgang mit realen Datenlayouts und -größen

1 Kommentare

 
GN⁺ 2025-06-25
Hacker-News-Kommentare
  • Neugier, wie gut die Optimierung ganzer Programme auf Compiler-Ebene inzwischen funktioniert; die aktuelle Vorgehensweise, jede einzelne LLM-Architektur separat zu optimieren, wirkt irgendwie rückständig

  • Erfahrungsbericht über den Versuch, auf derselben 4070 llama.cpp und vllm laufen zu lassen, um mehr Prompts im Batch zu verarbeiten: Ab Batch 8 wurde llama.cpp drastisch langsamer, und obwohl die GPU-Auslastung ordentlich aussah, lag tatsächlich ein Bottleneck vor; vllm kam damit spürbar viel besser zurecht

    • vllm nutzt einen paged KV-Cache und ein fully coalesced Layout, das die GPU bevorzugt, und liefert dadurch batch-optimierte Performance; llama.cpp verwendet dagegen ein flaches Layout, das für einzelne Prompts gut ist, aber bei Batches die L2-Speicherzugriffsmuster zerstört und so zu Geschwindigkeitseinbußen führt

    • Geteilte Erfahrung, dass in llama.cpp durch Interleaving des KV-Tensors von [seq, head, dim] zu [head, seq, dim] die Art der Datenzufuhr zum fused attention kernel aus vllm nachvollzogen wurde und sich die Rechenleistung sofort etwa verdoppelte

    • Der Bottleneck lag nicht an der GPU selbst, sondern daran, wie Shared-Memory-Zugriffe und Global Reads entworfen sind; genau dort setzt vllm mit der Layout-Änderung an

    • Die Analyse dieses Bottlenecks dauerte mehr als zwei Tage, war anhand der GPU-Auslastungsgrafen nicht erkennbar und wurde größtenteils nur durch Trial-and-Error verständlich

    • Es wird die Frage aufgeworfen, ob es eine Möglichkeit gibt, solche Experimente einfacher und iterativer per Hot Reload durchzuführen

    • Hinweis darauf, dass zwar gesagt wurde, die GPU sei nicht der Bottleneck, in Wirklichkeit aber die Ineffizienz des Speicherlayouts letztlich doch einen Bottleneck bei der Recheneffizienz der GPU verursachte

    • Erwähnung des gestern von einem DeepSeek-Mitarbeiter veröffentlichten Projekts nano-vllm; mit nur 1200 Zeilen soll es schneller sein als vanilla vllm https://github.com/GeeeekExplorer/nano-vllm

    • Frage, ob das geänderte Layout in llama.cpp als Pull Request eingereicht wurde; eine Verdopplung wäre für alle ein großer Gewinn

    • Empfehlung, auch das Projekt ik_llama.cpp auszuprobieren https://github.com/ikawrakow/ik_llama.cpp

  • Einschätzung, dass es sich um einen informativen Artikel handelt und dass der Inhalt eher davon handelt, welche Entscheidungen NVIDIA bei der Entwicklung der GPU-Architektur trifft; mit dem Hinweis, die Unterschiede zu anderen Anbietern nicht misszuverstehen

    • Zum Beispiel verschiebt sich beim AMD Instinct MI300 mit bis zu 160 TFLOPS bei FP32 und 6 TB/s HBM3/3E-Bandbreite der Ridge Point; das entspricht 27 FLOPs/Byte, also mehr als dem Doppelten des A100 mit 13 FLOPs/Byte. Der große HBM-Speicher (128–256 GB) verändert außerdem die realistischen Trade-offs zwischen Tiling-Tiefe und Occupancy. Solche GPUs sind allerdings teuer und bringen den Trade-off fehlender CUDA-Unterstützung mit sich

    • Meinung, dass NVIDIA-GPUs alternativlos sichtbar bleiben werden, solange AMD nicht deutlich mehr in Computing-Software investiert

  • Als Spoiler wird betont, dass letztlich nicht die Funktionsweise der GPU selbst entscheidend ist, sondern wie sie für Machine-Learning-Berechnungen genutzt wird

    • Der Inhalt sei im Grunde eher eine allgemeine Zusammenfassung von CUDA und habe abgesehen vom relu-Beispiel und der Erwähnung von torch nicht viel mit Machine Learning zu tun
  • Meinung, dass Kontrastfarben unbedingt verwendet werden sollten, mit Betonung auf Lesbarkeit

    • Erfahrungsbericht zur Nutzung von font-weight: 300: Viele Mac-Designer entwickeln mit Blick auf die Font-Smoothing-Optionen und stellen Dinge so ein, dass sie im Allgemeinen wie „normal“ wirken; Macs lassen dünne Schriften halbwegs dicker erscheinen, weshalb Designer oft dünnere Fonts wählen, um einen „normalen“ Eindruck zu erzeugen; dazu wird ein passender Link geteilt https://news.ycombinator.com/item?id=23553486

    • Vermutung, dass der Autor im Dark Mode editiert und formatiert hat; mit edge://flags/#enable-force-dark seien die Links gut sichtbar

    • Hinweis, dass insbesondere Links und Kommentare in Code-Blöcken beim Lesen besonders anstrengend waren; Vorschlag, den Kontrast zu erhöhen, bei zugleich sehr positiver Bewertung der Inhaltsqualität

    • Kritik daran, dass die Website Alpha-Transparenz für Text verwendet und damit den Kontrast massiv verschlechtert

  • Vorschlag, dass ein Titel wie „Grundlegende Fakten über Nvidia-GPUs“ eigentlich passender wäre; mit der Erläuterung, dass der Begriff WARP ein Merkmal moderner Nvidia-GPUs ist und Nvidia-GPUs um 2003 noch reine Hardware für Videospiel-Rendering waren, also völlig anders als heutige GPUs für General-Purpose-Computing; zusammengefasst sei der Beitrag daher keine allgemein auf alle GPUs anwendbare Erklärung

  • Dank dafür, dass es sich um sehr gutes Einstiegsmaterial handelt; Rückblick, dass beim Selbstbau eines AI-PCs mehrere Tage lang zu GPUs recherchiert wurde und dieser Text die unverzichtbaren Kernthemen und hochgradig wertschöpfenden Anwendungsfelder wie Generative AI sehr gut zusammenfasse und deshalb enorm geholfen habe; besonders das Diagramm zur Speicherhierarchie der A100-GPU sei sehr nützlich gewesen

  • Verwunderung über die Verwendung von ASCII-Diagrammen