CUDA (Compute Unified Device Architecture) GPGPU - General-purpose computing on graphics processing units CUDA (Compute Unified Device Architecture) W. Bożejko
Plan Wstęp Model programowania Model pamięci CUDA API Przykład – iloczyn skalarny
Wstęp
Tesla C870 Produkt Tesla C870 Obudowa ATX, 4.38” x 12.28” Ilość GPU Tesla 1 Dedykowana pamięć 1.5 GB GDDR3 Szczytowa wydajność Ponad 500 gigaflopów Precyzja obliczeń zmiennoprzecinkowych Pojedyncza precyzja w standardzie IEEE 754 Interfejs pamięci 384-bit Przepustowość pamięci 76.8 GBps Maksymalny pobór mocy 170W Interfejs systemowy PCI Express x16 Dodatkowe źródła zasilania Tak (2) Liczba slotów 2 Chłodzenie wentylator całkowity rozmiar pamięci globalnej 1,61 GB liczba multiprocesorów 16 liczba rdzeni (procesorów) 128 całkowity rozmiar pamięci stałej 65536 KB całkowity rozmiar pamięci współdzielonej przypadającej na jeden blok 16384 KB liczba rejestrów dostępna dla każdego bloku 8192 częstotliwość zegara 1,35 GHz
CUDA – model programowania GPU jest widziane jako urządzenie obliczeniowe mogące wykonać część aplikacji która musi być wykonana wielokrotnie może być wyizolowana jako funkcja działa niezależnie na różnych danych (model SIMD) Taka funkcja może być skompilowana o wykonana na GPU
CUDA – model programowania Blok wątków (Thread Block) Wątki mogą kooperować Mają szybką pamięć współdzieloną Są zsynchronizowane można je łatwo rozróżniać (mają Thread ID) Blok może być 1,2 lub 3-wymiarową tablicą
CUDA – model programowania Grid bloków wątków Ograniczona ilość wątków w bloku Pozwala wywołać większą liczbę wątków za pomocą jednego wywołania Bloki są identyfikowane za pomocą block ID Wymaga zmniejszenia kooperacji wątków Bloki mogą być 1 lub 2-wymiarowymi tablicami
CUDA – model programowania
CUDA – model pamięci
CUDA – model pamięci Shared Memory Wbudowana w chip Znacznie szybsza niż pamięć lokalna i globalna Tak szybka jak rejestry (jeśli nie ma konfliktów) Dzielna na równej wielkości banki Kolejne 32-bitowe słowa są przypisane do kolejnych banków, Każdy bank ma przepustowość (bandwidth) 32 bity na 1 cykl zegara
CUDA – model pamięci Shared Memory
CUDA API Rozszerzenie języka C Kwalifikatory typu funkcji specyfikujące wykonanie na procesorze (host) lub na urządzeniu GPU Kwalifikatory typu zmiennej specyfikujące rodzaj pamięci w GPU Nowe składnia <<< mówiąca jak wykonać program na urządzeniu Cztery wbudowane zmienne pamiętające rozmiary grid’a i bloku oraz numery bloku i wątku
CUDA API Kwalifikatory typu funkcji __device__ __global__ __host__ Wykonywane na GPU Wywoływane tylko z GPU __global__ Wywoływane tylko z procesora głównego (host’a) __host__ Wykonywane na host’cie,
CUDA API Kwalifikatory typu zmiennych __device__ Umieszone w pamięci globalnej Widoczne przez cały czas działania programu Dostępne dla wszystkich wątków w grid’zie oraz z hosta (poprzez runtime library) __constant__ (ewentulanie razem z __device__) Umieszczone w pamięci stałej (constant memory space), __shared__ (ewentulanie razem z __device__) Umieszczone w pamięci współdzielonej (shared memory) bloku danego wątku Widoczne tak długo jak istnieje blok Dostępne tylko dla wszystkich wątków w bloku
CUDA API Konfiguracja wykonania Musi być sprecyzowana dla kazdego wywołania funkcji typu __global__ Definiuje rozmiary grid’a i bloków Umieszczana pomiędzy nazwą funkcji a listą argumentów: funkcja: __global__ void Func(float* parameter); musi być wywołana tak: Func<<< Dg, Db, Ns >>>(parameter);
CUDA API Konfiguracja wykonania gdzie Dg, Db, Ns są: Dg jest typu dim3 wymiar i rozmiar grida Dg.x * Dg.y = ilość uruchamianych bloków; Db jest typu dim3 wymiar i rozmiar bloków Db.x * Db.y * Db.z = ilość wątków na blok; Ns jest typu size_t ilość bajtów w pamięci współdzielonej (shared memory) która jest dynamiczne alokowana dodatkowo do pamięci alokowanej statycznie Ns jest opcjonalne; domyślnie 0.
CUDA API Wbdowane zmienne gridDim typu dim3 wymiary grida. blockIdx typu uint3 number bloku w grid’zie blockDim typu dim3 wymiary bloku threadIdx is of type uint3 numer wątku w bloku
Przykład – iloczyn skalarny Policzyć iloczyn skalarny 32 par wektorów Kożdy po 4096 elementów Efektywna organizacja obliczeń: grid składający się z 32 bloków z 256 wątkami na blok Otrzymamy 4096/265 = 16 segmentów na wektor
Przykład – iloczyn skalarny Dane będą trzymane w GPU jako dwie tablice; wynik umieszczony zostanie w tablicy Każdy iloczyn par wektórw An, Bn będzie obliczany w segmentach, dodawanych do wyniku … Vector A0 Vector A1 Vector AN-1 … Vector B0 Vector B1 Vector BN-1 Results 0 to N-1 segment 0 segment 1 … segment S-1 Vector A0 Vector B0 Partial results 0 to S-1 Results 0 Results 1
Przykład – iloczyn skalarny Program dla host’a int main(int argc, char *argv[]){ CUT_CHECK_DEVICE(); … h_A = (float *)malloc(DATA_SZ); cudaMalloc((void **)&d_A, DATA_SZ); cudaMemcpy(d_A, h_A, DATA_SZ, cudaMemcpyHostToDevice); ProdGPU<<<BLOCK_N, THREAD_N>>>(d_C, d_A, d_B); cudaMemcpy(h_C_GPU, d_C, RESULT_SZ, cudaMemcpyDeviceToHost); CUDA_SAFE_CALL( cudaFree(d_A) ); free(h_A); CUT_EXIT(argc, argv); }
Przykład – iloczyn skalarny Funkcja dla GPU (Kernel Function) Parametry: d_C: wskaźnik do wyniku (tj. tablicy) d_A, d_B wskaźniki do danych (tablic) Tablice lokalne: t[]: wynkki8 pojedynczego wątku r[]: używane do dodawania wyników segmentów I: numer (Id) wątku w bloku __global__ void ProdGPU(float *d_C, float *d_A, float *d_B){ __shared__ float t[THREAD_N]; __shared__ float r[SLICE_N]; const int I = threadIdx.x; for(int vec_n=blockIdx.x; vec_n<VECTOR_N; vec_n+=gridDim.x){ int base = ELEMENT_N * vec_n; for(int slice = 0; slice < SLICE_N; slice++, base += THREAD_N){ t[I] = d_A[base + I] * d_B[base + I]; __syncthreads(); for(int stride = THREAD_N / 2; stride > 0; stride /= 2){ if(I < stride) t[I] += t[stride + I]; } if(I == 0) r[slice] = t[0]; for(int stride = SLICE_N / 2; stride > 0; stride /= 2){ if(I < stride) r[I] += r[stride + I]; if(I == 0) d_C[vec_n] = r[0];
Przykład – iloczyn skalarny Funkcja dla GPU Uruchamiane dla każdej pary wektorów wejściowych Zostanie uruchomione tylko raz, ponieważ: Grid dimension == number of vectors vector number = block Id __global__ void ProdGPU(float *d_C, float *d_A, float *d_B){ __shared__ float t[THREAD_N]; __shared__ float r[SLICE_N]; const int I = threadIdx.x; for(int vec_n=blockIdx.x; vec_n<VECTOR_N; vec_n+=gridDim.x){ int base = ELEMENT_N * vec_n; for(int slice = 0; slice < SLICE_N; slice++, base += THREAD_N){ t[I] = d_A[base + I] * d_B[base + I]; __syncthreads(); for(int stride = THREAD_N / 2; stride > 0; stride /= 2){ if(I < stride) t[I] += t[stride + I]; } if(I == 0) r[slice] = t[0]; for(int stride = SLICE_N / 2; stride > 0; stride /= 2){ if(I < stride) r[I] += r[stride + I]; if(I == 0) d_C[vec_n] = r[0];
Przykład – iloczyn skalarny Funkcja dla GPU Uruchamiane dla każdego segmentu wektorów wejściowych Każdy wątek wylicza jeden iloczyn i zapamiętuje go __global__ void ProdGPU(float *d_C, float *d_A, float *d_B){ __shared__ float t[THREAD_N]; __shared__ float r[SLICE_N]; const int I = threadIdx.x; for(int vec_n=blockIdx.x; vec_n<VECTOR_N; vec_n+=gridDim.x){ int base = ELEMENT_N * vec_n; for(int slice = 0; slice < SLICE_N; slice++, base += THREAD_N){ t[I] = d_A[base + I] * d_B[base + I]; __syncthreads(); for(int stride = THREAD_N / 2; stride > 0; stride /= 2){ if(I < stride) t[I] += t[stride + I]; } if(I == 0) r[slice] = t[0]; for(int stride = SLICE_N / 2; stride > 0; stride /= 2){ if(I < stride) r[I] += r[stride + I]; if(I == 0) d_C[vec_n] = r[0];
Przykład – iloczyn skalarny Funkcja dla GPU Wyliczenie wyniku częściowego dla segmentu Zapamiętanie wyniku częsciowego __global__ void ProdGPU(float *d_C, float *d_A, float *d_B){ __shared__ float t[THREAD_N]; __shared__ float r[SLICE_N]; const int I = threadIdx.x; for(int vec_n=blockIdx.x; vec_n<VECTOR_N; vec_n+=gridDim.x){ int base = ELEMENT_N * vec_n; for(int slice = 0; slice < SLICE_N; slice++, base += THREAD_N){ t[I] = d_A[base + I] * d_B[base + I]; __syncthreads(); for(int stride = THREAD_N / 2; stride > 0; stride /= 2){ if(I < stride) t[I] += t[stride + I]; } if(I == 0) r[slice] = t[0]; for(int stride = SLICE_N / 2; stride > 0; stride /= 2){ if(I < stride) r[I] += r[stride + I]; if(I == 0) d_C[vec_n] = r[0]; t[0] += t[128] t[1] += t[129] t[0] += t[64] t[2] += t[130] t[1] += t[65] … t[0] += t[1] … … … t[64]+= t[127] t[127]+= t[255]
Przykład – iloczyn skalarny Funkcja dla GPU Dodanie wyników dla wszystkich segmentów Zapisanie wyniku w pamięci __global__ void ProdGPU(float *d_C, float *d_A, float *d_B){ __shared__ float t[THREAD_N]; __shared__ float r[SLICE_N]; const int I = threadIdx.x; for(int vec_n=blockIdx.x; vec_n<VECTOR_N; vec_n+=gridDim.x){ int base = ELEMENT_N * vec_n; for(int slice = 0; slice < SLICE_N; slice++, base += THREAD_N){ t[I] = d_A[base + I] * d_B[base + I]; __syncthreads(); for(int stride = THREAD_N / 2; stride > 0; stride /= 2){ if(I < stride) t[I] += t[stride + I]; } if(I == 0) r[slice] = t[0]; for(int stride = SLICE_N / 2; stride > 0; stride /= 2){ if(I < stride) r[I] += r[stride + I]; if(I == 0) d_C[vec_n] = r[0];