UNIVERZA V MARIBORU FAKULTETA ZA ELEKTROTEHNIKO, RAČUNALNIŠTVO IN INFORMATIKO. Filip Urh DINAMIČNI PARALELIZEM NA GPE.

Similar documents
Reševanje problemov in algoritmi

R V P 2 Predavanje 05

Izvedba algoritmov računske geometrije. na arhitekturi CUDA

TOPLJENEC ASOCIIRA LE V VODNI FAZI

ENAČBA STANJA VODE IN VODNE PARE

Vzporedni algoritmi za urejanje podatkov

Iskanje najcenejše poti v grafih preko polkolobarjev

OA07 ANNEX 4: SCOPE OF ACCREDITATION IN CALIBRATION

Minimizacija učne množice pri učenju odločitvenih dreves

Strojno učenje v porazdeljenem okolju z uporabo paradigme MapReduce

Zgoščevanje podatkov

Domen Perc. Implementacija in eksperimentalna analiza tehnike razvrščanja podatkov s konsenzom

Optimizacija delovanja in povečanje obiska na spletni strani

UNIVERZA NA PRIMORSKEM FAKULTETA ZA MATEMATIKO, NARAVOSLOVJE IN INFORMACIJSKE TEHNOLOGIJE

UNIVERZA NA PRIMORSKEM FAKULTETA ZA MATEMATIKO, NARAVOSLOVJE IN INFORMACIJSKE TEHNOLOGIJE

Drevesno preiskovanje Monte Carlo v porazdeljenem okolju

UNIVERZA NA PRIMORSKEM FAKULTETA ZA MATEMATIKO, NARAVOSLOVJE IN INFORMACIJSKE TEHNOLOGIJE

Topološka obdelava slik

Izvedbe hitrega urejanja za CPE in GPE

OSNOVE UMETNE INTELIGENCE

Sistem za sledenje in analizo uporabe računalniških aplikacij

Linearna regresija. Poglavje 4

AKSIOMATSKA KONSTRUKCIJA NARAVNIH

Cveto Trampuž PRIMERJAVA ANALIZE VEČRAZSEŽNIH TABEL Z RAZLIČNIMI MODELI REGRESIJSKE ANALIZE DIHOTOMNIH SPREMENLJIVK

Multipla korelacija in regresija. Multipla regresija, multipla korelacija, statistično zaključevanje o multiplem R

Vsebina Od problema do načrta programa 1. del

OPTIMIRANJE IZDELOVALNIH PROCESOV

Paralelni in distribuirani algoritmi v numerični analizi

UNIVERZA NA PRIMORSKEM FAKULTETA ZA MATEMATIKO, NARAVOSLOVJE IN INFORMACIJSKE TEHNOLOGIJE. Verjetnostni algoritmi za testiranje praštevilskosti

NIKJER-NIČELNI PRETOKI

Hipohamiltonovi grafi

ENERGY AND MASS SPECTROSCOPY OF IONS AND NEUTRALS IN COLD PLASMA

Simulacija dinamičnih sistemov s pomočjo osnovnih funkcij orodij MATLAB in Simulink

Miha Troha. Robotsko učenje in planiranje potiskanja predmetov

UNIVERZA V LJUBLJANI PEDAGOŠKA FAKULTETA POLONA ŠENKINC REŠEVANJE LINEARNIH DIFERENCIALNIH ENAČB DRUGEGA REDA S POMOČJO POTENČNIH VRST DIPLOMSKO DELO

Verifikacija napovedi padavin

SVM = Support Vector Machine = Metoda podpornih vektorjev

UNIVERZA V MARIBORU FAKULTETA ZA NARAVOSLOVJE IN MATEMATIKO. Oddelek za matematiko in računalništvo DIPLOMSKO DELO.

Ekstrakcija časovnega znanja iz dogodkov v spletnih novicah

POLDIREKTNI PRODUKT GRUP

Pohitritev izvajanja evolucijskih algoritmov z večprocesorskimi in multiračunalniškimi sistemi

Hibridizacija požrešnih algoritmov in hitrega urejanja

OPTIMIZACIJA Z ROJEM DELCEV

Univerza v Ljubljani Fakulteta za matematiko in fiziko. Oddelek za fiziko. Seminar - 3. letnik, I. stopnja. Kvantni računalniki. Avtor: Tomaž Čegovnik

Razvoj spletnega slovarja slovenskega znakovnega jezika

Problem umetnostne galerije

Jamova cesta Ljubljana, Slovenija Jamova cesta 2 SI 1000 Ljubljana, Slovenia

2 Zaznavanje registrske tablice

OPP Programska oprema

Implementacija modula r.cuda.los v odprtokodnem paketu GRASS GIS z vzporednim računanjem na grafičnih karticah NVIDIA CUDA

Grafični gradnik za merjenje kvalitete klasifikatorja s pomočjo krivulj

Univerza na Primorskem. Fakulteta za matematiko, naravoslovje in informacijske tehnologije. Zaznavanje gibov. Zaključna naloga

UNIVERZA V LJUBLJANI FAKULTETA ZA RAČUNALNIŠTVO IN INFORMATIKO. Gorazd Kovačič. Avtomatsko vizualno testiranje spletnih strani

MECHANICAL EFFICIENCY, WORK AND HEAT OUTPUT IN RUNNING UPHILL OR DOWNHILL

PRIMERJAVA ANALITIČNIH PROGRAMSKIH ORODIJ PRI REŠEVANJU PROBLEMOV ODLOČANJA V POSLOVNIH PROCESIH

Uporaba preglednic za obdelavo podatkov

VAJE 2: Opisna statistika

Preverjanje optimiziranosti spletnih strani

APLIKACIJA ZA DELO Z GRAFI

Gradnja Vietoris-Ripsovega simplicialnega kompleksa

Miha Sedej. Analiza lastnosti pločevine z metodami podatkovnega rudarjenja

Razpoznavanje znakov prstne abecede na osnovi računalniškega vida

Stiskanje slik z algoritmi po vzorih iz narave

NALOGE ZA PRVO SKUPINO. Kaj izpiše naslednji program? R: 9 Odgovor primerno utemelji!

Dejan Petelin. Sprotno učenje modelov na podlagi Gaussovih procesov

USING THE DIRECTION OF THE SHOULDER S ROTATION ANGLE AS AN ABSCISSA AXIS IN COMPARATIVE SHOT PUT ANALYSIS. Matej Supej* Milan Čoh

UČNI NAČRT PREDMETA / COURSE SYLLABUS (leto / year 2017/18) Študijska smer Study field ECTS

SIMETRIČNE KOMPONENTE

JEDRSKA URA JAN JURKOVIČ. Fakulteta za matematiko in fiziko Univerza v Ljubljani

Open Data Structures (za programski jezik Java) v slovenščini. Izdaja 0.1F. Pat Morin

arxiv: v1 [cs.dm] 21 Dec 2016

Kode za popravljanje napak

Projektovanje paralelnih algoritama II

USING SIMULATED SPECTRA TO TEST THE EFFICIENCY OF SPECTRAL PROCESSING SOFTWARE IN REDUCING THE NOISE IN AUGER ELECTRON SPECTRA

Ana Mlinar Fulereni. Delo diplomskega seminarja. Mentor: izred. prof. dr. Riste Škrekovski

Iterativne metode podprostorov 2010/2011 Domače naloge

UPORABA STROJNEGA UČENJA PRI ANALIZI VREDNOSTNIH PAPIRJEV

UČNI NAČRT PREDMETA / COURSE SYLLABUS Numerical linear algebra. Študijska smer Study field. Samost. delo Individ. work Klinične vaje work

MICROWAVE PLASMAS AT ATMOSPHERIC PRESSURE: NEW THEORETICAL DEVELOPMENTS AND APPLICATIONS IN SURFACE SCIENCE

Pozicioniranje v zaprtih prostorih z uporabo NFC tehnologije

ODKRIVANJE TEMATIK V ZAPOREDJU BESEDIL IN SLEDENJE NJIHOVIM SPREMEMBAM

Analogna elektronska vezja. Uvodna vaja

OFF-LINE NALOGA NAJKRAJŠI SKUPNI NADNIZ

Luka Taras Korošec ANALIZA IN NADGRADNJA APLIKACIJE ZA DELO Z GRAFI

Odročno nalaganje razredov za knjižnico Akka

Osnove numerične matematike

UNIVERZA NA PRIMORSKEM FAKULTETA ZA MATEMATIKO, NARAVOSLOVJE IN INFORMACIJSKE TEHNOLOGIJE

TEORIJA GRAFOV IN LOGISTIKA

UNIVERZA V LJUBLJANI PEDAGOŠKA FAKULTETA SAŠO ZUPANEC MAX-PLUS ALGEBRA DIPLOMSKO DELO

