Die Präsentation wird geladen. Bitte warten

Die Präsentation wird geladen. Bitte warten

< Best practices >

Ähnliche Präsentationen


Präsentation zum Thema: "< Best practices >"—  Präsentation transkript:

1 < Best practices >
CUDA < Best practices >

2 Compute Unified Device Architecture
Was ist CUDA? Hardware – Software Architektur Ermöglicht general-purpose computing auf einer GPU Wie funktioniert CUDA? GPU fungiert als Coprozessor für Haupt-CPU Bearbeitet alle datenparallelen und rechenintensiven Teile einer Anwendung Diese Programmteile (kernel genannt): werden in die Instruktionssprache der GPU übersetzt auf der GPU hochgeladen und ausgeführt Vorteile Frei erhältlich Leicht einzusteigen → ähnliche Syntax wie C Was ist CUDA? Hardware – Software Architektur die es ermöglicht general-purpose computing auf einer GPU auszuführen Wie funktioniert CUDA? GPU fungiert als Coprozessor für Haupt-CPU, die alle datenparallelen und rechenintensiven Teile der Anwendung übernimmt Diese Programmteile (kernel genannt) werden in die Instruktionssprache der GPU übersetzt und auf die GPU hochgeladen und ausgeführt Kernel: ein Teil einer Anwendung der sehr oft ausgeführt wird aber unabhängig auf verschiedene Daten operiert.

3 Performance - Vergleich
CPU vs. GPU: Rechenkapazität: 367 GFLOPS vs. 32 GFLOPS Memory Bandwidth: 86.4 GB/s vs. 8.4 GB/s Der Hauptgrund hinter einer solchen Entwicklung ist, dass die GPU für rechenintensive Bearbeitung im Grafikbereich ausgelegt war (also gezielter Anwendungsbereich), und nicht wie die CPU für den general purpose Bereich entwickelt wurde Floating-Point Operationen pro Sekunde für eine CPU und eine GPU.

4 Struktur – Vergleich (Programmers view)
Control Cache 16 Multiprozessoren (SIMD Einheiten) 8 X 32 Bit SP Im Grafikrendering geht es um paralleles Rechnen, und deshalb ist die GPU so konzipiert, dass mehr Transistoren der Datenverarbeitung dediziert sind und weniger für Daten-Caching und Flusskontrolle benutzt werden. Die GPU ist geeignet für Algorithmen die massiv parallelisiert werden können, d.h. nicht nur für graphische Applikationen sondern auch für z.B. Digitale Signalverarbeitung, physikalische Simulationen u.a. Aktuelle GPUs haben ein Set von 16 Multiprozessoren - SIMD Einheiten(Single Instruction Multiple Data) die jeweils aus 8 – 32 Bit Streaming - Prozessoren bestehen => 128 Prozessoren 2 solche Multiprozessoren werden zu einem Shader – Cluster zusammengefügt. => 8 Shader – Cluster Aus Programmierer Sicht sieht einer der 16 Multiprozessoren so aus: Wie gesagt besteht es ausmehrere Streaming - Prozessoren Jeder Streaming – Prozessor kann auf Shared Memory zugreifen um Daten mit anderen Streaming Prozessoren austauschen zu können. Die Daten aus dem Texture Memory und Constant Memory (die sich in dem Device Memory befinden) werden gecached → beim Wiederverwendung der gleichen Daten kann dieser Streaming Prozessor direkt auf dem jeweiligen cache zugreifen, anstatt unter der großen Latenz des Speichers zu leiden.

5 Programmiermodell Host = CPU Device = GPU Die CPU Jeder Kernel Grid
leitet Kernel-Aufrufen an die GPU Jeder Kernel wird von mehrere Threads bearbeitet die in einem Gitternetz von Thread-Blöcken organisiert sind Thread Gleichzeitig ausgeführter Code (parallel mit andere Threads) Kosten niedriger als CPU – Threads (Erzeugungskosten, Ressourcenverbrauch, umschalten zw. den verschiedenen Threads der GPU sind viel kleiner) Warp Gruppe von Threads Physisch parallele Ausführung Wird eigentlich von dem Programmierer nicht gesehen. Thread Block Threads die auf einen einzigen Multiprozessor ausgeführt werden Threads können miteinander kooperieren Synchronisation ihrer Abläufe Datenaustausch durch den Shared Memory Threads aus verschiedenen Blöcke können nicht miteinander kooperieren: keine Synchronisationsfunktion vorhanden Datenaustausch nur durch den Globalen Speicher Möglich. Grid Gruppe von Thread – Blocks Führen einen einzigen CUDA – Programm (Kernel) aus Laufen logisch in parallel Execution Model: Jeder Block wird auf einen Multiprozessor ausgeführt s.d. er Gebrauch von der on chip shared memory machen kann. Ein Multiprozessor kann mehrere Blocks gleichzeitig ausführen. Die shared Memory und die Register werden zwischen den Threads aller gleichzeitig laufende threads verteilt.

