Sunteți pe pagina 1din 17

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

Lucrarea 4 - APLICATII MPI

In aceasta lucrare se vor dezvolta mai multi algoritmi paraleli folosind biblioteca MPI (Message Passing Interface) din distribuția open-source OpenMPI. Se incepe cu instalarea si configurarea bibliotecii OpenMPI (dacă nu a fost realizată deja), apoi se testeaza cativa algoritmi de bază simpli, care sa permita intelegerea functionarii programelor MPI. Dupa aceasta, studentii vor realiza si testa mai mulți algoritmi paraleli mai complecși. Experimentele se pot efectua atât în clusterele Linux din laboratoarele B125a și B138 (ITEMS), cât și în clusterul HPC Dell PowerEdge, programele sunt compatibile datorită bibliotecii OpenMPI care se poate instala pe oricare dintre sisteme.

4.1. Instalarea și utilizarea bibliotecii OpenMPI

În clusterele Linux-Ubuntu din laboratoarele B125a și B138, mai intai se instalează g++ (nu este instalat implicit in distributia Ubuntu 12.04) si se face Update la sistem. Distributia in format sursă a versiunii OpenMPI curente stabile (openmpi-1.6.5.tar.gz) se obtine de pe site-ul oficial www.open- mpi.org. Sunt mai multe posibilitati de instalare si executie a programelor MPI in retea. Este posibil ca biblioteca MPI sa fie instalata intr-un sistem de fisiere partajate (NFS – Network File System) sau se pot instala copii separate pe fiecare host, cu conditia ca arborele de executie al instalarii (run-time tree) sa fie acelasi in toate statiile din cluster. Pentru aceasta este suficient ca directorul de instalare (prefix) fie acelasi in toate statiile. In continuare vom exemplifica instalarea in cea de-a doua modalitate (copii separate instalate local in fiecare statie)

4.1.1. Instalare OpenMPI în clusterele Linux-Ubuntu

Instalarea in directorul implicit /usr/local. Cu un user cu drepturi de administrator se face download fisierul openmpi-1.6.2.tar.gz (sau ultima versiune stabila, dacă a apărut alta) si se execută urmatoarele comenzi:

tar xvf openmpi-1.6.2.tar.gz cd openmpi-1.6.2 ./configure sau ./configure --prefix /usr/local sudo make all install

Dupa aceasta se introduc linkuri simbolice in /usr/lib catre bibliotecile MPI din /usr/local/lib/

$cd /usr/lib /usr/lib$ sudo ln --symbolic -T /usr/local/lib/libmpi.so.1 libmpi.so.1 /usr/lib$ sudo ln --symbolic -T /usr/local/lib/libopen-pal.so.4 libopen-pal.so.4 /usr/lib$ sudo ln --symbolic -T /usr/local/lib/libmca_common_sm.so.3 \

libmca_common_sm.so.3

/usr/lib$ sudo ln --symbolic -T /usr/local/lib/libopen-rte.so.4 libopen-rte.so.4

Se dă restart si se testeaza unele comenzi. Daca totul a fost instalat corect, comanda

mpicc -show afiseaza:

1

Lucrarea 4 – Aplicatii MPI

gcc -I/usr/local/include -pthread -L/usr/local/lib -lmpi -ldl -lm -Wl,-- export-dynamic -lrt -lnsl -lutil -lm -ldl

Comanda ompi_info afiseaza:

Package: Open MPI user@Ubuntu-12-1 Distribution Open MPI: 1.6.2 Open MPI SVN revision: r27344 Open MPI release date: Sep 18, 2012 Open RTE: 1.6.2 Open RTE SVN revision: r27344 Open RTE release date: Sep 18, 2012 OPAL: 1.6.2 OPAL SVN revision: r27344 OPAL release date: Sep 18, 2012 MPI API: 2.1 Ident string: 1.6.2 Prefix: /usr/local

Instalarea OpenMPI într-un director oarecare. Daca se doreste instalarea OpenMPI în alt director decât directorul implicit (/usr/local), de exemplu in /usr/local/openmpi se specifică acest director ca argument al optiunii –-prefix al comenzii configure:

./configure --prefix /usr/local/openmpi sudo make all install

Calea către executabile (/usr/local/openmpi/bin) se adauga in PATH in fisierul .profile din directorul HOME al userului care utilizeaza biblioteca - ultima linie:

PATH="/usr/local/openmpi/bin:$PATH"

Linkurile simbolice trebuie sa contina targetul /usr/local/openmpi/lib, de exemplu:

/usr/lib$ sudo ln --symbolic -T /usr/local/openmpi/lib/libmpi.so.1 \

libmpi.so.1

4.1.2 Instalarea OpenMPI în HPC Dell PowerEdge

În HPC, administratorul de sistem a instalat biblioteca OpenMPI (versiunea 1.6.2) în directorul /opt/openmpi. Toate setările au fost deja efectuate și se pot executa compilări și lansări de programe OpenMPI pe toate cele 64 de procesoare (cores) ale nodurilor HPC. În plus, software-ul de cluster Rocks asigură replicarea automată a directoarelor utilizatorilor pe cele 4 noduri, astfel încât nu este necesară copierea unui executabil MPI pe fiecare nod.

4.1.3 Configurare SSH pentru comunicația cu autentificare fără parolă

În clusterele Linux (laborator B125a și B138) trebuie instalat serverul SSH pe fiecare dintre stații. Dintr-un utilizator cu drepturi de administrare se instaleaza serverul SSH cu comanda:

sudo apt-get install openssh-server

Se verifică fișierul de configurare /etc/ssh/sshd_config astfel încât sa conțină opțiunile:

RSAAuthentication yes PubkeyAuthentication yes

2

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

