10. Frameworks im Überblick
C ++ 11
C ++
C
Device
Framework
CUDA
Runtime
C++
AMP
DirectX
AMDTreiber
TDD
WDDM
Thrust
C++-
Wrapper
Library
OpenCL
Bolt
Intel
AMD
CUDA
Driver
11. Vor- und Nachteile
CUDA
+ Verbreitung + C++ 11 - nur NVIDIA
C++ AMP
+ C++ 11 - DirectX - geringe Verbreitung
OpenCL
+ Offen - C99 - Nicht viele Libraries
12. Speedup
Schnelle erste Erfolge
2x – 5x Speedup
Dann wird es schwieriger …
Oft sind 10x drin
2 3 4 5 6 7 8 9 10 11 …
„Enabler“
17. Aber: Gleiche Taktfrequenz
Seit 2005 keine wesentlichen Fortschritte
Gründe: Abwärme, Energieverbrauch, Größe
18. The Free Lunch is over
Automatischer Fortschritt
Mehr Transistoren, höhere Taktfrequenz
“Abwarten” bei Performance-Problemen
Aber
“The Free Lunch Is Over: A Fundamental Turn
Toward Concurrency in Software” 2005
Siehe http://www.gotw.ca/publications/concurrency-ddj.htm
19. Concurrency vs. Parallelism
Concurrency = Nebenläufigkeit
Verschiedene Programme
Unterschiedliche Aufgaben
Parallelism = Parallelität
Das gleiche Programm in Teile zerlegt
Gleiche Aufgabe
20. Aufgabe: Parallelität finden
Erfordert Übung, wird mit der Zeit einfacher
Zerlege einen Algorithmus
In Teile, die unabhängig voneinander
berechnet werden können
Minimiere die Abhängigkeiten von Teilen
var
21. Definition „unabhängig“
Zwei Programmteile sind „unabhängig“,
wenn sie parallel ausgeführt werden
können
und die „Semantik“ des Programms gleich
bleibt
39. 2d-map
Z. B. in Bildverarbeitung
Wo sind die Performance-Probleme?
for y = 0 to n
for x = 0 to n
int i = y*w+x
a[i] = f(e[i]);
40. Arithmetische Intensität
Verhältnis Rechnungen zu Speicherzugriffen
𝑎𝑖 =
𝑅𝑒𝑐ℎ𝑒𝑛𝑜𝑝𝑒𝑟𝑎𝑡𝑖𝑜𝑛𝑒𝑛
𝑆𝑝𝑒𝑖𝑐ℎ𝑒𝑟𝑧𝑢𝑔𝑟𝑖𝑓𝑓𝑒
Für jeden Speicherzugriff sollte genügend zu
rechnen da sein
41. CPU Aufbau
CPU 1
Core
L1 / L2
Core
L1 / L2
L3 Cache
Memory Controller PCIe
Global Memory Devices
CPU 2
Core
L1 / L2
Core
L1 / L2
L3 Cache
QPI
Memory ControllerPCIe
Global MemoryDevices
≈25 <14
<26
Der Bus ist der „Von-Neumann-
Flaschenhals“ (1977)
≈50
≈100
42. Cache
Darum ist Cache bei CPUs wichtig
Benötigt viel Platz
http://download.intel.com/pressroom/kits/corei7/images/Nehalem_Die_callout.jpg
43. Cache
Der Cache speichert „lines“ von 64 Bytes
Für x = 0 wird auch x=1 mitgelesen
for y = 0 to n
for x = 0 to n
int i = y*w+x
a[i] = f(e[i]);
48. OpenMP funktioniert, wenn …
Daten-paralleles Problem, wie map
Keine abhängigen Schleifen
Code muss ansonsten umgeformt werden
Benutzt gleiches Memory-Model wie CPU
Daten liegen im gleichen Speicher (NUMA)
49. OpenMP und ungleiche Last
Wenn Rechenlast ungleich verteilt ist
#pragma omp parallel for schedule(dynamic, 1)
Ungleich
52. Scheduling
Was macht OpenMP, wenn ein Kern schon
etwas anderes tut?
Homogen (Gleiche Prozessoren)
Gleiche Arbeit
Heterogen (Unterschiedliche Prozessoren)
Messen und gewichten
58. Anzahl der Kerne
Unterschiedliche Anzahl
GeForce 1080
2560 Kerne in 40 SMs
Geforce 1060, 6 MB
1280 Kerne in 20 SMs
59. Tesla für Rechenzentren
ECC Speicher
Größerer Speicher
Bessere Double-Performance
Kein Monitor-Anschluss
Keine Kühlung
60. Multi-GPU
Auf normalen Motherboards 2 – 3
Achtung
Auslastung PCIe
Spezial-Boards
Bis zu 16 GPU pro Knoten
Abhängig von
Mainboard, CPU, Chipsatz, PCIe-Controller
61. Aufbau der GPU
CPU 1
Core
L1 / L2
Core
L1 / L2
L3 Cache
Memory Controller PCIe
Global Memory Devices
≈25
<14
≈50
≈100
<14
GPU
Global Memory
Constant Texture
Prozessor (SM)
Shared / L1
Registers
C
L2 Cache
C
C
C
C
C
C
C
C
C
C
C
C
C
SM
SM
SM
SM
SM
SM
SM
SM
SM
SM
>8000
>1600
320 -
700
Device-Speicher << Host-Speicher
87. Runtime vs. Driver API
CUDA Driver API
Low level, C, ähnlich OpenCL 1.x
CUDA Runtime API
Higher Level, C++
Thrust
Ähnlich STL-Bibliothek
96. Bestimmung der ID
threadIdx innerhalb Block
blockIdx innerhalb Grid
blockDim Größe des Grids
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
0
1
2
3
4
5
6
7
0 1
2 3
Daten, Pixel, Voxel Grid
97. Viele, viele Threads
Block & Grid ersetzt die for-Schleifen
Ein dim3(x, y, z) Block besteht aus x*y*z
Threads
Ein dim3(X, Y, Z) Grid besteht aus X*Y*Z
Blöcken
Insgesamt X*Y*Z*x*y*z Threads
99. Stand der Dinge
Wir haben
Kernel
Aufruf des Kernels
Wir benötigen
Speicher auf dem Device
GPU
Global Memory
Constant Texture
Prozessor (SM)
Shared / L1
Registers
C
L2 Cache
C
C
C
C
C
C
C
C
C
C
C
C
C
SM
SM
SM
SM
SM
SM
SM
SM
SM
SM
101. Unified Memory
cudaMallocManaged()
Speicher wird auf Host und Device angelegt
Ab CUDA 8.0 und Pascal-Karte
Beliebige Größe (Swapping automatisch)
Vorher ab CC 3.0
Muss in Device-Speicher passen
102. … und Pascal-Karte ?
Große Unterschiede zwischen Karten-
Generationen: Pascal, Maxwell, Kepler, Fermi
Compute Capability
Zahl Major.Minor, z. B. 3.0, 5.2, 6.0
Kann mit „deviceQuery“ ermittelt werden
Siehe http://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#compute-capabilities
103. Compute Capability (CC)
Beispiel
Es gibt mehr,
Das sind nicht alle
Aus https://devblogs.nvidia.com/parallelforall/inside-pascal/
105. CC und Code
Problem
Für welche CC schreibt man den Code?
Für die neueste? Das ist am einfachsten!
Aber Kunden mit älteren Karten?
Maintenance-Probleme mit Versionen?
106. Explizit auch möglich
Anlegen auf Host und Device
cudaMallocHost(&ptr, size_t)
cudaMalloc(&ptr, size_t)
Kopieren
cudaMemcpy(dst, src, sz, dir)
dir: H2H, H2D, D2H, D2D
Freigabe mit cudaFree(ptr)
110. Teil 7: nvcc und IDEs
Kompilierung
Nvcc
Visual Studio
111. Compilieren mit nvcc
Präprozessor
Teile Host- und Device-Code
Host-Code wird mit C++ kompiliert
Device-Code wird zu PTX kompiliert
(Assembler-Sprache)
Aus PTX wird CUBIN-Binärformat
113. PTX und Cubin
PTX ist eine Assembler-Sprache
Cubin ist für eine CC kompiliertes PTX
Kompilierung
Offline: mit ptxas
Online: durch Treiber (just-in-time, JIT)
114. Nvcc erzeugt Exe‘s
Exe nur mit CUBIN
Kleine Datei, aber nur für bestimmte CCs
Exe mit PTX
--fatbin (default)
JIT für alle CCs, auch in Zukunft
PTX sollte in Exe inkludiert werden
131. Algorithmus
For all pixel at x,y in image
sum = 0, c = 0
For all dx,dy in neighborhood
sum += vol[x+dx, y+dy]
c += 1
vol‘[x,y] = sum / c
134. Beispieldaten
Aus didaktischen Gründen vereinfacht
In Float
Mit RGB analog
Nur Mittelwert, keine gewichtete Summe
Mit Array von Gewichten analog
141. Windows-Treiber
WDDM (Windows Display Driver Model)
Der normale Treiber, auch für Spieler
Kernel wird nach max. 2 Sekunden
abgebrochen von Windows
TCC (nur für Tesla-Karten oder Linux)
Kein Display, keine Beschränkung
142. Ändern des Defaults
Siehe https://msdn.microsoft.com/en-us/library/ff569918(v=vs.85).aspx
danach Reboot
143. Vergleich der Laufzeiten
Das sieht für die GPU nicht gut aus.
Was ist da los?
i7 5930K 4 GHz GTX 1080
w h 1-Thread 12 Threads 128 Block
32768 4096 1908 261 624
32768 12288 5697 772 1853
32768 28672 13283 1761 4279
7,31 3,06 0,42
7,38 3,07 0,42
7,54 3,10 0,41
Speedup
144. Keine Panik!
Für GPU-Einsteiger normal
Hintergrundwissen erforderlich
GPU darf keine Black-Box mehr sein
Keine automatische Code-Optimierung
Wissen über GPUs erforderlich
145. Teil 9: CUDA genauer betrachtet
NVVP
Speicher
Kernel und Warps
Occupancy
146. Profiling allgemein
Nur „Release“-Code profilen
„Debug“-Code ist nicht repräsentativ
Code wird u. U. sehr oft ausgeführt
Keine großen Beispiele verwenden
Auch nicht zu klein
155. Arten der Auslastung
Computation Bound
Alle Prozessoren 100% ausgelastet
Memory Bound
Bandbreite zum Speicher voll ausgelastet
Latency Bound
Warten auf die Daten
161. Lösung
cudaMallocManaged
„black box“
„Schlechtes“ Speichermanagement
Oder Heuristiken für diesen Fall schlecht
→ Nur mit „kleineren“ Buffern arbeiten
172. Kernel-Aufruf
Hat eine SM freie Kapazitäten, wird ihr ein
Block zugewiesen
Der Block wird in Warps unterteilt
Ein Warp besteht aus 32 Threads
Kleinste Einheit des Schedulers
173. Warp = 32 Threads
Ähnlich Vektorrechner
Single Instruction Multiple Threads!
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
t t t t t t t t t t t t t t t t t t t t t t t t t t t t t t t t
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
f f f f f f f f f f f f f f f f f f f f f f f f f f f f f f f f
174. Divergenz
Single Instruction Multiple Threads!
Mask-Bit für jeden Thread im Warp
Laufzeit
𝑇 = 𝑇𝑖𝑓 + 𝑇𝑒𝑙𝑠𝑒
Ansonsten (Single-Thread)
𝑇 = max(𝑇𝑖𝑓, 𝑇𝑒𝑙𝑠𝑒)
Die anderen warten …
0 1 2 3
int tid = treadIdx.x;
if (tid < 2) {
call_expensive_function()
} else {
call_expensive_function2()
}
Threads Code
175. Auswirkung von Divergenz
Wenn divergente Threads vorhanden sind
Kerne sind idle
Wird SM nicht ausgelastet
Berechnung dauert länger als notwendig
Daher Divergenz vermeiden
176. Warp-Scheduling
Hardware GPU
n SM mit je k Kernen
Software Kernel-Konfiguration
Block-Größe bestimmt Anzahl der Threads
Grid bestimmt Anzahl der Blöcke
Wie kann man GPU möglichst gut auslasten?
179. Latency Hiding
Speicherzugriffe sind langsam
Durch Berechnungen „überdecken“
Immer genügend Warp „in flight“ haben
DER Grund für gute Performance der GPU
W0SM* W1 W2 W3 W4 -- -- --
180. Warum nicht immer 100%
Platz auf SM beschränkt
Warps auf SM teilen sich
Shared-Memory
L1-Cache
Register-Speicher
GPU
Global Memory
Constant Texture
Prozessor (SM)
Shared / L1
Registers
C
L2 Cache
C
C
C
C
C
C
C
C
C
C
C
C
C
SM
SM
SM
SM
SM
SM
SM
SM
SM
SM
181. Warum nicht immer 100%
Wenn
Kernel sehr viel Register für Variablen
benötigt
oder viel Shared-Memory benutzt
dann passen nur weniger „drauf“
Muss für jeden Kernel bestimmt werden
182. Occupancy ist komplex
Berücksichtige (Werte für CC 3.5)
Max Threads pro Block 1024
Max Blocks pro SM 16
Max Warps pro SM 64
Max Threads pro SM 2048
Anzahl 32-bit Register pro SM 64K
Max Anzahl Register pro Thread 255
Max Shared Mem pro SM 48 KB
Max Shared Mem pro Block 48 KB
185. Arten von Host-Speicher
malloc() „unpinned“
D.h. auslagerbar in den virtuellen Speicher
cudaMallocHost() „pinned“
Virtueller
Speicher
Physikalischer
Speicher
malloc() „pinned“
Device
Speicher
knapp
186. Unterschied CPU vs. GPU
Erstellung eines Threads
Auf CPU „teuer“
Auf GPU „billig“
Occupancy / Over-subscription
Divergenz
Speicher „coalescing“
192. Was optimieren?
Ermittle das Bottleneck mit Profiler
Verwende Amdahls Gesetz für Abschätzung
Siehe https://devblogs.nvidia.com/parallelforall/cuda-8-features-revealed/
195. Parallelität
CPU 1
Core
L1 / L2
Core
L1 / L2
L3 Cache
Memory Controller PCIe
Global Memory Devices
GPU
Global Memory
Constant Texture
Prozessor (SM)
Shared / L1
Registers
C
L2 Cache
C
C
C
C
C
C
C
C
C
C
C
C
C
SM
SM
SM
SM
SM
SM
SM
SM
SM
SM
CPU || GPU
Cores
Calc || Mem
SMs
Cores
HT
Kernel || Copy
198. Swapping & Streaming
Parallel
Für alle „ungeraden“ Partitionen p
Kopiere H2D für p in Stream0
Rufe Kernel auf für p in Stream0
Kopiere D2H für p in Stream0
Für alle „geraden“ Partitionen q
Kopiere H2D für q in Stream1
Rufe Kernel auf für q in Stream1
Kopiere D2H für q in Stream1
201. Streams mit CUDA
Create src_h, dest_h buffers on host
Create s streams
Create s src_d, dest_s buffers on device
Put all commands into stream
Wait for stream to finish
Destroy everything …
202. Schleife durch Partitionen
For all partitions p
let s = current stream
Copy async src_h[p] -> src_d[s]
Kernel async (src_d[s], dest_d[s])
Copy async dest_d[s] -> dest[p]
s = (s+1) mod num_streams
204. Tipp
Vorher immer eine synchrone Version
erstellen
100% funktionstüchtig und korrekt
Dann die Synchronisation umsetzen
Asynchronizität ist verwirrend
210. Speicherzugriffe in einem Warp
dim3(32,1,1)
32*float Werte „nebeneinander“
Ein „coalesced“ Zugriff á 128 Byte
dim3(4,8,1)
4*float Werte in 8 Reihen
8 Zugriffe á 16 Byte
211. Speicherzugriffe in einem Warp
GPU kann Speicherzugriffe innerhalb eines
Warps bündeln („coalescing“)
32, 64 und 128 Byte
Auch Alignment beachten
212. Speicherzugriffe in einem Warp
Also immer „neben“ zugreifen
Nicht „hintereinander“ (wie auf der CPU)
for x .. for y … statt for y … for x
Gut für CPU Gut für GPU
213. AoS vs. SoA
Array of Structures vs. Structure of Arrays
Siehe Stroustrup, „C++ Prog. Language“, 4th ed., p. 207
mem 0 1 2 3 4 5
AoS value 1 2 3 4 5 6
SoA value 1 3 5 2 4 6
214. AoS
mem 0 1 2 3 4 5
AoS value 1 2 3 4 5 6
SoA value 1 3 5 2 4 6
0 1 20 1 2
Verschwenden Bandbreite
215. SoA
mem 0 1 2 3 4 5
AoS value 1 2 3 4 5 6
SoA value 1 3 5 2 4 6
0 1 2 0 1 2
Nebeneinander gut
Aber Alignment = 3
219. Maximiere Speicherdurchsatz
Benutze On-Chip-Speicher
Register
Shared-Memory
Programmierbarer Cache
Nicht für Anfänger
Häufig ist L1 gut genug
GPU
Global Memory
Constant Texture
Prozessor (SM)
Shared / L1
Registers
C
L2 Cache
C
C
C
C
C
C
C
C
C
C
C
C
C
SM
SM
SM
SM
SM
SM
SM
SM
SM
SM
221. Berechnungsdurchsatz
Minimiere Divergenz
Berechnen statt Speichern
„do not repeat yourself“ (DRY )
const int i = y*w + x
Aber besser ‚inlinen‘ oder Makro
Fusion von Kerneln
222. Loop-Unrolling
Interne Parallelität in Kern /Core ausnutzen
Ausfalten von Schleifen fixer Länge
Siehe http://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#pragma-unroll
223. Fast Math
Arithmetik
Präzision vs. Geschwindigkeit
Spezielle Funktionen verfügbar
Siehe http://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#maximize-instruction-throughput
226. Gleitkommaarithmetik
Ergebnisse von Prozessor zu Prozessor
unterschiedlich!
Unterschiedliche Optimizereinstellungen
Können bei C/C++ zu Differenzen führen
228. Float vs. Double
Auf CPU ca gleich schnell
Anders bei der GPU
Tesla-Karten ca. 1/2
Statt einem Kern mit 32 bit werden zwei
Kerne pro Operation verwendet
Geforce nur 1/8 bis 1/16
229. Kernel in der Praxis
Aus Wilt „The CUDA Handbook“, S. 218
„Index-
Schlacht“
OptimiertNachteil: Speicherorganisation
fest verdrahtet
Zentrales Element der GPU-Ifizierung
„Task decomposition“, Task Parallelism, Divide & Conquer, Geometrische Bisektion
Granularität: Coarse vs. Fine
Denken erforderlich, Algorithmen umschreiben, Abhängigkeiten managen
Viel zu tun, auch für theoretische Informatiker, die optimale Algos für GPUs finden möchten
„Dependency graph“
Patterns, „best practices“, Einheitliche Sprache, Klärung, dann muss man das Rad nicht immer neu erfinden
„best practices“
„think parallel“
„Ein weites Feld“,
Daumenregel: Überall da, wo es C gibt, gibt es auch Optimierungsmöglichkeiten
Für jeden Voxel: Berechne den Durchschnitt
Laufzeit N^3 * W^3
Parallelisierung: Einfach, da Berechnung unabhängig
x*y*z Threads
Wichtig: Work per Datum, Granularität
„Volle Pulle“
„Die Straßen sind voll“
„Wo bleibt das Taxi nur?“
Jeder Thread hat eigenen Pfad
Complex algorithms tend to be harder to parallelize, are more susceptible to divergence, and offer less flexibility when it comes to optimisationParallel programming is often less about how much work the program performs as it is about whether that work is divergent or not. Algorithmic complexity often leads to divergence, so it is important to try the simplest algorithm first.
Thread-Block-Größe
Vielfaches von 32/64
Hardware-Kenntnisse erforderlich
Hardware
Bandbreiten, Overlap
#Prozessoren und Threads
Compute Capability
Runtime/Driver API Version
Wenn man den obigen Code dreimal hintereinander ausführt
Schlechte Auslastung der Grafikkarte
Nur visuell angedeutet, nicht mathematisch exakt
Mehr dazu später bei der Optimierung
WICHTIG: Anordnung / Reihenfolge des Codes
Wir teilen die Partionen einfach in zwei Teile, eine Art „Doppelschleife“
Bisher waren die Kopien synchron
Daher haben wir alle Buffer doppelt (bis auf das Eingabe-Volumen und das Ausgabevolumen)
Eine Art Fenster in die großen Volumen. Diese werden pinned angelegt.
Hier verfügen die Karten über unterschiedliche Möglichkeiten, manche können auch ein H2D und D2H gleichzeitig ausführen.
Traditionellerweise lesen wir von rechts nach links. Dann liegen die Daten von zwei benachbarten Threads aber „übereinander“ und nicht „nebeneinander“. Deshalb sollten Threads von unten nach oben lesen. Dann werden die Zugriffe „coalesced“, d.h. in einem Schritt ausgeführt.
Co alesco "grow up", alescere
Traditionellerweise lesen wir von rechts nach links. Dann liegen die Daten von zwei benachbarten Threads aber „übereinander“ und nicht „nebeneinander“. Deshalb sollten Threads von unten nach oben lesen. Dann werden die Zugriffe „coalesced“, d.h. in einem Schritt ausgeführt.
Co alesco "grow up", alescere
Minimiere Kopien zwischen Host und Device, Besser eine große als viele kleine
Shared Mem
Bankkonflikte
Constant Memory
Nur bei gleichem Index im Warp
Register
Read-after-write
Minimiere Kopien zwischen Host und Device, Besser eine große als viele kleine
Shared Mem
Bankkonflikte
Constant Memory
Nur bei gleichem Index im Warp
Register
Read-after-write
#pragma unroll
int in for-Schleife
wg. Optimierung
unsigned int hat Overflow-Checks
Arithmetik
Float statt Double
Fast Math-Kompilieroption
„Intrinsic“-Funktionen
Divisionen vermeiden
__functionName() versus functionName()
Fast path vs. Slow path
Größe von x
sincosf()
x*x*x statt pow(x, 3)
Das grobe Bild: es gibt da noch Möglichkeiten innerhalb eines Warps zu Syncen
atomicSub(), atomicExch()