6 Anwendungsbeispiel Matrix – Multiplikation
Thread – Blöcke laden Asub und Bsub jeder Thread 1 Element Thread – Block → Submatrix Csub 1 Thread → eine Multiplikation Beispiel (Matrix Multiplikation): Ein Beispiel damit man ein Gefühl kriegt wie das Ganze auf ein reales Problem läuft. Angenommen wir haben A*B = C Diese Matrizen werden in Blöcke der Größe BlockSize eingeteilt. Die Blöcke aus A und B haben als Aufgabe die Elemente der Submatrix Asub bzw. die Elemente der Matrix Bsub aus dem globalen Speicher in dem Shared Memory zu holen. Jeder Thread aus so einem Block ladet einen einzigen Element in der Shared Memory. Die Blöcke aus C haben die Aufgabe zwei Untermatrizen aus A und B zusammen zu multiplizieren und sie zu speichern. Jeder Thread aus einem Block führt eine einzige Multiplikation zwischen 2 Elemente (einer aus Asub und einer aus Bsub) aus.

7 Best practices < CUDA>

8 Algorithmus für die GPU optimieren
Daten dürfen nicht von einander abhängig sein Maximierung der arithmetischen Intensität (math/bandwidth) möglichst viel auf den schon geladenen Daten arbeiten Daten aus dem Speicher holen ist teuer Auf der GPU arbeiten Strukturen auf Device anlegen, benutzen und löschen Transfer von Daten zwischen Device und Host vermeiden Schwächer parallelisierte Berechnungen können schneller sein als stark parallelisierte Berechnungen die immer zwischen Device und Host Daten transferieren. Unabängige Daten: Daten müssen unabhängig von anderen Daten modifizierbar sein, also keine Abhängigkeiten zwischen den Daten Arithmetische Intensität: Daten auf dem Speicher holen ist teuer und um den Durchsatz zu verbessern soll möglichst viel auf den existierenden Daten (schon geladene Daten) gerechnet werden.

9 Parallelität ausnutzen
Arbeit so einteilen dass die HW voll ausgelastet ist Min. so viele Blöcke wie die GPU Multiprozessoren (SIMD - Einheit) hat verwenden Anzahl der Threads pro Block als Vielfaches der Warp-Size wählen besser noch: ein Vielfaches von 64 (laut nVidia) Je mehr Threads pro Block desto besser Vorsicht: je mehr Threads je weniger Register pro Thread verfügbar (gleich Beispiel) Konfigurationen mit 100% Auslastung eines Multiprozessors Ein Multiprozessor kann bis zu 768 Threads bearbeiten 2 Blocks x 384 Threads 3 Blocks x 256 Threads 4 Blocks x 192 Threads 6 Blocks x 128 Threads 8 Blocks x 96 Threads Nummer der Blöcke: Das führt aber dazu dass der Multiprozessor in IDLE Zustand wechselt, sobald ein synchronize bzw. ein Speicherzugriff auftritt, wenn dieser Block nicht über genügend Threads verfügt, die eingelastet werden können um diese Latenz zu verbergen. Deswegen wäre es besser mehrere Blöcke pro Multiprozessor einzuplanen um zwischen wartenden Blöcke bzw. Laufbereite Blöke schnell wechseln zu können. Die Nummer der Blöcke pro Grid sollte wenigstens 100 sein wenn man seine Anwendung auch für zukünftige GPUs gestalten will. 1000 Blöcke werden für ein paar Generationen ausreichen. Nummer der Threads: Warum Vielfaches von Warp-Size? Angenommen Warp-Size ist 32. So eine Warp wird von 8 Prozessoren bearbeitet * 4 Zyklen = 32 bearbeitete Threads = 1 bearbeite Warp. Wenn man jetzt eine Warp unterbelegt dan arbeiten ain paar der Prozessoren der SIMD nicht >> Ressourcenverschwendung Warum Vielfaches von 64? Der Compiler und der Thread – Scheduler versuchen die Instruktionen so gut wie möglich einzuplanen um Register Bank Konflikte zu vermeiden. Sie liefern laut nVidia die besten Ergebnisse wenn die Anzahl der Threads per Block ein Vielfaches von 64 sind. Außer diese Regel zu beachten, hat die Applikation keinen direkten Einfluß auf diese Bank Konflikte. Je mehr Threads per block desto besser? Weil wenn z.B. ein paar threads neue Daten aus dem globalen Speicher brauchen dann starten sie ein request und nach 400 – 600 Zyklen antwortet der Speicehr. In dieser Wartezeit könnten schon andere Threads eingalastet werden und arbeiten. Konfiguration: Um 100% Auslastung eines Multiprozessors zu erreichen muss mann 768 Threads einplannen.

