Rinnakkaisen ohjelmakoodin kirjoittaminen

Käy läpi seuraava lista ennen rinnakkaistamista

  • Valitse numeerisesti vakaa ja tehokas algoritmi
    • Tehoton tai epävakaa rinnakkainen ohjelmakoodi ei ole kenellekkään hyödyksi
  • Etsi ohjelmakoodin ns. "kuumat pisteet" (hotspots)
  • Tunnista rinnakkaistamista rajoittavat tekijät
  • Selvitä voidaanko hotspotit rinnakkaistaa

Onko tehtävä rinnastettavissa?

Rinnakkaistuva tehtävä

  • Laske sisätulo \[ x \cdot y = \sum_{i=1}^{n} x_iy_i, \; x,y \in \mathbb{R}^n \]
  • Joukko \(\{1, 2, \dots, n\}\) voidaan jakaa osajoukkoihin \(M_1, M_2, \dots, M_m\), jolloin \[ x \cdot y = \sum_{j=1}^{m} \underbrace{\left( \sum_{i \in M_j} x_iy_i \right)}_{\text{Rinnakkainen tehtävä}} \]

Havainnekuva sisätulon laskemisesta rinnakkain

Ei-rinnakkaistuva tehtävä

  • Laske Fibonaccin luvut \[ F_1 = 0, \\ F_2 = 1, \\ F_i = F_{i-2} + F_{i-1}, \; 3 \leq i \]
  • Seuraava luku jonossa riippuu kahdesta edellisestä, tehtävä ei rinnakkaistu:

Keskity hotspotteihin

  • Tunnista ohjelmakoodin hotspot'it eli koodirivit, joissa tehdään eniten töitä
  • Keskity hotspot'ien rinnakkaistamiseen, unohda turha virittely
  • Koodirivien määrä \(\neq\) tehtävä työ:

Hotspottien tunnistaminen

  • Hotspottien löytäminen ei ole välttämättä triviaali tehtävä vaan vaatii algoritmin toiminnan syvällistä ymmärrystä
  • Jos ohjelmakoodi on tuntematon, sisäkkäisten silmukoiden etsiminen on hyvä paikka aloittaa. Laske iteraatiomäärät ja arvioi iteraation hinta (kompleksisuusanalyysi):

  • Erilaiset profilointityökalut ovat myöskin hyödyksi

Esimerkkejä kompleksisuusanalyysistä

  • Luokkaa \(\mathcal{O}(1)\) laskentaoperaatiota:
double y = x + 7.0;
  • Luokkaa \(\mathcal{O}(2N) = \mathcal{O}(N)\) laskentaoperaatiota:
for(int i = 0; i < N; i++) 
    y[i] = 4.0 * x[i] + 7.0;
  • Luokkaa \(\mathcal{O}(N(1 + 2M)) = \mathcal{O}(NM)\) laskentaoperaatiota:
for(int i = 0; i < N; i++) {
    x[i] = x[i] + 5.0;
    for(int j = 0; j < M; j++)   
        y[i] =  A[i][j] * x[i] + 7.8;
}

Tehtävän osituksesta

  • Rinnakkaisen ohjelmakoodin toteutuksessa lähdetään liikkeelle tehtävän jakamisesta osiin eli osituksesta (partition, decomposition)
  • Kaksi lähestymistapaa: toiminnallinen ositus (functional decomposition) ja tilaositus (domain decomposition)
  • Myöskin termit tehtävärinnakkaisuus (task parallel) ja datarinnakkaisuus (data parallel) esiintyvät usein

Toiminnallinen ositus / Tehtävärinnakaisuus

  • Laskennalliset tehtävät jaetaan laskentasolmujen kesken

Tilaositus / Datarinnakaisuus

  • Laskennassa käytettävä data jaetaan laskentasolmujen kesken

Tehtävärinnakaisuus vs datarinnakaisuus

  • Tehtävä- ja datarinnakkaisuuden välillä sijaitsee jatkumo
  • Ohjelman eri osat saattavat hyödyntää eri suhteissa tehtävä- ja datarinnakkaisuutta

Esimerkki tehtävä- ja datarinnakkaisuuden yhdistämisestä

Tarvitaanko kommunikointia aina?

  • Jotkut tehtävät voidaan rinnakkaistaa ilman kommunikointia
  • Termiä embarrassingly parallel viljellään paljon tässä yhteydessä
  • Myöskin termi triviaalisti rinnakkaistuva on esiintynyt joissakin lähteissä
  • Esimerkiksi kahden vektorin yhteenlasku \(x+y\), \[ (x + y)_i = x_i + y_i, \; i=1,\dots,n, \; x,y \in \mathbb{R}^n, \] ei vaadi kommunikointia ja on siten triviaalisti rinnakkaistuva:

Milloin kommunikointia tarvitaan?

  • Usein kommunikointi on tarpeellista
  • Esimerkiksi sisätulon laskeminen tarvitsee kommunikointia:

Kommunikoinnin laajuus (scope)

  • Pisteestä pisteeseen -kommunikointi (point-to-point): Kaksi tehtävää kommunikoivat keskenään. Toinen tehtävistä on lähettäjä ja toinen vastaanottaja.

Kollektiivinen kommunikointi

  • Kollektiivinen kommunikointi (collective): Useat tehtävät osallistuvat kommunikointiin

Synkroninen kommunikointi

  • Synkroniseen kommunikointiin liittyy tyypillisesti jonkinlainen kättely (handshaking)
  • Viestin vastaanottaja kuittaa viestin ja lähettäjä saa tiedon onnistuneesta siirrosta
  • Kyseessä on ns. blocking-tyyppinen kommunikointi eli prosessien tulee odottaa kunnes kommunikointi on saatettu loppuun

Esimerkki synkronisesta kommunikoinnista

Asynkroninen kommunikointi

  • Asynkroniseen kommunikointiin ei liity kättelyä
  • Viestin lähettäjä ei odota kuittausta
  • Kyseessä on ns. non-blocking-tyyppinen kommunikointi eli prosessit eivät odota kommunikoinnin loppumista
  • Laskenta ja kommunikointi voivat tapahtua samanaikaisesti
  • Parantaa useimmissa tilanteissa ohjelmakoodin tehokkuutta, mutta on hankalampi toteuttaa

Esimerkki asynkronisesta kommunikoinnista

Synkronointi

  • Synkronointi mahdollistaa rinnakkaisien tehtävien suorituksen koordinoinnin
  • Käytetään tilanteissa, joissa halutaan
    • varmistua siitä, että kaikki tehtävät ovat suorittaneet jonkin toiminnon (esim. ratkaisseet osatehtävän)
    • halutaan suojata jokin resurssi (esim. jaettu muistiresurssi)

Synkronointi “esteen” (barrier) avulla

  • Käytetään tilanteissa, joissa halutaan varmistua siitä, että kaikki tehtävät ovat suorittaneet jonkin toiminnon
  • Yleensä kaikki tehtävät osallistuvat synkronointiin
  • Kaikki tehtävät tekevät töitä kunnes saavuttavat erityisen synkronointipisteen
  • Tehtävät odottavat muita tehtäviä synkronointipisteessä
  • Synkronointiin saattaa liittyä erityisen ohjelmakoodin suorittaminen tai tehtävät saattavat jatkaa suoritustaan normaalisti

Esimerkki synkronoinnista esteen avulla

Synkronointi lukon tai semaforin avulla

  • Käytetään tyypillisesti jonkin resurssin suojaamiseen
  • Synkronointiin osallistuvien tehtävien määrä voi vaihdella
  • Ensimmäinen tehtävä ottaa haltuunsa lukon/semaforin. Vain yksi tehtävä voi omistaa lukon/semaforin kerrallaan
  • Muiden tehtävien tulee odottaa lukon/semaforin vapautumista
  • Lukko vs samafori
    • Semafori asettaa tehtävät jonoon FIFO-periaatteella
    • Lukon tapauksessa estetyt tehtävät yrittävät toistuvasti saada lukon käyttöönsä

Esimerkki kriittisen ohjelmakoodin suojaamisesta semaforin avulla

Synkronointi ja laitteisto

  • Esteen tai lukon/semaforin avulla tapahtuva synkronointi nojaa usein laitteistotason ominaisuuksiin:
    • Erillinen konekielinen synkronointikomento rautatasolla tapahtuvaan synkronointiin (esim. GPU)
    • Erilliset atomiset operaatiot lukkojen ja semaforien turvalliseen käsittelyyn (normaali prosessori)

Synkronointi viestinvälityksen avulla

  • Synkronointi voi tapahtua myös synkronisen viestinvälityksen avulla:
    • Yksi tehtävä (master) odottaa muilta tehtäviltä synkronointipyytöä
    • Muut tehtävät lähettävät synkronointipyynnön masterille ja jäävät odottamaan
    • Master lähettää kuittauksen muille tehtäville saatuaan synkronointipyynnön kaikilta tehtäviltä ja jatkaa suoritustaan
    • Muut tehtävät jatkavat suoritusta saatuaan kuittauksen masterilta

