Mitică Craus Cristian-Mihai Amarandei Bogdan Romanescu
ALGORITMI ŞI LIMBAJE PENTRU CALCULUL PARALEL Indrumar de laborator
Iaşi 2005
Partea I : Algoritmi paraleli Primitive ale calculului paralel.............................................................. 9 Comprimarea (Reducerea) ............................................................... 9 Calculul Prefixelor (Scan)............................................................... 11 Dublarea (scurtcircuitarea) ............................................................ 13 Comunicǎri colective ............................................................................ 15 Difuzarea unu-la-toţi ....................................................................... 15 Comunicarea personalizatǎ unu-la-toţi (scatter).......................... 20 Difuzarea toţi-la-toţi ........................................................................ 22 Comunicarea personalizatǎ toţi-la-toţi .......................................... 26 Sortarea paralelǎ .................................................................................. 29 Sortarea paralelǎ bazata pe metoda numǎrǎrii............................ 29 Sortarea par-impar (Odd-Even-Sort)............................................ 31 Sortarea prin interclasarea de secvenţe bitone ............................. 32 Sortarea rapidǎ pe hipercub........................................................... 37 Calcul matricial .................................................................................... 38 Transpunerea unei matrice............................................................. 38 Inmulţirea a douǎ matrici ............................................................... 40 Sisteme de ecuatii.................................................................................. 44 Drumuri optime în grafuri .................................................................. 47 Comentarii bibliografice ................................................................. 53
Partea a-II-a : Introducere in MPI Standardul MPI .................................................................................... 57 Modelul de execuţie .............................................................................. 58 Tipuri de date în MPI .......................................................................... 59 Comunicarea între procese.................................................................. 60 Comunicǎri unu-la-unu blocante ................................................... 60 Comunicǎri unu-la-unu neblocante ............................................... 64 Comunicǎri colective ............................................................................ 69 Bariera .............................................................................................. 70 Difuzia (Broadcast).......................................................................... 70 Comunicarea personalizatǎ unu-la-toţi (Scatter) ......................... 72 Comunicarea toţi-la-unu - Colectarea datelor (Gather) .............. 73 Operaţii de reducere........................................................................ 76 Calculul prefixelor ........................................................................... 81 Corectitudinea operaţiilor colective............................................... 82 Tipuri de date definite de utilizator ............................................... 86 2
Constructori pentru tipuri de date................................................. 87 Utilizarea tipurilor de date derivate .............................................. 92 Lungimea mesajelor ........................................................................ 93 Topologii pentru procese ..................................................................... 94 Topologii virtuale............................................................................. 95 Instructiuni de instalare, compilare şi execuţie a aplicaţiilor care folosesc biblioteca MPI.................................................................. 103
Partea a-III-a : Exemple de programe MPI ……………………… 109 Bibliografie ..................................................................................... 163
3
4
Prefaţǎ Intenţia autorilor acestei lucrǎri a fost de a oferi studenţilor de la Facultatea de Automaticǎ si Calculatoare ansamblul de informaţii necesare realizǎrii lucrǎrilor de laborator aferente disciplinei „Algoritmi şi Limbaje de Calcul Paralel”. Lucrarea se adreseazǎ însǎ tuturor celor care doresc iniţierea în calculul paralel. Atunci când au fost selectate temele abordate s-a considerat cǎ este cunoscut conţinutul cursului „Algoritmi şi Limbaje de Calcul paralel” sau al cǎrţii ‚Algoritmi pentru prelucrǎri paralale”, Editura „Gh. Asachi”, Iaşi, 2002 , autor Miticǎ Craus. Autorii mulţumesc studenţilor masteranzi Raluca Macovei, Roxana Ungureanu şi Ionuţ Vasiliu, pentru contribuţia lor la realizarea secţiunii „Comunicǎri colective” .
5
6
PARTEA I ALGORITMI PARALELI Autor: Mitică Craus
7
8
Primitive ale calculului paralel - Comprimarea (reducerea) - Calculul prefixelor (scan) - Dublarea (scurtcircuitarea)
Comprimarea (Reducerea) Fie M o mulţime de n = 2m elemente, M = {ai | i = 1,...,n} ⊂ Mr. Mulţimea M urmeazǎ a fi procesatǎ pentru a calcula valoarea a1⊕...⊕an unde ⊕ este o lege de compoziţie asociativǎ definitǎ pe Mr. Mulţimea de referinţǎ Mr poate fi R iar ⊕ poate fi +,min,max, etc. Pentru simplitate se presupune cǎ datele de intrare sunt iniţial memorate într-un tablou unidimensional A[0..n-1] iar n este de forma n = 2d.
Algoritm recursiv de comprimare proc comprim_rec (A,l,l+m-1) begin if (m < 2) then return A[l] else return comprim_rec(A,l,l+m/2-1)⊕ comprim_rec(A,l+m/2,l+m-1)) end
Algoritm iterativ de comprimare Iniţial cele n elemente a1, a2,...,an sunt memorate în locaţiile A[n], A[n+1],...,A[2n-1] ale unui tablou de dimensiune 2n. proc sum(A,⊕) begin for k = m-1 down to 0 do for all j:2k ≤ j ≤ 2k+1-1 par do A[j]=A[2j]⊕A[2j+1]; end
9
Exemplu de comprimare
Complexitatea La terminarea execuţiei algoritmului A[1] = a1⊕...⊕an. Complexitatea timp paralel a algoritmului de comprimare (implementat pe o maşinǎ CREW-PRAM sau pe o ahitecturǎ VLSI de tip arbore binar) este O(logn). Aceasta deoarece adâncimea arborelui de calcul este logn. Numǎrul procesoarelor utilizate este p ≥ n/2.
Reducerea numǎrului de procesoare Se poate observa cǎ în fiecare pas l, l = 0,...,m-1, sunt active [n/( 2l)] procesoare. Aceasta sugereazǎ faptul ca este posibilǎ reducerea numǎrului de procesoare fǎrǎ a afecta complexitatea algoritmului. Presupunând ca p < n/2, se partiţionezǎ mulţimea celor n elemente a1,...,an în p submulţimi astfel: primele p-1 submultimi contin k = n/p elemente, iar ultima conţine n-(p-1)•n/p ( ≤ n/p) elemente. Fiecǎrei submulţimi i se asociazǎ un procesor. Procesorul asociat unei submulţimi S = {ai1,...,aik} rezolvǎ problema determinǎrii valorii ai1⊕...⊕aik în n/p-1 unitǎţi de timp. Acţionând în paralel, cele p procesoare reduc problema iniţialǎ la o problemǎ similarǎ de n dimensiune p, în /p-1 unitǎţi de timp. In continuare, aplicând comprimarea asupra problemei rezultate, se obţine rezultatul final în logp unitǎţi de timp. Rezultǎ un timp total de 10
n/p-1+logp unitǎţi de timp. Dacǎ p = [n/ logn] atunci n/p-1+logp ≅ 2logn-1-loglogn ∈ O(logn).
Calculul Prefixelor (Scan) Tehnica calculǎrii prefixelor dezvoltǎ tehnica comprimǎrii recursive în care parcurgerea arborelui asociat execuţiei se face dinspre frunze spre rǎdǎcinǎ. Algoritmii de calcul a prefixelor parcurg arborele în ambele sensuri. Utilizând notaţiile anterioare, calculul prefixelor constǎ în determinarea valorilor a1, a1⊕a2, … , a1⊕...⊕an unde ⊕ este o operaţie binarǎ asociativǎ. Se presupune cǎ datele de intrare sunt iniţial memorate într-un tablou unidimensional A[0..2n-1] (A[n+i]=ai) iar n este de forma n = 2d.
Algoritmi de calcul al prefixelor Operaţia ⊕ admite element simetric proc calcul_prefixe(A,B,⊕) begin for k = m-1 down to 0 do for all j:2k ≤ j ≤ 2k+1-1 par do A[j]=A[2j]⊕A[2j+1]; B[1]= A[1]; for k = 1 to m do for all j:2k ≤ j ≤ 2k+1-1 par do if bit0(j) = 1 then B[j]=B[[(j-1)/2]]; else B[j]=B[j/2]⊕(-A[j+1]); End
Complexitatea In final, valorile A[n]⊕...⊕A[n+j], j = 0,...,n-1 vor fi memorate în locaţiile B[n+j] ale tabloului B[1..2n-1]. Timpul de execuţie este de O(logn). Numǎrul procesoarelor utilizate este O(n). Reducerea numǎrului procesoarelor se poate face printr-o tehnicǎ asemǎnǎtoare celei descrise la reducere. 11
Exemplu
Operaţia ⊕ nu admite element simetric proc calcul_prefixe_gen(A,B,⊕) begin for k = m-1 down to 0 do for all j:2k ≤ j ≤ 2k+1-1 par do A[j]=A[2j]⊕A[2j+1]; for k = 0 to m do for all j:2k ≤ j ≤ 2k+1-1 par do if j = 2k then B[j]=A[j] else if bit0(j) = 1 then B[j]=B[[(j-1)/2]] else B[j]=B[[(j-2)/2]]⊕A[j]); end 12
Complexitatea Timpul de execuţie = O(logn) Numǎrul de procesoare = O([n/logn])
Aplicaţii ale operaţiei scan la recurenţe Fie recurenţa Xk = Xk-1⊕A[k], k > 1, X1 = A[1] Dacǎ ⊕ este un operator binar asociativ atunci Xk= A[1] ⊕ A[2] ⊕ ... ⊕ A[k] Astfel, recurenţe simple pot fi rezolvate cu algoritmi paraleli pentru calculul prefixelor.
Dublarea (scurtcircuitarea) Fie L o listǎ simplu înlǎnţuitǎ formatǎ din n elemente. Fiecare element k ∈ L are asociat un numar val[k] şi un pointer p[k]. Se presupune cǎ fiecǎrui element k ∈ L îi este asignat un procesor Pk.
Algoritm de scurtcircuitare proc scurtcircuitare(L,value,⊕) begin repeat logn times for all k:k ∈ L par do if p[k] ≠ p[p[k]] then val[k] = val[k]⊕val[p[k]]; p[k] = p[p[k]] end Obs: ⊕ este o operaţie binarǎ oarecare.
Complexitatea Complexitatea timp a algoritmului de scurtcircuitare (implementat pe o maşinǎ CREW-PRAM) este O(logn).
13
Aplicaţii ale scurtcircuitǎrii: Calcul ranguri Un exemplu tipic de utilizare a tehnicii dublǎrii îl constituie calcularea numerelor de ordine ale elementelor unei liste, faţǎ de sfârşitul acesteia. Pentru un element k ∈ L, rang[k] va conţine în final numǎrul i al elementor din lista L care se aflǎ în listǎ dupa elementul k, în ordinea datǎ de pointeri.
Vizualizare
Algoritm Calcul ranguri proc calcul_ranguri(L,rang,+) begin /*Initializarea vectorilor p si rang*/ for all k:k ∈ L par do p[k]=succ(k); /*succ(k)=elementul care urmeaza lui k in lista L*/ if p[k] ≠ k then rang[k]=1 else rang[k]=0 /*Aplicarea tehnicii dublarii*/ scurtcircuitare(L,rang,+); end 14
Comunicǎri colective Comunicǎrile colective pot fi clasificate astfel: - unu-la-toţi: difuzare comunicare personalizatǎ - toţi-la-toţi difuzare comunicare personalizatǎ - toţi-la-unu
Difuzarea unu-la-toţi Un procesor trimite acelaşi mesaj M la toţi ceilalţi:
Difuzarea unu-la-toţi pe un inel Procesoarele sunt numerotate de la 0 la p. Procesorul 0 este emitentul mesajului M de lungime m, unde m este numǎrul componentelor atomice (cuvinte) ale mesajului. Fiecare pas de transmisie este ilustrat printr-o sǎgeatǎ de la sursǎ la destinaţie, punctatǎ şi numerotatǎ. Numǎrul semnificǎ pasul în care este transmis mesajul. Sursa trimite mesajul celor 2 vecini ai sǎi în paşi diferiţi. Fiecare procesor primeşte un mesaj de la unul din vecinii sǎi şi îl trimite mai departe la celǎlalt vecin. Procesul continuǎ sǎ fie activ pânǎ când toate procesoarele au primit o copie a mesajului. Pentru o reţea de tip inel cu p procesoare, difuzia va fi finalizatǎ în [p/2] paşi. Timpul de difuzie este dat de relaţia: Tone_to_all =( ts+twm ) p/2
15
unde ts este timpul necesar iniţializǎrii unui transfer iar tw este timpul necesar transferului unei componente atomice a mesajului M de la un nod la unul din vecinii sǎi.
Difuzarea unu-la-toţi pe o plasǎ toroidalǎ Considerǎm problema difuziei unu-la-toţi pe o plasa toroidalǎ cu p liniii si p coloane. Procesoarele sunt numerotate de la 0 la p. Procesorul 0 este emitentul mesajului. In prima fazǎ de comunicare este executatǎ o difuzie de la sursǎ la cele ( p -1) procesoare de pe aceeaşi linie. Dupǎ ce toate procesoarele de pe linie au primit mesajul, este iniţiatǎ o difuzie unu-la-toţi pe coloana corespunzǎtoare. La sfârşitul fazei a doua, fiecare procesor din plasǎ conţine o copie a mesajului iniţial. Paşii de comunicare sunt prezentaţi în figura de mai jos, unde procesorul 0 este sursa. Paşii 1 şi 2 corespund primei faze, iar paşii 3 şi 4 corespund celei de-a doua faze. Dacǎ dimensiunea mesajului este m, difuzia unu-la-toţi pe linie dureazǎ (ts+twm)[ p /2] (timpul necesar unei difuzii unu-la-toţi pe un inel cu p procesoare). În a doua fazǎ, difuzia unu-la-toţi pe coloane se desfǎşoarǎ în paralel, iar timpul de transmisie este egal cu cel din prima fazǎ. Timpul total de difuzie este: Tone_to_all =2( ts+twm ) [ p /2] Putem folosi o procedurǎ similarǎ pentru difuzia unu-la-toţi pe o plasǎ tridimensionalǎ. In acest caz, liniile de p1/3 procesoare care corespund la cele 3 dimensiuni ale plasei sunt tratate ca inele. Aplicând procedura pentru inel în trei faze, câte una pentru fiecare dimensiune, timpul de difuzie unu-la-toţi este: Tone_to_all =3( ts+twm ) [ p /2]
16
Difuzarea unu-la-toţi pe hipercub Figura de mai jos exemplificǎ difuzia unu-la-toţi pe un hipercub cu 8 procesoare, procesorul 0 fiind sursa mesajului.
Aşa cum se poate observa din figurǎ, procesul are 3 faze de comunicare. Se poate de asemenea observa cǎ ordinea în care sunt alese dimensiunile pentru comunicare nu afecteazǎ rezultatul. In planificarea ce corespunde figurii, comunicarea începe de-a lungul celei mai mari dimensiuni ( ce corespunde bitului cel mai semnificativ al reprezentǎrii binare a etichetei unui procesor) şi continuǎ de-a lungul dimensiunilor mai mici, în paşi consecutivi.
Algoritm de difuzare unu-la-toţi pe un cub d-dimensional In algoritmul prezentat mai jos se considerǎ nodul 0 ca sursǎ a mesajului M difuzat. Procedura se executǎ în paralel pe toate procesoarele. Fiecare procesor îşi cunoaşte propriul identificator my_id. 17
Procedura executǎ p paşi de comunicare, unul pe fiecare dimensiune a hipercubului. Comunicarea se desfǎşoarǎ de la dimensiunea cea mai mare spre cea mai micǎ (desi aceasta ordine nu conteazǎ). Indicele i al buclei indicǎ dimensiunea curentǎ pe care are loc comunicarea. Numai procesoarele ce au valoarea 0 pe cei mai puţin semnificativi i biţi participǎ la comunicarea pe dimensiunea i. De exemplu, pentru hipercubul tridimensional din figura anterioarǎ i are valoarea 2 în prima fazǎ. De aceea numai procesoarele 0 si 4 comunicǎ, având cei mai puţin semnificativi 2 biti 0. In faza urmǎtoare, când i = 1 toate procesoarele cu ultimul bit 0 (0, 2, 4, 6) participǎ la comunicare. Variabila mask este folositǎ pentru determinarea procesoarelor ce iau parte la comunicare într-o iteraţie. Variabila mask are d(=log p) cifre binare care iniţial sunt toate 1. La începutul fiecarei iteraţii, cel mai semnificativ bit diferit de 0 este resetat la 0. Apoi se determinǎ procesoarele ce vor participa în acest pas la comunicare. De exemplu, pentru hipercubul din figura anterioarǎ, mask are iniţial valoarea 111 şi va deveni 011 în timpul iteraţiei corespunzǎtoare lui i = 2. Dintre procesoarele alese pentru comunicare de-a lungul dimensiunii i, procesoarele cu bitul i =0 trimit mesajul, iar cele cu bitul i=1 primesc mesajul. Algoritmul se terminǎ dupǎ ce comunicarea s-a desfǎşurat pe toate dimensiunile. proc ONE_TO_ALL_BC(d, my_id, M) begin mask =2d-1; /*Toate cele d cifre binare ale maştii mask devin 1*/ for i = d-1 downto 0 do mask = mask XOR 2i; /* Bitul i din mask devine 0 */ if (my_id AND mask)=0 then /* Dacǎ ultimele i cifre binare din my_id sunt 0 */ if (my_id AND 2i)=0 then msg_destination = my_id XOR 2i; send M to msg_destination; else msg_source = my_id XOR 2i; receive M from msg_source; end
18
Fiecare din cei logp paşi de comunicare dureazǎ ts+twm pentru transferul unui singur mesaj pe fiecare dimensiune. Astfel, timpul total necesar pentru difuzia unu-la-toţi pe un hipercub cu p procesoare este: Tone_to_all =( ts+twm ) log p Procedura de mai sus funcţioneazǎ corect numai dacǎ nodul 0 este sursa difuziei. Pentru o sursǎ arbitrarǎ trebuie ca procesoarele hipercubului sǎ fie renumerotate cu valoarea rezultatǎ din aplicarea operaţiei XOR etichetei iniţiale şi celei a sursei. Procedura ce funcţioneazǎ corect pentru orice sursǎ din multimea {0, … p-1} este urmǎtoarea: proc GENERAL_ONE_TO_ALL_BC(d, my_id, source, M) begin my_virtual_id = my_id XOR source; mask = 2d – 1; for i = d - 1 downto 0 do /* Bucla externa*/ begin mask = mask XOR 2i; /* Seteaza bitul i al mastii in 0*/ if (my_virtual_id AND mask) = 0 then if (my_virtual_id AND 2i) = 0 then begin virtual_dest = my_virtual_id XOR 2i; send M to (virtual_dest XOR source); /* Converteste virtual_dest in eticheta destinatiei fizice*/ end else begin virtual_source = my_virtual_id XOR 2i; receive M from (virtual_source XOR source); /*Converteste virtual_source in eticheta sursei fizice */ end end end
19
Comunicarea personalizatǎ unu-la-toţi (scatter) Un procesor trimite câte un mesaj Mp la fiecare procesor p.
Comunicarea personalizata unu-la-toţi pe hipercub Comunicarea unu-la-toţi personalizatǎ este diferitǎ de difuzia unu-latoţi deoarece procesorul sursǎ trasmite p mesaje – câte unul pentru fiecare procesor. Spre deosebire de difuzia unu-la-toţi, acest tip de comunicare nu implicǎ multiplicarea mesajelor. Operaţia inversǎ este comunicarea toţi-la-unu (gather), în care un singur procesor colecteazǎ mesaje unice de la toate celelate procesoare. Operatia de gather este diferitǎ de cea de acumulare deoarece nu implicǎ operaţii de combinare sau comprimare (reducere a datelor).
Comunicarea personalizatǎ unu-la-toţi pe diferite arhitecturi este similara cu cea a difuziei unu-la-toţi. Din aceste considerente va fi prezentatǎ detaliat doar comunicarea personalizatǎ unu-la-toţi pe hipercub. Figura de mai jos descrie paşii de comunicare pentru comunicarea personalizatǎ unu-la-toţi pe un hipercub cu 8 procesoare. Iniţial, nodul sursǎ (procesorul 0) conţine mesajele destinate fiecǎrui nod al hipercublui. In figurǎ mesajele sunt definite ca etichete ce indicǎ nodul destinaţie. In prima etapǎ, sursa trimite jumǎtate din mulţimea mesajelor de care dispune unuia dintre vecinii sǎi. În paşi consecutivi, fiecare procesor care dispune de mesaje, transfera jumǎtate din mulţimea mesajelor de care dispune unui vecin care nu a primit încǎ nici un set de mesaje. Vor fi log p paşi de comunicare, corespunzǎtor celor logp dimensiuni ale hipercubului. Modul în care se desfǎşoarǎ comunicǎrile este identic cu cel de la difuzia toţi-la-toti. Numai dimensiunea şi conţinutul mesajelor sunt diferite.
20
Toate muchiile unui hipercub cu p procesoare de-a lungul unei anumite dimensiuni leagǎ douǎ subcuburi cu p/2 procesoare. Dupǎ cum este ilustrat în figurǎ, în fiecare pas de comunicare personalizatǎ, mesajele trec de la un subcub la altul. Din mulţimea mesajelor de care dispune un procesor înainte de începerea comunicǎrii ce corespunde la o anumitǎ dimensiune, jumatate trebuie trimise unui procesor din celǎlalt subcub. In fiecare pas, un procesor pǎstreazǎ jumǎtate din mulţimea mesajelor de care dispune, mesaje necesare procesoarelor din propriul subcub şi trimite cealalta jumǎtate a mulţimii mesajelor cǎtre procesoare din celǎlalt subcub. Timpul în care datele sunt distribuite este: Tone_to_oll_pers = ts log p + tw m ( p/2+p/4+p/8+…+1) = = ts log p + tw m ( p-1)
21
Difuzarea toţi-la-toţi Fiecare procesor trimite un mesaj Mp la toţi ceilalţi. Operaţia inversǎ o reprezintǎ acumularea multinod, în care fiecare procesor este destinaţia unei acumulǎri într-un nod.
O modalitate de a executa o operaţie de difuzare toţi-la-toţi este aceea de a executa p operaţii de difuzare unu-la-toţi. Canalele de comunicaţii pot fi utilizate mai eficient prin concatenarea mesajelor transmise într-un pas de comunicare într-un singur mesaj.
Difuzarea toţi-la-toţi pe un inel In difuzarea unu-la-toţi pe inel, cel mult douǎ cǎi de comunicare sunt active la un moment dat. In cazul difuzǎrii toţi-la-toţi, toate canalele pot fi ocupate simultan deoarece întotdeauna fiecare procesor va avea o informaţie ce trebuie trimisǎ vecinilor sǎi. Fiecare procesor trimite întâi mesajul sǎu unuia dintre vecini. In fiecare pas ulterior, mesajul primit de la un vecin este trimis celuilalt vecin. Timpul necesar difuziei toţi-la-toţi pe un inel este: Tall-to-all=(ts+twm)(p-1)
22
Difuzarea toţi-la-toţi pe o plasǎ bidimensionalǎ Ca şi în cazul difuzǎrii unu-la-toţi, difuzarea toţi-la-toţi pe o plasǎ bidimensionalǎ e bazatǎ pe algoritmul pe inel, liniile şi coloanele fiind tratate ca inele. Comunicarea se desfǎşoarǎ în 2 faze. In prima fazǎ, pe fiecare linie din plasǎ se executǎ o difuzare toţi-la-toţi utilizând procedura pe inel. In aceastǎ fazǎ procesoarele colecteazǎ cele p mesaje de la cele p procesoare corespunzǎtoare unei linii. Fiecare procesor grupeazǎ mesajele într-un singur mesaj. A doua fazǎ reprezintǎ o difuzare toţi-la-toţi pe coloane a mesajelor rezultate în urma grupǎrii. Timpul total necesar comunicarii este: Tall-to-all=2ts( p - 1)+twm(p - 1) 23
proc ALL_TO_ALL_BC_MESH(my_id, my_msg, p, result) begin /*Comunicatie de-a lungul liniilor*/ left = (my_id - 1) mod p; right = (my_id + 1) mod p; result = my_msg; for i = 1 to p -1 do send msg to right; receive msg from left; result = result U msg; end; /* Comunicatie pe coloane*/ up= (my_id - p p) mod p; down= (my_id + p p) mod p; msg = result; for i = 1 to p -1 do send msg to down; receive msg from up; result = result ∪ msg; end; end.
24
Difuzarea toţi-la-toţi pe hipercub Procedura necesitǎ logp paşi. In fiecare pas, comunicarea se desfǎşoarǎ pe câte o dimensiune a hipercubului cu p procesoare. În fiecare pas, perechi de procesoare schimbǎ mesaje şi dubleazǎ dimensiunea mesajului ce va fi transmis în pasul urmǎtor concatenând mesajul sǎu cu cel primit. Figura de mai jos prezintǎ aceşti paşi pentru un hipercub cu 8 noduri, cu canale de comunicaţie bidirecţionale. Dimensiunea mesajului transmis în pasul i este 2(i-1)m. Timpul necesar unei perechi de procesoare sǎ primeascǎ şi sǎ transmitǎ mesaje unul celuilalt este ts+2(i-1)twm. Timpul necesar întregii proceduri este: Tall-to-all=ts log p+twm(p - 1)
Algoritm de difuzare toţi-la-toţi pe un cub d-dimensional proc ALL_TO_ALL_BC_HCUBE(my_id, my_msg, d, result) begin result = my msg; for i=0 to d-1 do partner = my_id XOR 2i; send result to partner; receive msg from partner; result = result U msg; end 25
Comunicarea personalizatǎ toţi-la-toţi Fiecare procesor s trimite câte un mesaj Msd la fiecare procesor d.
Comunicarea personalizatǎ toţi-la-toţi pe un inel Figura de mai jos descrie paşii unei comunicǎri personalizate toţi-la toţi pe un inel de 6 procesoare. Pentru a efectua aceasta operaţie, fiecare procesor trimite p-1 mesaje, fiecare de dimensiune m. In figurǎ aceste mesaje sunt definite de perechile de întregi (i, j), unde i reprezintǎ sursa şi j destinaţia finalǎ a mesajului.
In primul pas, fiecare procesor trimite toate mesajele sale ca un mesaj de dimensiune m(p-1) la unul dintre vecinii sǎi (toate procesoarele trimit în aceeaşi direcţie). Din cele m mesaje primite de un procesor în acest pas, un singur mesaj îi este adresat. De aceea fiecare procesor extrage informaţia ce îi aparţine şi trimite mai departe restul mesajelor (p-2). Acest proces continuǎ. Dimensiunea mesajului transmis scade cu valoarea m în fiecare pas. Intr-un pas, fiecare procesor adaugǎ în lista mesajelor primite un mesaj diferit de cele din lista sa. Deci, în p-1 paşi, fiecare procesor primeşte mesajele trimise de celelalte procesoare. Dimensiunea unui mesaj transmis în faza i pe un inel de procesoare fiind m(p-1), timpul total al acestei operaţii este: Tall_to_all_pers = (ts + ½ twmp)(p-1)
26
In procedura descrisǎ mai sus, toate mesajele au fost trimise în aceeaşi direcţie. Dacǎ jumǎtate din mulţimea mesajelor este trimisǎ întro direcţie şi cealaltǎ jumǎtate este expediatǎ în direcţia opusǎ, valoarea lui tw poate fi redusǎ la jumǎtate.
Comunicarea personalizatǎ toţi-la-toţi pe o plasǎ bidimesionalǎ In cazul comunicǎrii personalizate toţi-la-toţi pe o plasǎ cu p procesoare ( p × p ), fiecare procesor îşi grupeazǎ cele p mesaje în funcţie de coloana procesorului destinaţie. In figura de mai jos este prezentatǎ o plasǎ de 3x3 procesoare, în care fiecare procesor are iniţial 9 mesaje de dimensiune m – câte unul pentru fiecare procesor. Fiecare procesor grupeazǎ mesajele în 3 pachete de câte 3 mesaje fiecare (în general p pachete de câte p mesaje fiecare). Primul pachet conţine mesajele destinate procesoarelor 0, 3, 6; al doilea pachet conţine mesaje pentru procesoarele 1, 4, 7; ultimul pachet conţine mesaje pentru procesoarele 2, 5, 8. Dupa ce mesajele au fost împachetate, comunicarea personalizatǎ toţi-la-toţi este executatǎ independent pe fiecare linie, cu pachete de dimensiune m p . Un pachet conţine mesajele pentru toate cele
p procesoare de pe o coloana.
La sfârşitul celei de-a doua faze, procesorul i are mesajele ({0,i},...,{8,i}), unde 0≤i ≤8. Grupurile de procesoare ce comunicǎ în fiecare fazǎ sunt încercuite cu o linie punctatǎ. 27
Inainte de a doua fazǎ a comunicǎrii, mesajele în fiecare procesor sunt sortate din nou, în funcţie de linia procesorului destinaţie de aceastǎ datǎ. Apoi comunicarea se desfǎşoarǎ similar primei faze, în coloanele plasei. La sfârşitul acestei faze fiecare procesor a primit un mesaj de la toate celelalte procesoare. Timpul total al comunicǎrii personalizate toţila-toţi cu mesaje de dimensiune m pe o retea bidemensionalǎ cu p procesoare este: Tall_to_all_pers = (2ts + ½ twmp)( p -1) Expresia de mai sus nu ia în considerare timpul necesar rearanjǎrii datelor.
Comunicarea personalizatǎ toţi-la-toţi pe un hipercub Paşii de comunicare necesari realizǎrii acestei operaţii pe un hipercub cu p noduri (procesoare) sunt în numǎr de logp. In fiecare pas, comunicarea se desfǎşoarǎ pe câte o dimensiune a hipercubului şi fiecare procesor conţine p mesaje de dimensiune m. In timpul comunicǎrii pe o anumitǎ dimensiune, procesorul trimite p/2 din aceste mesaje. Destinaţiile acestor mesaje sunt procesoarele din celǎlalt subcub. Timpul total de comunicare este: Tall_to_all_pers = (2ts + ½ twmp)log p
28
Sortarea paralelǎ Existǎ o vastǎ literaturǎ având ca subiect probleme de sortare. Aceasta se explicǎ prin faptul cǎ sortarea apare ca substask în soluţiile algoritmice a foarte multe probleme. Problema poate fi enunţatǎ astfel: Date fiind n numere a1,...,an, se doreşte renumerotarea lor astfel încât ai≤aj, ∀i,j∈{1,...,n}: i
Sortarea paralelǎ bazata pe metoda numǎrǎrii Alg. Muller si Preparata, 1975 proc sortare_prin_numarare () begin for all i,j: 1 ≤ i,j ≤ n par do if A[i] ≤ A[j] R[i+n-1,j]=1 else R[i+n-1,j]=0; for all j: 1 ≤ j ≤ n par do sum(R[j],+); P[j] = R[1,j]; for all j: 1 ≤ j ≤ n par do A[P[j]] =A[j]; end Tabloul A de dimensiune n conţine iniţial şirul (ai)1 ≤ ι ≤ n. R este un tablou bidimensional de mǎrime (2n-1)xn, R[j] desemneazǎ coloana j a tabloului R. Poziţia P[j] în care aj trebuie plasat este calculatǎ astfel încât ai
29
Exemplu: 2,6,3,8 i\j
1
2
3
4
1
1
3
2
4
2
1
1
1
2
3
0
2
1
2
4
1
1
1
1
5
0
1
0
1
6
0
1
1
1
7
0
0
0
1
Explicare algoritm Procedura are trei faze: Faza 1: determinarea rangurilor relative R[i+n-1,j]; sunt necesare n2 procesoare CREW-PRAM şi O(1) unitǎţi de timp sau n2/logn procesoare si O(logn) unitǎţi de timp. Faza 2: calcularea poziţiilor P[j]; timpul de execuţie este de O(logn) cu nn/logn procesoare CREW-PRAM . Faza 3: permutarea (plasarea) elementelor aj pe poziţiile corecte; n procesoare, O(1) timp.
Complexitatea timp Algoritmul necesitǎ O(logn) timp si O(n2/logn) procesoare CREWPRAM . Citirile simultane din prima fazǎ pot fi evitate fǎrǎ a afecta complexitatea.
30
Sortarea par-impar (Odd-Even-Sort) Versiune a algoritmului BubbleSort pe un lant de procesoare procedure ODD-EVEN_PAR(n) begin id = eticheta procesorului for i=1to n do if i este impar then if id este impar then compare-exchange_min(id+1) ; else compare-exchange_max(id-1) ; if i este par then if id este par then compare-exchange_min(id+1) ; else compare-exchange_max(id-1) ; endfor end
Exemplu: Sortarea a 8 elemente cu algoritmul Par-Impar
31
Complexitatea O(n) timp cu n processoare CREW-PRAM. Algoritmul este optimal pentru arhitecturã. Fiecare procesor este solicitat O(n) timp. Totuşi, costul nu este optimal: nr.procesoare×timpul paralel = n2, iar timpul pentru cel mai rapid algoritm secvential = O(nlogn).
Sortarea prin interclasarea de secvenţe bitone Operaţia de bazǎ: sortarea unei secvenţe bitone. O secvenţǎ bitonǎ este o secventǎ de elemente cu urmǎtoarele proprietǎţi: 1. Existǎ i a.î. este monoton crescǎtoare şi este monoton descrescǎtoare SAU 2. Existǎ o permutare circularǎ a.î. sǎ fie satisfacutǎ condiţia anterioarǎ.
Exemple de secvenţe bitone <1, 2, 4, 7, 6, 0> : întâi creşte şi apoi descreşte, i = 3. <8, 9, 2, 1, 0, 4> : shift stânga cu 4 poziţii, i = 3.
Proprietǎţi ale secvenţelor bitone
1. 2. 3. 4.
Fie s = o secvenţǎ bitonǎ, s1= şi s2= In secvenţa s1 existǎ bi = min{ai, an/2+i} atfel încât toate elementele din faţa lui bi sunt din secvenţa crescǎtoare şi toate elementele de dupǎ bi sunt din secvenţa descrescǎtoare. Secvenţa s2 are şi ea un punct similar. Secvenţele s1 şi s2 sunt bitone. Fiecare element din s1 este mai mic decât fiecare element din s2.
Esenţa sortǎrii unei secvenţe bitone Problema sortǎrii unei secvenţe bitone de lungime n se reduce la sortarea a douǎ secvenţe bitone de dimensiune n/2.
32
Exemplu de sortare a unei secvenţe bitone
Comparatori pentru sortarea a doua numere
Reţea de sortare R1 de secvenţe bitone (n=16)
33
Conversia unei secvenţe oarecare într-o secvenţa bitonǎ Pentru a sorta o secvenţǎ de n elemente, prin tehnica sortǎrii unei secvenţe bitone, trebuie sǎ dispunem de o secvenţǎ bitonǎ formatǎ din n elemente. 1. Douǎ elemente formeazǎ o secvenţǎ bitonǎ. 2. Orice secvenţǎ nesortatǎ este o concatenare de secvenţe bitone de lungime 2. Ideea de sortare: Combinarea într-o secvenţa mai largǎ pânǎ când se obţine o secvenţǎ bitonǎ de lungime n.
Reţea de comparatori (R2) care transformǎ o secvenţǎ oarecare într-o secvenţǎ bitonǎ
Concluzii Cele douǎ reţele combinate în ordinea (R2,R1) transformǎ o secvenţǎ oarecare într-o secvenţǎ sortatǎ.
Algoritm de sortare paralelǎ bazatǎ pe interclasarea de secvenţe bitone Remarcabil pentru simplitatea sa este algoritmul lui Batcher, 1968, cunoscut sub numele de ,,bitonic-merge-sorting’’. Algoritmul utilizeazǎ paradigma combinǎrii recursive, uşor modificatǎ. Convenţii: 1. A[0:n-1] este tabloul de intrare; 34
2. d este un parametru binar specificând ordinea crescǎtoare (d = 0) sau descrescǎtoare (d = 1) a cheilor de sortare; 3. COMP_EX(x,y;d) desemneazǎ operaţia care aranjeazǎ douǎ numere x şi y în ordine crescǎtoare (d=0) sau descrescǎtoare(d=1).
Algoritmul Batcher de sortare bitonicǎ proc sortare_bitonica(A[i:i+b-1],d) begin if (b = 2) (A[i],A[i+1]) = COMP_EX(A[i],A[i+1];d) else sortare _bitonica (A[i:i+b/2-1],0); sortare _bitonica (A[i+b/2:i+b-1],1); interclasare _bitonica (A[i:i+b-1],d); end
Algoritm de interclasare a douǎ secvenţe bitone proc interclasare_bitonica(A[i:i+b-1],d) begin if b = 2 (A[i],A[i+1]) = COMP_EX(A[i],A[i+1];d) else for all j:0 ≤ j < b/2 par do (A[i+j],A[i+b/2+j]) = COMP_EX(A[i+j],A[i+b/2+j];d); interclasare _bitonica (A[i:i+b/2-1],d); interclasare _bitonica (A[i+b/2:i+b-1],d) end
Implementare pe o maşinǎ CREW-PRAM Interclasarea_bitonicǎ necesitǎ n procesoare şi O(logn) timp, rezultând pentru sortare_bitonica un timp de O(log2n) şi un necesar de O(n) procesoare. Eficienţa algoritmului este, evident, O(1/logn).
Implementare pe o reţea de sortare Reţeaua (R2,R1) implementeazǎ algoritmul lui Batcher. Numǎrul de faze este logn. Fazele sunt compuse din O(logn) paşi.
35
Implementarea algoritmului lui Batcher pe hipercub Cubul binar multidimensional este o arhitecturǎ idealǎ pentru implementarea procedurii sortare_bitonica. Sortarea bitonica constǎ în d faze de combinare (merging): M0,M1,...,Md-1, unde Mi realizeazǎ combinarea perechilor de secvenţe de lungime 2i. Modelul algoritmic este paradigma combinǎrii recursive în varianta execuţiei combinarii înaintea celor douǎ apeluri recursive. Execuţia fazei Mi pe hipercub necesitǎ utilizarea succesivǎ a dimensiunilor Ei,Ei-1,...,E1,E0. Planificare dimensiunilor Planificarea utilizǎrii dimensiunilor pentru o sortare completǎ poate fi reprezentatǎ prin urmatoarea paradigmǎ: Fazǎ Dimensiuni active M0 : E0 E1 E0 M1 : M2 : E2 E1 E0 ............ Ed-1 Ed-2 ... E1 E0 Md-1 : Planificarea dimensiunilor unui hipercub de ordinul 4
Planificarea dimensiunilor corespunde la fazele sortǎrii prin interclasarea de secvenţe bitone.
36
Sortarea rapidǎ pe hipercub Sǎ ne amintim cǎ un hipercub cu d dimensiuni este format din douǎ hipercuburi cu d-1 dimensiuni. Ideea de sortare rapidǎ pe hipercub este de a plasa cele douǎ secvenţe rezultate în urma operaţiei de partiţionare în jurul pivotului a secvenţei de sortat memoratǎ în nodurile hipercubului. Operaţia se repetǎ apoi recursiv pe subcuburi. Selectarea pivotului este problema cheie. Selectarea unui pivot care sǎ partitioneze secvenţa de sortat în douǎ subsecvente de dimensiuni aproximativ egale este dificilǎ.
Algoritmul de sortare rapida pe hipercub proc sortare_rapida_pe_hipercub(B, n) begin id = eticheta procesorului; for i=1 to d do x = pivot; partiţioneazǎ B în B1 şi B2 a.î. B1 ≤ x ≤ B2; if ith bit is 0 then trimite B2 procesorului vecin pe a dimensiunea i; C = subsecvenţa primitǎ de la vecinul dimensiune i; B = B1 U C; else trimite B1 procesorului vecin pe a dimensiunea i; C = subsecvenţa primitǎ de la vecinul dimensiune i; B = B2 U C; Aplicǎ lui B sortarea rapidǎ secvenţialǎ; end
37
Calcul matricial Transpunerea unei matrice Formularea problemei: Datǎ fiind matricea An×n se cere sǎ se calculeze matricea AT[i,j] = A[j,i] pentru toţi i,j=1,2,…,n Obs. Nu se efectuezǎ calcule, ci doar mişcari de elemente. Timpul secvenţial Ts(n) = Θ(n2). Algoritmul implementat pe o maşina EREW PRAM este banal şi are cost Θ(1).
Algoritmul secvenţial standard: proc MAT_TRANSP(A) begin for i = 0 to n-1 do for j = i+1 to n-1 do interchange(A[i,j],A[j,i]) ; end Transpunerea unei matrice 4×4 cu o plasa de 16 de procesoare
Paşii de comunicare
38
Configuraţia finalǎ
Transpunerea unei matrice 8×8 cu o plasa de 16 de procesoare
Paşii de comunicare
Rearanjamente locale
Algoritm recursiv de transpunere a unei matrice 8×8 - Pasii I-II
Divizarea matricei în 4 blocuri Intreschimbarea blocurilor stânga-jos cu dreapta-sus
Divizarea fiecǎrui bloc în sub-blocuri În fiecare bloc: Intreschimbarea sub-blocurilor stânga-jos cu dreapta-sus
39
Algoritm recursiv de transpunere a unei matrice 8×8 - Pasii III-IV
Ultima divizare
Configuraţia finalǎ
Implementare pe un hipercub cu p procesoare Hipercubul este divizat în 4 subcuburi cu p/4 procesoare. Sferturile de matrice (quadranţii) sunt mapate pe cele 4 subcuburi. In fiecare subcub se interschimbǎ blocurile stânga-jos cu dreapta-sus. Se repetǎ procedeul în fiecare subcub.
Complexitatea Recusia se opreşte când dimensiunea blocului este n2/p. Numǎrul de paşi = log4p=(log2p)/2 . Fiecare pas de comunicare se realizeazǎ pe douǎ din dimensiunile hipercubului (2 muchii). Transpunerea localǎ este Θ(n2/p). Rezultǎ urmǎtoarele: - Timpul total este Θ ((n2/p)logp) - Costul = Θ (n2logp) – nu este optimal.
Inmulţirea a douǎ matrici proc MAT_MULT (A,B,C) begin for i = 0 to n-1 do for j = 0 to n-1 do C[i,j] =0 ; for k =0 to n-1 do C[i,j] = C[i,j] +A[i,k]×B[k,j]; end 40
Algoritmul înmulţirii de blocuri proc MAT_MULT (A,B,C) begin for i = 0 to q-1 do for j = 0 to q-1 do Ci,j = [0] ; for k =0 to q do Ci,j = Ci,j +Ai,k×Bk,j; end Matricile A si B sunt divizate în blocuri Ai,k şi respectiv Bk,j , de dimensiuni (n/q)×(n/q).
Implementare standard Arhitectura naturala este arhitectura cu topologie de tip plasǎ. Numǎrul de procesoare p=q2. Fiecare procesor Pi,j dispune în memoria localǎ de blocul Ai,j şi un bloc Bi,j şi calculeaza Ci,j. Pentru a calcula Ci,j procesorul Pi,j are nevoie de Ai,k si Bk,j k=0,…,q-1. Pentru fiecare k, blocurile Ai,k şi Bk,j sunt obţinute printr-o difuzie toţi-la-toţi pe linii şi apoi pe coloane.
Algoritmul lui Cannon Algoritmul lui Cannon este asemǎnǎtor cu cel corespunzǎtor implementǎrii standard dar cu consum mai mic de memorie. Diferenta constǎ în faptul cǎ fiecare bloc este procesat la momente diferite în locuri diferite. Pentru aceasta, blocurile sunt deplasate ciclic. Iniţial blocurile sunt aliniate prin deplasǎri ciclice la stânga (în A) şi în sus (în B). Urmeaza √p-1 paşi de înmulţiri locale şi adunǎri locale de blocuri, urmate de deplasǎri ciclice la stânga (în A) şi în sus (în B).
41
Alinierea iniţialǎ (prima fazǎ)
A si B dupa alinierea iniţialǎ
Poziţiile blocurilor dupǎ a II-a deplasare
42
Poziţiile blocurilor dupǎ prima deplasare
Poziţiile blocurilor dupǎ a III-a deplasare
Complexitatea implementǎrii pe o plasǎ de proecesoare Timpul consumat de algoritmul lui Cannon este acelaşi cu cel din cazul implementarii standard dar consumul de memorie este mult mai mic.
43
Sisteme de ecuatii Sistem standard de ecuatii liniare
Reducerea la forma triunghiularǎ (eliminarea Gaussiana) Algorimul eliminǎrii Gaussiene proc GAUSSIAN ELIMINATION (A, b, y) begin for k = 0 to n-1 do for j = k+1 to n-1 do A[k,j] = A[k,j] / A[k,k]; /* pasul de divizare */ y[k] = b[k] / A[k,k]; A[k,k] = 1; for i = k+1 to n-1 do for j = k +1 to n-1 do /* pasul de eliminare */ A[i,j] = A[i,j] - A[i,k] × A[k,j]; b[i] = b[i] - A[i,k]×y[k]; A[i,k] = 0; end
44
Versiunea paralelǎ proc GAUSSIAN ELIMINATION (A, b, y) begin for k = 0 to n-1 do for j = k+1 to n-1 do A[k,j] = A[k,j] / A[k,k]; /* pasul de divizare */ y[k] = b[k] / A[k,k]; A[k,k] = 1; for i = k+1 to n-1 par do for j = k +1 to n-1 do /* pasul de eliminare */ A[i,j] = A[i,j] - A[i,k] × A[k,j]; b[I] = b[i] - A[i,k]×y[k]; A[i,k] = 0; end
Implementare pe o reţea liniarǎ Cele n procesoare se noteazǎ cu P0,….Pn-1. Fiecare procesor Pi dispune în memoria localǎ de coeficienţii ecuaţiei a i-a (linia i a matricei A a sistemului de ecuaţii plus bi). Timpul consumat în iteraţia k este Θ(n-k-1). Rezulta pentru întregul algoritm timpul Θ(n2). Implementarea pasului de divizare for j = k+1 to n-1 do A[k,j] = A[k,j] / A[k,k]; y[k] = b[k] / A[k,k]; A[k,k] = 1;
45
Difuzia unu-la-toţi a liniei A[k,*]
Implementarea pasului de eliminare for j = k +1 to n-1 do A[i,j] = A[i,j] - A[i,k] × A[k,j]; b[i] = b[i] - A[i,k]×y[k]; A[i,k] = 0;
46
Drumuri optime în grafuri Problema algebricǎ a drumurilor Problema algebricǎ a drumurilor unificǎ strategiile utilizate în rezolvarea a trei clase mari de probleme, fiecare dezvoltatǎ independent, cu algoritmi proprii: determinarea închiderii tranzitive a unei relaţii, determinarea drumurilor minime într-un graf, rezolvarea sistemelor de ecuaţii liniare (metoda Gauss-Jordan).
Algoritmul lui Kucera Algoritmul lui Kucera, dezvoltat pentru o maşinǎ CREW-PRAM, pentru problema drumurilor minime poate fi considerat de bazǎ. Algoritmul utilizeazǎ ca date iniţiale matricea Dn×n a ponderilor muchiilor unui graf G = (V,E), (V = {1,...,n}, E ⊂ V×V). ∀i,j ∈ {1,...,n} : D[i,j] = dij, dacǎ (i,j) ∈ E ∞, dacǎ (i,j) ∉ E unde dij este distanţa dintre nodurile i şi j (dii = 0). Rezultatul execuţiei algoritmului lui Kucera este o matrice Cn×n unde : C[i,j] = 0, dacǎ i = j C[i,j] = min[((i,i1,…,ik,j)) || drum in G] { D[i,i1]+D[i1,i2]+…+D[ik,j]}, dacǎ i ≠ j Pseudocod pentru Algoritmul lui Kucera for all i,j:1 ≤ i,j ≤ n par do C[i,j] = D[i,j]; repeat logn times for all i,j,k: 1 ≤ i,j,k ≤ n par do w[i,k,j] = C[i,k]+C[k,j]; for all i,j: 1 ≤ i,j ≤ n par do C[i,j] = min{C[i,j],mink{w[i,k,j]; k = 1,...,n}}; end
Complexitatea Operaţia min{C[i,j],mink{w[i,k,j]; k = 1,...,n}} poate fi implementatǎ prin procedura sum, ,,⊕” fiind în acest caz operatorul de minimizare. Daca se utilizeazǎ modelul CREW-PRAM, complexitatea timp a acestei operaţii este O(logn) iar numǎrul de procesoare utilizate este n/logn. 47
Rezultǎ pentru algoritm un timp de execuţie T(n)=O(log2n) si un necesar de procesoare de O(n3/logn). Pe o maşinǎ CRCW-PRAM, operaţia de minimizare anterior menţionatǎ se poate efectua în timp constant cu n2 procesoare. Consecinţa este reducerea timpului de execuţie la O(logn), rezultând însǎ o creştere a numǎrului procesoarelor la O(n4).
Definirea problemei algebrice a drumurilor Inlocuind în algoritmul lui Kucera: D cu A (matricea de adiacenţǎ), + cu “şi logic”, min cu “sau logic”, se obtine un algoritm pentru problema închiderii tranzitive. Menţionǎm cǎ pentru un graf G, problema închiderii tranzitive constǎ în determinarea unei matrice Cn×n: C[i,j] = 1, daca exista în G un drum de la i la j 0, altfel Generalizând, problema algebricǎ a drumurilor poate fi definitǎ astfel: Dat fiind un graf ponderat G = (V,E,w), unde w:E→ H, iar (H,⊕,⊗) formeazǎ un inel cu unitate, sǎ se determine matricea Cn×n astfel încât C[i,j] = ⊕ w(p) [p drum de la i la j] k −1
Obs. Daca p = i0i1...ik-1ik atunci w(p) = ⊗ w[il,il+1]. l =0
Implementari sistolice Abordǎrile sistolice ale problemei algebrice a drumurilor sunt foarte numeroase. Între acestea, implementarea algoritmului lui Kleen pe o retea de tip plasǎ, datǎ de Y. Robert si D. Trystram, 1986, este remarcabilǎ. Parametrii algoritmului sunt T = 5n-2 si A ∈ O(n2).
48
Algoritmul sistolic al lui Y. Robert si D. Trystram
Retea sistolicǎ pentru drumuri cu aceeaşi sursǎ Reţeaua lui Y. Robert şi D. Trystram poate fi modificatǎ si adaptatǎ problemei drumurilor de cost minim sau maxim de la un nod n la toate celelalte n-1 noduri ale unui digraf aciclic G = (V,A), V = {1,2,...,n}, E ⊂ V×V. Vom prezenta în continuare o arhitecturǎ sistolicǎ pentru problema drumurilor de cost maxim de la un nod n la toate celelalte n-1 noduri ale unui digraf aciclic G = (V,A). Iniţial, Cn×n este matricea Costn×n a costurilor arcelor digrafului aciclic G. cij este costul arcului (i,j).
49
Algoritmul implementat de reţeaua de mai sus este derivat din algoritmul Warshal-Floyd astfel: proc drumuri_maxime(C,n); begin for k = 1 to n-1 do for j = 1 to n do for i = k+1 to n do C[i,j] = max{C[i,j],C[i,k]+C[k,j]} end; Procedura drumuri_maxime transformǎ elementele matricei C dupǎ formula: Ck[i,j] = max{Ck-1[i,j],Ck-1[i,k]+Ck-1[k,j]} unde Ck-1 respectiv Ck definesc starea matricei C înainte şi dupǎ iteraţia k. Elementele Cn-1[i,j] vor conţine costurile drumurilor maxime de la i la j. În final doar elementele liniei n a matricei C vor ajunge în starea n-1, deci vor conţine valori corespunzatoare drumurilor maxime.
50
Exemplu de aplicare a algoritmului de drumuri maxime într-un digraf aciclic
51
52
Comentarii bibliografice Secţiunea „Primitive ale calculului paralel” are la bază cartea, „Efficient Parallel Algorithms”, Cambridge University Press, 1988, autori A. Gibbons, W. Rytter [1]. Secţiunile „Comunicǎri colective”, “Calcul Matricial” şi “Sisteme de ecuaţii” sunt inspirate din cartea “Introduction to Parallel Computing: Design and Analysis of Algorithms”, Benjamin-Cummings,1994, autori V. Kumar, A. Grama A. Gupta şi G Karypis [2]. Secţiunile „Sortarea paralelǎ” şi „Drumuri optime în grafuri” sunt sinteze realizate pe baza lucrǎrilor [1]-[5]. Unele figuri şi/sau exemple sunt preluate din cǎrţile [1], [2], [5].
53
54
PARTEA a-II-a INTRODUCERE ÎN MPI Autor: Cristian-Mihai Amarandei
55
56
Standardul MPI MPI (Message Passing Interface) reprezintă o bibliotecă pentru programarea paralelă. MPI a fost creat în cadrul grupului MPI Forum între 1993 şi 1994, când a fost elaborat şi standardul 1.2. Acest standard defineşte mai mult de 120 de funcţii care oferă suport pentru: • Comunicǎri point-to-point: Operaţiile de bază în recepţia şi trimiterea mesajelor de către procese cu MPI sunt send şi receive cu variantele de implementare a acestora. • Operaţii colective: Sunt operaţii definite pentru comunicǎri care implică un grup de procese. • Grupuri de procese şi contexte de comunicare • Topologia proceselor: În MPI un grup de procese este o colecţie de n procese în care fiecare proces are atribuit un rang între 0 şi n-1. În multe aplicaţii paralele o atribuire liniară a rangurilor nu reflectă logica comunicǎrii între procese. Adesea procesele sunt aranjate în modele topologice ca de exemplu o plasă cu 2 sau 3 dimensiuni. Într-un caz şi mai general procesele pot fi descrise de un graf şi vom face referire la aranjarea acestor procese într-o “topologie virtuală”. O distincţie clară trebuie fǎcută între o topologie virtuală şi topologia de bază a suportului, hardware-ul propriu-zis. Topologia virtuală poate fi exploatată de sistem în atribuirea proceselor către procesoarele fizice, dacă acest lucru ajută la creşterea performanţelor de comunicare pentru o anumită maşină, dar realizarea acestui lucru nu face obiectul MPI. Descrierea unei topologii virtuale depinde numai de aplicaţie şi este independentă de sistemul de calcul. • Suport pentru limbajele Fortran77 şi C: Aplicaţiile paralele folosind MPI pot fi scrise in limbaje ca Fortran77 şi C. • Obţinerea informaţiilor despre mediu şi managementul acestora: Sunt introduse rutine pentru obţinerea şi setarea unor parametri pentru execuţia programelor corespunzătoare diferitelor implementări ale MPI. Standardul MPI a ajuns la versiunea 2.0 şi oferă în plus suport pentru: • Extinderea operaţiilor colective: În MPI-1 intercomunicarea reprezenta comunicarea între două grupuri de procese disjuncte. În multe funcţii argumentul de comunicare poate fi unul de intercomunicare, excepţie facând operaţiile colective. Acest lucru este implementat în MPI-2 care extinde utilizarea funcţiilor pentru operaţiile colective. 57
•
•
•
•
Crearea proceselor şi managementul dinamic al acestora: Obiectivele principale sunt oferirea posibilitaţii de a crea aplicaţii cu un numar variat de task-uri (task farm) şi cuplarea aplicaţiilor după modelul client/server. One-sided Communication: Ideea de bază este aceea ca un task să aibă acces direct la memoria altui task (asemanător modelului shared memory), ca taskul proprietar al unei zone de memorie să permită accesul şi oferirea unor metode de sincronizare. Parallel File I/O: Mecanismele care oferă operaţii de I/O paralele, permit taskurilor să acceseze fişierele într-un mod controlat dar sunt dependente de arhitectură. MPI-2 defineşte rutine de nivel înalt pentru a furniza un model de programare unic pentru operaţiile de I/O paralele. Suport pentru alte limbaje de programare: Este oferit suportul pentru C++. De asemenea există şi o variantă pentru Java şi o variantă orientată obiect (OOMPI).
Modelul de execuţie Execuţia unui program scris cu apeluri MPI constă în rularea simultană a mai multor procese, definite în mod static (MPI-1) sau dinamic (MPI-2), identice sau diferite şi care comunică colectiv sau de la unul la altul (point-to-point). Procesele comunicante fac parte din acelaşi comunicator, care defineşte domeniul de comunicare. În cadrul domeniului de comunicare fiecare proces are un rang. De fapt comunicatorul asigură un mecanism de identificare a grupurilor de procese şi de protecţie a comunicǎrii. Un comunicator poate fi de două feluri: intracomunicator pentru procese din interiorul aceluiaşi grup şi intercomunicator pentru comunicǎrile dintre grupuri. Există şase apeluri de funcţii mai importante care ar putea fi suficiente pentru scrierea unui program MPI: • • • • • •
58
MPI_Init MPI_Finalize MPI_Comm_size MPI_Comm_rank MPI_Send MPI_Receive
iniţiază un calcul MPI; termină un calcul MPI; determină numărul proceselor; determină identificatorul procesului; trimite un mesaj; primeşte un mesaj.
Observaţie: După apelul MPI_Finalize nu se mai poate apela nici o funcţie MPI. Exemplu: main(int argc, char **argv) { MPI_Init (&argc, &argv); MPI_Comm_size (MPI_COMM_WORLD, count); MPI_Comm_rank (MPI_COMM_WORLD, &myrank); printf (“Eu sunt %d din %d”, myrank, count); MPI_Finalize(); }
Parametrul MPI_COMM_WORLD precizează că toate procesele aparţin aceluiaşi grup şi reprezintă comunicatorul iniţial care este valabil pentru toate procesele aceluiaşi grup. Procesele pot executa instrucţiuni diferite, dar pentru aceasta trebuie să se asigure un mecanism de diferenţiere: Exemplu : main(int argc, char **argv) { MPI_Init (&argc, &argv); ............ MPI_Comm_size (MPI_COMM_WORLD, count); MPI_Comm_rank (MPI_COMM_WORLD, &myrank); if (myrank == 0) master(); else slave(); ............ MPI_Finalize(); }
Tipuri de date în MPI Trebuie observat faptul cǎ de fiecare dată este specificată lungimea mesajului ca numǎr de “intrări”, nu ca număr de bytes. Tipurile de date de bază corespund tipurilor de date de bază ale limbajului folosit. Astfel avem: MPI datatype MPI_CHAR MPI_SHORT MPI_INT MPI_LONG
C datatype signed char signed short int signed int signed long int 59
MPI_UNSIGNED_CHAR MPI_UNSIGNED_SHORT MPI_UNSIGNED MPI_FLOAT MPI_DOUBLE MPI_LONG_DOUBLE MPI_BYTE MPI_PACKED
unsigned char unsigned short int unsigned int float double long double
Tipurile de date MPI_BYTE şi MPI_PACKED nu corespund nici unui tip de date din C.
Comunicarea între procese O mare parte a funcţiilor MPI sunt funcţii pentru comunicare. Comunicǎrile între procese pot fi punct-la-punct (blocante sau neblocante) sau colective.
Comunicǎri unu-la-unu blocante O funcţie de comunicare cu blocare se încheie odată cu trimiterea, respectiv primirea, mesajului chiar dacă operaţia în sine nu s-a terminat. Astfel, la apelul funcţiei MPI_Send execuţia poate continua după revenirea din apel chiar dacă mesajul nu a ajuns încă la destinaţie. Asemănător, se revine din apelul funcţiei MPI_Recv imediat ce mesajul a ajuns în zona de destinaţie şi poate fi citit. Apelul unei funcţiei send: MPI_Send(buf, count, datatype, dest, tag, comm) [IN [IN [IN [IN [IN [IN
buf] count] datatype] dest] tag] comm]
- adresa zonei tampon pentru datele de trimis - numărul datelor de trimis - tipul datelor pentru fiecare element din buffer - identificatorul procesului destinaţie - identificator de mesaj - comunicatorul
Prototipul funcţiei este următorul: int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)
Apelul unei funcţiei receive: MPI_Recv(buf, count, datatype, source, tag, comm, status)
60
[OUT buf] [IN count] [IN datatype] [IN source] [IN tag] [IN comm] [ OUT status]
- adresa zonei de memorie în care este recepţionat mesajul în cazul în care este primit corespunzător parametrilor funcţiei - numărul de elemente care pot fi primite în zona tampon - tipul datelor pentru fiecare element din buffer primit - identificatorul procesului sursă - identificator de mesaj - comunicatorul - variabilă utilă pentru aflarea dimensiunii, identificatorului de mesaj şi a sursei, codurilor de eroare
Prototipul funcţiei este următorul: int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status)
Exemplu: #include "mpi.h" main( int argc, char **argv ) { char message[20]; int myrank; MPI_Status status; MPI_Init( &argc, &argv ); MPI_Comm_rank( MPI_COMM_WORLD, &myrank ); if (myrank == 0) /* codul pentru procesul zero */ { strcpy(message,"Hello"); MPI_Send(message, strlen(message), MPI_CHAR, 1, 99, MPI_COMM_WORLD); } else /* codul pentru procesul 1 */ { MPI_Recv(message, 20, MPI_CHAR, 0, 99,MPI_COMM_WORLD, &status); printf("received :%s:\n", message); } MPI_Finalize(); }
Moduri de realizare a comunicǎrii Există mai multe moduri de realizare a comunicǎrii, indicate în numele funcţiei printr-o literă astfel: B pentru bufferată, S – sincronă şi R pentru ready. 61
O operaţie de tip send în mod buffered poate fi iniţiată indiferent dacă operaţia receive corespunzătoare a fost iniţiată sau nu. În acest mod terminarea nu depinde de apariţia unei operaţii receive. Pentru a termina operaţia poate fi necesar ca mesajul să fie într-un buffer local. În acest scop, bufferul trebuie furnizat de către aplicaţie. Spaţiul (de memorie) ocupat de buffer este eliberat atunci când mesajul este transferat către destinaţie sau când operaţia este anulată. O operaţie de tip send în mod sincron poate fi iniţiată chiar dacă o operaţie receive corespunzătoare a fost sau nu iniţiată. Oricum, operaţia va fi încheiată cu succes numai dacă o operaţie receive corespunzătoare a fost iniţiată şi a început recepţia mesajului trimis prin send. Astfel, încheierea operaţiei send sincrone nu indică numai că bufferul poate fi reutilizat, dar de asemenea indică şi faptul că receptorul a ajuns într-un anumit punct din execuţia sa şi anume faptul că a iniţiat o operaţie receive corespunzătoare. Observaţie: Modul sincron asigură faptul că o comunicare nu se termină la fiecare sfârşit de proces înainte ca ambele procese să ajungă la aceeaşi operaţie de comunicare. În modul sincron operaţia de comunicare este încheiată simultan de către ambele funcţii indiferent care este primul apel. O operaţie de tip send în mod ready poate fi iniţiată numai dacă o operaţie receive a fost deja iniţiată, altfel operaţia este o sursă de erori. Pe unele sisteme acest lucru poate permite înlăturarea operaţiilor de tip hand-shake şi au ca rezultat creşterea performanţelor. De aceea, într-un program scris corect o operaţie ready –send poate fi înlocuită de una standard fără a avea alt efect asupra comportării programului decât performanţa. Apelul funcţiei Bsend: MPI_Bsend (buf, count, datatype, dest, tag, comm) [ [ [ [ [ [
IN IN IN IN IN IN
buf] count] datatype] dest] tag] comm]
-adresa zonei tampon (bufferului) pentru datele de trimis - numărul de elemente de trimis din buffer - tipul datelor pentru fiecare element din buffer - rangul destinaţiei - identificator de mesaj - comunicatorul
Prototipul funcţiei este următorul: int MPI_Bsend (void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)
62
Observaţie: În modul de lucru cu zonă tampon (buffered), trebuie să se asigure spaţiul tampon, prin apelul MPI_buffer_attach(), după care este eliberat cu MPI_buffer_detach(). Apelul funcţiei Ssend: MPI_Ssend (buf, count, datatype, dest, tag, comm) [ [ [ [ [ [
IN IN IN IN IN IN
buf] count] datatype] dest] tag] comm]
- adresa zonei tampon (bufferului) pentru datele de trimis - numărul de elemente de trimis din buffer - tipul datelor pentru fiecare element din buffer - rangul destinaţiei - identificator de mesaj - comunicatorul
Prototipul funcţiei este următorul: int MPI_Ssend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)
Observaţie: În modul sincron, indiferent care este primul apel, operaţia se încheie simultan de către ambele funcţii. Apelul funcţiei Rsend: MPI_Rsend (buf, count, datatype, dest, tag, comm) [ [ [ [ [ [
IN IN IN IN IN IN
buf] count] datatype] dest] tag] comm]
- adresa zonei tampon (bufferului) pentru datele de trimis - numărul de elemente de trimis din buffer - tipul datelor pentru fiecare element din buffer - rangul destinaţiei - identificator de mesaj - comunicatorul
Prototipul funcţiei este următorul: int MPI_Rsend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)
Observaţie: În modul ready apelul receive trebuie să-l preceadă obligatoriu pe cel pentru operaţia send.
63
Comunicǎri unu-la-unu neblocante Un mecanism care adesea conferă performaţe mai bune este utilizarea comunicǎrilor neblocante. Comunicǎrile neblocante pot fi folosite în cele patru moduri ca şi cele blocante: standard, bufferate, sincrone şi ready. Modul de folosire este acelaşi cu cel de la comunicǎrile blocante. Cu excepţia modului ready operaţia send neblocantă poate fi iniţiată chiar dacă o operaţie receive a fost sau nu iniţiată, iar una de tip ready numai dacă a fost iniţiată o operaţie receive. În toate cazurile operaţia send este iniţiată local şi returnează imediat indiferent de starea altor procese. Apelul funcţiei Isend: MPI_Isend(buf, count, datatype, dest, tag, comm, request) [ [ [ [ [ [ [
IN buf] IN count] IN datatype] IN dest] IN tag] IN comm] OUT request]
- adresa zonei tampon (bufferului) pentru datele de trimis - numărul de elemente de trimis din buffer - tipul datelor pentru fiecare element din buffer - rangul destinaţiei - identificator de mesaj - comunicatorul - communication request
Prototipul funcţiei este următorul: int MPI_Isend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request)
Apelul funcţiei Irecv: MPI_Irecv(buf, count, datatype, source, tag, comm, request) [ IN buf] [ [ [ [ [ [
IN count] IN datatype] IN source] IN tag] IN comm] OUT request]
- adresa zonei tampon (bufferului) pentru datele de trimis - numărul de elemente de trimis din buffer - tipul datelor pentru fiecare element din buffer - identificatorul procesului sursă - identificator de mesaj - comunicatorul - communication request
Prototipul funcţiei este următorul:
64
int MPI_Irecv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request *request)
Din apelul de recepţie se revine chiar dacă nu există nici un mesaj primit. Acest mod de lucru poate conduce la erori şi în acest caz se folosesc funcţii de detectare a terminării operaţiei: MPI_Wait() şi MPI_Test(). Exemplu: MPI_Comm_Rank (MPI_COMM_WORLD,&myrank); if (myrank == 0) { int a; MPI_Isend(&a,1,MPI_INT,msgtag,MPI_COMM_WORLD,req1); calcul(); MPI_Wait(req1, status); } else if (myrank == 1) { int b; MPI_Recv(&b,1,MPI_INT,0,mesgtag,MPI_COMM_WORLD ,status); }
Funcţia MPI_Wait() se termină după terminarea operaţiei, iar MPI_Test() se termină imediat şi returnează un indicator a cărui stare arată dacă operaţia s-a încheiat sau nu. În afară de aceste funcţii mai există MPI_Waitany(), MPI_Testany(),MPI_Waitall(),MPI_Testall(), MPI_Waitsome(), MPI_Testsome(). Pentru mai multe detalii despre aceste tipuri de funcţii consultaţi helpul distribuţiei MPI. Observaţie: 1. Funcţiile de comunicare fără blocare oferă posibilitatea suprapunerii în timp a operaţiilor de comunicare cu cele de calcul, ceea ce poate reduce timpul de execuţie al aplicaţiei. 2. Funcţia receive poate fi apelatǎ numai în modul standard. Exemplu: MPI_Comm_Rank (MPI_COMM_WORLD,&myrank); if (myrank == 0) { int a; MPI_Isend(&a,1,MPI_INT,msgtag,MPI_COMM_WORLD,req1); calcul();
65
MPI_Wait(req1, status); } else if (myrank == 1) { nt b; MPI_Irecv(&b,1,MPI_INT,0,mesgtag,MPI_COMM_WORLD,req1); MPI_Wait(req1, status); }
Exemplu: Model de aplicaţie care implementează problema producătoriconsumatori (cazul n:1 ) folosind comunicǎri neblocante [1]. ... typedef struct{ char data[MAXSIZE]; int datasize; MPI_Request req; }Buffer; Buffer *buffer; MPI_Status status; ... MPI_Comm_rank(comm, &rank); MPI_comm_size(comm, &size); if (rank!=size-1) { //codul pentru producator //initializare – producatorul aloca un buffer buffer= (Buffer *)malloc(sizeof(Buffer)); while(1) { //bucla principala //producatorul umple dufferul cu date si intoarce //numarul de bytes din buffer produce(buffer->data, &buffer->datasize); //trimite datele MPI_Send(buffer->data, buffer->datasize, MPI_CHAR, size-1, tag, comm); } //end while }//end if else{ // rank==size-1; codul pentru consumator //initializare – consumatorul aloca un buffer pentru //fiecare producator buffer=(Buffer )malloc(sizeof(Buffer)*(size-1)); // se apleleaza un receive pentru fiecare producator for(i=0; i
66
//un nou receive MPI_Irecv(buffer[i].data, MAXSIZE, MPI_CHAR, i, tag, comm, &(buffer[i].req)); }//end for }//end else
Exemplu: Model de aplicaţie care implementează problema producătoriconsumatori (cazul n:1 ) folosind funcţia de comunicare neblocante şi funcţia MPI_Test()[1] . ... typedef struct{ char data[MAXSIZE]; int datasize; MPI_Request req; }Buffer; Buffer *buffer; MPI_Status status; ... MPI_Comm_rank(comm, &rank); MPI_comm_size(comm, &size); if (rank!=size-1) { //codul pentru producator //initializare – producatorul aloca un buffer buffer= (Buffer *)malloc(sizeof(Buffer)); while(1) { //bucla principala //producatorul umple dufferul cu date si intoarce //numarul de bytes din buffer produce(buffer->data, &buffer->datasize); //trimite datele MPI_Send(buffer->data, buffer->datasize, MPI_CHAR, size-1, tag, comm); } //end while }//end if else{ // rank==size-1; codul pentru consumator //initializare – consumatorul aloca un buffer pentru //fiecare producator buffer=(Buffer )malloc(sizeof(Buffer)*(size-1)); // se apleleaza un receive pentru fiecare producator for(i=0; i
67
// consumatorul elibereaza bufferul de date consume(buffer[i].data, buffer[i].datsize); //un nou receive MPI_Irecv(buffer[i].data, MAXSIZE, MPI_CHAR, i, tag, comm, &(buffer[i].req)); }//end while }//end else
Alte funcţii neblocante: MPI_Ibsend, MPI_Issend, MPI_Irsend. Apelul funcţiei Ibsend: MPI_Ibsend(buf,count,datatype,dest,tag,comm,request) [ IN buf] [ [ [ [ [ [
IN count] IN datatype] IN dest] IN tag] IN comm] OUT request]
- adresa zonei tampon (bufferului) pentru datele le trimis - numărul de elemente de trimis din buffer - tipul datelor pentru fiecare element din buffer - rangul destinaţiei - identificator de mesaj - comunicatorul - communication request
Prototipul funcţiei este următorul: int MPI_Ibsend(void* buf, int count, MPI_Datatype datatype,int dest, int tag, MPI_Comm comm, MPI_Request *request)
Apelul funcţiei Issend: MPI_Issend(buf,count,datatype,dest,tag,comm,request) [ IN buf] [ [ [ [ [ [
IN count] IN datatype] IN dest] IN tag] IN comm] OUT request]
- adresa zonei tampon (bufferului) pentru datele de trimis - numărul de elemente de trimis din buffer - tipul datelor pentru fiecare element din buffer - rangul destinaţiei - identificator de mesaj - comunicatorul - communication request
Prototipul funcţiei este următorul: int MPI_Issend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request)
Apelul funcţiei Irsend: MPI_IRSEND(buf,count,datatype,dest,tag,comm,request)
68
[ [ [ [ [ [ [
IN buf] IN count] IN datatype] IN dest] IN tag] IN comm] OUT request]
- adresa zonei tampon (bufferului) pentru datele de trimis - numărul de elemente de trimis din buffer - tipul datelor pentru fiecare element din buffer - rangul destinaţiei - identificator de mesaj - comunicatorul - communication request
Prototipul funcţiei este următorul: int MPI_Irsend(void* buf, int count, MPI_Datatype datatype,int dest, int tag, MPI_Comm comm, MPI_Request *request)
Observaţii: Sistemele de comunicare cu transfer de mesaje sunt în general nedeterministe, adică nu garantează că ordinea în care se transmit mesajele este aceeaşi cu ordinea în care mesajele sunt recepţionate, programatorului revenindu-i sarcina găsirii mecanismelor necesare pentru obţinerea unui calcul determinist.
Comunicǎri colective Conceptul cheie al comunicǎrilor colective este acela de a avea un “grup” de procese participante. Pentru aceasta biblioteca MPI oferă următoarele funcţii: • bariera - sincronizarea prin barieră pentru toţi membrii grupului (MPI_Barrier); • broadcast - transmiterea de la un membru al grupului către ceilalţi membri (MPI_Bcast); • gather - adunarea datelor de la toţi membrii grupului la un singur membru din grup (MPI_Gather); • scatter - împrăştierea datelor de la un membru la toţi membrii grupului (MPI_Scatter); • all gather - o variaţie a MPI_Gather în care toţi membrii grupului primesc rezultatele, nu numai procesul “root” (MPI_Allgather); • all-to-all scatter/gather - adunarea/împraştierea datelor de la toţi membrii grupului către toţi membrii (MPI_Alltoall – o extensie a MPI_Allgather în care fiecare proces trimite date distincte către fiecare proces din grup); • operaţii de reducere – adunare, înmulţire, min, max, sau funcţii definite de utilizator în care rezultatele sunt trimise la toţi membrii grupului sau numai la un membru. 69
Observaţii: - Toate funcţiile sunt executate colectiv (sunt apelate de toate procesele dintr-un comunicator); toate procesele folosesc aceeaşi parametri în apeluri. - Funţiile nu au un identificator de grup ca argument explicit. În schimb avem un comunicator ca argument. Un inter-comunicator nu este permis ca argument în funcţiile ce realizează operaţiile colective decât în implementările standardului MPI 2.0. Caracteristicile comunicǎrilor colective: - Comunicǎrile colective şi cele de tip unu-la-unu nu se vor influenţa reciproc. - Toate procesele trebuie sǎ execute comunicǎri colective. - Sincronizarea nu este garantată, excepţie realizând doar bariera. - Nu există comunicǎri colective neblocante. - Nu există tag-uri. - Bufferele de recepţie trebuie sǎ fie exact de dimensiunea potrivită .
Bariera Bariera este un mecanism de sincronizare a unui grup de procese. Dacă un proces a făcut apelul MPI_Barrier(com), unde com este comunicatorul, atunci el va aştepta până când toate procesele au efectuat acest apel. Apelul funcţiei Barrier: MPI_Barrier( comm ) [IN comm] - comunicator
Prototipul funcţiei este: int MPI_Barrier(MPI_Comm comm )
Difuzia (Broadcast) Funcţia MPI_Bcast trimite un mesaj de la procesul cu rangul root către toate procesele din grup, inclusiv procesului root.
70
P0
A0
broadcast
A0 P0
P1
A0 P1
P2
A0 P2
P3
A0 P3
P4
A0 P4
P5
A0 P5
Funcţia este apelată de toţi membrii grupului, folosind acelaşi argument pentru comunicator, root. La ieşirea din funcţie, conţinutul buffer-ului de comunicare al procesului root a fost copiat de toate procesele. Apelul funcţiei Bcast: MPI_Bcast( buffer, count, datatype, root, comm ) [ [ [ [ [
INOUT buffer] IN count] IN datatype] IN root] IN comm]
- adresa de început a buffer-ului pentru date - numărul de intrări din buffer - tipul datelor din buffer - rangul procesului root care iniţiază comunicarea - comunicator
Prototipul funcţiei este următorul: int MPI_Bcast(void* buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm )
Exemplu 8: #include void main (int argc, char *argv[]) { int rank; double param; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&rank); if(rank==5) param=23.0; MPI_Bcast(¶m,1,MPI_DOUBLE,5,MPI_COMM_WORLD); printf("P:%d dupa broadcast parametrul este %f\n", rank, param); MPI_Finalize(); }
71
Comunicarea personalizatǎ unu-la-toţi (Scatter) Procesul root trimite conţinutul sendbuffer-ului către toate procesele din grup (inclusiv către procesul root). P0
A0 A1 A2 A3 A4 A5
scatter
A0 P0 A1 P1 A2 P2 A3 P3 A4 P4 A5 P5
Rezultatul este acelaşi ca şi cum procesul root ar executa n operaţii de tip send MPI_Send(sendbuf+i* sendcount* extent(sendtype), sendcount, sendtype, i,...), şi fiecare proces execută o operaţie receive MPI_Recv(recvbuf, recvcount, recvtype, i,...). Apelul funcţiei Scatter: MPI_Scatter (sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, root, comm) [IN sendbuf] [IN sendcount] [IN sendtype] [OUT recvbuf] [IN recvcount] [IN recvtype] [IN root] [IN comm]
- adresa de început a zonei buffer-ului pentru datele de trimis - numărul de elemente din buffer - tipul datelor din buffer - adresa buffer-ului de recepţie - numărul de elemente pentru fiecare recepţie în parte - tipul datelor din buffer-ul de recepţie - rangul proceselor care trimite datele - comunicator
Prototipul funcţiei este următorul: MPI_Scatter(void* sendbuf, int sendcount, MPI_Datatype sendtype, void* recvbuf, int recvcount, MPI_Datatype recvtype,int root, MPI_Comm comm)
Observaţii: - Într-o altă interpretare, funcţia Scatter ar însemna: procesul root trimite un mesaj cu MPI_Send(sendbuf, sendcount-n, 72
sendtype, ...). Acest mesaj este împărţit în n segmente egale,
segmentul i fiind transmis la procesul i din grup şi fiecare proces primeşte mesajul după descrierea anterioară. - Toate argumentele funcţiei sunt semnificative numai în procesul root, în timp ce pentru celelalte procese numai argumentele recvbuf, recvcount, recvtype, root şi comm sunt semnificative. - Sendbuffer este ignorat în procesele care nu au rangul root. 100
100
100 Toate procesele
100
100
100 Procesul root
sendbuf
Exemplu: Transmiterea a 100 de întregi de către procesul root către fiecare proces din grup (inversul exemplului 8). MPI_Comm comm; int gsize,*sendbuf; int root, rbuf[100]; ... MPI_Comm_size( comm, &gsize); sendbuf = (int *)malloc(gsize*100*sizeof(int)); ... MPI_Scatter( sendbuf, 100, MPI_INT, rbuf, 100, MPI_INT, root, comm);
Comunicarea toţi-la-unu - Colectarea datelor (Gather) Fiecare proces (inclusiv procesul root) trimite conţinutul sendbufferului către procesul root. Procesul root primeşte mesajele şi le stochează în ordinea rangului.
73
P0
A0 A1 A2 A3 A4 A5
gather
A0 P0 A1 P1 A2 P2 A3 P3 A4 P4 A5 P5
Rezultatul este acelaşi ca şi cum fiecare proces (inclusiv procesul root) ar executa un apel al funcţiei MPI_Send(sendbuf, sendcount, sendtype, root, ...) şi procesul root ar executa n apeluri MPI_Recv(recvbuf+i*recvcount*extent(recvtype), recvcount, recvtype, i ,...) unde extent(recvtype) este tipul obţinut din apelul MPI_Type_extent( ) vezi help-ul. Apelul funcţiei Gather: MPI_Gather( sendbuf,sendcount,sendtype,recvbuf, recvcount, recvtype, root, comm) [ [ [ [
IN sendbuf] IN sendcount] IN sendtype] OUT recvbuf]
-
[ IN recvcount]
-
[ IN recvtype]
-
[ IN root] [ IN comm]
-
adresa de început a zonei buffer-ului pentru date numărul de elemente din buffer tipul datelor din buffer adresa buffer-ului de recepţie (are semnificaţie numai la procesul root) numărul de elemente pentru fiecare recepţie în parte (are semnificaţie numai la procesul root) tipul datelor din buffer-ul de recepţie (are semnificaţie numai la procesul root) rangul proceselor care recepţionează datele comunicator
Prototipul funcţiei este următorul: int MPI_Gather(void* sendbuf, int sendcount, MPI_Datatype sendtype, void* recvbuf, int recvcount, MPI_Datatype recvtype, int root,MPI_Comm comm)
74
Observaţii: - Într-o altă intrepretare, funcţia Gather ar însemna: n mesaje trimise de procesele din grup sunt concatenate în ordinea rangului proceselor şi mesajul rezultat este primit de procesul root. - Buffer-ul de recepţie este ignorat pentru procesele care nu au rangul root. - Argumentul recvcount la procesul root indică dimensiunea datelor primite de la fiecare proces şi nu numărul total de date primite. Exemplu: Recepţia a 100 întregi de la fiecare proces din grup MPI_Comm comm; int gsize,sendarray[100]; int root, *rbuf; ... MPI_Comm_size( comm, &gsize); rbuf = (int *)malloc(gsize*100*sizeof(int)); MPI_Gather( sendarray, 100, MPI_INT, rbuf,100, MPI_INT, root, comm); 100
100
100 Toate procesele
100
100
100 Procesul root
recvbuf
Exemplu: Exemplul de mai sus modificat – numai procesul root alocă memorie pentru buffer-ul de recepţie. MPI_Comm comm; int gsize,sendarray[100]; int root, myrank, *rbuf;
75
... MPI_Comm_rank( comm, myrank); if ( myrank == root) { MPI_Comm_size( comm, &gsize); rbuf = (int *)malloc(gsize*100*sizeof(int)); } MPI_Gather( sendarray, 100, MPI_INT, rbuf, 100, MPI_INT, root, comm);
Operaţii de reducere Funcţiile pe care le vom prezenta în continuare realizează operaţii de reducere peste toţi membrii unui grup. Operaţiile de reducere implică date distribuite peste un grup de procese. Funcţiile de reducere globale sunt următoarele: o operaţie de reducere care întoarce rezultatul la un singur nod, o operaţie care întoarce rezultatul la toate nodurile şi o operaţie de scanare (prefixul paralel). În plus există o operaţie de tipul reduce-scatter care combină funcţionalitatea operaţiei de reducere şi a operaţiei scatter. Operaţiile care se realizează asupra datelor pot fi predefinite sau definite de utilizator. Funcţia MPI_Reduce combină elementele din buffer-ul de intrare al fiecărui proces din grup, folosind operaţia op, şi întoarce rezultatul în buffer-ul de ieşire al procesului root. P0 data buff
Pn-1
P1 data
data
reduce();
reduce();
+
reduce();
reduce
P0
A0
B0
C0
P1
A1
B1
C1
P1
P2
A2
B2
C2
P2
76
A0+A1+A2
B0+B1+B2
C0+C1+C2
P0
Apelul funcţiei Reduce: MPI_Reduce(sendbuf,recvbuf,count,datatype,op,root,comm) [IN sendbuf] [OUT recvbuf] [IN [IN [IN [IN [IN
count] datatype] op] root] comm]
- adresa buffer-ului de trimitere - adresa buffer-ului de recepţie (are semnificaţie numai pentru procesul root) - numărul de elemente din buffer-ul de trimitere - tipul datelor din buffer-ul de trimitere - operaţia de reducere - rangul procesului root - comunicator
Prototipul funcţiei este următorul: int MPI_Reduce (void* sendbuf, void* recvbuf, int count, MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm)
Standardul MPI include variante pentru fiecare operaţie de reducere în care rezultatul este returnat tuturor proceselor din grup ca în figura de mai jos ( MPI_Allreduce). De asemenea trebuie ca toate procesele care participă la aceste operaţii să primească acelaşi rezultat. P0
A0
B0
C0
P1
A1
B1
P2
A2
B2
allreduce
A0+A1+A2
B0+B1+B2
C0+C1+C2
P0
C1
A0+A1+A2
B0+B1+B2
C0+C1+C2
P1
C2
A0+A1+A2
B0+B1+B2
C0+C1+C2
P2
Apelul funcţiei Allreduce: MPI_Allreduce(sendbuf,recvbuf,count,datatype,op, comm) [IN sendbuf] [OUT recvbuf] [IN count] [IN datatype] [IN op] [IN comm]
- adresa buffer-ului de trimitere - adresa buffer-ului de recepţie - numărul de elemente din buffer-ul de trimitere - tipul datelor din buffer-ul de trimitere - operaţia de reducere - comunicator
Prototipul funcţiei este următorul:
77
int MPI_Allreduce (void* sendbuf, void* recvbuf, int count, MPI_Datatype datatype, MPI_Op op, MPI_Comm comm)
Reduce-scatter este varianta în care rezultatul este transmis folosind scatter către toate procesele din grup. reduce-scatter
A0+A1+A2
P0
C1
B0+B1+B2
P1
C2
C0+C1+C2
P2
P0
A0
B0
C0
P1
A1
B1
P2
A2
B2
Apelul funcţiei Allreduce: MPI_Reduce_scatter(sendbuf,recvbuf,recvcounts,datatype,op, comm) [ IN sendbuf] [ OUT recvbuf] [ IN recvcounts] [ IN datatype] [ IN op] [ IN comm]
- adresa buffer-ului de trimitere - adresa buffer-ului de recepţie - şir de întregi care specifică numărul de elemente din rezultatul distribuit către fiecare proces. Acesta trebuie sa fie identic la toate apelurile din procese. - tipul datelor din buffer-ul de trimitere - operaţia de reducere - comunicator
Prototipul funcţiei este următorul: int MPI_Reduce_scatter (void* sendbuf, void* recvbuf, int *recvcounts, MPI_Datatype datatype, MPI_Op op, MPI_Comm comm)
Operaţii de reducere predefinite Următoarele operaţii sunt predefinite pentru operaţia de reducere: MPI_MAX MPI_MIN MPI_SUM MPI_PROD MPI_LAND MPI_BAND 78
maximum minimum sumă produs şi logic şi pe biţi
MPI_LOR MPI_BOR MPI_LXOR MPI_BXOR MPI_MAXLOC MPI_MINLOC
sau logic sau pe biţi xor logic xor pe biţi max value and location min value and location
Operaţii de reducere definite de utilizator Definirea unei operaţii de către utilizator se face folosind funcţia MPI_Op_create: Apelul funcţiei este: MPI_Op_create( function, commute, op) [IN function] [IN commute] [OUT op]
1. funcţia definită de utilizator 2. daca este comutativă true, altfel false 3. operaţia
Prototipul funcţiei este următorul: int MPI_Op_create (MPI_User_function *function, int commute, MPI_Op *op)
Operaţiile definite de utilizator se presupun a fi asociative. Dacă commute = true, atunci operaţia trebuie să fie şi comutativă şi asociativă. Dacă commute = false, atunci ordinea operanzilor este fixă şi este definită să fie ascendentă, în ordinea rangului proceselor, pornind cu procesul zero. Ordinea evaluării poate fi schimbată, având avantajul asociativităţii operaţiei. Dacă commute =true atunci ordinea evaluării poate fi schimbată, având avantajele comutativităţii şi asociativităţii. function este funcţia definită de utilizator şi trebuie să aibă următorul prototip: void MPI_User_function ( void *invec, void *inoutvec, int *len, MPI_Datatype *datatype);
Exemplu: Un exemplu de implementare ineficientă a funcţiei MPI_Reduce: if (rank > 0) { MPI_Recv(tempbuf, count, datatype, rank-1,...)
79
User_reduce( tempbuf, sendbuf, count, datatype) } if (rank < groupsize-1) { MPI_Send( sendbuf, count, datatype, rank+1, ...) } /*avem raspunsul în procesele groupsize-1 şi le trimitem la root */ if (rank == groupsize-1) { MPI_Send( sendbuf, count, datatype, root, ...) } if (rank == root) { MPI_Recv(recvbuf, count, datatype, groupsize-1,...) }
Operaţia de reducere începe, secvenţial, de la procesul 0 la procesul group_size-1. Această ordine este aleasă pentru a se respecta ordinea unui operator ne-comutativ definit de funcţia User_reduce(). O implementare mai eficientă este obţinută folosind avantajul asociativităţii. Comutativitatea poate fi folosită pentru a avantaja cazurile în care argumentul commute al MPI_Op_create este true. Operaţiile de reducere predefinite pot fi implementate ca o bibliotecă de operaţii definite de utilizator. Când nu mai este nevoie de operaţia definită de utilizator trebuie eliberată memoria. Acest lucru se face folosind funcţia MPI_Op_free ce se apelează astfel: MPI_Op_free( op)
[ IN op]
operaţia
Prototipul funcţiei este următorul: int MPI_op_free ( MPI_Op *op)
Exemplu: Calculul produsului unui şir de numere complexe (vezi şi anexa): typedef struct { double real,imag; } Complex; /* funcţia definită de utilzator */ void myProd( Complex *in, Complex *inout, int *len, MPI_Datatype *dptr) { int i;
80
Complex c; for (i=0; i< *len; ++i) { c.real = inout->real*in->real inout->imag*in->imag; c.imag = inout->real*in->imag + inout->imag*in->real; *inout = c; in++; inout++; }
} ... /* fiecare proces are un şir de 100 numere complexe */ Complex a[100], answer[100]; MPI_Op myOp; MPI_Datatype ctype; /* se spune MPI cum este definit tipul Complex */ MPI_Type_contiguous( 2, MPI_DOUBLE, &ctype ); MPI_Type_commit( &ctype ); /*se crează operaţia definită de utilizator pentru înmulţire*/ MPI_Op_create( myProd, True, &myOp ); MPI_Reduce( a, answer, 100, ctype, myOp,root,comm ); /*În acest punct, raspunsul, care constă în 100 numere complexe este în procesul root*/
Calculul prefixelor Pentru calculul prefixului paralel ca operaţie de reducere a datelor distribuite peste un grup de procese, standardul MPI introduce funcţia MPI_Scan. Operaţia întoarce în buffer-ul recvbuf a procesului cu rangul i, reducerea valorilor din buffer-ul sendbuf a proceselor cu rangurile 0, …, i (inclusiv – vezi figura). Tipul operaţiilor suportate, precum şi limitările legate de sendbuf şi recvbuf sunt cele ale MPI_Reduce. scan
A0
B0
C0
P0
C1
A0+A1
B0+B1
C0+C1
P1
C2
A0+A1+A2
B0+B1+B2
C0+C1+C2
P2
P0
A0
B0
C0
P1
A1
B1
P2
A2
B2
81
Apelul funcţiei este: MPI_Scan( sendbuf, recvbuf, count, datatype, op, comm ) [ [ [ [ [ [
IN sendbuf] OUT recvbuf] IN count] IN datatype] IN op] IN comm]
-
adresa buffer-ului de trimitere adresa buffer-ului de recepţie numarul de elemente din buffer-ul de trimitere tipul datelor din buffer-ul de trimitere operaţia comunicator
Prototipul funcţiei este următorul: int MPI_Scan(void* sendbuf, void* recvbuf, int count, MPI_Datatype datatype, MPI_Op op, MPI_Comm comm )
Operaţia a fost definită ca fiind “inclusive scan” adică calculul prefixului se face inclusiv pentru procesul cu rangul i. O alternativă ar fi definirea operaţiei într-o manieră exclusivă, când rezultatul pentru rangul i va include datele până la rangul i-1. Ultima variantă pare a fi mai avantajoasă deoarece o operaţie de tipul inclusive scan poate fi realizată dintr-o operaţie de tip exclusive scan fǎră comunicare suplimentarǎ. Datorită apariţiei unor complicaţii (scrierea operaţiilor definite de utilizator) la scrierea operaţiei de tip inclusive scan ca operaţie de tip exclusive scan, acestea fiind lăsate în grija programatorului, s-a renunţat la varianta exclusive scan.
Corectitudinea operaţiilor colective Un program scris corect în MPI trebuie să apeleze comunicǎrile colective astfel încât să nu apară blocaje, indiferent dacă comunicǎrile sunt sincronizate sau nu. În următorul exemplu este arătat un mod de folosire a operaţiilor colective care poate duce la blocare. Exemplu: Utilizare greşită: switch(rank) { case 0: MPI_Bcast(buf1, MPI_Bcast(buf2, break; case 1: MPI_Bcast(buf2, MPI_Bcast(buf1, break;
82
count, type, 0, comm); count, type, 1, comm); count, type, 1, comm); count, type, 0, comm);
}
Observaţie: Se presupune că grupul de comunicare este {0,1}. Două procese execută broadcast în ordine inversă. Dacă operaţiile sunt sincronizate apare blocaj. Operaţiile colective trebuie executate în aceeaşi ordine de toţi membrii grupului de comunicare. Exemplu:Utilizare greşită: switch(rank) { case 0: MPI_Bcast(buf1, MPI_Bcast(buf2, break; case 1: MPI_Bcast(buf1, MPI_Bcast(buf2, break; case 2: MPI_Bcast(buf1, MPI_Bcast(buf2, break; }
count, type, 0, comm0); count, type, 2, comm2); count, type, 1, comm1); count, type, 0, comm0); count, type, 2, comm2); count, type, 1, comm1);
Observaţii: - Presupunem că grupul de comunicare comm0 este {0,1}, comm1 este {1,2} şi comm2 este {2,0}. Daca operaţia broadcast este sincronizată, atunci apare o dependenţă ciclică: broadcast în comm2 se termină numai după broadcast din comm0; broadcast în comm0 se termină numai după broadcast din comm1; şi broadcast în comm1 se termină numai după broadcast din comm2. Astfel aplicaţia se blochează. - Operaţiile colective trebuiesc executate astfel încât să nu apară dependenţe ciclice. Exemplu: Utilizare greşită: switch(rank) { case 0: MPI_Bcast(buf1, count, type, 0, comm); MPI_Send(buf2, count, type, 1, tag, comm); break; case 1: MPI_Recv(buf2, count, type, 0, tag, comm, status); MPI_Bcast(buf1, count, type, 0, comm); break;
83
}
Observaţie: - Procesul 0 execută broadcast, urmat de o operaţie send blocantă. Procesul 1 execută mai întâi o operaţie receive blocantă corespunzătoare operaţiei send, urmată de un broadcast corespunzător celui din procesul 0. Acest program se poate bloca. Operaţia broadcast din procesul 0 se poate bloca până când procesul 1 execută operaţia broadcast corespunzătoare, şi astfel nu este executată operaţia send. Procesul 1 se va bloca pe receive şi nu va executa operaţia broadcast. - Ordinea execuţiei operaţiilor colective şi a celor point-to-point trebuie să fie realizată astfel încât chiar dacă operaţiile colective şi cele point-to-point sunt sincronizate să nu apară blocaje. Exemplu: Un program corect: switch(rank) { case 0: MPI_Bcast(buf1, count, type, 0, comm); MPI_Send(buf2, count, type, 1, tag, comm); break; case 1: MPI_Recv(buf2, count, type, MPI_ANY_SOURCE, tag, comm, status); MPI_Bcast(buf1, count, type, 0, comm); MPI_Recv(buf2, count, type, MPI_ANY_SOURCE, tag, comm, status); break; case 2: MPI_Send(buf2, count, type, 1, tag, comm); MPI_Bcast(buf1, count, type, 0, comm); break; }
Două posibile execuţii în urma cărora programul nu se blochează sunt prezentate în figura următoare:
84
O primă execuţie posibilă Proces
O a doua execuţie posibilă
Exemplu: Folosirea operaţiilor de reducere predefinite MPI_MAXLOC şi MPI_MINLOC: #include /* Rulare pentru 16 procese */ struct { double value; int rank; } in, out; void main (int argc, char *argv[]) { int rank; int root; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&rank); in.value=rank+1; in.rank=rank; root=7; MPI_Reduce(&in,&out,1,MPI_DOUBLE_INT,MPI_MAXLOC,root, MPI_COMM_WORLD); if(rank==root) printf("PE:%d max=%lf la rangul %d\n", rank, out.value, out.rank); MPI_Reduce(&in,&out,1,MPI_DOUBLE_INT,MPI_MINLOC,root, MPI_COMM_WORLD); if(rank==root) printf("PE:%d min=%lf la rangul %d\n", rank, out.value, out.rank); MPI_Finalize(); }
85
Exemplu: #include typedef struct { double real,imag; } complex; void cprod(complex *in, complex *inout, int *len, MPI_Datatype *dptr) { int i; complex c; for (i=0; i<*len; ++i) { c.real=(*in).real * (*inout).real - (*in).imag * (*inout).imag; c.imag=(*in).real * (*inout).imag + (*in).imag * (*inout).real; *inout=c; in++; inout++; } } void main (int argc, char *argv[]) { int rank; int root; complex source,result; MPI_Op myop; MPI_Datatype ctype; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&rank); MPI_Type_contiguous(2,MPI_DOUBLE,&ctype); MPI_Type_commit(&ctype); MPI_Op_create(cprod,TRUE,&myop); root=2; source.real=rank+1; source.imag=rank+2; MPI_Reduce(&source,&result,1,ctype,myop,root, MPI_COMM_WORLD); if(rank==root) printf ("PE:%d rezultatul este %lf + %lfi\n", rank,result.real, result.imag); MPI_Finalize(); }
Tipuri de date definite de utilizator Mecanismele de Comunicare implementate în MPI ce au fost studiate până acum permit numai transmiterea şi recepţia secvenţelor de elemente identice care reprezintă zone continue în memorie. Este de 86
dorit să se poată transmite date care nu sunt omogene, cum sunt structurile, sau care nu sunt continue în memorie, ca de exemplu anumite elemente dintr-un vector. Pentru rezolvarea acestor probleme, MPI pune la dispoziţie două mecanisme: • utilizatorul poate defini tipuri de date derivate, care pot fi utilizate în Comunicările cu MPI – derived datatype; • un proces poate împacheta explicit datele necontinui într-un buffer continuu şi apoi să îl trimită, iar procesul care primeşte datele poate desface datele primite în buffer şi să le stocheze în locaţii necontinue – pack/unpack.
Constructori pentru tipuri de date Cel mai simplu constructor este MPI_TYPE_CONTIGUOUS care permite multiplicarea unui tip de date în locaţii continue. Apelul funcţiei Contiguous: MPI_Type_contiguous (count, oldtype, newtype) [IN count] [IN oldtype] [OUT newtype]
- numărul de copii (întreg pozitiv) - tipul de dată iniţial - tipul de dată nou
Prototipul funcţiei este următorul: int MPI_Type_contiguous(int count, MPI_Datatype oldtype,MPI_Datatype *newtype)
Observaţie: newtype reprezintă tipul de date obţinut prin concatenarea a count copii ale oldtype ca în figura de mai jos:
87
oldtype count = newtype Funcţia MPI_TYPE_VECTOR este un constructor mai general care permite multiplicarea unui tip de date în locaţii care constau în blocuri aflate la distanţe egale (vezi figura următoare). Fiecare bloc este obţinut prin concatenarea unui acelaşi număr de copii ale tipului de date oldtype. Spaţiul dintre blocuri este multiplu de dimensiunea tipului de date oldtype.
oldtype count = 3, blocklength = 2, newtype Apelul funcţiei Vector: MPI_Type_vector ( count, blocklength, stride, oldtype, newtype) [IN count] [IN blocklength] [IN stride] [IN oldtype] [OUT newtype]
- numărul de blocuri - numărul de elemente din fiecare bloc - spaţiul dintre blocuri, măsurat în numărul de elemente (întreg) fiecărui bloc - tipul de dată vechi - tipul de dată nou
Prototipul funcţiei este următorul: int MPI_Type_vector(int count, int blocklength, int stride, MPI_Datatype oldtype, MPI_Datatype *newtype)
88
Funcţia MPI_TYPE_HVECTOR este identică cu MPI_TYPE_VECTOR, cu excepţia faptului că pasul este dat în bytes şi nu în elemente (vezi figura următoare). oldtype count = 3, blocklength = 2, newtype Apelul funcţiei Hvector: MPI_ Type_hvector ( count, oldtype, newtype) [IN count] [IN blocklength] [IN stride] [IN oldtype] [OUT newtype]
blocklength, stride, -
numărul de blocuri numărul de elemente din fiecare bloc numărul de bytes dintre blocuri tipul de dată iniţial tipul de dată nou
Prototipul funcţiei este următorul: int MPI_Type_hvector(int count, int blocklength, MPI_Aint stride,MPI_Datatype oldtype, MPI_Datatype *newtype)
Exemplu: struct Partstruct { int class; /* clasa particulei */ double d[6]; /* coordonatele */ char b[7]; /* informaţii suplimentare*/ }; void SendParticles(MPI_Comm comm) { struct Partstruct particle[1000]; int dest, tag; MPI_Datatype Locationtype; MPI_Type_create_hvector(1000, 3, sizeof(struct Partstruct),MPI_DOUBLE, &Locationtype); MPI_Type_commit(&Locationtype); MPI_Send(particle[0].d,1,Loationtype, dest, tag, comm); ... }
89
Funcţia MPI_TYPE_INDEXED permite multiplicarea unui tip de date într-o secvenţă de blocuri (fiecare bloc este concatenat la tipul de date oldtype), iar fiecare bloc poate conţine un număr diferit de copii şi poate avea un deplasament diferit (vezi figura următoare). Toate deplasamentele blocurilor sunt multipli ai dimensiunii tipului de date oldtype. oldtype count = 3, blocklength = (2,3,1), deplasament = newtype Apelul funcţiei Indexed: MPI_ Type_indexed ( count, array_of_blocklengths, array_of_displacements, oldtype,newtype) [IN count]
[IN array_of_blocklengths] [IN array_of_displacements] [IN oldtype] [OUT newtype]
- numărul de blocuri; de asemenea şi numărul de intrări în array_of_displacements şi array_of_blocklengths - numărul de elemente din fiecare bloc - deplasamentul pentru fiecare bloc, măsurat ca număr de elemente - tipul de dată iniţial - tipul de dată nou
Prototipul funcţiei este următorul: int MPI_Type_indexed(int count, int *array_of_blocklengths, int *array_of_displacements, MPI_Datatype oldtype,MPI_Datatype *newtype)
Observaţie: Există şi un constructor de tip Hindexed care are aceeaşi semnificaţie cu Hvector, fiind diferit de acesta doar prin parametri. Funcţia MPI_TYPE_STRUCT este un constructor general. În plus generalizează funcţia Hindexed prin aceea că permite fiecărui bloc să reprezinte o multiplicare a unor tipuri de date diferite (vezi figura de mai jos): 90
oldtype count = 3, blocklength = (2,3,4), deplasament = newtype Apelul funcţiei Struct: MPI_TYPE_STRUCT(count, array_of_blocklengths, array_of_displacements, array_of_types, newtype) [IN count] [IN array_of_blocklength] [IN array_of_displacements] [IN array_of_types] [OUT newtype]
- numărul de blocuri; de asemenea şi numărul de intrări în array_of_types, array_of_displacements şi array_of_blocklengths - numărul de elemente din fiecare bloc - deplasamentul în biţi al fiecărui bloc - tipul elementelor din fiecare bloc - noul tip de dată
Prototipul funcţiei este următorul: int MPI_Type_struct(int count, int *array_of_blocklengths,MPI_Aint *array_of_displacements, MPI_Datatype *array_of_types, MPI_Datatype *newtype)
Exemplu :Trimiterea unui tablou de structuri: struct Partstruct { int class; /* clasa particulei */ double d[6]; /* coordonatele */ char b[7]; /* informaţii suplimentare*/ }; struct Partstruct int MPI_Comm comm;
particle[1000]; dest, tag;
MPI_Datatype Particletype; MPI_Datatype type[3]={MPI_CHAR, MPI_DOUBLE, MPI_CHAR}; Int blocklen={0, sizeof(double), 7*sizeof(double)};
91
MPI_Type_create_struct(3, blocklen, disp, type, & Particletype); MPI_Type_commit(&Particletype); MPI_Send(particle,1000, Particletype, dest, tag, comm); ...
Utilizarea tipurilor de date derivate Un tip de dată trebuie creat înainte de a putea fi utilizat în Comunicări. Un tip de date creat mai poate fi folosit ca argument în construirea unor noi tipuri de date. Observaţie: Nu este necesară creearea tipurilor de date de bază. Pentru crearea tipurilor de date se foloseşte funcţia: MPI_ Type_commit (datatype) [INOUT datatype]
- tipul de date care trebuie creat
Prototipul funcţiei este următorul: int MPI_Type_commit(MPI_Datatype *datatype)
Operaţia commit crează tipul de date, care reprezintă de fapt descrierea formală a buffer-ului de Comunicare şi nu conţinutul acestuia. De aceea după ce un tip de date a fost creat, acesta poate fi reutilizat pentru a comunica schimbarea conţinutului buffer-ului sau, de asemenea, pentru schimbarea conţinutului mai multor buffer-e cu adrese de început diferite. Eliberarea unui tip de date se poate face folosind funcţia Free: MPI_ Type_free (datatype) [INOUT datatype]
−
tipul de date care trebuie eliberat
Prototipul funcţiei este următorul: int MPI_Type_free( MPI_Datatype *datatype )
Funcţia MPI_TYPE_FREE marchează tipul de date primit ca parametru cu tipul de date pentru dealocare şi setează tipul de date pe 92
MPI_DATATYPE_NULL. Orice comunicare care utilizează acest tip de date se va termina fǎră erori. Tipurile de date derivate care au fost definite pornind de la tipul de date eliberat nu sunt afectate.
Lungimea mesajelor Dacă un proces primeşte un mesaj folosind un tip de date definit de utilizator, atunci un apel al funcţiei MPI_GET_COUNT(status, datatype, count) va returna numărul de copii ale tipului de dată primit. Dacă operaţia de recepţie este MPI_RECV, atunci MPI_GET_COUNT poate returna orice valoare k ,unde 0<= k <= count. Dacă funcţia va returna valoarea k, atunci numărul de elemente primite este n*k, unde n este numărul de elemente din tipul de date. Pentru determinarea numărului de elemente din tipul de date se poate folosi funcţia MPI_GET_ELEMENTS : MPI_GET_ELEMENT(status, datatype, count) [IN
status]
[IN datatype] [OUT count]
- variabilă utilă pentru aflarea dimensiunii, identificatorului de mesaj şi a sursei - tipul de date utilizat în operaţia receive - numărul de elemente din tipul de date primit
Prototipul funcţiei este: MPI_Get_elements(MPI_Status *status, MPI_Datatype datatype, int *count)
Exemplu: Lucrul cu structuri de date de tip “union”: union { int ival; float fval; } u[1000] int /*
utype; Variabila utype “păstrează urma” tipului curent */
MPI_Datatype int MPI_Aint MPI_Datatype MPI_Aint
type[2]; blocklen[2] = {1,1}; disp[2]; mpi_utype[2]; i,j;
93
/* prelucrarea unui tip de date pentru fiecare tip de “union” posibil */ MPI_Address( u, &i); MPI_Address( u+1, &j); disp[0] = 0; disp[1] = j-i; type[1] = MPI_UB; type[0] = MPI_INT; MPI_Type_struct(2, blocklen, disp, type, &mpi_utype[0]); type[0] = MPI_FLOAT; MPI_Type_struct(2, blocklen, disp, type, &mpi_utype[1]); for(i=0; i<2; i++) MPI_Type_commit(&mpi_utype[i]); /* Comunicarea efectivă */ MPI_Send(u, 1000, mpi_utype[utype], dest, tag, comm);
Topologii pentru procese O topologie oferă un mecanism convenabil pentru denumirea proceselor dintr-un grup, şi în plus, poate oferi soluţii sistemului, în timpul rulării, pentru maparea proceselor pe hardware-ul existent. Un grup de procese în MPI reprezintă o colecţie de n procese. Fiecare proces din grup are atribuit un rang între 0 şi n-1. În multe aplicaţii paralele o atribuire liniară a acestor ranguri nu reflectă logica comunicărilor. Adesea rangurile proceselor sunt aranjate în topologii ca de exemplu o plasă cu 2 sau 3 dimensiuni. Mai general, logica aranjării proceselor este descrisă de un graf. În continuare, această aranjare logică a proceselor o vom numi: “topologie virtuală”.
94
Fig. Topologia virtuală tor bidimensional [10] Trebuie facută distincţie între topologia virtuală a proceselor şi topologia fizică (hardware). Descrierea topologiilor virtuale, depinde numai de aplicaţie şi este independentă de maşină.
Topologii virtuale Structura comunicărilor pentru un set de procese poate reprezenta un graf, în care nodurile reprezintă procesele iar muchiile conectează procesele care comunică între ele. MPI asigură transmiterea mesajelor între perechile de procese din grup. Nu este nevoie ca un canal de comunicare să fie deschis explicit. De aceea, o “muchie lipsă” în graful de procese definit de utilizator nu asigură faptul că procesele care nu sunt “conectate” nu fac schimb mesaje. Aceasta înseamnă că aceste conexiuni sunt neglijate în topologia virtuală. Această strategie implică faptul că topologia nu oferă o cale convenabilă pentru stabilirea căilor de comunicare. Muchiile din graful de comunicare nu au ponderi, deci procesele sunt conectate sau nu. Informaţiile legate de topologie sunt asociate comunicatorilor. Funcţiile MPI_GRAPH_CREATE şi MPI_CART_CREATE sunt folosite pentru a crea o topologie virtuală generală (un graf) şi topologii carteziene. Aceste funcţii sunt operaţii colective. Ca şi pentru alte apeluri
95
de funcţii colective, programul trebuie scris să funcţioneze corect indiferent dacă apelurile se sincronizează sau nu.
Topologii carteziene Funcţia MPI_CART_CREATE poate fi folosită pentru a descrie structuri carteziene de dimensiune arbitrară. Pentru fiecare coordonată se poate specifica dacă structura este periodică sau nu. De exemplu, o topologie 1D, este liniară dacă nu este periodică şi este un inel dacă este periodică. Pentru o topologie 2D, avem un dreptunghi, un cilindru sau un tor după cum o dimensiune este sau nu periodică. Observaţie: Un hipercub cu n – dimensiuni este un tor cu n – dimensiuni cu câte 2 procese pe fiecare coordonată. De aceea nu este necesar un suport special pentru o structură de hipercub. Apelul funcţiei Cart_create: MPI_Cart_create(comm_old, ndims, dims, periods, reorder, comm_cart) [IN comm_old] [IN ndims] [IN dims] [IN periods] [IN reorder]
- comunicator - numărul de dimensiuni ale topologiei carteziene - tablou de întregi de dimensiune ndims care specifică numărul de procese de pe fiecare dimensiune - tablou de dimensiune ndims care specifică dacă “grila” este periodică (true) sau nu (false) pe fiecare dimensiune - rangurile pot fi reordonate (true) sau nu (false) [OUT comm_cart] comunicator cu noua topologie carteziană
Prototipul funcţiei este următorul: int MPI_Cart_create(MPI_Comm comm_old, int ndims, int *dims, int *periods, int reorder, MPI_Comm *comm_cart)
Exemplu: Implementarea topologiei de tor virtual din figura anterioară : MPI_Comm vu; int dim[2], period[2], reorder; dim[0]=4; dim[1]=3; period[0]=TRUE; period[1]=FALSE; reorder=TRUE; MPI_Cart_create(MPI_COMM_WORLD,2,dim,period,reorder, &vu);
Pentru topologiile carteziene, funcţia MPI_DIMC_CREATE ajută utilizatorul să realizeze o distribuire echilibrată a proceselor pe fiecare 96
direcţie de coordonate. O posibilitate ar fi împărţirea tuturor proceselor într-o topologie n-dimensională. Apelul funcţiei este: MPI_ Dims_create(nnodes, ndims, dims) [IN nnodes] [IN ndims] [INOUT dims]
- numărul de noduri - numărul de dimensiuni ale topologiei - tablou de întregi care specifică numărul de noduri pe fiecare dimensiune
Prototipul funcţiei este următorul: int MPI_Dims_create(int nnodes, int ndims, int *dims)
Pentru fiecare dims[i] setat de apelul funcţiei MPI_ Dims_create, dims[i] va fi ordonat descrescător. Tabloul dims este potrivit pentru a fi utilizat ca intrare în apelul funcţiei MPI_Cart_create. Exemplu: dims Înainte de apel (0,0) (0,0) (0,3,0)
MPI_DIMS_CREATE(6, 2, dims) MPI_DIMS_CREATE(7, 2, dims) MPI_DIMS_CREATE(6, 3, dims)
(0,3,0)
MPI_DIMS_CREATE(7, 3, dims)
apelul funcţiei
dims la return (3,2) (7,1) (2,3,1) apel eronat
Odată topologia stablită, pot fi necesare interogări cu privire la topologie. Aceste funcţii sunt următoarele şi sunt apelate local: MPI_Cartdim_get(comm, ndims) [IN comm] [OUT ndims]
- comunicator cu structura carteziană - numărul de dimensiuni al structurii carteziene
Prototipul funcţiei este: int
MPI_Cartdim_get(MPI_Comm comm, int *ndims)
MPI_Cart_rank (comm, coords, rank) [IN comm] - comunicator cu structura carteziană [IN coords] - tablou de întregi care specifică coordonatele carteziene ale unui proces [OUT rank] - rangul procesului specificat
97
Prototipul funcţiei este următorul: int MPI_Cart_rank(MPI_Comm comm, int *coords, int *rank)
Pentru un grup de procese cu o structură carteziană, funcţia MPI_CART_RANK transformă coordonatele logice ale procesului în rangul procesului aşa cum ele sunt utilizate în comunicările point-topoint. De exemplu pentru coords = (1,2) vom avea rank = 6.
MPI_Cart_get(comm, maxdims, dims, periods, coords) [IN comm] [IN maxdims] [OUT dims] [OUT periods] [OUT coords]
- comunicator cu structura carteziană - lungimea vecorilor dims, periods, şi coords în program - numărul de procese pe fiecare dimesiune carteziană - periodicitatea ( true/ false) pentru fiecare dimesiune - coordonatele procesului care aplează funcţia in structura carteziană
Prototipul funcţiei este următorul: int MPI_Cart_get(MPI_Comm comm, int maxdims, int *dims, int *periods, int *coords)
Pentru fiecare dimensiune i cu periods(i) = true, dacă coordonata coords(i) este în afara limitelor, adică coords(i) < 0 sau coords(i) >= dims(i), este adus înapoi în intervalul 0 <= coords(i) < dims(i). Dacă topologia este una periodică în ambele dimensiuni (tor), atunci coords=(4,6) va fi de asemenea rank=6. Coordonatele din afara limitelor generează eroare pentru dimensiuni neperiodice. Exemplu: #include
98
/* Rulare cu 12 procese */ void main(int argc, char *argv[]) { int rank; MPI_Comm vu; int dim[2],period[2],reorder; int coord[2],id; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&rank); dim[0]=4; dim[1]=3; period[0]=TRUE; period[1]=FALSE; reorder=TRUE; MPI_Cart_create(MPI_COMM_WORLD, 2, dim, period, reorder, &vu); if(rank==5){ MPI_Cart_coords(vu, rank, 2, coord); printf("P:%d My coordinates are %d d\n", rank, coord[0],coord[1]); } if(rank==0) { coord[0]=3; coord[1]=1; MPI_Cart_rank(vu, coord, &id); printf("The processor at position (%d, %d) has rank%d\n",coord[0], coord[1], id); } MPI_Finalize(); }
Programul va afişa: The processor at position (3,1) has rank 10 P:5 My coordinates are 1 2
Crearea topologiei de tip graf MPI_Graph_create(comm_old, nnodes, index, edges, reorder, comm_graph) [ [ [ [ [ [
IN comm_old] IN nnodes] IN index] IN edges] IN reorder] OUT comm_graph]
-
comunicator de intrare numarul de noduri in graf tablou de întregi care descriu nodurile tablou de întregi care descriu muchiile grafului rangurile pot fi ordonate sau nu comunicator cu topologia grafului
Prototipul funcţiei este următorul: int MPI_Graph_create(MPI_Comm comm_old, int nnodes, int *index, int *edges, int reorder, MPI_Comm *comm_graph)
99
Funcţia MPI_GRAPH_CREATE are ca valoare de return un handler către noul comunicator care are ataşate informaţiile privind topologia grafului . Parametrii nnodes, index şi edges definesc structura grafului. Numărul total de valori din index este nnodes şi numarul de valori din edges este egal cu numărul de muchii ale grafului. Examplu: Să presupunem că procesele 0,1,2,3 au următoarea matrice de adiacenţă: proces 0 1 2 3
vecini 1,3 0 3 0,3
Argumentele de intrare sunt: nnodes=4, index=2,3,4,6 şi edges=1,3,0,3,0,2 Următoarele informaţii legate de structura topologiei sunt stocate de comunicator: - Tipul topologiei (carteziană sau graf) - Pentru o topologie carteziană: ndims (numărul de dimensiuni),
dims (numărul de procese pe direcţia de coordonate),
periods (periodicitatea informaţiei),
own_position (poziţia din topologia creată).
- Pentru o topologie de tip graf: index, muchii. Pentru o structură de tip graf, numărul de noduri este egal cu numărul de procese din grup. De aceea numărul nodurilor nu trebuie memorat separat. Funcţiile MPI_GRAPHDIMS_GET şi MPI_GRAPH_GET primesc informaţii privind topologia grafului care au fost asociate cu un comunicator de către funcţia MPI_GRAPH_CREATE. Informaţiile 100
furnizate de MPI_GRAPHDIMS_GET pot fi utilizate pentru dimensionarea corectă a vectorilor index şi edges pentru a realiza un apel MPI_GRAPH_GET. MPI_ Graphdims_get (comm, nnodes, nedges) [ IN comm] [ OUT nnodes] [ OUT nedges]
- comunicator cu grupul care are structura de graf - numărulde noduri din graf (acelaşi cu numărul proceselor din grup) - numărul de muchii din graf
Prototipul funcţiei este următorul: int MPI_Graphdims_get(MPI_Comm comm, int *nnodes, int *nedges) MPI_Graph_get (comm, maxindex, maxedges, index, edges) [ IN comm] [ IN maxindex] [ IN maxedges] [ OUT index] [ OUT edges]
comunicator cu structura grafului lungimea vectorului index în programul apelant lungimera vectorului edges în programul apelant tablou de întregi ce conţine structura grafului tablou de întregi ce conţine structura grafului
Prototipul funcţiei este următorul: int MPI_Graph_get(MPI_Comm comm, int maxindex, int maxedges, int *index, int *edges)
Funcţiile MPI_ Graph_neighbors_count şi MPI_Graph_neighbors furnizează informaţii suplimentare pentru o topologie de tip graf. MPI_Graph_neighbors_count (comm, rank, nneighbors) [ IN comm] [ IN rank] [ OUT nneighbors]
- comunicator cu topologia grafului - rangul procesului din grupul comm - numărul vecini ai procesului specificat
Prototipul funcţiei este următorul: int MPI_Graph_neighbors_count(MPI_Comm comm, int rank, int *nneighbors) MPI_Graph_neighbors (comm, rank, maxneighbors, neighbors)
101
[ IN comm] [ IN rank] [ IN maxneighbors] [ OUT neighbors]
-
comunicator cu topologia grafului rangul procesului din grupul comm mărimea tabloului cu vecinii rangurile proceselor care au ca vecini procesul specificat
Prototipul funcţiei este următorul: int MPI_Graph_neighbors(MPI_Comm comm, int rank, int maxneighbors, int *neighbors)
Funcţii pentru interogarea topologiei Extragerea informaţiilor privind o anumită topologie se poate face cu următoarele funcţii: MPI_Topo_test (comm, status) [ IN comm] [ OUT status]
- comunicator - tipul topologiei comunicatorului
Prototipul funcţiei este următorul: int MPI_Topo_test(MPI_Comm comm, int *status)
Funcţia MPI_Topo_test întoarce tipul topologiei asignate unui comunicator. Aceste valori pot fi: [ MPI_GRAPH] [ MPI_CART] [ MPI_UNDEFINED]
102
- topologie de tip graf - topologie de tip cartezian - nici o topologie
Instructiuni de instalare, compilare şi execuţie a aplicaţiilor care folosesc biblioteca MPI Pentru instalarea oricărei distribuţii de MPI, comercială sau academică, trebuie citite cu atenţie instrucţiunile de instalare puse la dispoziţie de producător. Cerinţele sistem sunt fie Windows NT 4.0, Windows 2000 sau WindowsXP, fie un sistem Linux (orice distributie) sau Unix. În cazul Linux, producătorii RedHat şi Suse (Novell) au inclus în distribuţia standard versiunea LAM-MPI (http://www.lam-mpi.org/) şi/sau MPICH (http://www-unix.mcs.anl.gov/mpi/). Indiferent de producatorul ales, instalarea si comenzile de utilizare sunt aproximativ asemănătoare. Pentru a putea folosi în programele proprii funcţiile puse la dispoziţie de biblioteca MPI, în codul sursa trebuie să existe următoarea linie: #include “mpi.h”
Crearea si compilarea unei aplicaţii folosind VisualC++ 6.0 [5]: 1. Deschidere MS Developer Studio - Visual C++ şi crearea unui proiect nou.
103
2. Se alege Project->Settings (sau Alt +F7 ) 3. Se schimbǎ setările pentru a utiliza biblioteca cu fire de execuţie.
104
4. Se modificǎ calea pentru includerea bibliotecilor (de exemplu: C:\Program Files\MPICH\SDK\include).
5. Se modificǎ calea pentru includerea fisierelor obiect asociate bibliotecii mpi.h (de exemplu: C:\Program Files\MPICH\SDK\lib )
105
6. Se adaugǎ mpich.lib la proiect
7. Se închide casuţa de dialog cu setarile proiectului
106
8. Se adaugă fişierele sursǎ la proiect.
9. Compilare Rularea aplicaţiei se face folosind fie interfaţa graficǎ guiMPIRun,
fie linia de comanda C:\Program Files\MPICH\bin\mpd\mpirun –np 4 prog.exe
107
Crearea şi compilarea unei aplicaţii pe sisteme Unix si Linux [4]: MPICH: Compilarea folosind GNU C Compiler (gcc): cc –o prog prog.c –I/usr/local/mpi/include \ –L/usr/local/mpi/lib -lmpi
Rularea unei aplicaţii pentru 4 procese: mpirun –np 4 prog
LAM-MPI: Compilarea folosind GNU C Compiler (gcc): mpicc –o prog prog.c
Pentru rularea unei aplicatii trebuie pornit mediul MPI (implicit pe localhost) lamboot –v
Pentru a rula aplicatia pe mai multe sisteme trebuie creat un fisier 1 lamhosts in care se scriu adresele IP ale sistemelor sau numele : calc1.domeniu.ro 192.168.247.10 calc2.domeniu.ro 192.168.247.11 calc3.domeniu.ro 192.168.247.12 calc4.domeniu.ro 192.168.247.13 Linia de comanda se va modifica corespunzator: lamboot –v lamhosts
Verificarea faptului ca LAM a fost pornit se va face folosind: recon -v lamhosts
Rularea programelor: mpirun-lam
-np
4
prog
În caz de eroare se foloseşte comanda lamcleen
Pentru oprirea LAM: wipe –v lamhosts 1
Numele si adresele IP sunt pentru exemplificare
108
PARTEA a-III-a EXEMPLE DE PROGRAME MPI Autor: Bogdan Romanescu
109
110
/**************** Calculul prefixelor *****************/ /* Compilare : mpicc -o xxx xxx.c Executie : mpirun -np 8 xxx Observatii: - Algoritmul se va rula doar pt un nr de procese(noduri) N+1 putere a lui 2,minim 8(8, 16,...) - Nodul 0 nu participa la comunicare si nu face parte din arborele prefixelor */ #include #include "mpi.h" #define N
7
//numarul de noduri; size va trebui sa //fie >= N+1
main(int argc, char **argv) { int A[N], B[N]; int k, j, aux, aux1, aux2; int myrank, size; MPI_Status status; //initializari MPI MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myrank); MPI_Comm_size(MPI_COMM_WORLD, &size); if(myrank == 0) printf("\n***** Calculul prefixului paralel *****\n\n"); /********* Calcul Prefixe ********/ //nodurile frunza if(myrank >= size/2) { //informatia initiala o constitue rangurile //nodurilor A[myrank] = myrank; //se trimite nodului parinte informatia - inceputul //fazei 1: parcurgerea arborelui de jos in sus MPI_Send(&A[myrank], 1, MPI_INT, myrank/2, 99, MPI_COMM_WORLD);
111
//se asteapta de la nodul parinte primirea B-ului MPI_Recv(&aux, 1, MPI_INT, myrank/2, 99, MPI_COMM_WORLD, &status); B[myrank] = aux; //nodurile pare asteapta si informatia de la nodul //impar imediat urmator(de pe acelasi nivel) if(myrank%2==0) MPI_Recv(&B[myrank], 1, MPI_INT, myrank+1, 99, MPI_COMM_WORLD, &status); //nodurile impare efectueaza calculul prefixului si //trimit rezultatul in stanga(pe acelasi nivel) la //nodul par inferior else { aux = B[myrank] - A[myrank]; MPI_Send(&aux, 1, MPI_INT, myrank-1, 99, MPI_COMM_WORLD); } } //nodurile de pe nivelele superioare, cu exceptia //radacinii si a nodului 0 care nu apartine arborelui if(myrank < size/2 && myrank !=0 && myrank!=1) { //se asteapta primirea datelor de la cei 2 fii MPI_Recv(&aux1, 1, MPI_INT, myrank*2, 99, MPI_COMM_WORLD, &status); MPI_Recv(&aux2, 1, MPI_INT, myrank*2+1, 99, MPI_COMM_WORLD, &status); //se calculeaza A-ul nodului care este trimis si //parintelui A[myrank] = aux1+aux2; MPI_Send(&A[myrank], 1, MPI_INT, myrank/2, 99, MPI_COMM_WORLD); //se asteapta B-ul de la parinte MPI_Recv(&aux, 1, MPI_INT, myrank/2, 99, MPI_COMM_WORLD, &status); //nodurile pare primesc datele de la cele impare //superioare(+1) de pe acelasi nivel si fac calculul if(myrank%2==0) { MPI_Recv(&aux1, 1, MPI_INT, myrank+1, 99, MPI_COMM_WORLD, &status); B[myrank] = aux - aux1; } //nodurile impare isi actualizeaza informatia legata //de B si trimit datele celor pare pt a face //calculele else {
112
B[myrank] = aux; MPI_Send(&A[myrank], 1, MPI_INT, myrank-1, 99, MPI_COMM_WORLD); } //nodurile parinte trimit catre cei 2 fii datele MPI_Send(&B[myrank], 1, MPI_INT, myrank*2, 99, MPI_COMM_WORLD); MPI_Send(&B[myrank], 1, MPI_INT, myrank*2+1, 99, MPI_COMM_WORLD); } //nodul radacina - nu face comunicarea pe orizontala if(myrank==1) { //se asteapta de la nodurile 2 si 3 A-urile MPI_Recv(&aux1, 1, MPI_INT, myrank*2, 99, MPI_COMM_WORLD, &status); MPI_Recv(&aux2, 1, MPI_INT, myrank*2+1, 99, MPI_COMM_WORLD, &status); A[myrank] = aux1+aux2; //se calculeaza B-ul si se incepe parcurgerea //arborelui de sus in jos - faza a 2a : calculul B//urilor B[myrank] = A[myrank]; MPI_Send(&B[myrank], 1, MPI_INT, myrank*2, 99, MPI_COMM_WORLD); MPI_Send(&B[myrank], 1, MPI_INT, myrank*2+1, 99, MPI_COMM_WORLD); } //la final se verifica rezultatele if(myrank == 0) printf("Rezultatele din fiecare nod la finalul algoritmului:\n\n"); //toate procesele sunt asteptate sa termine MPI_Barrier(MPI_COMM_WORLD); //nodul 0 nu e implicat in algoritm si nu afiseaza if(myrank!=0) printf("P%d: A[%d]=%d B[%d]=%d\n", myrank, myrank, A[myrank], myrank, B[myrank]); MPI_Finalize(); }
113
/********** Scurtcircuitarea; Calcul ranguri **********/ /* Compilare : mpicc -o xxx xxx.c -lm Executie : mpirun -np 8 xxx Observatii: - Numarul de procese poate fi oricat de mare(in program maxim 99) - log2(x) = log(x)/log(2) */ #include #include #include "mpi.h" main(int argc, char **argv) { //p este lista de legaturi - utila pt a vedea in fiecare //pas al algoritmului modificarea legaturilor int p[100], i; int rank, r[100]; int myrank, size; MPI_Status status; int succ=0; int root = 0; //initializari MPI MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myrank); MPI_Comm_size(MPI_COMM_WORLD, &size); if(myrank == 0) printf("\n***** Scurtcircuitarea; Calcul ranguri *****\n\n"); MPI_Barrier(MPI_COMM_WORLD); /****** Calcul ranguri *******/ //construirea legaturile initiale intre noduri if(myrank==0) succ = 0; else succ = myrank-1; //initial nodul 0 are rang 0;celelalte noduri au rangul 1 if(succ != myrank) rank = 1; else rank = 0;
114
//rootul colecteaza informatiile despre succesori si //ranguri de la celelate noduri MPI_Gather(&succ, 1, MPI_INT, p, 1, MPI_INT, root, MPI_COMM_WORLD); MPI_Gather(&rank, 1, MPI_INT, r, 1, MPI_INT, root, MPI_COMM_WORLD); //si apoi le redistribue inapoi MPI_Bcast(p, size, MPI_INT, root, MPI_COMM_WORLD); MPI_Bcast(r, size, MPI_INT, root, MPI_COMM_WORLD); //afisare inainte de inceperea algoritmului printf("P%d - are inital rangul %d si lista de legaturi: ", myrank, rank); for(i=0; i complexitatea //O(log(nr_noduri)) for(i=0; i
115
/********* Difuzarea unu-la-toti pe hipercub *********/ /* Compilare : mpicc -o xxx xxx.c -lm Executie : mpirun -np 8 xxx Observatii: - size trebuie sa fie putere a lui 2 (se lucreaza pe hipercub) */ #include #include #include #include
"mpi.h"
main(int argc, char **argv) { char message[20]; int myrank, size, p; double d; MPI_Status status; int mask, i, msg_dest, msg_src; int rez; //initializari MPI MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myrank); MPI_Comm_size(MPI_COMM_WORLD, &size); if(myrank == 0) printf("\n***** Difuzarea unu-la-toti pe hipercub *****\n\n"); //procesul root pregateste mesajul de trimis; celelalte //procese trec in asteptare if(myrank == 0) strcpy(message, "Mesajul de trimis"); else strcpy(message, "Mesajul nu a ajuns"); if(myrank == 0 ) printf("Inainte de inceperea algoritmului:\n"); //afisare la inceput MPI_Barrier(MPI_COMM_WORLD); printf("P %d initial: %s\n", myrank, message); MPI_Barrier(MPI_COMM_WORLD); //se calculeaza numarul pasilor == numarul dimensiunilor //cubului d = log(size)/log(2);
116
/***** ONE_TO_ALL_BC *****/ //se seteaza toti bitii mastii pe 1 mask = pow(2, d) - 1; for(i=d-1; i>=0; i--) { //se seteaza bitul i al mastii pe 0 p = (int)pow(2, i); // p este de fapt 2^i mask = mask ^ p; if((myrank & mask) == 0) { //daca bitii inferiori i sunt 0, nodul trimite if((myrank & p) == 0) { msg_dest = myrank ^ p; MPI_Send(message, strlen(message), MPI_CHAR, msg_dest, 99, MPI_COMM_WORLD); } //daca nu, primeste else { msg_src = myrank ^ p; MPI_Recv(message, strlen(message), MPI_CHAR, msg_src, 99, MPI_COMM_WORLD, &status); } } //verificare dupa fiecare etapa if(myrank == 0 ) printf("\nDupa faza %d:\n", d-i); MPI_Barrier(MPI_COMM_WORLD); printf("P %d : %s\n", myrank, message); MPI_Barrier(MPI_COMM_WORLD); } MPI_Barrier(MPI_COMM_WORLD); if(myrank == 0 ) printf("\nLa finalul algoritmului:\n"); MPI_Barrier(MPI_COMM_WORLD); //verificarea la final - mesajul initial trebuie sa fie la //toate celelalte noduri de pe hipercub printf("P %d final: %s\n", myrank, message); MPI_Finalize(); }
117
/********** Difuzarea toti-la-toti pe hpercub **********/ /* Compilare: mpicc -o xxx xxx.c -lm Executie: mpirun -np 8 xxx Observatii: - size trebuie sa fie putere a lui 2 (se lucreaza pe hipercub) - log2(x) = log(x)/log(2) */ #include #include #include #include
"mpi.h"
#define N 8 //numarul maxim de noduri main(int argc, char **argv) { //vectorii de mesaje: lungime fiecare mesaj(20) * N + 1 char message[21], recv_mes[161], all_mes[161], aux[161]; int myrank, size, p; double d; MPI_Status status; int
i, j, msg_partener;
//initializari MPI MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myrank); MPI_Comm_size(MPI_COMM_WORLD, &size); if(myrank == 0) printf("\n***** Difuzarea toti-la-toti pe hpercub *****\n\n"); //se pregateste mesajul de trimis strcpy(message, "Mesajul de trimis //'\0' trebuie sa fie pe pozitia 20 message[18] = '0'+ myrank;
");
//toate celelalte procese au numai '_' in mesaje la //momentul initial for(i=0; i<160; i++) { all_mes[i] = '_'; recv_mes[i] = '_'; aux[i] = '_'; } all_mes[160] = '\0'; recv_mes[160] = '\0';
118
//fiecare nod isi pregateste propriul mesaj in pozitia //corespunzatoare din vectorul de mesaje for(i=0; i<19; i++) all_mes[20*myrank+i] = message[i]; if(myrank == 0 ) printf("Inainte de inceperea algoritmului:\n"); MPI_Barrier(MPI_COMM_WORLD); printf("\tP %d : %s**\n\n", myrank, all_mes); //dimensiunea hipercubului d = log(size)/log(2); /******* ALL_TO_ALL_BC *******/ for(i=0; i<=d-1; i++) { //comunicarea cu nodul partener - la fiecare pas se //lucreaza pe cate o dimensiune msg_partener = myrank ^ (int)(pow(2, i)); MPI_Send(all_mes, 161, MPI_CHAR, msg_partener, 99, MPI_COMM_WORLD); MPI_Recv(recv_mes, 161, MPI_CHAR, msg_partener, 99, MPI_COMM_WORLD, &status); //mesajele primite de la partener sunt copiate in //pozitiile libere for(j=0; j<160; j++) if(all_mes[j]=='_' ) all_mes[j] = recv_mes[j]; } //afisarea mesajelor din noduri pentru verificare if(myrank == 0 ) printf("\n\nLa finalul algoritmului:\n"); MPI_Barrier(MPI_COMM_WORLD); printf("P %d: %s\n", myrank, all_mes); MPI_Finalize(); }
119
/****************** Sortarea bitonica ******************/ /* Compilare : mpicc -o xxx xxx.c -lm Executie : mpirun -np 8 xxx Observatii: - N_MAX(8 in program) reprezinta numarul elementelor de sortat si trebuie sa fie egal cu numarul de procese - d=0 sortare crescatoare, d=1 descrescatoare */ #include #include #include #include
"mpi.h"
#define N_MAX 8 //N_MAX tb sa fie egal cu nr de procese /***** algortim de interclasare a 2 secvente bitonice ***/ void interclasare_bitonica(int A[N_MAX], int pi, int pf, int d,int rank) { int b, aux; int j, i, new_val; MPI_Status status; b = pf - pi+1; //daca se compara doar 2 elemente, doar nodul 0 sorteaza //elementele if(b == 2) { if(rank==0) { if(A[pi] < A[pf]) { if(d==1) { aux = A[pf]; A[pf] = A[pi]; A[pi] = aux; } } else { if(d==0) { aux = A[pf]; A[pf] = A[pi]; A[pi] = aux; }
120
else
}
}
}
//pe aceasta ramura lucreaza in paralel toate //procesele { //actualizarea valorilor fiecarui nod MPI_Barrier(MPI_COMM_WORLD); MPI_Bcast(A, N_MAX, MPI_INT, 0, MPI_COMM_WORLD); new_val = A[rank]; for(j=b/2-1; j>=0; j--) { //actualizarea vectorilor la toate procesele MPI_Bcast(A, N_MAX, MPI_INT, 0, MPI_COMM_WORLD); //mai intai se sorteaza cate 2 valori; //sortarea e realizata de procesele mici(de //rang inferior) if(rank==pi+j)//proces inferior { if(A[pi+j] < A[pi+b/2+j]) { if(d==1) { aux = A[pi+b/2+j]; A[pi+b/2+j] = A[pi+j]; A[pi+j] = aux; } } else { if(d==0) { aux = A[pi+b/2+j]; A[pi+b/2+j] = A[pi+j]; A[pi+j] = aux; } } new_val = A[pi+j]; //procesele mai mici trimit rezultatul //catre cele superioare MPI_Send(&A[pi+b/2+j], 1, MPI_INT, pi+b/2+j, 99, MPI_COMM_WORLD); } if(rank == pi+b/2+j)//proces superior { //procesele superioare primesc
121
MPI_Recv(&new_val, 1, MPI_INT, pi+j, 99, MPI_COMM_WORLD, &status); }
} //dupa comparatii, se reactualizeaza datele din //noduri MPI_Barrier(MPI_COMM_WORLD); MPI_Gather(&new_val, 1, MPI_INT, A, 1, MPI_INT, 0, MPI_COMM_WORLD); MPI_Bcast(A, N_MAX, MPI_INT, 0, MPI_COMM_WORLD);
}
MPI_Barrier(MPI_COMM_WORLD); interclasare_bitonica(A, pi, pi+b/2-1, d,rank); interclasare_bitonica(A, pi+b/2, pf, d,rank); }
/******** algoitmul Batcher de sortare bitonica *********/ void sortare_bitonica(int A[N_MAX], int pi, int pf, int d,int rank) { int b,i, aux; b = pf - pi+1; //daca se compara 2 elemente, if(b == 2) { if(rank==0) { if(A[pi] < A[pf]) { if(d==1) { aux = A[pf] A[pi] } } else { if(d==0) { aux = A[pf] A[pi] } } } } else
122
sortarea o face nodul 0
A[pf]; = A[pi]; = aux;
A[pf]; = A[pi]; = aux;
{ sortare_bitonica(A, pi,pi+b/2-1, 0, rank); sortare_bitonica(A, pi+b/2, pf, 1, rank);
}
//se reactualizeaza informatiile de la toate //nodurile MPI_Bcast(A, N_MAX, MPI_INT, 0, MPI_COMM_WORLD); interclasare_bitonica(A, pi, pf, d, rank); }
/********************************************************/ main(int argc, char **argv) { int myrank, size; MPI_Status status; int id, i, d; //numerele de sortat int A[N_MAX]= {7, 5, 9, 10, 2, 6, 3, 8}; //initializari MPI MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myrank); MPI_Comm_size(MPI_COMM_WORLD, &size); if(myrank==0) { printf("\n***** Sortarea bitonica (Algoritmul lui Batcher) *****\n\n"); printf("Inainte de sortare:\n"); for(i=0; i
123
/***************** Sortarea prin numarare **************/ /* Compilare : mpicc -o xxx xxx.c -lm Executie : mpirun -np 8 xxx Observatii: - numarul de procese = 2 * nr_elem - numarul de elemente trebuie sa fie putere a lui 2; eventual se poate completa vectorul de sortat cu 0 */ #include #include #include #include
"mpi.h"
#define N_MAX
4 //numarul maxim de elemente
main(int argc, char **argv) { int myrank, size; MPI_Status status; int A[N_MAX], A_sortat[N_MAX]; int R[2*N_MAX-1][N_MAX], Rsend[N_MAX], Rrecv_1[N_MAX], Rrecv_2[N_MAX]; int P[N_MAX], poz; int i, j; //valorile initale A[0] = 2; A[1] = 6; A[2] = 3; A[3] = 8; //initializari MPI MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myrank); MPI_Comm_size(MPI_COMM_WORLD, &size); //afisare initiala if(myrank==0) { printf("\n***** Sortarea prin numarare *****\n\n"); printf("P %d - lista initiala: ", myrank); for(i=0; i
124
//Faza 1: inceperea sortarii - determinarea rangurilor //relative; //pentru usurarea comunicarii,tabloul bidimensional este //conceput ca un arbore; //ultimele N noduri(frunze) fac calculul initial if(myrank >=size/2) { for(i=0; i= size/2) MPI_Send(R[myrank], N_MAX, MPI_INT, myrank/2, 99, MPI_COMM_WORLD); //parintele prelucreaza datele else { //pentru noduri diferite de radacina arborelui(nodul //0 nu este folosit) if((myrank < size/2) && (myrank!=0) && (myrank!=1)) { //primeste de la cei 2 fii MPI_Recv(&Rrecv_1, N_MAX, MPI_INT, myrank*2, 99, MPI_COMM_WORLD, &status); MPI_Recv(&Rrecv_2, N_MAX, MPI_INT, myrank*2+1, 99, MPI_COMM_WORLD, &status); //actualizeaza vectorul propriu prin sumare for(i=0; i
125
}
}
MPI_Recv(&Rrecv_1, N_MAX, MPI_INT, myrank*2, 99, MPI_COMM_WORLD, &status); MPI_Recv(&Rrecv_2, N_MAX, MPI_INT, myrank*2+1, 99, MPI_COMM_WORLD, &status); //calculeaza rezultatul final for(i=0; i
MPI_Barrier(MPI_COMM_WORLD); //nodul 1 distribuie nodurilor pozitiile pe care trebuie //sa se afle elementele lor //informatia va fi folosita doar de primele N_MAX noduri MPI_Bcast(R[1], N_MAX, MPI_INT, 1, MPI_COMM_WORLD); //fiecare proces determina pozitia elementului sau // se pleaca de la 0 if(myrank < N_MAX) poz = R[1][myrank]-1; //nodul 0 actualizeaza imediat vectorul final if(myrank == 0) A_sortat[poz] = A[0]; //el astepta apoi de la urmatoarele N_MAX-1 noduri : intai //pozitia si apoi valoarea de inserat if(myrank==0) { for(i=1;i
126
}
}
//afisarea finala if(myrank==0) { printf("\nP %d - lista sortata: ", myrank); for(i=0; i
127
/********** Sortarea par-impar (ODD_EVEN_SORT) *********/ /* Compilare : mpicc -o xxx xxx.c Executie : mpirun -np 8 xxx Observatii: - Numarul de procese este egal cu numarul elementelor de sortat(acesta poate fi schimbat in program) */ #include #include #include "mpi.h" #define N
8 //numarul elementelor de sortat
main(int argc, char **argv) { int myrank, size; MPI_Status status; int id, i, n, cur_number, aux_number; int V[N]={3, 2, 3, 8, 5, 6, 4, 1}; //initializari MPI MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myrank); MPI_Comm_size(MPI_COMM_WORLD, &size); //afisarea initiala if(myrank==0) printf("\n***** Sortarea par-impar (ODD_EVEN_SORT) *****\n\n"); /******* Odd_Even_Sort **********/ id = myrank+1; n = size; //din vectorul initial, fiecare nod vede un singur element cur_number = V[myrank]; //nodurile afiseaza numerele proprii if(id==1) printf("Inainte de sortare:\n"); MPI_Barrier(MPI_COMM_WORLD); printf("P[%d]: %d\n", id, cur_number); //pt comparatie ,procesele mai mari trimit elementele //catre cele mai mici for(i=1; i<=n; i++) { //sincronizare necesara pt ca toate procesele sa //inceapa o noua etapa concomitent MPI_Barrier(MPI_COMM_WORLD);
128
if(i % 2 == 1)//i impar { //id impar - proces "inferior" if(id % 2 == 1) { //primeste de la corespondent MPI_Recv(&aux_number, 1, MPI_INT, (id1)+1, 99, MPI_COMM_WORLD, &status); //dupa calcul, trimite procesului //corespondent maximul; se pastreaza //minimul if(aux_number < cur_number) { MPI_Send(&cur_number, 1, MPI_INT, (id-1)+1, 99, MPI_COMM_WORLD); cur_number = aux_number; } else MPI_Send(&aux_number, 1, MPI_INT, (id-1)+1, 99, MPI_COMM_WORLD); } else//proces "superior" { //trimite valoarea sa pentru a putea fi //comparata MPI_Send(&cur_number, 1, MPI_INT, (id1)-1, 99, MPI_COMM_WORLD); //se primeste maximul MPI_Recv(&cur_number, 1, MPI_INT, (id-1) -1, 99, MPI_COMM_WORLD, &status); } } else//i par { //primul si ultimul nod nu comunica in aceasta //etapa if((id!=1) && (id!=N)) { if(id % 2 == 0)//proces "inferior" { //se primeste valoarea //corespondentului "superior" MPI_Recv(&aux_number, 1, MPI_INT, (id-1)+1, 99, MPI_COMM_WORLD, &status);
129
}
}
//se trimite maximul si se //pastreaza minimul if(aux_number < cur_number) { MPI_Send(&cur_number, 1, MPI_INT, (id-1)+1, 99, MPI_COMM_WORLD); cur_number = aux_number; } else MPI_Send(&aux_number, 1, MPI_INT, (id-1)+1, 99, MPI_COMM_WORLD); } else//proces "superior" { //trimite valoare sa si apoi //receptioneaza maximul de la //corespondent MPI_Send(&cur_number, 1, MPI_INT, (id-1)-1, 99, MPI_COMM_WORLD); MPI_Recv(&cur_number, 1, MPI_INT, (id-1)-1, 99, MPI_COMM_WORLD, &status); } }
//afisarea finala if(id == 1) printf("\n\nDupa sortare:\n"); MPI_Barrier(MPI_COMM_WORLD); printf("P[%d]: %d\n", id, cur_number); MPI_Finalize(); }
130
/************ Sortare bitonica pe hipercub ************/ /* Compilare : mpicc -o xxx xxx.c -lm Executie : mpirun -np 8 xxx Observatii: - Numarul de elemente de sortat tb sa fie egal cu numarul de procese(8 in program); - Deoarece se lucreaza pe hipercub, numarul de procese trebuie sa fie putere a lui 2. - Secventa de la care se pleaca tb sa fie bitonica - Sortarea se realizeaza: d=0 crescator, d=1 descrescator */ #include #include #include #include "mpi.h" #define N_MAX
8 // egal cu numarul de procese
/****** comunicarea pe o anumita dimensiune : E ********/ void E(int A[N_MAX], int dim, int myrank, int d) { int msg_partener; int aux, data_recv, data_send; MPI_Status status; //determinarea partenerului de comunicatie msg_partener = myrank ^ (int)(pow(2, dim)); //nodurile mai mici efectueaza calculele;tot ele pastreaza //minimul daca d = 0/maximul pentru d=1 if(myrank < msg_partener) { MPI_Recv(&data_recv, 1, MPI_INT, msg_partener, 99, MPI_COMM_WORLD, &status); if(d == 0) //sortare crescatoare { if(data_recv < A[myrank]) //datele trebuie interschimbate { data_send = A[myrank]; A[myrank] = data_recv; } else data_send = data_recv; } else //sortare descrescatoare { if(data_recv > A[myrank]) //datele trebuie interschimbate {
131
else
else
}
data_send = A[myrank]; A[myrank] = data_recv; }
data_send = data_recv; } //se trimite partenerului de comunicatie rezultatul //calculelor MPI_Send(&data_send, 1, MPI_INT, msg_partener, 99, MPI_COMM_WORLD); } //nodurile superioare trimit datele si asteapta //rezultatul comparatiei { //se trimite data MPI_Send(&A[myrank], 1, MPI_INT, msg_partener, 99, MPI_COMM_WORLD); //si se asteapta rezultatul calculelor MPI_Recv(&A[myrank], 1, MPI_INT, msg_partener, 99, MPI_COMM_WORLD, &status); }
/************ procesul de combinare M ******************/ // necesita utilizarea succesiva a dimensiunilor E void Merge(int A[N_MAX], int pas, int myrank, int d) { int i; for(i=pas-1; i>=0; i--) { if(myrank == 0) printf("Se comunica pe dimensiunea %d.\n", i+1); E(A, i, myrank, d); } } /** sortarea bitonica pe cubul binar multidimensional ***/ void BitonicMergeSort(int A[N_MAX], int dim, int myrank, int d) { int i; for(i=dim-1; i>=0; i--) { if(myrank == 0) printf("\nFaza %d de combinare.\n", i); Merge(A, i+1, myrank, d);
132
}
}
/********************************************************/ main(int argc, char **argv) { int myrank, size; MPI_Status status; int id, i, d; double dim; //vectorul de sortat int A[N_MAX]={2, 3, 4, 8, 7, 6, 5, 1}; //initializari MPI MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myrank); MPI_Comm_size(MPI_COMM_WORLD, &size); //dim = log2(size) dim = log(size)/log(2); if(myrank==0) { printf("\n***** BitonicMergeSort pe hypercube *****\n\n"); printf("Inainte de sortare:\n"); for(i=0; i
133
/************* Sortare rapida pe hipercub ***************/ /* Compilare : mpicc -o xxx xxx.c -lm Executie : mpirun -np 8 xxx Observatii: - size trebuie sa fie putere a lui 2(se lucreaza pe hipercub) - Numarul maxim de elemente de sortat = 20(poate fi schimbat in program) - Modul de alegere a pivotului este media aritmetica; daca numerele de sortat au o abatere mare, vor fi noduri care la final nu vor avea elemente */ #include #include #include #include
"mpi.h"
#define N
20 // nr maxim de elemente ale vectorului
/*************** partitionare vector *****************/ //impartirea lui B in 2 subsiruri B1 si B2 a.i. //B1 <= pivot <= B2 void Partition(int B[N+1], int B1[N+1], int B2[N+1], int pivot) { int i, nr_b1, nr_b2; nr_b1 = 0; //nr de elemente din cei 2 vectori : B1 si B2 nr_b2 = 0; //daca vectorul B are elemente if(B[0] != 0) { for(i=1; i<= B[0]; i++) { //se face trierea fata de pivot if( B[i] <= pivot) { nr_b1++; B1[nr_b1] = B[i]; } else { nr_b2++; B2[nr_b2] = B[i]; } } }
134
//pe prima pozitie a vectorilor se memoreaza nr lor de //elemente B1[0] = nr_b1; B2[0] = nr_b2; } /********* media aritmetica a valorilor unui vector *****/ int Medie(int X[N+1]) { int S = 0; int i; //daca vectorul are elemente if(X[0] > 0) { for(i=1; i<=X[0]; i++) S = S + X[i]; S = S / X[0]; } else S = 0; return S; } /**************** Quick Sort secvential *****************/ // va fi folosit pentru sortarea elementelor finale din //fiecare nod int partition_seq( int a[N+1], int low, int high ) { int left, right; int aux; int pivot_item; pivot_item = a[low]; left = low; right = high; while ( left < right ) { // Muta stanga while item < pivot while( a[left] <= pivot_item ) left++; // Muta dreapta while item > pivot while( a[right] > pivot_item ) right--; if ( left < right ) //SWAP(a,left,right); { aux = a[left];
135
a[left] = a[right]; a[right] = aux; }
} // right este poz finala pt pivot a[low] = a[right]; a[right] = pivot_item; return right; } void quicksort_seq( int a[N+1], int low, int high ) { int pivot; if ( high > low ) { pivot = partition_seq( a, low, high ); quicksort_seq( a, low, pivot-1 ); quicksort_seq( a, pivot+1, high ); } } /********************************************************/ main(int argc, char **argv) { int B[N+1], B1[N+1], B2[N+1], C[N+1]; //N+1 elemente deoarece pe prima pozitie se memoreaza nr //de elemente //vectorul original de valori int V[N]={2, 7, 5, 4, 11, 10, 16, 3, 18, 20, 21, 16, 10, 5, 6, 921, 16, 32, 0, 13}; int nod_decizie, nr_transmisii, medie_val; int myrank, size, p, aux, pivot; double d; MPI_Status status; int
i, j, k, l, msg_partener;
//initializari MPI MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myrank); MPI_Comm_size(MPI_COMM_WORLD, &size); //initializarea vectorilor for(i=1; i
136
C[i] = -1; } //distribuirea elementelor in noduri se face in mod egal; //daca raman elemente, acestea vor fi preluate de ultimul //nod if(myrank != size-1) { j = 0; for(i=0; i< N/size; i++) { B[i+1] = V[myrank * (N/size) + i]; j++; } B[0] = j; } else { j = 0; for(i=0; i< N/size; i++) { B[i+1] = V[myrank * (N/size) + i]; j++; } for(i=0; i< N%size; i++) { B[N/size+i+1] = V[(myrank+1)* (N/size)+ i]; j++; } B[0] = j; } if(myrank == 0 ) { printf("***** QuickSort pe hypercube *****\n"); printf("Inainte de inceperea algoritmului:\n"); } MPI_Barrier(MPI_COMM_WORLD); printf("P %d la inceput - %d elemente: ", myrank, B[0]); for(j=1; j<= B[0]; j++) printf("%d ", B[j]); printf("\n"); //d este log2(size) d = log(size) / log(2); for(i=d; i>=1; i--)
137
{ MPI_Barrier(MPI_COMM_WORLD); //faza de calculare a pivotului //nodul la care se iau deciziile privitoare la //pivot; aceste nod se modifica functie de pasul //comunicarii nod_decizie = myrank | ((int)(pow(2, i))-1); medie_val = Medie(B); //nr de noduri ce tb sa comunice cu nodul-decizie nr_transmisii = (int)(pow(2, i)); //toate nodurile transmit cu nodul-decizie //corespunzator MPI_Send(&medie_val, 1, MPI_INT, nod_decizie, 99, MPI_COMM_WORLD); //nodul decizie asteapta datele if(myrank == nod_decizie) { //se calculeaza pivotul pivot = 0; for(j=0; j< nr_transmisii; j++) { MPI_Recv(&aux, 1, MPI_INT, myrank-j, 99, MPI_COMM_WORLD, &status); //se adauga media la suma deja existenta pivot = pivot+aux; } //se face media mediilor si se determina //pivotul pivot = pivot / nr_transmisii; } //pivotul calculat este distribuit if(myrank == nod_decizie) { for(j=0; j< nr_transmisii; j++) MPI_Send(&pivot, 1, MPI_INT, myrank-j, 99, MPI_COMM_WORLD); } //toate procesele primesc decizia de la nodurile de //decizie MPI_Recv(&pivot, 1, MPI_INT, nod_decizie, 99, MPI_COMM_WORLD, &status); //si realizeaza partitionarea corespunzator Partition(B, B1, B2, pivot); MPI_Barrier(MPI_COMM_WORLD);
138
//procesul de comunicare in cadrul sortarii //determinarea partenerului de comunicatie msg_partener = myrank ^ (int)(pow(2, i-1)); //daca bitul i este 0 if(( myrank & ((int)(pow(2, i-1))) )== 0) { //se trimite B2 si se primeste C MPI_Send(&B2, N+1, MPI_INT, msg_partener, 99, MPI_COMM_WORLD); MPI_Recv(&C, N+1, MPI_INT, msg_partener, 99, MPI_COMM_WORLD, &status); //reuniunea multimilor B1 si C k = 1; l = 1; //daca exista elemente in vectorii primiti, //ele se trec in vectorul final B if(B1[0] > 0) { for(k=1; k<= B1[0]; k++) B[k] = B1[k]; } if(C[0] > 0) { for(l=1; l<= C[0]; l++) B[k+l-1] = C[l]; } //actualizarea nr de elem al lui B B[0] = k+l-2; } else//daca bitul i nu e 0 { //se trimite B1 si se primeste C MPI_Send(&B1, N+1, MPI_INT, msg_partener, 99, MPI_COMM_WORLD); MPI_Recv(&C, N+1, MPI_INT, msg_partener, 99, MPI_COMM_WORLD, &status); //reuniunea multimilor B2 si C k = 1; l = 1; //daca exista elemente in vectorii primiti, //ele se trec in vectorul final B if(B2[0] > 0) { for(k=1; k<= B2[0]; k++) B[k] = B2[k]; } if(C[0] > 0) { for(l=1; l<= C[0]; l++)
139
B[k+l-1] = C[l]; } //actualizarea nr de elem al lui B B[0] = k+l-2; } MPI_Barrier(MPI_COMM_WORLD); } //la final, nodurile care au cel putin 1 elem fac sortarea //secventiala if(B[0]>0) quicksort_seq(B, 1, B[0]); //verificare la final if(myrank == 0 ) printf("\nLa finalul algoritmului:\n"); MPI_Barrier(MPI_COMM_WORLD); printf("P %d la final - %d elemente: ", myrank, B[0]); if(B[0] > 0) { for(j=1; j<= B[0]; j++) printf("%d ", B[j]); printf("\n"); } else printf("\n"); MPI_Finalize(); }
140
/**** Transpunerea unei matrici pe arhitecturi plasa ****/ /* Compilare : mpicc -o xxx xxx.c -lm Executie : mpirun -np 4 xxx Observatii: - Numarul de procese(noduri) va trebui sa fie p = (N/Q)^2, unde N este nr de elem de pe fiecare linie si coloana a matricii initiale, iar Q este dimensiunea matricilor ce revin fiecarui proces - Pentru rularea exemplului pt matrice 8x8 pe o plasa de 16 procesoare, trebuie sa avem N=8 si Q=2 -> 16 procese */ #include #include #include #include
"mpi.h"
#define N #define Q
8 4 //nr de procese va tb sa fie p = (N/Q)^2
/*********** algoritmul de mutare ********************/ /*(x1, y1) - colt stanga sus al matricii in care se produce mutarea (x2, y2) - colt dreapta jos al matricii in care se produce mutarea Matricea are 4 cadrane egale ca dimensiune II | I ----|---III | IV */ void Muta(int x1, int y1, int x2, int y2, int pas, int myrank, int L[Q][Q]) { int i, j, k; int send[Q][Q], recv[Q][Q]; int dim_latura; int rand, col, src, dest; MPI_Status status; dim_latura = N/Q; for(k=0; k IV; III -> II for(i=0; i< pas/2; i++) //pas reprezinta nr de mutari ce //tb facute {
141
//se pregateste matricea de trimis for(k=0; k IV //pentru fiecare rand ce tb considerat for(rand = x1+i; rand< x1+i+pas/2; rand++) { //pe coloanele din cadranele drepte for(col = y1+pas/2; col<=y2; col++) { //nodul care trimite if( (myrank/ dim_latura == rand) && (myrank % dim_latura == col) ) { dest = myrank + dim_latura; MPI_Send(&send, Q*Q, MPI_INT, dest, 99, MPI_COMM_WORLD); } //nodul care primeste - pe un rand mai //jos if( (myrank/ dim_latura == rand+1) && (myrank % dim_latura == col) ) { src = myrank - dim_latura; MPI_Recv(recv, Q*Q, MPI_INT, src, 99, MPI_COMM_WORLD, &status); } } } //mutarea in sus : cadranul III -> II //pentru fiecare rand ce tb considerat for(rand = x2-i; rand> x2-i-pas/2; rand--) { //pe coloanele din cadranele drepte for(col = y1; col
142
} //nodul care primeste - pe un rand //sus if( (myrank/ dim_latura == rand-1) (myrank % dim_latura == col) { src = myrank + dim_latura; MPI_Recv(recv, Q*Q, MPI_INT, 99, MPI_COMM_WORLD, &status); } }
mai && ) src,
} //se asteapta terminarea unui rand de transmiteri MPI_Barrier(MPI_COMM_WORLD); } MPI_Barrier(MPI_COMM_WORLD); //mutarile pe orizontala - cadrane : II -> I; IV -> III for(i=0; i< pas/2; i++) { //se pregateste matricea de trimis for(k=0; k I //pentru randurile din cadranele superioare for(rand = x1; rand< x1+pas/2; rand++) { //pe coloanele ce tb considerate for(col = y1+i; col
143
}
if( (myrank/ dim_latura == rand) && (myrank % dim_latura == col+1) ) { src = myrank - 1; MPI_Recv(recv, Q*Q, MPI_INT, src, 99, MPI_COMM_WORLD, &status); } }
//mutarea in stanga : cadranul IV -> III //pe randurile din cadranele inferioare for(rand = (x2-pas/2)+1; rand<=x2; rand++) { //pe coloanele ce tb considerate for(col = y2-i; col> y2-i-pas/2; col--) { //nodul care trimite if( (myrank/ dim_latura == rand) && (myrank % dim_latura == col) ) { dest = myrank - 1; MPI_Send(&send, Q*Q, MPI_INT, dest, 99, MPI_COMM_WORLD); } //nodul care primeste - o coloana la //stanga if( (myrank/ dim_latura == rand) && (myrank % dim_latura == col-1) ) { src = myrank + 1; MPI_Recv(recv, Q*Q, MPI_INT, src, 99, MPI_COMM_WORLD, &status); } } } //se asteapta terminarea unui rand de transmiteri MPI_Barrier(MPI_COMM_WORLD); } //matricile din cadranele I si IV salveaza ceea ce au //primit if( (myrank / dim_latura >= x1) && (myrank / dim_latura < x1+pas/2) && (myrank % dim_latura >=y1+pas/2) && (myrank % dim_latura <= y2)) { for(k=0; k
144
}
for(j=0; j
if( (myrank / dim_latura >= x1+pas/2) && (myrank / dim_latura <= x2) && (myrank % dim_latura >=y1) && (myrank % dim_latura < y1+pas/2)) { for(k=0; k
145
for(i=0; i=2; pas=pas/2) //se parcurg toate variantele de submatrici for(i=0; i< N/Q; i=i+pas) for(j=0; j
146
}
A[i][j] = L[i][j];
//toate procesele trimit catre procesul 0 matricile locale if(myrank != 0) MPI_Send(&L, Q*Q, MPI_INT, 0, 99,MPI_COMM_WORLD); else { for(i=1; i
147
/*Algoritmul lui Cannon pentru inmultirea a doua matrici*/ /* Compilare : mpicc -o xxx xxx.c Executie : mpirun -np 4 xxx Observatii: - Numarul de procese va trebui sa fie p = (N/Q)^2, unde N este numarul de elemente de pe fiecare linie si coloana a matricii initiale, iar Q este dimensiunea matricilor ce revin fiecarui proces - Pentru rularea exemplului pt matrice 8x8 pe o plasa de 16 procesoare, trebuie sa avem N=8 si Q=2 -> 16procese */ #include #include #include #include
"mpi.h"
#define N #define Q
4 2 //nr de procese va tb sa fie p = (N/Q)^2
main(int argc, char **argv) { int myrank, size; MPI_Status status; MPI_Request req; int A[N][N], B[N][N], C[N][N]; int Alocal[Q][Q], Blocal[Q][Q], Clocal[Q][Q], Aaux[Q][Q], Baux[Q][Q], Caux[Q][Q]; int destA, srcA, destB, srcB; int rand, col, dist; int dim_latura; //cate noduri sunt pe fiecare latura a //plasei int nr_mutari; int pas; int i, j, k, l, a; //initializarea matricilor originale cu A == B k = 0; for(i=0; i
148
MPI_Comm_rank(MPI_COMM_WORLD, &myrank); MPI_Comm_size(MPI_COMM_WORLD, &size); //procesul 0 afiseaza matricile initiale if(myrank == 0) { printf("\n***** Algoritmul lui Cannon *****\n\n"); printf("Matricea initiala A:\n"); for(i=0; i
149
//daca este linia corespunzatoare if (myrank / dim_latura == rand) { //se calculeaza partenerii de comunicatie dist = (myrank+pas)% dim_latura; destA = rand * dim_latura + dist; dist = (myrank+ ( dim_latura - pas) ) % dim_latura; srcA = rand* dim_latura + dist; MPI_Send(&Alocal, Q*Q, MPI_INT, destA, 99,MPI_COMM_WORLD); MPI_Recv(&Aaux, Q*Q, MPI_INT, srcA, 99, MPI_COMM_WORLD, &status);
}
//actualizarea informatiilor primite for(k=0; k< Q; k++) for(l=0; l
MPI_Barrier(MPI_COMM_WORLD); //Etapa I - alinierea initiala //pe verticala pentru B-uri for(col=1; col< dim_latura; col++) { pas = dim_latura - col; //daca este coloana corespunzatoare if (myrank % dim_latura == col) { //se calculeaza partenerii de comunicatie destB = (myrank+pas*dim_latura) % size; srcB = (myrank +(dim_latura-pas)*dim_latura) % size; MPI_Send(&Blocal, Q*Q, MPI_INT, destB, 99,MPI_COMM_WORLD); MPI_Recv(&Baux, Q*Q, MPI_INT, srcB, 99, MPI_COMM_WORLD, &status); //actualizarea informatiilor primite for(k=0; k< Q; k++) for(l=0; l
} MPI_Barrier(MPI_COMM_WORLD);
150
//se pregatesc matricile rezultat locale pt inmultire for(l=0; l< Q; l++) for(j=0; j
151
//Etapa II - ultima inmultire locala - cea de dupa ultima //deplasare for(i=0; i< Q; i++) for(j=0; j
152
for(k=0; k
153
/*Rezolvarea unui sistem de ecuatii prin metoda eliminarii gaussiene*/ /* Compilare : mpicc -o xxx xxx.c Executie : mpirun -np 3 xxx Observatii: - Dimensiunea sistemului (numarul de necunoscute) trebuie sa fie egal cu numarul de procese(sistemul trebuie sa fie compatibil determinat). In program este rezolvat un sistem de grad 3: x1 + x2 + x3 = 6 -x1 + 2*x2 x3 = 0 3*x1 + 3*x2 - 3*x3 = 0 */ #include #include #include #include
"mpi.h"
#define N
3 //acesta va tb sa fie si nr de procese
/************ algoritmul eliminarii gaussiene **********/ void GAUSSIAN_ELIMINATION(double A[N][N],double b[N],double y[N], int myrank) { int k, i, j; MPI_Status status; for(k=0; k
154
{ // fiecare proces in paralel face rectificarea //conform pivotului if(myrank == i) { for (j = k +1; j
}
/************ rezolvarea unui sistem triunghiular *******/ /* -acest proces tb sa se execute secvential doar de un singur nod -paralelizarea este imposibila dat dependentelor fata de calculele precedente -pe baza lui y si a matricii A sup triunghiulare se calculeaza y final - ac va fi solutia sistemului */ void RezSistTri(double A[N][N], double y[N], int myrank) { int i, j, k; double aux; if(myrank == 0) for(k= N-2; k>=0; k--) { aux = 0; for(i=k+1; i
155
}
}
/********************************************************/ main(int argc, char **argv) { int myrank, size; MPI_Status status; double A[N][N] = { {1, 1, 1}, {-1,2,-1}, {3, 3,-3} }; double b[N] = {6, 0, 0}; double y[N] = {0, 0, 0}; int i, j, k, a; //initializari MPI MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myrank); MPI_Comm_size(MPI_COMM_WORLD, &size); //procesul 0 afiseaza matricea initala if(myrank == 0) { printf("\n***** Rezolvarea unui sistem de ecuatii prin metoda eliminarii gaussiene *****\n\n"); printf("Matricea initiala:\n"); for(i=0; i
156
//procesul 0 afiseaza matricea finala if(myrank == 0) { printf("\nMatricea A finala:\n"); for(i=0; i
157
/********** Problema drumurilor minime cu aceeasi sursa (implementare sistolica)***********/ /* Compilare : mpicc -o xxx xxx.c Executie : mpirun -np 15 xxx Observatii: - Numarul de noduri al retelei este 15 = numarul de procese. Nodul 14 este cel in care se aduna informatiile - Nr noduri retea = Noduri_graf * (Noduri_graf + 1) / 2 - -1 = infinit, -2 = invalid(data lipsa) */ #include #include "mpi.h" #define N
5 //nr de noduri al grafului -> 15 procese
main(int argc, char **argv) { int myrank, size; MPI_Status status; MPI_Request req; //C - matricea cost : //1 2 3 4 5 int C[N][N] = { { 0, -1, -1, -1, 0},//1 { 3, 0, 2, 25, -1}, //2 { 10, -1, 0, 6, -1},//3 { 15, -1, -1, 0,-1},//4 { -1, -1, 4, 5, -1},//5 }; int Cmax5[N]; int Cext[5][18] = { { 0, -1, -1, -1, 0, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 },//1 { -2, 3, 0, 2, 25, -1, 0, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2 },//2 { -2, -2, 10, -1, 0, 6, -1, -1, 0, -1, -2, -2, -2, -2, -2, -2, -2, -2 },//3 { -2, -2, -2, 15, -1, -1, 0, -1, -1, -1, 0, -2, -2, -2, -2, -2, -2, -2 },//4 { -2, -2, -2, -2, -1, -1, 4, 5, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2 },//5 }; int dest_drt, dest_jos, src_sus, src_stg; int tip_nod; int sz; int i, j, k, l, a, init;
158
int ain, aout, bin, bout, r; //initializari MPI MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myrank); MPI_Comm_size(MPI_COMM_WORLD, &size); if(myrank == 0) printf("\n***** Problema algebrica a drumurilor pentru drumuri cu aceiasi sursa(implementare sistolica) *****\n\n"); //setare vecini: -1==vecin lipsa switch(myrank) { case 0: dest_drt = myrank+1; dest_jos = -1; src_sus = -1; src_stg = -1; break; case 1: dest_drt = myrank+1; dest_jos = myrank+4; src_sus = -1; src_stg = myrank-1; break; case 2: dest_drt = myrank+1; dest_jos = myrank+4; src_sus = -1; src_stg = myrank-1; break; case 3: dest_drt = myrank+1; dest_jos = myrank+4; src_sus = -1; src_stg = myrank-1; break; case 4: dest_drt = -1; dest_jos = myrank+4; src_sus = -1; src_stg =myrank-1; break; case 5:dest_drt = myrank+1; dest_jos = -1; src_sus = myrank-4; src_stg = -1; break; case 6: dest_drt = myrank+1; dest_jos = myrank+3; src_sus = myrank-4; src_stg = myrank-1; break; case 7: dest_drt = myrank+1; dest_jos = myrank+3; src_sus = myrank-4; src_stg = myrank-1; break; case 8: dest_drt = -1; dest_jos = myrank+3; src_sus = myrank-4; src_stg = myrank-1; break; case 9:dest_drt = myrank+1; dest_jos = -1; src_sus = myrank-3; src_stg = -1; break; case 10: dest_drt = myrank+1; dest_jos = myrank+2; src_sus = myrank-3; src_stg = myrank-1; break; case 11: dest_drt = -1; dest_jos = myrank+2; src_sus = myrank-3; src_stg = myrank-1; break;
159
case 12: dest_drt = myrank+1; dest_jos = -1; src_sus = myrank-2; src_stg = -1; break; case 13: dest_drt = -1; dest_jos = myrank+1; src_sus = myrank-2; src_stg = myrank-1; break; //nodul 14 are regim special - el aduna informatiile case 14:dest_drt = -1; dest_jos = -1; src_sus = myrank-1; src_stg = -1; break; } init = 1; ain = -2; bin = -2; aout = -2; bout= -2; r = -2; /*la fiecare pas - in fct de tipul sau, fiecare nod realizeaza alte actiuni Tipuri noduri: 1. Cele de pe prima diagonala(nu au vecin stang) 2. Restul */ for(i=0; i< 16; i++) { MPI_Barrier(MPI_COMM_WORLD); if(src_stg == -1)//noduri de pe diagonala stanga { aout = ain;
else
160
if(dest_drt !=-1) MPI_Send(&aout, 1, MPI_INT, dest_drt, 99, MPI_COMM_WORLD); if(src_sus != -1) MPI_Recv(&ain, 1, MPI_INT, src_sus, 99, MPI_COMM_WORLD, &status); else ain = Cext[myrank][i]; //incepand cu pasul 13, proc 14 inregistreaza //valorile if((myrank == 14) && (i > 11)) Cmax5[i-12] = ain; } { if( ain == -2) //invalid { aout = ain; bout = bin;
//daca nu sunt noduri de pe coloana din //dreapta if(dest_drt != -1) MPI_Send(&bout, 1, MPI_INT, dest_drt, 99, MPI_COMM_WORLD); if(src_stg != -1) MPI_Recv(&bin, 1, MPI_INT, src_stg, 99, MPI_COMM_WORLD, &status); if(dest_jos != -1) MPI_Send(&aout, 1, MPI_INT, dest_jos, 99,MPI_COMM_WORLD); //daca nu sunt noduri de pe prima linie if(src_sus != -1) MPI_Recv(&ain, 1, MPI_INT, src_sus, 99, MPI_COMM_WORLD, &status); else ain = Cext[myrank][i]; } else//nodul lucreaza dupa ecuatiile din //algoritm { if(init == 1) { r = ain; init = 0; bout = bin; aout = -2; //nil
else
//daca nu sunt noduri de pe //coloana din dreapta if(dest_drt != -1) MPI_Send(&bout, 1, MPI_INT, dest_drt, 99, MPI_COMM_WORLD); if(src_stg != -1) MPI_Recv(&bin, 1, MPI_INT, src_stg, 99, MPI_COMM_WORLD, &status); if(dest_jos != -1) MPI_Send(&aout, 1, MPI_INT, dest_jos, 99, MPI_COMM_WORLD); //daca nu sunt noduri de pe prima //linie if(src_sus != -1) MPI_Recv(&ain, 1, MPI_INT, src_sus, 99, MPI_COMM_WORLD, &status); else ain = Cext[myrank][i]; } { if(ain == -1) aout = r+bin;
161
if(r == -1) aout = ain; if(bin == -1) aout = ain; if((ain!=-1) && (bin!=-1) && (r!=-1) ) { if (ain < (r+bin)) aout = r+bin; else aout = ain; } bout = bin; //daca nu sunt noduri de pe //coloana din dreapta if(dest_drt != -1) MPI_Send(&bout, 1, MPI_INT, dest_drt, 99, MPI_COMM_WORLD); if(src_stg != -1) MPI_Recv(&bin, 1, MPI_INT, src_stg, 99, MPI_COMM_WORLD, &status); if(dest_jos != -1) MPI_Send(&aout, 1, MPI_INT, dest_jos, 99, MPI_COMM_WORLD); //daca nu sunt noduri de pe prima //linie if(src_sus != -1) MPI_Recv(&ain, 1, MPI_INT, src_sus, 99, MPI_COMM_WORLD, &status); else ain = Cext[myrank][i]; }
} } //afisare dupa fiecare pas pentru verificare if(myrank == 0) printf("\nDupa pasul %d:\n", i); MPI_Barrier(MPI_COMM_WORLD); printf("P[%d]: ain=%d bin=%d aout=%d bout=%d r=%d\n", myrank, ain, bin, aout, bout, r); MPI_Barrier(MPI_COMM_WORLD); } //afisare finala if(myrank == 14) { printf("\n\nDrumurile maxime de la 5 la restul nodurilor sunt:\n"); for(i=0; i%d: %d\n", i+1, Cmax5[i]); } MPI_Finalize(); }
162
Bibliografie 1. A. Gibbons, W. Rytter. Efficient Parallel Algorithms, Cambridge University Press, 1988 2. V. Kumar, A. Grama A. Gupta &G Karypis. Introduction to Parallel Computing: Design andAnalysis of Algorithms, BenjaminCummings,1994 3. Y. Robert, D. Trystram. Parallel Implementation of the Algebraic Path Problem, LNCS 237, 1986, p.149-155 4. Murray Cole. Note de curs, Edinburgh University, 2002 5. M. Craus. Algoritmi pentru prelucrǎri paralele, Editura „Gh.Asachi” Iasi, 2002 6. William Gropp, Steven Huss-Lederman, Andrew Lumsdaine, Ewing Lusk, Bill Nitzberg, William Saphir, and Marc Snir. MPI—The Complete Reference: Volume 2, The MPI-2 Extensions. MIT Press, Cambridge, MA, 1998. 7. William Gropp, Ewing Lusk, and Rajeev Thakur. Using MPI-2: Advanced Features of the Message-Passing Interface. MIT Press, Cambridge, MA, 1999. 8. A User’sGuide to MPI, Peter S. Pacheco, Department of Mathematics, Universitz of San Francisco, California, 1995 9. Installation and Users’s Guide to MPICH; D. Ashton, W. Gropp,E. Lusk; Argone National Laboratory 10. D. Grigoraş. Calculul paralel. De la sisteme la programarea aplicaţiilor, Computer Libris Agora, 2000, 11. B. Wilkinson, M. Allen. Parallel Programming, Prentice Hall, 1999 12. Marc Snir, Steve W. Otto, Steven Huss-Lederman, David W. Walker, and Jack Dongarra. MPI—The Complete Reference: Volume 1, The MPI Core, 2nd edition. MIT Press, Cambridge, MA, 1998. 13. T. Cormen, C. Leiserson, R. Rivest. Introducere în algoritmi, Computer Libris Agora, 2000 14. H. Attiya, J. Welch. Distributed Computing: Fundamentals, Simulations and Advanced Topics, The McGraw-Hill Companies, London, 1998
163