10 “Performance Clip“ Scenario: Lösung: 256 Threads/Block
3 Blocks pro SIMD - Einheit → 768 Threads (100%) Wir benutzen 11 Register pro Thread → 8448 Register G80 nur 8192 Register vorhanden Kernel kann nicht gestartet werden Lösung: Code so ändern dass es nur noch 10 Register pro Threads braucht → 7680 Register notwendig → Kernel läuft Warum funktioniert es nicht? Ich habe doch alles gut programmiert? Weil man die Architektur auch beim programmieren immer im Hinterkopf haben muss.

11 Flow Control – Branch Diverging
Jede Kontroll – Instruktion (if, switch, do, for, while) kann den Instruktionsdurchsatz wesentlich beeinflüssen Threads nehmen verschiedene Ausführungspfade Diese verschiedene Ausführungspfade werden dann serialisiert Ein allgemeiner Fall: Divergenz vermeiden, wenn Verzweigungsbedingung eine Funktion der Thread-ID ist Beispiel mit Divergenz: if(threadIdx.x > 2){//impl.} else{//impl.} Erzeugt zwei Ausführungspfade für Threads aus einem Block Verzweigungsgranularität < Warp Size; d.h. Threads 0 und 1 folgen einen anderen Pfad als die anderen Threads aus dem ersten Warp Beispiel ohne Divergenz: if(threadIdx.x / WARP_SIZE > 2){//impl.} else{//impl} Erzeugt auch zwei Ausführungspfade für den Threads aus einem Block Verzweigungsgranularität aber Vielfach der Warp Size; d.h. alle Threads in einem Warp folgen dem gleichen Pfad. Jede Kontroll – Instruktion (if, switch, do, for, while) kann den Instruktionsdurchsatz wesentlich beeinfußen, indem sie Threads aus denselben Warp zum Divergieren zwingen, d.h. sie zwingen die Threads verschiedene Ausführungspfade zu nehmen. Wenn das passiert dann müssen diese verschiedene Ausführungspfade serialisiert werden. Zuerst wird ein Ausführungspfad durchgearbeitet, dann das andere und am Ende konvergieren alle Threads zu denselben Ausführungspfad. Beispiel mit Divergenz: Unsere Multiprozessoren sind SIMD d.h Sie können nur eine Instruktion auf einmal bearbeiten und nicht zwei. Da aber hier ein Branching entstanden ist muss er zuerst in die if Abfrage eingehen und für 2 Threads alles bearbeiten, dann kehrt er zurück und bearbeitet den anderen Zweig und danach geht’s weiter mit alle. Beispiel ohne Divergenz Warp size kann zb auf einen Multiprozessor echt parallel bearbeitet werden. Wenn ich jetzt die Granularität der Verzweigungsbedingung auf die Warp ebene jetzt beziehe, dann läuft ein ganzer warp durch if oder durch else. z.B. Ein Warp läuft durch if und kann danach direkt in Rente gehen weil alle Threads bearbeitet wurden. Beim Branching würde der Prozessor zuerst ein paar der Threds bearbeiten dann den nächsten Teil und erst danach wird dieser Warp als bearbeitet abgehackt und in Rente gesetzt.

12 Speichernutzung Host Memory  maximale Latenz (> 600 Zyklen)
Transfer minimieren Temporäre Datenstrukturen im Device abspeichern Gruppentransfer schneller als viele Kleine Datenaustausch über high-performance DMA vom device Global Memory  hohe Latenz (400 – 600 Zyklen) Zugriff minimieren Typisches Vorgehen: Lade Daten aus DRAM in Shared Memory Synchronisiere Threads zum sicheren Lesen Bearbeite Daten im Shared Memory Synchronisiere Threads zum sicheren Zurückschreiben Schreibe Daten zurück in DRAM Latenz teilweise versteckt durch Thread-Scheduler Shared Memory  minimale Latenz (1 Zyklus) Hunderte mal schneller als Global Memory Threads desselben Blocks können kommunizieren - Daten zwischen Device und Host auszutauschen kostet am meisten Datenaustausch erfolgt über die high-performance DMA engine vom Device (Zugriff über spezielle API-Aufrufe)  ist schon schneller, als die DMA von der CPU optimales Ergebnis erziehlt man wenn man page-locked memory verwendet trotz hoher Bandbreite von 86,4 GB/s device memory latency sehr hoch im Vergleich zur latency der on-chip-Speicherbereiche Takte vs. 1-2 Takte (shared memory hunderte mal scheller als device memory) trotzdem Zugriff auf externen Speicher notwendig, um Daten zu bekommen deshalb devise: device memory Zugriff minimieren: soviel, wie nötig aber so wenig, wie möglich viel dieser global memory latency kann versteckt werden durch den thread scheduler, der die warps der aktiven Blöcke switched  deshalb ist es wichtig, damit der scheduler auch was zu tun hat, dass viele Threads pro Multiprozessor aktiv sind! effektive Bandbreite jedes memory spaces hängt massiv vom Zugriffsmuster ab! Local Memory  400 – 600 Zyklen Nicht gecached! Am Besten gar nicht verwenden Constant Memory  1 Zyklus bei Cache Hit, sonst wie DRAM Cached! Sehr effizient bei nur lesenden Zugriffen Zugriff auf verschiedene Adressen  Latenz steigt linear Texture Memory  1 Zyklus bei Cache Hit, sonst wie DRAM Auch nur lesbar Optimiert für 2D Speichereinheiten, hat spezielle Struktur Register  minimale Latenz (1 Zyklus) Beschränkt

13 Global Memory - Coalescing (vereinigt)
Koordiniertes Lesen durch einen Warp (32 Threads) Aufeinanderfolgende Speicherbereiche: 128 byte – jeder Thread liest ein Wort: int, float 256 byte – jeder Thread liest ein Doppel-Wort: int2, float2 512 byte – jeder Thread liest ein Vierfach-Wort: int4, float4 Einschränkungen: Startadresse eines Bereichs muss Vielfaches der Größe dieses Bereiches sein Der k-te Thread in dem Warp muss auf das k-te Element in dem gelesenen Speicherbereich zugreifen nicht alle Threads müssen am Schreib-/Lesevorgang teilnehmen Gewinn: das bis zu Fache - Global memory nicht gecached, deshalb ist es wichtig dem richtigen Zugriffmuster zu folgen, Um maximale Bandbreiten zu erreichen Device ist in der Lage 32, 64, 128 bit Wörter aus dem Hauptspeicher in nur einer Instruktion zu laden (4 Byte, 8 Byte, 16 Byte), Warp-size: 32 Threads Wenn alle Threads in einem Warp aus beliebigen Stellen des Hauptspeichers lesen, dauert dieser Zugriff sehr lange -built-in vector types erhalte ich durch: int2 make_int2(int x, int y)

14 Coalesced Access (Reading floats)

15 Uncoalesced Access (Reading floats)

16 Shared Memory - Bankkonflikte
16 KB organisiert in 16 Banken je 1 KB ist genauso schnell wie Register falls keine Bank Konflikte existieren Bank Konflikt: mehrere Threads in der gleichen Halb – Warp greifen auf der gleiche Bank zu Zugriffe müssen serialisiert werden → Parallelität geht verloren. Kosten = max (# der gleichzeitigen Zugriffe) Threads aus zwei Halfwarps können keinen Bankkonflikt generieren

17 Shared Memory - Keine Bankkonflikte
Lineare Adressierung Schrittweite = 1 Wort Zufällige Permutation Schrittweite = 3 Wörter Broadcast Broadcast: wenn alle Threads auf die gleiche Adresse zugreifen wollen dann wird ein broadcast (von der Hardware unterstützt) ausgeführt, und alle bekommen ihre Daten → kein Bankkonflikt

18 Shared Memory - Bankkonflikte
Lineare Adressierung Schrittweite = 2 Wörter Schrittweite = 8 Wörter kein Konflikt oder 5 Wege Konflikt Lineare Adressierung mit der Schrittweite von zwei 32-Bit-Worten verursacht einen 2-Wege-Bank Konflikt. Serialisierungsgrad 2 Lineare Adressierung mit der Schrittweite von acht 32-Bit-Worten verursacht einen 8-Wege-Bank Konflikt. Serialisierungsgrad 8

19 Lösung Um auf einen 32 Bit Array-Eintrag zuzugreifen wird meistens folgende Syntax benutzt: __shared__ float shared[32]; float data = shared[BaseIndex + s * tid]; Wenn die Schrittweite s eine ungerade Zahl ist werden keine Bankkonflikte entstehen Zugriff auf Elemente kleiner als 32 Bits: Konflikt: __shared__ char shared[32]; char data = shared[BaseIndex + tid]; Kein Konflikt: char data = shared[BaseIndex + 4 * tid]; Lösung: Schrittweite muss eine ungerade Zahl (weil wir eine gerade Anzahl von Banken haben)sein. Wenn die Threads aus verschiedenen Halb-Warps kommen können sie keinen Bankkonflikt erzeugen. Warum können Bankkonflikte zwischen Threads aus verschiedenen Halb-Warps nicht entstehen? Weil ein Shared Memory Request für eine Warp in zwei Teile geteilt wird, eine Speicheranforderung für die erste Hälfte eines Warps und eine Speicheranforderung für die zweite Hälfte eines Warps. Zugriff auf Elemente kleiner als 32 Bits: das erste Beispiel erzeugt einen Bankkonflikt weil shared[0], shared[1], shared[2] und shared[3] sich in der gleichen Bank befindet. ??? Warum gibt es keinen Konflikt bei BaseIndex + 4*tid????????

20 Lösung 2 Eine Struktur wird in so viele Memory – Request kompiliert wie es Elemente enthält. Also wird Folgender Code: __shared__ struct type shared[32]; struct type data = shared[BaseIndex + tid]; Drei separate Speicherzugriffe ohne BK. wenn type so definiert ist: struct type{ float x, y, z; }; Zwei separate Speicherzugriffe mit BK. wenn type so definiert ist: struct type{ float x, y; }; oder struct type{ float f; char c; }; Drei speicherzugriffe ohne Konflikt: Weil auf jedem Eintrag der Schrittweite 3 und die Wortbreite 32 Bits zugegriffen wird Zwei speicherzugriffe mit Konflikt: 1. Weil auf jedem Eintrag der Schrittweite 2 und die Wortbreite 32 Bits zugegriffen wird 2. Weil auf jedem Eintrag mit einer Schritweite von 5 Bytes zugegriffen wird

21 Debugging CUDA Debuggen → schwer Auf der CPU debuggen durch emulation
nvcc –deviceemu (oder linux-Makefile) man kann alle Funktionen die in C sind aufrufen, sogar in dem Device Code der Compiler erkennt Deadlocks Valgrind benutzen um Speicherzugriffsfehler zu finden Kann lange dauern Keine Speicherschutzfunktion auf der GPU Was ist der Device Code: der Code der auf die GPU hochgeladen wird.

22 Zusammenfassung Parallele Ausführung maximieren
Algorithmus datenparallel strukturieren Geeignete Konfiguration für maximale Auslastung wählen Instruktionsnutzung optimieren Ziel: maximaler Durchsatz 367 GFLOPS Intrinsic Funktionen statt regulärer Funktionen Single Precision statt Double Precision Divergenz bei Flow Control vermeiden Speicherzugriff optimieren Ziel: maximale Bandbreite 86,4 GB/s Transfer zw. Host u. Device minimieren Global Memory Zugriff (coalscaled) Shared Memory Zugriff (Bankkonflikte) Trade-Off zw. Genauigkeit und Geschwindigkeit - Aus Division mit 2er Potenz eine Shift-Operation machen (optimiert der Compiler zwar eh raus, aber besser ist es trotzdem) - (i/n)  (i>>log2(n)) - (i%n)  (i&(n-1)) - sin(x) – double precision - sinf(x) – single precision - __sinf(x) – instrinsic function unterschiedliche Kontrollpfade werden serialisiert lieber einen großen Datenblock vom Host holen als viele kleine temporäre Datenstrukturen direkt auf dem Device abspeichern, nicht erst rüber zum Host laden!

23 ?


Herunterladen ppt "< Best practices >"

Ähnliche Präsentationen


Google-Anzeigen