PWiR lab 10: Inne typy komunikacji w MPI

Na poprzednich laboratoriach poznaliśmy podstawowe funkcje MPI do komunikacji punkt do punktu oraz do komunikacji grupowej. Na dzisiejszym laboratorium przećwiczymy pozostałe schematy komunikacji grupowej oraz komunikację nieblokującą.

Literatura

Pytania


Spis treści

  1. Pliki, z których będziemy korzystać
  2. Komunikacja grupowa
  3. Ćwiczenie samodzielne: Wyznaczanie Pi
  4. Komunikacja nieblokująca
  5. Ćwiczenie samodzielne: Pierścień

Pliki, z których będziemy korzystać

W niniejszym scenariuszu będziemy korzystać z następujących przykładowych programów (do pobrania tutaj):
Makefile
Plik Makefile.
scatter-gather-max.c
Równoległy program obliczający maksimum używający schematu komunikacji scatter-gather udostępnianego przez MPI.
scatter-gather-max.ll
Specyfikacja zadania dla programu obliczającego maksimum poprzez scatter-gather.
ring-nonblocking.c
Równoległy program demonstrujący nieblokującą komunikację w MPI.
ring-nonblocking.ll
Specyfikacja zadania dla programu demonstrującego nieblokującą komunikację w MPI.

Komunikacja grupowa

Komunikacja przez rozgłaszanie, którą poznaliśmy na ostatnich zajęciach, jest tylko jednym z przykładów komunikacji grupowej. Inne schematy komunikacji grupowej ujęte w funkcjach MPI to:

MPI_Scatter i MPI_Gather
Odpowiednio rozpraszają i zbierają wyniki, jak w przykładzie 4 procesów poniżej.
              data                               data
        P0: D1 D2 D3 D4      scatter      P0: D1 -- -- --
        P1: -- -- -- --      ------>      P1: D2 -- -- --
        P2: -- -- -- --      <------      P2: D3 -- -- --
        P3: -- -- -- --       gather      P3: D4 -- -- --
MPI_Allgather
Jak gather, ale każdy z procesów otrzymuje wszystkie dane.
MPI_Reduce
Wywołuje operację agregacji (np. sumowanie, obliczanie maksimum, itp.) na określonych danych każdego procesu i umieszcza wynik w buforze jednego procesu.
MPI_Allreduce
J.w. tyle że kopia wyniku będzie umieszczona w buforze każdego procesu.

Dodatkowo, dość częstym mechanizmem synchronizacyjnym w aplikacjach równoległych jest tzw. bariera. Bariera to miejsce w kodzie aplikacji równoległej, do którego muszą dojść wszystkie procesy wykonujące tę aplikację, aby którykolwiek z procesów mógł przejść dalej. MPI udostępnia własną barierę, której używa się przy pomocy funkcji:

  MPI_Barrier

Więcej informacji o funkcjach można znaleźć w manualu. Przykład użycia schematu scatter-gather znajduje się w pliku scatter-gather-max.c.


Ćwiczenie samodzielne: Wyznaczanie Pi

Jak wiadomo z matematyki, wartość liczby π można wyznaczyć przez całkowanie funkcji 4 / (x2 + 1) na przedziale 0...1 (dla zapominalskich wyjaśnienie np. tutaj). Napisz program wyznaczający liczbę π poprzez sumowanie metodą trapezów. Przedział 0...1 dzielimy na P podprzedziałów numerowanych od 0 do P - 1. Każdy i-ty z N procesów (gdzie i = 0,1,...,N-1 a N <= P) wylicza sumę pól trapezów wyznaczonych przez podprzedziały i + j * N, dla kolejnych j=0,1,2,... takich że i + j * N < P. Następnie używając odpowiedniego wariantu funkcji MPI_Reduce proces zerowy sumuje częściowe wyniki z każdego procesu i wypisuje wartość liczby π na standardowe wyjście.

Uruchom program na klastrze, dla P = 1000000 oraz N = 10. Wypisz π z precyzją co najmniej 10 cyfr po przecinku.


Komunikacja nieblokująca

Jak wspomnieliśmy na poprzednim laboratorium, semantyka operacji MPI_Send nie jest ściśle sprecyzowana. Nie ma żadnych gwarancji co do tego, że po opuszczeniu funkcji MPI_Send, wysyłany komunikat został odebrany (i przetworzony przez obiorcę). Nie ma nawet gwarancji, że odbiorca zaczął odbierać komunikat. MPI gwarantuje jedynie, że po powrocie z funkcji, obszar pamięci zajmowany przez komunikat może zacząć zostać modyfikowany oraz wspomina, że operacja MPI_Send może się zablokować. Ta ostatnia cecha sprawia w szczególności, że poniższy kod jest niepoprawny.

  // Proces A:
  ...
  MPI_Send(..., processB, msgTagA, ...);
  MPI_Recv(..., processB, msgTagB, ...);
  ...

  // Proces B:
  ...
  MPI_Send(..., processA, msgTagB, ...);
  MPI_Recv(..., processA, msgTagA, ...);
  ...

