PWiR lab 09: Podział zadań i komunikacja w MPI

Na poprzednim laboratorium poznaliśmy podstawy MPI, w szczególności elementarne funkcje używane do komunikacji, i nauczyliśmy się uruchamiać programy korzystające z MPI. Na dzisiejszym laboratorium zajmniemy się przykładem ilustrującym, jak można podzielić obliczenia pomiędzy procesy i jak zorganizować komunikację pomiędzy tymi procesami, aby całość umożliwiała efektywne obliczenia nawet w przypadku, gdy dane wejściowe przekraczają pamięć pojedynczej maszyny.

Literatura

Pytania


Spis treści

  1. Wprowadzenie
  2. Pliki, z których będziemy korzystać
  3. Rozpraszanie obliczeń
  4. Ćwiczenie 1: Podział i wypisywanie danych
  5. Komunikacja przez rozgłaszanie
  6. Ćwiczenie 2: Implementacja algorytmu

Wprowadzenie

Algorytm Floyda-Warshalla oblicza najkrótsze ścieżki pomiędzy wszystkimi N2 parami wierzchołków w grafie ważonym o N wierzchołkach. Zakładając, że macierz M zawiera nieujemne i skończone bezpośrednie odległości między wierzchołkami (wagi), gdzie M[i, j] to odległość od wierzchołka o numerze i do wierzchołka o numerze j, algorytm wygląda następująco:

  for k := 0 to N-1 do
    for i := 0 to N-1 do
      for j := 0 to N-1 do
        if (M[i, j] > M[i, k] + M[k, j]) then
          M[i, j] := M[i, k] + M[k, j];

Na dzisiejszym laboratorium zaimplementujemy ten algorytm w MPI jako case study dla rozpraszania obliczeń i organizowania komunikacji.


Pliki, z których będziemy korzystać

W niniejszym scenariuszu będziemy korzystać z następujących plików (do pobrania tutaj):
Makefile
Plik Makefile.
graph-base.h
Definicja typu grafowego oraz deklaracje podstawowych funkcji na tym typie. Tego pliku nie będziemy modyfikować.
graph-base.c
Implementacja podstawowych funkcji na typie grafowym. Tego pliku nie będziemy modyfikować.
graph-utils.h
Deklaracje dodatkowych użytecznych funkcji na typie grafowym. Tego pliku raczej nie będziemy modyfikować.
graph-utils-seq.c
Implementacja dodatkowych użytecznych funkcji na typie grafowym zakładająca, że funkcje te używane są do obliczeń jednoprocesowych. Tego pliku nie będziemy modyfikować.
graph-utils-par.c
Szkielet implementacji dodatkowych użytecznych funkcji na typie grafowym zakładająca, że funkcje te używane są do obliczeń wieloprocesowych w MPI. Ten plik będziemy edytować w ramach ćwiczenia 1.
generator-seq.c
Program generujący i wypisujący graf, który zakłada, że będzie uruchamiany jednoprocesorowo. Tego pliku nie będziemy modyfikować.
generator-par.c
Program generujący graf, rozpraszający go pomiędzy procesy i zbierający jego części do wypisania, który zakłada, że może być uruchamiany wieloprocesorowo. Tego pliku nie będziemy modyfikować.
hello-world-bcast-par.c
Wieloprocesorowy program "Hello world" w MPI używający rozgłaszania. Tego pliku raczej nie będziemy modyfikować.
floyd-warshall-seq.c
Sekwencyjny program implementujący algorytm Floyda-Warshalla przy wykorzystaniu naszej implementacji grafu. Tego pliku nie będziemy modyfikować.
floyd-warshall-par.c
Szkielet równoległego programu implementującego algorytm Floyda-Warshalla przy wykorzystaniu naszej implementacji grafu. Ten plik będziemy edytować w ramach ćwiczenia 2.

Rozpraszanie obliczeń

Jednym z podstawowych problemów w obliczeniach rozproszonych jest podział pracy pomiędzy procesy. Problem ten staje się jeszcze bardziej interesujący, gdy dane wejściowe przekraczają pamięć dostępną dla pojedynczego procesu.

W przypadku algorytmu Floyda-Warshalla danymi wejściowymi jest macierz sąsiedztwa grafu zawierająca wagi poszczególnych krawędzi. Chcielibyśmy podzielić ją tak, aby umożliwić efektywne wykonanie rozproszonej wersji algorytmu. Jednocześnie chcemy zapewnić, że w żadnym momencie nie będzie konieczności, aby pojedynczy proces przechowywał całą macierz lub znaczną jej część w pamięci.

W tym celu macierz podzielimy wierszami na rozłączne bloki obejmujące sąsiednie wiersze tak, aby każdy proces przechowywał jeden blok. Co ważne, w celu zrównoważenia obciążenia pomiędzy procesami, chcemy, aby różnica w rozmiarze bloków przechowywanych przez dwa dowolne procesy nie przekraczała jednego wiersza.


Ćwiczenie 1: Podział i wypisywanie danych

