CUDA obliczenia ogólnego przeznaczenia na mocno zrównoleglonym sprzęcie W prezentacji wykorzystano materiały firmy NVIDIA (http://www.nvidia.com) 1
Architektura karty graficznej W porównaniu z tradycyjnym procesorem karta graficzna ma stosunkowo prosty moduł kontrolny, który jest silnie zrównoleglony. Dodatkową cechą charakterystyczną jest duża liczba jednostek obliczeniowych i relatywnie mniej pamięci cache.
Potencjał obliczeniowy Uwaga: rysunek przedstawia TEORETYCZNE możliwości sprzętu.
Przepustowość pamięci Uwaga: rysunek przedstawia TEORETYCZNE możliwości sprzętu.
Wady i zalety Zalety Bardzo dobry stosunek ceny do (potencjalnej) szybkości. Szybki rozwój sprzętu. Rozwojowa technologia. Wady Konieczność przepisania kodu problem przenośności. Uzyskanie rzeczywistej wydajności zbliżonej do teoretycznych możliwości sprzętu wymaga dużo pracy.
Technologie CUDA (od Compute Unified Device Architecture) platforma programistyczna i model programowania stworzony przez firmę NVIDIA. (C/C++, Fortran,...) OpenCL (Open Computing Language) szkielet do aplikacji na heterogenicznych platformach z CPU, GPU, DSP. (Język oparty o C99 i API). DirectCompute API dla Windows (Vista, 7, 8) wspierane przez Microsoft, część DirectX.
Dojrzałość CUDA wydaje się być rozwiązaniem najbardziej dojrzałym: powstała w 2006 roku (15.02.2007 upubliczniona dla MS Windows i Linuksa); obecna wersja to 5.0 (październik 2012); wsparcie dla wielu języków; zaimplementowane liczne biblioteki.
CUDA Specyfikacja architektury urządzenia pozwala wprowadzić drobnoziarnistą równoległość danych (Pcam) wątki, pozwala zoptymalizować współpracę wątków (pcam) grupy wątków, pozwala transparentnie mapować strukturę programu na architekturę urządzenia (pcam). Model programistyczny zbiór rozszerzeń/restrykcji/konwencji dla języków programowania, biblioteki.
Streaming Multiprocesor SM podstawowa jednostka architektury karty 8 skalarnych procesorów, wszystkie mogą działać (lub spać) równocześnie, przydział instrukcji za darmo, do 32 wątków (warp) wykonywanych na raz przez SM (w 4 cyklach zegara), do 24 aktywnych warpów w jednym SM, wątki współdzielą część pamięci, każdy wątek ma własną pamięć rejestrową.
Architektura GPU dla programu CPU
Skalowalność Użytkownik (programista) projektuje sieć wątków dopasowaną do danych. Sprzęt (karta) dopasowuje bloki do aktualnej architektury.
Przykład (1) oer:~/downloads/nvidia_cuda 5.0_Samples/bin/linux/release>./deviceQuery [...] Device 0: "GeForce 9800 GT" CUDA Driver Version / Runtime Version 5.0 / 5.0 CUDA Capability Major/Minor version number: 1.1 Total amount of global memory: 1023 MBytes (1073020928 bytes) (14) Multiprocessors x ( 8) CUDA Cores/MP: 112 CUDA Cores GPU Clock rate: 1500 MHz (1.50 GHz) Memory Clock rate: 900 Mhz Memory Bus Width: 256 bit Max Texture Dimension Size (x,y,z) 1D=(8192), 2D=(65536,32768), 3D=(2048,2048,2048) Max Layered Texture Size (dim) x layers 1D=(8192) x 512, 2D=(8192,8192) x 512 Total amount of constant memory: 65536 bytes Total amount of shared memory per block: 16384 bytes Total number of registers available per block: 8192 Warp size: 32 Maximum number of threads per multiprocessor: 768 Maximum number of threads per block: 512 Maximum sizes of each dimension of a block: 512 x 512 x 64 Maximum sizes of each dimension of a grid: 65535 x 65535 x 1 Maximum memory pitch: 2147483647 bytes Texture alignment: 256 bytes Concurrent copy and kernel execution: Yes with 1 copy engine(s) Run time limit on kernels: Yes Integrated GPU sharing Host Memory: No Support host page locked memory mapping: Yes Alignment requirement for Surfaces: Yes Device has ECC support: Disabled Device supports Unified Addressing (UVA): No Device PCI Bus ID / PCI location ID: 1 / 0 [...] 1: Tesla
Przykład (2) [jstar@n2 ~/cuda/bin/linux/release]./devicequery [...] Detected 4 CUDA Capable device(s) Device 0: "Tesla C2070" CUDA Driver Version / Runtime Version 5.0 / 5.0 CUDA Capability Major/Minor version number: 2.0 Total amount of global memory: 5375 MBytes (5636554752 bytes) (14) Multiprocessors x ( 32) CUDA Cores/MP: 448 CUDA Cores GPU Clock rate: 1147 MHz (1.15 GHz) Memory Clock rate: 1494 Mhz Memory Bus Width: 384 bit L2 Cache Size: 786432 bytes Max Texture Dimension Size (x,y,z) 1D=(65536), 2D=(65536,65535), 3D=(2048,2048,2048) Max Layered Texture Size (dim) x layers 1D=(16384) x 2048, 2D=(16384,16384) x 2048 Total amount of constant memory: 65536 bytes Total amount of shared memory per block: 49152 bytes Total number of registers available per block: 32768 Warp size: 32 Maximum number of threads per multiprocessor: 1536 Maximum number of threads per block: 1024 Maximum sizes of each dimension of a block: 1024 x 1024 x 64 Maximum sizes of each dimension of a grid: 65535 x 65535 x 65535 Maximum memory pitch: 2147483647 bytes Texture alignment: 512 bytes Concurrent copy and kernel execution: Yes with 2 copy engine(s) Run time limit on kernels: No Integrated GPU sharing Host Memory: No Support host page locked memory mapping: Yes Alignment requirement for Surfaces: Yes Device has ECC support: Enabled Device supports Unified Addressing (UVA): Yes Device PCI Bus ID / PCI location ID: 2 / 0 [...] 2: Fermi
CUDA Kernel Kernel (jądro) to specjalna funkcja, która może być uruchomiona przez wiele wątków na raz. Strukturę sieci wątków używanej do uruchomienia kernela określa się w momencie jego wywołania.
Przykład kernela W powyższym przykładzie kernel VecAdd jest uruchamiany na liniowej strukturze N wątków. Numer wątku jest dostępny wewnątrz kernela przy pomocy struktury (dim3) threadidx
Struktura wątków Zmienna threadidx ma trzy wymiary (x,y,z) do pozwala naturalnie rozpraszać obliczenia dla danych 1D, 2D i 3D Zmienna threadidx indeksuje wątki w ramach bloku, którego maksymalna wielkość nie może obecnie przekraczać 1024 (starsze karty mogą pozwalać na mniej). W ramach bloku wątki są numerowane zgodnie z zasadą x+y Dx + z Dx Dy
Przykład 2D
Sieć bloków wątków Bloki wątków można organizować w sieci. Kernel może zostać uruchomiony na dowolnej sieci bloków o identycznym kształcie. Podobnie jak blok, także i sieć może być 1-, 2- lub 3wymiarowa. Numer bloku jest dostępny wewnątrz kernela przez zmieną (strukturę dim3) blockidx.
Przykład sieci bloków (1) 256 (16x16) wątków w bloku, N/16 x N/16 = N2/256 bloków = 1 wątek na element tablicy.
threadidx.x blockdim.x blockdim.y Przykład sieci bloków (2) Bloki są wykonywane niezależnie, w dowolnym porządku, równolegle lub sekwencyjnie. Wątki w bloku współdzielą część pamięci (szybkiej). Wątki w bloku można synchronizować (lekka bariera).
Pamięć Wątki mogą korzystać z kilku obszarów pamięci: Każdy wątek ma przydzieloną prywatną pamięć lokalną Każdy blok wątków ma przydzielony wspólny obszar pamięci Wszystkie sieci wątków mogą korzystać z obszaru pamięci globalnej Wszystkie sieci wątków mogą też czytać pamięć stałą i pamięć tekstur.
Model programu Model programu CUDA przypomina trochę model OpenMP: części równoległe wykonywane na urządzeniu CUDA przeplatają się z kodem sekwencyjnym wykonywanym przez procesor. Urządzenie CUDA można traktować jako ko-procesor posiadający własną pamięć. Kopiowanie danych pomiędzy pamięcią procesora i pamięcią karty może być wąskim gardłem programu.
Program Pamięć karty może być dostępna na dwa sposoby. Przy pierwszym (dostęp liniowy) jest alokowana przez cudamalloc i zwalniana za pomocą cudafree. Adres jest 32b dla urządzeń 1.x i 40b dla urządzeń 2.x i nowszych (można używać wskaźników pomiędzy adresami). Kopiowanie danych na/z urządzenia za pomocą cudamemcpy.
Kompilacja (JIT)
Efektywność (szybkość) Maksymalizacja zrównoleglenia Aplikacja (w miarę możliwości asynchronicznie) Urządzenie CUDA (liczba bloków == liczba MP, użycie strumieni) Multiprocesor (aktywne warpy) Optymalizacja dostępu do pamięci Jak najmniej transferów host urządzenie Użycie szybkiej pamięci (shared, cache) Dopasowanie danych do struktury pamięci Optymalizacja użycia instrukcji Użycie lekkich instrukcji/funkcji Minimalizacja zróżnicowania wątków Minimalizacja zróżnicowania używanych instrukcji
Zrównoleglenie aplikacja Równoległe działanie hosta i urządzenia Nakładanie transferu danych i kerneli Równoległy transfer danych Strumienie Zdarzenia
Równoległe działanie hosta i urządzenia Następujące funkcje są wykonywane asynchronicznie Uruchamianie kerneli Kopiowanie danych w ramach jednego urządzenia Kopiowanie host urządzenie porcji danych <64kB Asynchroniczne funkcje kopiujące (xxxasync) Ustawianie wartości w pamięci Kernele uruchamiane są synchronicznie pod debuggerem, profilerem, przy kontroli pamięci i na urządzeniach 1.x. Można zablokować asynchroniczne uruchamianie kerneli przez ustawienie zmiennej środowiskowej CUDA_LAUNCH_BLOCKING=1
Równoległe działanie urządzenia w skali makro (1) Od urządzeń 1.1 kopiowanie danych z/na host może być wykonywane równolegle z wykonywaniem kerneli. Można sprawdzić, czy urządzenie umożliwia to przez wartość właściwości asyncenginecount (0 nie, dodatnie tak). W urządzeniach 1.x dotyczy to tylko liniowych danych. Od urządzeń 2.x wiele urządzeń potrafi kopiować z i na urządzenie równolegle. Można sprawdzić, czy urządzenie umożliwia to przez wartość właściwości asyncenginecount (2 i więcej tak).
Równoległe działanie urządzenia w skali makro (2) Od urządzeń 2.x wiele kerneli może być uruchamianych równolegle na tym samym urządzeniu. Można sprawdzić, czy urządzenie umożliwia to przez wartość właściwości concurrentkernels (1 tak). Liczba równoległych kerneli jest ograniczona do 16 (32 dla urządzeń 3.5 i nowszych). Ograniczeniem może też być pamięć używana przez kernel. Wszystkie rónoległe kernele muszą należeć do tego samego kontekstu (CUDA context ~ proces dla CPU).
Strumienie Strumień (stream) to sekwencja poleceń. Domyślnie istnieje jeden strumień o numerze 0. Można tworzyć dodatkowe strumienie i używać ich numerów, tworząc w ten sposób równoległe sekwencje poleceń.
Strumienie przykład (1) Dwa strumienie, każdy z nich kopiuje z hosta na urządzenie size danych, uruchamia MyKernel dla swoich danych, kopiuje dane na host. Ten kod będzie działał na kartach, które obsługują równoległy transfer danych.
Synchronizacja strumieni Jawna zestaw funkcji cudadevicesynchronize() cudastreamsynchronize() cudastream WaitEvent() cudastreamquery() Niejawna Alokacja pamięci na hoscie (cudamalloc) Alokacja/zapis/kopiowanie na urządzeniu Polecenie CUDA dla strumienia 0 Zmiana konfiguracji pamięci w urządzeniach 2.x
Wywołania wsteczne Funkcja MyCallback zostanie uruchomiona po zakończeniu wszystkich operacji na strumieniu uruchomionych przed rejestracją wywołania wstecznego.
Zdarzenia Zdarzenia są rejestrowane asynchronicznie w strumieniach Zdarzenia są kończone w momencie, gdy skończą się wszystkie zadania i komendy strumienia poprzedzające rejestrację zdarzenia. Zdarzenia w strumieniu 0 są kończone, gdy skończą się wszystkie zadania i komendy we wszystkich strumieniach.
Strumienie przykład (2) Dwa strumienie, każdy z nich kopiuje z hosta na urządzenie size danych, uruchamia MyKernel dla swoich danych, kopiuje dane na host. Ten kod będzie działał na kartach, które obsługują równoległy transfer danych i działanie kerneli.
Optymalizacja dostępu do pamięci urządzenia Pamięć współdzielona przez blok jest znacznie szybsza, niż pamięć globalna. Odpowiednia organizacja wątków i wykorzystanie pamięci współdzielonej może bardzo znacznie przyspieszyć kod.
Mnożenie macierzy (1)
Mnożenie macierzy (2.1)
Mnożenie macierzy (2.2)
Naiwnie Pamięć współdzielona
Transpozycja macierzy (1) #define NN (4096) int main(int args, char** vargs) { const int HEIGHT = NN; const int WIDTH = NN; const int SIZE = WIDTH * HEIGHT * sizeof(float); cudaevent_t start, stop; cudaeventcreate(&start); cudaeventcreate(&stop); dim3 blockdim(block_dim,block_dim); dim3 griddim(width / blockdim.x, HEIGHT / blockdim.y); float* m = (float*)malloc(size); float* md = NULL; cudaeventrecord(start, 0); cudamalloc((void**)&md, SIZE); cudamemcpy(md, m, SIZE, cudamemcpyhosttodevice ); float* td = NULL; cudamalloc((void**)&td, SIZE); global void transpose_naive(float *odata, float* idata, int w, int h) { unsigned int x = blockdim.x * blockidx.x + threadidx.x; unsigned int y = blockdim.y * blockidx.y + threadidx.y; if (x < w && y < h ) { transpose_naive<<<griddim, blockdim>>>(td, md, WIDTH, HEIGHT); unsigned int idx_in = x + w * y ; unsigned int idx_out = y + h * x; odata[idx_out] = idata[idx_in]; cudamemcpy(m, td, SIZE, cudamemcpydevicetohost); } } cudaeventrecord(stop, 0); cudaeventsynchronize(stop); float elapsedtime; cudaeventelapsedtime(&elapsedtime, start, stop); cudaeventdestroy(start); cudaeventdestroy(stop); 363.791 ms printf( "Time=%g\n", elapsedtime ); return 0; }
Transpozycja macierzy (2) global void transpose(float *odata, float *idata, int w, int h) { shared float block[block_dim][block_dim+1]; // read the matrix tile into shared memory unsigned int x = blockidx.x * BLOCK_DIM + threadidx.x; unsigned int y = blockidx.y * BLOCK_DIM + threadidx.y; if((x < w ) && (y < h )) { unsigned int index_in = y * w + x ; block[threadidx.y][threadidx.x] = idata[index_in]; } syncthreads(); // write the transposed matrix tile to global memory x = blockidx.y * BLOCK_DIM + threadidx.x; y = blockidx.x * BLOCK_DIM + threadidx.y; if((x < h ) && (y < w )) { unsigned int index_out = y * h + x ; odata[index_out] = block[threadidx.x][threadidx.y]; } }
Transpozycja macierzy (3) global void transpose(float *odata, float *idata, int w, int h) { shared float block[block_dim][block_dim+1]; // read the matrix tile into shared memory unsigned int x = blockidx.x * BLOCK_DIM + threadidx.x; unsigned int y = blockidx.y * BLOCK_DIM + threadidx.y; if((x < w ) && (y < h )) { unsigned int index_in = y * w + x ; block[threadidx.y][threadidx.x] = idata[index_in]; } syncthreads(); // write the transposed matrix tile to global memory x = blockidx.y * BLOCK_DIM + threadidx.x; y = blockidx.x * BLOCK_DIM + threadidx.y; if((x < h ) && (y < w )) { unsigned int index_out = y * h + x ; odata[index_out] = block[threadidx.x][threadidx.y]; } } 137.592 ms
Transpozycja na różnych zdolności obliczeniowych oer:~/pliki/dydaktyka/prir/prir/programy/cuda>./a.out 0 1 2 3 4 5 6 7 8 9 Time of copy kernel=3.26445 Time of transpose_naive kernel=246.948 Time of transpose kernel=19.1968 0 4096 8192 12288 16384 20480 24576 28672 32768 36864 oer:~/pliki/dydaktyka/prir/prir/programy/cuda> [jstar@n1 ~/mycuda]./a.out 0 1 2 3 4 5 6 7 8 9 Time of copy kernel=1.74317 Time of transpose_naive kernel=2.92221 Time of transpose kernel=2.06314 0 4096 8192 12288 16384 20480 24576 28672 32768 36864 [jstar@n1 ~/mycuda] Tesla (1.1) Fermi (2.0)