# Testat - CUDA ## Fragenkatalog 1 (leicht) --- **Wie kann man konstanten Speicher kopieren?** - Via `cudaMemcpyToSymbol` - Alternative: `cudaGetSymbolAddress` + `cudaMemcpy` --- **Erklären Sie den Unterschied zwischen CPU und GPU.** - GPU -> Graphics Processing Unit. - Eine GPU ist ebenfalls ein Prozessor wie die CPU, allerdings ist sie optimiert für die Grafik-Berechnung bei PCs, Konsolen, Smartphones, Tablets und Co. Eine GPU ist also der Grafikprozessor eines Geräts. <img src="https://i.imgur.com/PYM6JsU.png" alt="CPU vs GPU" width="80%" style="display: block; margin-left: auto; margin-right: auto;"> CPU - Kleine Anzahl an (komplexen) Kernen O(10) - Komplexe Kerne mit Steuerlogik zur Maximierung der Parallelität auf Befehlsebene - Out-of-Order Ausführung - Verzweigungsvorhersage - Relativ große Menge an Hauptspeicher pro Kern - Effiziente Ausnutzung von Caches GPU - Weniger Fokus auf Support-Strukturen (Control & Cache), sondern eher auf Processing Units - Mehr Transistoren für Data Processing - Hohe Anzahl an Kernen O(1000) - Wenig Steuerlogik - Sehr große Anzahl an Threads - realisiert Parallele Datenberechnungen - Code mit hoher Berechnungsintensität - Hilft Speicherlatenz zu verbergen - Kleinere caches --- **Wird alles in der Raytracing Lab mit einem Kernel ausgeführt?** Ja. --- **Erklären Sie die Funktionalität von `cudaMemcpy` inklusive Argumente.** ```c cudaError_t cudaMemcpy(void *dst, const void *src, size_t count enum cudaMemcpyKind kind) ``` - Kopiert Daten vom Speicher auf den src zeigt in den Speicher auf den dst zeigt - `dst`: Ziel-Speicheradresse - `src`: Quell-Speicheradresse - `count`: Größe in Bytes zum Kopieren - `kind`: Art der Übertragung - cudaMemcpyHostToHost - cudaMemcpyHostToDevice - cudaMemcpyDeviceToHost - cudaMemcpyDeviceToDevice Die Speicherbereiche dürfen sich nicht überschneiden --- ## Fragenkatalog 2 (mittel) --- **Erklären Sie die Funktionalität von `global`.** - Aufrufbar von Host und wird im Device ausgeführt - signalisiert Kernel Code - Kernel wird ausgeführt (auf der GPU ?) - `__global__` void KernelFunc() --- **Erklären Sie die Funktionalität von `cudaMalloc` inklusive Argumente und warum muss der erste Parameter ein Pointer sein?** ```c cudaError_t cudaMalloc(void **devPointer, size_t size) ``` - Alloziert Device Speicher - `**devPointer`: Pointer auf dem zugewiesenen Device Speicher - `size`: Angefragte Größe in Bytes - Der zugewiesene Speicher ist für jede Art von Variablen geeignet - (Begründung: Weil der zugewiesene Speicher für jede Art von Variablen geeignet ist und sein soll, ein void Pointer lässt sich problemlos zur benötigten Art casten) - weil ein Pointer auf eine Speicheradresse verweist - TODO --- **Erläutern Sie die Speicherhierachie.** - Die Speicherhierarchie ist eine Organisationsform des Speichers in Computersystemen. - Sie basiert auf der Grundlage, dass ein Speicher nicht gleichzeitig alle Anforderungen an Zugriffszeit, Datenübertragungsrate, Preis, Größe oder Energieverbrauch erfüllen kann. ![](https://i.imgur.com/6v6twaM.png =600x300) --- ## Fragenkatalog 3 (schwer) --- Gegeben sei folgende Abbildung (Originalbild) ![](https://i.imgur.com/Npxaade.png) Ungefähre Skizze (Pfeile sind unvollständig oder inkorrekt can't remember) ![](https://i.imgur.com/TCvax4t.png) Ergänzen Sie in den orangen Boxen die jeweiligen Speicherstellen. Antwort: Siehe Abbildung (Global, Constant?, Lokal, Geteilt) Fortführende Frage: Wie kann man shared Daten synchronisieren? - `__syncthreads();` --- Gegeben ist folgender Code-Abschnitt mit einem Fehler. beschreiben Sie wie der Fehler zustande kommt. - Unbekannter Code - fehlende Synchronisation, ohne die ein Read after Write entstanden ist --- **Gegeben sei folgender Code: Wo fehlt sync?** ```c const size_t N 1024; // Array size int host_array[N]; // Array allocated to host int *dev_array; // Pointer to a device array // Device code __global__ void partialSum(int *array) { __shared__ long s_array[N]; int idx = threadIdx.x; s_array[idx] = array[idx]; // HIER MUSS __syncthreads() for(unsigned int stride = blockDim.x / 2; stride > 0; stride /= 2) { if(idx < stride) { s_array[idx] += s_array[idx + stride]; } __syncthreads(); } if(idx == 0) { array[0] = s_array[0]; } } ``` ```c __global__ void stencil_1d(int *in, int *out) { __shared__ int tempB[BLOCK_SIZE + 2 * RADIUS]; int gindex = threadIdx.x + blockIdx.x * blockDim.x; int lindex = threadIdx.x + RADIUS; // Read input elements into shared memory temp[lindex] = in[gindex]; if(threadIdx.x < RADIUS) { temp[lindex - RADIUS] = in[gindex - RADIUS]; temp[lindex + BLOCK_SIZE] = in[gindex + BLOCK_SIZE]; } // HIER MUSS __syncthreads() // Apply the stencil int result = 0; for(int offset = -RADIUS; offset <= RADIUS; offset++) { result += temp[lindex + offset]; } // Store the result out[gindex] = result; } ``` - `__syncthreads();` nach der `if`, um zu gewährleisten, dass alle Daten vorhanden sind - `temp` kann ansonsten nicht fertig bearbeitet sein - gibt also race condition zwischen Threads in verschiedenen Warps --- **Nennen Sie die Speicherstelle, Scope, Lebenszeit für die Variablen `a`, `b`, `c`, `d` und `array`.** ```c #include "stdio.h" // Memory location, scope, lifetime of a, b, c, d, array __shared__ int a; __device__ int b; __constant__ int c; __device__ void kernel() { float d; float array[100]; } int main() { // ... } ``` - `a`: Shared, Block, Kernel - `b`: Global, Grid, Application - `c`: Constant, Grid, Application - `d`: Register, Thread, Kernel - `array`: Local, Thread, Kernel **Fortführende Frage: Welche Art Speicherstelle würde man für eine Matrixmultiplikation verwenden?** - Shared: Weil Effizienter. Nachfolgende Einträge können schneller gelesen werden - (in Vorlesung war das Beispiel mit den Fliesen (Tiles)) -> Threads können sich nämlich auf einen kleinen Ausschnitt der Input Matrix fokussieren und diese Werte im shared memory kollaborativ benutzen. --- **Irgendwas mit: Erklären Sie den Aufbau von GPU o.Ä.** - Multiprozessor von Multiprozessoren - die individuellen Multiprozessoren sind SIMT Architekturen - einige SM (= Streaming Multiprocessor) und SMC (= Streaming Multiprocessor Controller) - viele SPs (= Streaming Prozessorkerne) - viele Transistoren für die Datenverarbeitung (?) - kleine Caches **Erklären wie ein Block aufgebaut ist...** Block - Blöcke sind Elemente innerhalb eines Grids können parallel berechnet werden - Blöcke sind unabhängig voneinander - Skalierbarkeit gut - Block wird von einem SM ausgeführt - Auf Threads abgebildet - Threads organisiert in einer Hierarchie von Blöcken - Menge von Threads, die untereinander kooperieren durch Barrieresynchronisation und Zugriff auf blockeigenen Speicherplatz - Maximal 1024 Threads pro Block auf aktuellen GPUs --- ## Zusatz (Potentiell möglich?) ### Einfach --- **Wofür steht CUDA / Was macht CUDA?** - Compute Unified Device Architecture - CUDA execution model: Ausführung eines Kernel als grid von thread blocks - CUDA virtualisiert Multiprozessoren - folgt SPMD (Single Program Multiple Data) model - durch die Architektur wird Parallelisierung realisiert - Vorteil: Produktivität geseigert --> automatisches Thread management - CUDA device memories: registesr, shared memory, global memory / constant memory - (Nvidia entwickelte API, mit der Programmteile durch den Grafikprozessor (GPU) abgearbeitet werden können) **Was versteht man unter GPU?** - Graphics Processing Unit **Verwendung:** - Generierung von 2D & 3D Grafiken, Bilder und Videos - Anwendungen in GUIs, Videospielen, visuellen Bildgebungsanwendungen - Visual Computing, lineare Algebra, Vektoroperationen, ML, ... **Moderne GPU:** - hochparallel, mutithreadingfähiger Multiprozessor - optimiert für visual computing - Ermöglicht Echtzeit Interaktionen mit berechneten Objekten - Einheitliche Grafik- und Computerarchitektur - Programmierbarer Grafikprozessor - Skalierbare parallele Rechenplattform --- **Erklären Sie, was man unter Grid, Block und Kernel versteht?** Grid - Logische Sicht auf das gesamte Problem - Partitioniert in Blöcke - Blöcke können parallel berechnet werden - Auf Multiprozessoren abgebildet - Menge von Thread-Blöcken, die jeweils unabhängig voneinander ausgeführt werden können und somit parallel ausgeführt werden können Block - Blöcke sind Elemente innerhalb eines Grids und können parallel berechnet werden - Blöcke sind unabhängig voneinander - Skalierbarkeit gut - Block wird von einem SM ausgeführt - Auf Threads abgebildet - Threads organisiert in einer Hierarchie von Blöcken - Menge von Threads, die untereinander kooperieren durch Barrieresynchronisation und Zugriff auf blockeigenen Speicherplatz - Maximal 1024 Threads pro Block auf aktuellen GPUs Kernel - Ein Eintrag in einem Block - Berechnet einen Grid - Aufgerufen aus einem seriellen Hauptprogramm - Einfache Funktion oder vollständiges Programm - Parallele Ausführung über eine Reihe von parallelen Threads Jeder Thread berechnet ein oder mehrere Ergebniselemente Thread - ausgeführt von einem SP - Threads eines Blocks führen gleichzeitig aus und können bei einer Barriere synchronisieren <img src="https://i.imgur.com/nycrLib.png" alt="Visualisierung Grid, Block, Kernel" width="60%" style="display: block; margin-left: auto; margin-right: auto;"> --- **Wie haben sich GPUs über die Zeit entwickelt?** - Mehr programmierbar - Feste Funktiosnlogik durch programmierbare Prozessoren ersetzt - Präziser - Ganzzahl $\Rightarrow$ Festkomma, Übergang zu $\Rightarrow$ Single precision $\Rightarrow$ Double precision - Besser skalierbar - Vereinheitlichung verschiedener programmierbare Pipline-Elemente aus verschiedenen programmierbaren Prozessoren - Verbesserter Load Balancing - Vollständig programmierbar - Unterstützung für allgemeine (general-purpose) Programmiersprachen wie C und C++ --- **Beschreiben Sie den Aufbau einer logischen Grafikpipline.** <img src="https://i.imgur.com/d9url8v.png" alt="Logischen Grafikpipline" width="70%" style="display: block; margin-left: auto; margin-right: auto;"> - Vereinheitlichung verschiedener programmierbare Pipline-Elemente aus verschiedenen programmierbaren Prozessoren - Keine verschiedenen Recheneinheiten, sondern alles in einem - "Unified processor array" <img src="https://i.imgur.com/zgMcs7j.png" alt="Vereinheitlichung Pipeline-Elemente" width="60%" style="display: block; margin-left: auto; margin-right: auto;"> --- **Welche GPU Programming Interfaces gibt es?** - OpenGL - Plattformübergreifende API für das Schreiben von Anwendungen zur Erstellung von 2D und 3D-Computergrafiken - Direct3D - Microsoft DirectX Multimedia-Schnittstelle - Vulkan - Plattformübergreifende 3D-Grafik- und Rechen-API mit geringem Overhead - CUDA - (Compute Unified Device Architecture) - C/C++ Erweiterung von NVIDIA - OpenCL - C-Framework zum Schreiben von Programme, die auf heterogenen Plattformen bestehend aus CPUs, GPUs und anderen Prozessoren ausgeführt werden können - OpenACC & OpenMP - Direktiven baiserte Interfaces --- **Wie kann man sich vorstellen, wie CUDA parallele Berechnungen durchführt**? <img src="https://i.imgur.com/lhxQbCt.png" alt="CUDA Visualisierung" width="80%" style="display: block; margin-left: auto; margin-right: auto;"> --- Erklären Sie die Funktionalität von `cudaFree` inklusive Argumente. ```c cudaError_t cudaFree(void *devPointer) ``` - Gibt den Speicher im Device frei - `*devPointer`: Device Pointer auf freizugebenden Speicher --- **Erklären Sie die Funktionalität von `cudaGetErrorString` inklusive Argumente.** ```c const char* cudaGetErrorString(cudaError_t error) ``` - Gibt die Beschreibung als String für einen Fehlercode zurück - `error`: Fehlercode zur Umwandlung in einen String --- ### Mittel --- **Welche Arten von Speicher gibt es?** Globaler Speicher - R/W von Host und Device - R/W per Grid - Hohe Latenz und Bandbreite - Groß Geteilter Speicher - R/W von Device - R/W per Block - Niedrige Latenz Register - Wie Register auf der CPU - Zugriff pro Thread - Zugriff in einem Zyklus - Klein Constant Local --- **Erklären Sie den Begriff Warp.** - Streaming-Multiprozessor erstellt, verwaltet, plant und führt Threads in Gruppen von 32 parallelen Threads (auch "Warps" genannt) genannt --- **Was versteht man unter Warp Scheduling?** - TODO - Latenz verbergen --- **Wie wird ein Kernel ausgeführt?** - Beim Booten, nach Hardwarecheck ausgeführt - Kernel wird mithilfe eines Bootloaders in Speicher geladen und gestartet - initialisiert die Geräte vollständig und startet einen weiteren Prozess. (wikipedia) reicht das? --- **Welchen Arten von Kompatibilität gibt es?** Binäre Kompatibilität: - Binärcode ist architekturspezifisch (d. h. rechenfähig) - Garantierte Kompatibilität zwischen kleineren Revisionen PTX-Kompatibilität: - Einige Befehle werden nur auf Geräten mit höherer Rechenleistung unterstützt Anwendungskompatibilität: - Die Anwendung muss Binär- oder PTX-Code laden, der mit der Rechenleistung des Geräts kompatibel ist. - Um Code auf zukünftigen Architekturen auszuführen, für die noch keine Binärdateien erstellt werden können, muss eine Anwendung PTX-Code laden, der just-in-time kompiliert wird C/C++ Kompatibilität: - Vollständige C++-Unterstützung für Host-Code - Nur eine Teilmenge von C++ ist auf Devicecode unterstützt 64-Bit-Kompatibilität: - 64-Bit-Gerätecode wird nur mit 64-Bit-Hostcode unterstützt - 32-Bit-Gerätecode wird nur mit 32-Bit-Hostcode unterstützt --- **Welche CUDA-Erweiterungen zu funktionalen C-Deklarationen gibt es?** `__device__`: Aufrufbar von Device und wird im Device ausgeführt `__global__`: Aufrufbar von Host und wird im Device ausgeführt `__host__`: Aufrufbar von Host und wird im Host ausgeführt `__constant__`: Aufrufbar von Device --- **Erklären Sie die Funktionalität von `cudaDeviceSynchronize` inklusive Argumente.** ```c cudaError_t cudaDeviceSynchronize(int *device) ``` - wird gewartet, bis das Rechengerät fertig ist - Blockiert, bis das Gerät alle vorhergehenden angeforderten Aufgaben abgeschlossen hat - `*device`: Device Pointer auf freizugebenden Speicher --- **Erklären Sie die Funktionalität von `syncthreads` inklusive Argumente.** ```c void __syncthreads() ``` - Barrier Synchronisation - Wartet, bis alle Threads im Thread-Block diesen Punkt erreicht haben und alle globalen und gemeinsamen Speicherzugriffe, die von diesen Threads vor `__syncthreads()` vorgenommen wurden, für alle Threads im Block sichtbar sind - Hilft, potenzielle Gefahren durch Lesen-nach-Schreiben, Schreiben-nach-Lesen oder Schreiben-nach-Schreiben zu vermeiden. --- ### Schwer --- **Worin unterscheiden sich SIMD und SIMT?** SIMD: - Wendet eine Anweisung auf mehrere Datenspuren an - Erfordert, dass die Software die Datenparallelität explizit in jedem Vektorbefehl ausdrückt SIMT: - Wendet eine Anweisung auf mehrere unabhängige Threads in parallel - Ermittelt Datenparallelität zwischen unabhängigen Threads - Programme und Anweisungen beschreiben das Verhalten eines einzelnen unabhängigen Threads SIMT kombiniert die Leistung von SIMD mit der Produktivität von Multithreading --- **Wie ist ein Kompilierungs Workflow aufgebaut und welche Arten gibt es?** <img src="https://i.imgur.com/cbGtwAY.png" alt="Compilation Workflow" width="70%" style="display: block; margin-left: auto; margin-right: auto;"> (PTX = Parallel Thread Execution) Offline: - Compiler trennt Host- von Devicecode - Kompiliert Gerätecode in Assemblerform (PTX) und/oder Binärform (Cubin-Objekt) - Modifizierung des Host-Codes durch Ersetzen der <<<>>>-Syntax (Cuda-Kernel-Aufruf) durch Laufzeit-Funktionsaufrufe - Ausgabe des modifizierten Host-Codes entweder als C-Code, der von einem anderen Tool kompiliert werden kann einem anderen Tools kompiliert werden kann oder als Objektcode Just-in-time: - PTX-Code, der von einer Anwendung zur Laufzeit geladen wird, wird vom Gerätetreiber weiter zu Binärcode kompiliert - Erhöht die Ladezeit der Anwendung - Ermöglicht der Anwendung die Nutzung der neuesten Compiler-Verbesserungen - Gerätetreiber speichert Kopie des Binärcodes im Cache