Zaimplementuj funkcje dokonujące podziału macierzy i wypisujące rozproszoną macierz na standardowe wyjście. Dokładniej, w pliku graph-utils-par.c zaimplementuj funkcje oznaczone FIXME. Jednoprocesowa wersja tych funkcji (do podejrzenia jako wzór) znajduje się w pliku graph-utils-seq.c, zaś ich deklaracje w pliku nagłówkowym graph-utils.h. Pliki generator-seq.c i generator-par.c, kompilujące się odpowiednio do generator-seq.exe i generator-par.exe, zawierają natomiast testy, które pozwolą sprawdzić poprawność Twojego rozwiązania — uruchomienie generator-par.exe na dowolnej liczbie procesów powinno dać taki sam wynik, jak uruchomienie pojedynczego procesu generator-seq.exe, niezależnie od liczby wierzchołków w grafie (rozmiaru macierzy).

Zgodnie z wcześniejszymi założeniami, nie jest dozwolone, aby w jakimkolwiek momencie pojedynczy proces przechowywał całą macierz lub taką jej część, która istotnie przekracza rozmiar bloku tego procesu (oczywiście w przypadku, gdy liczba procesów jest większa niż jeden). Zauważ także, że, zgodnie z wersją sekwencyjną, generowanie kolejnych wierszy macierzy może wymagać pewnego stanu. Dlatego też powinno odbywać się w jednym procesie. Analogicznie, wypisywanie macierzy powinno być wykonywane przez pojedynczy proces, aby uniknąć problemów, które poruszaliśmy na poprzednim laboratorium.

Przed przejściem do kolejnych zadań upewnij się, że Twoje rozwiązanie faktycznie działa w różnych konfiguracjach liczby wierszy i procesów.


Komunikacja przez rozgłaszanie

Zgodnie z dotychczasowymi pomysłami, każdy proces posiada unikalny zbiór elementów macierzy sąsiedztwa. Jednocześnie, do obliczeń będzie potrzebował elementów posiadanych przez inne procesy. Procesy mogą więc chcieć rozgłosić w odpowiednim momencie część posiadanych przez siebie elementów macierzy do innych procesów.

Podstawowa komunikacja punkt do punktu pozwala budować dowolne wysokopoziomowe schematy komunikacji. Przykładowo, aby zaimplementować rozgłaszanie (zwane także komunikacją jeden do wszystich), rozgłaszający — proces i — może bezpośredio wysłać komunikat punkt do punktu do każdego odbierającego procesu j (j różne od i). Takie podejście jednak jest nieefektywne. Jeśli założymy, że koszty komunikacji pomiędzy dowolną parą procesów są stałe i takie same, najefektywniejsze rozgłaszanie działa w rundach wg następującego algorytmu:

W ten sposób rozgłaszanie będzie skończone w log2(N) rundach a nie w N - 1, jak w naiwnym algorytmie.

MPI udostępnia komunikację przez rozgłaszanie, co zademonstrujemy na przykładzie naszego prostego programu "Hello world" (plik hello-world-bcast-par.c). W nowej wersji programu, to proces 0 rozgłasza komunikat "Hello world from process 0!" do pozostałych procesów. Natomiast procesy te wypisują otrzymany komunikat na standardowe wyjście.

Rozgłaszanie odbywa się za pomocą funkcji MPI_Bcast (man MPI_Bcast):

  MPI_Bcast(
      <messagePtr>,
      <messageLen>,
      MPI_<TYPE>,
      <srcProcessNo>,
      MPI_COMM_WORLD
  );

Parametry przekazywane do tej funkcji są w większości takie same, jak w przypadku komunikacji punkt do punktu z następującymi wyjątkami. Po pierwsze, nie ma podziału na nadawców i odbiorców — wszyscy wywołują tą samą funkcję. To MPI decyduje, kto jest nadawcą a kto odbiorcą na podstawie czwartego parametru, który specyfikuje rangę nadawcy (<srcProcessNo> w naszym przypadku). Po drugie, nie ma znacznika komunikatu. To oznacza, że komunikaty rozgłoszeniowe są nierozróżnialne. Faktycznie, MPI_Bcast wymaga, aby wszystkie procesy w komunikatorze wywołały tę funkcję z tą samą rangą nadawcy i, co ważne, z taką samą długością (<messageLen>) i typem (MPI_<TYPE>) komunikatu. Semantyka MPI_Bcast dla nadawcy jest taka sama, jak MPI_Send, zaś dla odbiorcy taka sama, jak MPI_Recv.


Ćwiczenie 2: Implementacja algorytmu

Napisz równoległą implementację algorytmu Floyda-Warshalla korzystającą z MPI. Dokładniej, w pliku floyd-warshall-par.c napisz funkcję oznaczoną jako FIXME. Program sekwencyjny implementujący ten algorytm można znaleźć w pliku floyd-warshall-seq.c. Twoja implementacja, kompilująca się do floyd-warshall-par.exe, powinna, niezależnie od wielkości grafu i liczby procesów, dawać dokładnie takie same wyniki wyniki, jak wersja sekwencyjna, kompilująca się do floyd-warshall-seq.exe.

Policz speed-up'y uzyskiwane przez Twoją implementację dla różnych wielkości grafów, liczby procesów i mapowania tych procesów na fizyczne maszyny. Dla przypomnienia, speed-up algorytmu dla P procesów wynosi SP = T1 / TP, gdzie T1 to czas działania najlepszego algorytmu sekwencyjnego rozwiązującego ten sam problem (na nasze potrzeby — floyd-warshall-seq.exe) a TP to czas działania rozważanego algorytmu równoległego dla P procesów (czyli floyd-warshall-par.exe).

Zastanów się, jak poprawić osiągany speed-up (np. modyfikując instrukcje, sposób przechowywania macierzy, etc.).


Ostatnia modyfikacja: 04/05/2016