Powodem jest fakt, że każdy z procesów A i B może się zablokować na swojej funkcji MPI_Send i w rezultacie żaden z nich nie będzie mógł zacząć odbierać komunikatu od drugiego procesu, co doprowadzi do zakleszczenia.

MPI udostępnia jednak operacje o bardziej sprecyzowanej semantyce (patrz odpowiednie strony manuala). Operacje te dzielą się na blokujące i nieblokujące. Przykłady operacji blokujących to:

MPI_Ssend
Synchroniczny, blokujący wariant wysyłania. Wysyła komunikat i blokuje się dopóki pamięć zawierająca komunikat może zostać użyta ponownie przez aplikację zaś odbiorca zaczął faktycznie odbierać komunikat (za pomocą odpowiedniej funkcji *recv).
MPI_Bsend
Buforowany, blokujący wariant wysyłania. Funkcja kończy się, gdy wysyłany komunikat został skopiowany do lokalnego bufora. Komunikat być może nawet nie zaczął być faktycznie wysyłany. Bufory, do których kopiowane są komunikaty kontroluje się za pomocą pary funkcji MPI_Buffer_attach i MPI_Buffer_detach.
MPI_Rsend
Blokujący wariant zakładający gotowość odbiorcy. Może być użyty jedynie, gdy nadawca wie, że odbiorca wywołał już odpowiednią funkcję *recv. Uwaga: może prowadzić do błędów.
MPI_Sendrecv
Kombinacja wysyłania i odbierania. Rozpoczyna wysyłanie wiadomości (o semantyce takiej jak MPI_Send), ale wcześniej instaluje uchwyt do odbioru wiadomości. Funkcja kończy się, gdy pamięć zajmowana przez wysyłany komunikat może być reużyta a bufor użyty do odbioru zawiera odbierany komunikat. Uwaga: komunikat wysyłany nie musi być jeszcze dostarczony do odbiorcy.

Przykłady nieblokujących operacji natomiast to:

MPI_Isend
Rozpoczyna nieblokujące (asynchroniczne) wysyłanie komunikatu. Do momemtu zakończenia wysyłania, obszar pamięci zajmowany przez komunikat nie może być modyfikowany. Fakt dostarczenia komunikatu możemy sprawdzić używając funkcji MPI_Test. Możemy także zaczekać na dostarczenie komunikatu używając funkcji MPI_Wait.
MPI_Irecv
Rozpoczyna nieblokujące (asynchroniczne) odbieranie komunikatu. Do momentu zakończenia odbierania, bufor użyty do odbioru nie może być modyfikowany. Stan komunikatu możemy testować i kontrolować ponownie używając funkcji MPI_Test i MPI_Wait.
MPI_Issend, MPI_Ibsend, MPI_Irsend
Analogicznie jak dla wersji blokujących.
MPI_Probe
Nieblokująco sprawdza, czy dostępny jest komunikat — być może określonego odbiorcy lub określonego typu.

Przykład nieblokującej komunikacji znajduje się w pliku ring-nonblocking.c.

Więcej informacji o różnych semantykach komunikatów w MPI można znaleźć w podanej wyżej literaturze.


Ćwiczenie samodzielne: Pierścień

Używając nieblokującej komunikacji MPI, zaimplementuj następujący rozproszony algorytm obliczający maksimum. Procesy są zorganizowane w logiczny pierścień. Następnikiem procesu i jest proces (i + 1) mod N, gdzie N to liczba procesów. Analogicznie definiujemy poprzednika. Każdy proces i przechowuje parę (liczba Li, ranga Ri). Algortym działa w rundach. W każdej rundzie każdy proces losuje liczbę l, zapisuje ją i swoją rangę i jako parę (Li, Ri) i przekazuje tę parę do następnika. Jeśli proces i dostanie parę (L(i - 1) mod N, R(i - 1) mod N) od poprzednika, to:

Jeśli para wróci do jakiegoś procesu j (co można sprawdzić po randze) to proces j wypisuje skojarzoną liczbę na standardowe wyjście jako obliczone maksimum, przekazuje parę do następnika i czeka na barierze. Jeśli dowolny proces k (różny od j) dostanie parę z maksimum, to wypisuje skojarzoną liczbę na standardowe wyjście jako wynik maksimum, przekazuje do następnego procesu (o ile nie jest to j) i czeka na barierze. Po przejściu bariery, algorytm rozpoczyna kolejną rundę.

Wskazówka: Zastanów się, jak sprawić, aby algorytm się zakończył.

Osoby zainteresowane komunikacją, odpornością na błędy i synchronizacją w środowiskach rozproszonych zapraszam na przedmiot systemy rozproszone.


Ostatnia modyfikacja: 16/03/2014