Latenssi ja kaistanleveys

  • Latenssi on aika, joka kuluu minimaalisen viestin (0 tavua) välittämiseen, joko muistin ja prosessorin tai prosessorien välillä. Ilmoitetaan (milli- tai mikro-) sekunteina tai kellojaksoina.
  • Kaistanleveys kertoo paljonko informaatiota voidaan siirtää annetun ajanjakson aikana. Ilmoitetaan usein gigatavuina sekunnissa.
  • \(L\) tavun kommunikoimiseen kuluu siis \[ t = t_{lat} + L/B \] aikayksikköä, jossa \(t_{lat}\) on latenssi ja \(B\) kaistanleveys

Havainnekuva latenssin ja kaistaleveyden erosta

Laskenta vs kaistanleveys vs latenssi

  • Laskenta on verrattain halpaa:
    • Moderni CPU: \(\mathcal{O} (10^{10})\) Flops
    • Moderni GPU: \(\mathcal{O} (10^{12})\) Flops
  • Datan siirtäminen on hieman kalliimpaa:
    • Ethernet: \(\mathcal{O} (10^{8})\) B/s
    • RAM-muisti: \(\mathcal{O} (10^{10})\) B/s
    • GPU:n videomuisti: \(\mathcal{O} (10^{11})\) B/s

Laskenta vs kaistanleveys vs latenssi (jatkuu)

  • Latenssin aiheuttamat kustannukset ovat mittavat:
    • Ethernet \(\mathcal{O} (10^{-3})\) sekuntia
    • RAM-muisti / GPU:n videomuisti: \(\mathcal{O} (10^{-7})\) sekuntia
  • GPU voisi siis (erittäin karkeasti arvioiden)
    • suorittaa \(\mathcal{O}(10)\) laskentaoperaatiota yhtä siirrettyä tavua kohden
    • siirtää \(\mathcal{O}(10^4)\) tavua latenssiajan aikana
    • suorittaa \(\mathcal{O}(10^5)\) laskentaoperaatiota latenssiajan aikana

Laskenta vs kaistanleveys vs latenssi (jatkuu)

  • On tehokkaampaa pakata useampi pieni viesti suuremman viestin sisälle ja tehdä välissä mahdollisimman paljon laskentaa:

Yhteenveto: kommunikointi on kallista

  • Kellojaksoja kuluu datan odottamiseen laskennan sijasta
  • Monimutkaistaa ohjelmakoodia (synkronointi jne)
  • Kommunikointi saattaa estää rinnakkaistamisen käytännössä kokonaan
  • Rajoittaa kääntäjän toimintaa
  • Annetun ajanjakson aikana voidaan suorittaa vain rajoitettu määrä kommunikointia

Datariippuvuudet

  • Ohjelman komentojen välillä on datariippuvuus silloin kun komentojen suoritusjärjestys vaikuttaa lopputulokseen
  • Seuraa tyypillisesti tilanteista, joissa samaa tallennuspaikkaa käyttää useampi tehtävä
  • Datariippuvuuksien ymmärtäminen on tärkeää, koska ne ovat yksi tyypillisimmistä rinnakkaistamisen estäjistä

Esimerkki datariippuvuudesta

  • Laske Fibonaccin luvut \[ F_1 = 0, \\ F_2 = 1, \\ F_i = F_{i-2} + F_{i-1}, \; 3 \leq i \]
  • Seuraava luku jonossa riippuu kahdesta edellisestä, tehtävä ei rinnakkaistu:

Datariipuvuuksien esittäminen graafin avulla

  • Datariippuvuudet voidaan esittää kompaktissa muodossa graafirakenteen (dependency graph) avulla:

Datariipuvuuksien käsitteleminen

  • Tunnista ohjelmakoodin toisistaan riippumattomat osat, ne voidaan käsitellä rinnakkain
  • Datariippuvuuksia sisältävät kohdan vaativat jonkin tyyppistä synkronointia:
    • Hajautetun muistin tapaus: kommunikoi data synkronointipisteissä
    • Jaetun muistin tapaus: synkronoi muistin käyttö tehtävien välillä