Se editează fișierul /etc/hosts (cu sudo gedit) astfel ca sa contina numele si adresa stațiilor.

În laboratorul B125a adresele IP sunt statice și fișierul /etc/hosts arată astfel:

141.85.107.201

h1

141.85.107.202

h2

141.85.107.203

h3

141.85.107.216

h16

În laboratorul B138 (ITEMS) adresele IP sunt dinamice (prin router DHCP), dar s-a făcut rezervarea adreselor în router, astfel ca acestea să rămână nemodificate. Pentru aceasta se intră în administrarea routerului (http://191.168.0.1) și se selecteaza LAN IP Setup. Se introduce adresa fizica MAC si adresa de retea in tabela Address Reservation (fie manual, fie selectatând adresa dispozitivului respectiv din Address Reservation Table). Fișierul /etc/hosts arată astfel:

ITEMS01.imag.pub.ro 192.168.0.101 ITEMS02.imag.pub.ro 192.168.0.102

ITEMS12.imag.pub.ro 192.168.0.112

Pentru fiecare utilizator (standard sau administator), pentru care se doreste comunicatia SSH fara parola, se executa urmatoarele setari.

a) Pe fiecare mașină, pentru fiecare utilizator care va comunica in retea, se generează o pereche

de chei publică-privată cu comanda:

$ ssh-keygen -t dsa

Nu se da passphrase de stocare a cheii private. Implicit (pentru utilizatorul user) aceste chei se depun în locațiile:

Your identification has been saved in /home/user/.ssh/id_dsa Your public key has been saved in /home/user/.ssh/id_dsa.pub

b) Pentru conectarea ssh la localhost fara parola este necesar ca si cheia publica generata pentru

fiecare host/user sa fie copiata in propria lista de chei autorizate authorized_keys cu comenzile:

cd .ssh cat id_dsa.pub >> authorized_keys

După aceasta setare trebuie sa nu se ceara parola la conexiunea la localhost cu comanda:

ssh localhost

c) Se copiază cheia publică generată a fiecărei mașini pe toate celelalte mașini care trebuie să

accepte autentificarea fără parolă prin comanda:

$ ssh-copy-id user@remotehost

Acestea

se

adaugă

în

/home/username/.ssh/authorized_keys. Se verifică

autentificarea reciprocă fără parolă, din ambele mașini, prin comanda:

$ ssh user@hostname

Dacă nu se cere parola la autentificare, înseamnă că s-a realizat corect conexiunea ssh cu autentificare fără parolă.

În clusterul HPC sunt instalate de administrator serverele ssh și este setată configurarea pentru comunicația cu autentificare fără parolă și sincronizarea (replicarea) directoarelor utilizatorilor în toate cele 4 noduri.

3

Lucrarea 4 – Aplicatii MPI

4.1.4. Compilarea si executia programelor OpenMPI

MPI este o bibliotecă pentru dezvoltarea programelor paralele portabile în aplicaţii C şi Fortran executate atât în multicalculoatore cât şi în reţele de calculatoare. Biblioteca MPI este standard: MPI 1.0 (1994), MPI 2.0 (1997) [MPI09] şi oferă diferite implementări: MPICH (cu sursă liberă), OpenMPI, MPIPRO, LAM etc. Programele MPI se execută (cu comanda mpirun sau mpiexec) sub controlul unui server (process manager – mpd, hydra, gforker, smpd, orte) care lansează în execuție procesele MPI pe mașinile specificate direct sau într-un fişier de configurare (hostfile). Comunicația între procesele MPI folosește în general protocolul ssh (Secure Shell). Programarea paralelă folosind biblioteca MPI este explicită: programatorul este responsabil cu crearea şi distribuirea proceselor şi cu transferul de mesaje între acestea, folosind construcţii MPI. Biblioteca MPI oferă sute de funcţii; cu puţine excepţii, funcţiile MPI returnează un cod de eroare (număr întreg - de tipul int, care este 0 la execuția corectă). Funcţiile MPI de bază sunt:

int MPI_Init – initializează execuția proceselor MPI int MPI_Finalize – termină execuția proceselor MPI int MPI_Comm_size – determina numarul de procese MPI int MPI_Comm_rank – determina rangul procesului MPI int MPI_Send – transmiterea blocantă a unui mesaj int MPI_Recv – receptionarea blocantă a unui mesaj

Pentru compatibilitate, MPI definește propriile tipuri de date și corespondența cu tipurile limbajului de implementare (C /C++ sau Fortran.) De exemplu, în C/C++ o parte din tipurile de date și corespondența cu tipurile MPI:

Tip de date MPI MPI_CHAR MPI_SHORT MPI_INT MPI_FLOAT MPI_DOUBLE

Tip de date C signed char signed short int signed int float double

Utilizarea bibliotecii MPI se poate face de orice utilizator (standard sau administrator) pentru care s-a setat comunicatia ssh fără parolă. Descrierea functiilor bibliotecii OpenMPI se găseste pe site-ul oficial (http://www.open- mpi.org/doc/current/) si in paginile man pe orice calculator pe care este instalata biblioteca. Cel mai simplu program MPI:

// Hello_MPI.c #include <stdio.h> #include <mpi.h> // Se poate lansa cu oricate procese int main(int argc, char *argv[]) { int rank, len; char hostname[MPI_MAX_PROCESSOR_NAME];

}

MPI_Init (&argc,&argv); MPI_Comm_rank (MPI_COMM_WORLD,&rank); MPI_Get_processor_name(hostname, &len);

hostname[len] = 0;

printf ("Hello MPI - Process %d host %s\n", rank, hostname); MPI_Finalize();

return 0;

// sir terminat cu null

4

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

Pentru aflarea numelui hostului în care are loc executia se foloseste functia MPI :

