CUDA J. Sloup a I. Šimeček xsimecek@fit.cvut.cz Katedra počítačových systémů FIT České vysoké učení technické v Praze Ivan Šimeček, 2011 MI-PAP, LS2010/11, Predn.6 Příprava studijního programu Informatika je podporována projektem financovaným z Evropského sociálního fondu a rozpočtu hlavního města Prahy. Praha & EU: Investujeme do vaší budoucnosti
CUDA (Compute Unified Device Architecture) CUDA je architektura pro provádění paralelních výpočtů, která definuje: Programming model vlákna (threads), blok (block) a mřížka (grid) Memory model registry, lokální, sdílená a globální paměť Execution model spouštění vláken a jejich mapování na HW Vývojové prostředí zahrnuje: Driver Runtime spouštění C funkcí na GPU Toolkit překladač, debugger, profiler Libraries CUFFT, CUBLAS, SDK dokumentace + ukázky kódu CUDA je podporována na všech grafických procesorech NVIDIA (CUDA enabled-gpus) počínaje čipem G80 CUDA programy je možno psát v jazyku C/C++ nebo Fortran.
CUDA základní pojmy CPU (označované jako host hostitel) Kernel = část kódu, kterou chceme provádět paralelně na GPU (def. jako funkce) Vlákno (thread) = instance kernelu GPU (označované jako device zařízení) je tvořeno multiprocesory. Streaming Multiprocesor (dále jen SM) se skládá z několika (např. 8 u G80) procesorů (jader = SP). SM provádějí bloky vláken. Warp je skupina vláken spouštěná najednou, např. 32 na GT200 GPU má SIMT architekturu = všechna vlákna v rámci warpu jsou ovládány jednou instrukční jednotkou = provádí stejnou instrukci (kromě podmíněných příkazů) Vlákna během výpočtu přistupují k různým druhům pamětí
2D architektura = škálovatelnost Každý GPU se může lišit Frekvencí GPU Pamětí a velikostí GPU paměti Počtem SM Počtem jader na SM Schopnostmi (CUDA capabilities) Protože jednotlivé SM pracují víceméně nezávisle, je snadné zvyšovat výkon GPU zvýšením počtu SM
Provádění GPU kódu Vlákna jsou sdružována do bloků a bloky do mřížky (2D) Vlákna v rámci bloku: lze synchronizovat (pomocí bariéry) mohou sdílet data pomocí sdílené paměti Programátor zadá (téměř libovolně) počet bloků a počet vláken v bloku (2D z hlediska programátora) To jak přesně bude kód prováděn udává execution model. Ten se stará o korektní a efektivní mapování na dané HW. V HW totiž existuje daný počet SM a daný počet SP na jeden SM. (2D z hlediska HW)
Provádění GPU kódu Každý blok vláken je prováděn na právě jednom SM. Na každý SM může být namapováno více bloků najednou (až 8 u GT200) Blok vláken je rozdělen do několika warpů (podle čísel vláken). Centrální distribuce bloků metodou round-robin. V každém časovém kroku plánovač naplánuje (nulová režie!) k provedení připravený warp (který má všechna data a neprovádí bariéru). Vlákna warpu pak provedou paralelně jednu instrukci. Plánování je HW akcelerováno GeForce 8800: maximálně 24 rozpracovaných warpů GTX 280: maximálně 32 rozpracovaných warpů
Bloky kontra vlákna I Je dobré mít velký počet vláken v bloku Synchronizace a předávání dat pomocí sdílené paměti jen v rámci jednoho bloku! Díky přeplánování mohou být amortizovány velké latence. Ale: Blok může obsahovat maximálně 512 threadů Každý SM může (současně) provádět 8 bloků, ale skutečný počet závisí na paměťových požadavcích na registry a sdílenou paměť = např. všechny bloky na SM mohou alokovat max. 16 kb sdílené paměti. Stejně tak je limitován počet registrů pro vlákno
Bloky kontra vlákna II Bloky by měly být nezávislé Být navrženy tak, aby každé pořadí jejich vyhodnocení bylo korektní Mohou běžet paralelně nebo sekvenčně Data mohou být sdílena mezi bloky ale problém se synchronizací
Jednoduchý CUDA příklad // definice kernelu global void VecAdd(float* A, float* B, float* C) { //identifikacni cislo vlakna int i = threadidx.x; C[i] = A[i] + B[i]; } int main() { // volani kernelu z funkce main VecAdd<<<1, N>>>(A, B, C); Musí být void Číslo v rámci bloku } Počet vláken v bloku Počet bloků
Mřížka bloků Bloky mi mohou obecně tvořit (maximálně 2D) mřízku = grid Obdobně vlákna v rámci bloku mi mohou obecně tvořit (maximálně 3D) matici. Ale mřížka i matice jsou převedeny do 1D, takže nemají velký význam kromě vyššího komfortu pro programátora při přístupu do vícedimenzionálního pole.
Mapování
Kompilace CUDA soubory mají příponu.cu Kompilátor nvcc (přesněji compiler driver). Ten má podobné parametry jako gcc. nvcc vezme zdrojový kód v.cu (nebo i.c/.cpp) souboru a za použití dalších aplikací, jako gcc, cl, apod. zkompiluje standardní c/c++ zdrojový kód do spustitelné podoby. Kód určený pro GPU, je přeložen do ptx (Parallel Thread Execution) virtual machine mezikódu. Podporuje i budoucí generace GPU PTX je následně zkompilován během spuštení (JIT pomocí ovladače grafické karty) do binárního kódu pro dané zařízení (pro danou verzi compute capability). Společně se spustitelným kódem se slinkují i knihovny cudart (CUDA runtime library) a cuda (CUDA core library), nutné k běhu aplikace
Synchronizace s hostitelem Spuštení kernelů je asynchronní operace Vrací řízení CPU ihned jak je to možné Pokud je třeba počkat na dokončení práce všech CUDA volání cudathreadsynchronize() kernel je spuštěn po dokončení všech předchozích CUDA voláních Operace kopírování cudamemcpy() je synchronní, je spuštěna po dokončení všech předchozích CUDA voláních
Násobení matic (CUDA v1) I global void Mul_IJ( float *ina, float *inb, float *outc, int N ) { int i=blockidx.x; int j=threadidx.x; int k; float s=0.0; Číslo bloku Číslo v rámci bloku } for(k=0;k<n;k++) s+=ina[i*n+k]*inb[k*n+j]; outc[i*n+j]=s; syncthreads(); Bariéra v rámci bloku
Násobení matic (CUDA v1) II static void HandleError( cudaerror_t err, const char *file, int line ) { if (err!= cudasuccess) { printf( "%s in %s at line %d\n", cudageterrorstring( err ), file, line ); exit( EXIT_FAILURE ); }} Zpracování chyb #define HANDLE_ERROR( err ) (HandleError( err, FILE, LINE )) int main( void ) { cudadeviceprop prop; int whichdevice,n,i,j; cudaevent_t start, stop; float elapsedtime; float *hostm, *hostm2,*hostx,*hosty; float *devm,*devx,*devy; Vlastnosti GPU CUDA události Ukazatele
Násobení matic (CUDA v1) III HANDLE_ERROR( cudagetdevice( &whichdevice ) ); HANDLE_ERROR( cudagetdeviceproperties( &prop, whichdevice ) ); HANDLE_ERROR( cudaeventcreate( &start ) ); HANDLE_ERROR( cudaeventcreate( &stop ) ); Inicializace HANDLE_ERROR( cudamalloc( (void**)&devx, N * N* sizeof(float) ) ); HANDLE_ERROR( cudamalloc( (void**)&devy, N * N *sizeof(float) ) ); HANDLE_ERROR( cudamalloc( (void**)&devm, N * N * sizeof(float) ) ); HANDLE_ERROR( cudahostalloc( (void**)&hostx, N * N* sizeof(float), cudahostallocdefault ) ); HANDLE_ERROR( cudahostalloc( (void**)&hosty, N * N *sizeof(float), cudahostallocdefault ) ); HANDLE_ERROR( cudahostalloc( (void**)&hostm, N * N *sizeof(float), cudahostallocdefault ) ); HANDLE_ERROR( cudahostalloc( (void**)&hostm2, N * N *sizeof(float), cudahostallocdefault ) ); Alokace paměti na GPU Alokace paměti na CPU
Násobení matic (CUDA v1) IV cudamemcpy(devx,hostx,sizeof(int)*n*n,cudamemcpyhosttodevice); cudamemcpy(devy,hosty,sizeof(int)*n*n,cudamemcpyhosttodevice); Kopírování CPU -> GPU HANDLE_ERROR( cudaeventrecord( start, 0 ) ); Volání kernelu Mul_I<<<N,N>>>( devx, devy, devm, N ); cudathreadsynchronize(); HANDLE_ERROR( cudaeventrecord( stop, 0 ) ); Zjištění času HANDLE_ERROR( cudaeventsynchronize( stop ) ); HANDLE_ERROR( cudaeventelapsedtime( &elapsedtime, start, stop ) ); printf( GPU time taken: %g ms\n", elapsedtime ); cudamemcpy(hostm,devm,sizeof(float)*n*n,cudamemcpydevicetohost); Kopírování GPU -> CPU
Násobení matic (CUDA v1) V HANDLE_ERROR( cudaeventdestroy( start ) ); HANDLE_ERROR( cudaeventdestroy( stop ) ); HANDLE_ERROR( cudafreehost( hostx ) ); HANDLE_ERROR( cudafreehost( hosty ) ); HANDLE_ERROR( cudafreehost( hostm ) ); HANDLE_ERROR( cudafreehost( hostm2 ) ); HANDLE_ERROR( cudafree( devx ) ); HANDLE_ERROR( cudafree( devy ) ); HANDLE_ERROR( cudafree( devm ) );
Literatura [1] J. Sloup: přednášky z předmětu GPGPU, KPGI FEL ČVUT. [2] CUDA Programming Guide for CUDA Toolkit 3.1.1. [3] René Müller: Data Processing on GPUs and GPGPUs. Lecture in class 63-3502-00:Data Processing on Modern Hardware. ETH Zurich. Fall 2009. [4] http://www.root.cz/clanky/uvod-do-technologie-cuda/