CUDA Compute Unified Device Architecture Programowanie kart graficznych mgr inż. Kamil Szostek AGH, WGGIOŚ, KGIS Wykorzystano materiały z kursu Programowanie kart graficznych prostsze niż myślisz M. Makowski i inni
Literatura NVIDIA CUDA Programming Guide NVIDIA CUDA Reference Manual NVIDIA CUDA Best Practices Guide CUDA w przykładach - J. Sanders, E. Kandrot 2
Po co? 3
Schemat architektur CPU vs GPU 4
Warstwy Aplikacji GPU 5
6
Architektury Compute Capability (Potencjał obliczeniowy) 1.0 Architektura Tesla GPUs 1.1 G92, G94, G96, G98, G84, G86 1.2 GT218, GT216, GT215 G80 1.3 GT200, GT200b 2.0 2.1 3.0 Fermi Kepler GF100, GF110 GF104, GF106 GF108, GF114, GF116, GF117, GF119 GK104, GK106, GK107 3.2 Tegra K1 3.5 GK110, GK208 3.7 GK210 5.0 Maxwell GM107, GM108 5.2 GM200, GM204, GM206 5.3 Tegra X1 7
CUDA C + CUDA Łatwy start dla programistów C/C++ Podstawy Kluczowe abstrakcje hierarchia wątków hierarcha pamięci model pamięci dzielonej na potrzeby współpracy wątków model synchronizacji 8
Hierarchia wątków wątki uruchamiane i wykonywane równolegle przy pomocy kernela - kodu uruchamianego na GPU (device) wszystkie wątki wykonują ten sam (szeregowy) program, ale mogą wchodzić w różne gałęzie każdy blok i każdy wątek posiadają unikalny identyfikator bloki mogą być jedno-, dwu- lub trojwymiarowe 9
Multiprocesor tworzy, zarządza, kolejkuje i uruchamia wątki w 32 elementowych grupach wątków równoległych zwanych warp. 10
Hierarchia pamięci rejestry (registers) p. lokalna (local) p. dzielona (shared) p. globalna (global) p. stałych (constant) 11
Kwalifikatory zmiennych Przykład deklaracji Typ pamięci Widoczność double v; rejestr (chyba, że przekroczono jego wielkość) wątek double array[10]; lokalna wątek shared double v; dzielona blok device double v; globalna aplikacja constant double v; globalna aplikacja 12
Wydajność pamięci przepustowość opóźnienia Typ pamięci Koszt (obrazowo) rejestry 1 dzielona 1 globalna 100-1000 lokalna 100-1000 pamięć stałych 1 13
Kwalifikatory funkcji global konieczny dla kernela musi mieć typ void uruchamiany z hosta (chyba, że CC >= 3.5) device wywoływane na GPU z innych funkcji host wywoływane przez hosta (domyślnie) można połączyć z device 14
Zmienne wewnątrz kernela Specjalne zmienne do identyfikacji wątków dim3 threadidx;// id wątku dim3 blockidx; // id bloku dim3 blockdim; // rozmiar bloku dim3 griddim; // rozmiar siatki = ilość bloków Synchronizacja wewnątrz bloku syncthreads(); 15
Podstawowy wzorzec uruchamiania obliczeń 16
Klasyczne podejście 17
Zarządzanie urządzeniami cudagetdevicecount( int* count ) cudasetdevice( int device ) cudagetdevice( int *current_device ) cudagetdeviceproperties( cudadeviceprop* prop, int device ) cudachoosedevice( int *device, cudadeviceprop* prop ) 18
Zarządzanie pamięcią Możliwe jest zarządzanie pamięcią GPU zarówno z poziomu hosta jak i urządzenia Alokowanie i zwalnianie pamięci Kopiowanie danych do i z globalnej pamięci urządzenia: cudamalloc(void **pointer, size_t nbytes) cudamemcpy(void *dst, void* src, size_t nbytes, dir); cudamemset(void *pointer, int value, size_t count) cudafree(void *pointer) 19
Przykład alokacji pamięci bez transferu danych int nbytes = 1024*sizeof(int); int *d_a = 0; cudamalloc( (void**)&d_a, nbytes ); cudamemset( d_a, 0, nbytes); //opcjonalnie... cudafree(d_a); 20
Przykład alokacji pamięci z transferem danych 1. // alokacja pamięci Hosta 2. int numbytes = N * sizeof(float) 3. float* h_a = (float*) malloc(numbytes); 4. //... Wypełnianie h_a... 5. // alokacja pamięci na urządzeniu 6. float* d_a = 0; 7. cudamalloc((void**)&d_a, numbytes); 8. // kopiowanie danych z pamięci hosta do urządzenia 9. cudamemcpy(d_a, h_a, numbytes, cudamemcpyhosttodevice); 10. // uruchomienie kernela... 11. gpu_func <<<exec-dims>>> (params) 12. // kopiowanie danych z pamięci urządzenia do hosta 13. cudamemcpy(h_a, d_a, numbytes, cudamemcpydevicetohost); 14. //zwalnianie pamięci urządzenia 15. cudafree(d_a); 16. //... Dalsze operacja na danych 21
Uruchamianie kernela nazwakernela <<<dim3 grid, dim3 block>>>( ) Przykłady: nazwakernela <<<500, 128>>>(...); // uruchamia 500 bloków, w każdym 128 wątków ---------------------------------- dim3 rozmiar_grida(128,32); dim3 rozmiar_blokow(16,16,2); nazwakernela <<< rozmiar_grida, rozmiar_grida >>>(...); // uruchamia 128*32=4096 bloków, w każdym 16*16*2=512 wątków 22
#include "stdio.h" Printf wewnątrz kernela dla CC >=2.0 global void hellocuda(float f) { printf("hello thread %d, f=%f\n", threadidx.x, f); } int main() { hellocuda<<<1, 5>>>(1.2345f); cudadevicesynchronize(); return 0; } //----------- na wyjściu: Hello thread 2, f=1.2345 Hello thread 1, f=1.2345 Hello thread 4, f=1.2345 Hello thread 0, f=1.2345 Hello thread 3, f=1.2345 23
BRAK SYNCHRONIZACJI BLOKÓW! 24
Pamięć Każdy wątek może: R / W rejestr wątku R / W pamięć lokalna wątku R / W pamięć dzielona bloku R / W pamięć globalna R / - pamięć stała (constant) R / - pamięć tekstur 25
26
27
// alokacja pamięci Hosta int numbytes = N * sizeof(float) float* h_a = (float*) malloc(numbytes); //... Wypełnianie h_a... // alokacja pamięci na urządzeniu float* d_a = 0; cudamalloc((void**)&d_a, numbytes); // kopiowanie danych z pamięci hosta do urządzenia cudamemcpy(d_a, h_a, numbytes, cudamemcpyhosttodevice); // uruchomienie kernela increment_gpu<<< N/blockSize, blocksize>>>(d_a, b); decrement_gpu<<< N/blockSize, blocksize>>>(d_a, b); // kopiowanie danych z pamięci urządzenia do hosta // SYNCHRONIZACJA! cudamemcpy(h_a, d_a, numbytes, cudamemcpydevicetohost); //zwalnianie pamięci urządzenia cudafree(d_a); //... Dalsze operacja na danych 28
Porównanie kodów CPU i GPU CPU GPU void increment_cpu(float *a, float b, int N) { for (int idx = 0; idx<n; idx++) a[idx] = a[idx] + b; } global void increment_gpu(float *a, float b, int N) { } int idx = blockidx.x * blockdim.x + threadidx.x; if (idx < N) a[idx] = a[idx] + b; void main() {... increment_cpu(a, b, N); } void main() {... dim3 dimblock (blocksize); dim3 dimgrid( ceil( N / (float)blocksize) ); increment_gpu<<<dimgrid, dimblock>>>(a, b, N);... } 29
Przykład optymalizacji odczytów pamięci global void increment_gpu(float *a, { } float *b, float *c, float *a_out, float *b_out, int N) int idx = blockidx.x * blockdim.x + threadidx.x; if (idx < N) { } a_out[idx] = a[idx] + b[idx] + c[idx]; b_out[idx] = a[idx] * b[idx] * c[idx]; global void increment_gpu(float *a, { } float *b, float *c, float *a_out, float *b_out, int N) int idx = blockidx.x * blockdim.x + threadidx.x; float a = a[idx]; float b = b[idx]; float c = c[idx]; if (idx < N) { } a_out[idx] = a + b + c; b_out[idx] = a * b * c; 30
Pamięć dzielona shared short cache[size]; extern shared int cache[]; Kernell<<<gridS, blocks, sharedsize>>>( ) syncthreads(); 31
Redukcja 32
Redukcja 33
34