int MPI_Get_processor_name(char * hostname, int* len)

Bufferul în care se citește numele hostului (hostname) trebuie să aibă lungimea suficientă ca să încapă numele stației (MPI_MAX_PROCESSOR_NAME). Compilarea acestui program MPI se face cu comanda:

$ mpicc Hello_MPI.c -o Hello_MPI

Comanda de compilare mpicc este un “wrapper” pentru invocarea compilatorului gcc cu optiunile necesare (bibliotecile de inclus). Pentru executia cu mai multe procese pe hostul local (de exemplu pe hostul h1) se setează numărul de procese in optiunea –np a comenzii mpirun:

$ mpirun -np 3 Hello_MPI

Hello MPI – Process 0 host h1 Hello MPI – Process 2 host h1 Hello MPI – Process 1 host h1

Functionarea MPI in mod SPMD. Daca se doreste executia aceluiași program în mai multe statii (mod SPMD – Single Program, Multiple Data), se creează un fisier (hosts) care contine numele statiilor si numarul de slot-uri (procesoare disponibile pe care vor fi lansate procese MPI) pentru fiecare dintre ele; de ex. pe HPC:

$cat hosts_hpc

hpc

compute-0-1 slots=1 compute-0-2 slots=1

compute-0-3 slots=1

Statiile care se pot înscrie in fișierul hosts trebuie sa fie din cele incluse in fisierul /etc/hosts (cu adresa IP a fiecareia) si pentru care s-a setat comunicatia ssh fara parola pentru userul respectiv. După definirea fisierului hosts, se da acest fisier ca parametru opțiunii –hostfile a comenzii mpirun:

$ mpirun -hostfile hosts_hpc -np 10 Hello_MPI

Hello MPI - Proces 0 host hpc.intern Hello MPI - Proces 4 host hpc.intern Hello MPI - Proces 8 host hpc.intern Hello MPI - Proces 5 host compute-0-1.local Hello MPI - Proces 9 host compute-0-1.local Hello MPI - Proces 1 host compute-0-1.local Hello MPI - Proces 7 host compute-0-3.local Hello MPI - Proces 2 host compute-0-2.local Hello MPI - Proces 3 host compute-0-3.local

Functionarea este corectă dacă pe toate nodurile se gasesc aceeasi utilizatori si aceleasi fisiere executabile cu acelasi nume si cale în ierarhia de fișiere. În HPC aceste condiții sunt asigurate prin software-ul de cluster (Rocks). În clusterele din laboratoarele B125a sau B138, trebuie copiat manual fișierul executabil pe fiecare stație, în aceeași cale. Daca acest lucru este dificil, trebuie instalat NFS.

Dacă nu este instalat nici un planificator de procese (resource manager SLURM, Condor, PBS etc.), așa cum este cazul atâr în laborator cât și în HPC, procesele sunt creeate și distribuite în execuție de modulul de control al execuției din biblioteca OpenMPI, ORTE (OpenMPI Real-Time Executive).

slots=1

5

Lucrarea 4 – Aplicatii MPI

Numărul de procese care se creează este dat de argumentul opțiunii –np; în lipsa acestei opțiuni se creează atâtea procese cât este suma slots din fișierul dat ca argument opțiunii –hostfile; de exemplu, pentru fișierul dat hosts_hpc se vor creea 4 procese care se distribuie în cele 4 noduri, în ordinea din lista din hosts_hpc. Dacă argumentul np este mai mare decât suma slots din listă, se reia distribuirea în aceeași ordine, până ce se creează toate procesele, așa cum se vede în exemplul dat.

Functionarea MPI în mod MPMD. Se pot lansa programe diferite pe host-uri diferite (modul Multiple Programs, Multiple Data - MPMD) cu comanda:

$ mpirun --app appfile

Fisierul de executie apppfile conține câte un fișier executabil pentru fiecare host. Se creează încă trei fișiere sursă Hello_MPI_v0.c, Hello_MPI_v1.c și Hello_MPI_v2.c care afișează și versiunea de program (v0, v1, v2) și se adaugă versiunea și la numele executabilului. Se creează fișierul appfile_hpc astfel:

$ cat appfile_hpc

# Comments are supported; comments begin with #

# Application context files specify each sub-application in the

# parallel job, one per line. -np 1 -host user@hpc /home/user/Hello/Hello_MPI -np 1 -host user@compute-0-0 /home/user/Hello/Hello_MPI_v0 -np 1 -host user@compute-0-1 /home/user/Hello/Hello_MPI_v1 -np 1 -host user@compute-0-2 /home/user/Hello/Hello_MPI_v2

Se lansează execuția cu comanda:

$ mpirun --app appfile_hpc

Programul afișează mesajele:

Hello MPI - Process 0 host hpc.intern Hello MPI v1 - Process 1 host compute-0-1.local Hello MPI v2 - Process 2 host compute-0-2.local Hello MPI v3 - Process 3 host compute-0-3.local

Exercițiul E4.1.a Compilați și executați programul Hello_MPI.c cu diferite valori ale numărului de procese și hosturi în clusterul local și pe HPC. Experimentați modurile de execuție SPMD și MPMD.

4.1.6. Comunicații MPI

Biblioteca MPI implementează atât comunicații punct-la-punct cât și comunicaţii de grup (colective), transferându-se mesaje formate din vectori de date de un tip de date. În MPI sunt definite grupuri de procese și contexte de comunicație (communicators comunicatori). Un grup este o colecție ordonată de procese, fiecare proces cu un număr de identificare (rank – rang, id), care este folosit ca nume pentru comunicațiile între procese. Numerele rank încep cu 0 și sunt în continuare, în ordine. Un comunicator este un context de comunicație între procese MPI; există 2 tipuri:

Intra-comunicator – comunicator definit într-un singur grup de procese, atât pentru comunicații punct-la-punct, cât și pentru comunicații colective

Inter-comunicator – definește comunicația între două grupuri de procese disjuncte.

Toate implementările MPI definesc un intra-comunicator implicit (reprezentat prin constanta MPI_COMM_WORLD), care cuprinde grupul de procese creat la inițializare (MPI_Comm_Init). Pentru definirea altor grupuri de procese și comunicatori există funcții MPI speciale.

6

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

Pentru definirea dinamică a altor procese și regiuni paralele există funcția MPI_COMM_SPWAN, dar aceasta este foarte costisitoare ca timp de execuție și nu este recomandată (nici în standardul MPI, nici în doc. de implementare). De aceea, în general în MPI sunt preferați algoritmii cu o singură reg paralelă.

Comunicații MPI punct-la-punct. Biblioteca MPI implementează atât comunicații punct-la- punct cât și comunicaţii de grup (colective) prin functii de transfer multiplu. O comunicaţie punct-la-punct are loc între 2 procese dintr-un comunicator. Procesul sursă trimite un mesaj (send) către procesul destinaţie (care execută funcția receive) “Completarea” unei comunicaţii înseamnă că transferul mesajului s-a terminat și locaţiile de memorie folosite (bufferul) pot fi accesate în siguranţă:

la transmitere (send), poate fi reutilizat bufferul de tranmisie

la recepţie (receive), pot fi folosite variabilele recepționate în bufferul de recepție

Modurile de comunicaţie MPI diferă după modul de completare a transferului:

blocante – revenirea din funcţiile apelate înseamnă că tranferul este complet

ne-blocante – funcţiile revin imediat, dar utilizatorul trebuie să testeze completarea

Funcţia de transmitere blocantă:

int MPI_Send (void *buffer, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)

Tipul și semnificația argumentelor este următoarea:

buffer

- adresa unde este memorat mesajul la transmiţător

count

- numărul de date care se transmit

datatype

- tipul datelor transmise

dest

- adresa (rangul) procesului destinație (receptor)

tag

- markerul mesajului

comm

- comunicatorul în care are loc transmisia

Funcţia de recepţie blocantă:

int MPI_Recv (void *buffer, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status)

Starea comunicaţiei se obţine într-o structură de tip MPI_Status:

typedef struct MPI_Status { int MPI_SOURCE; int MPI_TAG; int MPI_ERROR;

/*

inf. privind nr. de octeti receptionati*/ };

Procesul receptor poate recepţiona de la orice proces sursă (dacă se specifică MPI_ANY_SOURCE) şi cu orice marker de mesaj (MPI_ANY_TAG) Pentru execuţia corectă a unei comunicaţii este necesar ca:

Procesele transmiţător şi receptor să aibă adrese (rang) valide în acelaşi comunicator

Mesajele să aibă acelaşi marker (tag)

Bufferul de recepţie să fie suficient de mare astfel încât să încapă mesajul primit

După terminarea recepţiei, procesul receptor poate obţine următoarele informaţii:

Sursa reală a mesajului (MPI_SOURCE – direct din structura de tip MPI_Status)

Markerul real al mesajului (MPI_TAG – direct din structura de tip MPI_Status)

7

Lucrarea 4 – Aplicatii MPI

Numărul real de date recepţionate se obține în variabila indicată de pointerul count - din structura de tip MPI_Status, prin apelul funcției:

int MPI_Get_count(MPI_Status *status,MPI_Datatype datatype,int *count)

Exemplu – Send_Receive_MPI.c

// Send_Receive_MPI.c

#include <stdio.h> #include <mpi.h> // Se lanseaza in executie cel putin 2 procese MPI int main(int argc, char *argv[]) { int rank, i, count, len = 8; float sendbuf[100], recvbuf[100]; MPI_Status status; MPI_Init (&argc,&argv); MPI_Comm_rank (MPI_COMM_WORLD, &rank);

if(rank == 0) {

// procesul transmitator

for(i = 0; i < len; ++i) sendbuf[i] = i; MPI_Send(sendbuf, len, MPI_FLOAT, 1, 55, MPI_COMM_WORLD);

}

else if (rank == 1){

// procesul receptor

MPI_Recv(recvbuf, 100, MPI_FLOAT, MPI_ANY_SOURCE, 55, MPI_COMM_WORLD, &status); MPI_Get_count(&status, MPI_FLOAT, &count); printf("Procesul %d a primit %d numere de la procesul %d \n", rank, count, status.MPI_SOURCE); for (i = 0; i < count; i++) printf(“%4.0f “); printf(“\n”);

}

MPI_Finalize(); return 0;

}

Compilare :

$ mpicc Send_Receive_MPI.c -o Send_Receive_MPI

Execuție:

$ mpirun -np 2 Send_Receive_MPI

Mesajul afișat la consolă : Procesul 1 a primit 8 numere de la procesul 0:

0

1

2

3

4

5

6

7

Comunicații colective MPI. Comunicațiile colective sunt sincrone, adică funcțiile nu revin decât după ce s-a terminat transferul. Aceasta funcționare asigură sincronizarea între procesele comunicante; de ex. între procesul root si toate celelalte procese în comunicațiile one-to-all, all-to-one:

procesul root nu continuă decât după ce s-au term toate transferurile. Dar procesele care nu comunică între ele pot să termine operațiile colective asincron și, dacă este necesară sincronizarea, trebuie să apelăm explicit MPI_Barrier().

implementează o operație de sincronizare (barieră) între toate procesele din

comunicatorul dat ca parametru:

MPI_Barrier

int MPI_Barrier (MPI_Comm comm);

