PWiR lab 11: Inne typy komunikacji w MPI i optymalizacje

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ą. Dodatkowo, zobaczymy, jak można optymalizować programy współbieżne.

Literatura

Pytania


Spis treści

  1. Pliki, z których będziemy korzystać
  2. Komunikacja grupowa
  3. Komunikacja nieblokująca
  4. Ćwiczenie 1: Metoda kolejnych nadrelaksacji i jej optymalizacje

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.
ring-nonblocking.c
Równoległy program demonstrujący nieblokującą komunikację w MPI.
laplace-seq.c
Sekwencyjny program implementujący metodę kolejnych nadrelaksacji dla równania Laplace'a.
laplace-par.c
Szablon równoległego programu implementującego metodę kolejnych nadrelaksacji dla równania Laplace'a. Ten plik będziemy edytować w ramach ćwiczenia 1.
laplace-common.h
Deklaracje pomocniczych funkcji do programów implementujących metodę nadrelaksacji. Ten plik nie będzie nas interesował.
laplace-common-impl.h
Definicje pomocniczych funkcji do programów implementujących metodę nadrelaksacji. Ten plik nie będzie nas interesował.

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 dla 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.

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.


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 1: Metoda kolejnych nadrelaksacji i jej optymalizacje

Równanie różniczkowe Laplace'a opisuje rozmaite ekwilibria w procesach fizycznych. Analiza takich procesów często sprowadza się do zdyskretyzowania równania wraz z warunkami brzegowymi i numerycznego rozwiązania powstałego układu równań. Jedną z używanych metod numerycznych jest metoda kolejnych nadrelaksacji. Program sekwencyjny realizujący wariant tej metody w dwóch wymiarach można znaleźć w pliku laplace-seq.c.

W programie tym zakładamy równanie Laplace'a w dwuwymiarowym układzie współrzędnych δ2(x, y)/δx2 + δ2(x, y)/δy2 = 0 wraz z pewnym warunkami brzegowymi. Równanie to jest zdyskretyzowane na siatce N x N równoodległych punktów.

Algorytm numeryczny przybliżający rozwiązanie równania działa następująco dla każdego punktu siatki (za wyjątkiem punktów brzegowych):

  1. policz średnią wartość sąsiednich punktów siatki (górnego, dolnego, prawego i lewego sąsiada);
  2. nadaj nową wartość punktowi równą średniej ze średniej policzonej w poprzednim punkcie oraz starej wartości punktu używając zadanego współczynnika relaksacji — omega — jako wagi;
  3. jeśli dla któregoś punktu różnica pomiędzy starą wartością a nową przekracza zadane epsilon to wykonaj kolejną iterację algorytmu.

Aby zapewnić, że wartości punktów, z których liczona jest średnia (pkt. 2 powyższego algorytmu) pochodzą z tej samej iteracji, użyty jest algorytm szachownicowy. Punkty pokolorowane są na biało i czarno, jak szachownica. Każda iteracja składa się z dwóch faz: w pierwszej liczone są nowe wartości punktów białych (na podstawie starych wartości punktów czarnych), w drugiej — nowe wartości punktów czarnych (na podstawie nowych wartości punktów białych).

Mając dany szablon programu równoległego, spraw, aby program ten poprawnie rozwiązywał równanie Laplace'a na więcej niż jednym procesorze wykorzystując MPI do komunikacji pomiędzy procesami.

Po poprawnym zaimplementowaniu algorytmu, policz speed-up'y dla różnej liczby punktów i liczby procesów. Jakie są Twoje obserwacje?

Zoptymalizuj program równoległy tak, aby osiągał on lepsze wyniki. Zbadaj co jest wąskim gardłem. Postaraj się skrócić ścieżkę krytyczną oraz zrównoleglić obliczenia z komunikacją. (Szablon programu równoległego celowo zawiera pewne nieefektywności, aby było duże pole do optymalizacji.)


Ostatnia modyfikacja: 17/05/2016