24(83) Information Coding / Computer Graphics, ISY, LiTH CUDA En lösning för generella beräkningar. En introduktion: Programmingsmodell och språk Minnesareor och minnesaccess Delat minne Exempel 24(83)
CUDA = Compute Unified Device Architecture Utvecklat av NVidia Fungerar bara på NVidia-kort, G80 eller bättre GPU-arkitektur Designat för att dölja att hårdvarans egentligen är för grafik, och för att ge mer kontroll och flexibilitet 25(83)25(83)
Liknar shader-baserade lösningar: 1. Ladda upp data till GPU 2. Exekvera beräkningskärna 3. Ladda ner resultat 26(83)26(83)
Integrerad kod Källkoden till värdprogram och beräkningskärnor kan vara i samma källfil, skriva som ett enda program Betydande skillnad mot shaders (och OpenCL) där kärnan är separat och explicit laddas och kompileras av värden. Beräkningskärnor indentifieras med speciella modifierare i koden. 27(83)27(83)
CUDA Arkitektur och utvidgning av C för parallellbearbetning. Skapar ett stort antal trådar som körs parallellt /mer eller mindre). Mycket är precis som i grafik Du kan inte anta att alla trådar körs parallelt. De körs ett antal i taget: en warp (varp, syftar på vävning). Men nu ser det mer ut som ett vanligt C-program. Inget trassel med data som lagras som pixlar, som i GLSL. Vi kan jobba med vanliga arrayer 28(83)28(83)
Enkelt CUDA-exempel Ett fungerande, kompilerbart, körbart exempel #include <stdio.h> const int N = 16; const int blocksize = 16; global void simple(float *c) { c[threadidx.x] = threadidx.x; } int main() { int i; float *c = new float[n]; float *cd; const int size = N*sizeof(float); cudamalloc( (void**)&cd, size ); dim3 dimblock( blocksize, 1 ); dim3 dimgrid( 1, 1 ); simple<<<dimgrid, dimblock>>>(cd); cudamemcpy( c, cd, size, cudamemcpydevicetohost ); cudafree( cd ); for (i = 0; i < N; i++) printf("%f ", c[i]); printf("\n"); delete[] c; printf("done\n"); return EXIT_SUCCESS; } 29(83)29(83)
Enkelt CUDA-exempel Ett fungerande, kompilerbart, körbart exempel #include <stdio.h> const int N = 16; const int blocksize = 16; Kernel global void simple(float *c) { c[threadidx.x] = threadidx.x; } thread identifier int main() { int i; float *c = new float[n]; float *cd; const int size = N*sizeof(float); cudamalloc( (void**)&cd, size ); dim3 dimblock( blocksize, 1 ); dim3 dimgrid( 1, 1 ); simple<<<dimgrid, dimblock>>>(cd); Call kernel cudamemcpy( c, cd, size, cudamemcpydevicetohost ); cudafree( cd ); for (i = 0; i < N; i++) printf("%f ", c[i]); printf("\n"); delete[] c; printf("done\n"); return EXIT_SUCCESS; } Allocate GPU memory 1 block, 16 threads Read back data 30(83)30(83)
Modifierare för kodtyper Tre modifierare anger hur kod skall användas: global körs på GPU, startas av CPU. Detta är beräkningskärnans ingångspunkt. device är kod som körs på GPU host är kod som körs på CPU (default). CPU host myhostfunc() GPU device mydevicefunc(() global myglobalfunc(() 31(83)31(83)
Minneshantering cudamalloc(ptr, datasize) cudafree(ptr) Liknar CPUns minnesallokering, men görs av CPUn för att allokera på GPU cudamemcpy(dest, src, datasize, arg) arg = cudamemcpydevicetohost or cudamemcpyhosttodevice 32(83)32(83)
Körning av kärnan simple<<<griddim, blockdim>>>( ) (Mycket egendomlig syntax.) Grid är en grid av block. Varje block har nummer inom grid och varje tråd har nummer inom sitt block. Inbyggda variabler för kärnan: threadidx och blockidx blockdim och griddim (OBS, inget prefix som i GLSL.) 33(83)33(83)
Kompilera Cuda nvcc nvcc är nvidias kompilator, /usr/local/cuda/bin/nvcc Källfiler har suffix.cu Enklast möjliga kommandorad: nvcc simple.cu -o simple (Fler parametrar finns för bibliotek mm.) 34(83)34(83)
Kompilera Cuda för större tillämpningar nvcc och gcc i kombination nvcc för.cu-filer gcc för.c/.cpp etc Det går bra att blanda språk. Länkning måste inkludera C++-runtimebibliotek. Exempel: En C-fil, en CU-fil 35(83)35(83)
Exempel på multi-fil-kompilering Källfiler: cudademokernel.cu och cudademo.c nvcc cudademokernel.cu -o cudademokernel.o -c gcc -c cudademo.c -o cudademo.o -I/usr/local/cuda/include g++ cudademo.o cudademokernel.o -o cudademo -L/usr/local/ cuda/lib -lcuda -lcudart -lm Länka med g++ för att inkludera C++-runtime 36(83)36(83)
C/CUDA program code.cu CUDA-kompilering bakom scenen nvcc CPU binary PTX code PTX to target Target binary code 37(83)37(83)
Beräkningar med CUDA Organisation och åtkomst Blocks, threads... 38(83)38(83)
Warps En warp är det minsta antal trådar som processas parallellt i en CUDA-kapabel enhet. Detta antal är satt till 32. Vi behöver normalt inte tänka så mycket på warps utan i första hand tänka på block och threads. 39(83)39(83)
Processing organization 1 warp = 32 threads 1 kernel - 1 grid 1 grid - many blocks 1 block - 1 thread processor 1 block - many threads Använd många trådar och många block > 200 blocks rekommenderas. Antal trådar bör vara multipel av 32 40(83)40(83)
Fördelning av beräkningar över threads och blocks Hierarkisk modell Grid Block 0,0 Block 1,0 Block 2,0 Block 3,0 Block n,n Thread 0,0 Thread 1,0 Thread 2,0 Thread 3,0 Block 0,1 Block 1,1 Block 2,1 Block 3,1 Thread 0,1 Thread 1,1 Thread 2,1 Thread 3,1 Thread 0,2 Thread 1,2 Thread 2,2 Thread 3,2 griddim.x * griddim.y blocks Thread 0,3 Thread 1,3 Thread 2,3 Thread 3,3 BlockDim.x * blockdim.y threads 41(83)41(83)
Indexera data med thread/block-ids Beräkna index via blockidx, blockdim, threadidx Enkelt exempel, beräkna kvadrat av varje element (enbart kärnan): // Kernel that executes on the CUDA device 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]; } 42(83)42(83)
Värdprogram för kvadratexempel Sätt block size och grid size // main routine that executes on the host int main(int argc, char *argv[]) { float *a_h, *a_d; // Pointer to host and device arrays const int N = 10; // Number of elements in arrays size_t size = N * sizeof(float); a_h = (float *)malloc(size); cudamalloc((void **) &a_d, size); // Allocate array on device // Initialize host array and copy it to CUDA device for (int i=0; i<n; i++) a_h[i] = (float)i; cudamemcpy(a_d, a_h, size, cudamemcpyhosttodevice); // Do calculation on device: int block_size = 4; int n_blocks = N/block_size + (N%block_size == 0? 0:1); square_array <<< n_blocks, block_size >>> (a_d, N); // Retrieve result from device and store it in host array cudamemcpy(a_h, a_d, sizeof(float)*n, cudamemcpydevicetohost); // Print results and cleanup for (int i=0; i<n; i++) printf("%d %f\n", i, a_h[i]); free(a_h); cudafree(a_d); } 43(83)43(83)
Minnesaccess Vitalt för prestanda Minnestyper Coalescing Exampel på hur man kan använda delat minne 44(83)44(83)
Minnestyper Global Shared Constant (read only) Texture cache (read only) Local Registers Viktiga när man optimerar 45(83)45(83)
Globalt minne 400-600 cycles latency Använd lokalt delat minne som snabb mellanlagring Ordnade minnesaccesser (Coalescing) Kontinuerligt Starta på 2-potens-adress Addressera enligt trådnumrering Använd shared memory för att omorganisera data 46(83)46(83)
Använd shared memory för att minska antalet accesser av globalt minne Läs block till shared memory Processa Skriv tillbaka det som behövs Shared memory är en manuell cache Exempel: Matrismultiplikation 47(83)47(83)
Matrismultiplikation För att multiplicera två N*N-matriser måste varje element accessas N gånger Naiv implementation: 2N 3 globala minnesaccesser 48(83)48(83)
Matrismultiplikation Låt varje block generera en del av utdata Läs in de delar av matrisen som blocket behöver i shared memory. 49(83)49(83)
Destination element for thread Destination block for thread C A B All blocks on the same row in A are needed to produce the destination block And all blocks in the same column of C For every block, the thread reads one element matching the destination element For every block, we loop over the part of one row and column to perform that part of the computation What one thread reads is used by everybody in the same row (A) or column (B) 50(83)50(83)
Modifierad beräkningsmodell: Ladda upp data till globalt GPU-minne För valda delar: Ladda upp delar av data till shared memory Processa partiella data Skriv partiella data till globalt minne Läs ner resultatet till värden 51(83)51(83)
Synkronisering Så fort du gör något där en del av beräkningen beror av en annan så måste du synkronisera syncthreads() Typisk implementation: Läs till shared memory syncthreads() Processa shared memory synchthreads() Skriv resultatet till globalt minne. 52(83)52(83)
Accelering med coalescing Minnesöverföringar kan vara 10x snabbare om man kan addressera enligt coalescing-reglerna Exempel: Matristransponering Inga beräkingar Enbart minnesaccesser. 53(83)53(83)
Matristransponeing Naiv implementation global void transpose_naive(float *odata, float* idata, int width, int height) { unsigned int xindex = blockdim.x * blockidx.x + threadidx.x; unsigned int yindex = blockdim.y * blockidx.y + threadidx.y; } if (xindex < width && yindex < height) { unsigned int index_in = xindex + width * yindex; unsigned int index_out = yindex + height * xindex; odata[index_out] = idata[index_in]; } Hur kan detta vara dåligt? 54(83)54(83)
Matristransponeing Coalescing-problem Rad för rad och kolum för kolumn. Kolumnvis access är non-coalesced 55(83)55(83)
Matristransponering Lösning med coalescing Läs från globalt minne till shared memory Läs i ordning från globala minnet, godtycklig ordning till shared Skriv till globalt minne Skriv i ordning till globalt minne, godtycklig ordning från shadred 56(83)56(83)
Bättre matristransponering med CUDA global void transpose(float *odata, float *idata, int width, int height) { shared float block[block_dim][block_dim+1]; Shared memory for temporary storage // read the matrix tile into shared memory unsigned int xindex = blockidx.x * BLOCK_DIM + threadidx.x; unsigned int yindex = blockidx.y * BLOCK_DIM + threadidx.y; if((xindex < width) && (yindex < height)) { unsigned int index_in = yindex * width + xindex; block[threadidx.y][threadidx.x] = idata[index_in]; } Read data to temporary buffer syncthreads(); // write the transposed matrix tile to global memory xindex = blockidx.y * BLOCK_DIM + threadidx.x; yindex = blockidx.x * BLOCK_DIM + threadidx.y; if((xindex < height) && (yindex < width)) { unsigned int index_out = yindex * height + xindex; odata[index_out] = block[threadidx.x][threadidx.y]; } } Write data to tglobal memory 57(83)57(83)
Tumregler för coalescing Starta på multipel av 64 Addressera i ordning enligt trådnummer Man hoppa över vissa om man vill. Data bör vara i block om 4, 8 eller 16 bytes 58(83)58(83)
Texturminne Cachad Kan vara mycket snabbt att läsa från om mönstren är cache-vänliga. Texturfiltrering, linjär interpolation. Speciellt bra på att hantera 4 floats i taget (float4). cudabindtexturetoarray() binder data till en texturenhet. Programmeringen är ökänd för att vara knepigare. 59(83)59(83)
Portning till CUDA 1. Parallelliserbar CPU-algoritm. 2. Trivial (seriell) CUDA-implementation. 3. Dela upp i block och trådar. 4. Utnyttja shared memory. 60(83)60(83)
OpenCL OpenCL är en ny (2009) lösning för GPU Computing Uppbackat av Apple Gjort för att ge större frihet i hårdvara Inbyggt i MacOSX 10.6 Har inte CUDAs eleganta integration 61(83)61(83)
Var kan jag köra CUDA? Alla NVidia efter 8800 ( alla) Korsplattform: MS Windows, Linux, MacOSX. En hyfsat modern Mac är perfekt för OpenCL. (Förinstallerat.) Olympen + Southfork (helst Southfork) 62(83)62(83)
GLSL, CUDA eller OpenCL? GLSL är överlägset mest portabelt och lättast att installera. CUDA är elegant och integrerat men kräver specialinstallation och är kräset på hårdvara. OpenCL är portabelt, kräver specialinstallation överallt utom på OSX. => räkna inte ut GPGPU på shaders än 63(83)63(83)
Parallellprogrammering är framtiden All shaderprogrammering är parallellprogrammering. Så gott som all prestandaökning i framtiden kommer från parallelism. Vi fortsätter i TDDD56....och kanske i era projekt i denna kurs? 64(83)64(83)