MPI_Bcast - operaţie de difuziune unul-la-toate (one-to-all) prin care procesul root trimite count date de tipul datatype din bufferul buffer, tuturor proceselor din comunicatorul dat ca parametru (comm); prototipul funciei MPI_Bcast() :

int MPI_Bcast(void *buffer, int count, MPI_Datatype datatype, int root, \ MPI_Comm comm)

8

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

Exemplu - Bcast_MPI.c:

// Bcast_MPI.c #include <stdio.h> #include <mpi.h> /* Executie cu oricate procese */ int main (int argc, char *argv[]) { int rank, root = 0, buffer[8]; MPI_Init (&argc, &argv); MPI_Comm_rank (MPI_COMM_WORLD, &rank); if (rank == root) for (i = 0; i < 8; i++) buffer[i] = i; MPI_Bcast (&buffer, 8, MPI_INT, root, MPI_COMM_WORLD); printf("P%d: “); for (i = 0; i < 8; i++) printf(“%4d”,buffer[i]); printf(“\n”); MPI_Finalize(); return 0;

}

MPI_Gather () – comunicaţie toţi-la-unul (all-to-one) - toate procesele transmit date procesului root, care le asamblează într-un vector, în ordinea rangului proceselor (rank).

int MPI_Gather(void* sendbuf,int sendcnt,MPI_Datatype sendtype, \ void* recvbuf,int recvcnt,MPI_Datatype recvtype,int root,MPI_Comm comm)

Toate argumentele au semnificație numai în procesul root; pentru celelalte procese au

semnificație numai sendbuf, sendcount, sendtype, root și comm.

Nu se admite suprapunerea bufferelor recvbuf și sendbuf. Dacă datele aferente procesului root există deja în bufferul de recepție exact în poziția dorită, nu se mai face transferul pentru root și se pasează ca argument sendbuf MPI_IN_PLACE (numai în procesul root) . Dacă se colectează blocuri de date de dimensiuni diferite, se folosește MPI_Gatherv().

MPI_Scatter() - comunicaţie unul-la-toţi (one-to-all): procesul root transmite partiții de date diferite fiecărui proces din comunicator, în ordinea rangului.

int MPI_Scatter(void* sendbuf,int sendcnt,MPI_Datatype sendtype, \ void* recvbuf,int recvcnt,MPI_Datatype recvtype,int root, MPI_Comm comm)

Toate argumentele au semnificație numai în procesul root, pentru celelalte procese au semnificație numai recvbuf, recvcount, recvtype, root și comm; argumentele root și comm trebuie să aibă aceeași valoare pentru toate procesele. Nu se admite suprapunerea bufferelor recvbuf și sendbuf. Dacă datele aferente procesului root există deja în bufferul de recepție, nu se mai face transferul pentru root și se dă ca argument recvbuf MPI_IN_PLACE (numai în procesul root). Dacă se distribuie blocuri de date de dimensiuni diferite, se folosește funcția MPI_Scatterv() Atât în MPI_Scatter cât și în MPI_Gather numărul elementelor de tipul datatype care se transmit (sendcnt) sau se recepționează (recvcnt) este numărul care revine unui singur proces, nu cel pe care îl transmite, respectiv recepționează procesul root. De exemplu, pentru distribuirea sau colectarea unui vector de n elemente la p procese, sendcnt = recvcnt = s = n / p.

Exemplu - Scatter_Gather_MPI.c

// Scatter_Gather_MPI.c #include <stdio.h> #include <stdlib.h>

9

Lucrarea 4 – Aplicatii MPI

#include <mpi.h> int main (int argc, char *argv[]) { int i, p, s, rank, root = 0; MPI_Init (&argc, &argv); MPI_Comm_rank (MPI_COMM_WORLD, &rank); MPI_Comm_size (MPI_COMM_WORLD, &p); s = n / p; int *X = (int*)malloc(sizeof(int)*n); if (rank == root) for (i = 0; i < n; i++) X[i] = i; else for (i = 0; i < n; i++) X[i] = 0;

// Procesul root distribuie partitiile de date celorlalte procese if (rank==root) MPI_Scatter(X,s,MPI_INT,MPI_IN_PLACE,s,MPI_INT, root,\ MPI_COMM_WORLD); else MPI_Scatter(X,s,MPI_INT,X, s, MPI_INT, root, MPI_COMM_WORLD);

printf("P%d dupa MPI_Scatter: ", rank); for (i = 0; i < n; i++) printf("%d ", X[i]); printf ("\n");

// Modificare date in fiecare proces for (i = 0; i < s; i++) X[i] *= 10;

MPI_Barrier(MPI_COMM_WORLD); if (rank == root) printf("MPI_Barrier\n"); // Rezultatul se colecteaza in procesul root if (rank == root) MPI_Gather(MPI_IN_PLACE, s,MPI_INT, X, s, MPI_INT, \ root, MPI_COMM_WORLD); else MPI_Gather(X, s, MPI_INT, X, s, MPI_INT, root, MPI_COMM_WORLD); printf("P%d dupa MPI_Gather: ", rank); for (i = 0; i < n; i++) printf("%d ", X[i]); printf ("\n");

MPI_Finalize(); return 0;

}

Datele din vectorul X se initializează în procesul root cu valori crescătoare, de la 0 la (n-1) iar în celelalte procese cu 0. Procesul root transmite câte o partiție de s elemente celorlalte procese, care o recepționează în prima partiție (primele s elemente ale vectorului) apoi le modifică înmulțindu-le cu 10. După modificare toate procesele așteaptă la o barieră de sincronizare, după care procesul root colectează datele de la celelalte procese. Rezultatul execuției este:

$ mpirun -np 4 Scatter_Gather P0 dupa MPI_Scatter: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 P1 dupa MPI_Scatter: 4 5 6 7 0 0 0 0 0 0 0 0 0 0 0 0 P2 dupa MPI_Scatter: 8 9 10 11 0 0 0 0 0 0 0 0 0 0 0 0 P3 dupa MPI_Scatter: 12 13 14 15 0 0 0 0 0 0 0 0 0 0 0 0 MPI_Barrier P1 dupa MPI_Gather: 40 50 60 70 0 0 0 0 0 0 0 0 0 0 0 0 P2 dupa MPI_Gather: 80 90 100 110 0 0 0 0 0 0 0 0 0 0 0 0 P3 dupa MPI_Gather: 120 130 140 150 0 0 0 0 0 0 0 0 0 0 0 0 P0 dupa MPI_Gather: 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150

10

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

Exercițiul E4.1.b. Compilați și executați toate programele de comunicații MPI (Send_Receive.c, Bcast.c, Scatter_Gather.c). Urmăriți și explicați funcționarea pentru diferiți parametri.

4.2. Program MPI de inmultire a doua matrice

Se paralelizează același algoritm secvențial ca și în lucrările precedente, adică cel mai simplu algoritm sevenţial de înmulţire a două matrice pătrate a şi b de dimensiuni n x n, cu rezultat matricea c:

for (i = 0; i < n; i++) for (j = 0; j < n; j++) { c[i][j] = 0; for (k = 0; k < n; k++) c[i][j] += a[i][k]*b[k][j];

}

In fisierul Matrix_Mult_MPI.c este dat programul MPI de înmulţire paralelă a două matrice prin partiţionare unidimensională orientată pe linii. Această partiţionare este echivalentă cu distribuirea iteraţiilor buclei exterioare a algoritmului, deoarece aceste iteraţii sunt independente. Partea de program de alocare a datelor și înmulțire a submatricelor este următoarea:

#include <mpi.h>

int main(int argc, char *argv[]) {

int n = 1024; int p = 2; int max_rep = 1;

int rank, root = 0; int i, j, k, s, rep; // Citire parametri n, p, max_rep din linia de comanda // Initializare MPI MPI_Init (&argc,&argv); MPI_Comm_rank (MPI_COMM_WORLD, &rank); MPI_Comm_size(MPI_COMM_WORLD, &p); s = n / p;

// Diensiune matrice // Nr procese MPI // Numar de repetari executie

// Alocarea matricelor float **a = (float**)malloc(sizeof(float*)*n); float **b = (float**)malloc(sizeof(float*)*n); float **c = (float**)malloc(sizeof(float*)*n); float *ma = (float*)malloc(sizeof(float)*n*n); float *mb = (float*)malloc(sizeof(float)*n*n); float *mc = (float*)malloc(sizeof(float)*n*n); for (i = 0; i < n; i++){ a[i] = ma; ma += n; b[i] = mb; mb += n; c[i] = mc; mc += n;

}

// Initializarea matricelor // Transmiterea matricelor a, b if (rank == root && root == 0)

MPI_Scatter(a[0],s*n,MPI_FLOAT,MPI_IN_PLACE,s*n,MPI_FLOAT,root,\

MPI_COMM_WORLD); else MPI_Scatter(a[0],s*n,MPI_FLOAT,a[0],s*n,MPI_FLOAT,root, \

11

Lucrarea 4 – Aplicatii MPI

MPI_COMM_WORLD); MPI_Bcast(b[0], n*n, MPI_FLOAT, root, MPI_COMM_WORLD);

// Inmultire submatrice de s linii in fiecare proces for (i = 0; i < s; i++) for (j = 0; j < n; j++){ c[i][j] = 0; for (k = 0; k < n; k++) c[i][j] += a[i][k] * b[k][j];

}

// Colectare rezultate if (rank == root && root == 0) MPI_Gather(MPI_IN_PLACE,s*n,MPI_FLOAT, c[0], s*n, MPI_FLOAT, root,\ MPI_COMM_WORLD); else MPI_Gather(c[0], s*n, MPI_FLOAT, c[0], s*n,MPI_FLOAT, root, \ MPI_COMM_WORLD); // Memorare si afisare timp de executie MPI_Finalize(); return 0;

}

Numărul de procese MPI (p) nu este definit în program; se crează atâtea procese câte sunt specificate prin comanda mpirun, dar trebuie ca n (dimensiunea liniilor şi coloanelor matricelor a, b,

c) să fie divizibil cu p; dacă n nu este divizibil, trebuie să se adauge cod care să trateze această situaţie.

Fiecare din cele p procese prelucrează s = n / p linii (partiționare pe linii, cu partiții continue); dacă p < P (numărul de proceseare din cluster), atunci cele p procese se execută pe p procesoare diferite, performanțele depinzând de p și de încărcarea generală a clusterului.

Dimensiunea matricelor (n) se introduce ca parametru de execuție (valoarea implicită este n = 1024), iar matricele a, b, c se aloca dinamic, fiecare matrice fiind un tablou bidimensional ca bloc continuu de memorie, cu dimensiunea n x n. În acest fel liniile matricelor sunt memorate la adrese consecutive în memorie și pot fi transmise mai multe linii consecutive direct din matrice (cu MPI_Scatter, MPI_Bcast, MNPI_Gather), fără să fie nevoie să fie copiate într-un buffer. Această implementare este posibilă și dacă se alocă static matricele (dar atunci trebuie recompilat programul pentru fiecare nouă valoare a lui n) și dacă se alocă matricele ca tablouri unidimensionale (dar atunci elementul a[i][j] se accesează cu expresia a[i*n+j]). În schimb, alocarea matricelor cu linii neadiacente (așa cum s-a făcut în programele Pthread și OpenMP) nu garantează continuitatea blocului de memorie în care sunt memorate liniile consecutive și programul nu va funcționa.

