Wprowadzenie do programowania w środowisku CUDA Środowisko CUDA 1
Budowa procesora CPU i GPU Architektura GPU wymaga większej ilości tranzystorów na przetwarzanie danych Control ALU ALU ALU ALU Cache DRAM DRAM CPU GPU Środowisko CUDA 2
Architektura SIMT SIMT single-instruction, multiple-thread Mamy wiele procesorów na których działa ten sam program. Dokładnie posiada strumieniowe multiprocesory (SM Streaming Multiprocessor), które zawierają 32 małe, szybkie procesory. Na jednym multiprocesorze wątki (grupę taką nazywamy warpem) muszą wykonywać tą samą instrukcję tego samego programu (instrukcję spod tego samego adresu). Wątki łączymy w bloki, które następnie łączymy w grid. Bloki/gridy stanowią jedno-, dwu- lub trzywymiarową kostkę. Środowisko CUDA 3
Warp itp. Wątki w ramach jednego bloku wykonywane są w podziale na warp-y. Jeden warp to 32 wątki. Półwarp (half-warp) to 16 wątków (pierwsze 16 spośród 32 lub drugie 16 spośród 32). Jeśli w bloku jest mniej niż 32 wątki, nie jest wykorzystywana pełna moc obliczeniowa procesora! W ramach warp-a wszystkie wątki muszą wykonywać tę samą instrukcję, jednak w tym samym czasie inny warp z tego samego bloku może być wykonywany na innym procesorze i te drugie 32 wątki mogą wykonywać wspólnie inną instrukcję. System zarządzający wykonywaniem warp-ów sam decyduje, który w danej chwili uruchomić. Jeśli chcemy zsynchronizować wszystkie wątki w ramach jednego bloku należy użyć metody syncthreads(). Nie ma możliwości synchronizacji wątków w ramach całego grid-u. Zamiast tego można wywoływać z kodu hosta kolejne metody kernela. Występuje problem ze zmiennymi lokalnymi oraz pamięcią współdzieloną. Środowisko CUDA 4
Podział na bloki i wątki Grid Block (0,0) Block (1,0) Block (2,0) Block (0,1) Block (1,1) Block (1,2) Block (1,1) Thread(0,0) Thread(1,0) Thread(2,0) Thread(3,0) Thread(0,1) Thread(1,1) Thread(2,1) Thread(3,1) Thread(0,2) Thread(1,2) Thread(2,2) Thread(3,2) Środowisko CUDA 5
Rodzaje pamięci Każdy z wątków ma dostęp do różnego rodzaju pamięci: Pamięć rejestrów pamięć najbliżej procesora, najszybsza, bardzo mała liczba rejestrów Pamięć lokalna szybka pamięć dostępna dla konkretnego wątku Pamięć współdzielona szybsza pamięć dostępna dla wszystkich wątków w bloku, niezbyt duża, chociaż większa niż lokalna Pamięć stała podobna do pamięci globalnej, jednak szybsza, gdyż nie można jej zapisywać (read-only) Pamięć tekstur podobna do pamięci stałej Pamięć globalna wolna pamięć dostępna dla wszystkich wątków w gridzie Środowisko CUDA 6
Hierarchia pamięci rejestry Wątek pamięć lokalna Blok pamięć współdzielna Grid pamięć globalna Pamięć stała i tekstur Środowisko CUDA 7
Architektura CUDA Środowisko CUDA 8
Architektura CUDA 1. Silnik przetwarzania równoległego wewnątrz NVIDIA GPU. 2. Wsparcie na poziomie kernela OS dla inicjalizacji sprzętu, konfiguracji itp. 3. Sterownik na poziomie użytkownika, który dostarcza API dla programisty na poziomie urządzenia. 4. PTX instruction set architecture (ISA) dla kerneli i funkcji przetwarzania równoległego Środowisko CUDA 9
Przydział bloków do multiprocesorów Środowisko CUDA 10
Wykonywanie kodu na GPU Komputer (lub inne urządzenie) na którym jest zainstalowana karta z CUDA nazywany jest Hostem. Karta graficzna nazwana jest device. Program działający na hoście wywołuje funkcję napisaną dla device. Funkcja taka nazywana jest kernelem (parallel kernel). Po powrocie z wywołania funkcji program na hoście kontynuuje wykonanie własnego kodu. Może również wywołać następny kernel. Środowisko CUDA 11
Wykonywanie programu // kod sekwencyjny Host Kernel0<<<>>>() Device Block Block Grid // kod sekwencyjny Host Kernel1<<<>>>() // kod sekwencyjny Device Grid Host Block Block Block Block Block Block Środowisko CUDA 12
Synchronizacja kodu dla host i device Dopóki nie nastąpi synchronizacja, program na hoście i program na device mogą działać niezależnie. Program na hoście czeka na zakończenie programu na device, gdy: Stara się wykonać kolejną funkcję na tym device Następuje kopiowanie danych pomiędzy pamięcią globalną a pamięcią na hoście. Środowisko CUDA 13
Grid, blok, wątek Wątki grupuje się dwupoziomowo. Funkcję na device uruchamia się dla gridu, który jako składowe posiada bloki. Każdy blok natomiast dzieli się na wątki. Dla wygody indeks wątku w ramach bloku opisany jest jako 3- elementowy wektor. Pozwala to w praktyce używać jedno-, dwu- i trzywymiarowego indeksowania wątku. Na obecnych GPU maksymalna liczba wątków na blok wynosi 512. Jest to uwarunkowane szybkością pamięci współdzielonej. Grid może być opisany jako jedno- lub dwuwymiarowa siatka bloków. Bloki muszą działać poprawnie, niezależnie czy będą wykonywane równolegle, sekwencyjnie (w dowolnej kolejności). Nie ma możliwości wpływania bezpośredniego na kolejność wykonywania bloków ani na ich synchronizację. Dokładne graniczne rozmiary gridu lub bloku mogą zależeć od karty GPU i zmieniać się w przyszłości. Środowisko CUDA 14
Ograniczenia na funkcję kernela Kernel funkcja działająca na device Nie może korzystać z rekurencji W ciele funkcji nie można deklarować zmiennych statycznych Nie może zwracać wartości Ilość parametrów musi być określona Łączny rozmiar parametrów nie może przekraczać 256 bajtów (w Compute Compability 1.x) lub 4KB (w Compute Compability 2.0) Środowisko CUDA 15
Program pisany jak w C/C++ Kompilator NVCC Istnieją dodatkowe specyfikatory oraz znaczniki dla funkcji kernela, rodzaju pamięci oraz wywoływania tych funkcji. Przykładowy Makefile: CODENAME=dodMacierz CC=nvcc FLAGS=-L $(CUDA_LIB_PATH) -L $(CUDA_SDK_PATH)/C/lib/ -I $(CUDA_SDK_PATH)/C/common/inc FLAGS+=$(CUDA_NVCC_MACHINE_FLAG) FLAGS+=-arch=sm_13 FLAGS+=-lcutil $(CODENAME): $(CODENAME).cu $(CC) $(FLAGS) $< -o $@ Środowisko CUDA 16
Numer wątku Ponieważ wątki są uruchamiane w ramach bloku, a blok w ramach gridu, każdemu wątkowi przydzielany jest trzyelementowy wektor określający miejsce wątku w ramach bloku (w zmiennej lokalnej threadidx o polach x,y,z), trzyelementowy wektor określający rozmiar bloku (w zmiennej lokalnej blockdim o polach x,y,z) oraz trzyelementowy wektor określający miejsce bloku w ramach gridu (w zmiennej lokalnej blockidx o polach x,y,z). Jeśli liczba rozmiarów bloku/gridu jest mniejsza niż 3, odpowiednie pola (y lub y,z)mają wartość 1 Środowisko CUDA 17
Kernel funkcje i ich wywołanie // Definicja kernela global void VecAdd(float* A, float* B, float* C) { int i = threadidx.x; C[i] = A[i] + B[i]; } int main() {... // wywołanie kernela VecAdd<<<1, N>>>(A, B, C); } global jest specyfikatorem funkcji urządzenia wywoływanej z hosta, natomiast potrójne znaki mniejszości i większości służą do zdefiniowania gridu oraz bloku w ramach tego gridu. W tym przypadku grid jest jednowymiarowy (pierwszy parametr jest liczbą) i w dodatku zawiera tylko jeden blok. Blok ten też jest jednowymiarową strukturą i zawiera N wątków. Powoduje to, że w funkcji kernela wystarczy używać tylko pola x ze zmiennej threadidx. Środowisko CUDA 18
// Kernel definition Blok dwuwymiarowy global void MatAdd(float A[N][N], float B[N][N], float C[N][N]) { int i = threadidx.x; int j = threadidx.y; C[i][j] = A[i][j] + B[i][j]; } int main() {... // Kernel invocation dim3 dimblock(n, N); MatAdd<<<1, dimblock>>>(a, B, C); } W tym przypadku blok ten też jest dwuwymiarową strukturą i zawiera N*N wątków. Powoduje to, że w funkcji kernela wystarczy użyć tylko pól x i y ze zmiennej threadidx. Jak widać w konstruktorze struktury dim3 można podać dwa wymiary (zamiast 3), ostatni wymiar wynosi wówczas 1. W tym przykładzie parametry funkcji kernela są tablicami również o wymiarach N*N, ale nie jest to konieczne, ani wymagane. Środowisko CUDA 19
Grid dwuwymiarowy // Kernel definition global void MatAdd(float A[N][N], float B[N][N],float C[N][N]) { int i = blockidx.x * blockdim.x + threadidx.x; int j = blockidx.y * blockdim.y + threadidx.y; if (i < N && j < N) C[i][j] = A[i][j] + B[i][j]; } int main() {... // Kernel invocation dim3 dimblock(16, 16); dim3 dimgrid((n + dimblock.x 1) / dimblock.x,(n + dimblock.y 1) / dimblock.y); MatAdd<<<dimGrid, dimblock>>>(a, B, C); } W tym przykładzie blok i grid są dwuwymiarowymi strukturami. Każdy blok jest siatką 16*16 wątków. Natomiast wymiary grida są tak dobrane, aby można było przetworzyć macierz N*N elementów. Jeśli N % 16 nie jest 0, to w blokach skrajnych nie wszystkie wątki powinny pracować i stąd warunek w funkcji kernela. Dla wyznaczenia który wątek odpowiada za obliczenia poszczególnych elementów macierzy należy odpowiednio użyć zmiennych lokalnych blockidx, blockdim, threadidx. Analogicznie będzie jeśli blok będzie strukturą 3-wymiarową, ale trzeba wtedy użyć dodatkowo pól z odpowiednich zmiennych. Środowisko CUDA 20
Deklaracje pamięci W celu korzystania (zarezerwowania/zwolnienia oraz kopiowania z/do) z pamięci globalnej z poziomu hosta należy korzystać z odpowiednich funkcji: cudamalloc(void** ptr, size) rezerwacja pamięci pod wskaźnikiem zwracanym przez ptr o rozmiarze size bajtów cudamemcpy(void *ptrto, void *ptrfrom, size, int tryb) kopiowanie między pamięcią hosta a pamięcią device. Kierunek kopiowania definiuje tryb: cudamemcpyhosttodevice cudamemcpydevicetohost cudafree(void *ptr) zwalnia pamięć zarezerwowaną poprzez cudamalloc. cudamallocpitch(), cudamalloc3d(), cudamemcpy2d(), cudamemcpy3d() analogiczne do powyższych metod, ale z wykorzystaniem wyrównania celem osiągnięcia większej efektywności dostępu do pamięci. Środowisko CUDA 21
Pamięć c.d. Istnieją jeszcze inne metody rezerwowania pamięci z wyrównaniem. Wskaźniki do pamięci w parametrach funkcji kernela muszą być wskaźnikami do pamięci globalnej device. Przykład: // Device code global void VecAdd(float* A, float* B, float* C) { int i = blockdim.x * blockidx.x + threadidx.x; if (i < N) C[i] = A[i] + B[i]; } Środowisko CUDA 22
Kod c.d. int main() // Host code { int N =...; size_t size = N * sizeof(float); // Allocate input vectors h_a and h_b in host memory float* h_a = malloc(size); float* h_b = malloc(size); // Allocate vectors in device memory float *d_a, *d_b, *d_c; cudamalloc((void**)&d_a, size); cudamalloc((void**)&d_b, size); cudamalloc((void**)&d_c, size); // Copy vectors from host memory to device memory cudamemcpy(d_a, h_a, size, cudamemcpyhosttodevice); cudamemcpy(d_b, h_b, size, cudamemcpyhosttodevice); // Invoke kernel int threadsperblock = 256; int blockspergrid = (N + threadsperblock 1)/threadsPerBlock; VecAdd<<<blocksPerGrid, threadsperblock>>>(d_a, d_b, d_c); // Copy result from device memory to host memory // h_c contains the result in host memory cudamemcpy(h_c, d_c, size, cudamemcpydevicetohost); cudafree(d_a); // Free device memory cudafree(d_b); cudafree(d_c); } Środowisko CUDA 23
Pamięć współdzielona Pamięć współdzieloną wskazuje się przez kwalifikator shared Zadeklarowanie zmiennej tego typu w procedurze urządzenia tworzy pamięć, do której dostęp mają wszystkie wątki w jednym bloku. Po deklaracji każdy wątek wypełnia pewną część tej pamięci (każdy inną). Najczęściej jest to kopiowanie komórek z pamięci globalnej. Następnie wątki wykonują operacje matematyczne na tej pamięci (zamiast pamięci globalnej), a po zakończeniu obliczeń przepisują wyniki z pamięci współdzielonej do pamięci globalnej (znów każdy wątek inną część pamięci współdzielonej) Pamięć współdzielona nie może być inicjowana w momencie deklaracji. Gdy używana jest pamięć współdzielona należy synchronizować działanie wątków. Gdyby tego zrobiono, pewna część pamięci współdzielonej może nie być jeszcze zainicjowana lub obliczona. Jest to spowodowane wykonywaniem się bloku w podziale na warpy. Środowisko CUDA 24
Kod (fragment 1) // Matrix multiplication kernel global void MatMulKernel(Matrix A, Matrix B, Matrix C) { // Block row and column int blockrow = blockidx.y; int blockcol = blockidx.x; // Each thread block computes one sub-matrix Csub of C Matrix Csub = GetSubMatrix(C, blockrow, blockcol); // Each thread computes one element of Csub // by accumulating results into Cvalue float Cvalue = 0; // Thread row and column within Csub int row = threadidx.y; int col = threadidx.x; // Loop over all the sub-matrices of A and B that are // required to compute Csub // Multiply each pair of sub-matrices together // and accumulate the results for (int m = 0; m < (A.width / BLOCK_SIZE); ++m) { // Get sub-matrix Asub of A Środowisko CUDA 25
Kod (fragment 2) Matrix Asub = GetSubMatrix(A, blockrow, m); // Get sub-matrix Bsub of B Matrix Bsub = GetSubMatrix(B, m, blockcol); // Shared memory used to store Asub and Bsub respectively shared float As[BLOCK_SIZE][BLOCK_SIZE]; shared float Bs[BLOCK_SIZE][BLOCK_SIZE]; // Load Asub and Bsub from device memory to shared memory // Each thread loads one element of each sub-matrix As[row][col] = GetElement(Asub, row, col); Bs[row][col] = GetElement(Bsub, row, col); // Synchronize to make sure the sub-matrices are loaded // before starting the computation syncthreads(); // Multiply Asub and Bsub together for (int e = 0; e < BLOCK_SIZE; ++e) Cvalue += As[row][e] * Bs[e][col]; } // Synchronize to make sure that the preceding // computation is done before loading two new // sub-matrices of A and B in the next iteration syncthreads(); } // Write Csub to device memory // Each thread writes one element SetElement(Csub, row, col, Cvalue); Środowisko CUDA 26
Klasyfikator device Procedury (wewnętrzne) w ramach urządzenia posiadają klasyfikator device. Pamięć globalna w ramach urządzenia (inna niż z parametrów) posiada również klasyfikator device. Środowisko CUDA 27
Kod (Fragment 3) // Matrices are stored in row-major order: M(row, col) = *(M.elements + row * M.stride + col) typedef struct { int width; int height; int stride; float* elements; } Matrix; // Get a matrix element device float GetElement(const Matrix A, int row, int col){ return A.elements[row * A.stride + col]; } // Set a matrix element device void SetElement(Matrix A, int row, int col, float value){ A.elements[row * A.stride + col] = value; } // Get the BLOCK_SIZExBLOCK_SIZE sub-matrix Asub of A that is located col sub-matrices to the right and row sub-matrices down from the upper-left corner of A device Matrix GetSubMatrix(Matrix A, int row, int col){ Matrix Asub; Asub.width = BLOCK_SIZE; Asub.height = BLOCK_SIZE; Asub.stride = A.stride; Asub.elements = &A.elements[A.stride * BLOCK_SIZE * row + BLOCK_SIZE * col]; return Asub; } // Thread block size #define BLOCK_SIZE 16 Środowisko CUDA 28
Kod (Fragment 4) // Matrix multiplication - Host code; Matrix dimensions are assumed to be multiples of BLOCK_SIZE void MatMul(const Matrix A, const Matrix B, Matrix C){ Matrix d_a; // Load A and B to device memory d_a.width = d_a.stride = A.width; d_a.height = A.height; size_t size = A.width * A.height * sizeof(float); cudamalloc((void**)&d_a.elements, size); cudamemcpy(d_a.elements, A.elements, size,cudamemcpyhosttodevice); Matrix d_b; d_b.width = d_b.stride = B.width; d_b.height = B.height; size = B.width * B.height * sizeof(float); cudamalloc((void**)&d_b.elements, size); cudamemcpy(d_b.elements, B.elements, size, cudamemcpyhosttodevice); Matrix d_c; // Allocate C in device memory d_c.width = d_c.stride = C.width; d_c.height = C.height; size = C.width * C.height * sizeof(float); cudamalloc((void**)&d_c.elements, size); // Invoke kernel dim3 dimblock(block_size, BLOCK_SIZE); dim3 dimgrid(b.width / dimblock.x, A.height / dimblock.y); MatMulKernel<<<dimGrid, dimblock>>>(d_a, d_b, d_c); // Read C from device memory cudamemcpy(c.elements, d_c.elements, size, cudamemcpydevicetohost); // Free device memory cudafree(d_a.elements); cudafree(d_b.elements); cudafree(d_c.elements); } Środowisko CUDA 29