• Nem Talált Eredményt

5. Az OpenMPI API

6.9. példa - statistics.c

5.3.3. Kollektív kommunikáció

float tmp;

err= MPI_Irecv((void*)numbers, N, MPI_FLOAT, 0, 0, MPI_COMM_WORLD, requests);

ERROR(err, "MPI_Recv");

err= MPI_Wait(requests, MPI_STATUS_IGNORE);

ERROR(err, "MPI_Wait");

statistics[0]= 0;

tmp= 0;

for ( i= 0; i < N; ++i ) {

statistics[0]+= numbers[i];

tmp+= numbers[i]*numbers[i];

}

tmp/= N;

statistics[0]/= N;

statistics[0]= sqrt(tmp - statistics[0]*statistics[0]);

err= MPI_Isend(statistics, 1, MPI_FLOAT, 0, 0, MPI_COMM_WORLD, requests);

ERROR(err, "MPI_Send");

}

if ( rank != 0 )

MPI_Wait(requests, MPI_STATUS_IGNORE);

err= MPI_Finalize();

ERROR(err, "MPI_Finalize");

return 0;

}

user@home> mpirun -machinefile hostfile.home -np 4 statistics Adjon meg 10 szamot!

2 10 4 3 5 7 8 1 9 6 Minimum: 1.000000 Maximum: 10.000000 Atlag: 5.500000 Szoras: 2.872281

Ahogy az a kimenetből látható, programunk a korábbival egyező eredményt ad, helyesen működik.

Nyilvánvaló, hogy a nem-blokkoló függvények használata esetén hosszabb kódot kell írnunk, mert a kommunikáción kívül a kommunikációs művelet befejezését is ellenőriznünk kell. A példában a 0 azonosítójú vezérlő folyamat küldi szét a beolvasott számokat a dolgozó folyamatoknak. Mivel azonban a vezérlő folyamatnak nem kell megvárnia a tömb megérkezését, ezért a nem-blokkoló küldő és fogadó függvények hívása után rögtön hozzáláthat a tömb legkisebb elemének meghatározásához. A kommunikációs műveletek befejeződését csak a vezérlő folyamat utolsó művelete, az eredmények kimenetre írása előtt kell biztosítanunk, erre az MPI_Waitall függvényt használjuk. A dolgozó szálak szerkezete hasonló: első lépésként meg kell kapniuk a felhasználó által megadott számokat tartalmazó tömböt. Annak megérkezése után végrehajtják a dedikált számítási feladatot, majd visszaküldik az eredményt a vezérlő folyamatnak. Azt, hogy a dolgozó folyamatok ne fejeződjenek be az eredmények biztonságos visszaküldése előtt, a program végén található MPI_Wait függvényhívással biztosítjuk. Figyeljük meg a példaprogramban, hogy ugyanazon változókat a különböző folyamatok különböző célokra használják!

5.3.3. Kollektív kommunikáció

A pont-pont kommunikációt megvalósító függvények mellett az OpenMPI számos eszközt biztosít kollektív üzenetküldésre, illetve folyamatok kollektív szinkronizációjára. Ezen szakaszban a kollektív kommunikációhoz kapcsolódó leggyakrabban használt függvényeket tekintjük át röviden, néhány példaprogramon keresztül.

A kollektív kommunikációs függvények közös tulajdonsága, hogy minden esetben blokkolók és egy kommunikátorhoz rendelt összes folyamattal egyszerre kommunikálnak. Használatuk előnye, hogy egyszerűsíthetjük a kódunkat. Továbbá, mivel beépített és optimalizált megoldásokról van szó, jogosan várhatjuk, hogy használatuk hatékonyabb, mint ha az egyes kommunikációs műveleteket egyesével kódolnánk le. Meg kell azonban jegyeznünk, hogy amit kollektív kommunikációs műveletekkel megvalósítunk, azt

