Pokročilé architektury počítačů Tutoriál 3 CUDA - GPU Martin Milata
Výpočetní model CUDA Organizace kódu Sériově organizovaný kód určený pro CPU Paralelní kód prováděný na GPU Označuje se jako kernel GPU kernel provádí se na tzv. grid struktuře Grid se skládá z bloků (1D nebo 2D struktura) Blok je skupina až 512 vláken organizovaných do 1, 2 nebo 3 rozměrného pole Hierarchizované uspořádání paralelismů
Paměťový model CUDA Definuje různé adresní prostory pro přístup v rámci GPU a komunikaci s CPU Globální paměť, paměť konstant a textur jsou přístupné GPU a CPU Registry, lokální a sdílená paměť přístupné GPU Latence paměti Rychlá paměť s nízkou latencí registry a sdílená paměť Frame buffer (DRAM paměť) lokální paměť, paměť konstant a textur
GT200 Více-jádrový čip s dvou vrstvou hierarchií 10x Thread Processing Cluster (TPC) na vyšší úrovni 3x Streaming Multiprocessor (SM) nebo Thread Processor Array (TPA) na nižší úrovni v rámci každého TPC Hierarchie definovaná realizací přístupu do paměti SM v rámci TPC sdílí přístup k hierarchií cache pamětí v rámci přístupu do paměti textur
Bližší pohled na výpočetní architekturu GT200 Streaming Multiprocessor (SM) SM Controller Instrukční cache Warp buffer Tabulka skóre 8x Streaming Processor (SP) Funkční jednotka 2x speciální funkční jednotka souvisí s grafickými výpočty lze je použít jako násobičky 1x 64bit funkční jednotka 16k 32bit registrů 16kB sdílené paměti
Třetí generace SM 32x CUDA Core provádí celočíselné operace i operace v plovoucí řádové čárce ALU a FPU single i double precision 64KB Sdílené paměti Konfiguračně rozdělena mezi L1 cache a sdílenou paměť (16KB x 48KB pro sdílenou paměť nebo cache) 16x L/S jednotka Jednotná cesta přístupu do paměti (dříve separována paměť textur čtení a výstupní pixel zápis) 4x SFU Speciální operace sin, cos, exp, rcp Obrázek převzat z: Whitepaper NVIDIA's Next Generation CUDA Compute Architecture: Fermi
Třetí generace SM plánování instrukcí Obrázek převzat z: nvidia.com
Máte CUDA enabled zařízení? Převzato z: http://www.nvidia.com/object/cuda_gpus.html Seznam není aktuální (verze z roku 2009)
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Stanovení velikosti vektoru Počet prvků vektoru (10) Nastavení počtu vláken bloku Maximální počet vláken v bloku (4) Důsledek je popis velikosti gridu int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); Počet prvků / Počet vláken bloku printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Definice CUDA kernelu Realizuje paralelní výpočet druhých mocnin prvků vektoru na GPU float *a ukazatel do paměti GPU zařízení int N skutečný počet prvků int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Definice CUDA kernelu Realizuje paralelní výpočet druhých mocnin prvků vektoru na GPU float *a ukazatel do paměti GPU zařízení int N skutečný počet prvků int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Alokace a inicializace vektoru v hlavní paměti ( paměť CPU ) Vektor a_h velikosti N float hodnot je k dispozici v paměti CPU size jako celková velikost pole pro uložení N float hodnot bude použita i při následné alokaci GPU paměti int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Alokace a kopírování hodnot vektoru do paměti GPU Vektor a_d velikosti N float hodnot je alokován v paměti GPU Při kopírování pamětí je specifikován a_d cíl a a_h zdroj přenosu, size velikost kopírované oblasti a cudamemcpyhosttodevice směr přenosu int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Výpočet počtu bloků vláken, do kterých bude provádění rozprostřeno Proměnná n_blocks definuje počet bloků v rámci gridu v 1D uspořádání Případný nenulový zbytek celočíselného podílu počtu prvků a počtu vláken v bloku vynutí alokaci bloku navíc (nebude plně využit) int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Volání provádění CUDA kernelu Za voláním funkce kernelu následují parametry direktivy volání n_blocks počet bloků a block_size jejich velikost (počet vláken v bloku) Vektor a_d je předán jako ukazatel do paměti GPU, následuje konstantní N int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Kopírování hodnoty vypočtem modifikovaného vektoru zpět do paměťového prostoru CPU Při kopírování pamětí je opět specifikován a_h cíl a a_d zdroj přenosu, size velikost kopírované oblasti a cudamemcpydevicetohost směr přenosu int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, size, cudamemcpydevicetohost); printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Zobrazení výsledku int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, size, cudamemcpydevicetohost); printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Příklad: Druhá mocnina prvků vektoru const int N = 10; const int blocksize = 4; global void square_array(float *a,int N) { int idx=blockidx.x*blockdim.x+threadidx.x; if (idx<n) a[idx] = a[idx] * a[idx]; Úklid dynamicky alokovaných pamětí před ukončením provádění programu Uvolnění alokované paměti v prostoru GPU pomocí cudafree(a_d) Uvolnění paměti v prostoru CPU pomocí free(a_h) int main(void) { float *a_h; const size_t size = N*sizeof(float); a_h = (float *)malloc(size); a_h[i] = (float)i; float *a_d; cudamalloc((void **) &a_d, size); cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<<n_blocks, block_size>>>(a_d,n); cudamemcpy(a_h, a_d, size, cudamemcpydevicetohost); printf("%d %f\n", i, a_h[i]); cudafree(a_d); free(a_h);
Vykonávání vláken Struktura GPU Obsahuje N multiprocesorů Každý multiprocesor obsahuje M skalárních procesorů Každý multiprocesor zpracovává skupiny bloků vláken Blok vláken může běžet jen na jednom multiprocesoru Bloky jsou rozděleny do skupin vláken tzv. warp Warp je prováděn paralelně V současné době obsahuje 32 vláken Vlákna jsou ve warp řazena pokud možno se sekvenčně vzrůstajícím threadid Plánovač přepíná provádění mezi warp instrukcemi
CUDA výpočetní server Server Host: Uživatel: Heslo: Přihlášení tesla.cs.vsb.cz pap_cuda cuda_test ssh -X pap_cuda@tesla.cs.vsb.cz Kopírování zdrojového kódu na serveru mkdir./<login> cp./src/*./<login>/ Kompilace CPU gcc -lrt <soubor>.c GPU nvcc <soubor>.cu
CUDA práce s kódem Editace kódu Úkol gedit ~/<login>/<soubor> & Získejte doby provádění kódu pro vyplnění následující tabulky Inicializace (příprava matic) kopírování dat (přesun mezi pamětmi CPU a GPU) výpočet kopírování dat (přesun mezi pamětmi GPU a CPU CPU GPU
Literatura D. Kanter: NVIDIA's GT200: Inside a Papallel Processor NVIDIA CUDA C Programming Guide Johan Seland: CUDA Programming Paul H. J. Kelly, Advanced Computer Architecture Lecture notes 332 P. N. Glaskowsky: NVIDIA s Fermi: The First Complete GPU Computing Architecture Internetové zdroje: http://www.nvidia.com/ http://gpgpu.org/