Računanje na grafičkim
karticama
Jurica TeklićPrirodoslovno-matematički fakultet
Sveučilište u Splitu
Arhitektura grafičkog procesora
• Do 240 skalarnih procesora
• Do 30 multiprocesora (streaming multi.)
– 8 skalarnih procesora
– Dijeljena memorija
Memorija
• Globalna memorija - 4GB
• Dijeljena memorija - 16384 bajta po
multiprocesoru
• Lokalna memorija - 16384 32-bitnih registara
po bloku (8192 bajta po jezgri)
CUDAArhitektura za paralelno računanje na nVidia grafičkim
karticama
CUDA osnove
• osnove upravljanja memorijom
• jednostavni kerneli i izvođenje na grafičkoj
kartici
Memorijski prostori
• CPU i GPU imaju odvojene memorije
– Podaci se prenose preko PCIe sabirnice
– Za alociranje, postavljanje i kopiranje se koriste
funkcije
• Pointeri su samo adrese
– Adresom nije određeno o kojoj se memoriji radi
– Oprez pri dereferenciranju
Alokacija i oslobadjanje GPU memorije
• CPU upravlja memorijom na GPU-u– cudaMalloc (void ** pointer, size_t nbytes)
– cudaMemset (void * pointer, int value, size_t count)
– cudaFree (void* pointer)
int n = 1024;
int nbytes = 1024*sizeof(int);
int * d_a = 0;
cudaMalloc( (void**)&d_a, nbytes );
cudaMemset( d_a, 0, nbytes);
cudaFree(d_a);
Kopiranje podataka
• cudaMemcpy( void *dst, void *src, size_t nbytes,enum cudaMemcpyKind direction);
– Blokira CPU nit dok ne završi kopiranje
– Kopiranje će početi samo ako su svi prethodni CUDA pozivi završeni
• enum cudaMemcpyKind
– cudaMemcpyHostToDevice
– cudaMemcpyDeviceToHost
– cudaMemcpyDeviceToDevice
cudaMemcpy (p_cpu, p_gpu, byte_num, cudaMemcpyDeviceToHost);
• Postoje i neblokirajuće funkcije za kopiranje
CUDA compiler driver
• CUDA kod ekstenzije .cu se kompajlira
naredbom
nvcc kod.cu
nvcc –o program kod.cu
nvcc kod.cu –arch=sm_13 (–arch=sm_11)
Zadatak prvi
• Alocirati memoriju za n cijelih brojeva na CPU
• Alocirati memoriju za n cijelih brojeva na GPU
• Inicijalizirati GPU memoriju
• Kopirati sa GPU na CPU
• Ispis
Kod 1#include <stdio.h>
int main()
{
int dimx = 16;
int num_bytes = dimx*sizeof(int);
int *d_a=0, *h_a=0; // device and host pointers
Kod 1#include <stdio.h>
int main()
{
int dimx = 16;
int num_bytes = dimx*sizeof(int);
int *d_a=0, *h_a=0; // device and host pointers
h_a = (int*)malloc(num_bytes);
cudaMalloc( (void**)&d_a, num_bytes );
if( 0==h_a || 0==d_a )
{
printf("couldn't allocate memory\n");
return 1;
}
Kod 1#include <stdio.h>
int main()
{
int dimx = 16;
int num_bytes = dimx*sizeof(int);
int *d_a=0, *h_a=0; // device and host pointers
h_a = (int*)malloc(num_bytes);
cudaMalloc( (void**)&d_a, num_bytes );
if( 0==h_a || 0==d_a )
{
printf("couldn't allocate memory\n");
return 1;
}
cudaMemset( d_a, 0, num_bytes );
cudaMemcpy( h_a, d_a, num_bytes,
cudaMemcpyDeviceToHost );
Kod 1#include <stdio.h>
int main(){
int dimx = 16;int num_bytes = dimx*sizeof(int);
int *d_a=0, *h_a=0; // device and host pointers
h_a = (int*)malloc(num_bytes);
cudaMalloc( (void**)&d_a, num_bytes );
if( 0==h_a || 0==d_a )
{
printf("couldn't allocate memory\n");
return 1;
}
cudaMemset( d_a, 0, num_bytes );
cudaMemcpy( h_a, d_a, num_bytes, cudaMemcpyDeviceToHost );
for(int i=0; i<dimx; i++)
printf("%d ", h_a[i] );
printf("\n");
free( h_a );
cudaFree( d_a );
return 0;
}
CUDA programski model
• Na kartici se paralelni kod pokreće i izvršava
kroz mnogo niti
• Niti su grupirane u blokove
• Paralelni kod je pisan za nit
– Svaka nit se može drukčije izvršavati
– Ugrađeni su identifikatori niti i blokova
Hijerarhija niti
• Pokrenute niti su particionirane po blokovima
– mreža = svi blokovi u kernelu
• Blok je grupa niti koja može
– Sinhronizirati svoje izvođenje
– Komunicirati preko dijeljene memorije
Identifikatori i dimenzije
• Niti
– 3D identifikator, jedinstven unutar bloka
• Blokovi
– 2D identifikator, jedinstven unutar grid-a
• Dimenzije
– Mogu biti jedinstvene za svaki grid
• Ugrađene varijable
– threadIdx, blockIdx
– blockDim, gridDim
Izvršavanje koda na GPU
• C funkcija sa restrikcijama– Pristupa samo GPU memoriji
– Fiksni broj argumenata
– Bez statičkih varijabli
– Bez rekurzije
• Mora biti deklarirana sa kvalifikatorom– __global__ : CPU pokreće, ne može biti pozvana sa GPU-a,
vraća jedino void
– __device__ : poziva se iz drugih GPU funkcija, CPU je ne može pokrenuti
– __host__ : pokreće se na CPU-u
– __host__ i __device__ se mogu kombinirati
Kod 2
• Temelji se na kodu 1
• Napisati kernel za inicijalizaciju cijelih brojeva
• Kopirati rezultat natrag na CPU
• Ispis
Kernel (izvršava se na grafičkoj)
__global__ void kernel( int *a )
{
int idx = blockIdx.x*blockDim.x + threadIdx.x;
a[idx] = 7;
}
Pokretanje kernela
• Pokretački parametri
– Dimenzije grida su najviše 2D i tipa dim3
– Dimenzije bloka su najviše 3D i tipa dim3
– Dijeljena memorija (broj bajtova po bloku) i stream ID
• Nije nužno, inače je 0
dim3 grid(16, 16);
dim3 block(16,16);
kernel<<<grid, block, 0, 0>>>(...);
kernel<<<32, 512>>>(...);
#include <stdio.h>
__global__ void kernel( int *a )
{
int idx = blockIdx.x*blockDim.x + threadIdx.x;
a[idx] = 7;
}
int main()
{
int dimx = 16;
int num_bytes = dimx*sizeof(int);
int *d_a=0, *h_a=0; // device and host pointers
h_a = (int*)malloc(num_bytes);
cudaMalloc( (void**)&d_a, num_bytes );
if( 0==h_a || 0==d_a )
{
printf("couldn't allocate memory\n");
return 1;
}
cudaMemset( d_a, 0, num_bytes );
dim3 grid, block;
block.x = 4;
grid.x = dimx / block.x;
kernel<<<grid, block>>>( d_a );
cudaMemcpy( h_a, d_a, num_bytes, cudaMemcpyDeviceToHost );
for(int i=0; i<dimx; i++)
printf("%d ", h_a[i] );
printf("\n");
free( h_a );
cudaFree( d_a );
return 0;
}
Varijacije kernela i izlaz
__global__ void kernel( int *a )
{
int idx = blockIdx.x*blockDim.x + threadIdx.x;
a[idx] = 7;
} Output: 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7
__global__ void kernel( int *a )
{
int idx = blockIdx.x*blockDim.x + threadIdx.x;
a[idx] = blockIdx.x;
} Output: 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3
__global__ void kernel( int *a )
{
int idx = blockIdx.x*blockDim.x + threadIdx.x;
a[idx] = threadIdx.x;
} Output: 0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 3
Kod 3
• Računanje broja π
Kod 3
• Računanje broja π
Blokovi
• Neovisnost
– skalabilnost
• Postepenost i istodobnost
Sinkronizacija
• Pokretanje kernela je asinkrono– Kontrola se vraća CPU niti
– Kernel se pokreće ako su svi predhodni CUDA pozivi završili
• Memorijska kopiranja su sinkrona– Kontrola se vraća CPU niti kada je kopiranje završeno
– Kopiranje počinje tek kada su predhodni CUDA pozivi završili
• cudaThreadSynchronize()– Blokira nastavak dok svi CUDA pozivi nisu završili
• Postoje i asinkroni CUDA pozivi
Dijeljena memorija
• __shared__ kvalifikator
• On-chip memorija– Za dva reda veličine manja latencija nego globalnoj
memoriji
– Za red veličine veća propusnost nego globalnoj memoriji
– 16KB po multiprocesoru
• Alocira se za blok– Samo niti istog bloka joj mogu pristupiti
• Najčešće se koristi za komunikaciju među nitima iz istog bloka
• void __syncthreads();– Za sinkronizaciju niti unutar bloka
SIMT arhitektura
Revizije CUDA modela
O greškama i debuggeru
• Svako pozivanje CUDA funkcija vraća kod za
grešku
– Osim poziva kernela
– cudaError_t tip
• Postoji debugger ali za sada je bolje koristiti
emulator (u VS odabrati emulation)
– Smanjiti broj niti
nvcc –deviceemu kod.cu