Matrix vermenigvuldiging op parallelle computers met gebruikmaking van BSP Geert Mulders [email protected] Joost van Bruggen [email protected] 13 januari 2005 Inhoudsopgave 1 Inleiding 2 2 2-Dimensionaal Algoritme 2.1 inleiding . . . . . . . . . . . . . . . 2.2 datadistributie . . . . . . . . . . . 2.3 communicatie van submatrices . . 2.4 vermenigvuldigen van submatrices 2.5 het algoritme . . . . . . . . . . . . 2.6 implementatie . . . . . . . . . . . . 2.7 kosten analyse . . . . . . . . . . . 2.7.1 communicatie . . . . . . . . 2.7.2 berekeningen . . . . . . . . 2.7.3 totale kosten . . . . . . . . 2.7.4 geheugen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 3 4 4 4 5 5 6 6 6 6 3 3-Dimensionaal Algoritme 3.1 inleiding . . . . . . . . . . . . . . . . . 3.2 initiële datadistributie . . . . . . . . . 3.3 stap 1: datadistributie . . . . . . . . . 3.4 stap 2: submatrices vermenigvuldigen 3.5 stap 3: optellen van producten . . . . 3.6 in het algemeen . . . . . . . . . . . . . 3.7 algoritme . . . . . . . . . . . . . . . . 3.8 kosten analyse . . . . . . . . . . . . . 3.8.1 communicatie . . . . . . . . . . 3.8.2 berekenen . . . . . . . . . . . . 3.8.3 totale kosten . . . . . . . . . . 3.8.4 geheugen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 7 7 7 8 8 9 9 9 9 10 10 4 Test resultaten 4.1 inleiding . . . . . . . . 4.2 het 2d programma . . 4.3 het 3d programma . . 4.4 sequentieel programma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 11 11 11 11 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Conclusie 13 A Source code 2d programma 14 B Source code 3d programma 18 C Source code sequentiëel programma 21 1 Hoofdstuk 1 Inleiding In dit verslag wordt een tweetal algoritmen besproken om 2 vierkante n × n matrices, aangeduid met A en B, met elkaar te vermenigvuldigen. De resultaatmatrix noemen we C = AB. De programma’s zijn geschreven in de programmeertaal C met gebruikmaking van BSP [1] en getest op de supercomputer Teras van het Sara instituut. Dit project is een onderdeel van de cursus Parallelle Algoritmen van de faculteit Wiskunde en Informatica aan de Universiteit Utrecht, die gegeven wordt door de heer Bisseling [2]. De code die naar aanleiding van dit verslag is geschreven is te vinden op onze homepage [3] en is als appendix opgenomen in dit verslag. 2 Hoofdstuk 2 2-Dimensionaal Algoritme 2.1 inleiding In dit hoofdstuk wordt een zogenaamd 2-dimensionaal algoritme voor het vermenigvuldigen van vierkante matrices op een parallelle computer besproken. De term 2-dimensionaal slaat op de verdeling van de inputmatrices over de processoren en op de manier waarop de processoren genummerd zijn. 2.2 datadistributie Voor de datadistributie gebruiken we een blokdistributie; dat wil zeggen dat de n × n inputmatrices A en B in blokken ter grootte van bs = n/q over p = q 2 processoren worden verdeeld. We nemen hierbij aan dat p een kwadraat is van een integer en dat n deelbaar is door q zodat de blocksize bs op elke processor dezelfde is. Elke processor, die met een 2-dimensionaal processornummer P (s, t) wordt aangeduid, heeft dus 2 submatrices ter grootte bs × bs van respectievelijk inputmatrix A en B in zijn geheugen. We duiden deze submatrices op processor P (s, t) aan met Ast en Bst . Zie figuur 2.1 voor een grafische weergave van deze manier van distribueren. Voor deze distributie is communicatie vereist omdat alleen processor P (0, 0) IO kan uitvoeren. size P(0,0) bs P(2,1) Figuur 2.1: Blokdistributie van een size×size inputmatrix over 9 processoren. Op elke processor P (s, t) wordt de grootte van de submatrices gegeven door bs × bs In de opdracht mag er van uitgegaan worden dat elke processor de 2 bijbehorende submatrices uit respectievelijk A en B heeft. In het door ons geschreven programma kan gebruik gemaakt worden van tekst files (a.txt en b.txt) met daarin de 2 inputmatrices, die door processor P (0, 0) worden ingelezen en vervolgens verdeeld over de andere processoren. Omdat dit nogal wat tijd vergt, hebben we een optie toegevoegd die de submatrices op de verschillende processoren 3 volgens een simpel algoritme met data vult, zonder dat daarbij communicatie gebruikt wordt. Er dient hierbij opgemerkt te worden dat de tekstfiles a.txt en b.txt wel aanwezig moeten zijn om het programma te laten werken. Het programma zal als grootte van de met het algoritme gegenereerde matrices het eerste getal uit de tekstfiles nemen. 2.3 communicatie van submatrices Na de initiële datadistributie worden op elke processor die submatrices met elkaar vermenigvuldigd zodat op processor P (s, t) precies het goede blok van de resultaatmatrix, dat wil zeggen Cst , berekend wordt. Hiervoor moet natuurlijk weer gecommuniceerd worden, omdat processor P (s, t) de blokken van alle processoren die in dezelfde rij als P (s, t) zitten (dit geven we voortaan aan met P (s, ∗)), nodig heeft, alsook de blokken van alle processoren in dezelfde kolom : P (∗, t). 2.4 vermenigvuldigen van submatrices Heeft processor P (s, t) eenmaal 2 blokken (een blok van A en het bijbehorende blok van B), dan kunnen die met elkaar vermenigvuldigd worden. Dan moeten de volgende 2 blokken van de juiste processoren gehaald worden en hun product bij het vorige opgeteld. Deze stap wordt zolang herhaald totdat alle blokken uit rij P (s, ∗) vermenigvuldigd zijn met de bijbehorende blokken uit kolom P (∗, t). De vermenigvuldiging van 2 submatrices [a] en [b] maakt gebruik van het volgende: [c]ij = bs−1 X [a]ik [b]kj , k=0 wat gemakkelijk met behulp van 3 for loops geı̈mplementeerd kan worden. 2.5 het algoritme Samenvattend komen we tot Algoritme 1, dat op elke processor hetzelfde uiterlijk heeft, maar op andere data werkt (het zogenaamde Single Program Multiple Data principe). Algoritme 1 2D Algoritme 1: p ← bsp_nprocs() {het aantal gebruikte processoren} √ 2: q ← p 3: s ← bsp_pid() {elke processor heeft een uniek nummer (0,1,..,p-1)} 4: a ← s div q {van 1D naar 2D processornummering (rij)} 5: b ← s mod q {van 1D naar 2D processornummering (kolom)} 6: initialiseer outputsubmatrix [c] met 0-en 7: for t = 0 to q − 1 do 8: [a] ← [a](t+a∗q) {get submatrix [a] van andere processor (zelfde rij) } 9: [b] ← [b](t∗q+b) {get submatrix [b] van andere processor (zelfde kolom)} 10: [c] ← [c]oud + [a] · [b] {vermenigvuldig de submatrices en tel op} 11: end for We zien hier duidelijk het communiceren van de submatrices optreden. Hierbij wordt expliciet gebruik gemaakt van de 2-dimensionale processornummering, terwijl binnen de BSP bibliotheek processoren met 1 getal worden aangeduid (een integer s in het bereik 0 ≤ s < p). De submatrices [a] worden van elke processor in dezelfde rij als de uitvoerende processor gehaald, terwijl de bijbehorende submatrices [b] van de processoren in dezelfde kolom worden gehaald. Daarna zien we de vermenigvuldiging van de submatrices plaatsvinden. De · stelt dan ook het matrixproduct voor en de gebruikte methode is de in sectie 2.4 genoemde. 4 2.6 implementatie Nu gaan we in op de implementatie van het hiervoor beschreven algoritme. Voordat 2 submatrices op een bepaalde processor met elkaar vermenigvuldigd kunnen worden, moeten die lokaal aanwezig zijn. Hiervoor maken we gebruik van get operaties. Na de get operaties wordt een synchronisatie gedaan, zodat het zeker is dat de benodigde data aanwezig zijn. Zie code fragment 2.1 voor de details. De functies die beginnen met bsp zijn BSP specifieke functies [2]. Om de communicatie gebalanceerd te houden, beginnen we op elke processor met het get-en van de lokale submatrix [a] en de bijbehorende (in het algemeen niet lokale) submatrix [b]. Daarna verkrijgen we door middel van een get-operatie de submatrix [a] van de processor die zich naast de processor bevindt waarop het programma draait en de submatrix [b] van de processor die zich onder de vorige bevindt. Zo wordt er doorgegaan totdat alle processoren uit de rij (en dus ook alle processoren uit bijbehorende kolom) aan bod zijn geweest. Listing 2.1: submatrices communiceren en vermenigvuldigen int int int int s p q bs = = = = bsp pid ( ) ; bsp nprocs ( ) ; s q r t ( p + 0 . 5 ) ; // + 0 . 5 : a f r o n d i n g g a a t dan z e k e r goed n/q ; double ∗ ∗ matrixA = m a t a l l o c d ( bs , bs ) ; double ∗ pA = matrixA [ 0 ] ; double ∗ ∗ matrixAtemp = m a t a l l o c d ( bs , bs ) ; double ∗ pAtemp = matrixAtemp [ 0 ] ; b s p p u s h r e g (pA , bs ∗ bs ∗SZDBL ) ; b s p p u s h r e g ( pAtemp , bs ∗ bs ∗SZDBL ) ; // idem voor matrixB , pB , matrixBtemp , pBt for ( t =0; t<q ; t++) { // 1D naar 2D processornummering int a = s / q ; // r i j nummer vd p r o c e s s o r int b = s % q ; // kolom nummer vd p r o c e s s o r int c = ( b+t ) % q ; // b e s p a a r t r e k e n w e r k // g e b a l a n c e e r d e communicatie van s u b m a t r i c e s b s p g e t ( a∗q + c , pA , 0 , pAt , bs ∗ bs ∗SZDBL ) ; b s p g e t ( b + c ∗q , pB , 0 , pBt , bs ∗ bs ∗SZDBL ) ; bsp sync ( ) ; // s u b m a t r i c e s v e r m e n i g v u l d i g e n for ( int i =0; i <bs ; i ++) for ( int j =0; j <bs ; j ++) for ( int k =0; k<bs ; k++) matrixC [ i ] [ j ] += matrixAtemp [ i ] [ k ] ∗ matrixBtemp [ k ] [ j ] ; } bsp sync ( ) ; 2.7 kosten analyse Om de kosten van het algoritme te bepalen, moeten we ons realiseren dat die uit 3 belangrijke ingrediënten bestaan: communicatie, berekeningen en synchronisatie. We zullen gebruik maken 5 van de door Bisseling [2] voorgestelde kostenfuncties. De kosten om een h-relatie te communiceren, bedragen Tcomm (h) = hg + l waarbij g de kosten zijn om 1 datawoord te communiceren (in flops) en l een parameter is die alle vaste kosten (in flops) in zich verzamelt. De kosten van een rekenstap worden gegeven door Tcomp (w) = w + l, waarbij l dezelfde betekenis en waarde heeft als bij de communicatie kosten en w het maximaal aantal flops in de computatie stap is (op 1 processor). Hiermee kan de kostenfunctie van elk BSP algoritme geschreven worden als: T = a + bg + cl. (2.1) Een BSP computer kunnen we karakteriseren door 4 parameters: p, r, g, en l. Hierbij staat p voor het aantal processoren en r voor de snelheid van 1 processor (in flop/s). Nu kunnen we een schatting maken van de benodigde tijd om het programma uit te voeren (mits we de waarden van de parameters van de gebruikte machine kennen, bijv. door benchmarking); deze is namelijk t = T /r. Verder speelt de gebruikte hoeveelheid geheugen een rol; deze zullen we ook analyseren. 2.7.1 communicatie Elke processor vraagt q keer 2 submatrices van steeds een andere processor. Die submatrices hebben een grootte van n/q × n/q = n2 /p. Aangezien elke processor op hetzelfde moment zulke datablokken opvraagt en als eerste altijd de lokale submatrix a en bijbehorende submatrix b (die in het algemeen niet lokaal is) ontvangt, verzendt elke processor steeds evenveel als hij ontvangt. We kunnen dus spreken van een volle h-relatie, met h = 2n2 /p in het geval dat er geen lokale submatrix (op alle processoren!) bij betrokken is. De allereerste communicatiestap is echter zeker goedkoper, omdat de submatrix [a] niet gecommuniceerd hoeft te worden; dan geldt h = n2 /p. De totale communicatie kosten komen hiermee op: 2 n2 n n2 n2 + l) + (g × + l) = 2 √ − Tcomm = (q − 1) × (g × 2 g + ql. p p p p 2.7.2 berekeningen Voor het product van 2 submatrices worden 2(bs)3 berekeningen gedaan, waarbij de (bs)3 komt van de 3 gebruikte for loops en de factor 2 omdat er steeds én een optelling én een vermenigvuldiging wordt uitgevoerd. In totaal worden er op elke processor q keer 2 submatrices vermenigvuldigd, zodat we op een totaal uitkomen van: Tcomp = q × (2(bs)3 + l) = 2 2.7.3 n3 + ql. p totale kosten De totale kosten worden met behulp van de eerder voorgestelde kostenfunctie (2.1) gegeven door: 2 n3 n n2 T = Tcomm + Tcomp = 2 g + 2ql. + 2√ − p p p 2.7.4 geheugen Op elke processor worden 5 matrices gedeclareerd ter grootte bs × bs. Bij elkaar hebben we dus 5 × n × n × SZDBL geheugen nodig. Bovendien worden op processor P (0, 0) de 2 inputmatrices A en B ingelezen, waarvoor 2 × n × n × SZDBL geheugen nodig is. De parameter SZDBL = sizeof(double) geeft de grootte van een double in bytes. Er wordt gebruik gemaakt van tijdelijke matrices (zie code fragment 2.1) omdat de lokale matrices in het algemeen nog nodig zijn op andere processoren en dus niet overschreven dienen te worden. 6 Hoofdstuk 3 3-Dimensionaal Algoritme 3.1 inleiding In dit hoofdstuk wordt de 3-dimensionale variant van het hiervoor besproken algoritme onderzocht. De term 3-dimensionaal slaat weer op de verdeling van de inputmatrices over de √ processoren en op de manier waarop de processoren genummerd zijn. In dit geval geldt q = 3 p. 3.2 initiële datadistributie Zoals aangegeven in de opdracht, mocht er van uitgegaan worden dat de matrices over de processoren zijn verdeeld. We hebben er dan ook voor gekozen om het programma volgens een simpel algoritme getallen in de submatrices op de verschillende processoren te laten genereren en het daadwerkelijke verdelen achterwegen te laten. De submatrix Asu van inputmatrix A is bij aanvang van het algoritme gedistribueerd over de rij van processoren met nummers P (s, ∗, u), terwijl de submatrix But van inputmatrix B gedistribueerd is over de kolom van processoren met nummers P (∗, t, u). Dit betekent dus dat beide inputmatrices in q 2 submatrices ter grootte n/q ×n/q zijn verdeeld. Elke submatrix is bovendien over zijn bijbehorende kolom of rij gedistribueerd, wat inhoudt dat elke processor in die rij of die kolom een rij of kolom krijgt uit de submatrix. Zie figuur 3.1 voor een expliciet voorbeeld. √ De grootte van de vierkante blokken is bs = n/q. Hierbij heeft q de waarde 3 p. We nemen aan dat p de derde macht is van een integer en dat n een veelvoud is van q, zodat de blocksize op elke processor dezelfde waarde heeft. 3.3 stap 1: datadistributie In de eerste stap van het algoritme wordt door elke processor de data die lokaal aanwezig zijn, verzonden naar die processoren die de data later in het algoritme nodig hebben. Voor data uit inputmatrix A zijn dit de processoren in dezelfde rij als de versturende processor en voor de data uit B de processoren in dezelfde kolom (zie figuur 3.2). Hierbij moet er op gelet worden dat de data in de goede volgorde in de lokale submatrices terecht komen. 3.4 stap 2: submatrices vermenigvuldigen Na de datadistributie worden op elke processor de 2 aanwezige submatrices met elkaar vermenigvuldigd (op de manier zoals beschreven in sectie 2.4). Hiervoor hoeft dus geen communicatie plaats te vinden. 7 size a00 a01 a02 a03 a10 a11 a12 a13 bs z p=8 q=2 y a20 [a]10 a21 a22 a23 a30 a31 a32 a33 b00 b01 b02 b03 b10 b11 b12 b13 b20 b21 b22 b23 x b00 b01 P001 P011 P101 P111 [b]00 b30 b31 b32 b33 P010 P000 [b]00 b10 b11 a20 a30 P100 a21 a31 z=1 P110 z=0 [a]10 Figuur 3.1: De initiële verdeling van twee submatrices van de twee 4 × 4 inputmatrices A en B over 8 processoren Figuur 3.2: De datadistributie in een processorkolom 3.5 stap 3: optellen van producten De derde en laatste stap is het optellen van de matrixproducten. We zorgen er voor dat de resultaatmatrix C op een zelfde manier als de inputmatrix B verdeeld is over de processoren. We doen dit om er voor te zorgen dat er zo min mogelijk gecommuniceerd hoeft te worden. Zo komt in het voorbeeld van figuur 3.1 de bovenste rij van de som van de 2 submatrices die het resultaat zijn van de lokale vermenigvuldiging op processoren P (1, 0, 0) en P (1, 0, 1) terecht op P (1, 0, 0) terwijl de onderste rij op P (1, 0, 1) terecht komt. Op deze manier hoeven niet hele submatrices gecommuniceerd te worden; het volstaat om voor de processor P (x, y, i) in het i-de z-vlak de i-de rij van elk van de submatrices op de processoren met dezelfde x en y op te halen. 3.6 in het algemeen In het algemeen is het zo dat er niet 1 rij of 1 kolom gecommuniceerd wordt, maar dat er sprake is van een blok van aanliggende rijen of kolommen. Er zullen in het algemeen namelijk meer kolommen (of rijen) in een submatrix zijn dan er processoren zijn. Het komt er op neer dat bs/q > 1. Deze verzamelingen rijen of kolommen noemen we een blok ; een blok heeft een breedte van bs/q en elke submatrix wordt in q van die blokken verdeeld. 8 3.7 algoritme Samenvattend komen we tot Algoritme 2, dat op elke processor weer hetzelfde uiterlijk heeft, maar op andere data werkt (het zogenaamde Single Program Multiple Data principe). Algoritme 2 3D Algoritme 1: p ← bsp_nprocs() {het aantal gebruikte processoren} √ 2: q ← 3 p 3: s ← bsp_pid() {elke processor heeft een uniek nummer (0,1,..,p-1)} 4: x ← s mod (q ∗ q) mod q {van 1D naar 3D processornummering} 5: y ← s mod (q ∗ q) div q 6: z ← s div (q ∗ q) {STAP 1: datadistributie, zie figuur 3.2} 7: op processor P (i, j, k) 8: for all x 6= i,y 6= j do 9: get rij x van [a] op processor P (x, j, k) en stop die in rij x van de lokale [a] 10: get kolom y van [b] op processor P (i, y, k) en stop die in kolom y van de lokale [b] 11: end for {STAP 2: submatrices vermenigvuldigen} 12: [c] ← [a] · [b] {STAP 3: optellen van resultaten} 13: op processor P (i, j, k) 14: for all z 6= k do 15: get rij i van [c] op processor P (i, j, z) en en tel die op bij rij i van de lokale [c] 16: end for 3.8 kosten analyse We maken weer gebruik van de theoretische relaties die in het BSP model gelden en die al in sectie 2.7 zijn besproken. 3.8.1 communicatie In de stappen 1 en 3 van het algoritme vindt communicatie plaats. In stap 1 zendt elke processor een kolom van de lokale submatrix [a] en een rij van de lokale submatrix [b] naar de q 1 processoren in dezelfde processor-rij respectievelijk dezelfde processorkolom. Tegelijkertijd worden ook zulke kolommen en rijen ontvangen. De grootte van zo’n kolom (of rij) is 1/q × (bs)2 = n2 /p. Aangezien er naar q processoren wordt gezonden, en elke processor gelijktijdig met de andere bezig is, hebben we een h-relatie met h = 2n2 /p2/3 . In stap 3 worden de resultaten weer in de bekende verdeling gebracht. Hiervoor geldt h = n2 /p2/3 , omdat er nu slechts steeds 1 rij hoeft te worden gecommuniceerd (uit de lokale resultaatsubmatrix). De totale communicatie kosten komen hiermee op: Tcomm = 3g 3.8.2 n2 + ql. p2/3 berekenen Berekenen vindt plaats in stappen 2 en 3. 1 Eigenlijk wordt er naar q − 1 processoren gezonden. Door gebruik te maken van q zijn de uiteindelijke uitdrukkingen van een mooiere vorm. 9 In stap 2 worden de submatrices vermenigvuldigd. Dit levert de kosten Tcomp,2 = 2 × (bs)3 = 2n3 /p op. In stap 3 worden q keer 2 rijen ter grootte n2 /p opgeteld, wat de volgende kosten met zich meebrengt: Tcomp,3 = q × n2 /p = n2 /p2/3 . De totale reken kosten worden gegeven door: Tcomp = 3.8.3 n2 2n3 + 2/3 + ql. p p totale kosten De totale kosten van het 3-dimensionale algoritme komen met gebruikmaking van de eerder voorgestelde kostenfunctie (2.1) en het voorgaande op: T = Tcomp + Tcomm = 2 3.8.4 n3 n2 n2 + 2/3 + 3 2/3 g + 2p1/3 l. p p p geheugen Op elke processor worden 4 submatrices ter grootte bs × bs gedeclareerd. We vinden een van het aantal processoren afhankelijk geheugengebruik van 4 p3/2 × n2 × SZDBL. 10 Hoofdstuk 4 Test resultaten 4.1 inleiding In dit hoofdstuk wordt besproken hoe de twee verschillende implementatie’s presteren. Hiervoor zijn de 2 geschreven programma’s gedraaid op de parallelle computer Teras van SARA Rekenen Netwerkdiensten [4] met matrices van verschillende grootte als input en met gebruikmaking van verschillende aantallen processoren. 4.2 het 2d programma Om te kijken hoe het programma schaalt met het aantal gebruikte processoren, hebben we een aantal runs uitgevoerd. Zie tabel 4.1 voor de resultaten. size \ p 100 300 360 600 720 1080 1440 1800 1 0.11 2.84 24.2 38.5 - 4 0.04 0.87 1.33 6.03 9.84 33.5 89.4 195.6 9 0.40 0.70 2.90 4.62 17.6 40.8 80.7 Tabel 4.1: Resultaten verkregen met het 2d programma; de aangegeven tijden zijn in seconden. Bij gebruik van dit programma is het voordeel van het parallelliseren duidelijk zichtbaar. 4.3 het 3d programma Ook voor het programma dat gebruik maakt van het 3-dimensionale algoritme, hebben we gekeken hoe het schaalt met het aantal gebruikte processoren. Zie tabel 4.2 voor de resultaten. 4.4 sequentieel programma Ter vergelijking is nog een sequentiële implementatie gemaakt van het algoritme om matrices te vermenigvuldigen. Zoals verwacht is dit programma iets sneller dan de parallelle versies als die gebruik maken van slechts 1 processor. Zie tabel 4.3 Zie de appendix voor de code van het sequentiële programma. 11 size \ p 100 200 300 400 500 600 700 800 900 1000 1500 2000 1 0.10 0.74 2.31 6.12 11.1 23.5 38.3 61.6 93.9 134.0 - 8 0.22 0.85 2.00 3.63 5.66 9.06 12.5 17.8 21.7 29.4 89.6 241.8 Tabel 4.2: Resultaten verkregen met het 3d programma; de aangegeven tijden zijn in seconden. size \ p 100 300 500 700 900 1000 1 0.08 2.19 10.8 36.4 88.1 131.1 Tabel 4.3: Resultaten verkregen met het sequentiële programma; de aangegeven tijden zijn in seconden. 12 Hoofdstuk 5 Conclusie Bij gebruik van meerdere processoren zijn de geschreven programma’s sneller dan een standaard programma dat geen gebruik gemaakt van parallellisme. Het programma dat gebruik maakt van het 3-dimensionale algoritme is bij de gedane test-runs niet sneller dan het programma dat gebruik maakt van het 2-dimensionale algoritme. Het is echter wel zo dat er een trend zichtbaar is die laat zien dat het 3-dimensionale algoritme bij gebruik van grote matrices en veel processoren, zoals ook uit het vergelijken van de theoretische kostenfunctie’s te zien is, voordeliger zal zijn. 13 Bijlage A Source code 2d programma /************************************************************************************ * parallel program for computing the product of two square N x N - matrices A and B * * it can make use of p=1,4,9,16,... processors (with a maximum of N x N processors) * * and the restriction N mod sqrt(p) = 0 has to be satisfied * * the two input matrices are read from the files a.txt and b.txt * * in these files, the first number indicates the value of N * *************************************************************************************/ #include "bspedupack.h" int P; double** A; double** B; int size; /* /* /* /* number of processors requested */ input matrix A */ input matrix B */ size (in one dimension) of input matrices */ /****************** * SPMD FUNCTION * *******************/ void matrixm() { int p; /* number of processors being used */ int s; /* current processor number */ int q; /* sqrt of p; number of blocks on a row (or in a column) */ int bs; /* blocksize: size of submatrices (which are bs x bs) */ int i, j, k, t; /* counters */ int a, b; /* matrices */ double** matrixA; double** matrixB; double** matrixC; double** matrixAtemp; double** matrixBtemp; /* pointers to these matrices used for get operations */ double* pA; double* pB; double* pAt; double* pBt; /* timers */ double time0, time1; /* file pointer */ // FILE * file; /* BEGIN PARALLEL PART */ bsp_begin(P); p = bsp_nprocs(); s = bsp_pid(); q = (int) (sqrt(p)+0.5); /* CHECKS */ if(q*q != p) bsp_abort("Error: wrong value of q!"); if (size*size < p) /* blocksize will be smaller than 1 */ { if (s==0) printf("Error: too many processors requested. Blocksize will be smaller than 1.\n"); exit(1); } 14 if (size%q == 0) bs = size/q; else /* blocksize will differ among processors */ { if (s == 0) printf("Error: matrix dimensions incompatible with number of processors.\n"); exit(1); } /* DATA DISTRIBUTION */ /* allocate matrices */ matrixA = matallocd(bs, bs); matrixB = matallocd(bs, bs); matrixC = matallocd(bs, bs); matrixAtemp = matallocd(bs, bs); matrixBtemp = matallocd(bs, bs); /* set pointers */ pA = matrixA[0]; pB = matrixB[0]; pAt = matrixAtemp[0]; pBt = matrixBtemp[0]; /* register pointers */ bsp_push_reg(pA, bs*bs*SZDBL); bsp_push_reg(pB, bs*bs*SZDBL); bsp_push_reg(pAt, bs*bs*SZDBL); bsp_push_reg(pBt, bs*bs*SZDBL); bsp_sync(); time0 = bsp_time(); // // // // // // // /* DISTRIBUTE the data */ if (s == 0) for (i = 0; i < size; i++) for (j = 0; j < size; j++) { bsp_put( ((i/bs)*q + j/bs)%p, &A[i][j], pA, (i*bs + j%bs) % (bs*bs) * SZDBL, SZDBL); bsp_put( ((i/bs)*q + j/bs)%p, &B[i][j], pB, (i*bs + j%bs) % (bs*bs) * SZDBL, SZDBL); } /* submatrix generation for testing purposes (faster) */ for (i = 0; i < bs; i++) for (j = 0; j < bs; j++) { a = i+j; matrixA[i][j] = a; matrixB[i][j] = a; } bsp_sync(); time1 = bsp_time(); // // // // // if (s == 0) { printf("Time required for data distribution: %.12lf seconds.\n", time1 - time0); fflush(stdout); } /* COMPUTATION */ /************************************************************************************************ * get blocks from other processors and put those blocks locally in matrixAtemp and matrixBtemp * * the A-blocks come from all processors in the same processor row (2D processor numbering) * * the B-blocks come from all processors in the same processor column (2D processor numbering) * ************************************************************************************************/ /* initialize output matrix */ for (i = 0; i < bs; i++) for (j = 0; j < bs; j++) matrixC[i][j] = 0; /* convert 1D numbering to 2D */ a = s / q; b = s % q; /* communicate and compute */ for(t = 0; t < q; t++) { /* get blocks (initialize the transfer) */ bsp_get( t + a*q, pA, 0, pAt, bs*bs*SZDBL); bsp_get( t*q + b, pB, 0, pBt, bs*bs*SZDBL); bsp_sync(); /* actually GET the blocks (do the transfer) */ /* calculate product of two blocks */ 15 for (i = 0; i < bs; i++) for (j = 0; j < bs; j++) for (k = 0; k < bs; k++) matrixC[i][j] += matrixAtemp[i][k] * matrixBtemp[k][j]; } bsp_sync(); time0 = bsp_time(); if (s == 0) { printf("Time required for computation: %lf seconds.\n", time0-time1); fflush(stdout); } // // // /* RESULT MATRIX */ for (i = 0; i < bs; i++) for (j = 0; j < bs; j++) printf("%lf\t", matrixC[i][j]); /* DEREGISTRATION and DEALLOCATION */ bsp_pop_reg(pA); bsp_pop_reg(pB); bsp_pop_reg(pAt); bsp_pop_reg(pBt); matfreed(matrixA); matfreed(matrixB); matfreed(matrixC); matfreed(matrixAtemp); matfreed(matrixBtemp); bsp_end(); } /************************************************************************************************************************* * reads a matrix from a file, stores the entries in memory and returns a pointer to that memory * * the first number in the file must be an int, representing the size of one dimension of the square matrix * * the following numbers can be doubles and are the entries of the matrix in order (row1 column1) (row1 column2) etc. * **************************************************************************************************************************/ double** readMatrix(char* filename, int* size) { int i, j; double** matrix; FILE* matrixfile; /* open file */ if ((matrixfile = fopen(filename, "r")) == 0) { printf("Error: file does not exist.\n"); exit(1); } /* allocate memory */ fscanf(matrixfile, "%d", size); matrix = matallocd(*size, *size); /* write file to memory */ for (i = 0; i < *size; i++) for (j = 0; j < *size; j++) fscanf(matrixfile, "%lf", &matrix[i][j]); return matrix; } /************************************************************************* * prints the entries of a two-dimensional memory block in matrix form * **************************************************************************/ void printMatrix(double** matrix, int size) { int i, j; if (size == 0) printf("Error: the matrix size is zero.\n"); /* actually print */ for (i = 0; i < size; i++) { for (j = 0; j < size; j++) printf("%lf\t", matrix[i][j]); printf("\n"); } printf("\n"); 16 } /************ * MAIN * *************/ int main(int argc, char** argv) { // int sizeA, sizeB; int Q; /* initialize bsp */ bsp_init(matrixm, argc, argv); // // // // // // // // // /* sequential part */ A = readMatrix("a.txt", &sizeA); B = readMatrix("b.txt", &sizeB); if (sizeA != sizeB) { printf("Error: incompatible matrix dimensions.\n"); exit(1); } else size = sizeA; /* size of matrices */ printf("Give matrix size:"); fflush(stdout); scanf("%d", &size); /* number of processors to use */ printf("How many processors do you want to use?\n"); fflush(stdout); scanf("%d", &P); /* P has to have a value equal to 1,4,9,16,... */ Q = (int)sqrt(P); if ( Q*Q - P != 0 ) { P = Q*Q; printf("Warning: using %d processors.\n", Q*Q); } /* P cannot be bigger than number of available processors */ if ( P > bsp_nprocs() ) { printf("Error: not enough processors available.\n"); exit(1); } /* SPMD part */ matrixm(); exit(0); } 17 Bijlage B Source code 3d programma #include "bspedupack.h" /* This is a program that can be used for computing matrix-matrix multiplications in a 3D fashion Therefore it will only run on 1,8,27,64,... processors */ int P; int size; /* number of processors requested */ /* size (in one dimension) of input matrices */ /* these functions are usefull when transforming processornumbers from 1D to 3D and vice versa the offset function transforms a 2D matrix index in a memory position (1D) */ int pid(int q, int x, int y, int z) { return ((q*q*z) + (q*y) + x); } int xpid(int q, int s) { return (s%q); } int ypid(int q, int s) { return ((s/q)%q); } int zpid(int q, int s) { return (s/(q*q)); } int offset(int bs, int x, int y) { return ((x + bs*y)*SZDBL); } /****************** * SPMD FUNCTION * *******************/ void matrixm() { int p; /* number of processors being used */ int s; /* current processor number 1D */ int q; /* q^3 = p; number of blocks on a row (or in a column) */ int bs; /* blocksize: size of submatrices (which are bs x bs) */ int b; /* b = bs/q */ int x, y, z; /* current processor numbers 3D */ int i, j, k; /* counters */ double t0, t1; /* timers */ /* matrices */ double ** A, ** B, ** C, ** D; double * pA, * pB, * pC; /* BEGIN PARALLEL PART */ bsp_begin(P); p = bsp_nprocs(); s = bsp_pid(); q = (int) (pow(p, 0.333)+0.5); x = xpid(q,s); y = ypid(q,s); z = zpid(q,s); /* checks */ if(q*q*q != p) /* rounding errors? */ bsp_abort("Error: q has wrong value!"); if(size%q == 0) 18 { bs = size/q; b = bs/q; } else /* blocksize will differ among processors */ { if (s == 0) printf("Error: matrix dimensions incompatible with number of processors.\n"); exit(1); } /* allocation, registration and initialization of matrices */ A = matallocd(bs, bs); B = matallocd(bs, bs); C = matallocd(bs, bs); D = matallocd(bs, bs); pA = A[0]; pB = B[0]; pC = C[0]; bsp_push_reg(pA, bs*bs*SZDBL); bsp_push_reg(pB, bs*bs*SZDBL); bsp_push_reg(pC, bs*bs*SZDBL); for(i = 0; i < bs; i++) for(j = 0; j < bs; j++) { A[i][j] = 0; B[i][j] = 0; C[i][j] = 0; D[i][j] = 0; } /* STEP 0: initial distribution */ for(i = 0; i < bs; i++) for(j = 0; j < b; j++) { A[i][j+y*b] = s; B[j+x*b][i] = 2*s; } bsp_sync(); t0 = bsp_time(); /* STEP 1: distribution of elements */ for(i = 0; i < q; i++) /* loop over processors for(k = 0; k < b; k++) /* loop over rows/columns in blocks in submatrices for(j = 0; j < bs; j++) /* loop over elements in row/column of blocks of submatrices { if(i != y) /* skip local get */ bsp_get( pid(q,x,i,z), pA, offset(bs,i*b+k,j), &A[j][i*b+k], SZDBL); /* get from all processors with same x and y number get from their submatrices A get a block of possibly more than one row (depends on b) and put it in local if(i != x) bsp_get( pid(q,i,y,z), pB, offset(bs,j,i*b+k), &B[i*b+k][j], SZDBL); } bsp_sync(); /* STEP 2: multiplication of local submatrices */ for(i = 0; i < bs; i++) for(j = 0; j < bs; j++) for(k = 0; k < bs; k++) C[i][j] += A[i][k]*B[k][j]; /* STEP 3: add up z-planes; first communicate and than sum up */ /* STEP 3a: communicate */ for(i = 0; i < q; i++) // loop over processors in z-direction for(j = 0; j < bs; j++) // loop over elements for(k = 0; k < b; k++) // loop over rows/columns in a block bsp_get( pid(q,x,y,i), pC, offset(bs,j,z*b+k), &B[i*b+k][j], SZDBL); // get from processors with same x and y number // get from C submatrix // save in B submatrix (q*b x bs = bs x bs; so it will fit) bsp_sync(); /* STEP 3b: sum up locally */ for(i = 0; i < q; i++) for(k = 0; k < b; k++) for(j = 0; j < bs; j++) D[k+x*b][j] += B[i*b+k][j]; bsp_sync(); t1 = bsp_time(); /* test: result */ if (s == 0) printf("Time needed: %lf\n", t1 - t0); 19 */ */ */ submatrix A */ // for(i = 0; i < bs; i++) // for(j = 0; j < bs; j++) // printf("3 s = %d: D[%d][%d] = %f\n", s, i, j, D[i][j]); bsp_end(); } /************ * MAIN * *************/ int main(int argc, char** argv) { int Q; /* initialize bsp */ bsp_init(matrixm, argc, argv); /* size of matrices */ printf("Give matrix size.\n"); fflush(stdout); scanf("%d", &size); /* number of processors to use */ printf("How many processors do you want to use?\n"); fflush(stdout); scanf("%d", &P); /* P has to have a value equal to 1,8,27,64,... */ Q = (int)(pow(P, 0.333) + 0.5); if ( Q*Q*Q - P != 0 ) { P = Q*Q*Q; printf("Warning: using %d processors.\n", Q*Q*Q); } /* P cannot be bigger than number of available processors */ if ( P > bsp_nprocs() ) { printf("Error: not enough processors available.\n"); exit(1); } /* SPMD part */ matrixm(); exit(0); } 20 Bijlage C Source code sequentiëel programma #include <time.h> #include <stdio.h> double **matallocd(int m, int n) { /* This function allocates an m x n matrix of doubles */ int i; double *pd, **ppd; if (m==0) { ppd= NULL; } else { ppd= (double **)malloc(m*sizeof(double *)); if (ppd==NULL) { printf("matallocd: not enough memory"); exit(1); } if (n==0) { for (i=0; i<m; i++) ppd[i]= NULL; } else { pd= (double *)malloc(m*n*sizeof(double)); if (pd==NULL) { printf("matallocd: not enough memory"); } ppd[0]=pd; for (i=1; i<m; i++) ppd[i]= ppd[i-1]+n; } } return ppd; } /* end matallocd */ int main() { int i, j, k, size; double ** A, ** B, ** C, time; clock_t t0, t1; printf("Gimme size: "); fflush(stdout); scanf("%d", &size); A = matallocd(size, size); B = matallocd(size, size); C = matallocd(size, size); for(i = 0; i < size; i++) for(j = 0; j < size; j++) 21 { A[i][j] = i*j; B[i][j] = i*j; } t0 = clock(); for(i = for(j = for(k = C[i][j] 0; 0; 0; += i < size; i++) j < size; j++) k < size; k++) A[i][k]*B[k][j]; t1 = clock(); time = (double)((double)t1 - (double)t0)/(double)CLOCKS_PER_SEC; printf("\n\nTicks: %d, CLK_TCK: %d, Total time: %lf\n", t1-t0, CLOCKS_PER_SEC, time); return 0; } 22 Bibliografie [1] http://www.bsp-worldwide.org/ [2] Rob H. Bisseling, Parallel Scientific Computation, Oxford University Press, 2004 [3] http://www.phys.uu.nl/~vbrug/vakken/wipa/project1 [4] http://www.sara.nl 23