ˇ SVEUCILIŠTE U ZAGREBU FAKULTET ORGANIZACIJE I INFORMATIKE VARAŽDIN Krunoslav Ðuras ˇ KARTICA KAO GRAFICKA ˇ MASIVNO-PARALELNO RACUNALO DIPLOMSKI RAD Varaždin, 2013. ˇ SVEUCILIŠTE U ZAGREBU FAKULTET ORGANIZACIJE I INFORMATIKE VARAŽDIN Krunoslav Ðuras Redoviti student Broj indeksa: 40525/11-R Smjer: Baze podataka i baze znanja Diplomski studij ˇ GRAFICKA KARTICA KAO ˇ MASIVNO-PARALELNO RACUNALO DIPLOMSKI RAD Mentor: Doc. dr. sc. Ivan Hip Varaždin, kolovoz 2013. Sadržaj 1. Uvod . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2. GPGPU . . . . . . . . . . . . . . . . 2.1. Nvidia CUDA . . . . . . . . . . . 2.2. OpenACC . . . . . . . . . . . . . 2.3. OpenCL . . . . . . . . . . . . . . 2.4. OpenCL vs CUDA . . . . . . . . 2.4.1. Postoje´ce implementacije . 2.4.2. Portabilnost . . . . . . . . 2.4.3. Brzina . . . . . . . . . . . 2.4.4. Mogu´cnosti . . . . . . . . 2.4.5. Dostupne biblioteke . . . 2.5. CPU vs. GPU processing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 3 3 5 6 7 8 8 8 8 9 3. OpenCL . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1. Arhitektura OpenCL-a . . . . . . . . . . . . . . . . . 3.1.1. Model platforme . . . . . . . . . . . . . . . . 3.1.2. Model izvršavanja . . . . . . . . . . . . . . . 3.1.2.1. Kontekst . . . . . . . . . . . . . . . 3.1.2.2. Redovi naredbi . . . . . . . . . . . . 3.1.2.3. Kategorije kernela . . . . . . . . . . 3.1.3. Model memorije . . . . . . . . . . . . . . . . 3.1.4. Model programiranja . . . . . . . . . . . . . . 3.1.4.1. Podatkovni paralelizam . . . . . . . 3.1.4.2. Zada´cni paralelizam . . . . . . . . . 3.1.4.3. Sinkronizacija . . . . . . . . . . . . 3.2. OpenCL Framework . . . . . . . . . . . . . . . . . . 3.2.1. OpenCL Platform Layer . . . . . . . . . . . . 3.2.1.1. Dohva´canje informacija o platformi . 3.2.1.2. Dohva´canje informacija o uredajima ¯ 3.2.1.3. Konteksti . . . . . . . . . . . . . . . 3.2.2. OpenCL Runtime . . . . . . . . . . . . . . . . 3.2.2.1. Redovi naredbi . . . . . . . . . . . . 3.2.2.2. Kerneli . . . . . . . . . . . . . . . . 3.2.2.3. Izvršavanje kernela . . . . . . . . . 3.3. C programski jezik OpenCL-a . . . . . . . . . . . . . 3.3.1. Ugradeni ¯ tipovi podataka . . . . . . . . . . . . 3.3.1.1. Skalarni tipovi podataka . . . . . . . 3.3.1.2. Vektorski tipovi podataka . . . . . . 3.3.1.3. Objekti . . . . . . . . . . . . . . . . 3.3.2. Ugradene funkcije . . . . . . . . . . . . . . . ¯ 3.3.2.1. Funkcije za rad s radnim stavkama . 3.3.2.2. Matematiˇcke funkcije . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 12 12 13 13 13 13 14 17 17 17 17 18 19 19 20 20 21 21 21 22 23 24 24 24 25 25 25 26 4. Instalacija potrebnih programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 4.1. Arch linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 4.2. Ubuntu 13.04 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 I . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 4.3. Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 4.4. clinfo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 4.5. SWIG . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 5. Primjeri programa koji koriste OpenCL . . . . . . . . . . . . . . 5.1. Hashkill . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2. oclHashcat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.3. Usporedba brzine izvodenja na centralnom i grafiˇckom procesoru . ¯ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 37 37 38 6. Implementacije algoritama u OpenCL-u . . . . . . . . . . . . . . . . . . . . 6.1. Metaprogramiranje za grafiˇcke procesore . . . . . . . . . . . . . . . . . . . 6.2. PyOpenCL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2.1. Izgled jednostavnog programa u PyOpenCL-u . . . . . . . . . . . . . 6.3. Množenje matrica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3.1. Programski kod . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3.2. Objašnjenje programskog koda . . . . . . . . . . . . . . . . . . . . . 6.3.3. Rezultati testiranja . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.4. Množenje matrica uz korištenje lokalne memorije OpenCL uredaja . . . . . . ¯ 6.4.1. Programski kod . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.4.2. Objašnjenje programskog koda . . . . . . . . . . . . . . . . . . . . . 6.4.3. Rezultati testiranja . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.5. Mandelbrot fraktal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.5.1. Programski kod . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.5.2. Objašnjenje programskog koda . . . . . . . . . . . . . . . . . . . . . 6.5.3. Rezultati testiranja . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.6. Sortiranje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.6.1. Selection sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.6.2. Selection sort — poboljšanje korištenjem lokalne memorije . . . . . 6.6.3. Selection sort — poboljšanje smanjenjem ukupnog broja radnih stavki 6.6.4. Selection sort — kombinacija 1. i 2. poboljšanja . . . . . . . . . . . 6.6.5. Usporedba implementiranih verzija selection sort algoritma . . . . . 6.6.6. Bitonic sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 41 42 43 46 47 50 51 54 54 55 56 58 58 61 63 64 65 69 71 73 75 76 7. Zakljuˇcak . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 8. Literatura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 II 1. Uvod Moderne grafiˇcke kartice zapravo su masivno-paralelna raˇcunala na kojima je uz pomo´c odgovaraju´cih programskih suˇcelja kao što su CUDA ili OpenCL mogu´ce implementirati sve vrste proraˇcuna (GPGPU - General-Purpose computing on Graphics Processing Units). Cilj ovog diplomskog rada je da se korištenjem programskog suˇcelja OpenCL implementira paralelno rješavanje nekoliko standardnih problema na GPU te ispita koliko je doista mogu´ce dobiti na brzini u usporedbi s implementacijom na CPU. Nakon ovog uvoda, u 2. poglavlju je dan op´ceniti opis i definicija GPGPU-a. Osim toga u 2. poglavlju je opisana Nvidia CUDA tehnologija, OpenACC i OpenCL standardi te je prikazana usporedba OpenCL-a i Nvidia CUDA-e prema raznim kriterijima. Nakon toga dana je usporedba izmedu ¯ naˇcina na koje se provode izraˇcuni na grafiˇckom i centralnom procesoru te za koje vrste problema i algoritama su prikladniji grafiˇcki procesori, a za koje vrste problema i algoritama su prikladniji centralni procesori raˇcunala. Tre´ce poglavlje daje opis arhitekture OpenCL-a, OpenCL Frameworka te varijante C programskog jezika koji se koristi za pisanje OpenCL kernela. U poglavlju 4 opisan je postupak instalacije drivera za grafiˇcku karticu, programa potrebnih za korištenje OpenCL-a te PyOpenCL modula za Python. Opis je dan za Windows operacijski sustav te za Ubuntu i Arch Linux distribucije linuxa. U tom poglavlju takoder ¯ je opisana naredba clinfo i prikazani su podaci o hardveru korištenom kod testiranja algoritama implementiranih u OpenCL-u. U poglavlju 5 opisana su dva programa koja se cˇ esto koriste u praksi u podruˇcju kriptografije, a imaju podršku za OpenCL i Nvidia CUDA tehnologije. Osim toga u tom poglavlju dan je prikaz usporedbe brzine izvršavanja tih aplikacija korištenjem centralnog procesora raˇcunala naspram korištenja grafiˇckog procesora. U poglavlju 6 opisan je PyOpenCL modul za Python te je prikazan primjer jednostavnog programa u OpenCL-u. U tom poglavlju takoder ¯ su prikazane implementacije nekoliko razliˇcitih algoritama u OpenCL-u te je prikazano koliko se dobiva na performansama korištenjem OpenCL-a naspram korištenja standardnih algoritama koji se izvršavaju na CPU raˇcunala. Množenje matrica je prvi algoritam koji je implementiran u OpenCL-u i testiran. Jednostavna implementacija množenja matrica u OpenCL-u je poboljšana korištenjem lokalne memorije OpenCL uredaja te su na taj ¯ naˇcin pove´cane njezine performanse. Implementirano je i raˇcunanje Mandelbrotovog fraktala uz pomo´c OpenCL-a. Nakon toga prikazana je jednostavna implementacija selection sort algoritma u OpenCL-u te mogu´ci naˇcini optimiranja. Konaˇcno, u OpenCL-u je implementiran i testiran bitonic sort algoritam. Na kraju se nalaze zakljuˇcak i popis korištene literature. 1 2. GPGPU GPGPU (engl. General-purpose computing on graphics processing units) predstavlja primjenu grafiˇckih procesora (koji se cˇ esto koriste samo u raˇcunalnoj grafici) za provodenje izraˇcuna u apli¯ kacijama koje najˇceš´ce koriste samo centralni procesor raˇcunala. Grafiˇcki procesori se mogu koristiti za takve izraˇcune jer podržavaju dovoljan broj instrukcija i mogu´cnosti, pa se mogu koristiti za sve vrste izraˇcuna. Osim toga, korištenje ve´ceg broja grafiˇckih kartica na jednom raˇcunalu ili velikog broja grafiˇckih kartica raspodijeljenih na više raˇcunala omogu´cuje daljnju paralelizaciju ve´c paralelnog naˇcina provodenja izraˇcuna na grafiˇckim procesorima. ¯ Trenutno najˇceš´ce korišteni otvoreni jezik za GPGPU je OpenCL. Kod jezika koji nisu otvorene prirode najˇceš´ce se koristi Nvidia CUDA. GPGPU i ostale sliˇcne tehnologije koje se koriste radi postizanja boljih performansi kod izracˇ una poˇcele su se koristiti zbog pove´cane potrebe za performansama u znanosti i inženjerstvu. Te tehnologije se najˇceš´ce koriste za paralelno provodenje razliˇcitih izraˇcuna nad velikim skupovima ¯ podataka. Potreba za tim tehnologijama stvorila je jaku potrebu za arhitekturama za razvoj softvera koje omogu´cuju razvoj softvera koji je sposoban iskoristiti grafiˇcke procesore i sliˇcne tehnologije za masivno paralelno izvodenje razliˇcitih algoritama. Obrada op´ce namjene na grafiˇckim proces¯ nim jedinicama (GPGPU) je priliˇcno novi trend u istraživanjima raˇcunalnih inženjera. Grafiˇcki procesori su visoko optimizirani koprocesori za obradu raˇcunalne grafike. Obrada raˇcunalne grafike je podruˇcje u kojem dominiraju paralelne podatkovne operacije – toˇcnije, matriˇcne operacije linearne algebre. (Kirk & mei W. Hwu, 2010) Grafiˇcki procesor je specijalizirana vrsta procesora koja zadovoljava potrebe u zahtjevnim zadacima prikaza 3D grafike visoke rezolucije. (Kirk & mei W. Hwu, 2010) Osim toga grafiˇcki procesori su se razvili tako da omogu´cuju obradu velikih koliˇcina podataka. Ovaj dizajn je puno efikasniji nego dizajni klasiˇcnih CPU-ova kod zadataka paralelne obrade velikih koliˇcina podataka, kao što su npr. – DFT (engl. Discrete Fourier transform), – obrada slike (engl. image processing), – procesiranje audio signala, – fizikalne simulacije, – sortiranje velikih listi i – statistiˇcki izraˇcuni. 2 2.1. Nvidia CUDA CUDA (engl. Compute Unified Device Architecture) je platforma za paralelno provodenje izra¯ cˇ una koju je kreirala Nvidia i koju mogu koristiti grafiˇcke kartice koje proizvodi Nvidia. CUDA ukljuˇcuje programski jezik temeljen na programskom jeziku C. Za razliku od OpenCL-a, CUDA je vlasniˇcka (engl. proprietary) arhitektura i programski jezik koji podržava samo hardver proizvodaˇ ¯ ca Nvidia. To je jedan od glavnih nedostataka ove tehnologije, pogotovo u odnosu na otvorene standarde kao što je OpenCL. CUDA je prvi puta uvedena 2006. godine, a od tada je korištena u mnogobrojnim aplikacijama i cˇ esto je spominjana i obradivana u znanstvenim cˇ lancima i knjigama. ¯ Postoji više naˇcina na koji programeri mogu iskoristiti mogu´cnosti koje pruža CUDA. Programeri mogu – iskoristiti ve´c postoje´cu biblioteku koja zamjenjuje ili nadograduje biblioteke kao što su ¯ MKL BLAS, IPP, FFTW ili neku drugu od cˇ esto korištenih biblioteka, – automatski paralelizirati petlje u C ili Fortran kodu koriste´ci OpenACC direktive ili – razviti vlastite paralelne algoritme i biblioteke koriste´ci neki poznati programski jezik, npr. C, C++, C#, Fortran, Java, Python, itd. CUDA u odnosu na sliˇcne tehnologije ima raznih prednosti i nedostataka. Glavne prednosti i nedostaci CUDA tehnologije u usporedbi s OpenCL-om po raznim kriterijima opisane su u šestom poglavlju ovog diplomskog rada. Zanimljivo je da tvrtka Nvidia koja je razvila CUDA tehnologiju dio svojih resursa ulaže u razvoj otvorenog standarda OpenCL, iako je OpenCL jedan od glavnih konkurenata CUDA-i. 2.2. OpenACC OpenACC je standard koji se koristi kod paralelnog programiranje i koji je razvijen kako bi programerima koji programiraju u C, C++ ili Fortran programskom jeziku omogu´cio jednostavno korištenje mogu´cnosti paralelizacije koje pružaju grafiˇcki procesori i sliˇcan hardver. Standard su razvili Cray, CAPS, Nvidia i PGI. OpenACC omogu´cuje programerima da s jednostavnim direktivama u kodu oznaˇce dijelove koda koje c´ e program prevodilac (engl. compiler) automatski paralelizirati bez da programer mora uvoditi daljnje promjene u kodu. Program prevodilac provodi sve potrebne promjene tokom prevode¯ nja (engl. compiling) tako da dobivena izvršna verzija programa iskorištava dostupne mogu´cnosti paralelizacije. Direktive koje se mogu koristiti omogu´cuju programerima da oznaˇce dijelove koda 3 koji c´ e se paralelizirati za izvodenje na grafiˇckom procesoru ili sliˇcnom uredaju. Osim toga di¯ ¯ rektive se mogu koristiti za upravljanje prijenosom podataka izmedu ¯ hosta i akceleratora (npr. grafiˇckog procesora) te omogu´cuju pokretanje i zaustavljanje akceleratora. Neke od podržanih direktiva su – #pragma acc parallel – #pragma acc kernels – #pragma acc data – #pragma acc loop – #pragma acc cache – #pragma acc update – #pragma acc declare – #pragma acc wait U nastavku je prikazan program za izraˇcunavanje broja π koriste´ci OpenACC. #include <stdio.h> #define N 1000000 int main(void) { double pi = 0.0f; long i; #pragma acc parallel loop reduction(+:pi) for (i=0; i<N; i++) { double t= (double)((i+0.5)/N); pi +=4.0/(1.0+t*t); } printf("pi=%16.15f\n",pi/N); return 0; } Isti program u Pythonu koriste´ci PyOpenCL modul za Python prikazan je u nastavku. import pyopencl as cl import numpy platforms = cl.get_platforms() 4 device = platforms[0].get_devices(cl.device_type.GPU)[0] ctx = cl.Context([device]) queue = cl.CommandQueue(ctx) N = 10000000 mf = cl.mem_flags b_dev = cl.Buffer(ctx, mf.WRITE_ONLY, size = N * 4) prg = cl.Program(ctx, """ __kernel void Pi(__global float * b) { int i = get_global_id(0); float x = 4.0 / (1 + ((i + 0.5) / %d) * ((i + 0.5) / %d)); b[i] = x; } """ % (N, N)).build() prg.Pi(queue, (N,), b_dev) b = numpy.empty((N,1)).astype(numpy.float32) cl.enqueue_copy(queue, b, b_dev) print numpy.sum(b) / N Isti program u C-u implementiran za OpenCL imao bi puno više koda, pa se odmah vidi prednost korištenja Python programskog jezika i PyOpenCL modula za Python kod razvoja OpenCL aplikacija. Ova i ostale prednosti PyOpenCL-a c´ e biti detaljnije objašnjene kasnije u ovom radu. 2.3. OpenCL OpenCL je otvoreni standard kojeg održava neprofitni konzorcij Khronos Group, a predstavlja novi industrijski standard za podatkovno i zada´cno paralelno provodenje izraˇcuna na mnogim razliˇcitim ¯ vrstama procesora (CPU, GPU, DSP, FPGA i sl.). OpenCL definira skup osnovnih funkcionalnosti koje podržavaju svi uredaji. Osim toga, OpenCL ¯ definira i neke dodatne funkcionalnosti koje se mogu koristiti samo na odredenim uredajima. Iako ¯ ¯ OpenCL garantira portabilnost koda i mogu´cnost izvršavanja na razliˇcitim uredajima (CPU, GPU i ¯ sl.), ta portabilnost ima svojih granica, a ona ovisi o hardverskoj arhitekturi uredaja. Ipak, progra¯ mer ima mogu´cnost pisati program za jednu odredenu arhitekturu, a da taj program može ispravno ¯ 5 raditi i na ve´cini drugih arhitektura. OpenCL je otvoreni standard kojeg može koristiti hardver velikog broja proizvodaˇ ¯ ca, ukljuˇcuju´ci grafiˇcke katrice stolnih i prijenosnih raˇcunala od proizvodaˇ ¯ ca ATI i Nvidia. Ako na hostu nije dostupan podržani grafiˇcki procesor, tada se izvršavanje može obaviti na centralnom procesoru. OpenCL ukljuˇcuje jezik (temeljen na C99 jeziku) za pisanje kernela (funkcija koje se izvode na OpenCL uredajima) i API-je koji se koriste za kontrolu izvršavanja. OpenCL se može koristiti kako ¯ bi aplikacijama omogu´cio pristup grafiˇckom procesoru radi izvršavanja razliˇcitih vrsta izraˇcuna koji ne moraju nužno imati veze s raˇcunalnom grafikom. Prvotno je razvijen od Apple-a, ali ga sada održava konzorcij Khronos Group koji održava i neke druge standarde vezane uz audio i video (npr. OpenGL standard). OpenCL je razvijen s namjerom da olakša programerima posao kod pisanja aplikacija za heterogene sustave te osim toga rješava problem sve ve´ce potrebe za platformama za programiranje aplikacija koje se izvršavaju na velikom broju procesorskih jezgri odjednom. (B. Gaster, Howes, Kaeli, Mistry, & Schaa, 2011) 2.4. OpenCL vs CUDA CUDA i OpenCL su frameworkovi za programiranje koji omogu´cuju korištenje grafiˇckih procesora za paralelnu obradu podataka i low-level pristup hardveru, ali samo OpenCL pruža otvoreni framework. Zbog toga je OpenCL dobio podršku od gotovo svih proizvodaˇ ¯ ca centralnih i grafiˇckih procesora ukljuˇcuju´ci AMD, Intel i Nvidia kao i proizvodaˇ ¯ ca procesora za mobilne platforme i servere. Kao rezultat toga aplikacije pisane u OpenCL-u su prenosive na velik broj razliˇcitih vrsta GPU-ova i CPU-ova. Trenutno za CUDA-u postoji više biblioteka i high-level API-a, ali ve´c postoje biblioteke za OpenCL i neke se još razvijaju tako da ovaj nedostatak OpenCL-a u budu´cnosti vjerojatno ne´ce biti od tolike važnosti. Za oˇcekivati je da c´ e se s vremenom razviti i ve´ci broj high-level API-a za OpenCL nego što ih ima za CUDA-u budu´ci da je OpenCL otvoreni standard i sve više se koristi. Jedna važna prednost cˇ injenice da je OpenCL otvoreni standard leži u mogu´cnosti da bilo koji proizvodaˇ ¯ c može implementirati podršku OpenCL-a za svoje proizvode. CUDA i OpenCL su podržani na razliˇcitim operacijskim sustavima (Windows, Linux, MacOS). Veliki broj programera najprije razvija kod na svojem osobnom raˇcunalu prije nego ga prebacuju na skupinu raˇcunala (engl. cluster) ili radne stanice (engl. workstation), pa je mogu´cnost korištenja hardvera proizvodaˇ ¯ ca razliˇcitih od Nvidia-e vrlo korisna. To je jedna od prednosti OpenCL-a u odnosu na CUDA-u. Osim toga puno ljudi se ne želi odluˇciti za platformu koja je usko vezana uz odredenog proizvodaˇ ¯ ¯ ca i njihove proizvode, pa takvi programeri izbjegavaju CUDA-u. Medu ¯ neke od manje bitnih razlika izmedu ¯ OpenCL-a i CUDA-e spada cˇ injenica da za CUDA- 6 CUDA term OpenCL term host CPU host streaming multiprocessor (SM) compute unit (CU) scalar core processing element (PE) host thread host program thread work-item thread block work-group grid NDRange shared memory local memory constant memory space constant memory texture memory space constant memory Tablica 2.1: Usporedba oznaka u OpenCL-u i CUDA-i u postoji jedan programski paket koji ukljuˇcuje sve potrebne alate za razvoj programa koriste´ci CUDA tehnologiju. S druge strane, za razvoj programa koriste´ci OpenCL postoje razliˇciti SDKovi, integrirana razvojna okruženja, debuggeri i sl., a svaki od njih može biti od razliˇcitog proizvodaˇ ¯ ca. To je tako jer je OpenCL otvoreni standard koji ima specifikacije koje može zadovoljiti bilo koji proizvodaˇ ¯ c ako to želi, te može izdati vlastitu implementaciju OpenCL-a, vlastite alate za rad s OpenCL-om itd. Možemo zakljuˇciti da postoji sva programska podrška potrebna za razvoj OpenCL programa (iako je softver možda potrebno nabaviti od više razliˇcitih proizvodaˇ ¯ ca), što znaˇci da tvrdnje da ne postoji dovoljno alata za razvoj u OpenCL-u nisu istinite. Prije poˇcetka programiranja za GPGPU potrebno je donijeti odluku da li koristiti OpenCL ili CUDA-u. Kod donošenja te odluke u obzir se mogu uzeti – postoje´ce implementacije, – portabilnost, – dostupne mogu´cnosti, – brzina i – dostupne biblioteke (engl. library). 2.4.1. ´ implementacije Postojece Trenutno postoji samo jedna implementacija CUDA-e, a to je implementacija od strane Nvidia-e. Kod OpenCL-a postoji izbor izmedu ¯ više implementacija. Trenutno su to sljede´ce implementacije – AMD, – Nvidia, 7 – Apple (samo za MacOS X), – Intel, – IBM (za PowerPC) i – Portable OpenCL - implementacija OpenCL-a otvorenog koda. 2.4.2. Portabilnost Iako je OpenCL otvoreni standard koji podržava velik broj razliˇcitih uredaja ¯ na kojima se mogu izvršavati programi pisani u OpenCL-u, to ne znaˇci da c´ e OpenCL kod raditi optimalno na svim ˇ niti uredajima bez nekih izmjena u kodu potrebnih za optimiziranje na odredenom uredaju. Cak ¯ ¯ ¯ ne postoji garancija da c´ e kod uop´ce raditi na nekom drugom uredaju ¯ ako se radi o kompleksnijem kodu koji je pisan tako da koristi neka posebna svojstva odredenog uredaja. Bez obzira na to, ako ¯ ¯ se programer drži OpenCL specifikacija i izbjegava dodatke vezane uz odredenog proizvodaˇ ¯ ¯ ca, kod bi se morao mo´ci bez problema izvršiti na razliˇcitim uredajima, iako možda performanse ne ¯ budu optimalne. Tu se odmah vidi prednost OpenCL-a, jer ne samo da se kod može izvršavati na uredajima razliˇcitih proizvodaˇ ¯ ¯ ca, nego se kod može izvršavati i na razliˇcitim vrstama uredaja ¯ (GPU, CPU, DSP i sl.). S druge strane, CUDA GPGPU aplikacije se mogu izvršavati samo na grafiˇckim procesorima proizvodaˇ ¯ ca Nvidia. 2.4.3. Brzina Ako se koriste na istom hardveru, CUDA i OpenCL bi trebali dati iste performanse. To varira ovisno o driverima, ali na dulji rok performanse CUDA-e i OpenCL-a bi se trebale uravnotežiti. 2.4.4. ´ Mogucnosti OpenCL ima ve´c ugradenu potporu za generiranje koda za vrijeme izvodenja (engl. run-time code ¯ ¯ generation), dok za CUDA-u postoje posebni alati koji to omogu´cavaju. 2.4.5. Dostupne biblioteke Trenutno postoji više biblioteka (engl. library) za CUDA-u nego za OpenCL, pa je to trenutno jedna od prednosti CUDA-e. Biblioteke za OpenCL se razvijaju i vjerojatno c´ e ubrzo postojati dovoljan broj takvih biblioteka pa stoga ova prednost CUDA-e ne´ce biti toliko znaˇcajna. Na primjer, za OpenCL ve´c postoji besplatna biblioteka za matematiku poznata pod nazivom ViennaCL koja sadrži razliˇcite funkcije za linearnu algebru te algoritme za iterativno rješavanje odredenih vrsta ¯ problema. 8 2.5. CPU vs. GPU processing Pove´canje performansi izvodenja algoritama na grafiˇckom procesoru u usporedbi s izvodenjem ¯ ¯ na centralnom procesoru raˇcunala izmedu ¯ ostalog ovisi i o prikladnosti korištenja centralnog / grafiˇckog procesora za rješavanje odredene vrste problema (Kirk & mei W. Hwu, 2010). Centralni ¯ procesor raˇcunala može dati jako dobre performanse kod rješavanja odredenih vrsta problema, tako ¯ da je teško dobiti znatno pove´canje performansi korištenjem grafiˇckog procesora za rješavanje tog problema. Ve´cina aplikacija ima dijelova koji se mogu puno brže izvršiti na centralnom procesoru raˇcunala. Zbog toga je kod izrade aplikacija bitno koristiti centralni procesor za izvršavanje takvih dijelova programa, a grafiˇcki procesor je potrebno koristiti za rješavanje problema za cˇ ije rješavanje je GPU pogodniji tako da se GPU i CPU medusobno nadopunjuju i u globalu program ima najbolje ¯ mogu´ce performanse. Razlog zbog kojeg postoji velika razlika u performansama centralnog i grafiˇckog procesora je cˇ injenica da su dizajnirani tako da zadovoljavaju odredenu namjenu. Centralni procesor je dizajniran ¯ tako da daje najbolje performanse kod izvršavanja sekvencijalnog koda. Osim toga centralnim procesorima stoji na raspolaganju velika koliˇcina priˇcuvne (engl. cache) memorije cˇ ime se smanjuje vrijeme potrebno za dohva´canje instrukcija velikih i kompleksnih aplikacija. (Kirk & mei W. Hwu, 2010). Razvoj grafiˇckih procesora jako je podložan zahtjevima industrije igara (engl. gaming industry), koja zahtijeva da grafiˇcki procesori budu dizajnirani tako da mogu izvršavati jako veliki broj izracˇ una nad podacima s pomiˇcnim zarezom kod iscrtavanja slike (engl. frame) u grafiˇcki naprednim video igrama. Zbog toga proizvodaˇ ¯ ci grafiˇckih kartica dizajniraju grafiˇcke procesore tako da je optimizirana mogu´cnost izvodenja jako velikog broja dretvi, a što rezultira cˇ injenicom da se grafiˇcki ¯ procesori osim za primjenu u raˇcunalnoj grafici mogu koristiti i za rješavanje ostalih problema cˇ ije rješavanje se može paralelizirati. Osim razlike u samoj namjeni i naˇcinu dizajna grafiˇckih i centralnih procesora, na razlike u performansama izmedu ¯ njih utjeˇce i njihova memorijska propusnost. Grafiˇcki procesori imaju znatno ve´cu memorijsku propusnost. Tako su grafiˇcki procesori ve´c 2006. godine imali oko 10 puta ve´cu propusnost od ve´cine centralnih procesora raspoloživih u to vrijeme. Razlog za takve razlike u propusnostima su zahtjevi i memorijski modeli koje moraju podržavati grafiˇcki i centralni procesori. Centralni procesori radi kompatibilnosti s hardverom i programskom podrškom imaju jaˇce ograniˇcene mogu´cnosti, pa je zbog toga i njihova memorijska propusnost puno manja od memorijske propusnosti grafiˇckih procesora koji ne moraju zadovoljiti toliko puno razliˇcitih zahtjeva za kompatibilnost. (Kirk & mei W. Hwu, 2010) Arhitekture centralnih procesora i grafiˇckih procesora su dizajnirane s razliˇcitim namjenama (Kirk & mei W. Hwu, 2010): 9 – Centralni procesori su dizajnirani tako da su maksimizirane performanse izvodenja pojedinih ¯ dretvi – Grafiˇcki procesori su dizajnirani tako da je maksimizirana mogu´cnost izvršavanja jako velikog broja dretvi istovremeno po cijenu nižih performansi izvodenja pojedine dretve. ¯ Centralni procesori zahtijevaju (B. R. Gaster & Howes, 2013): – veliku koliˇcinu priˇcuvne memorije za optimiziranje dohva´canja instrukcija za izvodenje i ¯ – kompleksnu logiku sklopova za podržavanje kompleksnih programa. Grafiˇcki procesori zahtijevaju (B. R. Gaster & Howes, 2013): – podršku vektora velikih veliˇcina, što je problem jer nije lako vektorizirati sav kod, i – veliku koliˇcinu memorijskog prostora za pohranu stanja kako bi se podržale aktivne dretve (engl. active threads). Prema svemu opisanom vidljivo je da grafiˇcki i centralni procesori imaju razliˇcite hardverske implementacije te je potrebno algoritme dizajnirati tako da oni budu prikladni za izvodenje na cen¯ tralnom, odnosno grafiˇckom procesoru. Slika 2.1: Pove´canje performansi CPU-ova i GPU-ova tokom godina Izvor: (Kirk & mei W. Hwu, 2010) Na slici 2.1 prikazan je porast performansi u GFLOPS-ima (milijardama operacija na podacima s pomiˇcnim zarezom u sekundi - engl. Giga FLoating-point Operations Per Second) CPU-ova i 10 GPU-ova od 2001. do 2009. godine. Vidljiv je trend brzog pove´canja performansi grafiˇckih procesora u posljednjih nekoliko godina i nešto sporijeg pove´canja performansi centralnih procesora raˇcunala. Trenutno se performanse grafiˇckih procesora i dalje brzo pove´cavaju. Trenutno najbrža lako dobavljiva grafiˇcka kartica od AMD-a je Radeon HD 7990 koji ima procesorsku snagu od 8.2 TFLOPs-a za aritmetiku brojeva s pomiˇcnim zarezom jednostruke preciznosti i 2.04 TFLOPs-a za aritmetiku brojeva s pomiˇcnim zarezom dvostruke preciznosti. To je nekoliko puta ve´ca brzina od brzine koje su pružali grafiˇcki procesori prije nekoliko godina. Trenutno jedna od najbržih grafiˇckih kartica od proizvodaˇ ¯ ca Nvidia je GeForce GTX 780. Ova kartica ima 2304 CUDA jezgre, 3GB video memorije i 7.1 milijardu tranzistora. Grafiˇcka kartica autora ovog rada na kojoj su testirani algoritmi koje je autor implementirao u OpenCL-u je Radeon HD 6850. Grafiˇcki procesor ove grafiˇcke kartice ima procesorsku snagu od 1.5 TFLOPs-a. U sljede´coj tablici prikazana je usporedba osobina trenutno najbržih GPU-ova proizvodaˇ ¯ ca ATI i Nvidia. Podaci su dobiveni sa službenih web stranica proizvodaˇ ¯ ca ATI i Nvidia. GTX 780 GTX Titan GTX 680 AMD Radeon 7970 Broj jezgri 2304 2688 1536 2048 Texture Units 192 240 128 128 ROPs 48 48 32 32 Broj tranzistora 7.1 milijarda 7.1 milijarda 3.5 milijarde 4.3 milijarde Koliˇcina video memorije 3GB 6GB 2GB 3GB Thermal Design Power (TDP) 250W 250W 195W 250W Tablica 2.2: Usporedba osobina trenutno najbržih GPU-ova ROPs (engl. Render Output Unit(s) ili Raster Operations Pipeline) služe za provodenje izraˇcuna ¯ pozicija piksela koji se prikazuju na ekranu, kao i ostalih izraˇcuna vezanih uz prikaz slike (antialiasing, blending, Z-buffering i sl.). Korištenjem anti-aliasinga i sliˇcnih algoritama rezultiraju´ca slika izgleda puno bolje. TDP (engl. thermal design power) se odnosi na teoretski maksimalnu energiju koju sustav za hladenje odredenog uredaja ¯ ¯ ¯ mora raspršiti. 11 3. OpenCL 3.1. Arhitektura OpenCL-a Da bi se opisala arhitektura OpenCL-a potrebno je opisati sljede´ce dijelove arhitekture OpenCL-a (Munshi, 2012): – Model platforme (engl. Platform Model) – Model memorije (engl. Memory Model) – Model izvršavanja (engl. Execution Model) – Model programiranja (engl. Programming Model) 3.1.1. Model platforme Model platforme OpenCL-a je prikazan na slici 3.1. Model se sastoji od glavnog raˇcunala (engl. host) koji je povezan s jednim ili više OpenCL uredaja ¯ (engl. compute device). OpenCL uredaj ¯ je podijeljen na jedan ili više raˇcunskih jedinica (engl. compute unit) koji se dalje dijele na jedan ili više procesnih elemenata (engl. processing element). Izraˇcuni na uredaju se izvršavaju na ¯ procesnim elementima. Slika 3.1: OpenCL model platforme Izvor: (Munshi, 2012) 12 3.1.2. Model izvršavanja Izvršavanje programa u OpenCL-u je u dva zasebna dijela: – Kerneli - izvršavaju se na jednom ili više OpenCL uredaja ¯ – Program koji se izvršava na glavnom raˇcunalu. Program na glavnom raˇcunalu služi za odabir OpenCL uredaja koji c´ e se koristiti, kreiranje konteksta, prijenos podataka s glavnog ¯ raˇcunala u memoriju OpenCL uredaja ¯ i obrnuto te dodavanje komandi u red komandi. 3.1.2.1. Kontekst Prvi korak kod inicijalizacije i korištenja OpenCL-a je kreiranje konteksta (engl. context). Kod kreiranja konteksta potrebno je naznaˇciti za koji OpenCL uredaj, ¯ odnosno za koje OpenCL uredaje ¯ se kreira taj kontekst. Sve ostalo vezanu uz rad s OpenCL-om (prevodenje kernela, rad s memori¯ jom, dodavanje naredbi u red komandi) se obavlja unutar tog konteksta. Jedan kontekst može biti asociran s ve´cim brojem uredaja, a unutar konteksta OpenCL garantira konzistentnost memorije ¯ izmedu ¯ razliˇcitih uredaja. ¯ 3.1.2.2. Redovi naredbi Redovi naredbi (engl. command-queue) upravljaju redosljedom izvršavanja naredbi na OpenCL uredaju. Te komande se izvršavaju asinkrono na glavnom raˇcunalu i na OpenCL uredaju. Komande ¯ ¯ se izvršavaju u odnosu na druge komande na jedan od dva naˇcina: – In-order Execution - naredbe se izvršavaju onim redosljedom u kojem se pojavljuju u redu naredbi i završavaju po redu. To znaˇci da se naredba koja slijedi neku naredbu u redu komandi izvršava tek nakon što je završilo izvršavanje svih prethodnih naredbi. – Out-of-order Execution - naredbe se pokre´cu onim redom kojim su zadane u redu komandi, ali naredba koja slijedi neku naredbu u redu komandi ne cˇ eka da završi prethodna naredba. 3.1.2.3. Kategorije kernela Postoje dvije kategorije kernela – OpenCL kerneli (engl. OpenCL kernels) i – nativni kerneli (engl. native kernels). 13 OpenCL kerneli (jezgre) pisani su u OpenCL C programskom jeziku i prevode se pomo´cu programa prevodilaca za OpenCL. Sve implementacije OpenCL-a podržavaju OpenCL kernele. Razliˇcite implementacije OpenCL-a mogu pružati i neke druge naˇcine za kreiranje OpenCL kernela. Nativnim kernelima se pristupa preko pokazivaˇca funkcije koja pokazuje na funkciju na glavnom raˇcunalu. Nativni kerneli se na isti naˇcin stavljaju u red naredbi za izvršavanje na uredaju kao i ¯ OpenCL kerneli te s njima dijele memorijske objekte. Mogu´cnost izvršavanja nativnih kernela je dodatna mogu´cnost OpenCL-a te njihova semantika ovisi o pojedinoj implementaciji OpenCL-a. OpenCL API pruža funkcije s kojima se može provjeriti koje mogu´cnosti pruža odredeni uredaj ¯ ¯ te se na taj naˇcin može provjeriti da li uredaj ¯ podržava izvršavanje nativnih kernela. U nativnim kernelima mogu, na primjer, biti definirane funkcije iz neke biblioteke. 3.1.3. Model memorije Radne stavke (engl. work-item) koje izvršavaju kernel imaju pristup sljede´cim dijelovima memorije: – Globalna memorija (engl. global memory) - Ovaj dio memorije dozvoljava pisanje i cˇ itanje svim radnim stavkama iz svih radnih grupa (engl. work-group). Radne stavke mogu pisati i cˇ itati bilo koji element globalne memorije. Pisanje i cˇ itanje u globalnu memoriju može koristiti priruˇcnu memoriju (engl. cache) ovisno o mogu´cnostima uredaja ¯ na kojem se izvodi OpenCL kernel. – Memorija za konstante (engl. constant memory) - Ovaj dio globalne memorije ostaje nepromijenjem tokom cijelog vremena izvršavanja kernela. Glavno raˇcunalo alocira i inicijalizira memorijske objekte pohranjene u memoriju za konstante. – Lokalna memorija (engl. local memory) - Dio memorije koji je dostupan radnim stavkama iz odredene radne grupe. Svaka radna grupa ima alocirana svoj dio lokalne memorije i ima ¯ pristup samo svojoj lokalnoj memoriji. – Privatna memorija (engl. private memory) - Dio memorije koji je zaseban za svaku radnu stavku. Svaka radna stavka ima svoju privatnu memoriju i može cˇ itati i pisati samo u svoju privatnu memoriju. Kod optimizacije koda za OpenCL bitno je koristiti lokalnu memoriju umjesto globalne kada je to mogu´ce. Bolje je koristiti lokalnu memoriju jer ona ima puno ve´cu propusnost od globalne memorije, pa u nekim sluˇcajevima njeno (ne)korištenje može jako utjecati na performanse programa u OpenCL-u. Propusnost lokalne memorije je viša od 2 TB/s, a to je nekoliko puta ve´ca propusnost od propusnosti globalne memorije. 14 OpenCL Memory Types CUDA Equivalent global memory global memory constant memory constant memory local memory shared memory private memory local memory Tablica 3.1: Tipovi memorije u OpenCL-u i CUDA-i Izvor: (Kirk & mei W. Hwu, 2010) Slika 3.2: Konceptualni prikaz arhitekture OpenCL-a Izvor: (Munshi, 2012) OpenCL kerneli se izvršavaju istovremeno nad virtualnom mrežom koja je definirana kodom na glavnom raˇcunalu. Ta mreža može imati više dimenzija, na sljede´coj slici prikazana je dvodimenzionalna mreža, ali mreže nad kojima se izvršavaju kerneli u OpenCL-u mogu biti jednodimenzionalne, dvodimenzionalne ili trodimenzionalne. Na primjer, jednodimenzionalna mreža može biti polje brojeva koje treba sortirati, a dvodimenzionalna mreža može biti matrica. 15 Slika 3.3: Radne stavke i radne grupe Izvor: (Munshi, 2012) Radna stavka je element prethodno opisane virtualne mreže nad kojim se izvršava OpenCL kernel. Za svaku radnu stavku pokre´ce se zasebna dretva koja izvršava OpenCL kernel. Radna stavka se izvršava kao jedan od elemenata radne grupe, a one se razlikuje od drugih radnih stavki prema svojem globalnom i lokalnom ID-u. Radna stavka se može identificirati prema svojem jedinstvenom lokalnom ID-u ili prema svojem globalnom ID-u i rednom broju radne grupe. Globalni i lokalni ID su uredene n-torke, gdje je n ¯ broj dimenzija koje se koriste, a može biti jednak 1, 2 ili 3. Radne stavke u radnoj grupi dijele lokalnu memoriju i barijere radne grupe. Kod izraˇcuna Mandelbrotovog fraktala implementiranog i objašnjenog kasnije u ovom diplomskom radu jedna radna stavka predstavlja jedan piksel Mandelbrotovog fraktala koji raˇcuna implementirani OpenCL kernel, a kod primjera sortiranja jedna radna stavka predstavlja jedan od N brojeva koje treba sortirati. Postoje odredena ograniˇcenja za veliˇcine radnih grupa. Preporuˇca se da broj radnih stavki radne ¯ grupe bude višekratnik broja 8, a najˇceš´ce se koriste veliˇcine izmedu ¯ 64 do 128 radnih stavki u radnoj grupi. OpenCL uredaji ¯ imaju ograniˇcenje veliˇcine radne grupe (najˇceš´ce 256 radnih stavki). Op´cenito, veliˇcina radne grupe može imati velik utjecaj na performanse, ali optimalna veliˇcina radne grupe ovisi o samom problemu koji kernel rješava te o kodu kernela, a nešto manje o arhitekturi GPU-a. 16 3.1.4. Model programiranja OpenCL-ov model izvršavanja podržava podatkovni paralelizam (engl. data parallelism) i zada´cni paralelizam (engl. task parallelism) kao modele programiranja. Osim toga podržava i hibridne modele koji su kombinacija podatkovnog i zada´cnog paralelizma. Primarni model koji je namjenjen izvodenju u OpenCL-u je model podatkovnog paralelizma. ¯ 3.1.4.1. Podatkovni paralelizam Podatkovni paralelizam je svojstven programskim petljama. Paraleliziranje petlji cˇ esto vodi do sliˇcnih (ne nužno identiˇcnih) operacijskih sekvenci ili funkcija izvodenih na elementima velikih ¯ ˇ podatkovnih struktura. Cesto je javlja u znanstvenim i inženjerskim problemima. 3.1.4.2. ´ paralelizam Zadacni Zada´cni paralelizam je svojstvo paralelnog programa koji potpuno razliˇcite izraˇcune može izvesti na istom ili razliˇcitom skupu podataka. Ovo predstavlja suprotnost s podatkovnim paralelizmom, gdje se isti izraˇcun provodi na istim ili razliˇcitim skupovima podataka. Zada´cni paralelizam uobicˇ ajeno ne raste razmjerno s veliˇcinom problema. 3.1.4.3. Sinkronizacija U OpenCL-u postoje dvije domene sinkronizacije – sinkronizacija izmedu ¯ radnih stavki u jednoj radnoj grupi i – sinkronizacija komandi u redu komandi koje se nalaze u jednom kontekstu. Sinkronizacija izmedu ¯ radnih stavki u radnoj grupi se obavlja pomo´cu barijere radne grupe (engl. work-group barrier). Sve radne stavke unutar radne grupe moraju pro´ci barijeru prije nego bilo koja radna stavka može nastaviti izvršavanje koda koji se nalazi na poziciji nakon barijere. Potrebno je napomenuti da kroz barijeru moraju pro´ci sve radne stavke unutar radne grupe ili niti jedna radna stavka unutar radne grupe, pa je potrebno paziti na tu cˇ injenicu ako se koristi if-thenelse. Ne postoji mehanizam za sinkronizaciju rada razliˇcitih radnih grupa. Postoje dva naˇcina sinkronizacije komandi u redovima naredbi, a to su – Barijera reda komandi (engl. command-queue barrier) - Ova barijera osigurava da su izvršene sve komande u redu komandi koje se nalaze prije barijere, te da su izvršene sve promjene u memoriji koje su nastale kao razultat prethodnih komandi u redu, tako da se 17 komande koje se izvršavaju nakon barijere mogu pravilno izvršiti. Može se koristiti samo za sinkroniziranje komandi u jednom redu komandi. ˇ – Cekanje na odredeni dogadaj ¯ ¯ - sve OpenCL API funkcije koje u red komandi stavljaju naredbu vra´caju dogadaj ¯ koji identificira komandu i memorijske objekte koje komanda ažurira. Komanda koja cˇ eka na taj dogadaj ¯ c´ e se tek izvršiti kada se ažuriraju svi memorijski objekti i te izvršene promjene nad memorijskim objektima budu vidljive. Svaki zasebni red komandi se može izvršavati in-order ili out-of-order. Za in-order redove komandi naredbe se izvršavaju po onom redu u kojem su zadane. Potrebno je eksplicitno sinkronizirati rad više redova komandi zato jer – Ako postoji više uredaja ¯ na kojima se izvršava neki kod, svaki od tih uredaja ¯ ima zasebni red komandi – Mogu´ce je da postoji više redova komandi na jednom uredaju ¯ – Za sinkronizaciju više redova komandi potrebno je koristiti dogadaje ¯ (engl. events). 3.2. OpenCL Framework Framework OpenCL-a omogu´cuje aplikacijama da koriste glavno raˇcunalo (engl. host) i jedan ili više OpenCL uredaja ¯ kao jedan jedinstveni paralelni raˇcunalni sustav. OpenCL Framework se sastoji od sljede´cih komponenti – OpenCL Platform layer - Omogu´cava glavnom raˇcunalu da otkrije dostupne OpenCL uredaje, mogu´cnosti koje ti uredaji ¯ ¯ podržavaju te osim toga omogu´cava programu koji se odvija na glavnom raˇcunalu da kreira OpenCL kontekste. – OpenCL Runtime - Omogu´cava programu koji se izvršava na glavnom raˇcunalu da radi promjene na OpenCL kontekstima nakon što su oni jednom kreirani. – OpenCL Compiler - Kreira izvršne datoteke koje sadrže OpenCL kernel. C programski jezik kojeg podržava program prevodilac podržava podskup mogu´cnosti ISO C99 programskog jezika uz dodatke za paralelizam. Taj programski jezik je detaljnije opisan u sljede´cem podpoglavlju. 18 Slika 3.4: Naˇcin rada OpenCL programa Izvor: (Klckner, 2009) 3.2.1. OpenCL Platform Layer 3.2.1.1. ´ Dohvacanje informacija o platformi Za dohva´canje informacija o platformi može se koristiti sljede´ca funkcija cl_int clGetPlatformInfo(cl_platform_info param_name, size_t param_value_size, void *param_value, size_t *param_value_size_ret) Na temelju rezultata ove funkcije možemo izmedu ¯ ostalog dobiti informacije o verziji OpenCL-a koja je podržana korištenom implementacijom OpenCL-a ,a što je važna informacija jer novije verzije OpenCL-a imaju više podržanih mogu´cnosti pa treba o tome voditi brigu kod pisanja aplikacija za OpenCL. Detaljnije informacije o podacima koje vra´ca ova funkcija mogu se prona´ci u specifikacijama OpenCL-a. 19 3.2.1.2. ´ Dohvacanje informacija o uredajima ¯ Informacije o dostupnim uredajima se mogu dobiti pomo´cu sljede´ce funkcije ¯ cl_int clGetDeviceIDs(cl_device_type device_type, cl_uint num_entries, cl_device_id *devices, cl_uint *num_devices) Sa argumentom device_type može se odabrati tip uredaja ¯ o kojima se žele dohvatiti podaci. Mogu´ci tipovi uredaja ¯ su (Munshi, 2012): – CL_DEVICE_TYPE_CPU – CL_DEVICE_TYPE_GPU – CL_DEVICE_TYPE_ACCELERATOR – CL_DEVICE_TYPE_DEFAULT – CL_DEVICE_TYPE_ALL Sa sljede´com funkcijom mogu se dobiti detaljne informacije o odredenom uredaju ¯ ¯ cl_int clGetDeviceInfo(cl_device_id device, cl_device_info param_name, size_t param_value_size, void *param_value, size_t *param_value_size_ret) Na temelju rezultata ove funkcije možemo dobiti informacije o verziji drivera, podržanim mogu´cnostima, podržanoj verziji OpenCL-a, veliˇcini lokalne memorije, nazivu i proizvodaˇ ¯ cu uredaja, ¯ tipu uredaja itd. Detaljnije informacije o podacima koje vra´ca ova funkcija mogu se prona´ci u ¯ specifikacijama OpenCL-a. 3.2.1.3. Konteksti Pomo´cu sljede´ce funkcije može se kreirati kontekst koji se dalje koristi za sve vezano uz rad s OpenCL-om (prevodenje kernela, odabir uredaja, rad s memorijom, kreiranje reda naredbi itd.): ¯ ¯ cl_context clCreateContext(cl_context_properties properties, cl_uint num_devices, 20 const cl_device_id *devices, void (*pfn_notify)(const char *errinfo, const void *private_info, size_t cb, void *user_data), void *user_data, cl_int *errcode_ret) 3.2.2. OpenCL Runtime OpenCL Runtime omogu´cava da se s razliˇcitim pozivima funkcija upravlja OpenCL objektima kao što su redovi naredbi, kerneli, memorijski objekti itd. Njime je omogu´ceno dodavanje komandi u redove naredbi, kreiranje memorijskih objekata, rad s memorijskim objektima, dohva´canje memorijskih objekata, mapiranje memorijskih objekata i sl. Jednostavno reˇceno OpenCL Runtime omogu´cava programu koji se izvršava na glavnom raˇcunalu da radi promjene na OpenCL kontekstima nakon što su oni jednom kreirani (sve operacije u OpenCL-u osim dohva´canja popisa OpenCL uredaja i sl. se moraju obaviti unutar OpenCL ¯ konteksta). 3.2.2.1. Redovi naredbi Objekti u OpenCL-u kao što su memorijski objekti i kerneli se kreiraju unutar odredenog konteksta. ¯ Sve operacije koje se izvode nad tim objektima se pokre´cu korištenjem redova naredbi. Pomo´cu redova naredbi može se pokrenuti niz operacija nad objektima koje c´ e se izvoditi po zadanom redu. Korištenje više redova naredbi omogu´cava izvršavanje nezavisnih naredbi bez potrebe za sinkronizacijom. To je mogu´ce ako objekti koji se mijenjaju izvršavanjem naredbi iz razliˇcitih redova naredbi nisu zajedniˇcki za više redova naredbi. Ako se neki objekti dijele u više redova naredbi potrebno je korištenje prikladnih metoda za sinkronizaciju. 3.2.2.2. Kerneli OpenCL kerneli (jezgre) pisani su u OpenCL C programskom jeziku i prevode se pomo´cu programa prevodilaca za OpenCL. Sve implementacije OpenCL-a podržavaju OpenCL kernele. Kerneli se kreiraju sa sljede´com funkcijom cl_kernel clCreateKernel(cl_program program, const char *kernel_name, cl_int *errcode_ret) 21 Naredba clCreateKernel vra´ca ispravan kernel objekt i varijablu errcode_retis postavljenu na vrijednost konstante CL_SUCCESS ako je kernel ispravno kreiran. U suprotnom vra´ca NULL vrijednost za kernel te varijablu errcode_retis postavi na vrijednost konstante koja predstavlja grešku do koje je došlo kod kreiranja kernela. Razliˇcite implementacije OpenCL-a mogu pružati i neke druge naˇcine za kreiranje OpenCL kernela. 3.2.2.3. Izvršavanje kernela Kerneli se mogu izvršiti sa sljede´com funkcijom cl_int clEnqueueNDRangeKernel(cl_command_queue command_queue, cl_kernel kernel, cl_uint work_dim, const size_t *global_work_offset, const size_t *global_work_size, const size_t *local_work_size, cl_uint num_events_in_wait_list, const cl_event *event_wait_list, cl_event *event) Ova funkcija dodaje izvršavanje kernela u zadani red naredbi. Bitni argumenti ove funkcije su argument kernel koji predstavlja ispravan kernel objekt, veliˇcina radne grupe, broj dimenzija, ukupan globalni broj radnih stavki i sl. Detalji o ovoj funkciji se mogu prona´ci u specifikacijama OpenCL-a. Bitno je za napomenuti da je autor za programiranje u OpenCL-u u svrhu izrade ovog diplomskog rada koristio Python programski jezik te PyOpenCL modul za Python koji znatno olakšava programiranje u OpenCL-u jer je relativno jednostavan za korištenje i kod njegovog korištenja nije potrebno detaljno poznavanje svih funkcija OpenCL-a. Dodatni razlozi za korištenje PyOpenCLa (i sliˇcnih modula za Python i druge programske jezike) opisani su u dijelu rada koji se bavi metaprogramiranjem za grafiˇcke procesore. 22 3.3. C programski jezik OpenCL-a Programski jezik koji se koristi za pisanje OpenCL kernela je temeljen na ISO C99 programskom jeziku. Varijanta tog programskog jezika koja se koristi ima neke dodatne funkcionalnosti, a neke od funkcionalnosti su izbaˇcene. Od najvažnijih funkcionalnosti koje su izbaˇcene potrebno je nabrojiti: – standardna C99 zaglavlja, – pokazivaˇci na funkcije, – rekurzije, – polja varijabilnih duljina i – bitovna polja. Najvažnije funkcionalnosti koje su dodane su: – radne grupe i radne stavke, – vektorski tipovi podataka, – sinkronizacija i – oznake za tipove adresnog prostora (__global, __local, __private, __constant). Osim navedenih dodatnih funkcionalnosti dostupan je i velik broj ugradenih funkcija: ¯ – funkcije za rad s radnim stavkama, – funkcije za rad s radnim grupama i – matematiˇcke funkcije, itd. Najvažnije i najˇceš´ce korištene funkcije te tipovi podataka koje podržava programski jezik za pisanje OpenCL kernela bit c´ e opisani u nastavku ovog podpoglavlja. 23 3.3.1. Ugradeni tipovi podataka ¯ 3.3.1.1. Skalarni tipovi podataka U sljede´coj tablici prikazani su ugradeni ¯ skalarni tipovi podataka Tip podataka bool char uchar short ushort int uint long ulong float half size_t ptrdiff_t intptr_t uintptr_t void Opis Tip podataka koji može imati vrijednost true ili false 8-bitni tip podataka s predznakom 8-bitni tip podataka bez predznaka 16-bitni cjelobrojni tip podataka s predznakom 16-bitni cjelobrojni tip podataka bez predznaka 32-bitni cjelobrojni tip podataka s predznakom 32-bitni cjelobrojni tip podataka bez predznaka 64-bitni cjelobrojni tip podataka s predznakom 64-bitni cjelobrojni tip podataka bez predznaka 32-bitni tip podataka s pomiˇcnim zarezom 16-bitni tip podataka s pomiˇcnim zarezom Cjelobrojni tip podataka bez predznaka koji je vra´cen kao rezultat sizeof operatora Cjelobrojni tip podataka koji je rezultat oduzimanja vrijednosti dvaju pokazivaˇca Pokazivaˇc kojeg predstavlja cjelobrojni tip podataka s predznakom Pokazivaˇc kojeg predstavlja cjelobrojni tip podataka bez predznaka Standardni void tip podatka Tablica 3.2: Podržani skalarni tipovi podataka Postoji i podrška za 64-bitne podatke s pomiˇcnim zarezom, ali njega ne podržavaju svi grafiˇcki procesori, pa zbog toga ti tipovi podataka nisu spomenuti u prethodnoj tablici. 3.3.1.2. Vektorski tipovi podataka Vektorski tip podataka definiran je svojim imenom (npr. char, uchar, short, itd.) i brojem koji oznaˇcava veliˇcinu vektora. Podržane vrijednosti za veliˇcinu vektora su 2, 4, 8 i 16. Tako je, na primjer, vektor int4 vektorski tip podataka koji se sastoji od 4 elemenata tipa int te u memoriji zauzima 16 bajtova (4 * 32 bitova). Podržani tipovi vektora su sljede´ci – charn – ucharn – shortn – ushortn – intn 24 – uintn – longn – ulongn – floatn pri cˇ emu zadnji znak n predstavlja broj koji može poprimiti vrijednost 2, 4, 8 ili 16. 3.3.1.3. Objekti U sljede´coj tablici prikazani su podržani tipovi objekata (Munshi, 2012): Tip podataka cl_platform_id cl_device_id cl_context cl_command_queue cl_mem cl_program cl_kernel Opis identifikator odredene OpenCL platforme ¯ identifikator odredenog OpenCL uredaja ¯ ¯ handle za odredeni ¯ OpenCL kontekst handle za red naredbi odredenog OpenCL uredaja ¯ ¯ handle za odredeni ¯ memorijski resurs handle za odredeni ¯ programski resurs handle za prevedeni kernel Tablica 3.3: Podržani tipovi objekata u OpenCL-u 3.3.2. Ugradene funkcije ¯ 3.3.2.1. Funkcije za rad s radnim stavkama Ovo su vrlo bitne funkcije te je njihovo poznavanje vrlo važno kako bi se uop´ce moglo programirati kernele za OpenCL. U OpenCL-u su definirane sljede´ce funkcije za rad s radnim stavkama (Munshi, Gaster, Mattson, Fung, & Ginsburg, 2011): – get_work_dim() - Vra´ca broj dimenzija koje se koriste. Ta vrijednost može biti 1, 2 ili 3. – get_global_size(uint dimindx) - Vra´ca ukupan broj radnih stavki za dimenziju dimindx. Ova vrijednost se najˇceš´ce koristi u petljama i predstavlja ukupan broj stavki koje treba obraditi. – size_t get_global_id(uint dimindx) - Vra´ca jedinstveni globalni ID radne stavke za dimenziju dimindx. – size_t get_local_size(uint dimindx) - Vra´ca broj radnih stavki u radnoj grupi za dimenziju dimindx. 25 – size_t get_local_id(uint dimindx) - Vra´ca lokalni ID radne stavke koji je jedinstven u radnoj grupi za dimenziju dimindx. – size_t get_num_groups(uint dimindx) - Vra´ca broj radnih grupa koje c´ e se izvršiti u kernelu za dimenziju dimindx. – size_t get_group_id(uint dimindx) - Vra´ca redni broj radne grupe koji može imati vrijednost od 0 do get_num_groups(dimindx) – 1. 3.3.2.2. ˇ Matematicke funkcije Podržane matematiˇcke funkcije možemo podijeliti u dvije kategorije – funkcije koje kao argumente primaju skalarne tipove podataka – funkcije koje kao argumente primaju vektorske tipove podataka Implementiran je veliki broj funkcija iz podruˇcja trigonometrije. Osim toga implementiran je veliki broj funkcija za rad s podacima s pomiˇcnim zarezom, funkcija za rad s cjelobrojnim podacima, funkcija za rad s vektorima, funkcija za rad s potencijama, logaritmima itd. Detaljni popis postoje´cih funkcija može se prona´ci u specifikacijama OpenCL-a. 26 4. Instalacija potrebnih programa U nastavku je prikazan postupak instalacije drivera i ostalih programa potrebnih za pokretanje implementacija algoritama u OpenCL-u prikazanih u 6. poglavlju. Prikazani su koraci instalacije na razliˇcitim operacijskim sustavima za ATI grafiˇcke kartice. 4.1. Arch linux Kako bi se mogli instalirati driveri za grafiˇcku karticu potrebno je u datoteku /etc/pacman.conf dodati sljede´ci repozitorij prije svih ostalih repozitorija: [catalyst] Server = http://catalyst.wirephire.com/repo/catalyst/$arch Mogu´ce je da repozitorij bude dostupan na nekoj drugoj adresi izlaskom novijih verzija Arch Linux operacijskog sustava. Toˇcna adresa i ostale upute vezane uz instalaciju drivera za grafiˇcku karticu mogu se prona´ci na Arch Wiki web stranicama. Nakon toga potrebno je izvršiti sljede´ce naredbe kako bi se instalirali driveri pacman -Syy pacman -S catalyst catalyst-utils catalyst-dkms opencl-catalyst PyOpenCL modul za Python se može instalirati pomo´cu sljede´ce naredbe sudo pacman -S python2-pyopencl ImagingTk modul za Python (koji se koristi kod prikaza Mandelbrotovog fraktala) može se instalirati pomo´cu sljede´ce naredbe sudo pacman -S python2-imaging Za instalaciju SWIG-a potrebno je pokrenuti sljede´cu naredbu sudo pacman -S swig 4.2. Ubuntu 13.04 Najprije je potrebno instalirati drivere za grafiˇcku karticu. Oni se mogu preuzeti s mrežnih stranica AMD-a. Pri tome je potrebno obratiti pozornost na verziju Ubuntu operacijskog sustava, te da li je instalirana 32-bitna ili 64-bitna verzija. Nakon preuzimanja drivera, oni se mogu instalirati pokretanjem sljede´cih naredbi (naredbe se mogu malo razlikovati ovisno o verziji drivera): 27 unzip amd-driver-installer-catalyst-13-4-linux-x86.x86_64.zip ./amd-driver-installer-catalyst-13-4-x86.x86_64.run Ako se radi nadogradnja drivera, potrebno je najprije deinstalirati stariju verziju drivera, te nakon toga pokrenuti gore navedenu komandu. Nakon instalacije drivera potrebno je instalirati AMD APP SDK kojeg treba preuzeti s mrežnih stranica AMD-a preuzeti AMD APP SDK te pokrenuti sljede´ce naredbe tar xzf AMD-APP-SDK-v2.8-lnx64.tgz sudo ./Install-AMD-APP.sh Nakon toga se može instalirati PyOpenCL modul za Python korištenjem sljede´ce naredbe sudo apt-get install python-pyopencl ImagingTk modul za Python (koji se koristi kod prikaza Mandelbrotovog fraktala) može se instalirati pomo´cu sljede´ce naredbe sudo apt-get install python-imaging-tk Za instalaciju SWIG-a potrebno je pokrenuti sljede´cu naredbu sudo apt-get install swig python-dev 4.3. Windows Najprije je potrebno preuzeti drivere za grafiˇcku karticu s mrežnih stranica AMD-a te ih instalirati i nakon toga na isti naˇcin potrebno je instalirati AMD APP SDK. PyOpenCL modul za Python je najlakše instalirati na Windows operacijskom sustavu pokretanjem instalacije ve´c prethodno prevedene verzije PyOpenCL-a (engl. windows binary version), koja se može prona´ci na raznim web stranicama na internetu. Kod instalacije je potrebno obratiti pozornost na verziju Pythona koja se koristi i da li se radi o 32-bitnom ili 64-bitnom operacijskom sustavu. Jedna od mrežnih stranica gdje se može prona´ci velik broj windows binary datoteka za razne Python module dostupna je na sljede´coj adresi: http://www.lfd.uci.edu/ ~gohlke/pythonlibs/. 28 4.4. clinfo Kako bi se provjerilo da li su uspješno instalirani driveri i ostali programi te kako bi se ujedno dobio uvid u resurse koji nam stoje na raspolaganju može se pokrenuti naredba clinfo. Opis ove naredbe i rezultati njenog pokretanja prikazani su u nastavku. Pomo´cu naredbe clinfo mogu se dobiti razni podaci vezani uz OpenCL uredaje, instaliranu verziju ¯ OpenCL-a, mogu´cnosti koje podržavaju dostupni OpenCL uredaji ¯ i sl. Rezultat pokretanja naredbe clinfo za Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 na kojima su testirane sve implementacije algoritama u OpenCL-u koje su prikazane u ovom diplomskom radu prikazan je u nastavku. Number o f p l a t f o r m s : 1 Platform Profile : FULL_PROFILE Platform Version : OpenCL 1 . 2 AMD−APP ( 1 1 1 3 . 2 ) P l a t f o r m Name : AMD A c c e l e r a t e d P a r a l l e l Processing P l a t f o r m Vendor : Advanced Micro D e v i c e s , I n c . Platform Extensions : cl_khr_icd cl_amd_event_callback cl_amd_offline_devices P l a t f o r m Name : AMD A c c e l e r a t e d P a r a l l e l Processing Number o f d e v i c e s : 2 D e v i c e Type : CL_DEVICE_TYPE_GPU D e v i c e ID : 4098 Board name : AMD Radeon HD 6800 S e r i e s Device Topology : PCI [ B# 1 , D# 0 , F#0 ] Max compute u n i t s : 12 Max work i t e m s d i m e n s i o n s : 3 Max work i t e m s [ 0 ] : 256 Max work i t e m s [ 1 ] : 256 Max work i t e m s [ 2 ] : 256 Max work g r o u p s i z e : 256 P r e f e r r e d v e c t o r width char : 16 P r e f e r r e d v e c t o r width s h o r t : 8 P r e f e r r e d v e c t o r width i n t : 4 P r e f e r r e d v e c t o r width long : 2 P r e f e r r e d v e c t o r width f l o a t : 4 P r e f e r r e d v e c t o r width double : 0 Native v e c t o r width char : 16 Native v e c t o r width s h o r t : 8 Native v e c t o r width i n t : 4 Native v e c t o r width long : 2 Native v e c t o r width f l o a t : 4 29 Native v e c t o r width double : 0 Max c l o c k f r e q u e n c y : 775Mhz Address b i t s : 32 Max memory a l l o c a t i o n : 134217728 Image s u p p o r t : Yes Max number o f i m a g e s r e a d a r g u m e n t s : 128 Max number o f i m a g e s w r i t e a r g u m e n t s : 8 Max image 2D w i d t h : 16384 Max image 2D h e i g h t : 16384 Max image 3D w i d t h : 2048 Max image 3D h e i g h t : 2048 Max image 3D d e p t h : 2048 Max s a m p l e r s w i t h i n k e r n e l : 16 Max s i z e o f k e r n e l a r g u m e n t : 1024 Alignment ( b i t s ) of base a d d r e s s : 2048 Minimum a l i g n m e n t ( b y t e s ) f o r any d a t a t y p e : 128 Single precision floating point capability Denorms : No Q u i e t NaNs : Yes Round t o n e a r e s t e v e n : Yes Round t o z e r o : Yes Round t o + ve and i n f i n i t y : Yes IEEE754 −2008 f u s e d m u l t i p l y −add : Yes Cache t y p e : None Cache l i n e s i z e : 0 Cache s i z e : 0 G l o b a l memory s i z e : 536870912 Constant buffer size : 65536 Max number o f c o n s t a n t a r g s : 8 L o c a l memory t y p e : Scratchpad L o c a l memory s i z e : 32768 K e r n e l P r e f e r r e d work g r o u p s i z e m u l t i p l e : 64 Error correction support : 0 U n i f i e d memory f o r H o s t and D e v i c e : 0 Profiling timer resolution : 1 Device e n d i a n e s s : Little Available : Yes Compiler a v a i l a b l e : Yes Execution c a p a b i l i t i e s : E x e c u t e OpenCL k e r n e l s : Yes Execute n a t i v e f u n c t i o n : No Queue p r o p e r t i e s : Out−of−O r d e r : No Profiling : Yes P l a t f o r m ID : 0 x00007f543cc534e0 Name : Barts 30 Vendor : Advanced Micro D e v i c e s , I n c . D e v i c e OpenCL C v e r s i o n : OpenCL C 1 . 2 Driver version : 1113.2 Profile : FULL_PROFILE Version : OpenCL 1 . 2 AMD−APP ( 1 1 1 3 . 2 ) Extensions : cl_khr_global_int32_base_atomics cl_khr_global_int32_extended_atomics cl_khr_local_int32_base_atomics cl_khr_local_int32_extended_atomics cl_khr_3d_image_writes cl_khr_byte_addressable_store cl_khr_gl_sharing c l _ e x t _ a t o m i c _ c o u n t e r s _ 3 2 c l _ a m d _ d e v i c e _ a t t r i b u t e _ q u e r y cl_amd_vec3 c l _ a m d _ p r i n t f cl_amd_media_ops cl_amd_popcnt D e v i c e Type : CL_DEVICE_TYPE_CPU D e v i c e ID : 4098 Board name : Max compute u n i t s : 4 Max work i t e m s d i m e n s i o n s : 3 Max work i t e m s [ 0 ] : 1024 Max work i t e m s [ 1 ] : 1024 Max work i t e m s [ 2 ] : 1024 Max work g r o u p s i z e : 1024 P r e f e r r e d v e c t o r width char : 16 P r e f e r r e d v e c t o r width s h o r t : 8 P r e f e r r e d v e c t o r width i n t : 4 P r e f e r r e d v e c t o r width long : 2 P r e f e r r e d v e c t o r width f l o a t : 4 P r e f e r r e d v e c t o r width double : 2 Native v e c t o r width char : 16 Native v e c t o r width s h o r t : 8 Native v e c t o r width i n t : 4 Native v e c t o r width long : 2 Native v e c t o r width f l o a t : 4 Native v e c t o r width double : 2 Max c l o c k f r e q u e n c y : 3200Mhz Address b i t s : 64 Max memory a l l o c a t i o n : 2147483648 Image s u p p o r t : Yes Max number o f i m a g e s r e a d a r g u m e n t s : 128 Max number o f i m a g e s w r i t e a r g u m e n t s : 8 Max image 2D w i d t h : 8192 Max image 2D h e i g h t : 8192 Max image 3D w i d t h : 2048 Max image 3D h e i g h t : 2048 Max image 3D d e p t h : 2048 31 Max s a m p l e r s w i t h i n k e r n e l : 16 Max s i z e o f k e r n e l a r g u m e n t : 4096 Alignment ( b i t s ) of base a d d r e s s : 1024 Minimum a l i g n m e n t ( b y t e s ) f o r any d a t a t y p e : 128 Single precision floating point capability Denorms : Yes Q u i e t NaNs : Yes Round t o n e a r e s t e v e n : Yes Round t o z e r o : Yes Round t o + ve and i n f i n i t y : Yes IEEE754 −2008 f u s e d m u l t i p l y −add : Yes Cache t y p e : Read / W r i t e Cache l i n e s i z e : 64 Cache s i z e : 65536 G l o b a l memory s i z e : 4149723136 Constant buffer size : 65536 Max number o f c o n s t a n t a r g s : 8 L o c a l memory t y p e : Global L o c a l memory s i z e : 32768 K e r n e l P r e f e r r e d work g r o u p s i z e m u l t i p l e : 1 Error correction support : 0 U n i f i e d memory f o r H o s t and D e v i c e : 1 Profiling timer resolution : 1 Device e n d i a n e s s : Little Available : Yes Compiler a v a i l a b l e : Yes Execution c a p a b i l i t i e s : E x e c u t e OpenCL k e r n e l s : Yes Execute n a t i v e f u n c t i o n : Yes Queue p r o p e r t i e s : Out−of−O r d e r : No Profiling : Yes P l a t f o r m ID : 0 x00007f543cc534e0 Name : AMD Phenom ( tm ) I I X4 955 Processor Vendor : AuthenticAMD D e v i c e OpenCL C v e r s i o n : OpenCL C 1 . 2 Driver version : 1113.2 ( sse2 ) Profile : FULL_PROFILE Version : OpenCL 1 . 2 AMD−APP ( 1 1 1 3 . 2 ) Extensions : cl_khr_fp64 cl_amd_fp64 cl_khr_global_int32_base_atomics cl_khr_global_int32_extended_atomics cl_khr_local_int32_base_atomics cl_khr_local_int32_extended_atomics cl_khr_int64_base_atomics cl_khr_int64_extended_atomics cl_khr_3d_image_writes cl_khr_byte_addressable_store cl_khr_gl_sharing c l _ e x t _ d e v i c e _ f i s s i o n c l _ a m d _ d e v i c e _ a t t r i b u t e _ q u e r y cl_amd_vec3 32 c l _ a m d _ p r i n t f cl_amd_media_ops cl_amd_popcnt Naredba clinfo vra´ca relevantne podatke za sve dostupne OpenCL uredaje. Najˇceš´ce su to grafiˇcki ¯ i centralni procesor raˇcunala. Najvažniji podaci koje naredba vra´ca su: – popis OpenCL platformi - Njih može biti više, ovisno o tome koje implementacije OpenCL-a su instalirane (Intel, AMD, Nvidia, Apple i sl.). – broj OpenCL uredaja ¯ - Najˇceš´ce se radi o dva OpenCL uredaja ¯ — centralni i grafiˇcki procesor raˇcunala, ali OpenCL podržava i druge vrste uredaja. ¯ – proizvodaˇ ¯ c OpenCL platforme (AMD, Intel, Nvidia, Apple i sl.), odnosno implementacije OpenCL-a. Svaka implementacija OpenCL-a omogu´cava korištenje OpenCL-a na odrede¯ nim uredajima (npr. AMD APP SDK omogu´cava korištenje OpenCL-a na centralnim i gra¯ fiˇckim procesorima tvrtke AMD). – verzija OpenCL platforme - U vrijeme pisanja ovog diplomskog rada to je najˇceš´ce bila verzija OpenCL 1.2, a za prethodno prikazani primjer to je konkretno verzija OpenCL 1.2 AMD APP (1113.2). Trenutno se razvijaju specifikacije za OpenCL 2.0. – dostupni OpenCL uredaji ¯ - To su najˇceš´ce grafiˇcki i centralni procesor raˇcunala, ali ovisno o implementaciji OpenCL-a mogu biti podržane i druge vrste uredaja. ¯ – proizvodaˇ ¯ c i naziv svakog OpenCL uredaja ¯ – veliˇcina lokalne memorije OpenCL uredaja - Veliˇcina lokalne memorije bitna je kod pi¯ sanja OpenCL kernela jer treba obratiti pozornost na ograniˇcenu veliˇcinu lokalne memorije OpenCL uredaja, a lokalna memorija bi se trebala koristiti ukoliko je to mogu´ce jer je znatno ¯ brža od globalne memorije, pa se korištenjem lokalne memorije mogu pove´cati performanse OpenCL kernela. – veliˇcina globalne memorije OpenCL uredaja ¯ - Veliˇcina globalne memorije je bitna jer kod pokretanja OpenCL kernela u obzir treba uzeti ograniˇcenu globalnu memoriju. Kod obrade podataka cˇ ija veliˇcina prelazi veliˇcinu globalne memorije OpenCL uredaja ¯ problem treba na neki naˇcin podijeliti na više manjih problema te rješenja tih manjih problema treba spojiti u konaˇcno rješenje za dani problem. Na primjer, ako se želi sortirati polje veliˇcine 64M koje se sastoji od podataka s pomiˇcnim zarezom jednostruke preciznosti, tada je potrebno da globalna memorija OpenCL uredaja ¯ ima dostupno barem 64M ∗ 4B = 256MB slobodnog prostora. – broj adresnih bitova te maksimalna veliˇcina memorije koja se može alocirati 33 – maksimalna veliˇcina radne skupine - Ova vrijednost je najˇceš´ce jednaka 256 za GPU, a 1024 za CPU, ali mogu´ce su i druge vrijednosti. Maksimalna veliˇcina radne skupine bitna je kod pisanja kernela te kod provodenja razliˇcitih optimizacija na kernelu. ¯ – tip i veliˇcina priruˇcne memorije (ukoliko ona postoji) - Za CPU se ispisuju podaci o L1 priruˇcnoj memoriji. – maksimalni radni takt procesora - Kod centralnih procesora ovaj takt je znatno ve´ci, dok je kod grafiˇckih procesora nešto niži. – preferirane duljine vektorskih tipova podataka - Arhitektura GPU-ova je takva da je najisplativije dohva´cati po 128 bitova podataka iz memorije odjednom, pa se prema tome ravnaju i preferirane duljine vektorskih tipova podataka. Ako je za neki tip podataka preferirana duljina vektora na nekom uredaju ¯ jednaka 0, tada taj tip podataka nije podržan na tom OpenCL uredaju. To je sluˇcaj kod Radeon HD 6850 grafiˇcke kartice koja je korištena kod testiranja ¯ implementacija prikazanih u 6. poglavlju te ne podržava podatke s pomiˇcnim zarezom dvostruke preciznosti, što se vidi i prema prethodno prikazanom rezultatu pokretanja naredbe clinfo. Velik broj grafiˇckih procesora ne podržava podatke s pomiˇcnim zarezom dvostruke preciznosti, a podršku imaju ve´cinom samo high-end grafiˇcki procesori. Opisani podaci mogu se dobiti i korištenjem PyOpenCL modula za Python pomo´cu sljede´ce Python skripte. 1 # -*- coding: utf-8 -*- 2 3 import pyopencl as cl 4 import numpy 5 6 platforms = cl.get_platforms() 7 devices = platforms[0].get_devices(cl.device_type.ALL) 8 9 for d in devices: 10 print "Verzija drivera:", d.driver_version 11 if d.type == cl.device_type.CPU == d.type: 12 13 14 print "Tip uredaja: CPU " elif d.type == cl.device_type.GPU == d.type: print "Tip uredaja: GPU " 15 print "Velicina globalne memorije:", d.global_mem_size 16 print "Velicina lokalne memorije:", d.local_mem_size 17 print "Broj racunskih jedinica:", d.max_compute_units 34 18 print "Maksimalna velicina radne grupe:", d.max_work_group_size 19 print "Maksimalna velicina memorij ekoja se moze alocirati:", \ d.max_mem_alloc_size 20 print "Maksimalni broj dimenzija radnih stavki:", \ 21 d.max_work_item_dimensions 22 23 print "Verzija OpenCL-a:", d.opencl_c_version 24 print "Naziv OpenCL uredaja:", d.name 25 print "Proizvodac OpenCL uredaja:", d.vendor 26 print "Broj adresnih bitova OpenCL uredaja:", d.address_bits 27 print Grafiˇcki procesori korišteni za testiranje algoritama prikazanih u 6. poglavlju razlikuju se po brzini koju mogu posti´ci kod provodenja izraˇcuna. Brzine prvenstveno ovise o broju stream procesora ¯ s kojima raspolažu grafiˇcki procesori, a grafiˇcki procesori korišteni za testiranje implementiranih algoritma raspolažu sa sljede´cim brojem stream procesora: – Radeon HD 6850 - 960 stream procesora, – Mobility Radeon HD 5850 - 800 stream procesora, – HD 6310 - 80 stream procesora. Podaci o broju stream procesora preuzeti su sa službenih mrežnih stranica tvrtke AMD. Raˇcunalo na kojem su testirani programi opisani u 5. poglavlju te implementacije algoritama u 6. poglavlju ima AMD Phenom X4 955 BE procesor takta 3.2GHz s 4 jezgre i Radeon HD 6850 grafiˇcku karticu. Algoritmi opisani u 6. poglavlju takoder ¯ su testirani na dva prijenosna raˇcunala. Radi se o jednom Acer prijenosnom raˇcunalu s i7 720QM centralnim procesorom takta 1.6 GHz s 4 jezgre i jednom IBM Thinkpad prijenosnom raˇcunalu s E-350 APU centralnim procesorom takta 1.6 GHz s 2 jezgre. 4.5. SWIG SWIG (engl. Simplified Wrapper and Interface Generator) je korišten kako bi se u Python programskom jeziku mogli koristiti selection sort algoritam i množenje matrica implementirano u programskom jeziku C. SWIG se osim u Pythonu može koristiti i u velikom broju drugih programskih jezika za pokretanje programa pisanih u C ili C++ programskom jeziku. SWIG se koristi za parsiranje interface datoteka te se na taj naˇcin generira programski kod koji povezuje neki od podržanih programskih jezika s programima pisanim u C ili C++ programskom jeziku. 35 Interface datoteka osim popisa funkcija takoder ¯ sadrži i funkcije koje pretvaraju strukture jednog od podržanih programskih jezika u strukture programskog jezika C i obrnuto. Primjer jedne takve funkcije prikazan je u nastavku, a prikazana funkcija služi za pretvaranje Python liste u C polje koje sadrži podatke s pomiˇcnim zarezom. 1 2 %typemap(in) float * { if (PyList_Check($input)) { int size = PyList_Size($input); 3 5 $1 = (float *) malloc(4 * size); int i = 0; 6 float pom; 7 for (i = 0; i < size; i++) { 4 PyObject *o = PyList_GetItem($input,i); pom = (float) PyFloat_AsDouble(o); 8 9 $1[i] = pom; 10 } 11 } 12 13 } 36 5. Primjeri programa koji koriste OpenCL 5.1. Hashkill Hashkill je alat otvorenog koda za otkrivanje lozinki. Jedna od važnih karakteristika alata je da kod izraˇcuna koristi grafiˇcki procesor (ima podršku za OpenCL i CUDA tehnologije), pa je iz tog razloga alat vrlo brz. Neke od važnijih osobina ovog alata su – Podrška za višejezgrene centralne procesore – Podržano je više od 60 razliˇcitih vrsta lozinki, neke od njih su MD5, SHA1, WPA, RAR arhive s lozinkama, dokumenti MS Office-a, particije kriptirane LUKS-om itd. – Alat podržava spremanje i uˇcitavanje stanja (engl. session) što omogu´cava nesmetano nastavljanje rada od tamo gdje se stanje zadnji puta automatski spremilo, pa tako ako sluˇcajno dode ¯ do rušenja programa to ne predstavlja nikakav problem. – Alat podržava razbijanje više razliˇcitih hasheva istovremeno. – Alat podržava korištenje ve´ceg broja grafiˇckih procesora istovremeno. Trenutna verzija ovog alata je 0.3.1. 5.2. oclHashcat oclHashcat je najbrži alat za razbijanje md5crypt, phpass, mscash2 i WPA / WPA2 hasheva. Ovaj alat kod izraˇcuna takoder ¯ može koristiti grafiˇcke procesore. Koriste se OpenCL i CUDA tehnologije. Neke važnije karakteristike ovog alata su: – Besplatan je. – Alat ima podršku za istovremeno korištenje do 128 grafiˇckih procesora. – Alat radi na više platformi (podrška za OpenCL i CUDA tehnologije). – oclHashcat ne troši puno memorijskih resursa. – Alat podržava dictionary napade - kod ovih napada se koristi datoteka u kojoj se nalaze najˇceš´ce korištene lozinke, rijeˇci odredenog jezika i sl. te se provjerava da li je neka rijeˇc iz ¯ te datoteke korištena kao lozinka. – Podržano je distribuirano razbijanje hasheva. 37 – Podržana je pohrana stanja. – Podržan je velik broj razliˇcitih hasheva, od onih najpoznatijih pa sve do manje poznatih hash funkcija. 5.3. ˇ Usporedba brzine izvodenja na centralnom i grafickom ¯ procesoru Prethodna dva navedena alata mogu se koristiti za razbijanje hasheva korištenjem grafiˇckog procesora i centralnog procesora raˇcunala. Korištenje grafiˇckog procesora daje znatno bolje performanse od korištenja samog centralnog procesora raˇcunala. Pri tome je potrebno obratiti pozornost na sljede´ce dvije cˇ injenice kod korištenja dictionary napada – Hashevi koji su dobiveni brzim algoritmima (MD4, MD5, NTLM i sl.) mogu se razbijati korištenjem grafiˇckog procesora uz pomo´c dictionary napada, ali to nije vrlo efikasno. Razlog za to je cˇ injenica da c´ e prijenos rijeˇci iz rijeˇcnika u globalnu memoriju grafiˇckog procesora trajati dovoljno dugo da je ipak isplativije i efikasnije koristiti samo CPU za razbijanje ove vrste hasheva. – Hashevi koji su dobiveni sporijim algoritmima s puno iteracija (md5crypt - 1000 iteracija, phpass - do 8000 iteracija, i sl.) mogu se vrlo efikasno razbijati korištenjem grafiˇckog procesora jer su oni namjerno dizajnirani tako da se sporo izvršavaju (kako bi se otežalo njihovo razbijanje), pa vrijeme potrebno za kopiranje rjeˇcnika u globalnu memoriju grafiˇckog procesora nema velikog utjecaja na ukupno utrošeno vrijeme za razbijanje hasheva. Autor je testirao brzine alata hashcat i oclHashcat na svom osobnom raˇcunalu. Za testiranje su korišteni programi oclHashcat-plus 0.14 i hashcat 0.45. 38 U sljede´coj tablici prikazana je usporedba brzina razbijanja hasheva korištenjem grafiˇckog procesora i centralnog procesora raˇcunala. MD5 SHA1 NTLM CPU 30.83M h/s 32.68M h/s 32.04M h/s GPU 2036.8M h/s 924.5M h/s 2652.7M h/s GPU / CPU 66.07 28.29 82.79 Tablica 5.1: Usporedba brzina razbijanja hasheva na centralnom i grafiˇckom procesoru Slika 5.1: Usporedba brzina razbijanja hasheva na centralnom i grafiˇckom procesoru Brzine raˇcunanja MD5, SHA1 i NTLM hasheva pomo´cu centralnog procesora iznose oko 30 milijuna hasheva u sekundi. Brzine raˇcunanja MD5, SHA1 i NTLM hasheva pomo´cu grafiˇckog procesora variraju izmedu ¯ oko 900 milijuna hasheva u sekundi pa sve do oko 2.5 milijardi hasheva u sekundi. Najmanje pove´canje brzine raˇcunanja izvodenjem na grafiˇckom procesoru dobiva se za ¯ SHA1 algoritam, a pove´canje brzine u odnosu na brzinu izvodenja na centralnom procesoru iznosi ¯ oko 30 puta. Najve´ce pove´canje brzine dobiva se za NTLM hash, a to pove´canje brzine iznosi oko 80 puta. Zanimljivo je da su brzine raˇcunanja zadanih hasheva na centralnom procesoru otprilike iste, dok kod raˇcunanja pomo´cu grafiˇckog procesora postoje znatne razlike. Vidljivo je da je ubrzanje znacˇ ajno, iako ono može varirati u ovisnosti o centralnom i grafiˇckom procesoru. 39 U sljede´coj tablici prikazana je usporedba brzina razbijanja nekih sporijih hasheva korištenjem grafiˇckog procesora i centralnog procesora raˇcunala. md5crypt phpass CPU 30.18k h/s 8.38k h/s GPU 1380.5k h/s 894.9k h/s GPU / CPU 45.74 106.79 Tablica 5.2: Usporedba brzina razbijanja sporijih hasheva na CPU i GPU Slika 5.2: Usporedba brzina razbijanja sporijih hasheva na CPU i GPU Brzina raˇcunanja md5crypt hasheva pomo´cu centralnog procesora iznosi oko 30 tisu´ca hasheva u sekundi, a za phpass brzina iznosi oko 8 tisu´ca hasheva u sekundi. Brzina raˇcunanja md5crypt hasheva pomo´cu grafiˇckog procesora autora iznosi oko 1.4 milijuna hasheva u sekundi, a za phpass brzina iznosi oko 900 tisu´ca hasheva u sekundi. Ubrzanje koje se dobiva raˇcunanjem md5crypt hasheva korištenjem grafiˇckog procesora umjesto korištenja centralnog procesora raˇcunala iznosi oko 45 puta, a za phpass hash dobiva se ubrzanje od oko 106 puta. Vidljivo je da je ubrzanje vrlo znaˇcajno, iako ono može varirati. Ove hash funkcije su zamišljene tako da budu sporije od klasiˇcnih hash funkcija kako bi dulje trajalo odredivanje lozinki na temelju ¯ poznatog hash-a. Lozinke se cˇ esto u baze podataka pohranjuju u obliku njihovog hash-a izraˇcunatog nekom hash funkcijom. Raˇcunala su sve brža i dodatno se pojavio GPGPU koji se može koristiti za isprobavanje jako velikog broja potencijalnih lozinki u sekundi (npr. 2.6 milijarde u sekundi kao za primjer NTLM-a na prethodnoj stranici). Zato su za pohranu lozinki razvijeni sporiji hashevi koji su cˇ ak i 1000 puta sporiji od klasiˇcnih hasheva. Klasiˇcni hashevi se i dalje koriste, recimo za provjeru integriteta podataka i sliˇcno. 40 6. Implementacije algoritama u OpenCL-u 6.1. ˇ Metaprogramiranje za graficke procesore Postoje velike razlike izmedu ¯ skriptnih jezika i GPGPU-a. Programi u OpenCL-u i CUDA-i – imaju visok stupanj paralelizacije, – vrlo su ovisni o arhitekturi uredaja ¯ na kojem se izvršavaju, – pisani su tako da se maksimizira broj izvršenih operacija u sekundi i propusnost memorije. S druge strane, programi pisani u skriptnim jezicima se mogu izvršavati na razliˇcitim uredajima i ¯ operacijskim sustavima i cˇ esto nisu optimizirani za brzinu. Zbog toga je kombiniranje skriptnih jezika i programa pisanih za izvodenje na grafiˇckim proceso¯ rima jako dobra ideja, jer se tako mogu iskoristiti glavne prednosti svake od tih tehnologija. Najˇceš´ci naˇcini pisanja koda koji daje visoke performanse su – pisanje koda u C, C++ ili Fortran programskom jeziku i – korištenje dostupnih biblioteka. Alternativni naˇcin na koji se mogu pisati programi koji imaju jako dobre performanse je korištenje skriptnih jezika za manje zahtjevne dijelove programa, a za raˇcunski zahtjevne dijelove programa može se koristiti GPGPU. Kod metaprogramiranja za grafiˇcki procesor programski kod koji se izvršana na grafiˇckom procesoru (kernel) ne mora biti uvijek isti, nego može ovisiti o parametrima koji se proslijede skriptnom jeziku. Ovisno o dobivenim parametrima skriptni jezik može generirati drukˇciji programski kod koji c´ e se izvršiti na grafiˇckom procesoru ili sliˇcnom uredaju. Jedan skriptni jezik koji se može ¯ koristiti u tu svrhu je Python — Python je dobar skriptni jezik i postoje moduli za Python koji omogu´cavaju korištenje CUDA i OpenCL tehnologija. Kao primjer metaprogramiranja za grafiˇcki procesor mogla bi se navesti Python skripta koja korisnika kod pokretanja pita da li želi sluˇcajno generirati polje brojeva s pomiˇcnim zarezom ili polje cijelih brojeva, te nakon toga na temelju odabira korisnika generira polje i kernel kod za OpenCL koji c´ e se koristiti za sortiranje tog polja brojeva. Na taj naˇcin se ne bi trebalo koristiti više skripti za obavljanje vrlo sliˇcne aktivnosti, ve´c bi se jedna skripta mogla nadopuniti na nekim mjestima tako da podržava sve mogu´ce tipove odredenog problema. ¯ 41 Slika 6.1: Korištenje Python programskog jezika za GPGPU Izvor: (Klckner, 2009) 6.2. PyOpenCL PyOpenCL je modul za Python koji omogu´cava korištenje OpenCL-a u programskom jeziku Python. Neke od najvažnijih osobina ovog modula su – PyOpenCL programeru daje na raspolaganje sve mogu´cnosti koje pruža OpenCL i omoguc´ ava korištenje svih funkcija koje su opisane u specifikacijama OpenCL-a – PyOpenCL je vrlo brz, jer je dio modula koji je vezan direktno uz OpenCL pisan u C++ programskom jeziku – Besplatan je za korištenje u komercijalne i nekomercijalne svrhe. Jedna zanimljiva mogu´cnost korištenja PyOpenCL modula je korištenje OpenCL-a kao alternativni naˇcin pisanja Python koda koji daje dobre performanse. U nekim sluˇcajevima korištenje PyOpenCL modula je jednostavnije od povezivanje Pythona s C ili C++ programskim jezikom korištenjem SWIG wrappera, pa zbog toga korištenje PyOpenCL-a ili sliˇcnih modula može biti alternativa korištenja SWIG-a. Primjeri programa koji koriste OpenCL prikazani u nastavku ovog diplomskog rada pisani su u Python programskom jeziku te koriste PyOpenCL modul. Za pisanje samih kernela koji se izvode na grafiˇckom procesoru korišten je programski jezik C u varijanti prilagodenoj za OpenCL kako je ¯ opisano u podpoglavlju 3.3. 42 Osnovne funkcije i mogu´cnosti PyOpenCL-a c´ e biti opisane pomo´cu programskog koda prikazanog u nastavku. 6.2.1. Izgled jednostavnog programa u PyOpenCL-u U nastavku je prikazan program koji sluˇcajno generira dva polja s podacima s pomiˇcnim zarezom te koristi OpenCL kako bi zbrojio ta dva polja. Program je pisan u programskom jeziku Python te koristi PyOpenCL modul koji omogu´cava korištenje OpenCL-a u Python-u. 1 import pyopencl as cl 2 import pyopencl.array as cl_array 3 import numpy 4 import time 5 6 count = 300 7 platforms = cl.get_platforms() 8 devices = platforms[0].get_devices(cl.device_type.GPU) 9 device = devices[0] 10 11 ctx = cl.Context([device]) 12 queue = cl.CommandQueue(ctx) 13 14 15 a = numpy.random.rand(1024*1024*16).astype(numpy.float32) b = numpy.random.rand(1024*1024*16).astype(numpy.float32) 16 17 t0 = time.time() 18 a_dev = cl_array.to_device(queue, a) 19 b_dev = cl_array.to_device(queue, b) 20 21 c_dev = cl_array.empty_like(a_dev) 22 23 prg = cl.Program(ctx, """ 25 __kernel void Sum(__global const float * a,__global const float * b, __global float * c) 26 { 24 27 int i = get_global_id(0); 28 c[i] = a[i] + b[i]; 29 } 43 30 """).build() 31 t1 = time.time() 32 for i in range(count): 33 event = prg.Sum(queue, a.shape, (256,), a_dev.data, b_dev.data, c_dev.data) 34 35 event.wait() 36 t2 = time.time() 37 c = c_dev.get() 38 t3 = time.time() 39 40 print t1 - t0 + (t2 - t1) / count + t3 - t2 41 print a 42 print b 43 print c Program radi na sljede´ci naˇcin: Najprije se importiraju svi potrebni Python moduli kako bi se kasnije mogle koristiti funkcije iz tih modula. Moduli koji se koriste su numpy, pyopencl i time. Numpy modul se u ovoj Python skripti koristi za generiranje sluˇcajnih brojeva. PyOpenCL modul se koristi za rad s OpenCL-om, a modul time se koristi za utvrdivanja vremena utrošenog na ¯ izvršavanje koda. Nakon što se importiraju potrebni moduli, dohva´ca se popis dostupnih OpenCL uredaja koji su ¯ grafiˇcki procesori (8. linija koda) te se odabire prvi takav dostupan uredaj ¯ (9. linija koda). Osim grafiˇckih procesora mogu se koristiti i druge vrste uredaja, kao što je na primjer centralni procesor ¯ raˇcunala. Naredbi get_devices() potrebno je proslijediti jedan od sljede´cih parametara, ovisno o tipu OpenCL uredaja ¯ koji se želi koristiti: – cl.device_type.ACCELERATOR, – cl.device_type.ALL, – cl.device_type.CPU, – cl.device_type.GPU ili – cl.device_type.DEFAULT. Nakon toga se kreira OpenCL kontekst (11. linija koda) te se u sklopu konteksta kreira red naredbi (12. linija koda). Pomo´cu numpy modula se generiraju dva polja s pseudosluˇcajnim brojevima s pomiˇcnim zarezom u rasponu vrijednosti od 0 do 1 (14. i 15. linija koda), te se ta polja kopiraju u memoriju odabranoga OpenCL uredaja ¯ pomo´cu naredbe cl_array.to_device() (18. i 19. 44 linija koda). Naredbi cl_array.to_device se kao parametri prosljeduju kreiran red naredbi te numpy ¯ polje koje se želi kopirati u globalnu memoriju OpenCL uredaja. Osim toga pomo´cu naredbe ¯ cl_array.empty_like() u globalnoj memoriji OpenCL uredaja ¯ se kreira polje iste veliˇcine kao i prva dva polja u koje c´ e se spremati zbrojevi vrijednosti elemenata prvog i drugog polja (21. linija koda). Osim pomo´cu funkcija cl_array.to_device() i cl_array.empty_like() podaci bi se u globalnu memoriju OpenCL uredaja ¯ mogli smjestiti pomo´cu sljede´cih naredbi: mf = cl.mem_flags a_dev = cl.Buffer(ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf = a) b_dev = cl.Buffer(ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf = b) c_dev = cl.Buffer(ctx, mf.WRITE_ONLY, size = b.nbytes) U tom sluˇcaju bilo bi mogu´ce specificirati razne parametre za alocirani dio globalne memorije OpenCL uredaja ¯ pomo´cu sljede´cih zastavica: – mf.READ_ONLY, – mf.READ_WRITE, – mf.WRITE_ONLY, – mf.ALLOC_HOST_PTR, – mf.COPY_HOST_PTR i – mf.USE_HOST_PTR. Mogu´ce je odabrati jednu od prve tri navedene zastavice, a zadnje tri navedene zastavice mogu´ce je kombinirati na pet naˇcina (Munshi, 2012): – Bez zastavica, – mf.COPY_HOST_PTR, – mf.ALLOC_HOST_PTR, – mf.ALLOC_HOST_PTR | mf.COPY_HOST_PTR, – mf.USE_HOST_PTR. 45 Nakon toga se u C programskom jeziku definira OpenCL kernel (23. do 30. linija koda) te se on izvrši na odabranom OpenCL uredaju ¯ onoliko puta koliko je zadano varijablom count (32. do 34. linija koda) kako bi se moglo toˇcnije odrediti prosjeˇcno vrijeme potrebno da se izvrši kernel. Kod jednog pokretanja kernela na OpenCL uredaju ¯ kernel se pokre´ce jednom za svaku radnu stavku. U ovom primjeru radi se o ukupno 16M radnih stavki, što znaˇci da se pri svakom od count pokretanja kernela kernel pokrene 16M puta. Kod pokretanja ovog kernela potrebno je kao argumente proslijediti adrese polja a, b i c, red naredbi u koji c´ e se dodati izvršavanje ovog kernela, veliˇcinu radne grupe i sl. Dodavanje kernela u red naredbi se vidi u 33. i 34. liniji koda: – Prvi argument je red naredbi u koji se dodaje akcija izvršavanja kernela. – Drugi argument su dimenzije virtualne mreže nad kojom se izvršava kernel (u ovom sluˇcaju mreža predstavlja jednodimenzionalno polje s brojevima). – Tre´ci argument je veliˇcina radne grupe (256 je maksimalna veliˇcina ako se koristi jedna dimenzija, a 16 × 16 bi bila maksimalna veliˇcina ako bi se koristile dvije dimenzije). – Ostali argumenti su polja s brojevima i polje u koje c´ e se spremiti rezultat u skladu s deklariranim parametrima kernela u 24. i 25. liniji koda. Osim polja ovdje se mogu proslijediti i ostali tipovi podataka koji su podržani u OpenCL-u. Raˇcuna se prosjeˇcno vrijeme potrebno da se izvrši kernel nad svim radnim stavkama. Rezultat izraˇcuna kernela se iz memorije OpenCL uredaja ¯ kopira u memoriju glavnog raˇcunala (37. linija koda), te se ispisuje vrijeme utrošeno na izraˇcun i sva tri polja (40. do 43. linija koda). Ovo je vrlo jednostavan program koji ne koristi naprednije mogu´cnosti OpenCL-a. Ne koristi lokalnu memoriju kako bi se ubrzalo izvršavanje (ovdje to ni nema smisla jer se radi o vrlo jednostavnom algoritmu), ne koristi ugradene matematiˇcke funkcije niti nikakve dodatne mogu´cnosti ¯ OpenCL-a. Jedina funkcija u kernelu koju ovdje treba spomenuti je funkcija get_global_id(), koja u ovom sluˇcaju za svaki element polja za koji se pokre´ce dretva vra´ca jedinstveni ID radne stavke. Kod ovog jednostavnog algoritma taj broj predstavlja indeks elementa u polju. Kod složenijih programa koriste se i funkcije za vra´canje lokalnog ID-a, rednog broja radne grupe i sl. 6.3. Množenje matrica Množenje matrica jedan je od najˇceš´cih zadataka asociranih uz GPGPU jer velik broj matrica koje se koriste u znanstvenim izraˇcunima velikih su dimenzija, a množenje matrica ukljuˇcuje provode¯ nje velikog broja raˇcunskih operacija bez složenih grananja u programskom kodu, pa je množenje matrica idealno za implementaciju u OpenCL-u (Scarpino, 2011). 46 Algoritam množenja matrica implementiran u OpenCL-u prikazan u ovom podpoglavlju usporeden ¯ je po performansama s numpy.dot rutinom za množenje matrica te s jednostavnom rutinom za množenje matrica implementiranom u programskom jeziku C. Razlog korištenja numpy.dot rutine je cˇ injenica da je ta rutina znatno brža od jednostavne rutine za množenje matrica implementirane u programskom jeziku C prikazane u ovom podpoglavlju. To je bitno jer je na taj naˇcin algoritam množenja matrica implementiran u OpenCL-u usporeden ¯ s jednostavnom rutinom s kakvom se programeri cˇ esto susre´cu, ali je usporeden ¯ i s implementacijom množenja matrica iz specijalizirane biblioteke koje ima vrlo dobre performanse. Time se korištenje specijaliziranih biblioteka funkcija za CPU prikazalo kao alternativa korištenju GPGPU-a. Nakon toga je u sljede´cem podpoglavlju prikazano poboljšanje implementacije množenja matrica u OpenCL-u kako bi se pokazalo da se implementacije algoritama u OpenCL-u mogu poboljšati na razliˇcite naˇcine i time se mogu u nekim sluˇcajevima dobiti implementacije za GPU koje su znatno brže od specijaliziranih biblioteka funkcija za CPU. 6.3.1. Programski kod U nastavku je prikazan programski kod pisan u Python programskom jeziku koji koristi PyOpenCL modul koji omogu´cava korištenje OpenCL-a u Pythonu. Kod služi za množenje dviju matrica. 1 import pyopencl as cl 2 from time import time 3 import numpy 4 5 KERNEL_CODE = """ 6 __kernel void 7 8 matrixMul(__global float* C, __global float* A, __global float* B, int wA, int wB) 9 10 11 { 12 int i = get_global_id(0); 13 int j = get_global_id(1); 14 15 float v = 0; 16 for (int k = 0; k < wA; ++k) 17 { 18 19 float vA = A[j * wA + k]; float vB = B[k * wB + i]; 47 v += vA * vB; 20 } 21 22 C[j * wA + i] = v; 23 24 } 25 """ 26 27 BS = 16 28 count = 5 # broj ponavljanja izracuna za benchmark 29 30 platforms = cl.get_platforms() 31 devices = platforms[0].get_devices(cl.device_type.GPU) 32 device = devices[0] 33 34 ctx = cl.Context([device]) 35 queue = cl.CommandQueue(ctx) 36 37 a_width = int(raw_input("Unesite sirinu matrice A: ")) 38 a_height = int(raw_input("Unesite visinu matrice A: ")) 39 b_width = int(raw_input("Unesite sirinu matrice B: ")) 40 b_height = a_width 41 42 host_a = numpy.random.rand(a_height, a_width).astype(numpy.float32) 43 host_b = numpy.random.rand(b_height, b_width).astype(numpy.float32) 44 host_c = numpy.empty((a_height, b_width)).astype(numpy.float32) 45 46 prg = cl.Program(ctx, KERNEL_CODE,).build() 47 kernel = prg.matrixMul 48 49 # host -> device -------------------------------------------------- 50 mf = cl.mem_flags 51 52 t1 = time() 53 54 55 56 57 device_a_buf = cl.Buffer(ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=host_a) device_b_buf = cl.Buffer(ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=host_b) 48 58 device_c_buf = cl.Buffer(ctx, mf.WRITE_ONLY, size=host_c.nbytes) 59 60 push_time = time()-t1 61 62 # benchmark ------------------------------------------------------- 63 t1 = time() 64 65 66 for i in range(count): event = kernel(queue, host_c.shape[::-1], 67 (BS, BS), device_c_buf, device_a_buf, 68 device_b_buf, numpy.int32(a_width), 69 numpy.int32(b_width)) 70 71 event.wait() 72 73 gpu_time = (time()-t1)/count 74 75 # device -> host -------------------------------------------------- 76 t1 = time() 77 cl.enqueue_copy(queue, host_c, device_c_buf) 78 pull_time = time()-t1 79 80 # timing output --------------------------------------------------- 81 gpu_total_time = gpu_time+push_time+pull_time 82 83 print "GPU total [s]:", gpu_total_time 84 print "GPU push [s]:", push_time 85 print "GPU pull [s]:", pull_time 86 print "GPU compute (host-timed) [s]:", gpu_time 87 88 # cpu comparison -------------------------------------------------- 89 t1 = time() 90 host_c_cpu = numpy.dot(host_a, host_b) 91 cpu_time = time()-t1 92 93 print 94 print "GPU==CPU:",numpy.allclose(host_c, host_c_cpu) 95 print 49 96 print "CPU time (s)", cpu_time 97 print 98 99 100 print "GPU speedup (with transfer): ", cpu_time/gpu_total_time print "GPU speedup (without transfer): ", cpu_time/gpu_time 6.3.2. Objašnjenje programskog koda Kod kernela za množenje matrica definiran je u programskom kodu s poˇcetkom na 5. liniji i završetkom na 25. liniji. Nakon toga definirane su varijable BS (block size) i count (27. i 28. linija koda). Varijabla BS definira visinu i širinu dvodimenzionalne matrice koja predstavlja jednu radnu grupu. Svaka radna grupa u ovom sluˇcaju ima 256 radnih stavki. Varijabla count definira koliko puta se ponavlja izvršavanje kernela nad svim radnim stavkama radi toˇcnijeg izraˇcuna prosjeˇcnog vremena utrošenog na izvodenje kernela nad svim radnim stavkama. ¯ Nakon što su definirane potrebne varijable i kod kernela, kre´ce se s dohva´canjem popisa dostupnih OpenCL uredaja, odabirom OpenCL uredaja ¯ ¯ koji c´ e se koristiti, kreiranjem konteksta te kreiranjem reda naredbi u koji c´ e se dodavati akcije pokretanja kernela. Sve navedeno u ovom odlomku odnosi se na 30. do 35. liniju koda. Nakon toga korisnika se pita za unos dimenzija matrica A i B za koje c´ e se izraˇcunati umnožak (42. do 44. linija koda). Matrice A i B moraju biti ulanˇcane kako bi se mogao odrediti njihov umnožak. Bitno je za napomenuti da širina i visina matrica A i B moraju biti višekratnici vrijednosti varijable BS, tj. u ovom sluˇcaju visina i širina moraju biti višekratnici broja 16. Nakon toga generiraju se matrice A i B koje sadrže pseudosluˇcajne brojeve s pomiˇcnim zarezom (42. i 43. linija koda) te prazna matrica C u koju c´ e se spremiti rezultat množenja matrica A i B (44. linija koda). Generirane matrice se kopiraju u globalnu memoriju OpenCL uredaja ¯ (54. do 57. linija koda) te se alocira dio globalne memorije OpenCL uredaja ¯ u koju c´ e se zapisati matrica C (58. linija koda). Slijedi kreiranje kernela koje c´ e se izvršiti na temelju njegovog izvornog koda (46. i 47. linija koda). Nakon što se u globalnu memoriju smjeste svi potrebni podaci slijedi pokretanje kernela za izraˇcun umnoška matrica ve´ci broj puta kako bi se toˇcnije odredilo prosjeˇcno vrijeme potrebno za izraˇcun umnoška zadanih matrica (65. do 71. linija koda). Kernel kod pokretanja najprije dohva´ca globalne ID-eve te ih pohranjuje u varijable i i j (12. i 13. linija koda). Te dvije varijable predstavljaju indekse retka matrice A i stupca matrice B cˇ iji se umnožak raˇcuna. Sam izraˇcun tog produkta nalazi se od 16. do 21. linije koda. Nakon što je izraˇcunat umnožak on se zapisuje na poziciju i, j u izlaznu matricu C (23. linija koda). 50 6.3.3. Rezultati testiranja Na sljede´cim slikama i u tablicama prikazano je vrijeme potrebno za izraˇcun produkta dviju matrica reda N pomo´cu centralnog procesora i grafiˇckog procesora raˇcunala. Prva slika i tablica daju podatke za brzine izvodenja za cjelobrojni tip podataka, a ostale slike i tablice daju podatke za tip ¯ podataka s pomiˇcnim zarezom. N = 480 N = 720 N = 960 N = 1200 CPU (numpy.dot) 0.22 s 0.7999 s 9.3800 s 21.9100 s GPU CPU / GPU 0.0079 s 27.50 0.0240 s 33.33 0.0639 s 173.70 0.1179 s 223.57 Tablica 6.1: Usporedba brzine množenja matrica - cjelobrojni tip podataka - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 Slika 6.2: Usporedba brzine množenja matrica - cjelobrojni tip podataka - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 Iz tablice 6.1 i slike 6.2 je vidljivo da su razlike u brzini jako velike. Iz slika 6.2 i 6.3 je takoder ¯ vidljivo da razlike u performansama nisu jednake za podatke s pomiˇcnim zarezom i cjelobrojni tip podataka. Razlog toga je cˇ injenica da funkcija za množenje matrica (numpy.dot) koja se izvršava na procesoru radi puno brže s brojevima s pomiˇcnim zarezom, a dosta sporije za cjelobrojni tip podataka, pa je zbog toga i velika razlika izmedu ¯ brzina množenja matrica cjelobrojnog tip podataka na grafiˇckom i centralnom procesoru raˇcunala. U oba sluˇcaja modul numpy koji se ovdje koristi za množenje matrica ima samo množenje (i velik broj ostalih funkcija) implementirano u programskom jeziku C, tako da za velike razlike u brzinama nije kriv Python. 51 N = 1200 N = 2400 N = 4800 CPU (numpy.dot) GPU CPU / GPU 0.21 s 0.1124 s 1.87 1.542 s 0.8199 s 1.88 11.9420 s 6.27 s 1.90 Tablica 6.2: Usporedba brzina množenja matrica - float32 - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 CPU (numpy.dot) GPU N = 1200 0.8897 s 0.15589 s N = 2400 7.552 s 1.1675 s N = 4800 62.9125 s 9.1253 s CPU / GPU 5.70 6.47 6.89 Tablica 6.3: Usporedba brzina množenja matrica - float32 - i7 720QM 1.6 GHz i Mobility Radeon HD 5850 CPU (numpy.dot) GPU CPU / GPU N = 1200 5.2559 s 1.135 s 4.63 N = 2400 41.8419 s 8.9357 s 4.68 Tablica 6.4: Usporedba brzina množenja matrica - float32 - E-350 APU 1.6 GHz s HD6310 Slika 6.3: Usporedba brzine množenja matrica - float32 - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 52 Vidljivo je i da su razlike u performansama ovisne o brzini grafiˇckog i centralnog procesora raˇcunala. Tako je u tablicama 6.3 i 6.4 vidljivo da su razlike u performansama ve´ce kod nešto sporijeg centralnog procesora (radi se o prijenosnim raˇcunalima). Usporedbom rezultata za grafiˇcke procesore u tablicama 6.2 do 6.4, kao i tablicama 6.7 do 6.9 vidi se da su performanse po prilici proporcionalne broju stream procesora. Vidljivo je da razlike u performansama zaista postoje, cˇ ak i kod ove jednostavnije implementacije množenja matrica u OpenCL-u koja se usporedivala s vrlo optimiziranom numpy.dot rutinom za ¯ množenje matrica. Algoritam koji je autor implementirao u OpenCL-u bi se svakako još dao optimizirati korištenjem lokalne memorije grafiˇckog procesora, tako da bi razlike u performansama bile nešto ve´ce. Ako implementirani algoritam u OpenCL-u usporedujemo s jednostavnom C rutinom za množenje ¯ matrica, tada su razlike u performansama puno ve´ce (tablice 6.5 i 6.6). CPU (C rutina) GPU CPU / GPU N = 1200 4.3254 s 0.1073 s 40.31 N = 2400 36.3261 s 0.7965 s 45.61 Tablica 6.5: Usporedba brzina množenja matrica - float32 - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 N = 1200 N = 2400 CPU (C rutina) GPU CPU / GPU 19.9522 s 1.1332 s 17.61 156.8220 s 8.9423 s 17.54 Tablica 6.6: Usporedba brzina množenja matrica - float32 - E-350 APU 1.6 GHz s HD6310 Kod usporedbe implementiranog OpenCL algoritma s C rutinom prikazanom u nastavku dobivaju se znaˇcajna pove´canja brzina, skoro 50 puta (tablica 6.5), dok se kod usporedbe OpenCL algoritma izvršenog na sporijem grafiˇckom procesoru s manjim brojem dostupnih stream procesora dobivaju nešto manja, ali ipak znaˇcajna ubrzanja (tablica 6.6). Vrlo je bitna implementacija s kojom se usporeduje ¯ OpenCL algoritam, jer kao što se može zakljuˇciti na temelju rezultata testiranja numpy.dot rutina je u nekim sluˇcajevima i više od 20 puta brža od obiˇcne C rutine za množenje matrica. Potrebno je napomenuti da je C rutina prikazana u nestavku poretkom petlji (kij) optimirana za korištenje priruˇcne memorije centralnog procesora, što donosi ubrzanje od oko 2 do 3 puta prema obiˇcnoj (ijk) rutini. 2 float **matrix_multiply (int hA, int wB, int wA, float **A, float **B) 3 { 1 4 int i, j, k; 53 5 float **C = (float**) malloc(sizeof(float*) * hA); for (i = 0; i < hA; i++) 6 7 C[i] = (float*) malloc(sizeof(float) * wB); 8 9 for (k = 0; k < wA; k++) 10 for (i = 0; i < hA; i++) 11 for (j = 0; j < wB; j++) 12 C[i][j] += A[i][k] * B[k][j]; 13 14 return C; 15 16 } 6.4. 6.4.1. Množenje matrica uz korištenje lokalne memorije OpenCL uredaja ¯ Programski kod U nastavku je prikazan programski kod kernela za množenje matrica. Autor kernela je tvrtka Nvidia. Kernel se može preuzeti na mrežnoj adresi https://developer.nvidia.com/ opencl, gdje se mogu prona´ci i mnogi drugi OpenCL kerneli. Prikazan je samo kernel bez koda u Pythonu jer je ostatak koda jednak kao i Python kod pod 13.3.1. 1 2 3 #define BLOCK_SIZE 16 #define AS(j, i) As[i + j * BLOCK_SIZE] #define BS(j, i) Bs[i + j * BLOCK_SIZE] 4 5 __kernel __attribute__((reqd_work_group_size(BLOCK_SIZE,BLOCK_SIZE,1))) 6 void 8 matrixMul( __global float* C, __global float* A, __global float* B, int WA, int WB) 9 { 7 11 __local float As[BLOCK_SIZE*BLOCK_SIZE]; __local float Bs[BLOCK_SIZE*BLOCK_SIZE]; 12 int HB = WA; 13 int WC = WB; 10 14 15 int bx = get_group_id(0); 54 int by = get_group_id(1); 16 17 18 int tx = get_local_id(0); 19 int ty = get_local_id(1); 20 22 int aBegin = WA * BLOCK_SIZE * by; int aEnd = aBegin + WA - 1; 23 int aStep = BLOCK_SIZE; 21 int bBegin = BLOCK_SIZE * bx; int bStep = BLOCK_SIZE * WB; 24 25 26 float Csub = 0.0f; 27 28 29 for (int a = aBegin, b = bBegin; 30 a <= aEnd; 31 a += aStep, b += bStep) { 32 AS(ty, tx) = A[a + WA * ty + tx]; BS(ty, tx) = B[b + WB * ty + tx]; 33 34 35 barrier(CLK_LOCAL_MEM_FENCE); 36 37 for (int k = 0; k < BLOCK_SIZE; ++k) 38 Csub += AS(ty, k) * BS(k, tx); 39 40 barrier(CLK_LOCAL_MEM_FENCE); 41 } 42 43 C[get_global_id(1) * get_global_size(0) + get_global_id(0)] = Csub; 44 45 46 } 6.4.2. Objašnjenje programskog koda Python programski kod za ovu implementaciju množenja matrica u OpenCL-u je jednak kao i kod prethodne implementacije množenja, pa c´ e autor ovdje samo opisati razlike koje se odnose na sam kernel. Kernel se razlikuje od prethodnog kernela po tome što koristi dvije pomo´cne matrice dimenzija 55 BS × BS. Te matrice nalaze se u lokalnoj memoriji svake radne grupe. One se definiraju u 10. i 11. liniji prikazanog koda, a s podacima se pune unutar jedne for petlje, što je vidljivo u 33. i 34. liniji koda. Svaka radna stavka dijelove pomo´cne matrice u lokalnoj memoriji puni s odredenim ¯ podacima iz matrica A i B. Radne stavke unutar radne grupe dijele lokalnu memoriju pa svaka radna stavka može u lokalnoj memoriji vidjeti sve podatke koje su ostale radne stavke pohranile u pomo´cne matrice. Zato one ne moraju te podatke dohva´cati iz globalne memorije OpenCL uredaja ¯ što znatno pove´cava performanse ovog kernela u usporedbi s prethodnim. Budu´ci da radne stavke koriste podatke iz lokalne memorije ukljuˇcuju´ci i podatke koje su u lokalnu memoriju pohranile druge radne stavke, potrebno je koristiti memorijske barijere (36. i 41. linija koda). Memorijske barijere osiguravaju da su sve radne stavke pohranile potrebne podatke u pomo´cne tablice u lokalnoj memoriji OpenCL uredaja, a time se omogu´cuje ispravan rad ker¯ nela. Ako se ne bi koristile memorijske barijere, radne stavke ne bi cˇ ekale da ostale radne stavku pohrane sve potrebne podatke u pomo´cne tablice, a to bi za posljedicu imalo netoˇcno izraˇcunati umnožak matrica. 6.4.3. Rezultati testiranja N = 1200 N = 2400 N = 4800 CPU (numpy.dot) GPU 0.23 s 0.04372 s 1.4970 s 0.3016 s 11.742 s 2.2406 s CPU / GPU 5.26 4.96 5.24 Tablica 6.7: Usporedba brzina množenja matrica - float32 - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 N = 1200 N = 2400 N = 4800 CPU (numpy.dot) GPU CPU / GPU 0.8582 s 0.0565 s 15.20 7.6354 s 0.4154 s 18.38 63.3533 s 3.1215 s 20.30 Tablica 6.8: Usporedba brzina množenja matrica - float32 - i7 720QM 1.6 GHz i Mobility Radeon HD 5850 CPU (numpy.dot) GPU CPU / GPU N = 1200 5.2390 s 0.6039 s 8.68 N = 2400 41.8379 s 4.6331 s 9.03 Tablica 6.9: Usporedba brzina množenja matrica - float32 - E-350 APU 1.6 GHz s HD6310 Prema prikazanim tablicama i slikama vidljivo je da ovaj kernel ima oko 3 puta bolje performanse od prethodnog kernela koji ne koristi lokalnu memoriju OpenCL uredaja. U nekim sluˇcajevima su ¯ brzine izvodenja na grafiˇckom procesoru cˇ ak i 20 puta ve´ce od izvodenja na centralnom procesoru ¯ ¯ 56 Slika 6.4: Usporedba brzine množenja matrica - float32 - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 raˇcunala (tablica 6.8 ), a to ovisi o brzini centralnog i grafiˇckog procesora raˇcunala i funkcije s kojom se usporeduje OpenCL funkcija. U tablicama 6.7, 6.8 i 6.9 prikazani su rezultati dobiveni ¯ kod usporedivanja implementiranog OpenCL algoritma i numpy.dot rutine, a u tablicama 6.10 i ¯ 6.11 prikazani su rezultati dobiveni kod usporedivanja implementiranog OpenCL algoritma i ve´c ¯ prethodno prikazane C rutine za množenje matrica. Na temelju tablica i slika prikazanih za ovaj i prethodni kernel može se zakljuˇciti da kvaliteta i optimiziranost kernela ima veliki utjecaj na brzinu njegovog izvršavanja. Na temelju uoˇcenih razlika izmedu ¯ brzina jednostavnih C rutina i rutina iz optimiziranih biblioteka te razlika u performansama izmedu ¯ optimiziranih i neoptimiziranih OpenCL kernela može se zakljuˇciti da je svakako dobra ideja isprobati postoje´ce biblioteke za CPU i vidjeti da li su njihove performanse zadovoljavaju´ce, a ako nisu tada se može implementirati algoritam u OpenCL-u pri cˇ emu se mogu koristiti metode optimizacije prikazane u ovom diplomskom radu. CPU (C rutina) GPU CPU / GPU N = 1200 4.3253 s 0.0455 s 95.12 N = 2400 36.3260 s 0.2968 s 122.40 Tablica 6.10: Usporedba brzina množenja matrica - float32 - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 Na temelju podataka u tablici 6.12 može se zakljuˇciti da nema znatnih razlika u brzinama izvode¯ nja implementiranog OpenCL programa s obzirom na korišteni operacijski sustav jer odstupanja trajanja izvodenja po razliˇcitim operacijskim sustavima nikad nisu velika bez obzira na prosjeˇcno ¯ 57 CPU (C rutina) GPU CPU / GPU N = 1200 19.9522 s 0.5948 s 33.54 N = 2400 156.8220 s 4.6335 s 33.85 Tablica 6.11: Usporedba brzina množenja matrica - float32 - E-350 APU 1.6 GHz s HD6310 trajanje izvodenja OpenCL programa. ¯ N = 1200 N = 2400 N = 4800 Windows 7 0.04372 s 0.3016 s 2.2406 s Ubuntu 13.04 Arch Linux 0.05013 s 0.04392 s 0.29597 s 0.2978 s 2.1819 s 2.2013 s Tablica 6.12: Usporedba brzina množenja matrica na razliˇcitim operacijskim sustavima 6.5. Mandelbrot fraktal Klasiˇcni algoritam za izraˇcun Mandelbrotovog skupa je vrlo prikladan za paralelizaciju, a time i implementaciju u OpenCL-u ili nekoj sliˇcnoj tehnologiji. Razlog zbog cˇ ega je algoritam tako prikladan za korištenje u sklopu GPGPU-a je cˇ injenica da algoritam ima vrlo mali broj ulaza te da je izraˇcun za svaku toˇcku potpuno nezavisan o drugim toˇckama. Može se pretpostaviti da c´ e implementacija u OpenCL-u biti puno brža od standardne implementacije koja se izvršava na centralnom procesoru raˇcunala. 6.5.1. Programski kod 2 # -*- coding: utf-8 -*import numpy as np 3 import time 4 import pyopencl as cl 5 import Tkinter as tk 6 import Image 7 import ImageTk 8 count = 10 # broj ponavljanja izracuna za benchmark 9 w = 1280 1 10 h = 720 11 broj_iteracija = 512 12 def izracunaj(q, broj_iteracija): 13 platforms = cl.get_platforms() 14 58 15 # može se zamijeniti CPU sa GPU 16 devices = platforms[0].get_devices(cl.device_type.GPU) 17 device = devices[0] 18 19 ctx = cl.Context([device]) 20 queue = cl.CommandQueue(ctx) 21 22 output = np.empty(q.shape, dtype=np.uint16) 23 24 # host -> device -------------------------------------------- 25 t1 = time.time() 26 mf = cl.mem_flags 27 q_opencl = cl.Buffer(ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=q) 28 29 output_opencl = cl.Buffer(ctx, mf.WRITE_ONLY, output.nbytes) 30 31 32 33 prg = cl.Program(ctx, """ __kernel void mandelbrot(__global float2 *q, __global ushort *output, ushort const broj_iteracija) 34 35 { 36 int gid = get_global_id(0); 37 float x_pom; 38 float x = 0; 39 float y = 0; 40 int iteracija = 0; 41 output[gid] = 0; 42 43 44 while (iteracija < broj_iteracija && x*x + y*y <= 4.0f) { x_pom = x*x - y*y + q[gid].x; y = 2 * x*y + q[gid].y; x = x_pom; 45 46 47 iteracija++; 48 49 } 50 51 52 if (iteracija >= broj_iteracija) output[gid] = 0; 59 else 53 output[gid] = iteracija; 54 55 } 56 """).build() 57 58 t1 = time.time() 59 for i in range(count): event = prg.mandelbrot(queue, output.shape, (256,), q_opencl, 60 output_opencl, np.uint16(broj_iteracija)) 61 62 event.wait() 63 gpu_time = time.time() - t1 64 gpu_time /= count 65 66 # device -> host -------------------------------------------- 67 t1 = time.time() 68 cl.enqueue_copy(queue, output, output_opencl).wait() 69 return output, gpu_time 70 71 x1, x2, y1, y2 = -2.13, 0.8, -1.3, 1.3 72 x_range = np.arange(x1, x2, (x2-x1)/w) 73 74 y_range = np.arange(y2, y1, (y1-y2)/h) * 1j q = np.ravel(x_range + y_range[:, np.newaxis]).astype(np.complex64) 75 76 output, gpu_time = izracunaj(q, broj_iteracija) 77 print("Utroseno vrijeme", gpu_time) 78 t = tk.Tk() 79 t.title("Mandelbrot fraktal") 80 M = (output.reshape((h,w)) / 81 82 M -= float(output.max()) * 255.).astype(np.uint8) 255 84 M *= -1 im = Image.fromarray(M) 85 image = ImageTk.PhotoImage(im) 86 label = tk.Label(t, image=image) 87 label.pack() 88 t.mainloop() 83 60 Slika 6.5: Mandelbrot fraktal generiran pomo´cu OpenCL-a 6.5.2. Objašnjenje programskog koda Program koristi Python module numpy, PyOpenCL, Image, Tkinter i ImageTk. Modul PyOpenCL se koristi za rad sa samim OpenCL-om, dok se modul numpy koristi za rad s matricama, generiranje matrica koje c´ e koristiti OpenCL i sl. Tkinter i ImageTk moduli se koriste kako bi se fraktal koji je generiran pomo´cu OpenCL-a mogao prikazati na ekranu korisnika u posebnom prozoru (Tkinter je modul koji se vrlo cˇ esto koristi za izradu grafiˇckih suˇcelja aplikacija pisanih u Pythonu). Modul Image se koristi kako bi se matrica koja predstavlja fraktal pretvorila u Python objekt koji predstavlja sliku. Medu ¯ prvim linijama koda vidi se koje veliˇcine c´ e biti matrica koja c´ e predstavljati fraktal. Varijabla w predstavlja širinu, a varijabla h visinu te matrice (i na kraju slike koje se dobije kao jedan od rezultata pokretanja Python skripte). O tim varijablama jako zavisi vrijeme koje c´ e biti potrebno za provodenje izraˇcuna. Bitna je i varijabla broj_iteracija o kojoj ovisi toˇcnost izraˇcunatog ¯ fraktala, ali kao posljedica toga i vrijeme potrebno za izraˇcun. Kod izraˇcuna je korišten takozvani escape time algoritam koji je vrlo popularan zbog svoje jednostavnosti. Dobro objašnjenje escape time algoritma dostupno je na sljede´coj mrežnoj adresi: http://yozh.org/2010/12/01/ mset005/. Najprije se definira broj ponavljanja izvršavanja kernela (kernel se izvršava više puta kako bi se 61 toˇcnije odredilo prosjeˇcno vrijeme potrebno za izraˇcun), dimenzije fraktala i broj iteracija (8. do 11. linija koda). Nakon toga definiraju se varijable x1, x2, y1 i y2 (71. linija koda) koje predstavljaju minimalnu x koordinatu, maksimalnu x koordinatu, minimalnu y koordinatu te maksimalnu y koordinatu u cˇ ijem rasponu se raˇcuna Mandelbrotov fraktal. Nastavlja se kreiranjem matrice q dimenzija h × w koja predstavlja toˇcke u koordinatnom sustavu s vrijednostima x u rasponu od x1 do x2 i vrijednostima y u rasponu od y1 do y2. Umjesto da se toˇcke u koordinatnom sustavu u matricu zapisuju kao uredeni parovi, toˇcke se zapisuju kao ¯ kompleksni brojevi kako bi bilo lakše korištenje njihovih koordinata u OpenCL kernelu. Matrica q se pretvara u jednodimenzionalnu matricu (tj. polje) sa w ∗ h elemenata. Sve što je opisano u ovom odlomku odnosi se na 70. do 74. liniju koda. Nakon što je kreirano polje q koje sadrži toˇcke za koje se raˇcuna pripadnost Mandelbrotovom skupu pokre´ce se funkcija izracunaj (76. linija koda). U toj funkciji dohva´ca se popis OpenCL uredaja ¯ te se nakon toga kreira OpenCL kontekst i red naredbi (13. do 20. linija koda). Slijedi kopiranje polja q u globalnu memoriju OpenCL uredaja ¯ te kreiranje polja u globalnoj memoriji OpenCL uredaja ¯ u koje c´ e se spremiti rezultat izvršavanja kernela (26. do 29. linija koda). Nakon toga kernel se izvršava nekoliko puta nad svim radnim stavkama te se raˇcuna prosjeˇcno vrijeme potrebno za njegovo izvršavanje nad svim radnim stavkama (31. do 64. linija koda). Polje s rezultatima se kopira u memoriju hosta (68. linija koda) te Python funkcija vra´ca to polje i prosjeˇcno vrijeme potrebno za izvršavanje kernela nad svim radnim stavkama (69. linija koda). Polje koje je dobiveno kao rezultat izraˇcuna sadrži w ∗ h elemenata koje imaju vrijednosti u mogu´cem rasponu od 0 do broj_iteracija - 1. To polje se pretvara u matricu dimenzija h × w (koja c´ e predstavljati sliku koju c´ e se iscrtati). Vrijednosti u dobivenoj matrici se skaliraju i pretvaraju u cijele brojeve tako da budu u rasponu od 0 do 255. Ovisno o tim vrijednostima iscrtat c´ e se slika, 0 znaˇci neka toˇcka nije element Mandelbrotovog skupa pa ta toˇcka bude bijela na rezultiraju´coj slici. Pove´canjem vrijednosti do 255 toˇcka koja c´ e se iscrtati na slici je sve tamnija jer je sve ve´ca vjerojatnost da je ta toˇcka u Mandelbrotovom skupu. Sve opisano u ovom odlomku te prikaz slike odnosi se na 78. do 88. liniju koda. Sam kernel je relativno jednostavan. Kao što je ve´c reˇceno, kernel koristi escape time algoritam za raˇcunanje vrijednosti pripadnosti toˇcaka Mandelbrotovom skupu. Kernel se pokre´ce w ∗ h puta, tj. za svaku toˇcku slike Mandelbrotovog fraktala pokrene se radna stavka koja izvrši kernel (te se to više puta ponavlja kako bi se toˇcnije izraˇcunalo prosjeˇcno utrošeno vrijeme). Kernel prilikom izvršavanja iz polja q uzima x i y koordinate toˇcke za koje treba provjeriti pripadnost Mandelbrotovom skupu te vra´ca vrijednost u rasponu od od 0 do broj_iteracija - 1. 62 6.5.3. Rezultati testiranja OpenCL CPU OpenCL GPU CPU / GPU 256x256 0.01118 s 0.00108 s 10.36 640x480 0.051604 s 0.00407 s 12.68 1280x720 0.15016 s 0.01099 s 13.66 1920x1080 0.34508 s 0.02473 s 13.95 Tablica 6.13: Usporedba brzina raˇcunanja Mandelbrot fraktala - 512 iteracija Slika 6.6: Usporedba brzina raˇcunanja Mandelbrot fraktala - 512 iteracija Na temelju prikazane slike 6.6 i tablice 6.13 vidljivo je da raˇcunanje Mandelbrotovog fraktala korištenjem centralnog procesora 6traje više od 10 puta dulje od obavljanja istih izraˇcuna na grafiˇckom procesoru, cˇ ak i kada se iskoriste sve jezgre procesora. Pove´canjem same veliˇcine matrice koja predstavlja Mandelbrotov fraktal postepeno se pove´cava i razlika izmedu ¯ brzine provodenja ¯ izraˇcuna na grafiˇckom i centralnom procesoru raˇcunala. Da se uzme dovoljno mala veliˇcina matrice mogu´ce je da bi se algoritam cˇ ak i izvršio brže na centralnom procesoru jer se dio performansi kod izraˇcuna pomo´cu grafiˇckog procesora gubi na prijenos samih podataka do grafiˇckog procesora i obrnuto. No, ako se žele provoditi jednostavniji izraˇcuni na malom skupu onda niti nema smisla ni potrebe za korištenjem GPGPU-a jer je vrijeme potrebno za provodenje takvih izraˇcuna u ve´cini sluˇcajeva vrlo malo. ¯ Isti OpenCL program isproban je i na centralnom i grafiˇckom procesoru raˇcunala. Na taj naˇcin su se kod izraˇcuna iskoristile sve dostupne jezgre centralnog procesora (jer su se kod izvodenje ¯ OpenCL koda na centralnom procesoru automatski iskoristile sve njegove jezgre). Najjednostavnija implementacija raˇcunanja Mandelbrotovog fraktala pisana u programskom jeziku C ili C++ bi 63 bez korištenja višedretvenog rada imala dosta niže performanse od OpenCL koda koji se izvršava na centralnom procesoru raˇcunala, jer OpenCL kod iskorištava sve jezgre procesora. Pretpostavka toga je da CPU ima više jezgri, što je istina za ve´cinu današnjih centralnih procesora. Ovo nas dovodi do još jedne od važnijih prednosti korištenja OpenCL-a naspram uobiˇcajenog rada s više dretvi uz sinkronizaciju sa semaforima i sliˇcnim mehanizmima operacijskog sustava, a ta prednost je cˇ injenica da (pogotovo ako se koristi unutar nekog skriptnog jezika) pisanje OpenCL koda ponekad može biti jednostavnije od pisanja koda koji koristi više dretvi kod izraˇcuna. Možemo zakljuˇciti da je korištenje OpenCL-a vrlo pogodno za raˇcunanje Mandelbrotovog (i sliˇcnih) fraktala. To znaˇci da je pretpostavka da c´ e korištenje OpenCL-a za raˇcunanje fraktala biti mnogo brže od korištenja standardnih algoritama za CPU bila dobra. 6.6. Sortiranje Korištenje GPGPU-a za sortiranje daje odredeno pove´canje performansi u odnosu na klasiˇcno ¯ sortiranje korištenjem centralnog procesora raˇcunala, ali pove´canja brzina nisu velika. Razlog za to je cˇ injenica da grafiˇcki procesori nisu najpogodniji za sortiranje zbog potrebe za velikim brojem pristupa globalnoj memoriji kod sortiranja. Ipak, pove´canje performansi se može dobiti zbog toga jer grafiˇcki procesori imaju puno ve´cu procesorsku mo´c od klasiˇcnih centralnih procesora raˇcunala. Pri tome je potrebno napomenuti da brzina sortiranja na grafiˇckom procesoru jako ovisi o samom algoritmu koji se koristi i kvaliteti implementacije tog algoritma, što je istina i za sve ostale vrste problema koji se mogu rješavati uz pomo´c GPGPU-a. Mogu´ce je da sortiranje pomo´cu grafiˇckog procesora bude puno sporije od sortiranja korištenjem centralnog procesora raˇcunala ako se koristi algoritam koji nije prikladan za paralelizaciju ili se usporeduju algoritmi za sortiranje razliˇcitih složenosti (npr. Quicksort i selection sort). Primjer ¯ jednog sporijeg algoritma za sortiranje je selection sort algoritam, koji daje jako loše performanse kod implementacije u OpenCL-u i u bilo kojem drugom programskom jeziku jer je njegova složenost jednaka O(n2 ). Selection sort se ne koristi u praksi zbog svoje sporosti, ali autor je ovaj algoritam odabrao i implementirao u OpenCL-u radi njegove jednostavnosti — algoritam je jednostavan za paralelizaciju i pogodan je za razne eksperimente s optimizacijama OpenCL kernela, što c´ e se vidjeti u nastavku. Razne optimizacije isprobane na selection sort algoritmu u principu bi se mogle primjeniti i na složenije algoritme, a na temelju selection sort algoritma one se mogu lakše shvatiti, što je takoder ¯ jedan od razloga za odabir ovog algoritma. S druge strane, autor je odabrao bitonic sort algoritam za implementaciju u OpenCL-u jer daje dosta dobre performanse kod izvodenja na GPU zbog toga što je dizajniran tako da se može iz¯ voditi paralelno, pa je vrlo prikladan za implementaciju u OpenCL-u te se može koristiti u praksi zbog dobrih performansi. Algoritam takoder ¯ daje jako dobre performanse kod implementacije na CPU jer njegova složenost iznosi O(n log n), zbog cˇ ega je algoritam puno brži od selection sort 64 algoritma što c´ e se vidjeti i po rezultatima testiranja u nastavku. Osim tih jednostavnih algoritama sortiranja, postoje i mnogi drugi složeniji algoritmi cˇ ijom implementacijom u OpenCL-u bi se vjerojatno postigle dobre performanse. Primjer jednog takvog algoritma je Duane Merrill Radix sort (Merrill & Grimshaw, 2011), Potrebno je napomenuti da razlike u performansama izmedu ¯ sortiranja na CPU i GPU jako ovise o kvaliteti implementacija algoritama koji se koriste, što se može vidjeti i po rezultatima testiranja u nastavku na temelju velike razlike izmedu ¯ brzine jednostavne implementacije bitonic sort algoritma u programskom jeziku C i numpy.sort rutine iz numpy modula za Python (iako oba algoritma za sortiranje imaju složenost O(n log n)) . Zbog toga se treba koristiti najbolja implementacija prikladnih algoritama (npr. implementacija iz neke biblioteke funkcija) ako se žele posti´ci najbolji rezultati. Dapaˇce, mogu´ce je da se korištenjem kvalitetnije implementacije algoritama za CPU postignu puno ve´ce brzine od nekih lošijih implementacija za GPU, pa u nekim sluˇcajevima niti nema potrebe za korištenjem GPGPU-a. Ipak, ako se žele maksimizirati performanse, korištenje dobre implementacije za GPU je opcija koja bi se svakako trebala uzeti u obzir. 6.6.1. Selection sort U nastavku je prikazan programski kod selection sort (Knuth, 1998) algoritma implementiranog u OpenCL-u. 2 # -*- coding: utf-8 -*import pyopencl as cl 3 import pyopencl.array as cl_array 4 import numpy 5 import numpy.linalg as la 6 import time 7 import diplomski 1 8 9 10 N = eval(raw_input("Unesite N: ")) a = numpy.random.rand(N).astype(numpy.float32) 11 12 platforms = cl.get_platforms() 13 devices = platforms[0].get_devices(cl.device_type.GPU) 14 device = devices[0] 15 16 ctx = cl.Context([device]) 17 queue = cl.CommandQueue(ctx) 65 18 19 # host -> device ------------------------------------------------ 20 t = time.time() 21 a_dev = cl_array.to_device(queue, a) 22 dest_dev = cl_array.empty_like(a_dev) 23 push = time.time() - t 24 # --------------------------------------------------------------- 25 26 t0 = time.time() 27 prg = cl.Program(ctx, """ 29 __kernel void ParallelSelection(__global const float * in, __global float * out) 30 { 28 31 int i = get_global_id(0); 32 int n = get_global_size(0); 33 float a = in[i]; 34 int pos = 0; 35 for (int j = 0; j < n; j++) 36 { 37 float b = in[j]; 38 bool manji = (b < a) || (b == a && j < i); 39 pos += (manji) ? 1 : 0; 40 } 41 out[pos] = a; 42 } 43 """).build() 44 45 t = time.time() 46 event = prg.ParallelSelection(queue, a.shape, (256,), a_dev.data, dest_dev.data) 47 48 event.wait() 49 gpu_time = time.time() - t 50 51 # device -> host ------------------------------------------------ 52 t = time.time() 53 sorted_a = dest_dev.get() 54 pull = time.time() - t 55 66 56 # --------------------------------------------------------------- 57 gpu_total = gpu_time + push + pull 58 t0 = time.time() 59 a = diplomski.selection_sort(N, a.tolist()) 60 t1 = time.time() 61 a = numpy.matrix(a).astype(numpy.float32).tolist()[0][:N] 62 #a.sort() 63 cpu_time = t1-t0 64 print "GPU total [s]:", gpu_total 65 print "GPU push [s]:", push 66 print "GPU pull [s]:", pull 67 print "GPU compute (host-timed) [s]:", gpu_time 68 69 # cpu comparison ------------------------------------------------ 70 print 71 print "GPU==CPU:",all(a == sorted_a) 72 print 73 print "CPU time (s)", cpu_time 74 print 75 76 print "GPU speedup (with transfer): ", cpu_time/gpu_total 77 print "GPU speedup (without transfer): ", cpu_time/gpu_time Dijelovi koda vezani uz kreiranje konteksta i reda naredbi, odabira OpenCL uredaja, kopiranja ¯ podataka u globalnu memoriju OpenCL uredaja ¯ i sliˇcnih akcija su vrlo sliˇcni ve´c prethodno objašnjenim kodovima, pa ih autor ovdje ne´ce detaljno opisivati. Ovdje je najbitnije opisati sam OpenCL kernel koji je definiran u programskom kodu s poˇcetkom na 27. liniji koda i završetkom na 43. liniji koda. Kernel se pokre´ce jednom za svaki od N brojeva koje treba sortirati. Kad se pokrene kernel za odredenu radnu stavku on dohva´ca globalni ID radne ¯ stavke te ukupan broj svih radnih stavki (tj. broj N ) (31. i 32. linija koda). Nakon toga pokre´ce se petlja koja se izvršava N puta koja prolazi po svim elementima polja koje treba sortirati te odreduje ¯ koliko brojeva unutar polja je manje od broja kojeg predstavlja trenutna radna stavka (35. do 40. linija koda). Odmah je oˇcito da je ovaj algoritam priliˇcno loš zbog jako velikog broja pristupa globalnoj memoriji OpenCL uredaja. ¯ Na temelju ukupnog broja vrijednosti koje su manje od vrijednosti koju predstavlja trenutna radna stavka odredeuje se pozicija u sortiranom polju na koju treba smjestiti vrijednost koju predstavlja ¯ trenutna radna stavka te se ta vrijednost zapisuje u sortirano izlazno polje (41. linija koda). 67 Brzine sortiranja na GPU usporedene su s brzinama sortiranja na centralnom procesoru raˇcunala, a ¯ u oba sluˇcaja korišten je selection sort algoritam. Sljede´ci kod predstavlja selection sort algoritam koji se izvršavao na centralnom procesoru raˇcunala implementiran u programskom jeziku C. 1 2 float* selection_sort(int N, float* A){ int i, j, Max; 3 float pom; 4 for (i = N; i > 1; i--) { 5 Max = 0; 6 for (j = 1; j < i; j++) if (A[j] > A[Max]) Max 7 8 pom = A[i-1]; 9 A[i-1] = A[Max]; A[Max] = pom; 10 11 } 12 return A; 13 = j; } Implementacija sortiranja u OpenCL-u prikazana u ovom dijelu diplomskog rada daje priliˇcno loše performanse, kao i implementacija tog algoritma na CPU zbog složenosti selection sort algoritma koja iznosi O(n2 ), što se može zakljuˇciti i na temelju sljede´cih tablica koje prikazuju rezultate testiranja ovog algoritma na razliˇcitim grafiˇckim i centralnim procesorima. U ovom poglavlju prikazana je i implementacija bitonic sort algoritma u OpenCL-u koja daje puno bolje performanse. N = 16K N = 32K N = 64K N = 128K N = 256K GPU CPU 0.0364 s 0.1890 s 0.0737 s 0.6916 s 0.2945 s 2.7325 s 1.0843 s 10.8105 s 4.2843 s 43.5514 s CPU / GPU 5.19 9.38 9.28 9.97 10.17 Tablica 6.14: Usporedba brzina sortiranja selection sort algoritmom - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 Iako je selection sort algoritam jedan od lošijih algoritama za sortiranje te ne daje najbolje performanse, implementacija tog algoritma u OpenCL-u daje bolje performanse od implementacije na CPU. Ubrzanje dobiveno korištenjem grafiˇckog procesora umjesto centralnog procesora ovisi o broju stream procesora kojima raspolaže GPU, a kod GPU-ova s ve´cim brojem stream procesora ubrzanje je ve´ce (tablice 6.14 i 6.15). Rezultati provedenih testiranja pokazuju ubrzanje i do 10 puta, dok ubrzanja na sporijem grafiˇckom procesoru iznose oko 2 puta (tablica 6.16). Ipak, izmjenama u OpenCL kernelu mogu se posti´ci i znatno ve´ca ubrzanja, što c´ e biti prikazano u nastavku. 68 N = 16K N = 32K N = 64K N = 128K N = 256K GPU CPU 0.0326 s 0.2145 s 0.1104 s 0.8427 s 0.4138 s 3.2758 s 1.6128 s 15.6254 s 6.3352 s 61.7627 s CPU / GPU 6.57 7.64 7.92 9.69 9.75 Tablica 6.15: Usporedba brzina sortiranja selection sort algoritmom - i7 720QM 1.6 GHz i Mobility Radeon HD 5850 GPU N = 16K 0.3383 s N = 32K 1.2835 s N = 64K 4.9626 s N = 128K 19.7430 s N = 256K 78.7903 s CPU CPU / GPU 0.62702 s 1.85 2.3934 s 1.86 9.4942 s 1.91 38.1019 s 1.92 157.1929 s 1.99 Tablica 6.16: Usporedba brzina sortiranja selection sort algoritmom - E-350 APU 1.6 GHz s HD6310 6.6.2. Selection sort — poboljšanje korištenjem lokalne memorije U nastavku je prikazan modificirani OpenCL kernel selection sort algoritma koji je izmijenjen tako da koristi lokalnu memoriju OpenCL uredaja. Prikazan je samo programski kod kernela pisan u ¯ modificiranom C jeziku kojeg koristi OpenCL bez koda u Pythonu jer je ostatak koda koji je pisan u Python programskom jeziku jednak kao i ve´c prikazani programski kod osnovnog selection sort algoritma implementiranog u OpenCL-u. Sliˇcno vrijedi i za ostala poboljšanja kernela prikazana u nastavku, pa c´ e i tamo biti samo prikazani kerneli bez programskog koda u Pythonu. 2 __kernel void ParallelSelection(__global const float * in, __global float * out) 3 { 1 4 __local float pom[256]; 5 int i = get_global_id(0); 6 int li = get_local_id(0); 7 int n = get_global_size(0); 8 float a = in[i]; 9 int pos = 0; 10 for (int j = 0; j < n; j+= 256) 11 { 12 pom[li] = in[j + li]; 13 barrier(CLK_LOCAL_MEM_FENCE); 14 for (int k = 0; k < 256; k++) { 69 15 float b = pom[k]; 16 bool manji = (b < a) || (b == a && (j+k) < i); 17 pos += (manji) ? 1 : 0; 18 } 19 barrier(CLK_LOCAL_MEM_FENCE); 20 } 21 out[pos] = a; 22 } Kernel je izmjenjen tako da koristi lokalnu memoriju na naˇcin da svaka radna skupina u lokalnoj memoriji ima dostupno polje od 256 elemenata (4. linija koda), gdje 256 predstavlja veliˇcinu radne skupine. Kernel radi tako da se u lokalnu memoriju sprema 256 elemenata poˇcetnog polja koje treba sortirati (12. linija koda), nakon toga svaka radna stavka provjerava koliko od tih 256 vrijednosti je ve´ce od vrijednosti koju predstavlja ta radna stavka (14. do 18. linija koda), te se nakon toga prelazi na sljede´cih 256 vrijednosti poˇcetnog polja. Taj postupak se ponavlja N/256 puta, gdje N predstavlja broj vrijednosti koje treba sortirati. Svaka radna stavka unutar radne grupe u lokalnu memoriji spremi jednu od 256 vrijednosti (12. linija koda). Da bi se osiguralo da su sve radne stavke vrijednosti pohranile u lokalnu memoriju koristi se memorijska barijera (13. linija koda). Radne stavke unutar radne grupe nakon toga mogu pristupiti vrijednostima koje su ostale radne stavke pohranile u lokalnu memoriju. Budu´ci da se iz polja vrijednosti koje treba sortirati redosljedom uzima po 256 vrijednosti te se te vrijednosti spremaju u lokalnu memoriju OpenCL uredaja, potrebno je koristiti memorijsku barijeru kako bi ¯ se osiguralo da vrijednosti pohranjene u lokalnu memoriju predstavljaju vrijednosti iz iste skupine od 256 vrijednosti iz polja kojeg treba sortirati (19. linija koda). Korištenjem lokalne memorije smanjuje se ukupan broj pristupa globalnoj memoriji, a to rezultira poboljšanjem performansi ovog kernela u usporedbi s prethodno prikazanim kernelom. N = 16K N = 32K N = 64K N = 128K N = 256K GPU CPU 0.0077 s 0.1889 s 0.0343 s 0.6916 s 0.0844 s 2.7320 s 0.3334 s 10.8103 s 1.2746 s 43.5513 s CPU / GPU 24.44 20.19 32.37 32.43 34.17 Tablica 6.17: Usporedba brzina sortiranja selection sort algoritmom - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 Prema rezultatima iz tablica 6.17, 6.18 i 6.19 može se zakljuˇciti da ovaj OpenCL kernel daje znatno bolje performanse od selection sort algoritma implementiranog za CPU. Ubrzanja dostižu cˇ ak i do oko 35 puta na grafiˇckim procesorima s ve´cim brojem dostupnih stream procesora (tablice 6.17 i 70 N = 16K N = 32K N = 64K N = 128K N = 256K GPU CPU 0.0109 s 0.2113 s 0.0387 s 0.8089 s 0.1285 s 3.3676 s 0.4802 s 15.2511 s 1.8918 s 62.5134 s CPU / GPU 19.41 20.88 26.21 31.76 33.04 Tablica 6.18: Usporedba brzina sortiranja selection sort algoritmom - i7 720QM 1.6 GHz i Mobility Radeon HD 5850 GPU N = 16K 0.0981 s N = 32K 0.3752 s N = 64K 1.4804 s N = 128K 5.9043 s N = 256K 23.5969 s CPU CPU / GPU 0.7692 s 7.84 2.9842 s 7.95 11.8765 s 8.02 47.9504 s 8.12 199.0612 s 8.44 Tablica 6.19: Usporedba brzina sortiranja selection sort algoritmom - E-350 APU 1.6 GHz s HD6310 6.18), dok na sporijem grafiˇckom procesoru ubrzanje iznosi oko 8 puta (tablica 6.19). U usporedbi s prethodnim kernelom ovaj kernel ima znatno bolje performanse, pa možemo zakljuˇciti da se korištenjem lokalne memorije može znaˇcajno skratiti vrijeme potrebno za izvršavanje kernela, a time i vrijeme potrebno za rješavanje odredenog problema korištenjem OpenCL-a. ¯ 6.6.3. Selection sort — poboljšanje smanjenjem ukupnog broja radnih stavki U nastavku je prikazan kernel najjednostavnije verzije selection sort algoritma implementiranog u OpenCL-u koji je modificiran tako da je smanjen ukupan broj radnih stavki, cˇ ime je ujedno smanjen i ukupan broj pristupa globalnoj memoriji OpenCL uredaja. Zbog smanjenog broja pris¯ tupa globalnoj memoriji ovaj kernel daje znatno bolje performanse od prvog opisanog kernela koji implementira selection sort algoritam. 2 __kernel void ParallelSelection(__global const float * in, __global float * out) 3 { 4 #define NB %d 1 5 6 int i = get_global_id(0); 7 int n = get_global_size(0); 8 9 float a[NB]; 71 int pos[NB]; 10 11 for (int k = 0; k < NB; k++) { 12 a[k] = in[NB * i + k]; pos[k] = 0; 13 14 } 15 16 for (int j = 0; j < NB * n; j++) { 17 18 19 float b = in[j]; 20 for (int k = 0; k < NB; k++) { bool manji = (b < a[k]) || (b == a[k] && j < NB * i + k); pos[k] += (manji) ? 1 : 0; 21 22 } 23 } 24 25 for (int k = 0; k < NB; k++) out[pos[k]] = a[k]; 26 27 } Smanjenje broja radnih stavki, a time i broja pristupa globalnoj memoriji postignuto je tako da su varijable a i pos zamijenjene poljima veliˇcine NB (9. i 10. linija koda). Vrijednosti NB koje su korištene u testiranju su: 1, 2, 4, 8 i 16. Prema rezultatima iz tablica 6.20, 6.21 i 6.22 može se zakljuˇciti da kernel u ve´cini sluˇcajeva daje najbolje performanse kad je vrijednost NB jednaka 8. Budu´ci da su varijable a i pos zamijenjene poljima veliˇcine NB, akcije pohrane i cˇ itanja podataka iz varijabli a i pos zamijenjene su for petljama koje iste akcije provode nad cijelim poljima a i pos (14. do 17. linija koda, 25. do 28. linija koda i 33. linija koda). GPU N = 128K, NB = 1 1.0831 s N = 128K, NB = 2 0.5628 s N = 128K, NB = 4 0.3360 s N = 128K, NB = 8 0.2958 s N = 128K, NB = 16 0.3852 s CPU 10.7964 s 10.7783 s 10.8107 s 10.8134 s 10.8271 s CPU / GPU 9.97 19.15 32.17 36.56 28.11 Tablica 6.20: Usporedba brzina sortiranja selection sort algoritmom - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 Performanse ovog kernela kad je NB = 8 vrlo su sliˇcne performansama prethodnog kernela koji koristi lokalnu memoriju OpenCL uredaja, a u oba sluˇcaja vidi se znatno pove´canja performansi u ¯ usporedbi s prvim i najjednostavnijim kernelom koji implementira selection sort algoritam. Vidljivo je i da se pove´canjem vrijednosti NB pove´cavaju i performanse kernela, no vrijednost NB se 72 GPU N = 128K, NB = 1 1.6167 s N = 128K, NB = 2 0.8255 s N = 128K, NB = 4 0.4667 s N = 128K, NB = 8 0.4510 s N = 128K, NB = 16 0.5435 s CPU 15.6132 s 15.5699 s 15.5923 s 15.6774 s 15.6445 s CPU / GPU 9.66 18.86 33.41 34.76 28.79 Tablica 6.21: Usporedba brzina sortiranja selection sort algoritmom - i7 720QM 1.6 GHz i Mobility Radeon HD 5850 GPU CPU N = 128K, NB = 2 9.8690 s 38.1140 s N = 128K, NB = 4 5.4970 s 37.9513 s N = 128K, NB = 8 5.0250 s 38.1058 s CPU / GPU 3.86 6.90 7.58 Tablica 6.22: Usporedba brzina sortiranja selection sort algoritmom - E-350 APU 1.6 GHz s HD6310 ne može pove´cavati u nedogled kako bi se pove´cale performanse ovog kernela jer nakon odredene ¯ vrijednosti NB (najˇceš´ce 8) performanse ovog kernela znatno opadaju zbog ograniˇcene lokalne memorije OpenCL uredaja. ¯ 6.6.4. Selection sort — kombinacija 1. i 2. poboljšanja U nastavku je prikazan kernel koji implementira oba prethodno opisana poboljšanja OpenCL implementacije selection sort algoritma (korištenje lokalne memorije i smanjenje ukupnog broja radnih stavki). 2 __kernel void ParallelSelection(__global const float * in, __global float * out) 3 { 1 4 #define NB %d 5 6 int i = get_global_id(0); 7 int n = get_global_size(0); 8 int li = get_local_id(0); 9 10 float a[NB]; 11 int pos[NB]; 12 __local float pom[256]; 13 14 for (int k = 0; k < NB; k++) { 73 a[k] = in[NB * i + k]; pos[k] = 0; 15 16 } 17 18 for (int j = 0; j < NB * n; j+= 256) { 19 20 21 pom[li] = in[j + li]; 22 barrier(CLK_LOCAL_MEM_FENCE); 23 for (int l = 0; l < 256; l++) { 24 float b = pom[l]; 25 for (int k = 0; k < NB; k++) { bool manji = (b < a[k]) || (b == a[k] && (j+l) < NB * i + k); pos[k] += (manji) ? 1 : 0; 26 27 } 28 29 } 30 barrier(CLK_LOCAL_MEM_FENCE); } 31 32 for (int k = 0; k < NB; k++) out[pos[k]] = a[k]; 33 34 } Korištena je lokalna memorija kao i u prvom poboljšanju kako bi se smanjio ukupan broj pristupa globalnoj memoriji, pa je u tu svrhu definirano lokalno polje od 256 elemenata (12. linija koda) u koje se redom sprema po 256 vrijednosti ulaznog polja koje sadrži vrijednosti koje treba sortirati. Osim toga varijable a i pos su zamijenjene poljima veliˇcine NB kao i u drugom prikazanom poboljšanju osnovne implementacije selection sort algoritma u OpenCL-u (10. i 11. linija koda). GPU CPU CPU / GPU N = 128K, NB = 1 0.2994 s 10.8144 s 36.12 N = 128K, NB = 2 0.2666 s 10.8088 s 40.54 N = 128K, NB = 4 0.2420 s 10.8120 s 44.68 N = 128K, NB = 8 0.2217 s 10.8500 s 48.57 N = 128K, NB = 16 0.2212 s 10.8204 s 48.93 N = 256K, NB = 8 0.8070 s 43.5502 s 53.97 N = 512K, NB = 8 3.2179 s 175.17976 s 54.44 Tablica 6.23: Usporedba brzina sortiranja selection sort algoritmom - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 Prema rezultatima iz tablica 6.23, 6.24 i 6.25 vidljivo je da kombinacija prvog i drugog poboljšanja selection sort algoritma u OpenCL-u daje performanse bolje od svakog poboljšanja zasebno te da performanse variraju ovisno o vrijednosti NB, a najbolje performanse se dobivaju kad je N B = 8. 74 GPU CPU N = 128K, NB = 1 0.4485 s 15.5288 s N = 128K, NB = 2 0.3730 s 15.7044 s N = 128K, NB = 4 0.3323 s 15.5116 s N = 128K, NB = 8 0.3214 s 15.6791 s N = 128K, NB = 16 0.3609 s 15.6851 s N = 256K, NB = 8 1.1828 s 62.4519 s N = 512K, NB = 8 4.7233 s 256.1932 s CPU / GPU 34.63 42.10 46.68 48.78 43.46 52.80 54.24 Tablica 6.24: Usporedba brzina sortiranja selection sort algoritmom - i7 720QM 1.6 GHz i Mobility Radeon HD 5850 GPU N = 128K, NB = 1 5.5416 s N = 128K, NB = 2 4.5612 s N = 128K, NB = 4 4.0379 s N = 128K, NB = 8 3.7085 s N = 256K, NB = 16 14.8604 s CPU CPU / GPU 37.9940 s 6.86 37.9932 s 8.33 37.9934 s 9.41 38.0608 s 10.26 156.7870 s 10.55 Tablica 6.25: Usporedba brzina sortiranja selection sort algoritmom - E-350 APU 1.6 GHz s HD6310 Poboljšanja zasebno daju ubrzanja od oko 30 do 35 puta kad je N ≥ 128K i N B = 8 na testiranim grafiˇckim procesorima s ve´cim brojem stream procesora, dok na testiranom grafiˇckom procesoru s manjim brojem dostupnih stream procesora ubrzanja iznose oko 8 puta. Ubrzanje ove implementacije selection sort algoritma u OpenCL-u s oba poboljšanja u usporedbi s implementacijom na CPU iznose oko 50 puta za N ≥ 128K i NB = 8 na testiranim grafiˇckim procesorima s vec´ im brojem stream procesora, dok na testiranom grafiˇckom procesoru s manjim brojem dostupnih stream procesora ubrzanja iznose oko 10 puta. 6.6.5. Usporedba implementiranih verzija selection sort algoritma Prvo poboljšanje je oko 4 puta brže od osnovne implementacije selection sort algoritma u OpenCLu, dok je drugo poboljšanje takoder ¯ oko 4 puta brže od osnovne implementacije. Kombinacija prvog i drugog poboljšanja je oko 5 puta brža od osnovne implementacije, što pokazuje da se kombiniranjem raznih poboljšanja može dodatno dobiti na performansama. 75 Slika 6.7: Usporedba brzina sortiranja korištenjem razliˇcitih implementacija selection sort algoritma - Radeon HD 6850 6.6.6. Bitonic sort U ovom dijelu rada bit c´ e obraden ¯ bitonic sort (Batcher, 1968) algoritam implementiran u OpenCLu koji sortira cijelo polje od N brojeva. U nastavku je prikazan programski kod bitonic sort algoritma implementiranog u OpenCL-u. 2 # -*- coding: utf-8 -*import pyopencl as cl 3 import pyopencl.array as cl_array 4 import numpy 5 import time 6 import diplomski 1 7 8 N = eval(raw_input("Unesite N: ")) 9 a = numpy.random.rand(N).astype(numpy.float32) 10 11 platforms = cl.get_platforms() 12 devices = platforms[0].get_devices(cl.device_type.GPU) 13 device = devices[0] 14 ctx = cl.Context([device]) 15 queue = cl.CommandQueue(ctx) 16 17 prg = cl.Program(ctx, """ 76 18 19 __kernel void ParallelBitonic_A(__global const float * in, __global float * out, 20 const int inc, 21 const int dir) 22 { 23 int i = get_global_id(0); 24 int j = i ^ inc; 25 26 float a = in[i]; 27 float b = in[j]; 28 29 bool manji = (b < a) || ( b == a && j < i ); 30 bool swap = manji ^ (j < i) ^ ((dir & i) != 0); 31 out[i] = (swap) ? b : a; 32 33 } 34 """).build() 35 36 push = 0 37 gpu_time = 0 38 pull = 0 39 count = 10 40 for i in range(count): 41 # host -> device -------------------------------------------- 42 t = time.time() 43 a_dev = cl_array.to_device(queue, a) 44 dest_dev = cl_array.empty_like(a_dev) 45 push += time.time() - t 46 # ----------------------------------------------------------- 47 48 t0 = time.time() 49 n = a.shape[0] 50 length = 1 51 while length < n: 52 inc = length 53 while inc > 0: 54 55 event = prg.ParallelBitonic_A(queue, a.shape, (256,), a_dev.data, 77 56 dest_dev.data, 57 numpy.int32(inc), 58 numpy.int32(length << 1)) 59 event.wait() 60 q = a_dev 61 a_dev = dest_dev 62 dest_dev = q 63 inc >>= 1 length <<= 1 64 65 66 t1 = time.time() 67 gpu_time += t1 - t0 68 69 70 # device -> host -------------------------------------------- 71 t = time.time() 72 sorted_a = a_dev.get() 73 pull += time.time() - t 74 75 gpu_time /= count 76 push /= count 77 pull /= count 78 79 # --------------------------------------------------------------- 80 b = a[:] 81 gpu_total = gpu_time + push + pull 82 t0 = time.time() 83 a = diplomski.bitonic_sort(N, a.tolist()) 84 t1 = time.time() 85 cpu_time = t1-t0 86 t0 = time.time() 87 b = b.sort() 88 t1 = time.time() 89 cpu_time2 = t1-t0 90 91 print "GPU total [s]:", numpy.round(gpu_total, 4) 92 print "GPU push [s]:", push 93 print "GPU pull [s]:", pull 78 94 print "GPU compute (host-timed) [s]:", gpu_time 95 96 # cpu comparison ------------------------------------------------ 97 print 98 print "GPU==CPU:",all(a == sorted_a) 99 print 100 print "CPU time (C - bitonic sort) (s)", numpy.round(cpu_time, 4) 101 print "CPU time (numpy.sort) (s)", numpy.round(cpu_time2, 4) 102 print 103 104 print "GPU speedup (with transfer) - C, bitonic sort: ", \ 105 numpy.round(cpu_time/gpu_total, 2) 106 print "GPU speedup (without transfer) - C, bitonic sort: ", \ 107 cpu_time/gpu_time 108 109 print "GPU speedup (with transfer) - numpy.sort: ", \ 110 numpy.round(cpu_time2/gpu_total, 2) 111 print "GPU speedup (without transfer) - numpy.sort: ", 112 cpu_time2/gpu_time \ Jednako kao i kod prethodnog programskog koda dijelovi koda vezani uz kreiranje konteksta i reda naredbi, odabira OpenCL uredaja, kopiranja podataka u globalnu memoriju OpenCL uredaja ¯ ¯ i sliˇcnih akcija su vrlo sliˇcni ve´c prethodno objašnjenim kodovima, pa ih autor ovdje ne´ce detaljno opisivati. Postoje bitne razlike izmedu ¯ ovog i prethodnog programskog koda. U ovoj implementaciji bitonic sort algoritma pokretanje kernela nad svim radnim stavkama nalazi se unutar dviju while petlji (51. do 64. linija koda). Vanjska petlja se vrti tako dugo dok je varijabla length manja od N , s time da se u svakoj iteracija varijabla length množi s brojem 2 (64. linija koda). Unutarnja petlja se odvija tako dugo dok je varijabla inc ve´ca od 0, s time da se u svakoj iteraciji varijabla inc dijeli s brojem 2 (63. linija koda). Poˇcetna vrijednost varijable inc je jednaka trenutnoj vrijednosti varijable length (52. linija koda). Kod pokretanja kernela prosljeduju se izmedu ¯ ¯ ostalog varijable inc i length. Nakon što se kernel izvrši zamijene se pokazivaˇci na izlazno polje i polje koje se sortira (60. do 62. linija koda). Na taj naˇcin izlazno polje brojeva koje je nastalo kao rezultat izvršavanja kernela postaje ulazno polje nad kojim c´ e se u sljede´coj iteraciji izvršavati novi kernel, a prethodno ulazno polje postaje izlazno polje za sljede´cu iteraciju. Na taj naˇcin nije potrebno ponovno kreiranje novih polja te njihovo kopiranje u globalnu memoriju OpenCL uredaja ¯ pa se time znatno dobiva na performansama. Sav opisan kod u biti predstavlja bitonic sort algoritam prilagoden ¯ za izvršavanje na grafiˇckom procesoru raˇcunala i kod je sliˇcan kodu standardnog bitonic sort algoritma. 79 Standardni bitonic sort algoritam implementiran u programskom jeziku C koji je korišten kod testiranja i usporedba brzina prikazan je u nastavku. 1 const int ASCENDING = 1; 2 const int DESCENDING = 0; 3 4 5 void compare(int i, int j, int dir, float* A) { if (dir==(A[i]>A[j])) 6 { 7 8 float h=A[i]; 9 A[i]=A[j]; A[j]=h; 10 } 11 12 } 13 14 15 void bitonicMerge(int lo, int cnt, int dir, float *A) { if (cnt>1) 16 { 17 18 int k=cnt/2; 19 int i; 20 for (i=lo; i<lo+k; i++) 21 compare(i, i+k, dir, A); 22 bitonicMerge(lo, k, dir, A); 23 bitonicMerge(lo+k, k, dir, A); } 24 25 } 26 27 28 29 30 void bitonicSort(int lo, int cnt, int dir, float *A) { if (cnt>1) { 31 int k=cnt/2; 32 bitonicSort(lo, k, ASCENDING, A); 33 bitonicSort(lo+k, k, DESCENDING, A); 34 bitonicMerge(lo, cnt, dir, A); 35 } 80 36 } 37 38 39 float* bitonic_sort(int N, float *A) { 41 float *B = malloc(4 * N); int i; 42 for (i = 0; i < N; i++) B[i] = A[i]; 43 bitonicSort(0, N, ASCENDING, B); 44 return B; 40 45 } U sljede´cim tablicama prikazani su rezultati dobiveni testiranjem implementiranog algoritma. GPU (bitonic sort) CPU (C - bitonic sort) CPU / GPU N = 1M 0.0727 s 0.693 s 9.53 N = 4M 0.2109 s 3.2363 s 15.34 N = 8M 0.4268 s 6.9739 s 16.34 N = 12M 0.60 s 9.9207 s 16.53 Tablica 6.26: Usporedba brzina sortiranja - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 N = 1M N = 4M N = 8M N = 12M GPU (bitonic sort) CPU (numpy.sort) CPU / GPU 0.0727 s 0.0876 s 1.21 0.2109 s 0.3874 s 1.84 0.4268 s 0.8143 s 1.91 0.60 s 1.2443 s 2.07 Tablica 6.27: Usporedba brzina sortiranja bitonic sort algoritmom implementiranog u OpenCL-u sa sortiranjem pomo´cu numpy.sort rutine - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 Prema tablici 6.27 i slici 6.8 vidljivo je da bitonic sort algoritam implementiran u OpenCL-u i izvršen na grafiˇckom procesoru Radeon HD 6850 ima oko 2 puta ve´cu brzinu od sortiranja na centralnom procesoru Phenom X4 955 BE 3.2 GHz ukoliko se kod sortiranja na CPU koristi numpy.sort rutina u Python programskom jeziku. Ubrzanje u usporedbi s bitonic sort algoritmom implementiranom u programskom jeziku C iznosi oko 15 puta (tablica 6.26), što je sasvim zadovoljavaju´ce. Možemo zakljuˇciti da je bitonic sort algoritam prikladan za implementaciju u OpenCL-u. Ako se usporeduju brzine sortiranja izmedu ¯ ¯ centralnog procesora i grafiˇckog procesora s manjim brojem dostupnih stream procesora (tablice 6.28 i 6.29) tada je korištenje OpenCL-a manje isplativo. U testiranjima su se postigla ubrzanja do oko 4 puta kada se OpenCL algoritam usporedivao ¯ s bitonic sort algoritmom implementiranim u programskom jeziku C (tablica 6.28), dok je implementirani OpenCL algoritam nešto sporiji od korištenja numpy.sort rutine (tablica 6.29). 81 Slika 6.8: Usporedba brzina sortiranja na CPU (numpy.sort rutina) i GPU - Phenom X4 955 BE 3.2 GHz i Radeon HD 6850 GPU (bitonic sort) CPU (C - bitonic sort) CPU / GPU N = 1M 0.6343 s 2.3482 s 3.7 N = 4M 2.3704 s 10.8494 s 4.58 Tablica 6.28: Usporedba brzina sortiranja - E-350 APU 1.6 GHz s HD6310 GPU (bitonic sort) CPU (numpy.sort) CPU / GPU N = 1M 0.6343 s 0.2468 s 0.39 N = 4M 2.3704 s 1.0947 s 0.46 Tablica 6.29: Usporedba brzina sortiranja bitonic sort algoritmom implementiranog u OpenCL-u sa sortiranjem pomo´cu numpy.sort rutine - E-350 APU 1.6 GHz s HD6310 Vidljiva je vrlo velika razlika u performansama u odnosu na selection sort implementiran u OpenCLu, a glavni razlog za to je cˇ injenica da selection sort algoritam ima složenost O(n2 ), dok bitonic sort algoritam ima složenost O(n log n). Ovaj algoritam sortiranja oˇcekivano daje puno bolje rezultate od prethodno implementiranog selection sort algoritma. Algoritam je i brži od sortiranja pomo´cu centralnog procesora raˇcunala, barem kad se sortiraju ve´ca polja brojeva i kada se koristi grafiˇcki procesor s dovoljnim brojem dostupnih stream procesora. 82 ˇ 7. Zakljucak Jednostavne implementacije algoritama za GPU u OpenCL-u su obiˇcno znatno brže od jednostavnih implementacija za CPU u C-u. To se u ovom radu pokazalo kod usporedbe jednostavnih implementacija množenja matrica, selection sort i bitonic sort algoritma u OpenCL-u s jednostavnim implementacijama tih algoritama u C-u. Medutim, za CPU su dostupne izuzetno optimirane ¯ rutine (na primjer numpy.sort i numpy.dot) koje tu razliku praktiˇcki anuliraju. Tek optimiranjem OpenCL implementacije korištenjem lokalne memorije GPU-a, smanjenjem broja radnih stavki i sliˇcno (što je u ovom diplomskom radu prikazano na primjerima množenja matrica i sortiranja), dakle uz znatan napor na implementaciji mogu´ce je posti´ci i znatno ubrzanje na GPU u usporedbi s optimiranim implementacijama na CPU. Prema svemu dosad objašnjenom i prikazanom u ovom diplomskom radu može se zakljuˇciti da GPGPU ima smisla koristiti za rješavanje odredenih vrsta problema, dok za odredene vrste pro¯ ¯ blema GPGPU jednostavno nije prikladan. Prema provedenim testiranjima u poglavlju 6. može se zakljuˇciti da se GPGPU isplati koristiti za izraˇcun Mandelbrotovog fraktala jer algoritam koji se koristi u tu svrhu ima vrlo mali broj ulaza i svaka toˇcka koju treba izraˇcunati potpuno je nezavisna o drugim toˇckama. GPGPU se takoder ¯ isplati koristiti za množenje matrica, dok je kod sortiranja isplativost korištenja GPGPU-a nešto niža. Ve´cina aplikacija ima dijelova koji se mogu puno brže izvršiti na centralnom procesoru raˇcunala. Zbog toga je kod izrade aplikacija bitno koristiti centralni procesor za izvršavanje takvih dijelova programa, a grafiˇcki procesor je potrebno koristiti za rješavanje problema za cˇ ije rješavanje je GPU pogodniji tako da se GPU i CPU medusobno nadopunjuju i time se dobivaju najbolje mogu´ce ¯ performanse. PyOpenCL modul za Python omogu´cava jednostavno pisanje OpenCL programa uz podršku za sve funkcionalnosti OpenCL-a, što znaˇci da se Python programski jezik može iskoristiti za brzo i jednostavno pisanje programa raznih namjena koji c´ e u procesorski zahtjevnim dijelovima koristiti OpenCL. Potrebno je napomenuti da je autor sve implementirane algoritme testirao na više operacijskih sustava (Arch Linux, Ubuntu 13.04 i Windows 7) i zakljuˇcio da nema znaˇcajnih razlika u brzinama izvodenja s obzirom na operacijski sustav ukoliko se koriste najnovije verzije drivera. ¯ 83 8. Literatura 1. Batcher, K. E. (1968). Sorting networks and their applications. In Proceedings of the April 30–May 2, 1968, spring joint computer conference (pp. 307–314). Dostupno na https://www.cs.duke.edu/courses/fall08/cps196.1/Literature/ bitonic_sort.pdf 2. Gaster, B., Howes, L., Kaeli, D. R., Mistry, P., & Schaa, D. (2011). Heterogeneous computing with OpenCL (1st ed.). Morgan Kaufmann. 3. Gaster, B. R., & Howes, L. (2013). The future of the APU braided parallelism. Dostupno na http://developer.amd.com/wordpress/media/2013/06/2901_final .pdf 4. Kirk, D. B., & mei W. Hwu, W. (2010). Programming massively parallel processors: A hands-on approach (applications of GPU computing series) (1st ed.). Morgan Kaufmann. 5. Klckner, A. (2009, 9). GPU metaprogramming using PyCUDA: Methods & applications. Dostupno na http://mathema.tician.de/news.tiker.net/files/pycuda-nvidia .pdf 6. Klockner, A. (2010, 9). Pycuda: Even simpler GPU programming with Python. Dostupno na http://mathema.tician.de/news.tiker.net/files/main.pdf 7. Knuth, D. E. (1998). Art of computer programming, volume 3: Sorting and searching (2nd edition). Addison-Wesley Professional. 8. Merrill, D., & Grimshaw, A. (2011). High performance and scalable radix sorting: A case study of implementing dynamic parallelism for GPU computing. Parallel Processing Letters, 21(02), 245–272. Dostupno na http://back40computing.googlecode.com/svn-history/r665/wiki/ documents/PplGpuSortingPreprint.pdf 9. Munshi, A. (2012, 9). The OpenCL specification. Dostupno na http://www.khronos.org/registry/cl/specs/opencl-1.2.pdf 10. Munshi, A., Gaster, B., Mattson, T. G., Fung, J., & Ginsburg, D. (2011). OpenCL programming guide (1st ed.). Addison-Wesley Professional. 11. Scarpino, M. (2011). OpenCL in action: How to accelerate graphics and computations. Manning Publications. 84
© Copyright 2024 Paperzz