Kuorman jakaminen

  • Kuorman jakamisella (load balancing) tarkoitetaan laskentakuorman jakamista tehtävien kesken siten, että kaikki tehtävät työllistyvät
  • Voidaan ajatella “tyhjäkäyntiajan” minimoimisena
  • Esim. tilanne, jossa suuri joukko tehtäviä synkronoi esteen avulla. Tällöin hitain tehtävä määrää tahdin.

Kuorman jakaminen tasan tehtävien kesken

  • Soveltuu tilanteisiin, joissa tehdään paljon samanlaista työtä
  • Taulukko, jonka jokainen alkio käsitellään samalla tavalla
  • Silmukka, jonka jokainen iteraatio on yhtä kallis

Kuorman jakaminen dynaamisesti

  • Joissain tilanteissa tehtävän työn määrä saattaa vaihdella dynaamisesti tai työn jakaminen tasan on muuten mahdotonta/hankalaa
  • Ratkaisuksi saattaa sopia vuorotin (scheduler) ja “työpooli”, josta työ jaetaan dynaamisesti tehtävien kesken

Parallel Overhead

  • Rinnakkaistaminen aiheuttaa usein ylimääräistä työtä eli ns. parallel overhead'ia
  • Pahimmassa tapauksessa parallel overhead'in määrä kasvaa kun rinnakkaisuuden määrää lisätään
  • Parallel overhead'iin voi sisältyä esimerkiksi
    • Tehtävien käynnistys- ja päättymisajat
    • Kommunikointi ja synkronointi
    • Dynaaminen kuorman jakaminen
    • Käytetyn laskenta-alustan piilotetut kustannukset (käyttöjärjestelmä jne)

Havainnekuva parallel overhead'ista

Kuinka suuriin osiin tehtävä kannattaa osittaa?

  • Jotkin tehtävät jakautuvat helposti moneen rinnakkaiseen osatehtävään ja toiset eivät:
    • Lineaarialgebran operaatioissa voidaan usein mennä vektorin komponenttien tasolle
    • Toissa tapauksissa rinnakkainen tehtävä saattaa käsittää gigatavuja dataa
    • Ositus vaikuttaa kommunikoinnin määrään (tästä lisää myöhemmin)
    • Datariippuvuudet

Kuinka suuriin osiin tehtävä kannattaa osittaa? (jatkuu)

  • Myöskin laitteisto vaikuttaa valintoihin
  • Laskentayksiköiden määrällä on suurin vaikutus. Kaikille pitäisi riittää töitä:

  • Laskentasolmun koko (laiha/thin vai paksu/fat) vaikuttaa tehtävän ositukseen. Osatehtävän tulee mahtua laskentasolmun käytössä olevaan muistiin ja resussit tulisi käyttää mahdollisimman tehokkaasti
  • Tarvittavan kommunikoinnin määrä vs kommunikointiresurssit

Kuinka suuriin osiin tehtävä kannattaa osittaa? (jatkuu)

  • Tehtävän osituksen ja laitteiston tulisi vastata toisiaan
    • Usein käytettävä algoritmi pitää vaihtaa toiseen kun laitteisto muuttuu liikaa
  • Ositusta voidaan myöskin tehdä monella eri tasolla
    • Laskentaklusteri / Tietokone / CPU vs GPU / GPU:n sisäinen hierarkia

Hienojakaisuus (Granularity)

  • Laskennan ja kommunikoinnin määrän välinen suhde
  • Suhdeluku voidaan laskea (tähän palataan myöhemmin)
  • Tyypillisesti laskentaa sisältävien jaksojen välissä tapahtuu kommunikointia:
    • Hienojakoinen (fine): Suhteellisen vähän laskentaa kommunikoinin välissä
    • Karkeajakoinen (coarse): Suhteellisen paljon laskentaa kommunikoinnin välissä

Hienojakoisesta rinnakkaisuudesta

  • Suhteellisen vähän laskentaa kommunikoinin välissä
  • Matala laskenta / kommunikointi -suhde

  • Kuorman jakaminen on helpompaa
  • Kommunikointiin liittyvä parallel overhead on usein suuri \(\implies\) rinnakkaisuus ei välttämättä paranna tehokkuutta merkittävästi
  • Liian hieno jako \(\implies\) kommunikoinin ja synkronoinnin hinta on liian suuri

Karkeajakoisesta rinnaisuudesta

  • Suhteellisen paljon laskentaa kommunikoinnin välissä
  • Korkea laskenta / kommunikointi -suhde

  • Kuorman jakaminen saattaa olla hankalaa
  • Vähemmän parallel overhead'ia \(\implies\) Rinnakkaistamisessa on enemmän potentiaalia.

These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.