Programowanie procesorów graficznych GPGPU 1
OpenCL projektowanie kerneli Przypomnienie: kernel program realizowany przez urządzenie OpenCL wątek (work item) rdzeń (processing element): jeden wątek wykonywany jest przez jeden rdzeń grupa wątków (work group) jednostka obliczeniowa (compute unit): grupa wątków wykonuje obliczenia na jednej jednostce obliczeniowej grupa wątków powinna mieć więcej wątków niż rdzeni w jednostce obliczeniowej grupa dzielona jest na podgrupy, wavefronts/warps, wątków wykonywanych jednocześnie w modelu SIMD przestrzeń wątków urządzenie: wszystkie watki z przestrzeni realizują zlecony kernel na urządzeniu 2
3
OpenCL organizacja wątków 4
Przestrzeń wątków 5
OpenCL projektowanie kerneli Przypomnienie: wielopoziomowa hierarchia pamięci: rejestry najszybsze zmienne lokalne (jeśli nie jest ich za dużo, register spilling) prywatne dla każdego wątku pamięć wspólna szybka zmienne (tablice) z atrybutem local wspólne dla wątków z jednej grupy roboczej pamięć globalna powolna zmienne lokalne nie mieszczące się w rejestrach zmienne (tablice) z atrybutem local» wspólne dla wszystkich wątków dodatkowe rodzaje pamięci: constant, texture 6
OpenCL model pamięci 7
8
9
OpenCL ukrywanie opóźnienia Ukrywanie opóźnienia (latency hiding) w dostępie do pamięci dla CPU i GPU Konieczne jest współbieżne wykonywanie co najmniej kilku (najlepiej kilkunastu) grup wątków (AMD wavefronts, NVIDIA warps) Podział na grupy wątków dokonujących jednoczesnego dostępu do pamięci jest ukryty przed programistą 10
OpenCL zależności danych 11
Projektowanie kerneli Model równoległości danych (data parallel programming) model zrównoleglenia: dekompozycja danych (data decomposition) każdy wątek realizuje ten sam kod (SPMD) synchronizacja sprzętowa (SIMD, SIMT) OpenCL pozwala uzyskać informacje o liczbie wymiarów przestrzeni wątków, liczbie wątków (globalnej oraz w pojedynczej grupie), liczbie grup oraz o identyfikatorze wątku: lokalnym (w grupie), globalnym, a także o identyfikatorze grupy: get_work_dim() get_global_id(dim_id), get_local_id(dim_id) get_global_size(dim_id), get_local_size(dim_id) get_num_groups(dim_id), get_group_id(dim_id) 12
Projektowanie kerneli Model SPMD wprowadzany klasycznie poprzez identyfikatory wątków wątek na podstawie swojego identyfikatora może: zlokalizować dane, na których dokonuje przetwarzania dane[ f(my_id) ] wybrać ścieżkę wykonania programu if(my_id ==...){...} określić iteracje pętli, które ma wykonać for( i=f1(my_id); i< f2(my_id); i += f3(my_id) ){... } w tym aspekcie programowanie GPU jest zbliżone do modeli Pthreads i MPI Środowiska programowania GPU pozwalają na wykorzystanie liczb wątków rzędu milionów 13
OpenCL model pamięci Spójność pamięci: dla wątków tej samej grupy spójny obraz pamięci jest uzyskiwany w punktach synchronizacji np. wywołanie barrier (CLK_GLOBAL_MEM_FENCE); barrier (CLK_LOCAL_MEM_FENCE); dla wszystkich wątków wykonujących kernel spójny obraz pamięci jest uzyskiwany po wykonaniu kernela Szybkość pamięci: global local: ponad 100GB/s zależna od wzorca dostępu (coalesced memory access) host global: 6GB/s (PCIe 2.0) 14
Przykład Przykład dodawania wektorów (OpenCL) dekompozycja zadania analiza wydajności wydajność przetwarzania wydajność dostępu do pamięci czynniki wpływające na wydajność:» liczba wątków» liczba współbieżnych żądań dostępu do pamięci dla pojedynczego wątku» rozmiar danych w pojedynczym żądaniu 15
Projektowanie kerneli przykład Przykład programu dodawania wektorów dekompozycja danych wariant 1 jeden wątek na jeden element wektora wykorzystanie możliwości tworzenia milionów wątków zawstydzająco równoległy algorytm brak komunikacji, synchronizacji kernel void vecadd_1_kernel( global const float *a, global const float *b, global float *result) { int gid = get_global_id(0); result[gid] = a[gid] + b[gid]; } 16
Projektowanie kerneli przykład Prosty przykład programu dodawania wektorów dekompozycja danych wariant 2 wykorzystanie wariantów podziału danych blokowanie jeden wątek operuje na bloku danych kernel void vecadd_2_blocks_kernel(..., const int size, const int size_per_thread) { int gid = get_global_id(0); int index_start = gid * size_per_thread; int index_end = (gid+1) * size_per_thread; for (int i=index_start; i < index_end && i < size; i++) { result[i] = a[i]+b[i]; }} 17
Projektowanie kerneli przykład Prosty przykład programu dodawania wektorów dekompozycja danych wariant 3 wykorzystanie wariantów podziału danych podział cykliczny kolejne wątki z grupy operują na kolejnych elementach wektorów strategia niewłaściwa dla CPU, najlepsza dla GPU kernel void vecadd_3_cyclic_kernel(..., const int size) { int index_start = get_global_id(0); int index_end = size; int stride = get_local_size(0) * get_num_groups(0); for (int i=index_start; i < index_end; i+=stride) { result[i] = a[i]+b[i]; }} 18
Projektowanie kerneli przykład Prosty przykład programu dodawania wektorów dekompozycja danych wariant 3 wykorzystanie wariantów podziału danych podział cykliczny wykorzystanie typów wektorowych żądanie dostępu do pamięci dla pojedynczego wątku dotyczy 128 bitów, zamiast 32 kernel void vecadd_4_cyclic_vect_kernel( global const float4 *a, global const float4 *b, global float4 *result, const int size) { int index_start = 4 * get_global_id(0); int index_end = size/4; int stride = 4 * get_local_size(0) * get_num_groups(0); for (int i=index_start; i < index_end; i+=stride) { result[i] = a[i]+b[i]; }} 19
Analiza wydajności Analiza wydajności może być przeprowadzana na dwa standardowe sposoby: wydajność względna klasycznie oznacza to przyspieszenie i efektywność jako funkcje liczby procesorów/rdzeni dla pojedynczego GPU trudno przeprowadzać takie analizy analizy porównujące wydajność CPU i GPU stwarzają wiele problemów metodologicznych i powinny być używane tylko jako pewne wskazówki wydajność bezwzględna procent teoretycznej maksymalnej wydajności uzyskany dla danego wykonania programu wydajność może być ograniczana przez wydajność przetwarzania lub wydajność transferu danych z/do pamięci (dla obliczeń na GPU powinno się uwzględnić szybkości transferu dla wszystkich poziomów pamięci) 20
Analiza wydajności W przypadku prostego kernela dodawania wektorów wydajność jest w całości ograniczana przez szybkość dostępu do pamięci W celu maksymalizacji szybkości zapisu/odczytu należy wygenerować maksymalna liczbę żądań dostępu do pamięci: należy zwiększać liczbę aktywnych wątków ale istnieją ograniczenia na liczbę aktywnych wątków i grup wątków można zwiększać liczbę niezależnych dostępów do danych przez pojedynczy wątek np. poprzez wykorzystanie pobierania float4 (128 bitów) zamiast zamiast zwykłego float (32 bity) lub przez modyfikację kodu źródłowego, np. rozwijanie pętli (należy upewnić się, że dostępy są traktowane jako niezależne) 21
Przykład paramerów GPU Przykład karty graficznej (laptop Dell Vostro 3450): AMD Radeon HD 6630M 6 CU 480 PE 16384 rejestrów wektorowych (128 bitowych) / CU 32 kb pamięci wspólnej (32 banki) / CU 8 kb L1 / CU, 256 kb L2 / GPU 480 Gflops 25,6 GB/s DDR3, 51,2 GDDR5 ok. 250 GB/s pamięć wspólna maksymalny rozmiar grupy 256 (cztery wavefronts ) maksymalna liczba wavefronts 248 / GPU 22
Przykład paramerów GPU NVIDIA Tesla M2075 (architektura Fermi) 14 CU 448 PE ( 32PE / CU ) 32768 rejestrów 32 bitowych / CU 48 kb pamięci wspólnej / CU 73.6 GB/s / CU 16 kb L1 / CU, 224 kb L2 / GPU 515 DP GFLOPS 150 GB/s GDDR5 maksymalny rozmiar grupy 1024 (32 warps ) maksymalna liczba aktywnych grup / CU 8 maksymalna liczba aktywnych warps / CU 48 maksymalna liczba rejestrów na wątek 63 maksymalna liczba aktywnych wątków 1536 23
Przykład paramerów GPU NVIDIA Tesla K20 (architektura Kepler) 13 CU 2496 PE ( 192PE / CU ) 65536 rejestrów 32 bitowych / CU 48 kb pamięci wspólnej / CU 16 kb L1 / CU, 208 kb L2 / GPU 1170 DP GFLOPS 5GB GDDR5 RAM przepustowość 208 GB/s maksymalny rozmiar grupy 1024 (32 warps ) maksymalna liczba aktywnych grup / CU 16 maksymalna liczba aktywnych warps / CU 64 maksymalna liczba rejestrów na wątek 255 maksymalna liczba aktywnych wątków 2048 24
Przykład paramerów GPU AMD (ATI) Radeon HD 5870 20 CU 320x5 PE ( 16x5PE / CU ) każdy rdzeń posiada 5 ALU 544 DP GFLOPS 16384 rejestrów 4x32 bitowych / CU 13056 GB/s 32 kb pamięci wspólnej / CU 100 GB/s / CU, 2176 GB/s 8 kb L1 / CU przepustowość 1088 GB/s 512 kb L2 / GPU przepustowość L1 L2: 435 GB/s 1 GB GDDR5 RAM przepustowość 154 GB/s maksymalny rozmiar grupy 1024 (32 warps ) maksymalna liczba aktywnych grup / CU 8 maksymalna liczba aktywnych wavefronts / CU 24.8 maksymalna liczba rejestrów na wątek 63 maksymalna liczba aktywnych wątków 1536 25
Wyniki AMD Radeon HD 5870 PARAMETERS: nr_cu 20, nr_cores 6400, nr_cores_per_cu 320 workgroup size 256, nr_workgroups 240,nr_workgroups_per_CU 12 nr_threads 61440, nr_threads_per_cu 3072, nr_threads_per_core 3840 array size 31457280, nr_entries_per_thread 512, nr_entries_per_core 1966080 4. executing kernel 1, on platform 0 and device 0 EXECUTION TIME: executing kernel: 0.079807 (profiler: 0.079346) Number of operations 31457280, performance 0.396455 GFlops GBytes transferred to processor 0.377487, speed 4.757464 GB/s 4. executing kernel 2, on platform 0 and device 0 EXECUTION TIME: executing kernel: 0.003577 (profiler: 0.003200) Number of operations 31457280, performance 9.829715 GFlops GBytes transferred to processor 0.377487, speed 117.956580 GB/s 4. executing kernel 3, on platform 0 and device 0 (GPU) EXECUTION TIME: executing kernel: 0.003008 (profiler: 0.002649) Number of operations 31457280, performance 11.873163 GFlops GBytes transferred to processor 0.377487, speed 142.477954 GB/s 26
GPGPU 27
Przykład transpozycja macierzy 28
Przykład transpozycja macierzy 29
Przykład transpozycja macierzy Wykorzystanie: pamięci wspólnej: jako szybkiej pamięci podręcznej umożliwiającej komunikację pomiędzy watkami jawnej synchronizacji pracy wątków 30
CUDA kernel global void transpose( float *out, float *in, int w, int h ) { shared float block[block_dim*block_dim]; unsigned int xblock = blockdim.x * blockidx.x; unsigned int yblock = blockdim.y * blockidx.y; unsigned int xindex = xblock + threadidx.x; unsigned int yindex = yblock + threadidx.y; unsigned int index_out, index_transpose; if ( xindex < width && yindex < height ) { unsigned int index_in = width * yindex + xindex; unsigned int index_block = threadidx.y * BLOCK_DIM + threadidx.x; block[index_block] = in[index_in]; index_transpose = threadidx.x * BLOCK_DIM + threadidx.y; index_out = height * (xblock + threadidx.y) + yblock + threadidx.x; } synchthreads(); if(xindex<width&&yindex<height) out[index_out]=block[index_transpose]; } 31
Przykład mnożenie macierzy Przypomnienie: mnożenie macierzy jest algorytmem, dla którego przy nieskończonej liczbie rejestrów występuje bardzo korzystny stosunek liczby operacji do liczby dostępów do pamięci 3 2 s = (2n )/(3n ) ~ 2n/3 pm (n rozmiar macierzy) przy małej liczbie rejestrów i małym rozmiarze pamięci podręcznej naiwna implementacja prowadzi do znacznego spadku stosunku spm: 3 3 spm = (2n )/(n +...) ~ 2 implementacja naiwna schemat przechowywania wierszami: c(row, col) = c[row*n + col] for(i=0;i<n;i++){ for(j=0;j<n;j++){ c[i*n+j]=0.0; for(k=0;k<n;k++){ c[i*n+j] += a[i*n+k]*b[k*n+j]; } } } 32
Przykład mnożenie macierzy Naiwna implementacja GPU jeden wątek na jeden element macierzy wynikowej C Operacje wyłącznie na pamięci globalnej Nieoptymalny dostęp do tablicy A kernel void mat_mul_1_kernel( global float* A, global float* B, global float* C, int N ) { int i; int row = get_global_id(1); int col = get_global_id(0); float temp = 0.0; for (i = 0; i < N; i++) { temp += A[row * N + i] * B[i * N + col]; } C[row * N + col] = temp; } 33
Przykład mnożenie macierzy Klasyczna technika optymalizacji blokowanie Wyróżnienie bloków w tablicach A, B i C przechowywanych w szybkiej pamięci Wykonanie jak największej liczby operacji na blokach w szybkiej pamięci 34
Przykład mnożenie macierzy Implementacja blokowania: wariant 1 duże bloki: rozmiar bloku dobrany tak, żeby blok mieścił się w szybkiej pamięci (cache blocking) pojedynczy wątek wykonuje obliczenia dla wielu wyrazów bloku wariant 2 małe bloki: rozmiar bloku dobrany tak, żeby wartości mogły być przechowywane w rejestrach dla CPU i jednego wątku wiele zmiennych dla GPU np. jedna zmienna, ale wiele wątków 35
Przykład mnożenie macierzy int row = get_global_id(1); int local_row = get_local_id(1); float temp = 0.0; int col = get_global_id(0); int local_col = get_local_id(0); int nr_blocks = N/BLOCK_SIZE; for(iblock = 0; iblock < nr_blocks; iblock++){ A_local[local_row * BLOCK_SIZE + local_col] = A[row * N + iblock*block_size + local_col]; B_local[local_row * BLOCK_SIZE + local_col] = B[(local_row+iblock*BLOCK_SIZE) * N + col]; barrier(clk_local_mem_fence); for(i=0; i< BLOCK_SIZE; i++){ temp += A_local[local_row*BLOCK_SIZE+i] * B_local[i*BLOCK_SIZE+local_col]; } barrier(clk_local_mem_fence); } } C[row * N + col] = temp; 36