Intervalske Bézierove krivulje in ploskve

LISREL. Mels, G. (2006). LISREL for Windows: Getting Started Guide. Lincolnwood, IL: Scientific Software International, Inc.

22. državno tekmovanje v znanju računalništva (1998) NALOGE ZA PRVO SKUPINO. Kaj izpiše naslednji program? Rešitev: str. 8

UNIVERZA V MARIBORU FAKULTETA ZA NARAVOSLOVJE IN MATEMATIKO. Oddelek za matematiko in računalništvo MAGISTRSKA NALOGA. Tina Lešnik

Attempt to prepare seasonal weather outlook for Slovenia

UNIVERZA NA PRIMORSKEM FAKULTETA ZA MATEMATIKO, NARAVOSLOVJE IN INFORMACIJSKE TEHNOLOGIJE

CS-206 Concurrency. Lecture 13. Wrap Up. Spring 2015 Prof. Babak Falsafi parsa.epfl.ch/courses/cs206/

UNIVERZA NA PRIMORSKEM FAKULTETA ZA MATEMATIKO, NARAVOSLOVJE IN INFORMACIJSKE TEHNOLOGIJE. O neeksaknotsti eksaktnega binomskega intervala zaupanja

FRAKTALNA DIMENZIJA. Fakulteta za matematiko in fiziko Univerza v Ljubljani

Kristijan Boček. Aritmetična knjižnica vgrajenega sistema za vodenje zaščitnega releja

UNIVERZA NA PRIMORSKEM FAKULTETA ZA MATEMATIKO, NARAVOSLOVJE IN INFORMACIJSKE TEHNOLOGIJE. Naknadna stabilizacija videoposnetkov

Transcription:

UNIVERZA V MARIBORU FAKULTETA ZA ELEKTROTEHNIKO, RAČUNALNIŠTVO IN INFORMATIKO Filip Urh DINAMIČNI PARALELIZEM NA GPE Diplomsko delo Maribor, september 2015

DINAMIČNI PARALELIZEM NA GPE Diplomsko delo Študent: Študijski program: Mentor: Lektor: Filip Urh univerzitetni študijski program Računalništvo in informacijske tehnologije izr. prof. dr. Damjan Strnad Simona Šoštar, prof. slov.

Zahvala Zahvaljujem se svojemu mentorju, izr. prof. dr. Damjanu Strnadu, za vso strokovno pomoč in napotke med izdelavo tega diplomskega dela. Iskrena zahvala tudi družini, ki mi je omogočila ta študij. Zahvala gre tudi Ani za vse vzpodbudne besede tekom študija in pisanjem tega diplomskega dela. iii

Ključne besede: grafična procesna enota, odločitveno drevo, CART, CUDA, dinamični paralelizem UDK: 004.274.032.24(043.2) Povzetek V diplomskem delu preučimo in predstavimo novo funkcionalnost arhitekture CUDA. Gre za dinamični paralelizem, ki omogoča poganjanje programskih jeder neposredno iz grafične procesne enote. V začetku podrobno predstavimo arhitekturo CUDA in algoritem CART za gradnjo odločitvenih dreves, ki smo ga uporabili za demonstracijo uporabe dinamičnega paralelizma. Algoritem smo implementirali v zaporedni različici na CPE ter v paralelnih različicah z in brez dinamičnega paralelizma na GPE. Predstavili smo primerjalne meritve časov izvajanja vseh treh implementacij in ugotovili, da uporaba dinamičnega paralelizma omogoča krajši čas izvajanja in lažjo implementacijo algoritma. iv

Dynamic parallelism on a GPU Key words: graphics processing unit, decision tree, CART, CUDA, dynamic parallelism UDK: 004.274.032.24(043.2) Abstract In the thesis, we investigate and introduce new functionality of CUDA architecture - the dynamic parallelism, which allows us to start new kernels directly from the GPU. In the beginning, we present in detail the CUDA architecture and algorithm CART for the construction of decision trees, which was used to demonstrate the use of dynamic parallelism. The serial algorithm was implemented on the CPU and the parallel versions with and without dynamic parallelism were implemented on the GPU. We presented a comparison of execution times for all three implementations and concluded that the usage of dynamic parallelism provides faster execution and easier implementation of the algorithm. v

KAZALO VSEBINE 1 UVOD 1 2 CUDA 3 2.1 Arhitektura in razvoj GPE........................... 3 2.2 Programski model CUDA........................... 6 2.3 Organizacija niti v CUDA........................... 10 2.4 Pomnilniška hierarhija............................ 11 2.5 Dinamični paralelizem............................ 13 3 ODLOČITVENA DREVESA IN ALGORITEM CART 17 3.1 Odločitvena drevesa.............................. 17 3.2 Algoritem CART............................... 18 4 IMPLEMENTACIJA 21 4.1 Grafični vmesnik............................... 21 4.2 Implementacija na CPE............................ 22 4.3 Implementacija na GPE brez dinamičnega paralelizma............ 23 4.4 Implementacija na GPE z dinamičnim paralelizmom............. 25 5 REZULTATI IN DISKUSIJA 27 5.1 Rezultati meritev............................... 27 5.2 Analiza rezultatov............................... 34 6 SKLEP 36 LITERATURA 38 vi

KAZALO SLIK 2.1 Primerjava arhitekture CPE in GPE...................... 4 2.2 Zgradba sodobne GPE s podporo CUDA................... 5 2.3 Izvajanje programa pri različnem številu pretočnih multiprocesorjev..... 6 2.4 Prevajanje programa CUDA.......................... 7 2.5 Paralelno seštevanje dveh vektorjev...................... 8 2.6 Organizacija niti v arhitekturi CUDA..................... 10 2.7 Pomnilniška hierarhija CUDA......................... 12 2.8 Statična konfiguracija jedra s premajhno gostoto mreže (levo) in s preveliko gostoto mreže (sredina) ter z dinamično konfiguracijo gostote mreže (desno). 14 2.9 Izvajanje programa brez dinamičnega paralelizma (levo) in z njim (desno).. 15 3.1 Učna množica za problem klasifikacije barve točk na podlagi njihovih koordinat (levo) in odločitveno drevo, ki izvaja takšno klasifikacijo (desno).... 18 3.2 Binarno poddrevo................................ 20 4.1 Glavno okno aplikacije............................. 22 4.2 Implementacija brez (levo) in z (desno) dinamičnim paralelizmom...... 25 5.1 Čas gradnje drevesa v odvisnosti od števila vzorcev za učno množico EEG Eye State.................................... 29 5.2 Čas gradnje drevesa v odvisnosti od števila vzorcev za učno množico MA- GIC Gamma Telescope............................. 31 5.3 Čas gradnje drevesa v odvisnosti od števila vzorcev za učno množico Quantum Physics................................... 32 vii

KAZALO TABEL 2.1 Rezervirane besede za deklaracijo funkcij................... 7 2.2 Lastnosti vseh tipov pomnilnika na napravi CUDA.............. 13 5.1 Uporabljena strojna in programska oprema.................. 28 5.2 Lastnosti učnih množic za testiranje...................... 28 5.3 Časi gradnje odločitvenega drevesa za učno množico EEG Eye State..... 29 5.4 Časi gradnje odločitvenega drevesa za učno množico MAGIC Gamma Telescope...................................... 30 5.5 Časi gradnje odločitvenega drevesa za učno množico Quantum Physics... 31 5.6 Poraba pomnilnika v MB pri gradnji odločitvenega drevesa za učno množico EEG Eye State................................. 33 5.7 Poraba pomnilnika v MB pri gradnji odločitvenega drevesa za učno množico MAGIC Gamma Telescope........................... 33 5.8 Poraba pomnilnika v MB pri gradnji odločitvenega drevesa za učno množico Quantum Physics................................ 33 viii

SEZNAM UPORABLJENIH KRATIC.NET - ogrodje za izdelavo aplikacij ALE - aritmetična logična enota CART - klasifikacijska in regresijska drevesa (Classification And Regression Trees) CPE - centralna procesna enota CUDA - programski model, ki omogoča izvajanje paralelnih programov na grafični procesni enoti (Compute Unified Device Architecture) DLL - dinamično povezljiva knjižnica (Dynamic-link Library) DMA - enota za upravljanje s pomnilnikom (Direct Memory Access) DRAM - dinamični pomnilnik z naključnim dostopom (Dynamic Random-access Memory) FLOPS - število operacij s plavajočo vejico na sekundo (Floating-point Operations Per Second) GDDR - vrsta grafičnega pomnilnika (Graphics Double Data Rate) GPE - grafična procesna enota GPGPU - splošnonamensko računanje na grafičnih procesnih enotah (General-purpose Computing on Graphics Processing Units) SM - pretočni multiprocesor (Streaming Multiprocessor) SP - pretočni procesor (Streaming Processor) WPF - del ogrodja.net za gradnjo grafičnih vmesnikov (Windows Presentation Foundation) ix