mindent megvalósíthatunk megfelelő számú pont-pont kommunikációs művelettel is. Ha az elindított folyamatoknak csak egy részhalmazával szeretnénk kommunikálni a kollektív kommunikációs eszközökkel, akkor alkalmas saját folyamatcsoportokat és kommunikátorokat kell létrehoznunk.

A szinkronizációs eszközök csoportjába tartozik az MPI_Barrier függvény.

Specifikáció:

int MPI_Barrier( MPI_Comm comm );

Paraméterek: comm - Egy kommunikátor objektum.

Visszatérési érték: Sikeres végrehajtás esetén MPI_SUCCESS= 0, ellenkező esetben hibakód.

Az MPI_Barrier függvény blokkolja a hívó folyamatot, egészen addig, amíg a paraméterként kapott comm kommunikátorhoz tartozó összes folyamat meg nem hívja az MPI_Barrier függvényt. Másképp fogalmazva, csak akkor folytatódik a folyamat futása, ha a kommunikátorhoz rendelt minden folyamat elért egy MPI_Barrier függvényhíváshoz.

A kommunikációs függvények közül az MPI_Bcast segítségével "broadcast"-olhatunk üzeneteket, azaz küldhetünk el egy üzenetet egy kommunikátorhoz rendelt minden folyamatnak.

Specifikáció:

int MPI_Bcast( void* buff, int count,

MPI_Datatype datatype, int root,

MPI_COMM comm);

Paraméterek: buff - Az elküldendő üzenet.

count - A buff címen lévő datatype típusú adatelemek száma.

datatype - Az adtaelemek típusa.

root - A küldő folyamat azonosítója.

comm - A kommunikációs műveletbe bevont folyamatok kommunikátora.

Visszatérési érték: Sikeres végrehajtás esetén MPI_SUCCESS= 0, ellenkező esetben hibakód.

Működését tekintve az MPI_Bcast függvény elküldi a count darab, datatype típusú adatelemet a buffer címről a comm kommunikátorhoz rendelt folyamatoknak a root azonosítójú folyamatból. Fontos kiemelnünk, hogy a nem root sorszámú folyamatokban nem kell üzenet fogadó függvényeket hívnunk, ezen folyamatokban az MPI_Bcast üzenet fogadó függvényként működik, azaz a paraméterként megadott pufferbe fogadja az count darabszámú datatype típusú adatelemet.

A "broadcast"-hoz hasonlóan kollektív üzenetküldést valósíthatunk meg az angol terminológiában "scatter"-nek nevezett művelettel, amely során a kommunikátor egy folyamata az összes többi folyamatnak azonos típusú és mennyiségű, de különböző adatokat küld. A "scatter" művelet ellentéte a "gather" művelet, amely során egy folyamat a kommunikátor összes többi folyamatától azonos típusú és mennyiségű adatokat gyűjt össze. Ezen műveletek OpenMPI-beli implementációi az MPI_Scatter és MPI_Gather függvények.

Specifikáció:

int MPI_Scatter( void* sendbuf, int sendcnt,

MPI_Datatype sendtype, void* recvbuf,

int recvcnt,

MPI_Datatype recvtype, int root,

MPI_COMM comm);

Paraméterek: sendbuf - Az elküldendő üzenet.

sendcnt - Az egyes folyamatoknak küldendő datatype típusú elemek száma.

sendtype - Az elküldendő elemek típusa.

recvbuf - A fogadó puffer címe.

recvcnt - A fogadó pufferben tárolható datatype típusú elemek száma.

recvtype - A fogadó puffer elemeinek típusa.

root - A küldő folyamat azonosítója a comm kommunikátorban.

comm - a kommunikációs műveletbe bevont folyamatok kommunikátora.

Visszatérési érték: Sikeres végrehajtás esetén MPI_SUCCESS= 0, ellenkező esetben hibakód.

Specifikáció:

int MPI_Gather( void* sendbuf, int sendcnt,

MPI_Datatype sendtype, void* recvbuf,

int recvcnt,

MPI_Datatype recvtype, int root,

MPI_COMM comm);

Paraméterek: sendbuf - Az elküldendő üzenet.

sendcnt - Az egyes folyamatoknak küldendő datatype típusú elemek száma.

sendtype - Az elküldendő elemek típusa.

recvbuf - A fogadó puffer címe.

recvcnt - A fogadó pufferben tárolható datatype típusú elemek száma.

recvtype - A fogadó puffer elemeinek típusa.

root - A küldő folyamat azonosítója a comm kommunikátorban.

comm - a kommunikációs műveletbe bevont folyamatok kommunikátora.

Visszatérési érték: Sikeres végrehajtás esetén MPI_SUCCESS= 0, ellenkező esetben hibakód.

Mind az MPI_Scatter, mind az MPI_Gather több üzenetküldő és fogadó függvényhívással lenne helyettesíthető. Az MPI_Scatter a root azonosítójú folyamatban található sendbuf puffer i*sendcnt indexű elemétől kezdődő sendcnt darab elemet küldi el a kommunikátor i azonosítójú folyamatának, amely a recvbuf pufferbe fogadja azt. Ezzel szemben az MPI_Gather függvény a fogadó puffer i*recvcnt indexű elemétől kezdődő recvcnt számú elem helyére fogadja az i azonosítójú folyamat által küldött, megegyező típusú, recvcnt számú adatelemet.

Ha a comm kommunikátorhoz rendelt folyamatok száma n, akkor az MPI_Scatter függvény hívása n darab MPI_Send függvényhívással helyettesíthető a root folyamatban és egy-egy MPI_Recv függvényhívással a nem root azonosítójú folyamatokban. Az MPI_Gather függvényhívás n darab MPI_Recv függvényhívással helyettesíthető a root azonosítójú folyamatban és egy-egy MPI_Send függvényhívással a nem root azonosítójú folyamatokban.

A kollektív kommunikációs függvények másik csoportja az OpenMP technológiánál már megismert redukció művelet megvalósításaihoz kapcsolódik. Ezek közül az MPI_Reduce függvényt emeljük ki. Az előre definiált redukciós műveleteket és a nekik megfelelő nevesített konstansokat az 6.2. táblázatban foglaljuk össze.

6.2. táblázat - Az OpenMPI redukciós operátoraihoz tartozó nevesített konstansok.

OpenMPI konstans Leírás MPI_MAX Maximum MPI_MIN Minimum MPI_SUM Összeg MPI_PROD Szorzat MPI_LAND Logikai 'és' MPI_BAND Bitenkénti 'és'

MPI_LOR Logikai 'vagy' MPI_BOR Bitenkénti 'vagy' MPI_LXOR Logikai 'kizáró vagy' MPI_BXOR Bitenkénti 'kizáró vagy'

MPI_MAXLOC Maximum érték és helyének meghatározása MPI_MINLOC Minimum érték és helyének meghatározása Specifikáció:

int MPI_Reduce( void* sendbuf, void* recvbuf, int count,

MPI_Datatype datatype, MPI_Op op,

int root, MPI_Comm comm);

Paraméterek: sendbuf - Az elküldendő üzenet.

recvbuf - A beérkező üzenet puffere.

count - Az elküldendő adatelemek száma.

datatype - A elküldendő adatelemek típusa.

op - A végrehajtandó redukciós műveletet reprezentáló nevesített konstans.

root - A redukciót megvalósító folyamat azonosítója.

comm - A kommunikációs műveletbe bevont folyamatok kommunikátora.

Visszatérési érték: Sikeres végrehajtás esetén MPI_SUCCESS= 0, ellenkező esetben hibakód.

