Programowanie Równoległe wykład 12 OpenGL + algorytm n ciał Maciej Matyka Instytut Fizyki Teoretycznej
CUDA z OpenGL 1. Dane dla kerneli znajdują się na karcie GFX. 2. Chcemy liczyć i rysować używając tych samych danych bez kopiowania CPU -> GPU -> CPU. 3. CUDA OpenGL interoperability: dane w buforach OpenGL (VBO, PBO) rysujemy bezpośrednio z buforów OpenGL mapujemy bufory na pamięć operacyjną CUDA wywołujemy kernele pracując na mapowanych adresach.
Bufor wierzchołków (OpenGL) Inicjalizacja bufora wierzchołków Dane na CPU float2 points[n]; Indeks bufora wierzchołków (VBO) GLuint pointsvbo; 1. Utworzenie nazwy bufora VBO (to generate) 2. Związanie (to bind) 3. Alokacja i skopiowanie danych (to buffer) glgenbuffers(1,&pointsvbo); glbindbuffer(gl_array_buffer, pointsvbo); glbufferdata(gl_array_buffer, sizeof(points), points, GL_STREAM_DRAW)
Używanie VBO przez CUDA CUDA OpenGL interoperability #include <cuda_gl_interop.h> Zgłoszenie zasobu OpenGL do API CUDA cudagraphicsglregisterbuffer( &pointsvbo_resource, pointsvbo, cudagraphicsmapflagsnone ); Mapowanie zasobu na adres pamięci CUDA cudagraphicsmapresources(1, &pointsvbo_resource, 0);... // pobierz adres bufora VBO... // wywołaj kernel cudagraphicsunmapresources(1, &pointsvbo_resource, 0);
Używanie VBO przez CUDA Pobranie adresu zgłoszonego i zmapowanego VBO float2 *pointsvbo_device; size_t num_bytes; cudagraphicsresourcegetmappedpointer( (void**)&pointsvbo_device, &num_bytes, pointsvbo_resource ); Wywołanie kernela (z przekazaniem adresu) global void movepardevice_vbo(float2 *p_device, float dt) { // p_device[..] =..; } void call_movepar_vbo(float2 *points_device, float dt) { movepardevice_vbo <<< NUM_BLOCKS, MAX_THREAD>>> (points_device, dt); }
Kompilacja LINUX Przetestowane na pracowni 426 Kompilacja CUDA + GLUT (OpenGL) > nvcc main.cpp kernels.cu -lglut lglu >./a.out Kompilacja CUDA + GLUT + GLEW > nvcc main.cpp kernels.cu -lglut lglu -lglew >./a.out
Podsumowanie OpenGL+CUDA Inicjalizuj urządzenie (cudaglsetgldevice(0); ) Inicjalizuj GLUT, GLEW Inicjalizuj dane na CPU Stwórz bufor VBO (lub inny) na karcie Zgłoś bufor do CUDA 1. Mapuj bufor na adres pamięci operacyjnej CUDA 2. Pobierz adres bufora w pamięci CUDA 3. Wywołaj kernel modyfikujący dane w buforze 4. Zwolnij zasoby (Unmap) 5. Idź do 1
(przykład 1000000 punktów)
Problem (426) Zgłoszenie zasobu do CUDA cudagraphicsglregisterbuffer( &pointsvbo_resource, pointsvbo, cudagraphicsmapflagsnone ); Nie działa w pracowni 426 ( GTS 250? ) Kod błędu: UnknownError OpenGL? Compute capability 1.1? Aplikacja przykładowa przetestowana została z sukcesem na: GTX 285 (Windows 7, CUDA 3.2), GTX 260 (Linux), GTX 460 (Linux, Ubuntu, Cuda 4.0)
Sprawdzanie błędów Większość funkcji CUDA zwraca kody błędów, np. cudaerror_t cudagraphicsglregisterbuffer ( struct cudagraphicsresource **resource, Gluint buffer, unsigned int flags ) Ww funkcja może zwrócić np. cudasuccess, cudaerrorinvaliddevice, cudaerrorinvalidvalue, cudaerrorinvalidresourcehandle, cudaerrorunknown Błędy można łatwo sprawdzić (if, switch, etc.)
CUTIL_SAFE_CALL MACRO biblioteki CUTIL Nie wchodzi w oficjalną dystrybucję API CUDA Używane w SDK dla CUDA Ogólnie nie jest polecane używanie CUTIL forum nvidii Ale możemy skorzystać z kodów źródłowych CUTIL: #define CUDA_SAFE_CALL_NO_SYNC( call) { cudaerror err = call; if( cudasuccess!= err) { fprintf(stderr, "Cuda error in file '%s' in line %i : %s.\n", FILE, LINE, cudageterrorstring(err)); exit(exit_failure); } }
Algorytm n Ciał N oddziałujących punktów materialnych Długozasięgowe oddziaływania par Potencjał grawitacyjny Potencjał Lennarda-Jonesa Pętla obliczeniowa: Obliczenie sił Całkowanie równań ruchu
Rozwiązanie 1. CPU Algorytm naiwny z podwójną pętlą - O(n^2) Kod wyznaczający pozycję punktów: (N=32000, 42 sekundy)
Rozwiązanie 2. GPU Algorytm naiwny z podwójną pętlą - O(n^2) Kod wyznaczający pozycję punktów: (N=32000, ~2 sekundy)
Rozwiązanie 3. GPU+pamięć shared Mapowanie wątek -> numer ciała int gtid = blockidx.x * blockdim.x + threadidx.x; N liczba punktów P liczba wątków w bloku N/P liczba bloków
Rozwiązanie 3. GPU+pamięć shared N wątków: 1. k-ty wątek wykonuje pętlę po blokach (i = 0 - N/P) 2. k-ty wątek wpisuje do pamięci shared swojego bloku: shpositions[k] = i * P + k 3. syncthreads(); 4. Akumulacja sił (pętla po shpositions[k]) 5. syncthreads(); 6. Wróć do 2.
global void movepardevice_vbo2(float2 *p_device, float dt) { int k = blockidx.x * blockdim.x + threadidx.x; if(k>=n) return; float2 p, f, v; v = v_device[k]; f = make_float2(0,0); int i,tile; p = p_device[k]; for(tile=0; tile < N / THREADS_PER_BLOCK; tile++) { i = tile * blockdim.x + threadidx.x; shposition[threadidx.x] = p_device[i]; syncthreads(); f = accumulate_tile(p, f); syncthreads(); } } // integrate v.x = v.x + f.x * dt; v.y = v.y + f.y * dt; p.x = p.x + v.x * dt; p.y = p.y + v.y * dt; p_device[k]=p; v_device[k]=v;
global void movepardevice_vbo2(float2 *p_device, float dt) { int k = blockidx.x * blockdim.x + threadidx.x; if(k>=n) return; float2 p, f, v; v = v_device[k]; f = make_float2(0,0); int i,tile; p = p_device[k]; for(tile=0; tile < N / THREADS_PER_BLOCK; tile++) { i = tile * blockdim.x + threadidx.x; shposition[threadidx.x] = p_device[i]; syncthreads(); f = accumulate_tile(p, f); syncthreads(); } } // integrate v.x = v.x + f.x * dt; v.y = v.y + f.y * dt; p.x = p.x + v.x * dt; p.y = p.y + v.y * dt; p_device[k]=p; v_device[k]=v;
global void movepardevice_vbo2(float2 *p_device, float dt) { int k = blockidx.x * blockdim.x + threadidx.x; if(k>=n) return; float2 p, f, v; v = v_device[k]; f = make_float2(0,0); int i,tile; p = p_device[k]; for(tile=0; tile < N / THREADS_PER_BLOCK; tile++) { i = tile * blockdim.x + threadidx.x; shposition[threadidx.x] = p_device[i]; syncthreads(); f = accumulate_tile(p, f); syncthreads(); } } // integrate v.x = v.x + f.x * dt; v.y = v.y + f.y * dt; p.x = p.x + v.x * dt; p.y = p.y + v.y * dt; p_device[k]=p; v_device[k]=v;
global void movepardevice_vbo2(float2 *p_device, float dt) { int k = blockidx.x * blockdim.x + threadidx.x; if(k>=n) return; float2 p, f, v; v = v_device[k]; f = make_float2(0,0); int i,tile; p = p_device[k]; for(tile=0; tile < N / THREADS_PER_BLOCK; tile++) { i = tile * blockdim.x + threadidx.x; shposition[threadidx.x] = p_device[i]; syncthreads(); f = accumulate_tile(p, f); syncthreads(); } } // integrate v.x = v.x + f.x * dt; v.y = v.y + f.y * dt; p.x = p.x + v.x * dt; p.y = p.y + v.y * dt; p_device[k]=p; v_device[k]=v;
Wyznaczenie sił dla kompletnego bloku: device float2 accumulate_tile(float2 p, float2 f) { int i; for(i=0; i < THREADS_PER_BLOCK; i++) f = force(shposition[i], p, f); } return f;
W działaniu N=16000 [uruchom] Porównanie wydajności 1 core CPU vs G80 [animacja]
N80 i n200 Rys: Porównanie wydajności naiwnego algorytmu n-body do wersji z pamięcią shared. różne wyniki dla innych architektur: n80 (lewy) i n200 (prawy) zmniejszone znaczenie pamięci shared efektywniejszy dostęp do pamięci globalnej
Dygresja: przepustowość max Teoretyczna przepustowość naszej karty GTS 250 używa pamięci DDR taktowanej zegarem1100mhz = 1100 x 1e6 =1.1e9 Hz Dostęp przez interfejs 512 bitowy = 64 bajtowy Maksymalna przepustowość teoretyczna 2 * częstotliwość * szerokość szyny / bity w gigabajtach Max Band = 2 x 1.1e9 x 64 / 1e9 = 140 GB / s
Dygresja: przepustowość efektywna Eff band = ( ( Br + Bw) / 1e9 ) / czas Br: bajty odczytane przez jądro Bw: bajty zapisane przez jądro 1e9: bity w gigabajcie czas: czas wykonania algorytmu Przykład: nbody N=32000 Br=32000+32000*32000 Bw=32000*2 Czas = 0.35 Efektywna przepustowość: 2,9 GBps
Za Best Programming Guide 3.2 GBps,
Efektywna przepustowość (nbody) optymalizacja G200 g80 Bez pamięci shared 1.79 GBps 0,55 GBps Z pamięcią shared 2.9 GBps 2,93 GBps Wnioski: g200 nie zwiększył bardzo przepustowości wydajnego algorytmu z pamięcią shared
Optymalizacje Rozwijanie pętli w kernelach (#pragma unroll ) device float2 accumulate_tile(float2 p, float2 f) { int i; #pragma unroll for(i=0; i < THREADS_PER_BLOCK; i++) f = force(shposition[i], p, f); } return f; Dzielenie (poj. precyzja) Zamiast / -> fdividef Pierwiastek Zamiast 1/sqrt -> rsqrtf() Więcej: http://developer.download.nvidia.com/compute/cuda/3_0/toolkit/docs/nvidia_cuda_programmingguide.pdf
Ćwiczenia points-cuda-gl-interop.zip Ściągnij Skompiluj i uruchom Zbadaj t(n) czas wykonania od liczby punktów Narysuj wykres t(n) Spróbuj zoptymalizować? nbody-nopt.zip Ściągnij Skompiluj i uruchom Zoptymalizuj (unroll, sqrt) Zbadaj t(n) czas wykonania od liczby punktów Narysuj wykresy t(n) w zależności od optymalizacji
Za tydzień Następny wykład 1.02.2011 Badanie wydajności w praktyce Cuda Thrust STL dla CUDA?