Poglavje 1 UVOD V diplomskem delu smo preučili uporabo dinamičnega paralelizma (dynamic parallelism), ki je dokaj nova funkcionalnost sodobnih grafičnih procesnih enot (GPE). Dinamični paralelizem omogoča zaganjanje novih jeder (kernel) na GPE iz kode, ki se že izvaja na GPE. To razvijalcem odpira nove možnosti pri implementaciji paralelnih algoritmov za GPE. V današnjem času količina informacij hitro narašča, zato je pohitritev izvajanja algoritmov na GPE vedno bolj priljubljena in včasih celo nujna za uporabo v realnem času. Cilj tega diplomskega dela je bil podrobno spoznati, kaj je dinamični paralelizem, kako deluje in za kaj se uporablja. Za lažje razumevanje in preverjanje učinkovitosti dinamičnega paralelizma smo implementirali algoritem CART (Classification And Regression Trees), ki je namenjen gradnji odločitvenih dreves. Sestavljen je iz več korakov, ki jih je možno paralelizirati, in je kot tak primeren za implementacijo na GPE. Ker smo želeli konkretno primerjavo hitrosti dinamičnega paralelizma proti drugim implementacijam, smo algoritem CART implementirali v treh različicah. Prva se je izvajala na CPE, druga na CPE in GPE, medtem ko je tretja uporabljala dinamični paralelizem in se je zato izvajala v celoti na GPE. Dinamični paralelizem podpira arhitektura CUDA (Compute Unified Device Architecture), ki jo je razvilo podjetje NVIDIA. Za razvoj algoritmov na tej arhitekturi smo zato omejeni na uporabo GPE omenjenega podjetja. Dinamični paralelizem sicer pozna pod imenom vgnezdeni paralelizem (nested parallelism) tudi različica 2.0 odprtega standarda OpenCL, ki ni omejen na GPE enega proizvajalca. Na žalost zadnje različice OpenCL še ne podpira prav veliko grafičnih procesorjev [4] [11]. Končni izdelek tega diplomskega dela je aplikacija, ki omogoča gradnjo maksimalnega odločitvenega drevesa. Pri tem lahko izberemo, katero izmed omenjenih treh implementacij želimo uporabiti pri gradnji drevesa. Diplomsko delo je sestavljeno iz več tematskih sklopov. Za uvodom najprej predstavimo zgradbo grafične procesne enote in arhitekturo CUDA. Spoznamo se s programskim modelom CUDA in organizacijo niti v tem modelu, opišemo oblike pomnilniškega prostora, ki jih pozna programski model CUDA, in zaključimo s podrobno predstavitvijo dinamičnega paralelizma. 1

V tretjem poglavju spoznamo algoritem CART za gradnjo odločitvenih dreves, ki ga podrobno opišemo. Sledi poglavje z opisom treh vrst implementacij, v katerem pojasnimo, iz katerih sklopov je sestavljena naša aplikacija in kako deluje. V petem poglavju predstavimo rezultate testiranj, s katerimi smo izvajali merjenje časa gradnje odločitvenega drevesa pri posamezni implementaciji. V ta namen smo uporabili tri različne množice podatkov. V tem poglavju predstavimo tudi izsledke analize rezultatov. Diplomsko delo zaključimo s povzetkom opravljenega dela in pridobljenih spoznanj, podamo tudi nekaj idej za nadaljnje delo, predvsem v smeri pohitritve in nadgradnje posameznih implementacij. 2

Poglavje 2 CUDA V tem poglavju bomo predstavili in opisali zgradbo sodobne grafične procesne enote in arhitekturo CUDA. Dobro poznavanje arhitekture je bistvenega pomena pri načrtovanju algoritmov. Le tako lahko napišemo časovno in prostorsko učinkovit program, ki bo znal kar najbolje uporabiti prednosti določene arhitekture. 2.1 Arhitektura in razvoj GPE GPE so bile razvite z namenom obdelave slikovnih informacij za prikaz na grafičnem zaslonu. Obdelovanje teh informacij se mora izvajati kar najhitreje, če želimo doseči izris slike v realnem času. Da dosežemo potrebno hitrost, so bile GPE zasnovane tako, da lahko obdelujejo podatke paralelno. Slika je sestavljena iz množice pikslov, katerih barve se izračunajo neodvisno ena od druge, zato lahko GPE obdeluje več pikslov hkrati. Prva GPE je bila GeForce 256, ki jo je izdelalo podjetje NVIDIA in je prišla v prodajo leta 1999. Njena zmogljivost je bila za takratne čase izjemna, saj je lahko osvežila 480 milijonov pikslov na sekundo. Naslavljala je do 128 MB globalnega pomnilnika [13]. Za splošnonamensko računanje se je paralelna obdelava podatkov najprej začela na CPE. V začetku so zmogljivost CPE povečevali z višanjem frekvence delovne ure. Ta trend se je upočasnil leta 2003, saj je visoka frekvenca s sabo prinesla vse višjo porabo energije. S tem se je večala tudi količina toplote, ki jo CPE proizvaja, in je ni bilo več možno učinkovito odvajati [8]. Istega leta so se proizvajalci procesorskih enot odločili za dve različni smeri, v kateri bo šla proizvodnja. Prva smer se osredotoča na procesorske enote z več jedri, ki so še vedno namenjene predvsem zaporedni obdelavi podatkov. Sprva so tako na trg poslali dvojedrne CPE, danes srečamo osem in več jedrne CPE, ki so namenjene uporabi v strežnikih [6]. V nasprotju s prvo se druga smer osredotoča na vzporedno obdelavo podatkov v več nitih, ki izvajajo iste ukaze na množici podatkov. Vsaka nit obdela le določen del podatkov, ki jih na koncu združimo v rezultat kot celoto. Lahko bi povzeli, da se prva smer danes ukvarja s proizvodnjo CPE in druga smer s proizvodjo GPE. Skupno obema smerema je povečanje zmogljivosti. 3

Skozi čas je med obema smerema nastala velika razlika v zmogljivosti. Če zmogljivost izrazimo z metriko FLOPS (število operacij s plavajočo vejico na sekundo), današnje zmogljivejše CPE dosegajo 0,5 TFLOPS (tera FLOPS), medtem ko lahko GPE dosežejo več kot 5 TFLOPS. Iz tega sledi, da so lahko GPE več kot 10-krat hitrejše od CPE pri obdelavi istih podatkov [17]. Vendar tega ne moremo posplošiti za vse vrste algoritmov. Nekateri algoritmi namreč ne dopuščajo visoke stopnje paralelizacije, kar za izvajanje na GPE ni ugodno in lahko celo upočasni delovanje algoritma. Slika 2.1 prikazuje primerjavo arhitekture sodobne CPE in GPE. Razmerja med deleži posameznih podenot v obeh procesorskih enotah se bistveno razlikujejo. Zgradba CPE je optimizirana za zaporedno obdelavo podatkov, zaradi česar ima veliko krmilne logike. Poleg tega ima CPE veliko predpomnilnika, ki skrbi za hitrejši dostop do podatkov, ki so potrebni za izvajanje programa. Na drugi strani razvoj GPE sledi potrebam po visoki računski zmogljivosti v realnem času pri izdelavi video iger. To so proizvajalci dosegli z velikim številom aritmetičnih logičnih enot v enem fizičnem čipu. GPE vsebuje še manjši predpomnilnik, ki omejuje potrebo po dostopanju do globalnega pomnilnika, katerega odzivnost je veliko počasnejša. Takšna arhitektura omogoča, da se posamezna nit izvaja počasneje kot v CPE, ker pa GPE izvaja po več sto niti naenkrat, je končna zmogljivost tako večja [8]. Razlika med obema arhitekturama je tudi v hitrosti prenosa podatkov v glavni pomnilnik in iz njega. CPE dosega hitrosti prenosa do približno 100 GB/s, medtem ko hitrosti pri GPE presegajo 330 GB/s [6] [14]. Slika 2.1: Primerjava arhitekture CPE in GPE. Do leta 2006 je bila uporaba GPE za splošnonamensko računanje zahtevna. Uporabnik je lahko do GPE dostopal le preko grafičnih knjižic, kot sta Direct3D ali OpenGL. Tako je 4

