Porównanie wydajności CUDA i OpenCL na przykładzie równoległego algorytmu wyznaczania wartości funkcji celu dla problemu gniazdowego Mariusz Uchroński 3 grudnia 2010
Plan prezentacji 1. Wprowadzenie 2. Problem gniazdowy 3. Metoda zrównoleglenia obliczeń 4. Eksperymenty obliczeniowe 5. Podsumowanie
Wprowadzenie CUDA (ang. Compute Unified Device Architecture) i OpenCL (ang. Open Computing Language) pozwalają na wykorzystanie GPU (ang. Graphics Processing Unit) do obliczeń. Wydajność CUDA i OpenCL została przeanalizowana na przykładzie równoległego algorytmu wyznaczania wartości funkcji celu dla problemu gniazdowego.
Problem gniazdowy Problem gniazdowy można zdefiniować w następujący sposób: w systemie produkcyjnym znajduje się określona liczna maszyn, na których należy wykonać określona liczbę zadań produkcyjnych, zadanie produkcyjne składa się z pewnej liczby operacji, które należy zrealizować w zadanej kolejności, dla każdej operacji określona jest dokładnie jedna maszyna, która może ja wykonać w zadanym czasie.
Problem gniazdowy Rysunek: Wykres Gantta.
Problem gniazdowy Rysunek: Graf reprezentujący rozwiązanie.
Metoda zrównoleglenia obliczeń Tradycyjna metoda wyznaczania wartości funkcji celu dla problemu gniazdowego jest trudna do zrównoleglenia ze względu na jej silnie sekwencyjny charakter. Modyfikacja metody wyznaczania wartości funkcji celu dla problemu gniazdowego do postaci, która pozwala na masowe zrównoleglenie obliczeń polega na wykorzystaniu algorytmu wyznaczania najdłuższej ścieżki w grafie w sensie liczby wierzchołków.
Algorytm sekwencyjny void FindPathsOnCPU(int o, int *graph, int size) { int tmp,max; for (int iter=1; iter <= size+1; iter++) for (int u = 0; u <= o; u++){ for (int v = 0; v <= o; v++){ tmp = 0; max = 0; for (int k = 0; k <= o; k++){ if (graph[u*(o+1)+k]!= 0 and graph[k*(o+1)+v]!= 0) { tmp = graph[u*(o+1)+k]+graph[k*(o+1)+v]; if (max < tmp) max = tmp; if (graph[u*(o+1)+v] < max) graph[u*(o+1)+v] = max;
Algorytm równoległy Główne elementy prostej w zrównolegleniu metody wyznaczania wartości funkcji celu dla problemu gniazdowego to: 1. Wyznaczenie najdłuższej ścieżki (w sensie liczby wierzchołków) w grafie reprezentującym rozwiązanie. 2. Posortowanie odległości od wierzchołka początkowego do pozostałych wierzchołków w kolejności rosnącej. Indeksy wierzchołków po posortowaniu powinny być w kolejności topologicznej.
Eksperymenty obliczeniowe Eksperymenty obliczeniowe zostały przeprowadzone na kartach graficznych: ATI Radeon HD 5970 2GB DDR5, Nvidia GeForce GTX 480 1536MB. Zaimplementowana została wersja algorytmu wykorzystująca pamięć globalną oraz pamięć współdzieloną. Zmierzone czasy uwzględniają czas kopiowania danych między GPU, a hostem.
Eksperymenty obliczeniowe Pamięć globalna kernel void JobShopKernel(int o, global int *graph, int size) { int idx = (int)get_global_id(0); for (int iter=1; iter <= size+1; iter++) for(int u=0;u<=o;u++) for(int v=0;v<=o;v++){ int max = 0, tmp =0; if(graph[u*(o+1)+idx]!=0 && graph[idx*(o+1)+v]!=0){ tmp=graph[u*(o+1)+idx]+graph[idx*(o+1)+v]; if(max < tmp) max = tmp; if (graph[u*(o+1)+v] < max) graph[u*(o+1)+v] = max; barrier(clk_global_mem_fence);
Eksperymenty obliczeniowe Pamięć globalna problem o OpenCL ATI OpenCL NV CUDA CPU la01-05 50 0.0425 0.0110 0.0110 0.0042 la06-10 75 0.0993 0.0294 0.0303 0.0191 la11-15 100 0.1766 0.0600 0.0624 0.0401 la16-20 100 0.1758 0.0591 0.0615 0.0359 la21-25 150 0.4525 0.1546 0.1629 0.1384 la26-30 200 0.8128 0.3139 0.3309 0.3312 la31-35 300 1.8591 0.9968 1.0479 1.2886 la36-40 225 1.0404 0.4063 0.4281 0.4375 Tabela: Porównanie czasów trwania obliczeń dla pamięci globalnej
Eksperymenty obliczeniowe Pamięć współdzielona global void JobShopKernelShared(int o, int *graph, int size) { int idx = threadidx.x ; extern shared int cache[]; for(int i=0;i<=o;i++) cache[i*(o+1)+idx]=graph[i*(o+1)+idx]; syncthreads(); for (int iter=1; iter <= size+1; iter++) for(int u=0;u<=o;u++) for(int v=0;v<=o;v++){ int max = 0, tmp =0; if(cache[u*(o+1)+idx]!=0 and cache[idx*(o+1)+v]!=0){ tmp = cache[u*(o+1)+idx] + cache[idx*(o+1)+v]; if(max < tmp) max = tmp; if (cache[u*(o+1)+v] < max) cache[u*(o+1)+v] = max; syncthreads(); for(int i=0;i<=o;i++) graph[i*(o+1)+idx]=cache[i*(o+1)+idx];
Eksperymenty obliczeniowe Pamięć współdzielona kernel void JobShopKernelLocal(int o, global int *graph, int size, local *cache) { int idx = (int)get_local_id(0); for(int i=0;i<=o;i++) cache[i*(o+1)+idx]=graph[i*(o+1)+idx]; barrier(clk_local_mem_fence); for (int iter=1; iter <= size+1; iter++) for(int u=0;u<=o;u++) for(int v=0;v<=o;v++){ int max = 0, tmp =0; if(cache[u*(o+1)+idx]!=0 && cache[idx*(o+1)+v]!=0){ tmp=cache[u*(o+1)+idx]+cache[idx*(o+1)+v]; if(max < tmp) max = tmp; if (cache[u*(o+1)+v] < max) cache[u*(o+1)+v] = max; barrier(clk_local_mem_fence); for(int i=0;i<=o;i++) graph[i*(o+1)+idx]=cache[i*(o+1)+idx];
Eksperymenty obliczeniowe Pamięć współdzielona problem o OpenCL ATI OpenCL NV CUDA CPU la01-05 50 0.0208 0.0056 0.0057 0.0042 la06-10 75 0.0436 0.0152 0.0165 0.0191 la11-15 100 N/A 0.0243 0.0263 0.0401 la16-20 100 N/A 0.0242 0.0261 0.0359 Tabela: Porównanie czasów trwania obliczeń dla pamięci współdzielonej
Podsumowanie Ten sam kod OpenCL wykonuje się dłużej na karcie ATI niż na karcie NVIDIA. Różnica czasu wykonania kodu OpenCL i CUDA na karcie NVIDIA jest niewielka. Użycie pamięci współdzielonej pozwala na przyspieszenie obliczeń o ok. 2x. Dla dużych rozmiarów danych czas obliczeń na GPU jest krótszy niż na CPU.
Koniec Dziękuję za uwagę!