programiranje u cudi b

41
 SVEUČILIŠTE U MOSTARU FAKULTET PRIRODOSLOVNO  MATEMATIČKIH I ODGOJNIH ZNANOSTI Programiranje u Cudi Mostar, rujan 2014.

Upload: darko-smiljanic

Post on 02-Nov-2015

47 views

Category:

Documents


4 download

DESCRIPTION

Programiranje u Cudi B

TRANSCRIPT

  • SVEUILITE U MOSTARU

    FAKULTET PRIRODOSLOVNO MATEMATIKIH I

    ODGOJNIH ZNANOSTI

    Programiranje u Cudi

    Mostar, rujan 2014.

  • 1. Uvod

    U proteklih 40 godina, namjenski grafiki procesori su proli put od istraivakih laboratorija i

    simulatora leta do komercijalnih radnih stanica i medicinskih ureaja, a kasnije sve do osobnih

    raunala i konzola za zabavu. U nedavno vrijeme poeli su se ugraivati u mobilne telefone i

    automobile.

    Grafiki procesori slue da ubrzaju razliite zadatke od iscrtavanja teksta i grafike u Internet web

    preglednicima do sofisticiranije sinteze trodimenzionalnih slika u raunalnim igrama. Ukratko demo

    objasniti prirodu procesiranja potrebnu za 3D slikovnu sintezu koja je osnova za mnoga podruja. Sve

    druge primjene grafikih procesora koriste podskup ovih sposobnosti za 3D procesiranje pa tako i

    opde-namjensko raunanje na grafikim procesorima(GPGPU).

    Kako je broj tranzistora u ovim ureajima poeo nadmaivati broj onih koji se nalaze u CPU, panja je

    se usmjerila na primjenu procesne modi prema raunalo intenzivnim problemima koji se ne odnosi ne

    grafiko renderiranje.

    Rani pristupi u koritenju GPU za opde kalkulacije datiraju jo u vrijeme 2000. Ipak, GPU hardware je

    u to vrijeme bio zasnovan na fiksnom protonom sustavu. Svi zadatci su se morali mapirati u grafiku

    domenu i u toj domeni se i rjeavati. Ovo je bio dosta iscrpljujudi zadatak jer je programer morao

    poznavati sintagme grafike obrade slike i morao je koristiti neki od jezika i API-ja kojim se pristupalo

    funkcijama grafikog protonog sustava. Kao odgovor na trend koritenja grafikih procesora u

    opdenite svrhe raunanja, Nvidia je pokrenula CUDA arhitekturu i pripadajudi API visoke razine

    kako bi razvijateljima aplikacija omogudili to jednostavniji rad u poznatom razvojnom okruenju.

    U ovom radu demo opisati evoluciju i arhitekturu grafikog protonog sustava odnosno grafikog

    procesora. Dati demo pregled i primjere tehnologije CUDA e koja se koristi u opde namjenskom

    raunanju na grafikim procesorima.

  • 2. Razvoj grafikih procesora

    Slijedede poglavlje de dati uvod u raunalnu grafiku i grafiki hardver koji formira pozadinu GPGPU.

    Tijekom ovoga, biti de nam potrebni osnovni termini raunalne grafike.: geometrijske primitive koje

    su predstavljeni jednostavnim atominim geometrijskim objektima kao toke, linije, trokuti i drugi

    poligoni. Krajnje toke ovih objekata se nazivaju vrhovima (vertex). Druga osnovna jedinica je

    fragment koji je osnova za piksele. Fragmenti sadre vrijednost boje, a takoer sadre druge

    informacije koje su potrebne prije nego se piksel nacrta, kao pozicija, dubina, alfa vrijednost (za

    transparentnost).

    2.1 Grafiki cjevovod Grafiki cjevovod (koji se zove cjevovod renderiranja) je model koji opisuje razliite korake koji se

    izvode u renderiranju scene. Koncept cjevovoda se moe usporediti sa CPU instrukcijskim

    cjevovodom: pojedinani koraci se rade paralelno, ali su blokirani sve dok se ne zavri posljednji

    korak. Jednostavan model za (fiksni) protoni sustav je naslikan na slici 1.

    Slika 1. Jednostavni grafiki cjevovod

    Aplikacija mijenja scenu primjerice reagirajudi na korisnike inpute. Komponente nove scene se

    prosljeuju (model i kamera) prema transformacijama. U ovom koraku, koordinate lokalnih objekata

    se transformiraju u globalni koordinatni sustav. Kamera se pozicionira u sceni i koordinatni sustav mu

    se prilagouje. Tijekom osvjetljenja, za sve vrhove se raunaju vrijednosti boje koje ovise o poziciji

    svijetla i svojstvima odgovarajudih trokuta. U koraku projekcije, 3D scena se mapira u 2D prostor

    slike. Tijekom rezanja (clipping), nepotrebne primitive se eliminiraju. U koraku rasterizacije uklanjaju

    se skrivene povrine (koristimo Z-buffer algoritam) i scena se transformira u bitmape na nain da se

    raunaju vrijednosti boja za svaki piksel.

  • 2.2 Grafiki API

    Grafiki API-ji pruaju programerima veliki nivo apstrakcije i pojednostavljuju softverski razvoj

    procesa skrivajudi kompleksnosti i mogudnosti grafikog sklopovlja i pogonskih ureaja.

    Direct3D je API za crtanje 3D grafike, a najprominentnija komponenta je DirectX API kolekcija za

    multimedijalne primjene na Microsoft platformama (Windows i Xbox). Prednost programerima za

    koritenje DirectX je velika zastupljenost na tritu koja omoguduje Microsoft-u da definira minimum

    hardverskih specifikacija za grafike komponente u suradnji sa proizvoaima grafikih kartica.

    Nedostaci su u injenici da je vlasnitvo Microsofta , nisku kompatibilnost sa starijim izdanjima te

    kratki ciklus putanja novih verzja. Ipak, zadnja dva argumenta takoer pruaju osnove za inovacije:

    Sve do pojave Direct3D 10, najinteresantniji razvoj za GPGPU je uvoenje razliitih shader modela.

    OpenGL je meu-platformski API za 2D i 3D raunalnu grafiku i glavna je alternativa Direct3D-u.

    OpenGl se razvija preko Khronos grupacije, industrijskog konzorcija koji ukljuuje vie od 100 lanova

    AMD, Intel i Nvidij-u. OpenGL kao i DirectX se uvelike oslanja na grafike shadere koji de biti

    prezentirane u slijededoj sekciji.

    2.3 Grafiki shaderi

    U raunalnoj grafici , shaderi su mali programi koji rade na GPU ili drugim procesorima. U poetku

    shaderi su oznaili tranziciju sa fiksnog protonog sustava preko konfigurabilnih do programibilnih

    sustava. Shaderi su nainili proces renderiranja fleksibilnijim i omogudili su nove grafike efekte.

    Isprva su shaderi uvedeni u OpenGl 1.5 i Direct3D 8. Kasnije verzije API-ja su revidirali shader

    specifikacije i povedali fleksibilnost, primjerice smanjujudi ogranienja vezanu za maksimalnu koliinu

    instrukcija po shaderu. API je specificirao tri tipa shadera koji se koriste za grafiku: vertex shaderi,

    geometrijski i piksel shaderi. Vertex shaderi mogu mijenjati koordinate ili vrhove dok su geometrijski

    shaderi u mogudnosti generirati ili duplicirati nove geometrijske primitive iz postojedih u cjevovodu

    (instanciranje stabala ili znakova). Oba shadera se mogu mapirati u geometrijski korak koji smo naveli

    ranije kod prezentacije fiksnog protonog sustava. Pixel shaderi (u OpenGl zvani fragmentni shaderi)

    se izvode nakon rasterizacijskog koraka. Operiraju na jednom piksel/fragmentu i izvode promjene

    boja ili sjena.

    Velika inovacija u grafikim shaderima je uvoenje unificiranih shadera. Unificirani shader model

    harmonizira instrukcijski set izmeu razliitih shader tipova. Sa unificiranom shader arhitekturom

    svi shaderi imaju istu hardversku podlogu i mogu se koristi dinamino kao vertex, geometrijski ili

    pixel shaderi. Nvidia je uvela unifed shader arhitekturu sa GeForce 8 serijom. Slika 2. Opisuje razliku

    izmeu arhitektura. Ako pogledamo desnu sliku, ista shader jezgra se upotrebljava za razliite shader

    zadatke. Dobrobit ovog pristupa je mogudnost da se dinamiki reagira na razliite piksel/

    geometrijske oblike u razliitim scenama kako bi se povedale performanse i efikasnost.

  • Slika 2. Usporedba diskretnog i unificiranog dizajna shadera

    Shaderi se programiraju u razliitim jezicima. Dok se programiranje na niskoj razini radi sa

    assemblerom , pristupi visokog nivoa kao High Level Shading Language (HLSL) ciljaju prema Direct3D

    programiranju koje je razvio Microsoft ili Open Gl shading Language za OpenGl programiranje.

  • 3. Arhitektura grafikog procesora

    Hardverska arhitektura grafike jedinice za obradu se razlikuje od normalnog procesora opde

    namjene u nekoliko kljunih aspekata. Ove razlike su uvjetovane potrebama iz podruja real-time

    raunalne grafike.

    Mnogi objekti kao pikseli i vrhovi se mogu obraivati u izolaciji i nisu meusobno ovisni.

    Postoje mnogo nezavisnih objekata (milijuni piksela, tisude vrhova,)

    Mnogi objekti zahtijevaju iscrpna raunanja

    GPU arhitektura je evoluirala da se suoi sa ovim zahtjevima.

    Kako bi bolje razumjeli razlike izmeu CPU i GPU arhitekture poeti demo sa CPU arhitekturom i

    uiniti nekoliko kljunih promjena sve dok ne dobijemo arhitekturu nalik na GPU.

    3.1. Moderni CPU

    Vedina modernih procesora koji se koriste u server i desktop sustavima su strojevi za raunanje. Kako

    bi neto mogao izraunati CPU treba:

    Da ima mogudnost preuzimanja i dekodiranja instrukcija iz memorije

    Jedinicu izvravanja koja zapravo provodi raunanja (ALU, FPU, ..)

    Neki vrstu konteksta izvravanja (registri)

    Samo sa tim dijelovima CPU bi bio jako spor zbog slijededih efekata:

    Memorijske latencije: Dohvatanje podataka iz glavne memorije je veoma vremenski

    rastrona operacija. Ako neka izvrna jedinica treba podatke za kalkulacije ona eka sve dok

    podaci ne budu dostupni, a to ekanje moe potencijalno biti jako dugo.

    Sub-optimalni programski tok: Nain na koji program koristi izvrne jedinice CPU-a moe biti

    neefikasno i ostaviti neku izvrnu jedinicu u stanju ekanja.

    CPU sadri nekoliko dodatnih kompleksnih podsustava kako bi prevladao ove probleme i ubrzao

    performanse:

  • Out of order izvravanje: Instrukcije su reorganizirane kako bi bolje iskoristili jedinicu

    izvravanja na CPU-u. Na primjer cjelobrojna aritmetika (ALU) i floating point operacije (FPU)

    se izvravaju paralelno. Nakon izvravanja izvorni red treba biti sauvan bududi je ova

    optimizacija transparentna programu.

    Predikcija grananja: Kada se program grana CPU ne zna koja je iduda instrukcija i koji podatak

    je potreban sve dok se ne izvri uvjet grananja. Na taj nain CPU bi bio osuen na ekanje dok

    se ne dohvate instrukcije i podaci nakon grananja. Kako bi limitirao utjecaj i udar grananja na

    performanse CPU pokuava predvidjeti koja grana de nastaviti sa izvoenjem te preuzeti

    instrukcije i podatke za to grananje. Ako je predikcija ispravna zastoj u cjevovodu se

    izbjegava, ako je pogrean izvravanje programa se zaustavlja i ne moe se nastaviti sve dok

    zahtijevani podatak i instrukcija nije moguda.

    Memorijski pre-dohvat : Na osnovi karakteristika programa CPU preuzima podatke iz glavne

    memorije koji bi bili potrebni u slijededim instrukcijama.

    Ke hijerarhija : Kako bi reducirali memorijsku latenciju CPU koristi nekoliko tipova ke

    memorije.

    Svrha svih ovih optimizacija je da unaprijedi performanse za jednu struju ili tok instrukcija. Bududi

    CPU tradicionalno izvrava jedan program/proces u trenutku vremena dugo su vremena jedini

    zahtjevi za optimiziranje bili fokusirani na performanse jednog toka izvravanja.

    No CPU arhitektura se optimizira na nain da se viestruki programi izvode koristedi vremensko

    multipleksiranje. Primjerice posjeduju jedinicu koja de upravljati memorijom i prenositi podatke u

    meuspremnik i tako efikasno implementirati virtualnu memoriju. Tijekom zadnjih godina dodavan

    je hardver koji rukuje sa drugom instrukcijskom strujom (drugi Fetch & Decode blok). Ako se jedna

    instrukcijska struja zaustavi zbog pogreno predvienog grananja druga struja moe hraniti jedinicu

    izvravanja to rezultira sveukupno boljem iskoritenju svih jedinca izvravanja.

    Ovaj pristup je prvo uveo Intel sa Netburst arhitekturom (Pentium 4 HT) i zvao se HyperThreding.

    Netburst arhitektura je posjedovala dugi cjevovod i zbog toga su zastoji u cjevovodu uvelike

    degradirali performanse. HyperThread je smanjio negativni utjecaj zastoja , ali je zahtijevao podrku

    od operacijskog sustava pa je to donekle limitiralo korisnost ove optimizacije. Niagara arhitektura

    (UlstraSparc T1 ) Sun mikrosustava je ak i unaprijedila ovu ideju. Automatski upravlja sa 4 dretve/niti

    po jezgri i svaki ciklus izvrava instrukcije razliitih dretve. Ovo ispreplitanje uva/skriva memorijsku

    latenciju. Kasnije verzije arhitekture su i povedale broj konkurentnih dretvi za 8 ili 16.

    3.2. Fokus nije na performansama zasnovanim na jednom instrukcijskom toku

    Zadade u raunalnoj grafici obino nude dobar potencijal za paralelizaciju. Mnoge operacije se

    trebaju uiniti na svim pikselima slike ili na svim vrhovima scene. Ove operacije se uobiajeno

    raunaju vie ili manje nezavisno od drugih . Sa hardverske toke gledita ova vrsta opteredenja se

    moe distribuirati na vie jezgri i ne zahtjeva striktne sekvencijalne operacije kao to rade mnogi CPU

    algoritmi.

  • Kako bi postigli veliki broj jezgri ove jezgre moraju biti jednostavne. Na taj nain se reduciraju na

    apsolutni minimum :

    Instrukcija fetch i decode (preuzmi i dekodiraj)

    Jedinica izvravanja

    Kontekst izvravanja

    Efektivno smo otklonili svu logiku koja ubrzava jedan tok performansi, ali smo

    dobili mogudnost da stavimo vie jezgri na ip. Ovo povedava potencijalnu

    raunalnu mod nae arhitekture. Na primjer, sada moemo staviti 16

    jednostavnih jezgri koje zajedno obrauju i procesiraju 16 instrukcijskih tokova

    paralelno.

    3.3. Dijeljenje instrukcija izmeu struja /tokova

    Neka imamo program koji izvrava istu instrukciju nad

    pikselima. Ista instrukcija se moe primijeniti na veoma

    veliku koliinu podataka. Kad bi imali sliku dimenzija

    1280*1024, program bi se izvrao 1.310.720 puta jer

    imamo upravo toliko piksela. U naem trenutnom 16

    jezgrenom dizajnu ove piksele demo procesirati u

    blokovima po 16 niti.

    Vedina od ovih 1.310.720 instrukcijskih tokova rade

    uglavnom isto. Izvravaju istu instrukciju na razliitim

    podacima. Ovo daje priliku za drugu optimizaciju

    arhitekture: single instruction multiple data (SIMD)

  • procesiranje. Umjesto jedne decode and fetch jedinice po instrukcijskom toku mi demo ponovno

    upotrijebiti decode and fetch za nekoliko instrukcijskih tokova. Sa ovom optimizacijom naa nova

    SIMD procesorska jezgra sadri jednu decode $ fetch jedinicu i vedi broj izvrnih jedinica, svaka sa

    pripadajudim kontekstom izvravanja. Na primjer, SIMD procesna jezgra iroka osam jedinica

    sadrava jednu decode & fetch jedinicu , 8 izvrnih jedinica i 8 konteksta izvravanja.

    Ako svih 8 izvrnih jedinica dijele istu instrukciju sve dobro funkcionira i mi dobijamo 8 puta bolje

    performanse bez velikog organizacijskog overhead-a. Ali ako dijeljena instrukcijska struja sadri

    instrukcije grananja moe se dogoditi da svih 8 izvrnih jedinica ne nastavi sa istom instrukcijom.

    Instrukcija grananja kao i uvjetni skok nastavlja upravljanje tokovima programa na razliitim

    pozicijama ovisno o uvjetima run-time (vrijednost varijable ili koordinata piksela). Sa takvom

    instrukcijom moe se dogoditi da odreena izvrna jedinica jedne SIMD jezgre treba nastaviti

    izvravanje na drugoj poziciji nego ostatak izvrnih jedinica. U tom sluaju moramo izvriti instrukcije

    za obje grane. Izvrne jedinice koje nisu u trenutno izvrnom grananju trebaju ignorirati ove

    instrukcije.

    Koliina grananja u dijeljenim instrukcijskim strujama ograniava korisnost SIMD optimizacije. to se

    pojavi vie grananja unutar iste SIMD jezgre to su nie SIMD performanse.

    Ako auriramo nau 16 jezgrenu arhitekturu tada mi dobivamo 16 SIMD procesnih jezgri, te svaka

    ima mogudnosti da izvodi istu operaciju na 8 podatkovnih tokova. U optimalnom sluaju mi moemo

    procesirati 16*8=128 programa paralelno. Moe se primijetiti da smo u mogudnosti rukovati jednim

    jedinstvenim instrukcijskim tokom po SIMD jezgri bez gubitka performansi bududi su SIMD jezgre

    neovisne od drugih. Ako ipak kontrola toka unutar jedne SIMD jezgre varira mi gubimo performanse

    bududi SIMD jezgra mora izvriti instrukcije svake grane sekvencijalno.

    3.4. Utjecaj SIMD programskog modela

    Postoji dva razliita naina za prezentiranje SIMD arhitekture programerima:

    Eksplicitno koritenjem vektorskih instrukcija

    Implicitno predstavljajudi svaku izvrnu jedinicu kao dretvu

    Oba pristupa se esto koriste i imaju svoje prednosti. EksplicitnI SIMD je najedi na CPU u formi SSE

    instrukcija. Jedna instrukcija moe operirati na 4 floating point vrijednosti jednostruke preciznosti.

    Na GPU ovaj eksplicitni pristup se koristi preko INTEL Larrabee tehnologije sa 16 komponentnim

    vektorima. Veliki nedostatak ovoga je to nije transparentan za programere . Dok pie izvorni kod

    programer mora koristiti vektorske podatkovne tipove i instrukcije ili uopde nede modi iskoristiti

    izvrne jedinice osim standardno jedne.

    Implicitni SIMD s druge strane je transparentan za programera. Iako je ovo obino prikladnije

    opasnost lei u iluziji da svaka dretva moe slijediti svoju kontrolu toka. Programer mora biti svjestan

    karakteristike arhitekture, ali ne mora eksplicitno koristiti vektorske podatkovne tipove i instrukcije.

  • 3.5. Ispreplitanje tokova u skrivanju(maskiranju) latencije

    U normalnim grafikim raunalnim aplikacijama se mora obraditi veliki broj nezavisnih objekata

    (pikseli ,vrhovi,). Sa tisudama ili milijunima instrukcijskih tokova koji su nam na raspolaganju

    moemo koristiti konkurentnost tih tokova za maskiranje memorijske latencije. Ako jedan

    instrukcijski tok ne moe nastaviti zbog nedostatka podataka prespajamo se na drugi mogudi

    instrukcijski tok. Ako se i ta struja zaustavi prespajamo se na tredi tok i tako dalje. Sa dovoljno

    velikim brojem instrukcijskih tokova izvrna jedinica se moe u potpunosti iskoristiti sve dok se

    podatak za prvi zaustavljeni instrukcijski tok ne oslobodi.

    Zavrni instrukcijski tok se moe proizvoljno dugo izvravati (ovisno koliko esto se zaustavi), ali kada

    gledamo sve tokove zajedno sveukupna propusnost se maksimizira.

    Prebacivanje izmeu instrukcijskih tokova mora biti veoma brzo kako bi se ovo efektivno

    implementiralo. Ovo ostvarujemo tako da uvamo vie od jednog izvrnog konteksta u izvrnoj

    jedinici. Umjesto mijenjanja (swapanja) registara pri svakoj promjeni konteksta jednostavno

    sauvamo sve kontekste izvravanja u registrima. Ovo zahtjeva veliku koliinu registara, ali zato

    omoguduje instantno prebacivanje izmeu instrukcijskih tokova.

    Ova arhitektura ima zanimljive posljedice: Jednostavni

    program zahtjeva samo mali izvrni kontekst (malo registara)

    za operiranje. Zbog toga se vie ovih izvrnih konteksta moe

    uvati na ipu te instrukcijski tokovi mogu biti bolje

    isprepleteni , veoma efektivno sakrivajudi memorijsku

    latenciju. Vie kompleksnih programa zahtjeva vedi kontekst

    izvravanja (vie registara) za operiranje sa umanjenim

    ispreplitanjem. Obino optimizacije dovode do kompleksnijih

    programa i zbog toga u nekim sluajevima jednostavni

    programi se mogu bre izvoditi kada govorimo o memorijskom

    pristupa nego oni optimiziraniji.

    3.6. Memorijski pristup

    Moderni CPU koriste sofisticiranu hijerarhiju kea da rijee veliku latenciju prema glavnoj radnoj

    memoriji. Ako je traeni podataka prisutan u jednom od keeva moe mu se pristupiti relativno brzo.

    Sam CPU sadri ke i brine se za prebacivanje izmeu kea i glavne memorije. Bududi programi

    uobiajeno pokazuju jednu vrstu memorijske lokalnosti u ponaanju (paternima) ovi su keevi za

  • vedinu zadada dosta efikasni. Hardver se brine za ove optimizacije pa su one transparente za

    programere. Za aplikacije visokih performansi moe biti korisno poznavanje semantike ke hijerarhije

    osobito kada se radi o viestrukim dretvama bududi one mogu traiti da se keevi sinkroniziraju to

    moe naruiti performanse(negativan uinak).

    GPU s druge strane obino uopde ne operira sa glavnom memorijom. GPU je najede spojena sa

    vlastitom off chip memorijom koja se koristi za teksture. Veliina ove memorije varira, ali povijesno je

    uvijek bila za etvrtinu ili polovinu manja od veliine glavne memorije. Prije nego GPU moe zapoeti

    sa radom prethodno se podaci moraju premjestiti na grafiku memoriju. Brzina ove operacije ovisi i

    od konekcije izmeu glavne memorije i grafike kartice. Zbog toga uvelike varira, ali ugrubo da

    dobijemo neku perspektivu je oko 5GiByte/s.

    GPU programi koji rade na grafikoj memoriji su bili relativno kratki u ranim danima.

    Nije bilo lokalnosti u pristupu memorijskim lokacijama osim kod pristupa teksturama. Ako jedan

    piksel prikazuje teksturni podatak na specifinoj poziciji velike su anse da i slijededi piksel takoer

    prikazuje teksturni podatak blizu iste te lokacije. GPU ne prua sofisticiranu ke hijerarhiju te umjesto

    toga nudi veliki bandwidth prema grafikoj memoriji i prua teksturni ke. Teksturni keevi u osnovi

    predstavljaju overlay na specifine blokove u grafikoj memoriji. Pristup takvom memorijskom bloku

    se keira sa specijalnim instrukcijama (teksturni dohvati). Ovo reducira latenciju kada se radi sa

    teksturnim podacima.

    Memorijski pristup koji se ne odvija kroz teksturne keeve pati od potpune latencije i kanjenja.

    U tom sluaju moramo se osloniti na ispreplitanje instrukcijskog toka. Memorijska sabirnica GPU-a je

    organizirana na nain da efikasno rukuje sa velikim skupom i blokovima podataka. Uobiajeno je blok

    dovoljno velik da nahrani sve izvrne jedinice SIMD jezgre sa jednom float vrijednosti ( najedi tip

    podataka u raunalnoj grafici danas). Ako program pristupa memoriji na nain da sve izvrne

    jedinice u SIMD jezgri mogu dobiti zahtijevane podatke sa jednim masivnim transferom onda

    moemo iskoristiti cijeli memorijski pojas (bandwidth) grafike memorije. Trenutno je to oko

    150GiByte/s. Meutim ako program pokazuje vie nasumini /random memorijski pristup ovi

    memorijski transferi se ne mogu efikasno koristiti.

    Kako bi prevaziao ova ogranienje u memorijskom pristupu vedina GPU programskih API-ja pruaju

    jednu vrstu lokalne ip memorije (uobiajeno registri). Program moe koristiti ovu memoriju kao

    runo upravljani ke. Naime podaci se prethodno prebace i alju iz grafike memorije prema ip

    lokalnoj memoriji i ta se memorija koristi u svim daljnjim kalkulacijama. Ovo limitira utjecaj latencije

    grafike memorije na jedan pristup u trenutku startanja programa. Ipak memorija na ipu nije velika,

    uobiajeno je rije o najvie nekoliko KiByte po SIMD jezgri.

    Nedostatak ovog pristupa je da se ip-lokalna memorija uobiajeno koristi da se uvaju dodatni

    konteksti izvravanja isprepletenih instrukcijskih tokova. to se vie ip lokalne memorije koristi kao

    runo upravljani ke to se manje instrukcijskih tokova moe ispreplitati da se sakrije memorijska

    latencija. Zbog ovoga programer mora balansirati koritenje ip lokalne memorije.

  • 3.7. Usporedba izmeu CPU i GPU arhitekture

    CPU i GPU arhitekture dijele isti osnovni model izvravanja. Posjeduju preuzmi i dohvati instrukciju,

    koju trebaju izvriti te koriste neki kontekst izvravanja ili registre. Ali GPU arhitektura se razlikuje

    od CPU arhitekture u trima kljunim konceptima koje smo objasnili:

    Fokus nije na performansama jednog toka instrukcija

    Dijeljenje instrukcija izmeu tokova (SIMD)

    Ispreplitanje tokova kako bi se sakrila latencija

    Ovi koncepti su proizili iz posebnih uvjeta raunalne grafike domene. Ipak neke od ideja iza ovih

    koncepata se mogu pronadi dananjim modernim CPU-ima.

    MMX i kasniji SEE instrukcijski set su takoer temeljeni na SIMD (single instruction multiple data).

    SEE doputa simultani rad na vie integer ili floating point vrijednosti jednostruke preciznosti. Ove

    instrukcije su dodane kako bi se omogudilo bre procesiranje video i audio podataka. U tom sluaju

    sirovi volumen multimedijskih podataka je forsirao nove optimizacije na CPU arhitekturi.

    Ipak vedina CPU programa ne koriste ove optimizacije kao standardne postavke. Obino se

    programski jezici fokusiraju na dobro poznate razvojne paradigme. Vektorizacija ili podatkovni

    paralelizam nije tako dobro poznata paradigma i obino se usvaja kada je zaista potrebna jer dodaje

    kompleksnost programu. Zbog toga vedina softvera ne koriste stil SIMD instrukcija ak i kada bi

    problemska domena imala od toga koristi. To osobito do izraaja onda kada se vrijeme razvoja

    smatra veoma skupim, a performanse nisu toliko bitne da bi opravdale optimizacije.

    Ovo SIMD instrukcijama na CPU-u daje prizvuk i karakteristiku dodatne mogudnosti ili add-on.

    SIMD arhitektura na GPU je usvojene iz nude i potrebe inae bi sa tako velikom koliinom podataka

    bilo jako teko rukovati. Takoer, izbjegava se dodavanje fetch i decode jedinice na svaku izvrnu

    jedinicu. Ovo uva prostor i ini ip kompaktnijim i jeftinijim za proizvodnju (vie ipova po

    silikonskom waferu).

    Iz sasvim drugih motiva CPU i GGPU arhitektura se pomakla u istom pravcu to se tie SIMD

    optimizacije.

    Druga slinosti izmeu CPU i GPU je trenutni razvojni trend prema viejezgrenim i mnogo jezgrenim

    CPU-ima. Zbog fizikih ogranienja (brzine svjetlosti, curenje napona u veoma malim krugovima) CPU

    ne moe vie povedavati performanse jednog instrukcijskog toka. CPU frekvencija i brzina instrukcija

    po sekundi se ne mogu povedavati u nedogled. Povedana potronja struje na visokim frekvencijama

    oteduje krugove i zahtjeva skupo hlaenje. Netburts arhitektura (Pentium 4) je dizajnirana za visoke

    frekvencije do 10 Ghz. Ipak zbog ekscesivne pretjerane potronje struje je limitirana na frekvencije

    izmeu 3 i 4 Ghz. Frekvencije do 7 Ghz su se mogle dobiti, ali u veoma specijalnim uvjetima hlaenja

    za veoma kratak period (do 1 minute, nakon toga CPU pregori)

    Ovo nije ostavilo CPU proizvoaima izbora nego da skaliraju horizontalno. Dodavanjem dodatnih

    jezgri teoretski se multipliciraju performanse. Kako bi se iskoristile performanse svih jezgri za jedan

  • zadatak program mora koordinirati nekoliko instrukcijskih tokova na razliitim jezgrama. Ipak ovo

    obino zahtjeva da se dretve sinkroniziraju u razliitim prigodama to ini programiranje i debagiranje

    teim.

    Kako bi se integriralo sve vie i vie jezgri na jedan CPU, brzina i kompleksnost jedne jezgre se

    uobiajeno reducira. Zbog toga stari jedno jezgreni procesori obino ima bolje performanse za jedan

    instrukcijski tok nego moderni quad core CPU. Ovo je slino ideji koritenoj u GPU arhitekturi: mnogo

    jednostavnih procesnih jezgri je efikasnije nego jedna velika jezgra. Iako su CPU jezgre mnogo

    kompleksnije nego GPU jezgre, s vremenom se mogu razviti u jednostavnije sklopove kako bi

    omogudile bolje skaliranje. GPU jezgre s druge strane mogu evoluirati u kompleksnije kako bi bile

    vie user friendly prema programerima. Ideja horizontalnog skaliranja je prisutna na obje

    arhitekture.

  • 4. CUDA

    Kao odgovor na trend koritenja grafikih procesora u opdenite svrhe raunanja, Nvidia je

    pokrenula CUDA-u, arhitekturu i pripadajudi API visoke razine kako bi razvijateljima omogudila to

    jednostavniji rad u poznatom razvojnom okruenju. CUDA (eng. Compute Unified Device

    Architecture) je arhitektura grafikih kartica za paralelno raunanje koja omogudava pristup

    grafikom sklopovlju i razvoj programske potpore za GPU pomodu programskog jezika C te radi na

    svim Nvidijinim grafikim karticama od serije G8X .

    CUDA uvodi neke mogudnosti koje joj daju prednost nad dotadanjim GPGPU arhitekturama . CUDA

    omogudava raspreno itanje memorije tj. itanje iz proizvoljnih adresa u grafikoj memoriji, uvodi

    potpunu potporu za operacije nad cjelobrojnim podacima i nad bitovima te implementira napredno

    upravljanje dretvama na grafikom ureaju.

    Dretvama koje se izvode na GPU omogudava pristup dijeljenoj memoriji koja moe posluiti kao

    priruna memorija ime se ostvaruje veda propusnost nego bi to bilo mogude koristedi dohvatanje

    podataka iz globalne memorije, a podran je i mehanizam sinkronizacije dretvi.

    4.1. Sklopovska implemenatcija

    Temelj arhitekture Tesla je podesivo polje viedretvenih viestrukih procesora toka (eng. streaming

    multiprocesor, skradeno SM) *5+. Na slici (Slika 4) prikazan je GPU s 14 SM-ova. Svaki SM sadri 8

    manjih jezgri procesora toka (eng. streaming processor, skradeno SP), dvije jedinice za posebne

    funkcije (eng. special function unit, skradeno SFU) te viedretvenu instrukcijsku jedinicu (eng.

    multithreaded instruction unit, skradeno MT IU).

    SM procesori takoer imaju skup 32-bitnih registara po svakom SP procesoru, dijeljenu memoriju

    (eng. shared memory) te prirunu memoriju za konstante i prirunu memoriju za teksture iz kojih

    moe samo itati.

  • SP jedinice mogu obavljati osnovne aritmetike i logike operacije nad cjelobrojnim brojevima te

    brojevima s pominim zarezom dok SFU jedinice slue za izvoenje nekih sloenijih operacija kao to

    su trigonometrijske funkcije i logaritmi. MT IU se brine o dohvatanju i izvravanju instrukcija

    pojedinih skupina dretvi. SM procesori grupirani su u parove pri emu svaki par ima svoju jedinicu za

    uzorkovanje tekstura (eng. texture unit) koja teksture iz glavne memorije dohvada preko L1

    prirune memorije ime se ubrzava itanje tekstura. SM procesori takoer mogu itati i pisati u

    glavnu DRAM memoriju, ali je pristup istoj mnogo sporiji od pristupa dijeljenoj memoriji i registrima.

    Kada raunalo domadin eli dodijeliti neki posao grafikom procesoru, prvo se prenesu potrebni

    podaci i instrukcije za njihovu obradu iz glavne memorije na raunalu u memoriju na grafikom

    ureaju. Nakon to CPU pokrene izvravanje, na GPU se istovremeno pokrede veliki broj dretvi, a

    jedinica za raspodjelu posla na CWD (eng. compute work distribution) na GPU ih dinamiki

    rasporeuje po dostupnim SM procesorima.

    Svaka od istovremeno pokrenutih dretvi na SM procesoru ima rezervirane vlastite registre u kojima

    se pohranjuje trenutni kontekst izvravanja pa je prebacivanje izvravanja s jedne dretve na drugu

    vrlo brzo jer nema zamijene konteksta kao to je to sluaj kod CPU-a.

    Osim toga, jedan SM moe koristedi sve SP jezgre istovremeno izvriti odreenu instrukciju nad

    cijelom skupinom dretvi (veliine 8 u ovom sluaju).

    Mogu se primijetiti oite slinosti izmeu arhitekture SM procesora i SIMD paradigme, meutim

    Nvidia ovakav pristup naziva SIMT (eng. single instruction, multiple thread) jer SIMD model

    proiruje naprednim upravljanjem dretvama o emu de vie rijei biti u sljededem poglavlju.

  • 4.2. Logika organizacija

    Kako bi se pojednostavio problem istovremenog izvravanja velikog broja dretvi, CUDA uvodi

    poseban nain hijerarhijskog grupiranja dretvi koji olakava raspodjelu cjelokupnog posla unutar

    GPU-a.

    Dretve su prije svega rasporeene u blokove. Blok je skup dretvi ija je veliina odreena s jednom

    dvije ili tri dimenzije te unutar kojeg se dretve mogu sinkronizirati i meusobno komunicirati

    pomodu dijeljene memorije. Dretve unutar pojedinog bloka uvijek se izvode na istom SM procesoru,

    ali jednom SM procesoru moe biti dodijeljeno vie blokova.

    Blokovi su rasporeeni u dvodimenzionalnu reetku (eng. grid) koja predstavlja logiku raspodjelu

    jednog zadatka odreenog jezgrenom funkcijom. To znai da sve dretve u blokovima unutar jedne

    reetke izvravaju istu jezgrenu funkciju. Dretve iz razliitih blokova ne mogu meusobno

    komunicirati niti se uskladiti pri izvravanju.

    Programer mora organizirati posao po blokovima i reetkama te u glavnom programu odrediti

    njihove dimenzije pri pokretanju jezgrene funkcije. GPU zatim instancira jezgrenu funkciju na

    reetku paralelnih blokova dretvi. Svaka dretva unutar bloka izvrava instancu jezgrene funkcije te

    ima svoj ID koji oznaava njezinu poziciju u bloku. Blok takoer ima svoj ID unutar reetke.

    Ovakva organizacija dretvi omogudava prilagodljivu raspodjelu poslova na GPU. Na primjer ako

    podijelimo reetku na 8 blokova, grafiki ureaj s dvije jezgre moe svakoj dodijeliti 4 bloka, dok bi

    grafiki ureaj s 4 jezgre svakoj mogao dodijeliti 2 bloka. Programer dakle treba samo logiki

    organizirati posao, dok de stvarnu raspodjelu posla po jezgrama ovisno o dostupnim sredstvima

    obaviti GPU, preciznije CWD jedinica

    Paralelno izvravanje i upravljanje dretvama obavlja se automatski. Stvaranjem dretvi, vremenskim

    upravljanjem i prekidom izvravanja rukovodi CUDA sustav izravno na sklopovlju i programer se o

    tome ne mora brinuti.

  • 4.3. Memorijski model

    Tijekom izvravanja, dretve mogu pristupiti razliitim memorijskim prostorima. Svaka dretva na

    raspolaganju ima odreeni broj registara, lokalnu memoriju, dijeljenu memoriju bloka te pristup

    memoriji za konstante, memoriji za teksture i globalnoj memoriji. Lokalna memorija koristi se za

    pomodne varijable koje ne stanu u registre dretve. Dijeljena memorija bloka vidljiva je svim dretvama

    u bloku i obino ima mnogo manje vrijeme kanjenja (eng. latency) od globalne memorije pa se

    koristi kao priruna memorija bloka te moe posluiti za ubrzavanje izvravanja i uinkovitu

    komunikaciju meu dretvama bloka. Dretve u pojedinom bloku mogu se sinkronizirati pozivom

    ugraenih funkcija za sinkronizaciju ime se osigurava da nijedna dretva nede nastaviti s

    izvravanjem dok sve dretve nisu dole do sinkronizacijske granice. Sinkronizacija je neophodna pri

    koritenju dijeljene memorije. Nakon prolaza sinkronizacijske granice, sve dretve mogu u dijeljenoj

    memoriji bloka vidjeti memorijske zapise ostalih dretvi bloka koje su napravljene prije sinkronizacije

    i na taj nain mogu meusobno komunicirati. Globalna memorija zajednika je dretvama svih

    blokova u reetki i koristi se za pribavljanje ulaznih podataka i zapisivanje krajnjih rezultata.

    Memorijski prostor za konstante, teksture te lokalna i globalna memorija fiziki se nalaze u DRAM

    memoriji na grafikom ureaju, ali konstantama i teksturama se pristupa preko prirune memorije te

    se tako ubrzava njihovo dohvadanje. Dretve koje se izvode na GPU iz memorija za konstante i

    teksture mogu samo itati dok u ostale memorije mogu i pisati.

    Raunalo domadin moe pomodu programskog suelja itati i pisati u globalnu memoriju te u

    memoriju za konstante i teksture.

  • Multiprocesri imaju veliki broj 32-bitnih registara: 8k za ureaje raunalne sposobnosti 1.0 i

    1.1 16k za ureaje raunalne sposobnosti 1.2 i 1.3 i 32k za ureaje raunalne sposobnosti 2.0

    ili vie. U tablici se nalazi opis razliitih vrste memorije dostupne na GPU.

    Registri Registri su najbra memorija, sa pristupom bez ikakve latencije na svakom ciklusu takta, kao i na regularnom CPU. Registri dretve se ne mogu dijeliti sa drugim dretvama.

    Dijeljena memorija Dijeljena memorija je usporediva sa L1 ke memorijom na CPU. Lei blizu multiprocesora i ima veoma kratak pristup vremena. Dijeljena memorija se dijeli meu svim dretvama na zadanom bloku.

    Globalna memorija Globalna memorija lei na ureaju, ali van ipa multiprocesora, tako da je vrijeme pristupa do 100 puta vede nego na dijeljenoj memoriji.

    Lokalna memorija Specifina memorija dretvi gdje se uva globalna memorija. Varijable se uvaju u lokalnoj memoriji dretvi ako kompajler odlui da nema dovoljno registara da uvaju podatke dretvi. Ova memorija je spora, iako se zove lokalna

    Konstantna memorija 64k konstante memorije se uva van ipa multiprocesora i memorija je koja se samo ita. Host kod pie u konstantu memoriju prije lansiranja kernela, a kernel moe itati ovu memoriju. Konstantna memorija je keirana. Svaki multiprocesor moe keirati do 8k konstantne memorije tako da slijedna itanja sa kontantne memorije mogu biti veoma brza. Sve dretve imaju pristup konstantnoj memoriji.

    Teksturna memorija Specijalizirana memorija za povrinsko mapiranje tekstura

  • Primjeri

    Programski primjer 1.

    CUDA zahtjeva od programera da razdvoji kod koji de se izvravati na grafikom procesoru u posebne

    funkcije koje se razlikuju od standardnih funkcija u C-u. Te funkcije nazivamo jezgrene funkcije ili

    kerneli. U slijededem primjeru imamo jednostavni primjer Hello World programa namijenjenog za

    CUDA model.

    #include #include "cuda_runtime.h" #include "device_launch_parameters.h" #define NUM_BLOCKS 4 #define BLOCK_WIDTH 32 __global__ void hello() { printf("Hello world! Ja sam dretva %d u bloku %d\n", threadIdx.x, blockIdx.x); } int main(int argc,char **argv) { // Pokrecemo jezgrenu funkciju hello(); // cudaDeviceSynchronize eka da kernel zavri cudaDeviceSynchronize(); printf("Kraj izvodjenja!\n"); return 0; }

    Nakon definicije konstante imamo funkciju void hello () koja predstavlja jezgrenu funkciju ovog

    programa. Ona se ne razlikuje puno od standardnih funkcija bez povratne vrijednosti osim po

    jezinom konstruktu __global__. Konstrukt __global__predstavlja deklaracijski specifikator preko

    kojeg kompajler zna da je rije o kodu koji de se izvravati na ureaju.

    Druga uoljiva razlika je u samom pozivu jegrene funkcije hello. Uoljivo je da se razlikuje od obinog

    poziva funkcije po trostrukom znaku manje () izmeu kojih se nalazi

    konfiguracija parametara. Parametrima odreujemo koliko dretvi de se pokrenuti te kako de se

    dretve organizirati po blokovima. Prvi parametar oznaava broj blokova, dok drugi odgovara broju

    dretvi. Nakon toga slijede obine zagrade u kojima moemo prenijeti argumente funkcije. U ovom

    sluaju ih nemamo.

    Nakon to kompajliramo program imati demo (4*32) 128 paralelno izvedenih jezgrenih funkcija.

    Svaka dretva ima svoju kopiju kernela kojeg izvrava. Tako da demo imati pri ispisu kernela imati

    ukupno 128 dretvi koji izvravaju funkciju printf. Svaka pokrenuta dretva dobije jedinstveni broj i

  • preko varijable threadIdx taj broj moemo saznati. Na slian nain moemo i doznati u kojem bloku

    se nalazi dretva preko varijable blockIdx jer se svakom bloku dodijeli jedinstvena vrijednost odnosno

    id bloka.

    Varijable threadIdx i blockIdx su strukture tipa dim3. To su C strukture koje imaju 3 lana (x,y,z). Svaki

    lan strukture predstavlja jednu dimenziju tako da dretve kao i blokovi mogu biti jedno, dvo i

    trodimenzionalni. Na taj nain veoma lako moemo pokrenuti primjerice matrice koje su

    dvodimenzionalne. Zadada programera je da organizira dretve po blokovima vodedi rauna da svaki

    blok moe imati maksimalno 512 dretvi za ureaje raunalne mogudnosti ispod 2.0 dok za one

    iznad mogude je i 1024 dretve po bloku.

    U naem sluaju imamo jednostavan sluaj od 32 dretve po jednom bloku tako da ukupno imamo

    128 dretvi koje se izvravaju paralelno. Bududi smo kao parametar kernela proslijedili jednostavne

    cjelobrojne vrijednosti kompajler podrazumijeva da je rije o jednodimenzionalnoj strukturi dim3 te

    stoga imamo samo x lanove za dretve i blokove(threadIdx.x i blockIdx.x).

    Valja napomenuti da se dretve izvravaju paralelno i na ispisu moemo vidjeti da se one ne moraju

    izvriti po redu i ne znamo kojim redoslijedom de se oni izvriti.

    Na slijededoj slici vidimo isjeak ispisa programa.

  • Programski primjer 2.

    U prolom primjeru vidjeli smo kako se izvodi jednostavni program koji ispisuje i javlja jedinstveni

    broje dretve i bloka u kojem se dretva nalazi. Primjedujemo da nije uraen nikakav proraun

    odnosno program nije obradio nikakve ulazne podatke.

    Tipian program za GPU operacije izgleda ovako. U kodu koji izvrava standardni procesor prethodno

    se moraju alocirati odreeni podaci na grafikom procesoru. Nakon toga se ti podaci kopiraju iz

    radne memorije standardnog opde-namjenskog procesora u memoriju na grafikom procesoru. Tek

    kada se podaci prebace i pohrane u memoriji GPU-a grafiki procesor moe izvriti zadani proraun.

    Nakon zavretka rada rezultat se prebacuje iz memorije na grafikom procesoru u radnu memoriju

    raunala. Moe se primijetiti da imamo dva koraka u kojem se miu podaci iz jedne u drugu

    memoriju. Generalno ako de se esto micati podaci s jedne na drugu memoriju ,a obrada nad tim

    podacima relativno mala tada CUDA tehnologija i grafiki procesor nede dodi do izraaja , ak je

    mogude da bude loija izvedba u odnosu na klasinu obradu na CPU.

    CUDA tehnologija najbolje dolazi do izraaja kod aplikacija koje rade malo transfera izmeu memorija

    i kada trebamo izvravati mnogo kalkulacija. Kaemo da imaju visok odnos procesiranja naspram

    komunikacije.

    U slijededem primjeru kojim demo izraunati kvadrate brojeva demo pokazati kako izgleda tipian

    program za GPGPU.

    Zapoeti demo sa main() rutinom, u njoj se nalazi kod koji de se izvravati. Prvo demo postaviti dvije

    konstante kojim demo definirati broj elemenata niza i veliinu memorije u bajtovima za niz.

    Konstante u ARRAY_SIZE demo iskoristiti kod deklarianje niza h_array kojeg demo i inicijalizirati na

    slijededi nain:

    int main(int argc, char ** argv) { const int ARRAY_SIZE = 64; const int ARRAY_BYTES = ARRAY_SIZE * sizeof(float); // generiramo ulazni niz na host-u float h_in[ARRAY_SIZE]; for (int i = 0; i < ARRAY_SIZE; i++) { h_in[i] = float(i); } float h_out[ARRAY_SIZE];

    Ono to je vrijedno napomenuti je konvencija imena. Imena varijabli namijenjenih izvravanju na CPU zapoinju sa h, dok za izvravanje na GPU zapoinju sa d. Deklariramo GPU memorijske pokazivae, te alociramo memoriju na GPU ureaju koritenjem cudaMalloc poziva.

  • // dekariramo GPU pokzaivae na prostor u memoriji float * d_in; float * d_out; // alociramo GPU memoriju cudaMalloc((void**) &d_in, ARRAY_BYTES); cudaMalloc((void**) &d_out, ARRAY_BYTES); // prebacujemo niz na GPU cudaMemcpy(d_in, h_in, ARRAY_BYTES, cudaMemcpyHostToDevice);

    Kako bi se uvjerili da su podaci uistinu na memoriji grafikog procesora, a ne na CPU pogledajmo slijedede dvije linije koda. Koristimo cudaMalloc sa dva argumenta; pokaziva na niz i broj bajtova za alociranje. Poziv cudaMalloc oznaava alokaciju podataka na GPU to je analogno klasinom pozivu malloc za dinamiko alociranje podataka za standardnom C programu. Nakon alokacije moramo kopirati podatke iz niza h_in u niz kojeg smo maloprije alocirali u grafikoj memoriji d_in. To radimo sa pozivom cudaMemcpy koji je slian regularnom pozivu memcpy osim to prima etiri argumenta umjesto tri. Prva tri argumenta su ista kao i regularni C memcpy (odredite, izvor, broj bajtova). etvrti argument govori o smjeru prijenosa gdje imamo tri izbora:

    cudaMemcpyHostToDevice kopira podatke sa host-a na ureaj

    cudaMemcpyDeviceToHost kopira podatke sa ureaja na host

    cudaMemcpyDeviceToDevice kopira podatke sa ureaja na ureaj Nakon svih ovih poziva imamo sve potrebne preduvjete da pokrenemo kernel ili jezgrenu funkciju na GPU. U tu svrhu koristimo operator pokretanja kojeg oznaavamo sa > znakovima. Izmeu trostukih manje i vie znakova ubacujemo konfiguraciju parametara kojom odreujemo koliko dretvi demo pokrenuti te kako demo organizirati dretve po blokovima. U prolom primjeru smo prenijeli jednostavne cjelobrojne vrijednosti kao parametre jezgrene funkcije. Ovaj put demo navesti eksplicitnu konfiguraciju preko dim3 strukture koja je sastavljena od 3 cjelobrojne vrijednosti bez predznaka.

    struct dim3 { unsigned int x, y, z; ... }; dim3 block_size; block_size.x = 128; block_size.y = 1; // configure a two dimensional grid as well dim3 grid_size; dim3 grid_size.x = 1;

    square(d_out, d_in);

  • Ovom linijom koda smo prenijeli u konfiguraciji dvije dim3 strukture grid_size i block_size. One zapravo govore da se na GPU pokrede kernel na mrei blokova sastavljenog od jednog bloka, a drugi parametar block_size predstavlja veliinu bloka po dretvama. Odnosno koliko de biti dretvi u jednom bloku. Moemo primijetiti da smo za y dimenziju strukture block_size dodijelili vrijednost 1 dok za y dimenziju strukture grid_size nismo nita dodjeljivali. Treba istaknuti da se pri samoj deklaraciji dim3 strukture sve vrijednosti odnosno dimenzije (x,y,z) inicijaliziraju na jedan. Tako da je linija koda block_size.y=1 zapravo suvina jer se podrazumijeva. Jezgrena funkcija izgleda ovako:

    __global__ void square(float * d_out, float * d_in){ int i = threadIdx.x; d_out[i] = d_in[i] *d_in[i]; }

    Za razliku od prolog primjera u ovoj jezgrenoj funkciji imamo dva argumenta; pokaziva prema ulaznom i izlaznom nizu. Oba pokazivaa se moraju alocirati na GPU inae de se program sruiti i upravo to spada u najede greke. Bududi je kernel tipa void tj. funkcija ne vrada nikakvu povratnu vriednost onda se izlazni rezultat upisuje u niz d_out koji predstavlja argument jezgrene funkcije. Potomo izvravamo kvadiriranje brojeva u liniji. CPU ima ulogu da pokrene 64 kopija kernela na 64 dretvi. Kao argumenti u jezgrenoj funkciji proslijeuju se pokazivai na nizove d_in i d_out. Za parametre jezgrene funkcije moemo pozvati samo podatke alocirane na GPU memoriji. Kada zavri naa jezgrena funkcija sa proraunom rezultati se nalazi se u nizu d_out odnosno u memoriji na ureaju stoga sa naredbom cudaMemcpy je potrebno u obrnutom smjeru izvriti transfer podataka sa memorije na ureaju na memoriju u host-u.

    cudaMemcpy(h_out, d_out, ARRAY_BYTES, cudaMemcpyDeviceToHost);

    Nakon ovog poziva dobiveni rezultati de se nalaziti u nizu h_out kojeg ispisujemo na izlaz. Nakon to se izvri proraun potrebno je osloboditi alociranu memoriju na memoriju u grafikom procesoru to ostvarujemo sa naredbom cudaFree().

  • #include __global__ void square(float * d_out, float * d_in){ int i = threadIdx.x; d_out[i] = d_in[i] *d_in[i]; } int main(int argc, char ** argv) { const int ARRAY_SIZE = 64; const int ARRAY_BYTES = ARRAY_SIZE * sizeof(float); // generiramo ulazni niz na host-u float h_in[ARRAY_SIZE]; for (int i = 0; i < ARRAY_SIZE; i++) { h_in[i] = float(i); } float h_out[ARRAY_SIZE]; // dekariramo GPU pokzaivae na prostor u memoriji float * d_in; float * d_out; // alociramo GPU memoriju cudaMalloc((void**) &d_in, ARRAY_BYTES); cudaMalloc((void**) &d_out, ARRAY_BYTES); // prebacujemo niz na GPU cudaMemcpy(d_in, h_in, ARRAY_BYTES, cudaMemcpyHostToDevice); // pokreemo kernel square(d_out, d_in); // kopiramo rezultat nazad na CPU cudaMemcpy(h_out, d_out, ARRAY_BYTES, cudaMemcpyDeviceToHost); // printamo rezultirajui niz for (int i =0; i < ARRAY_SIZE; i++) { printf("%f", h_out[i]); printf(((i % 4) != 3) ? "\t" : "\n"); } cudaFree(d_in); cudaFree(d_out); return 0; }

  • Programski primjer 3.

    Svaka dretva ima pristup svojim registrima koji su privatni za ovu dretvu . Dretva moe itati i pisati iz registara u dretvenom bloku imaju takoer pristup neemu to se zove dijeljena memorija. Stoga sve dretve u dretvenom bloku mogu pisati i itati po blokovima dijeljenu memoriju. Ovo je mala koliina memorije koja se nalazi direktno na SM-u. Konano tu je i globalna memorija, svaka dretva u cijelom sustavu u bilo koje vrijeme moe itati i pisati prema globalnoj memoriji. Stoga, dretve u jednom kernelu mogu itati i pisati iz nje, dretve u kasnijem kernelu mogu takoer itati pisati sa nje. Da rekapituliramo svaka dretva ima pristup svoji registrima, ima pristup dijeljenoj memoriji koja je dostupna ostalim dretvama u bloku te ima pristup globalnoj memoriji koja je dostupna svim dretvama. Cpu ima pristup svojoj memoriji koju nazovamo host memorijom ili memorijom domadina. U slijededem programskom primjeru prikazati demo kako programer moe koristiti razliite tipove

    memorije koje prua GPU. Meutim prije toga redi demo neto o pojmu sinkronizacije.

    Sinkronizacija

    Znamo da dretve mogu imati pristup rezultatima jedni drugih. Oni se dijele u globalnoj memoriji. Ovo

    znai da rade zajedno na raunanju, ali postoji problem. to ako dretva pokua itati rezultat prije

    nego druga dretva ima priliku da zapie rezultat ili ga uopde prorauna. Ovo znai da trebamo

    sinkronizaciju. Dretve se trebaju sinkronizirati sa jedno drugim kako bi se izbjegla ova situacija. Ova

    potreba za sinkronizacijom je jedna od najosnovnijih problema u paralelnom raunanju.

    Neka imamo slijededi odsjeak jezgrene funkcije kojim pomjeramo vrijednosti u niz za jedno mjesto u

    lijevo.

    int idx=threadIdx.x; __shared__ int niz[128]; niz[idx]=threadIdx.x; if(idx

  • int idx=threadIdx.x; __shared__ int niz[128]; niz[idx]=threadIdx.x; __syncthreads(); if(idx
  • Pogledajmo sada kernel i kod koji operira sa dijeljenom memorijom.

    __global__ void use_shared_memory_GPU(float *array) { // lokalne varijable, privatne za svaku nit int i, index = threadIdx.x; float average, sum = 0.0f;

    // __shared__ variable su vidljive svim dretvama u bloku i imaju isti ivotni vijek kao i blok dretvi

    __shared__ float sh_arr[128]; // kopiramo podatke iz "array" u globalnoj memoriji u sh_arr u dijeljenoj memoriji. // ovdje, svaka nit je odgovorna za kopiranje jednog elementa sh_arr[index] = array[index]; __syncthreads(); // osigurati da su sva pisanja u dijeljenu memoriju zavrena // sh_arr je popunjena. Pronaimo prosjek svih prijanjih elemenata for (i=0; i average) { array[index] = average; } // slijedei kod NEMA EFEKTA, modificira dijeljenu memoriju, ali rezultirajui modificirani podatak se nikad ne kopira u glovalnu memoriju i nestaje im za vri blok dretvi sh_arr[index] = 3.14; }

  • Sam poziv kernela koji koristi lokalnu, dijeljenu i globalnu memoriju se nalaze su slijededem kodu.

    // Koritenje razliitih memorisjkih prostora u CUDA #include /********************** * koritenje lokalne mmeorije* **********************/ // __device__ ili __global__ funkcija se izvodi na GPU __global__ void use_local_memory_GPU(float in) { float f; // varijabla "f" u lokalnoj memoriji, privatna za svaku nit f = in; // parametar "in" je u lokalnoj memoriji i privatan za svaku nit } /********************** * koristenje globalne memorije * **********************/ // __global__ function izvodi se na GPU i poziva se sa host-a __global__ void use_global_memory_GPU(float *array) { // "array" pokzaivac u globalnu memoriju na ureaju array[threadIdx.x] = 2.0f * (float) threadIdx.x; } /********************** * dijeljena memorija * **********************/ __global__ void use_shared_memory_GPU(float *array) { // lokalne varijable, privatne za svaku nit int i, index = threadIdx.x; float average, sum = 0.0f;

    // __shared__ variable su vidljive svim dretvama u bloku i imaju isti ivotni vijek kao i blok dretvi

    __shared__ float sh_arr[128]; // kopiramo podatke iz "array" u globalnoj memoriji u sh_arr u dijeljenoj memoriji. // ovdje, svaka nit je odgovorna za kopiranje jednog elementa sh_arr[index] = array[index]; __syncthreads(); // osigurati da su sva pisanja u dijeljenu memoriju zavrena // sh_arr je popunjena. Pronaimo prosjek svih prijanjih elemenata for (i=0; i average) { array[index] = average; } // slijedei kod NEMA EFEKTA, modificira dijeljenu memoriju, ali rezultirajui modificirani podatak se nikad ne kopira u glovalnu memoriju i nestaje im za vri blok dretvi sh_arr[index] = 3.14; } int main(int argc, char **argv)

  • { /* * First, call a kernel that shows using local memory */ use_local_memory_GPU(2.0f); /* * Potom, pozivamo kernel koji koristi globalnu memoriju */ float h_arr[128]; // konvencija: h_ variable ive na host float *d_arr; // konvencija: d_ variable ive na device (GPU global mem) // alociramo globalnu memoriju na ureaju, rezultat smjetamo u "d_arr" cudaMalloc((void **) &d_arr, sizeof(float) * 128); // kopiramo podateke iz host memorije "h_arr" u memoriju ureaja "d_arr" cudaMemcpy((void *)d_arr, (void *)h_arr, sizeof(float) * 128, cudaMemcpyHostToDevice); // pozivamo kernel (1 blok od 128 dretvi) use_global_memory_GPU(d_arr); // modificiramo sadraj niza u d_arr // kopiramo modificirani niz nazad na host, prepisujui sadraj od h_arr cudaMemcpy((void *)h_arr, (void *)d_arr, sizeof(float) * 128, cudaMemcpyDeviceToHost); /* * Potom, pozivamo kernel koji koristi dijeljenu memoriju */ // kao i prije proslijedimo pokaziva u globalnu memoriju use_shared_memory_GPU(d_arr); // kopiramo modificirani niz nazad u host cudaMemcpy((void *)h_arr, (void *)d_arr, sizeof(float) * 128, cudaMemcpyHostToDevice); return 0; }

    Imamo 128 elemenata i stoga demo imati i 128 dretvi. Argument jezgrene funkcije je float *array niz.

    Deklariramo par lokalnih varijabli koje su privatne za svaku dretvu.

    Potom deklariamo varijablu koja se nalazi u dijeljenoj memoriji ja konstruktom __shared__. Cijeli

    smisao dijeljene memorije da su vidljive svim varijablama u bloku dretvi. ivotni ciklus je onoliki

    koliko traje obrada.

    Prije toga deklariramo lokalne varijable i, index, average, sum. Potom kopiramo podatke iz globalne

    memorije niza array u dijeljeni niz sh_arr. Naravno potrebno je osigurati da se podaci u cjelosti

    kopiraju prije nego ponemo s njima baratati pa koristimo barijeru u vidu poziva syncthreads().

    Kada smo osigurali da su podaci u nizu sh_arr. Pristupamo raunaju prosjeka svih elemenata niza. U

    tu svrhu koristimo for petlju u rasponu od 0 do vrijednosti broja dretvi.

    Nakon prorauna prosjeka provjeravamo u petlji da li su elementi u nizu vedi od prosjeka . Ako jesu

    onda pripadajudem elementu sa pozicijom indeks pridruujemo vrijednost prosjeka.

    Nakon toga imamo dio koda sh_arr[index]=3.14 . Ovom linijom kodu nita nedemo promijeniti u globalnoj memoriji , a kako je dijeljena memorija ivi koliko ivi i blok dretvi onda de ovaj niz vrijednosti posve ieznuti, a mogude je da de ga i sam kompajler zanemariti.

  • Programski primjer 4.

    Ved smo ranije govorili o probleme sinkronizacije . Ovaj put imamo primjer kada mnogo dretvi ita i

    upisuje u iste memorijske lokacije.

    Ispod pomodne funkcije print_array koje demo koristiti za ispis napisan je kernel increment_naive

    koji sadri kao argument pokaziva na cjelobrojni niz. Svaka dretva de imati svoj broj indeksa i poziciju

    u bloku. Cilj zadatka je da svaka dretva uveda vrijednost elementa niza za jedan. Bududi imamo

    miilijun dretvi, a deset elemenata u nizu zaduiti demo po sto tisuda dretvi za svaki element. Kako bi

    to uradili mnoiti demo po modulu indeks dretve sa veliinom niza.

    injenica da imamo milijun dretvi koje piu u samo 10 elemenata znai da nakon to svaka dretva

    doda jedan odgovarajudi element u niz zavriti demo sa 10 elemenata u nizu koji svi sadre vrijednost

    od 100 000.

    Takoer mjeriti de se vrijeme izvoenja kernela i u tu svrhu definira se struktura GpuTimer.

    Promotrimo kod i ispis rezultata.

    #include #include "gputimer.h" #define NUM_THREADS 1000000 #define ARRAY_SIZE 100 #define BLOCK_WIDTH 1000 void print_array(int *array, int size) { printf("{ "); for (int i = 0; i < size; i++) { printf("%d ", array[i]); } printf("}\n"); } __global__ void increment_naive(int *g) { // koja je ovo dretva? int i = blockIdx.x * blockDim.x + threadIdx.x; // svaka nit inkrementira uzastopne elemente, do veliine ARRAY_SIZE i = i % ARRAY_SIZE; g[i] = g[i] + 1; } __global__ void increment_atomic(int *g) { // koja je ovo dretva? int i = blockIdx.x * blockDim.x + threadIdx.x; // svaka nit inkrementira uzastopne elemente, do veliine ARRAY_SIZE i = i % ARRAY_SIZE; //g[i]++;

  • atomicAdd(& g[i], 1); } int main(int argc,char **argv) { GpuTimer timer; printf("%d pokrenutih dretvi u %d blokova koji pisu %d elemenata niza\n", NUM_THREADS, NUM_THREADS / BLOCK_WIDTH, ARRAY_SIZE); // dekariramo i alociramo memoriju int h_array[ARRAY_SIZE]; const int ARRAY_BYTES = ARRAY_SIZE * sizeof(int); // deklariramo, alociramo, i inicijaliziramo na GPU int * d_array; cudaMalloc((void **) &d_array, ARRAY_BYTES); cudaMemset((void *) d_array, 0, ARRAY_BYTES); // pokreemo jezgru timer.Start(); //naivna i atomina implementacija jezgrene funkcije increment_naive(d_array); // increment_atomic(d_array); timer.Stop(); // kopiramo nazad polje sume iz GPU te ispisujemo cudaMemcpy(h_array, d_array, ARRAY_BYTES, cudaMemcpyDeviceToHost); print_array(h_array, ARRAY_SIZE); printf("Time elapsed = %g ms\n", timer.Elapsed()); // oslobaamo GPU mememoriju i izlazimo cudaFree(d_array); return 0; }

    #ifndef __GPU_TIMER_H__ #define __GPU_TIMER_H__ struct GpuTimer { cudaEvent_t start; cudaEvent_t stop; GpuTimer() { cudaEventCreate(&start); cudaEventCreate(&stop); } ~GpuTimer() { cudaEventDestroy(start); cudaEventDestroy(stop); } void Start() { cudaEventRecord(start, 0);

  • } void Stop() { cudaEventRecord(stop, 0); } float Elapsed() { float elapsed; cudaEventSynchronize(stop); cudaEventElapsedTime(&elapsed, start, stop); return elapsed; } }; #endif /* __GPU_TIMER_H__ */

  • Nakon to pokrenemo nekoliko puta program vidimo da ne dobivamo oekivani rezultat od po

    100000 u svakom elementu niza. Osim toga primijeti se kako je rezultat svaki put drugaiji odnosno

    imamo nedeterministiko izvravanje.

    Uzrok ovakvog ponaanja se nalazi u dijelu kodu koji inkrementira g[i]=g[i]+1. Ova operacija se ne

    odvija u jednom koraku ved zapravo u tri koraka. Prvo se mora proitati vrijednost iz lokacije i , pa

    potom modificirati te na istu lokaciju prepisati rezultat. Potrebno je odreeno vrijeme da svaka

    dretva proita vrijednost, inkrementira je te spremi rezultat. Zbog ovog vremenskog kanjenja mnoge

    dretve koje se simultano izvode de u meuvremenu proitati staru ne-inkrementiranu vrijednost te

    de se jedna te ista vrijednost uzastopice zapisivati. Takoer neke dretve koje kasnije ponu sa

    izvravanjem de prepisati rezultat preko onih ranije zavrenih pa onda nije ni udo to imamo

    nekonzistente rezultate.

    Ovaj problem smo imali ved kod sinkronizacije i moe se rijeiti barijerama. Meutim u ovom sluaju

    demo koristiti atomine operacije. Ideja je slijededa. Bududi je inkrementiranje operacija koja

    ukljuuje vie koraka potrebno je omoguditi da se ova operacija izvede u jednom koraka. Moramo

    osigurati da ova operacija bude atomina. CUDA osigurava atomine memorijske operacije koje

    spadaju u posebne instrukcije koje implementira GPU. Najkoritenije su atomicAdd, atomicMin,

    atomicXor itd. Neka imamo istovjetan kernel kao u prolom primjeru samo je razlika to umjesto

    jednostavnog zbrajanja koristimo funkciju atomicAdd.

    __global__ void increment_atomic(int *g) { // koja je ovo dretva? int i = blockIdx.x * blockDim.x + threadIdx.x; // svaka nit inkrementira uzastopne elemente, do veliine ARRAY_SIZE i = i % ARRAY_SIZE; //g[i]++; atomicAdd(& g[i], 1); }

    Funkcija atomicAdd kao parametar uzima pokaziva na niz te broj koji se dodaje. U naem sluaju je

    rije o broju 1. Kad ponovo pokrenemo program vidimo da dobivamo rezultat koji zapravo i

    oekujemo.

    S druge strane moe se primijetiti da se ove operacije izvravaju sporije nego standardne operacije pa

    ih valja koristiti mudro.

  • Atomine operacije imaju mnoga ogranienja. Prvenstveno rije je o tome da su samo odreene

    operacije i odreeni tipovi podrani. Podrani su samo jednostavni operacije kao oduzmi, dodaj,

    minimum, XOR operacija ili slino.

    Programski primjer 5.

    Iako je matrica kao pravokutno polje brojeva veoma jednostavna ona predstavlja najkorisniji i

    najosnovniji objekt u znanstvenom raunanju. Primjene su mnogobrojne i ukljuuju raunalnu

    grafiku, rjeavanje sustava jednadbi, usporeivanje sekvenci DNA, modeliranje elektrinih krugova

    raunalnih mrea itd. Kao matematiki objekti matrice mogu se dodavati, oduzimati mnoiti i

    ponekad dijeliti. Ovdje demo se interesirati samo za mnoenje.

    Implementacija Multipliciranja matrice

    Sada smo u poziciji pisati kernel koji doputa da se host kod umnoka matrica prebaci na GPU.

    Matricu demo prikazati kao strukturu sastavljenu od tri lana. Prvi lan width predstavlja irinu

    matrice odnosno broj elemenata u jednom retku(broj stupaca). lan height predstavlja visinu matrice

    ili broj elemenata u stupcu (broj redaka). Tredi lan elements de nam posluiti za odreivanje ukupne

    veliina matrice u bajtovima to se lako odredi mnoedi broj elemenata stupca i retka sa veliinom

    float tipa podatka.

    typedef struct { int width; int height; float* elements; } Matrix;

    Sada kada smo definirali strukturu koja de predstavljati matricu u main programu demo se pobrinuti

    za deklaraciju i odreivanje dimenzija matrica s kojima demo izvriti mnoenje. Trebati de nam tri

    matrice. Matrica A, B i rezultirajuda matrica C.

  • int main(int argc, char* argv[]){ Matrix A, B, C;

    printf("Molimo unesite dimenzije matrice A:\nBroj elemenata retka:"); scanf("%d",&A.height); printf("Broj elemenata stupca:"); scanf("%d",&A.width); printf("Molimo unesite dimenzije matrice B:\nBroj elemenata stupca:"); scanf("%d",&B.width); B.height=A.width; A.elements = (float*)malloc(A.width * A.height * sizeof(float)); B.elements = (float*)malloc(B.width * B.height * sizeof(float)); C.height = A.height; C.width = B.width; C.elements = (float*)malloc(C.width * C.height * sizeof(float));

    Nakon toga demo inicijalizirati elemente matrica A i B koristedi jednostavni pseudo-generator rand()

    u rasponu od 0 do 10.

    for(int i = 0; i < A.height; i++) for(int j = 0; j < A.width; j++) A.elements[i*A.width + j] = (float)(rand() % 10); for(int i = 0; i < B.height; i++) for(int j = 0; j < B.width; j++) B.elements[i*B.width + j] = (float)(rand() % 10);

    Ovaj dio koda nam je posebno zanimljiv iz razloga to kod inicijalizacije ne koristimo

    dvodimenzionalne nizove ved se sluimo jednodimenzionalnim nizom elements. Zapisi u radnoj

    memoriji su jednodimenzionalni to moemo iskoristiti kako bi zapisali matricu preko niza brojeva.

    Najdede se koristi row major zapis gdje se u memoriju slijedno upisuju retci matrica.

    Poslije inicijalizacije pokrede se funkcija MatMul sa argumentima matrica A,B i C. U ovoj funkciji se

    definiraju sve predradnje za izvravanje na ureaju.

    MatMul(A, B, C);

    Vratiti demo se kanije na ovu funkciju. Poslije nje uslijedit de ispis rezultata izvravanja matrice A, B i reuzultirajude matrice C. // Ispisujemo dio matrice do 10x10 elemenata for(int i = 0; i < min(10, A.height); i++){ for(int j = 0; j < min(10, A.width); j++) printf("%f ", A.elements[i*A.width + j]); printf("\n"); } printf("\n");

  • for(int i = 0; i < min(10, B.height); i++){ for(int j = 0; j < min(10, B.width); j++) printf("%f ", B.elements[i*B.width + j]); printf("\n"); } printf("\n"); for(int i = 0; i < min(10, C.height); i++){ for(int j = 0; j < min(10, C.width); j++) printf("%f ", C.elements[i*C.width + j]); printf("\n"); } printf("\n"); }

    void MatMul(const Matrix A, const Matrix B, Matrix C)

    Definirati demo matrice d_A, d_B i d_C, te kopirati vrijednosti odgovarajudih matrica koje smo

    prenijeli kao argumente funkcije.

    Nakon toga alociramo memoriju na ureaju na siguran nain. Naime vedina poziva koji se tiu

    alociranja, kopiranja, inicijaliziranja na memoriji ureaja mogu vratiti povratnu vrijednost tipa

    cudaError. Na ovaj nain se moe provjeriti da li je kod navedenih operacija dolo do greki, te koja je

    greka u pitanju.

    void MatMul(const Matrix A, const Matrix B, Matrix C) { // Uitavamo A i V memoriju ureaja Matrix d_A; d_A.width = A.width; d_A.height = A.height; size_t size = A.width * A.height * sizeof(float); cudaError_t err = cudaMalloc(&d_A.elements, size); printf("CUDA malloc A: %s\n",cudaGetErrorString(err)); err = cudaMemcpy(d_A.elements, A.elements, size, cudaMemcpyHostToDevice); printf("Copy A to device: %s\n",cudaGetErrorString(err)); Matrix d_B; d_B.width = B.width; d_B.height = B.height; size = B.width * B.height * sizeof(float); err = cudaMalloc(&d_B.elements, size); printf("CUDA malloc B: %s\n",cudaGetErrorString(err)); err = cudaMemcpy(d_B.elements, B.elements, size, cudaMemcpyHostToDevice); printf("Copy B to device: %s\n",cudaGetErrorString(err)); // Alociramo C u memoriju ureaja Matrix d_C; d_C.width = C.width; d_C.height = C.height; size = C.width * C.height * sizeof(float); err = cudaMalloc(&d_C.elements, size); printf("CUDA malloc C: %s\n",cudaGetErrorString(err));

  • Nakon obavljenih memorijskih transakcija pristupamo konfiguraciji jezgrene funkcije. Koristiti demo

    dvodimenzionalnu konfiguraciju bududi je rije o matrici.

    Definirajmo dim3 strukture.

    // Pozivamo jezgru dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE); dim3 dimGrid((B.width + dimBlock.x - 1) / dimBlock.x, (A.height + dimBlock.y - 1) / dimBlock.y); MatMulKernel(d_A, d_B, d_C); err = cudaThreadSynchronize(); printf("Izvodimo kernel: %s\n", cudaGetErrorString(err)); // itamo C iz memorije ureaja err = cudaMemcpy(C.elements, d_C.elements, size, cudaMemcpyDeviceToHost); printf("Kopiramo C off of device: %s\n",cudaGetErrorString(err)); // Oslobaamo memoriju ureaja cudaFree(d_A.elements); cudaFree(d_B.elements); // cudaFree(d_C.elements); }

    U dimBlock funkciji definiramo broj dretvi po bloku. Imamo dvodimenzionalnu konfiguraciju gdje je

    broj dretvi po x jednak BLOCK_SIZE ,isto kao i po y dimenziji. Nakon to smo ustvrdili veliinu bloka

    potrebno je vidjeti koliko de nam trebati blokova odnosno dimenzije mree. Kako dimenzije mree

    ovisi o dimenzijama samih matrica ne moemo jednostavno odrediti i upisati konstantnu vrijednost.

    Bududi su matrice dvodimenzionalne u dimGrid funkciju za x dimenziju demo imati B.width + dimBlock.x - 1) / dimBlock.x, dok (A.height + dimBlock.y - 1) / dimBlock.y za y

    dimenziju.

    Sada smo u poziciji pozvati jezgrenu funkciju koja je definirana na slijededi nain.

    __global__ void MatMulKernel(Matrix A, Matrix B, Matrix C) { // Svaka nit rauna jedan element od C // akumuliramo rezultate u Cvalue float Cvalue = 0.0; int row = blockIdx.y * blockDim.y + threadIdx.y; int col = blockIdx.x * blockDim.x + threadIdx.x; if(row > A.height || col > B.width) return; for (int e = 0; e < A.width; ++e) Cvalue += (A.elements[row * A.width + e]) * (B.elements[e * B.width + col]); C.elements[row * C.width + col] = Cvalue; }

    Mnoenje matrica radimo na klasian nain tako da mnoimo odgovarajude elemente retka matrice

    A sa elementima stupca matrice B, te ih meusobno zbrojimo. Postupak ponavljamo sa svim retcima

    i stupcima dok ne dobijemo rezultirajudu matricu. Ilustracija je prikazana na slici.

  • Definirati demo varijabla Cvalue koja de posluiti kao akumulator (sumator) produkta elemenata reda

    i stupca stoga je inicijaliziramo na 0.

    Slijedede linije koda pomau dretvi da otkriju redak i kolonu unutar matrice.

    U if grananju se nalazi uvjet po kojem se terminira dretvu ako je indeks tj pozicija u redu ili stupcu

    izvan granica produkta matrice.

    Slijedede dvije linije vrte u petlji elemente retka matrice A i elemente stupca B ( stupac i redak

    matrica su iste veliine) koji su potrebne za raunanje produkta zapisa (redak,stupac) zapis i sume

    ovih produkata akumuliranih u Cvalue varijablu. Matrice A i B se uvaju u globalnoj memoriji u row

    major redoslijedu to znai da je matrica sauvana kao jednodimenzionalno polje sa prvim redom

    kojeg sukcesivno slijedi drugi red itd.

    Jednodimenzionalni zapis u memoriji

    Bududi koristimo sukcesivni linearni zapis ne moemo pristupati elementima na uobiajeni nain (i,j )

    preko matrica gdje i predstavlja redak , a j stupac. Na taj nain pozicija elementa sa vrijednosti 7 bi

  • bila odreena parom i,j kao (2,3) ili u C jeziku kao A*1+*2+. Meutim u jednodimenzinalnom nizu ne

    moemo pristupati elementima na takav nain. Primjedujemo na slici da svaki red ima fiksno

    odreenu veliinu odnosno broj elemenata u retku koja je u naem primjeru odreena sa varijablom

    width. Stoga moemo iskoristiti zapis (i*width +j). Odnosno bududi je u naem sluaju irina retka

    (width) jednaka etiri 4 poziciju elementa sa vrijednosti 7 bi pronali kao (2*4+3) to je jednako 11 u

    jednodimenzionalnom polju. U C jeziku, jer brojanje poinje od nule, bi isto bilo prikazano kao

    A*1*4+2+ to je jednako A*6+.

    Konano, zadnja linija kernela kopira ovaj meusobni produkt retka i stupca u odgovarajude

    elemente matrice C u globalnoj memoriji ureaja.

    Kod:

    #include #include #include #include "cuda_runtime.h" #include "device_launch_parameters.h" // Matrice se uvaju u rednom poretku // M(row, col) = *(M.elements + row * M.width + col) typedef struct { int width; int height; float* elements; int stride; } Matrix; // Veliina bloka #define BLOCK_SIZE 16 __global__ void MatMulKernel(Matrix A, Matrix B, Matrix C); // Mnoenje matrica - Host kod // Dimenzije matrica se pretpostavlja da su umnoci od BLOCK_SIZE void MatMul(const Matrix A, const Matrix B, Matrix C) { // Uitavamo A i V memoriju ureaja Matrix d_A; d_A.width = A.width; d_A.height = A.height; size_t size = A.width * A.height * sizeof(float); cudaError_t err = cudaMalloc(&d_A.elements, size); printf("CUDA malloc A: %s\n",cudaGetErrorString(err)); err = cudaMemcpy(d_A.elements, A.elements, size, cudaMemcpyHostToDevice); printf("Copy A to device: %s\n",cudaGetErrorString(err)); Matrix d_B; d_B.width = B.width; d_B.height = B.height; size = B.width * B.height * sizeof(float); err = cudaMalloc(&d_B.elements, size); printf("CUDA malloc B: %s\n",cudaGetErrorString(err)); err = cudaMemcpy(d_B.elements, B.elements, size, cudaMemcpyHostToDevice); printf("Copy B to device: %s\n",cudaGetErrorString(err));

  • // Alociramo C u memoriju ureaja Matrix d_C; d_C.width = C.width; d_C.height = C.height; size = C.width * C.height * sizeof(float); err = cudaMalloc(&d_C.elements, size); printf("CUDA malloc C: %s\n",cudaGetErrorString(err)); // Pozivamo jezgru dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE); dim3 dimGrid((B.width + dimBlock.x - 1) / dimBlock.x, (A.height + dimBlock.y - 1) / dimBlock.y); MatMulKernel(d_A, d_B, d_C); err = cudaThreadSynchronize(); printf("Izvodimo kernel: %s\n", cudaGetErrorString(err)); // itamo C iz memorije ureaja err = cudaMemcpy(C.elements, d_C.elements, size, cudaMemcpyDeviceToHost); printf("Kopiramo C off of device: %s\n",cudaGetErrorString(err)); // Oslobaamo memoriju ureaja cudaFree(d_A.elements); cudaFree(d_B.elements); // cudaFree(d_C.elements); } // Jezgra umnoka matrica koja se poziva MatMul() __global__ void MatMulKernel(Matrix A, Matrix B, Matrix C) { // Svaka nit rauna jedan element od C // akumuliramo rezultate u Cvalue float Cvalue = 0.0; int row = blockIdx.y * blockDim.y + threadIdx.y; int col = blockIdx.x * blockDim.x + threadIdx.x; if(row > A.height || col > B.width) return; for (int e = 0; e < A.width; ++e) Cvalue += (A.elements[row * A.width + e]) * (B.elements[e * B.width + col]); C.elements[row * C.width + col] = Cvalue; } int main(int argc, char* argv[]){ Matrix A, B, C; int a1, a2, b1, b2; printf("Unesite dimenzije matrice A:\nBroj elemenata retka:"); scanf("%d",&A.height); printf("Broj elemenata stupca: "); scanf("%d",&A.width); A.stride=A.width; printf("Unesite dimenzije matrice B:\nBroj elemenata stupca:"); scanf("%d",&B.width); B.height=A.width; B.stride=B.width; A.elements = (float*)malloc(A.width * A.height * sizeof(float)); B.elements = (float*)malloc(B.width * B.height * sizeof(float)); C.height = A.height; C.width = B.width; C.elements = (float*)malloc(C.width * C.height * sizeof(float)); for(int i = 0; i < A.height; i++) for(int j = 0; j < A.width; j++) A.elements[i*A.width + j] = (float)(rand() % 3);

  • for(int i = 0; i < B.height; i++) for(int j = 0; j < B.width; j++) B.elements[i*B.width + j] = (float)(rand() % 2); MatMul(A, B, C); for(int i = 0; i < min(10, A.height); i++){ for(int j = 0; j < min(10, A.width); j++) printf("%f ", A.elements[i*A.width + j]); printf("\n"); } printf("\n"); for(int i = 0; i < min(10, B.height); i++){ for(int j = 0; j < min(10, B.width); j++) printf("%f ", B.elements[i*B.width + j]); printf("\n"); } printf("\n"); for(int i = 0; i < min(10, C.height); i++){ for(int j = 0; j < min(10, C.width); j++) printf("%f ", C.elements[i*C.width + j]); printf("\n"); } printf("\n"); }

    Literatura

    [1] Hochberg, R.,Matrix Multiplication with CUDA - A basic introductionto the CUDA programming

    model,2012http://www.shodor.org/media/content//petascale/materials/UPModules/matrixMultipli

    cation/moduleDocument.pdf

    [2] The NVIDIA Corporation. The CUDA C Best Practices Guide v4.0. NVIDIA Corporation,2011.

    [3] The NVIDIA Corporation. The CUDA C Programming Guide v4.0. NVIDIA Corporation,2011.

    [4]Zibula, A General Purpose Computation on Graphics Processing Units (GPGPU) using CUDA, 2009

    [5] Blythe, D., Rise of the Graphics Processor, Proceedings of the IEEE, Vol. 96, No. 5, May 2008

    [6] Rumpf M., Graphics Processor Units: New Prospects for Parallel Computing, Numerical Solution of

    Partial Differential Equations on Parallel Computers, 2005.