moral biti program, ki smo ga želeli izvesti, izražen kot funkcija za obdelavo piksla v sliki. Omenjena tehnika se imenuje splošnonamensko računanje na grafičnih procesnih enotah (general-purpose computing on graphics processing units - GPGPU). To se je spremenilo leta 2007, ko je podjetje NVIDIA predstavilo novo arhitekturo, imenovano CUDA [16]. S tem so omogočili veliko lažje programiranje, pri katerem uporabljamo GPE za razvoj paralelnih algoritmov. Če so hoteli to doseči, ni bila dovolj samo sprememba programskega dela, ampak se je spremenila tudi zgradba GPE. Tako ima prvi čip z novo arhitekturo, ki ima kodno ime G80, kot tudi vsi njegovi nasledniki, vgrajen vmesnik, ki skrbi za izvajanje splošnonamenskih programov. Arhitekturo moderne GPE, ki podpira izvajanje programov CUDA, lahko vidimo na sliki 2.2. Sestavljena je iz polja pretočnih multiprocesorjev (streaming multiprocessor - SM), izmed katerih lahko vsak izvaja večje število niti. Vsak multiprocesor je sestavljen iz večjega števila pretočnih procesorjev (streaming processor - SP), ki si med seboj delijo krmilno logiko in predpomnilnik ukazov. Vsak čip GPE je prav tako opremljen z globalnim pomnilnikom tipa GDDR DRAM. Pri grafičnih aplikacijah je ta pomnilnik namenjen shranjevanju slik in tekstur, kadar pa uporabljamo GPE za izvajanje programov CUDA, služi ta pomnilnik za shranjevanje podatkov. Za prenos podatkov med glavnim sistemskim pomnilnikom in globalnim pomnilnikom GPE skrbi enota DMA, kar razbremeni CPE [5]. Slika 2.2: Zgradba sodobne GPE s podporo CUDA. Dobra lastnost današnjih GPE je tudi njihova fleksibilnost. Večnitni program, ki se izvaja na 5

GPE, se razdeli v bloke niti. Izvajanje posameznega bloka se dodeli enemu samemu multiprocesorju. Ker pa imajo GPE različno število multiprocesorjev, se lahko multiprocesorjem dodeli različno število blokov. GPE s pomočjo prepletanja obdeluje večje število blokov, vendar se jih v vsakem trenutku obdeluje največ toliko, kolikor je multiprocesorjev. Lepo ponazoritev in prikaz izvajanja istega programa na GPE z različnim številom multiprocesorjev vidimo na sliki 2.3. Slika 2.3: Izvajanje programa pri različnem številu pretočnih multiprocesorjev. 2.2 Programski model CUDA Vsak program CUDA je v grobem sestavljen iz najmanj dveh delov. En del se izvaja na CPE, drugi na eni ali več GPE. V terminologiji CUDA se CPE imenuje gostitelj (host), GPE pa naprava (device). Za deklaracijo funkcij, za katere želimo, da se bodo izvajale na napravi, moramo uporabiti ustrezno rezervirano besedo. Prav tako je deklariranje spremenljivk, ki bodo dostopne na napravi, urejeno s posebnimi rezerviranimi besedami. Ker imamo v programu poleg običajnih rezerviranih besed tudi takšne, ki so bile dodane s strani CUDA, se 6

spremeni način prevajanja takega programa. Prevajalnik za tak program je del programskega paketa za CUDA in se imenuje NVCC [15]. Lastnost tega prevajalnika je, da program razdeli v dva dela. Funkcije, ki so namenjene izvajanju na gostitelju, se prevedejo s prevajalnikom za C/C++, medtem ko za prevajanje funkcij, ki se izvajajo na napravi, poskrbi NVCC. Grafično ponazoritev prevajanja takšnega programa vidimo na sliki 2.4. Slika 2.4: Prevajanje programa CUDA. Rezervirane besede, s katerimi programu CUDA ob deklaraciji funkcije povemo, kje se bo ta izvajala in kdo jo lahko kliče, so device, global in host. Tako velja, da se funkcija, ki je deklarirana z rezervirano besedo device izvaja na napravi in samo naprava lahko kliče takšno funkcijo. Rezervirana beseda global označuje jedro, tj. funkcijo, ki se izvaja na napravi in jo lahko ponavadi kliče samo gostitelj. Rezervirana beseda host napoveduje funkcijo, ki se izvaja na gostitelju in samo ta jo lahko tudi kliče [11]. Ker host v bistvu označuje običajno funkcijo, ki se kliče in izvaja na CPE, lahko oznako tudi izpustimo. Lastnosti vseh treh rezerviranih besed za deklariranje funkcij povzemamo v tabeli 2.1. Tabela 2.1: Rezervirane besede za deklaracijo funkcij. Primer deklaracije Funkcija se izvaja na: Funkcija se kliče z: device float DeviceFunc() napravi naprave global void KernelFunc() napravi gostitelja ali naprave host float HostFunc() gostitelju gostitelja 7

Izvajanje vsakega programa CUDA se začne na gostitelju. Ko izvajanje pride do klica funkcije na napravi, se izvede zagon ustreznega števila niti, ki paralelno obdelujejo podatke. Takšni funkciji, ki se izvaja na napravi, rečemo jedro (kernel). Ko se vse niti trenutnega jedra zaključijo, se program nadaljuje na gostitelju, dokler ni na vrsti za izvajanje novo jedro. Če želimo neko funkcijo izvesti na napravi, moramo najprej alocirati ustrezno velik del pomnilnika naprave s klicem funkcije cudamalloc. Nato s klicem funkcije cudamemcpy, ki ji kot smer kopiranja podamo cudamemcpyhosttodevice, iz glavnega sistemskega pomnilnika v alocirani del pomnilnika naprave prenesemo želene podatke. Po končanem izvajanju jedra moramo dobljene rezultate prenesti nazaj v glavni sistemski pomnilnik s klicem funkcije cudamemcpy in smerjo kopiranja cudamemcpydevicetohost, na napravi pa sprostiti alocirani prostor s klicem cudafree [11]. S prihodom CUDA verzije 6.0 se je pojavil tudi nov način dostopa do pomnilnika, ki se imenuje enotni pomnilnik (unified memory). Ta razvijalcem programov CUDA omogoča lažje delo s pomnilnikom, saj odpravlja potrebo po ločeni alokaciji pomnilnika in prenosu podatkov. Za alokacijo pomnilnika in samodejen prenos podatkov med izvajanjem jedra poskrbi ukaz cudamallocmanaged [9]. Klic jedra izgleda kot klasičen klic funkcije s podanimi parametri, le da med ime funkcije in njene parametre vrinemo konstrukt <<<B, T>>>. Pri tem B označuje dimenzijo mreže blokov, T pa dimenzijo vsakega bloka (t.j. število niti v bloku). Klic jedra se izvede asinhrono, kar pomeni, da se izvajanje na gostitelju nadaljuje med izvajanjem jedra na napravi. Oglejmo si primer programa za paralelno seštevanje dveh vektorjev števil. Koncept paralelne vsote prikazuje slika 2.5, s katere je razvidno, da i-ta nit vzame i-ti element iz vektorjev A in B. Vrednosti sešteje in rezultat vpiše v vektor C na i-to mesto. Pri tem so vsi trije vektorji dolžine N, i pa predstavlja celo število med 0 in N-1. Slika 2.5: Paralelno seštevanje dveh vektorjev. 8

Zraven programske kode se nahajajo tudi krajši komentarji, podrobnosti o posamezni funkciji pa se nahajajo v napotkih za programiranje v CUDA, ki jih izda NVIDIA z vsako novo različico programskega paketa CUDA [11]. Programska koda 2.1: Seštevanje dveh vektorjev na arhitekturi CUDA. // Vhod v funkcijo so kazalci na polja vektorjev A, B in C, // ter dolžina vektorjev N global void SestejVektorjaCUDA(float* A, float* B, float* C, int N) { // Izračunamo indeks posamezne niti int i = blockidx.x * blockdim.x + threadidx.x; // Preverimo, če je indeks niti manjši od dolžine vektorjev if(i < N) { // Nit i sešteje vrednosti na mestu i v A in B, ter // rezultat shrani na mesto i v C C[i] = A[i] + B[i]; } } // Vhod v funkcijo so kazalci na polja vektorjev A, B in C, // ter dolžina vektorjev N void SestejVektorja(float* A, float* B, float* C, int N) { float *d_a, *d_b, *d_c; // Alociranje pomnilnika in kopiranje podatkov cudamalloc(&d_a, sizeof(float) * N); cudamemcpy(d_a, A, sizeof(float) * N, cudamemcpyhosttodevice); cudamalloc(&d_b, sizeof(float) * N); cudamemcpy(d_b, B, sizeof(float) * N, cudamemcpyhosttodevice); cudamalloc(&d_c, sizeof(float) * N); // Klic funkcije z ceil(n/256) bloki, vsak ima 256 niti SestejVektorjaCUDA<<<ceil(N/256), 256>>>(d_A, d_b, d_c, N); // Kopiranje rezultata nazaj v sistemski pomnilnik cudamemcpy(c, d_c, sizeof(float) * N, cudamemcpydevicetohost); // Sprostitev pomnilnika na napravi cudafree(d_a); cudafree(d_b); cudafree(d_c); } 9