Procesul root initializează datele de intrare, distribuie datele de intrare celorlalte procese, calculează propria partiție de date şi colectează partiţiile rezultatului. Pentru distribuirea matricei a se foloseşte funcţia MPI_Scatter care transmite fiecărui proces

câte o partiţie de s= n/p linii. Matricea b este transmisă tuturor celorlalte procese cu funcţia MPI_Bcast. După distribuirea datelor folosind MPI_Scatter datele ajung în prima partiţie (partiţia cu indice

0) în fiecare proces şi operaţiile de calcul se execută folosind această partiţie (indicele i parcurge valori

de la 0 la s – 1), iar rezultatul se obţine tot în această partiţie. Colectarea rezultatului în procesul root cu funcţia MPI_Gather aranjează corect partiţiile rez., aşa cum se vede în figura de mai jos pentru operaţia de înmulţire a două matrice cu 4 procese MPI.

12

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

P 0 P 1 P 2 P 3 Matricea a în root după inițializare Matricea
P 0
P 1
P 2
P 3
Matricea a în root după
inițializare
Matricea a după MPI_SCATTER
Matricea c după înmulțire partiții
a după MPI_SCATTER Matricea c după înmulțire partiții Matricea b în toate procesele Matricea c în

Matricea b în toate procesele

c după înmulțire partiții Matricea b în toate procesele Matricea c în root dup ă MPI_Gather

Matricea c în root după MPI_Gather

În acest program a fost alocat spaţiu pentru toate datele (matricele a, b, c) în toate procesele. Dar se observă că în toate procesele care nu sunt root se poate aloca numai o partiţie de dimensiune s (s linii de câte n elemente din matricele a şi c). Pentru aceasta, se poate face o alocare dinamică a datelor cu dimensiuni care depind de n, size (p) şi root. Compilarea se face cu comanda:

$ mpicc Matrix_Mult_MPI.c -o Matrix_Mult_MPI

Lansarea în execuție într-un cluster Linux sau clusterul HPC se face cu comanda:

$ mpirun -hostfile hosts –np p Matrix_Mult_MPI

Numarul de procese lansate p este dat de argumentul optiunii np si aceste procese se distribuie uniform procesoarelor (cores, slots) din lista hosts. Înmulțirea a două matrice cu n = 2048 în HPC cu p = 1,2,4,8,16,32,64 procesoare dă rezultatele:

$ mpirun -hostfile hosts_hpc -np 1 Matrix_Mult_MPI 2048

1 rep, n = 2048, p = 1, t = 78.552010 sec

$ mpirun -hostfile hosts_hpc -np 2 Matrix_Mult_MPI 2048

1 rep, n = 2048, p = 2, t = 47.296356 sec

$ mpirun -hostfile hosts_hpc -np 4 Matrix_Mult_MPI 2048

1 rep, n = 2048, p = 4, t = 22.401253 sec

$ mpirun -hostfile hosts_hpc -np 8 Matrix_Mult_MPI 2048

1 rep, n = 2048, p = 8, t = 10.859475 sec

$ mpirun -hostfile hosts_hpc -np 16 Matrix_Mult_MPI 2048

1 rep, n = 2048, p = 16, t = 7.327429 sec

$ mpirun -hostfile hosts_hpc -np 32 Matrix_Mult_MPI 2048

1 rep, n = 2048, p = 32, t = 3.832197 sec

$ mpirun -hostfile hosts_hpc -np 64 Matrix_Mult_MPI 2048

1 rep, n = 2048, p = 64, t = 2.592258 sec

Pentru măsurarea și memorarea performanțelor de execuție paralelă a programului se lansează un script care conține comenzi de execuție pentru diferite valori ale lui n (dimensiunea matricelor) și, pentru fiecare dimensiune, se execută în cluster cu un număr variat de procese. Fișierul de execuție Exec_Matrix_Mult_MPI este asemănător cu celelalte fișiere de execuție. Rezultatele se obțin în fișierul Res_Matrix_Mult_MPI.txt și graficele se trasează setând numele acestui fișier în programul Grafice.R

13

Lucrarea 4 – Aplicatii MPI

14
14

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

Compilați și executați programul Matrix_Mult_MPI.c pe stația Linux și în

clusterul HPC, verificati funcționarea corectă pentru diferite valori ale matricelor. Executați scriptul Exec_Matrix_Mult_MPI pe HPC și reprezentați graficele cu programul R dat.

Exercitiul E4.2.

4.3. Program MPI de adunare a doi vectori

Se implementează același algoritm de adunare a doi vectori x și y de dimensiune n, cu rezultat în y folosit și în aplicațiile Pthread:

for (i = 0; i < n; i++) y[i] = y[i] + x[i];

Distribuirea datelor în algoritmul de adunare a doi vectori:

vectorii x și y: procesul root distribuie partiții celorlalte procese (cu MPI_Scatter)

vectorul y: procesul root colectează partițiile rezultate din celel. procese (cu MPI_Gather)

Exercițiul E4.3. Dezvoltați programul MPI de adunare a doi vectori Vector_Add_MPI.c, executați pe stația Linux și pe HPC. Generați matricea de valori ale timpului de execuție folosind un script Exec_Vector_Add_MPI asemănător celui pentru înmulțirea a două matrice pentru n = (4096, 32768, 262144, 16777216), p = (1, 2, 4, 8, 12, 16). Reprezentați graficele performanțelor TP (p), S(p), E(p) cu parametru n, folosind programul R dat.

4.4. Program MPI de adunare a două matrice

Se implementează același algoritm de adunare a două matrice pătrate a și b de dimensiuni n x n folosit și în aplicațiile Pthread:

for (i = 0; i < n; i++) for (j = 0; j < n; j++) c[i][j] = a[i][j] + b[i][j];

Programul Matrix_Add_MPI.c poate fi identic cu cel de înmulțire a două matrice, cu deosebirea că se distribuie și matricea b (nu se difiuzează cu MPI_Bcast) și se înlocuiește partea de calcul a elementului c[i][j] al matricei de ieșire.

Exercițiul E4.4. Dezvoltați programul MPI de adunare a două matrice Matrix_Add_MPI.c, în mod asemănător programul de inmulțire a două matrice. Executați programul pe stația Linux și pe HPC. Generați matricea de valori ale timpului de execuție folosind un script Exec_Matrix_Add_MPI pentru aceleași valori ale lui p (1, 2, 4, 8, 16, 32, 64) și n = (64, 256, 512, 4096, 32768). Reprezentați graficele performanțelor TP (p), S(p), E(p) folosind programul Grafice.R

4.5. Program MPI de reducere paralelă

Funcțiile de reducere sunt folosite pentru a calcula un rezultat prin combinarea datelor distribuite într-un grup de procese MPI:

Funcția MPI_Reduce returnează valoarea rezultată prin combinarea valorilor din sendbuf în bufferul recvbuf în procesul root, iar MPI_Allreduce în toate procesele. Operația se aplică la count valori din sendbuf, cu rezultat în recvbuf (count 1).

15

Lucrarea 4 – Aplicatii MPI

int MPI_Reduce (void *sendbuf, void *recvbuf, int count, MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm) int MPI_Allreduce (void *sendbuf, void *recvbuf, int count, MPI_Datatype datatype, MPI_Op op, MPI_Comm comm)

Operatorii de reducere MPI_Op sunt următorii:

MPI_MAX

Maximum

MPI_MIN

Minimum

MPI_SUM

Sum

MPI_PROD

Product

MPI_LAND

Logical AND

MPI_BAND

Bitwise AND

MPI_LOR

Logical OR

MPI_BOR

Bitwise OR

MPI_LXOR

Logical exclusive OR

MPI_BXOR

Bitwise exclusive OR

MPI_MAXLOC

Maximum and index

MPI_MINLOC

Minimum and index

Operatorii MPI_MAXLOC și MPI_MINLOC permit aflarea maximului (respectiv minimului) dintre valorile distribuite și un index atașat valorii respective. De exemplu, pentru MAX_LOC:

⎛⎞ uvw ⎛⎞ ⎛ ⎞

⎜⎟

⎝⎠ ⎝⎠ ⎝ ⎠

⊗= ⎜⎟

i

jk

where w

=

max

(

,

u v

),

k

=

i

min

j

(),

i

j

if

if

if

u > v ⎤ ⎥ ⎥ ⎥ ⎦

u

u

v

=

<

v

Argumentul datatype trebuie să reprezinte o pereche (valoare, index). În MPI sunt definite mai multe astfel de tipuri combinate (MPI_FLOAT_INT, MPI_DOUBLE_INT etc.). De exemplu:

struct { float val; int rank; } in, out;

// rezultatul se obtine in variabila out din procesul root (count =1) MPI_Reduce (&in,&out,1, MPI_FLOAT_INT, MPI_MAXLOC, root, comm); //rezultatul se obtine in variabila out din toate procesele (count =1) MPI_Allreduce (&in,&out,1, MPI_FLOAT_INT, MPI_MAXLOC, comm);

Exercițiul E4.5. Studiați și executați programul Reduction_MPI.c pe stația Linux și pe HPC. Generați matricea de valori ale timpului de execuție folosind scriptul Exec_Reduction_OpenMP. Reprezentați graficele performanțelor TP (p), S(p), E(p) folosind programul R dat.

4.6. Program MPI de înmulțire succesivă a matricelor

Se implementează același algoritm de înmulțire succesivă a matricelor : c = a x b; e = d x c ca și cel implementat Pthread. Acest program conține două bucle succesive. Fiecare buclă este o buclă imbricată pe 3 niveluri, cu bucla exterioară paralelizabilă, care se distribuie celor p procese. Exită dependențe dintre fiecare iterație din a doua buclă și toate iterațiile din prima buclă, de aceea este necesară o barieră de sincronizare între cele două bucle. În MPI, bariera de sincronizare necesară este introdusă prin apelul

funcției MPI_Barrier().

16

Felicia Ionescu, Valentin Pupezescu – Laborator de Calcul paralel

Exercițiul E4.6. Implementați algoritmul de înmulțire succesivă a matricelor folosind biblioteca OpenMPI, în mod asemănător cu înmulțirea a două matrice. Executați programul pe stația Linux și pe HPC. Generați matricea de valori ale timpului de execuție folosind un script Exec_Many_Matrix_Mult_MPI pentru aceleași valori ale lui p (1, 2, 4, 8, 12, 16) și n = (16, 32, 64, 256, 1024, 4096). Reprezentați graficele performanțelor TP (p), S(p), E(p) folosind programul Grafice.R.

4.7. Algoritmii Fox și Cannon de înmulțire a două matrice

Algoritmii Fox și Cannon de înmulțire a două matrice folosesc partiționarea în blocuri a matricelor pentru reducerea spațiului de memorare a matricelor și suprapunerea calculelor cu comunicația. Implementați acești algoritmi MPI folosind documentația disponibilă pe Internet (opțional).

Bibliografie

1. Felicia Ionescu, Calcul paralel, 2014, slide-uri publicate pe site-ul moodle ETTI.

2. Message Passing Interface Standard, 2009

3. B. Barney, Message Passing Interface, Lawrence Livermore National Laboratory, 2013, https://computing.llnl.gov/tutorials/MPI/

17