Az MPI_Reduce függvény a comm kommunikátorhoz rendelt minden folyamatban meghívásra kell, hogy kerüljön. Az egyes szálak azonos méretű és azonos típusú elemeket tartalmazó sendbuf puffereket adnak meg a függvényhívás során. A recvbuf fogadó puffernek csak a root sorszámú folyamatban van jelentősége. A recvbuf pufferben tárolható adatok típusa és száma, vagyis a puffer mérete meg kell, hogy egyezzen a sendbuf méretével. Az MPI_Reduce függvény a következő műveletet hajtja végre: a recvbuf puffer i. eleme a redukciós műveletben résztvevő folyamatok sendbuf puffereinek i. elemei között végrehajtott, a redukciós operátor által definiált művelet eredménye lesz.

A kollektív kommunikációs függvények működését a leíró statisztikákat kiszámító program különböző variációin keresztül szemléltetjük. Az első példában a felhasználó megad egy tömböt, amelyet "broadcast"-olunk minden folyamatnak. Az egyes folyamatok különböző leíró statisztikákat számítanak ki (minimum, maximum, átlag, szórás), majd a kiszámított értékeket a "gather" művelettel gyűjtjük össze.

6.11. példa - statistics.c

#include <stdio.h>

#include <mpi.h>

#include <float.h>

#include <error.h>

#include <math.h>

#define N 10

int main(int argc, char *argv[]) { int rank, err, i;

float numbers[N];

float statistics[4];

float result;

err= MPI_Init(&argc, &argv);

ERROR(err, "MPI_Init");

err= MPI_Comm_rank(MPI_COMM_WORLD, &rank);

ERROR(err, "MPI_Comm_rank");

err= MPI_Bcast((void*)numbers, N, MPI_FLOAT, 0, MPI_COMM_WORLD);

ERROR(err, "MPI_Send");

err= MPI_Gather(&result, 1, MPI_FLOAT, statistics, 1, MPI_FLOAT, 0, MPI_COMM_WORLD);

if ( rank == 0 )

printf("Minimum: %f\nMaximum: %f\nAtlag: %f\nSzoras: %f\n", statistics[0], statistics[1], statistics[2], statistics[3]);

err= MPI_Finalize();

ERROR(err, "MPI_Finalize");

return 0;

}

user@home> mpirun -machinefile hostfile.home -np 4 ./statistics Adjon meg egy 10 szamot!

1 2 3 4 5 6 7 8 9 10 Minimum: 1.000000 Maximum: 10.000000 Atlag: 5.500000 Szoras: 2.872281

Ha összehasonlítjuk a kódot a blokkoló függvények működését bemutató, azonos funkcionalitású program kódjával, láthatjuk, hogy a MPI_Reduce és MPI_Gather függvények használatával rövidebb, egyszerűbb, áttekinthetőbb megoldást kaptunk.

A következő példában a MPI_Scatter és MPI_Gather műveletek használatát szemléltetjük: a beolvasott számokat tartalmazó tömböt felosztjuk annyi részre, ahány folyamatot elindítottunk, majd a tömb egyes részeit elküldjük a folyamatoknak a MPI_Scatter függvény hívásával. Ezt követően a folyamatok az általuk megkapott résztömböt feldolgozzák (négyzetgyököt vonnak a tömb elemeiből), majd a feldolgozás befejeztével az eredményeket tartalmazó résztömböket összegyűjtjük a MPI_Gather függvény segítségével. Fontos különbség a "broadcast" és "scatter" műveletek között, hogy az előbbivel azonos üzenetet, az utóbbival különböző üzeneteket küldünk a folyamatoknak. Mivel nem biztos, hogy a feldolgozandó tömb elemszáma osztható a folyamatok számával, ezért előfordulhat, hogy a feldolgozásra szétküldött tömb utolsó néhány elemét egyik folyamat sem kapja meg. Ennek kezelésére a vezérlő szál kódját kiegészítjük a tömb végén maradt elemek feldolgozásával.