2.3 Organizacija niti v CUDA Niti so organizirane v hierarhično strukturo. Več niti skupaj sestavlja blok, medtem ko se več blokov skupaj združuje v mrežo. Do posamezne niti znotraj bloka dostopamo preko indeksa, ki je različen za vsako nit znotraj istega bloka. Indeksi se pri nitih v drugem bloku ponovijo. Prav tako do posameznega bloka znotraj mreže dostopamo preko indeksa, ki je unikaten za vsak blok. Združevanje niti v bloke in te v mrežo lahko poteka v 1D, 2D ali 3D prostoru. To omogoča poenostavitev reševanja določenih problemov. V bloku, v katerem so niti organizirane v obliki 2D, lahko posamezna nit preprosto dostopa do posameznega piksla v 2D sliki. Slikovno ponazoritev, kako so organizirane niti in kako se združujejo, vidimo na sliki 2.6. Slika 2.6: Organizacija niti v arhitekturi CUDA. Do indeksa posameznega bloka dostopamo preko vrednosti blockidx. Ker so bloki lahko organizirani tudi v 2D ali 3D prostoru, moramo ukaz razširiti tako, da povemo, za katero dimenzijo gre. Tako poznamo oznake blockidx.x, blockidx.y in blockidx.z, ki predstavljajo indekse v posameznih dimenzijah. Za dostopanje do posamezne niti znotraj bloka uporabimo isto tehniko kot pri dostopu do bloka. Ker so niti znotraj bloka lahko razporejene v največ tri dimenzije, tudi za dostop do niti uporabljamo tri oznake. To so threadidx.x, threadidx.y in threadidx.z [20]. Maksimalno število niti in blokov, ki se lahko nahajajo v posamezni smeri, 10

določa računska zmogljivost naprave, ki je odvisna od njene arhitekture. Za dostop do posamezne niti znotraj mreže potrebujemo še spremenljivko griddim, ki vrne število blokov v mreži, in blockdim, ki določa dimenzijo blokov. Obe spremenljivki sta prav tako strukturi s komponentami x, y in z. Imejmo N niti, ki so razporejene v M enodimenzionalnih blokov. Zaporedni linearni indeks niti bi izračunali po naslednji enačbi: indeksniti = blockidx.x blockdim.x + threadidx.x; (2.1) V primeru 2D blokov se zaporedni indeks niti izračuna kot: indeksniti = blockidx.x blockdim.x blockdim.y +threadidx.y blockdim.x + threadidx.x; (2.2) 2.4 Pomnilniška hierarhija Vsaka nit, ki se izvaja na GPE, ima dostop do šestih različnih oblik pomnilniškega prostora za shranjevanje podatkov. Največji med vsemi je globalni pomnilnik (global memory), do katerega ima dostop vsaka nit. Je najpočasnejši izmed vseh. Iz njega lahko posamezna nit prebere podatke in jih vanj tudi vpisuje. Sledi pomnilnik konstant (constant memory), v katerem so shranjeni podatki, ki se skozi čas ne spreminjajo, zato lahko niti iz tega pomnilnika samo berejo, medtem ko pisati vanj ne morejo. Podatke v pomnilnik konstant vpiše CPE pred zagonom jedra. Podatki iz pomnilnika konstant se lahko nahajajo v predpomnilniku in s tem se zmanjša dostopni čas do teh podatkov. Niti prav tako ne morejo pisati v pomnilnik tekstur (texture memory), ki je primeren za tiste podatke, ki jih moramo velikokrat prebrati. Za doslej omenjene oblike pomnilnika velja, da so podatki vidni vsem nitim v mreži. Deljeni pomnilnik (shared memory) je skupen bloku niti, ki si lahko preko njega izmenjujejo podatke. Posamezna nit lahko iz njega bere in vanj tudi vpisuje podatke. Ima nizek dostopni čas, saj se nahaja znotraj samega čipa GPE. Lokalni pomnilnik (local memory) je zasebni pomnilnik vsake niti, ki se nahaja v globalnem pomnilniku in zato spada med pomnilnike z najdaljšim dostopnim časom. Posamezna nit lahko bere in piše v lokalni pomnilnik. Druga zasebna oblika pomnilnika so registri, ki se nahajajo neposredno zraven računske enote in imajo najnižji dostopni čas. Vsaka izmed oblik pomnilnika se v GPE nahaja v omejenih količinah. Najbolj količinsko 11

omejena sta deljeni pomnilnik in registri, ki se nahajajo znotraj samega čipa GPE. Pri GPE z računsko zmogljivostjo 5.2 lahko vsak posamezen blok naslavlja največ 48 KB deljenega pomnilnika in ima 64 K 32-bitnih registrov. Vsaka nit lahko dostopa do največ 255 registrov [11]. Slika 2.7 prikazuje pomnilniško hierarhijo in povzema naš zgornji opis posameznih vrst pomnilnika. Možnost branja in pisanja je ponazorjena s puščicami, pri čemer smer puščice pomeni smer pretoka podatkov. Slika 2.7: Pomnilniška hierarhija CUDA. Vrsto pomnilnika za določeno spremenljivko določimo z uporabo rezervirane besede pred deklaracijo spremenljivke. Za lokalni pomnilnik in register ne rabimo posebnih oznak. V register se shranijo vsi skalarji, v lokalni pomnilnik vse ostale deklaracije, kot so recimo polja. Uporaba pomnilnika tekstur je zahtevnejša, saj moramo pripraviti ustrezen deskriptor, ki ga uporabimo v funkciji za alociranje pomnilnika tekstur. V tabeli 2.2 smo povzeli lastnosti vseh tipov pomnilnika, ki jih najdemo na napravi CUDA 12

in jih združili v pregledno obliko. R pomeni branje (read) in W pisanje (write). Tabela 2.2: Lastnosti vseh tipov pomnilnika na napravi CUDA. Pomnilnik Lokacija Dostopnost Življenjska doba Register Predpomnilnik Gostitelj: / Naprava: R/W Nit Lokalni Naprava Gostitelj: / Naprava: R/W Nit Deljeni Predpomnilnik Gostitelj: / Naprava: R/W Blok Globalni Naprava Gostitelj: R/W Naprava: R/W Aplikacija Konstant Naprava Gostitelj: R/W Naprava: R Aplikacija Tekstur Naprava Gostitelj: R/W Naprava: R Aplikacija Včasih si želimo, da bi se niti med seboj sinhronizirale. Povedano drugače, radi bi dosegli, da vse niti dosežejo neko točko, preden se izvajanje programa nadaljuje. To je pomembno predvsem zaradi konsistentnosti podatkov, saj niti večkrat berejo in pišejo v pomnilnik. Tako je pomembno, da se vsi podatki zapišejo v pomnilnik, preden se izvede naslednji korak, ki bo potreboval te podatke. Sinhronizacijo niti znotraj bloka dosežemo z ukazom syncthreads(), ki ga kličemo na napravi, sinhronizacijo na nivoju celotne naprave pa z ukazom cuda- DeviceSynchronize(), ki zaustavi izvajanje gostiteljske kode, dokler se vsa zagnana jedra ne zaključijo. 2.5 Dinamični paralelizem Dinamični paralelizem je funkcionalnost, ki jo podpirajo novejše GPE v kombinaciji z različico CUDA od 5.0 naprej [1]. Pred uvedbo dinamičnega paralelizma smo lahko jedro pognali samo iz CPE, sedaj pa lahko jedro, ki se že izvaja na GPE, zažene novo jedro. Dinamični paralelizem je pomemben predvsem zato, ker omogoča prilagajanje števila niti količini dela, ki se ustvarja dinamično med izvajanjem jedra. Statična konfiguracija velikosti mreže ob 13

