Laboratorul 11 - Funcții de comunicare și de sincronizare în MPI

Responsabili: Florin Mihalache, Radu Ciobanu, Georgiana Fechet

Funcții nonblocante

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:

  • la send (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ă).
  • la receive (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:

  • blocantă, care este sincronă
  • non-blocantă, care este asincronă

Trimiterea blocantă este în patru moduri:

  • standard (MPI_Send)
  • sincronizată (MPI_Ssend)
  • buffered (MPI_Bsend)
  • ready (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.

MPI_Isend

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.

MPI_Irecv

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.

MPI_Test

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 testat
  • flag arată dacă operația de send sau de receive s-a terminat cu succes
  • status conține date legate de mesaj (relevante la operația de receive)

MPI_Wait

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 testat
  • status 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();
}

Alte funcții de schimb de mesaje

MPI_Ssend și MPI_Issend

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.

MPI_Bsend și MPI_Ibsend

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 și MPI_Irsend

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();
}

MPI_Sendrecv

Î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();
}

MPI_Barrier

Î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)

Mai multe despre lucrul cu fișiere în MPI puteți găsi aici: MPI I/O.

Structuri în MPI

Î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 bloc
  • MPI_Aint array_of_displacements[] (↓) - deplasarea octeților pentru fiecare bloc
  • array_of_types[] (↓) - tipul de elemente din fiecare bloc
  • *newtype (↑) - noul tip de date

Convenție: parametrii de tip input sunt marcați cu ↓, iar parametrii de tip output cu ↑.

Deplasarea octeților pentru fiecare bloc se calculează cu ajutorul funcției 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();
}

Exerciții

Scheletul de cod

  1. Implementați algoritmul inel folosind folosind funcții non-blocante, folosind scheletul din 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).
  2. Rezolvați deadlock-ul din fișierul deadlock.c din scheletul de laborator în 3 variante (verificați și hint-ul de mai jos):
    • folosind MPI_Sendrecv
    • folosind MPI_Bsend
    • folosind funcții non-blocante - aici în loc de MPI_Test și MPI_Wait folosiți MPI_Waitall
  3. Plecând de la scheletul de cod din 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.

Pentru exercițiul 2 puteți să vă folosiți de acest hint.

Adăugarea în coadă se face în felul următor: q.arr[q.size++] = element;

apd/laboratoare/11.txt · Last modified: 2023/01/09 12:11 by gabriel.gutu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0