Responsabili: Florin Mihalache, Radu Ciobanu, Georgiana Fechet
Până acum în cadrul laboratoarelor de MPI ați lucrat cu funcții de comunicare blocante (MPI_Send
, MPI_Recv
). În cadrul comunicării blocante, funcțiile de send și de receive se blochează până când buffer-ul folosit pentru transmisia de mesaje poate fi refolosit, mai precis în felul următor:
MPI_Send
) se întoarce un rezultat când buffer-ul unde se pun datele pentru transmisie poate folosit din nou (până atunci trimiterea este blocată).MPI_Recv
) se întoarce un rezultat când toate datele transmise prin buffer pot fi prelucrate (până atunci primirea este blocată).Comunicarea mesajelor este împărțită în două mari categorii:
Trimiterea blocantă este în patru moduri:
MPI_Send
)MPI_Ssend
)MPI_Bsend
)MPI_Rsend
)
În ceea ce privește comunicarea non-blocantă, funcțiile de trimitere (MPI_Isend
) și de primire (MPI_Irecv
) a datelor întorc imediat un rezultat. Astfel, nu avem rezultate sigure legate de terminarea trimiterii de date, așadar ca să ne asigurăm că datele au fost trimise și primite corect și complet putem folosi funcțiile MPI_Test
și MPI_Wait
, despre care vom vorbi în detaliu mai jos.
Comunicarea non-blocantă este utilă pentru situații în care avem deadlock sau în care se trimit date de dimensiuni mari.
Semnătura funcției:
int MPI_Isend(const void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request)
Parametrii funcției MPI_Isend
se comportă la fel ca parametrii funcției MPI_Send
, însă ce este în plus este parametrul MPI_Request *request
, MPI_Request
fiind o structură folosită pentru testarea trimiterii și primirii datelor.
Semnătura funcției:
int MPI_Irecv(void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request *request)
Spre deosebire de MPI_Recv
, MPI_Irecv
nu are parametru de MPI_Status
, care este înlocuit de parametrul MPI_Request
. În rest totul este identic ca la MPI_Recv
.
Semnătura funcției:
int MPI_Test(MPI_Request *request, int *flag, MPI_Status *status)
unde:
request
este cel de la funcția de send sau de receive, al cărui rezultat este testatflag
arată dacă operația de send sau de receive s-a terminat cu successtatus
conține date legate de mesaj (relevante la operația de receive)Semnătura funcției:
int MPI_Wait(MPI_Request *request, MPI_Status *status)
unde:
request
este cel de la funcția de send sau de receive, al cărui rezultat este testatstatus
conține date legate de mesaj (relevante la operația de receive)
Mai întâi se testează folosind MPI_Test
dacă o funcție (send sau receive) a terminat de trimis / primit datele. Dacă nu s-a completat operația de send / receive, atunci se va folosi MPI_Wait
.
Exemplu de send-receive folosind funcții non-blocante:
#include <mpi.h> #include <stdio.h> #include <string.h> #include <unistd.h> #define MAX_LEN 10 int main(int argc, char *argv[]) { int numtasks, rank, dest, source, count, flag, tag = 1; char inmsg[MAX_LEN], outmsg[] = "Hello"; MPI_Status status; MPI_Request request; MPI_Init(&argc,&argv); MPI_Comm_size(MPI_COMM_WORLD, &numtasks); MPI_Comm_rank(MPI_COMM_WORLD, &rank); if (rank == 0) { // procesul 0 trimite catre procesul 1 dest = 1; source = 1; sleep(3); MPI_Isend(outmsg, strlen(outmsg) + 1, MPI_CHAR, dest, tag, MPI_COMM_WORLD, &request); MPI_Test(&request, &flag, &status); if (flag) { printf("[P0] The send operation is over\n"); } else { printf("[P0] The send operation is not over yet\n"); MPI_Wait(&request, &status); } printf("[P0] The send operation is definitely over\n"); } else if (rank == 1) { // procesul 1 asteapta mesaj de la procesul 0 dest = 0; source = 0; sleep(1); MPI_Irecv(inmsg, MAX_LEN, MPI_CHAR, source, tag, MPI_COMM_WORLD, &request); MPI_Test(&request, &flag, &status); if (flag) { printf("[P1] The receive operation is over\n"); } else { printf("[P1] The receive operation is not over yet\n"); MPI_Wait(&request, &status); } /* se foloseste variabila de status pentru a afla detalii despre schimbul de date MPI_Get_count - calculeaza cate elemente s-au primit */ MPI_Get_count(&status, MPI_CHAR, &count); printf("[P1] Received %d char(s) from process %d with tag %d: %s\n", count, status.MPI_SOURCE, status.MPI_TAG, inmsg); } MPI_Finalize(); }
Funcția MPI_Ssend
reprezintă o variantă sincronizată a funcției MPI_Send
, care poate debloca comunicarea înainte ca procesul destinație să confirme că a primit cu succes toate datele trimise de către procesul sursă.
MPI_Ssend
garantează că va fi deblocată comunicarea atunci când procesul destinație confirmă că a primit cu succes toate datele trimise de către procesul sursă, deci se poate considera că MPI_Ssend
este o variantă mai sigură a funcției MPI_Send
.
De notat faptul că MPI_Send
se comportă ca MPI_Ssend
atunci când se trimit date de dimensiuni mari.
Un dezavantaj al acestei funcții față de MPI_Send
este că MPI_Ssend
este mai predispusă la situații de deadlock.
Un exemplu de deadlock este următoarea situație:
if (rank == 0) { MPI_Send(&num1, SIZE, MPI_INT, 1, 0, MPI_COMM_WORLD); // MPI_Ssend(&num1, SIZE, MPI_INT, 1, 0, MPI_COMM_WORLD); MPI_Recv(&num2, SIZE, MPI_INT, 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); } else { MPI_Send(&num2, SIZE, MPI_INT, 0, 0, MPI_COMM_WORLD); // MPI_Ssend(&num2, SIZE, MPI_INT, 1, 0, MPI_COMM_WORLD); MPI_Recv(&num1, SIZE, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); }
MPI_Issend
este varianta non-blocantă a funcției MPI_Ssend
.
Funcția MPI_Bsend
reprezintă o variantă a funcției MPI_Send
, unde utilizatorul creează un buffer (folosind funcția MPI_Buffer_attach
), prin intermediul căruia sunt schimbate mesajele. În acest buffer ajung doar mesajele care au fost trimise folosind MPI_Bsend
.
Această funcție este utilă pentru situațiile când avem deadlock și când se trimit date de dimensiuni mari, similar ca la comunicarea non-blocantă. Diferența față de comunicarea non-blocantă este că la MPI_Bsend
se garantează, atunci când funcția întoarce un rezultat, că datele au fost copiate integral în buffer, în timp ce MPI_Isend
nu garantează acest lucru.
MPI_BSEND_OVERHEAD
reprezintă un overhead de memorie, care este creat atunci când se apelează MPI_Bsend
sau MPI_Ibsend
.
MPI_Ibsend
este varianta non-blocantă a funcției MPI_Bsend
.
Exemplu de folosire:
#include "mpi.h" #include <stdio.h> #include <stdlib.h> int main (int argc, char *argv[]) { int numtasks, rank; int size = 100; int arr[size]; MPI_Init(&argc, &argv); MPI_Comm_size(MPI_COMM_WORLD, &numtasks); MPI_Comm_rank(MPI_COMM_WORLD, &rank); if (rank == 0) { for (int i = 0; i < size; i++) { arr[i] = i; } printf("Process with rank [%d] has the following array:\n", rank); for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } printf("\n"); // declarăm mărimea buffer-ului int buffer_attached_size = MPI_BSEND_OVERHEAD + size * sizeof(int); // se creează spațiul folosit pentru buffer char* buffer_attached = malloc(buffer_attached_size); // se creează buffer-ul MPI folosit pentru trimiterea mesajelor MPI_Buffer_attach(buffer_attached, buffer_attached_size); MPI_Bsend(&arr, size, MPI_INT, 1, 1, MPI_COMM_WORLD); printf("Process with rank [%d] sent the array.\n", rank); // se detașează buffer-ul folosit pentru trimiterea mesajelor și este distrus MPI_Buffer_detach(&buffer_attached, &buffer_attached_size); free(buffer_attached); } else { MPI_Status status; MPI_Recv(&arr, size, MPI_INT, 0, 1, MPI_COMM_WORLD, &status); printf("Process with rank [%d], received array with tag %d.\n", rank, status.MPI_TAG); printf("Process with rank [%d] has the following array:\n", rank); for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } printf("\n"); } MPI_Finalize(); }
MPI_Rsend
reprezintă o variantă a funcției MPI_Send
, care poate să fie folosită după execuția funcției de receive (doar atunci poate fi folosită această funcție, deși putem folosi atunci și MPI_Send
).
Un exemplu de use case al acestei funcții este atunci când un proces A apelează MPI_Recv
sau MPI_Irecv
înainte de o barieră, iar un alt proces B apelează MPI_Rsend
(unde B îi trimite date procesului A).
MPI_Irsend
este varianta non-blocantă a funcției MPI_Rsend
.
Exemplu de folosire:
#include <stdio.h> #include <stdlib.h> #include <mpi.h> int main(int argc, char* argv[]) { int size, rank, value; MPI_Init(&argc, &argv); MPI_Comm_size(MPI_COMM_WORLD, &size); MPI_Comm_rank(MPI_COMM_WORLD, &rank); if (rank == 0) { MPI_Barrier(MPI_COMM_WORLD); value = 12345; printf("[P0] MPI process sends value %d.\n", value); MPI_Rsend(&value, 1, MPI_INT, 1, 0, MPI_COMM_WORLD); } else { MPI_Request request; MPI_Status status; int flag; MPI_Irecv(&value, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, &request); MPI_Barrier(MPI_COMM_WORLD); MPI_Test(&request, &flag, &status); if (flag) { printf("[P1] The receive operation is over\n"); } else { printf("[P1] The receive operation is not over yet\n"); MPI_Wait(&request, &status); } printf("[P1] MPI process received value %d.\n", value); } MPI_Finalize(); }
În cadrul funcției MPI_Sendrecv
se execută combinat operațiile de send și de receive, care sunt blocante în acest caz, unde mai precis se trimite un mesaj către un proces și se primește un alt mesaj de la un proces (poate să fie același proces căruia i s-a trimis mesajul sau un proces diferit).
MPI_Sendrecv
este util în situații în care poate apărea deadlock, de exemplu trimiterea înlănțuită sau în ciclu de mesaje, unde fiecare proces să facă mai întâi send, apoi receive, fapt ce duce la dependență ciclică de date, care rezultă în deadlock.
Exemplu de folosire:
#include <mpi.h> #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { int numtasks, rank, dest, source, count, tag = 1; char inmsg, outmsg; MPI_Status status; MPI_Init(&argc, &argv); MPI_Comm_size(MPI_COMM_WORLD, &numtasks); MPI_Comm_rank(MPI_COMM_WORLD, &rank); if (rank == 0) { // procesul 0 trimite către procesul 1 și apoi așteaptă răspuns dest = 1; source = 1; outmsg = '0'; MPI_Sendrecv(&outmsg, 1, MPI_CHAR, dest, tag, &inmsg, 1, MPI_CHAR, source, tag, MPI_COMM_WORLD, &status); } else if (rank == 1) { // procesul 1 așteaptă mesaj de la procesul 0, apoi trimite răspuns dest = 0; source = 0; outmsg = '1'; MPI_Sendrecv(&outmsg, 1, MPI_CHAR, dest, tag, &inmsg, 1, MPI_CHAR, source, tag, MPI_COMM_WORLD, &status); } // se folosește variabila de status pentru a afla detalii despre schimbul de date MPI_Get_count(&status, MPI_CHAR, &count); printf("Process %d received %d char(s) from process %d with tag %d: %c\n", rank, count, status.MPI_SOURCE, status.MPI_TAG, inmsg); MPI_Finalize(); }
În MPI există conceptul de barieră, care este similar ca funcționalitate cu cea din pthreads și din Java threads. Mai precis, bariera din MPI asigură faptul că niciun proces din cadrul comunicatorului nu poate trece mai departe de punctul în care este plasată bariera decât atunci când toate procesele din comunicator ajung în acel punct.
Semnătura funcției (varianta blocantă):
int MPI_Barrier(MPI_Comm comm)
unde parametrul comm reprezintă comunicatorul MPI în care rulează procesele.
De asemenea, există o versiune non-blocantă a barierei în MPI:
int MPI_Ibarrier(MPI_Comm comm, MPI_Request *request)
Use case: Vreau să mă asigur că un proces a terminat de scris înainte ca altul să citească. Exemplu de folosire:
#include <stdio.h> #include <mpi.h> #include <stdlib.h> int main(int argc, char **argv) { MPI_File out; int rank, numtasks; int ierr; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Comm_size(MPI_COMM_WORLD, &numtasks); remove("out.txt"); ierr = MPI_File_open(MPI_COMM_WORLD, "out.txt", MPI_MODE_CREATE | MPI_MODE_RDWR, MPI_INFO_NULL, &out); if (ierr) { if (rank == 0) fprintf(stderr, "%s: Couldn't open output file %s\n", argv[0], argv[2]); MPI_Finalize(); exit(1); } if (rank == 0) { char message_to_write[5] = "hello"; MPI_File_write_at(out, 0, message_to_write, 5, MPI_CHAR, MPI_STATUS_IGNORE); } MPI_Barrier(MPI_COMM_WORLD); if (rank == 1) { char message_to_read[5]; MPI_File_read_at(out, 0, message_to_read, 5, MPI_CHAR, MPI_STATUS_IGNORE); printf("\nMesajul citit este: "); for (int i = 0; i < 5; i++) printf("%c", message_to_read[i]); printf("\n"); } MPI_File_close(&out); MPI_Finalize(); return 0; }
Pentru citirea și scrierea în fișier am folosit funcțiile MPI_File_read_at
și MPI_File_write_at
. Amândouă citesc din fișier de la un offset specificat.
int MPI_File_read_at(MPI_File fh, MPI_Offset offset, void *buf, int count, MPI_Datatype datatype, MPI_Status *status)
În MPI putem să schimbăm date de tipul struct. Putem să facem acest lucru creând mai întâi un nou tip de date MPI (MPI_Datatype
), folosind MPI_Type_create_struct
căruia îi precizăm numărul câmpurilor de fiecare tip și mărimea lor din structură, așezarea câmpurilor în memorie (folosind offsets și MPI_Aint
).
Semnătura funcției MPI_Type_create_struct
este următoarea:
int MPI_Type_create_struct(int count, const int array_of_blocklengths[], const MPI_Aint array_of_displacements[], const MPI_Datatype array_of_types[], MPI_Datatype *newtype)
Și are următorii parametri:
count
(↓) - număr de blocuri (de asemenea, numărul de intrări în vectorii array_of_types, array_of_displacements și array_of_blocklengths)array_of_blocklengths[]
(↓) - numărul de elemente din fiecare blocMPI_Aint array_of_displacements[]
(↓) - deplasarea octeților pentru fiecare blocarray_of_types[]
(↓) - tipul de elemente din fiecare bloc*newtype
(↑) - noul tip de date
offsetof
. Mai multe detalii despre folosirea ei puteți găsi aici: tutorialspoint.
Exemplu de folosire:
#include <mpi.h> #include <stdio.h> #include <stddef.h> // structura aflata la baza tipului custom typedef struct { float f1, f2; char c; int i[2]; } custom_type; int main(int argc, char *argv[]) { int numtasks, rank, source = 0, dest = 1, tag = 1; custom_type t; MPI_Datatype customtype, oldtypes[4]; int blockcounts[4]; MPI_Aint offsets[4]; MPI_Status status; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Comm_size(MPI_COMM_WORLD, &numtasks); // campul f1 offsets[0] = offsetof(custom_type, f1); oldtypes[0] = MPI_FLOAT; blockcounts[0] = 1; // campul f2 offsets[1] = offsetof(custom_type, f2); oldtypes[1] = MPI_FLOAT; blockcounts[1] = 1; // campul c offsets[2] = offsetof(custom_type, c); oldtypes[2] = MPI_CHAR; blockcounts[2] = 1; // campul i offsets[3] = offsetof(custom_type, i); oldtypes[3] = MPI_INT; blockcounts[3] = 2; // se defineste tipul nou si se comite MPI_Type_create_struct(4, blockcounts, offsets, oldtypes, &customtype); MPI_Type_commit(&customtype); if (rank == 0) { t.f1 = 0.5; t.f2 = -1.2; t.c = 'a'; t.i[0] = 0; t.i[1] = 1; MPI_Send(&t, 1, customtype, dest, tag, MPI_COMM_WORLD); } else if (rank == 1) { MPI_Recv(&t, 1, customtype, source, tag, MPI_COMM_WORLD, &status); printf("Received custom type with f1=%.1f, f2=%.1f, c=%c, i[0]=%d, i[1]=%d\n", t.f1, t.f2, t.c, t.i[0], t.i[1]); } // se elibereaza tipul nou cand nu se mai foloseste MPI_Type_free(&customtype); MPI_Finalize(); }
ring.c
, unde procesul cu id-ul 0 trimite un număr aleatoriu către procesul 1 (vecinul său), iar apoi celelalte noduri vor primi numărul de la procesul precedent, îl incrementează cu 2 și îl trimit la următorul proces (de exemplu procesul 2 primește valoarea de la procesul 1, o incrementează și o trimite mai departe procesului 3), totul terminându-se când valoarea ajunge la procesul 0. Pentru fiecare proces trebuie să afișați rangul acestuia și valoarea primită (hint: primul exercițiu din laboratorul 8).deadlock.c
din scheletul de laborator în 3 variante (verificați și hint-ul de mai jos):MPI_Sendrecv
MPI_Bsend
queue.c
, unde rulează procesele în cadrul unei topologii inel, creați un nou tip de date în MPI, plecând de la structura definită în cod. Fiecare proces va genera un număr random, care va fi adăugat în structura respectivă, la final procesul 0 având fiecare număr generat de fiecare proces. După aceea, procesul 0 va afișa ce elemente se află în structura respectivă, care este o structură de tip coadă.
Bonus. Pornind de la schelet, folosiți bariera în fișierul barrier.c
pentru a vă asigura că în fișier output-ul este mereu hello.
q.arr[q.size++] = element;