zagonu jedra ni vedno optimalna za izvajanje algoritmov tipa deli in vladaj, ki jih tipično implementiramo z rekurzijo. Zato je pred uvedbo dinamičnega paralelizma jedro izvedlo en nivo rekurzije, nato pa predalo nadzor nazaj gostitelju. Ta je na podlagi rezultatov določil primerno konfiguracijo za naslednji nivo rekurzije in pognal novo jedro. Slabost takšnega pristopa je v relativno počasni komunikaciji med GPE in CPE. Kot zgled vzemimo simulacijo turbulence, ki nastane pri izgorevanju goriva. S premikanjem plinov skozi zrak se spreminja količina podrobnosti, ki jih moramo izračunati in izrisati. Na sliki 2.8 vidimo dva različna pristopa. Vsaka celica mreže na sliki predstavlja en blok niti na GPE, ki obdeluje pripadajoči del podatkov. Vsak blok kot rezultat vrne vrednost turbulence v območju celice. Leva in sredinska slika prikazujeta statično konfiguracijo jedra, kakršno bi uporabili v primeru brez dinamičnega paralelizma. Na sliki levo je gostota mreže premajhna, zato ne ujamemo dovolj podrobnosti. Gostota mreže v sredini je dovolj majhna, da ujamemo podrobnosti, vendar v področjih brez turbulence zahteva več nepotrebnega računanja in povzroči slab izkoristek GPE. Z uporabo dinamičnega paralelizma na desni sliki lahko število blokov, ki obdelujejo posamezno področje turbulence, dinamično spreminjamo s klicem novih jeder. Tako dosežemo ustrezno gostoto mreže na vseh področjih [1]. Slika 2.8: Statična konfiguracija jedra s premajhno gostoto mreže (levo) in s preveliko gostoto mreže (sredina) ter z dinamično konfiguracijo gostote mreže (desno). Razliko med izvajanjem CUDA programa, ki podpira dinamični paralelizem, in med tistim, ki ga ne, vidimo na sliki 2.9. Leva stran prikazuje potek programa brez dinamičnega paralelizma, desna stran pa z njim. S slike 2.9 je razvidna tudi komunikacija med CPE in GPE, katere je v primeru z dinamičnim paralelizmom bistveno manj. 14

Slika 2.9: Izvajanje programa brez dinamičnega paralelizma (levo) in z njim (desno). S stališča programerja dinamični paralelizem omogoča poganjanje vgnezdenih jeder na sami napravi. Programska koda 2.2 prikazuje zgled, v katerem gostitelj zažene jedro in ta nato novo vgnezdeno jedro. Programska koda 2.2: Primer dinamičnega paralelizma \\ CPE funkcija void FunkcijaCPE() { float* podatki Jedro_1<<<1, 1>>>(podatki); } \\ Funkcija jedra, ki jo pokličemo iz CPE global void Jedro_1(float* podatki) { \\ Vgnezdeni klic jedra - dinamični paralelizem Jedro_2<<<1, 1>>>(podatki); } \\ Funkcija jedra, ki jo pokličemo iz GPE global void Jedro_2(float* podatki) {... } 15

V prikazanem zgledu se jedro Jedro_1 imenuje oče, jedro Jedro_2 pa sin. Izvajanje v očetu se lahko nadaljuje takrat, ko se izvajanje sina zaključi [11]. Sin lahko pokliče novo jedro in tako postane oče. To se lahko nadaljuje do globine, ki jo omejuje arhitektura in trenutno znaša 64 [8]. Dejanska maksimalna globina je odvisna od količine pomnilnika, ki ga imamo na voljo, in je običajno precej nižja od arhitekturne omejitve. Pri uporabi dinamičnega paralelizma moramo biti pozorni predvsem na pravilen dostop do pomnilnika in prenašanje podatkov med očetom in sinovi. Najmanj težav imamo z uporabo globalnega pomnilnika, ki je tudi najpočasnejši. Dostop do njega imata tako oče kot sin. Spremembe, ki jih v pomnilnik vnese sin, so po sinhronizaciji vidne tudi očetu. Prav tako ni težav z dostopanjem do pomnilnika konstant, kot tudi ne prenašanje njegovih vrednosti preko parametrov funkcije. Več težav se pojavi pri ostalih vrstah pomnilnikov. Lokalni pomnilnik je omejen na posamezno nit in tako oče, ki ustvari sina, temu ne more posredovati kazalca, ki kaže na lokalni pomnilnik očeta. To lahko na začetku programerju povzroča nemalo težav. Če želimo podatke prenesti do sina, jih moramo alocirati v globalnem pomnilniku. Tudi deljeni pomnilnik je dostopen samo nitim znotraj istega bloka. Če kot parameter funkcije uporabimo kazalec na spremenljivko v tem pomnilniku, nam prevajalnik javi napako [12]. Če povzamemo, je dinamični paralelizem zmožnost jedra, da zaganja nova jedra brez posredovanja CPE. To programerju omogoča več svobode pri pisanju programov CUDA, hkrati lahko pridobimo na hitrosti takšnega programa. 16

Poglavje 3 ODLOČITVENA DREVESA IN ALGORITEM CART Odločitvena drevesa so eden izmed priljubljenih modelov strojnega učenja za klasifikacijo vzorcev. S pomočjo začetne učne množice algoritem za gradnjo dreves zgradi drevo, ki ga lahko nato uporabimo za klasifikacijo novih primerkov. Eden izmed algoritmov za gradnjo odločitvenih dreves je CART, ki ga obravnavamo in implementiramo v tem diplomskem delu. 3.1 Odločitvena drevesa Glavni namen uporabe odločitvenih dreves je klasifikacija vzorcev. Za gradnjo odločitvenih dreves potrebujemo začetno množico podatkov, ki ji rečemo učna množica. Sestavljena je iz vzorcev, za katere vemo, v kateri razred spadajo. Posamezen vzorec je opisan z več vrednostmi, ki jim pravimo atributi. Če je učna množica sestavljena iz ljudi in en človek iz te množice predstavlja vzorec, potem so atributi tega vzorca na primer starost, teža, višina, barva las in ostale značilnosti, ki jih lahko izmerimo ali pridobimo iz opazovanj. Atributi so lahko numerični ali kategorični. Za numerične atribute velja, da so to števila, ki so bila izmerjena in predstavljajo dejanske vrednosti. Kategorični atributi predstavljajo različne kategorije, v katero spada vrednost atributa. Običajno tudi kategorične atribute preslikamo v števila. Kategorični atribut je na primer spol človeka, ki bi ga lahko preslikali v "1" za moški spol in "2" za ženski spol. Odločitvena drevesa so sestavljena iz posameznih vozlišč. Vsako vozlišče je lahko oče, sin ali oboje. Oče je v primeru, kadar se iz njega razvija vsaj eno novo vozlišče oziroma sin. Vozlišče, ki se razvija iz očeta, se imenuje sin. Če neko vozlišče nima lastnosti očeta, torej je samo sin, takšnemu vozlišču rečemo tudi list. Kakšnega tipa bo posamezno vozlišče, ugotovimo pri gradnji odločitvenega drevesa. Gradnja odločitvenega drevesa se začne v vozlišču, ki ga imenujemo koren drevesa. Izbere se vrednost, ki razdeli podatke v dve ali več podmnožic. Vrednost se izbere na podlagi pogoja, ki optimalno razdeli množico podatkov in tako pripomore k pravilni klasifikaciji zgrajenega 17

drevesa. Vsakemu sinu pripada posamezna podmnožica. Če je posamezna podmnožica prazna (t.j. ne vsebuje nobenega vzorca), se tisti sin ne ustvari. Celoten postopek se nato rekurzivno ponovi na vseh ustvarjenih sinovih. Gradnja odločitvenega drevesa se konča, ko so v vseh listih vzorci istega razreda. Dodaten zaključni pogoj lahko predstavlja maksimalno število vzorcev v listu ali globina drevesa. Primer zgrajenega drevesa prikazuje slika 3.1. Odločitveno drevo klasificira barvo točke, glede na njeni koordinati x in y. Zaključni pogoj pri gradnji drevesa je njegova maksimalna globina pri vrednosti 3. Barvo, ki jo klasificira posamezen list, določimo kot barvo, ki ji pripada največ vzorcev v listu. Slika 3.1: Učna množica za problem klasifikacije barve točk na podlagi njihovih koordinat (levo) in odločitveno drevo, ki izvaja takšno klasifikacijo (desno). Splošna ideja gradnje odločitvenih dreves je iskanje pogojev v vseh notranjih vozliščih drevesa, ki bodo dosegli najvišjo natančnost pri klasifikaciji učnih vzorcev. Predpostavljamo, da bo tako zgrajeno drevo dobro posploševalo, t.j. klasificiralo tudi vse kasnejše vzorce, za katere bomo želeli izvedeti, kateremu razredu pripada posamezni vzorec. 3.2 Algoritem CART Algoritem CART je bil predstavljen leta 1984. Razvili so ga Leo Breiman, Jerome Friedman, Richard Olshen in Charles Stone. Takrat je predstavljal pomemben mejnik v razvoju umetne inteligence, strojnega učenja in podatkovnega rudarjenja. Danes se uporablja na področju finančnih trgov, elektrotehniki, kemiji, biologiji, raziskavah v zdravstvu in celo pri stiskanju slik s pomočjo kvantiziranih vektorjev [21]. 18

Odločitveno drevo, zgrajeno z algoritmom CART, je namenjeno klasifikaciji posameznih vzorcev v enega izmed razredov, ali regresiji, ki določi vrednost enega atributa na podlagi vrednosti ostalih. Pri tem lahko uporablja tako numerične kot kategorične vrednosti. Vsak oče ima lahko največ dva sinova, ki se lahko naprej rekurzivno delita. Ti dve lastnosti določata, da je CART binarni rekurzivni postopek [19]. Odločitveno drevo vedno zgradimo do njegove maksimalne velikosti, nato sledi postopek obrezovanja. Pri obrezovanju odstranjujemo tista poddrevesa, ki najmanj pripomorejo k povprečni pravilnosti klasifikacije [10]. Algoritem je tudi zelo prilagodljiv in odporen na napake, saj omogoča avtomatsko uravnotežanje razredov, v katerih se nahajajo podatki, določanje pomembnosti posameznega atributa in uporabo vzorcev z manjkajočimi vrednostmi atributov. To so lastnosti, ki dodatno izboljšajo gradnjo drevesa, vendar so težavne za implementacijo in jih v našem primeru ne potrebujemo. Programska koda 3.1 prikazuje psevdokod algoritma CART. Vhod: Učna množica vzorcev Izhod: Maksimalno drevo Programska koda 3.1: Psevdokod algoritma CART. ZAČETEK: Vse podatke dodeli korenu drevesa Koren drevesa določi kot trenutno aktivno vozlišče T DELI (T): Določi atribut in njegovo vrednost, ki določa najboljšo delitev Razdeli podatke glede na izbrano vrednost v množici P1 in P2 IF NOT(Vsi podatki P1 so v istem razredu) Ustvari novo vozlišče T1, ki je sin vozlišču T Vozlišču T1 dodeli podatke iz množice P1 Kliči funkcijo DELI z vozliščem T=T1 IF NOT(Vsi podatki P2 so v istem razredu) Ustvari novo vozlišče T2, ki je sin vozlišču T Vozlišču T2 dodeli podatke iz množice P2 Kliči funkcijo DELI z vozliščem T=T2 Deljenje podatkovne množice pri algoritmu CART poteka vedno v dve novi podmnožici. Označimo ti dve podmnožici s T L in T R, pri čemer T L predstavlja levega sina in T R desnega sina. Skupen obema je oče, ki ga označimo s T. Oblika pogoja za delitev množice vzorcev v staršu je odvisna od tipa atributa. V primeru numeričnega atributa se izbere vrednost P i, tako da gredo vsi vzorci z vrednostjo atributa manjšo ali enako P i v podmnožico T L, vsi ostali vzorci v podmnožico T R. Pri delitvi po kategoričnem atributu se določi podmnožica S i kategoričnih vrednosti tako, da v podmnožico T L padejo vsi vzorci, katerih vrednost atributa 19

pripada S i, vsi ostali vzorci padejo v podmnožico T R. Pri gradnji odločitvenega drevesa želimo poiskati najboljše delitve. Kvaliteto delitvenega pogoja pri algoritmu CART ovrednotimo z indeksom Gini (Gini impurity). Med vsemi možnimi delitvenimi pogoji po katerem koli atributu izberemo tistega z najvišjim indeksom Gini. Imejmo delitev, ki jo prikazuje slika 3.2. Očeta označimo s T, njegova množica podatkov naj vsebuje N vzorcev. Sinova označimo s T L in T R, velikosti njunih podmnožic pa z N L in N R. T(N) T L (N L ) T R (N R ) Slika 3.2: Binarno poddrevo. Vsak vzorec iz množice podatkov naj pripada enemu izmed k razredov. Indeks Gini za množico T izračunamo po naslednji enačbi: gini(t ) = 1 k i=1 p 2 i, (3.1) kjer je p i delež vzorcev iz množice T, ki pripada razredu i. Večja vrednost indeksa Gini pomeni bolj mešano množico vzorcev, glede na razred, v katerega spada posamezni vzorec. Množica, katere elementi so vzorci istega razreda, ima indeks Gini enak 0. Izmed vseh delitev izberemo tisto, ki množico T razdeli v čim bolj homogeni podmnožici T R in T L. To je enakovredno delitvi, ki doseže maksimalno vrednost po naslednji enačbi: C = gini(t ) N L N gini(t L) N R N gini(t R), (3.2) kjer je N število vzorcev v množici T, N L število vzorcev v podmnožici T L in N R število vzorcev v podmnožici T R. 20

Poglavje 4 IMPLEMENTACIJA Aplikacija je razdeljena na pet osnovnih delov. Za komunikacijo z uporabnikom skrbi grafični vmesnik, ki je napisan s pomočjo tehnologije WPF. Ogrodje je namenjeno izdelavi grafičnih vmesnikov za ogrodja.net. Pri izdelavi tega dela smo uporabili programska jezika C# in XAML. Prvi skrbi za implementacijo logike, z drugim oblikujemo postavitev posameznih elementov v aplikaciji. Naslednji trije deli, ki predstavljajo implementacijo algoritma v vseh treh načinih, so napisani v programskem jeziku C++ in prevedeni v knjižnice DLL. Ker grafični vmesnik ne more neposredno komunicirati s temi knjižnicami, smo napisali še vmesno kodo v programskem jeziku C++\CLI. Za razvoj celotne aplikacije smo uporabili programsko okolje Visual Studio 2013 in knjižnice CUDA različice 7.0. Pri implementaciji smo izpustili nekatere podrobnosti algoritma CART, saj smo se osredotočili predvsem na razliko, ki jo prinese dinamični paralelizem. Omejili smo se na numerične atribute in klasifikacijo v dva razreda. Gradnja drevesa poteka do maksimalne globine 24, drevesa pa ne obrezujemo. 4.1 Grafični vmesnik Namen grafičnega vmesnika je komunikacija z uporabnikom. Sestavljen je iz dveh glavnih oken. Prvo okno se odpre ob zagonu aplikacije in služi nadaljnjemu delu z aplikacijo. Razdeljeno je na tri podenote, kot je prikazano na sliki 4.1. Na vrhu je meni, ki ga uporabnik uporablja za izvajanje nadaljnjih opravil. To so uvoz podatkov, zagon algoritma za gradnjo drevesa in risanje zgrajenega drevesa. Pod menijem sta dve večji podenoti. Na levi strani so prikazani glavni podatki, ki uporabniku omogočajo lažje delo z aplikacijo. Primer takšnih podatkov je število atributov in vzorcev, ki jih vsebuje učna množica, razredi, v katere so razdeljeni ti vzorci in ostali za uporabnika pomembni podatki. To podenoto lahko uporabnik tudi skrije. Desna podenota je namenjena izrisu maksimalno zgrajenega drevesa. 21

Slika 4.1: Glavno okno aplikacije. V prikazu odločitvenega drevesa so zelena vozlišča tista, ki se delijo naprej. Kriterij delitve je zapisan v vsakem izmed teh vozlišč. Zapis A1 59 na primer pomeni, da se v tistem vozlišču podatki delijo glede na atribut A1 z vrednostjo praga 59. Vozlišča, ki so rdeče barve, predstavljajo liste, oznaka v njih pove, v kateri razred spadajo vzorci v tistem listu. 4.2 Implementacija na CPE Postopek gradnje drevesa na CPE je klasičen rekurzivni postopek. Gradnjo začnemo v korenu drevesa, kjer poiščemo atribut in vrednost praga, pri kateri je delitev celotne učne množice po kriteriju na osnovi enačbe 3.2 najboljša. Ko takšno delitev najdemo, celotno množico podatkov razdelimo na dve podmnožici. Če se v eni podmnožici nahajajo vzorci, ki pripadajo istemu razredu, se gradnja tistega poddrevesa zaključi, v nasprotnem primeru se ustvari nov sin in gradnja se rekurzivno nadaljuje, tako da sin postane obravnavano vozlišče. Postopek ponovimo še na drugi podmnožici. 22

Vsako vozlišče drevesa je shranjeno v posebni strukturi, ki vsebuje tip atributa, indeks atributa, indeks vzorca, oznako razreda in dva kazalca, ki kažeta na levega oziroma desnega sina. Definicijo strukture prikazuje programska koda 4.1. Programska koda 4.1: Struktura za hranjenje vozlišč. struct TreeNode { int typeofattribute; int indexofattribute; int indexofindices; unsigned int classtype; TreeNode* leftchildnode; TreeNode* rightchildnode; }; Iskanje atributa in praga delitve poteka pri tej implementaciji zaporedno. Za vsak atribut vzorce uredimo po vrednostih tega atributa, za kar smo uporabili hitro urejanje (quicksort). Nato računamo kvaliteto delitve po enačbi 3.2 samo za unikatne vrednosti praga. S tem, ko vzorce uredimo po izbranem atributu, lahko tudi takoj ugotovimo, koliko vzorcev bo imela desna in koliko leva podmnožica. Ker bi premeščanje podatkov v glavnem pomnilniku med urejanjem bilo prepočasno, urejanje izvedemo posredno preko polja indeksov vzorcev. Tako pri urejanju spreminjamo samo polje z indeksi in ne samih podatkov. 4.3 Implementacija na GPE brez dinamičnega paralelizma Zaradi omejitev glede uporabe pomnilnika na GPE, moramo prilagoditi način hranjenja podatkov. Podatke zato razdelimo v posamezne skupine polj, glede na tip podatkov, ki jih polja hranijo. Tako imamo skupino polj, v katerih so celoštevilski podatki, in skupino polj, v katerih so podatki z realnimi vrednostmi. Na vsako izmed teh skupin polj imamo prirejen kazalec. Posamezno polje znotraj skupine celoštevilskih polj zapišemo na naslednji način 4.2. Programska koda 4.2: Struktura za hranjenje celoštevilskega polja. struct GPUIntArray { unsigned int length; int* data; }; Podobno naredimo tudi za polja realnih števil. Tako imamo celoštevilske atribute ločene od atributov z realnimi vrednostmi, kar olajša prenos podatkov v pomnilnik GPE in obdelavo vzorcev. 23

Omenjeni dve skupini celoštevilskih in realnih polj prenesemo v globalni pomnilnik GPE, kamor prenesemo tudi polje, ki vsebuje vrednosti razreda, v katerega spada posamezen vzorec. V primeru posameznega polja moramo poznati tudi število elementov v tem polju. V konstantni del pomnilnika zato shranimo število elementov, ki jih vsebuje posamezno polje, in ostale parametre, ki jih med izvajanjem ni potrebno spreminjati. Izračun vrednosti Gini paraleliziramo tako, da poženemo toliko niti, kolikor je vzorcev v množici vozlišča. Vsaka nit izračuna vrednost Gini za posamezno delitev atributa. Ker moramo med temi vrednostmi poiskati največjo, uporabimo za hranjenje vmesnih rezultatov deljeni pomnilnik. S tem pridobimo na hitrosti, saj je ta pomnilnik zelo hiter v primerjavi z globalnim pomnilnikom in unikaten za vsak posamezen blok. Najprej izračunamo največjo vrednost znotraj posameznega bloka in stvar dodatno paraleliziramo. Nato poiščemo globalno največjo vrednost z uporabo funkcije atomicmax. Funkcija spada med atomske funkcije, kar pomeni, da se primerjava in prirejanje izvedeta istočasno. To je zelo pomembno, saj ne sme priti do stanja, kjer bi dve ali več niti naenkrat brali in spreminjali isto vrednost. S tem dobimo največjo vrednost za prvi atribut. Ta korak ponovimo tolikokrat, kolikor atributov vsebujejo vzorci, in vedno obdržimo največjo vrednost. Tako dobimo globalno največjo vrednost in s tem pogoj za deljenje, ki razdeli množico podatkov na dve podmnožici. Za urejanje vzorcev uporabimo funkcijo sort, ki je del knjižnice Thrust in omogoča urejanje na GPE. Ker do podatkov dostopamo posredno preko polja z indeksi, moramo spremeniti način delovanja funkcije za urejanje, da ureja polje z indeksi in ne podatkov samih. Funkcija sort lahko kot parameter sprejme poljubno strukturo, v kateri je tudi kazalec na metodo za primerjanje elementov. Primer strukture in funkcije, ki smo jo uporabili v našem primeru implementacije, prikazuje programska koda 4.3. Funkcijo za urejanje na GPE nato preprosto pokličemo z ukazom thrust::sort(kljuci, kljuci + len, cmpi(vrednosti)), kjer je kljuci polje z indeksi in dolžino len, vrednosti pa polje, kjer so shranjeni vzorci učne množice za posamezen atribut. Programska koda 4.3: Struktura za indirektno urejanje. struct cmpi : public thrust::binary_function<int, int, bool> { thrust::device_ptr<int> rawp; host device cmpi(thrust::device_ptr<int> ptr) : rawp(ptr) { } host device bool operator()(const int i, const int j) const { return rawp[i] < rawp[j]; } }; 24

Prednost paralelizma uporabimo tudi pri ugotavljanju, ali vsi vzorci znotraj množice spadajo v isti razred. Funkciji kot argument podamo polje, ki hrani razrede vzorcev, in dve logični spremenljivki razred1 in razred2. Funkcijo poženemo s toliko nitmi, kolikor je vzorcev v množici. Vsaka nit preveri razred pripadajočega vzorca in postavi ustrezno logično spremenljivko na true. Po vrnitvi iz funkcije moramo samo preveriti vrednosti obeh spremenljivk. Če je na true postavljena samo ena spremenljivka, to pomeni, da so bili vsi vzorci iz razreda, katerega spremenljivka je postavljena na true. Če sta na true postavljeni obe spremenljivki, pomeni, da vzorci pripadajo obema razredoma. 4.4 Implementacija na GPE z dinamičnim paralelizmom Implementacija na GPE z dinamičnim paralelizmom je zelo podobna implementaciji brez dinamičnega paralelizma. Podatki se hranijo v isti obliki in v istih delih pomnilnika, funkcije za izračun indeksa Gini, ugotavljanje homogenosti podmnožic in delitev množice v podmnožici pa so celo identične. Edina razlika je ta, da se tukaj glavna rekurzivna funkcija, ki zgradi posamezno vozlišče, izvaja na GPE, saj uporablja prednost dinamičnega paralelizma. Posamezno vozlišče drevesa se pri tej implementaciji shrani v pomnilnik GPE, zato je po končani gradnji potreben prenos zgrajenega odločitvenega drevesa v glavni sistemski pomnilnik. Razliko v izvajanju med implementacijo brez dinamičnega paralelizma in tisto z njim prikazuje slika 4.2. Slika 4.2: Implementacija brez (levo) in z (desno) dinamičnim paralelizmom. 25

Prenos drevesne strukture iz pomnilnika GPE v sistemski pomnilnik bi bil zapleten. Zaradi tega binarno drevo, ki ga tvori algoritem CART, pri tem pristopu implementiramo s statičnim poljem, v katerem se sinova očeta na indeksu i nahajata na indeksih i 2 + 1 in i 2 + 2. Elementi polja so strukture, ki hranijo tip atributa, indeks atributa, indeks vzorca, podatek o tem, ali gre za notranje vozlišče, ter razred, v katerega spada vozlišče list. 26

Poglavje 5 REZULTATI IN DISKUSIJA V tem poglavju bomo prikazali rezultate, ki smo jih dobili na podlagi testiranj algoritma CART v treh načinih implementacije. Pri tem nas je zanimal predvsem čas gradnje odločitvenega drevesa, dodatno pa tudi prostorska zahtevnost posameznih implementacij. Podatke, ki smo jih uporabili za gradnjo dreves, smo dobili na spletni strani KDD Cup 2004 [7] in UCI Machine Learning Repository [22]. Na slednji je zbranih veliko podatkovnih zbirk, ki so namenjene uporabi v algoritmih umetne inteligence, podatkovnega rudarjenja in strojnega učenja. 5.1 Rezultati meritev Čas izvajanja algoritma smo merili na dva različna načina, glede na to, kje se je izvajala glavna rekurzivna funkcija. Pri implementaciji algoritma na CPE in GPE brez dinamičnega paralelizma se je ta funkcija izvajala na CPE, zato smo za merjenje časa uporabili programsko kodo 5.1, ki kot rezultat vrne čas izvajanja v sekundah. Programska koda 5.1: Merjenje časa izvajanja algoritma na CPE. LARGE_INTEGER freq, start, end; QueryPerformanceFrequency(&freq); QueryPerformanceCounter(&start); // Tukaj poženemo gradnjo drevesa QueryPerformanceCounter(&end); double seconds = static_cast<double> (end.quadpart - start.quadpart) / freq.quadpart; Pri merjenju časa izvajanja za algoritem z dinamičnim paralelizmom smo morali uporabiti drugačen način merjenja, saj smo merili trajanje funkcije, ki se je izvajala na GPE. Pri tem smo uporabili programsko kodo 5.2, ki vrne čas izvajanja v milisekundah. Programska koda 5.2: Merjenje časa izvajanja algoritma na GPE. cudaevent_t start, end; cudaeventcreate(&start); cudaeventcreate(&end); cudaeventrecord(start, 0); 27