Wprowadzenie do MPI

Daniel Rychcik
muflon@mat.uni.torun.pl
26-11-2000

1. Wstęp

MPI (Message Passing Interface) to nazwa standardu biblioteki przesyłania komunikatów dla potrzeb programowania równoległego. Pod skrótem MPI kryje się tylko formalna specyfikacja interfejsu, nie jest to nazwa żadnego konkretnego pakietu oprogramowania. Aktualnie obowiązująca wersja standardu MPI to 1.2, obecnie na ukończeniu są jednak prace nad wersją 2.0.

Najbardziej znaną implementacją MPI jest MPICH, pochodzący z Argonne National Laboratory i rozwijany przez grupę pracowników działu matematyki i informatyki tej instytucji. Dostępna jest wersja zarówno na platformy UNIX-owe, jak i Windows NT/2000.

2. Instalacja

a) Systemy UNIX-owe

Instalacja mpich jest typowa dla pakietów UNIX-owych i przebiega następująco:

  • Ściągamy plik z kodem źródłowym - np. ftp://ftp.mcs.anl.gov/pub/mpi/mpich.tar.gz
  • Rozpakowujemy go w katalogu pomocniczym:
    # gzip -d -c mpich-1.2.1.tar.gz | tar xvf -
    # cd mpich-1.2.1
    
  • Konfigurujemy, kompilujemy, instalujemy:
    # ./configure --prefix=/usr/local/mpi
    # make
    # make install
    
  • Ważnym elementem pakietu jest plik $PREFIX/share/machines.$ARCH Zawiera on informacje o maszynach na których możemy wykonywać swoje zadania oraz ilości procesorów na każdej z nich. W najprostszym przypadku jest to jedyny plik, który należy poprawić, aby rozpocząć pracę.
  • Program do dystrybucji zadań na inne maszyny domyślnie używa polecenia rsh W mojej próbnej instalacji było to ssh, gdyż łatwiej jest przy użyciu tego protokołu uzyskać zdalne uruchomienie programu bez podawania hasła
  • (Można również użyć dostarczonego serwera $PREFIX/sbin/chp4_servs)

b) Windows NT/2000

Instalacja mpich w wersji Windowsowej jest prostsza i polega na:


3. Ogólna koncepcja systemu

a) Podstawy

MPI realizuje model przetwarzania współbieżnego zwany MIMD ( Multiple Instruction Multiple Data), a dokładniej SPMD (Single Program Multiple Data). Zakłada on, że ten sam kod źródłowy wykonuje się jednocześnie na kilku maszynach i procesy mogą przetwarzać równocześnie różne fragmenty danych, wymieniając informacje przy użyciu komunikatów

Takie podejście ma wiele zalet, z których najbardziej spektakularną jest chyba możliwość współbieżnych obliczeń wykonywanych na maszynach o zupełnie różnych architekturach (np. Linux-x86 oraz Solaris-Sparc). Zaletą (chyba:) jest również rezygnacja z koncepcji pamięci dzielonej i wynikające z tego ogólne uproszczenie programowania.

Realizując ten model, MPI umożliwia:

  • Wymianę komunikatów między procesami
    (Główny nacisk jest położony na wymianę danych, ale możliwe jest również wysyłanie komunikatów kontrolnych, czy synchronizacja procesów)
  • Uzyskiwanie informacji o środowisku
    (Typowy przykład to ilość aktywnych proces[-ów/-orów], czy numer aktualnego procesu)
  • Kontrolę nad systemem
    (Inicjalizacja/kończenie programu, kontrola poprawności przesyłanych komunikatów itp.)

Wszystkie te rzeczy są realizowane przy minimalnym stopniu skomplikowania kodu źródłowego - nie mając pojęcia o MPI i znając podstawy programowania równoległego byłem w stanie zrozumieć programy przykładowe i próbować pisać własne.

b) Komunikaty

Przy przesyłaniu komunikatów między procesami MPI stara się zachować niezależność od platformy (np. kolejności bajtów). Dla standardowych typów jest to proste, natomiast dla typów niestandardowych MPI dostarcza funkcje pozwalające na zdefiniowanie typów użytkownika dla potrzeb przesyłania komunikatów.

Możliwe jest adresowanie komunikatów zarówno do konkretnych procesów, jak i do określonych grup odbiorców. Dostępne są funkcje do definiowania grup procesów i późniejszego rozsyłania komunikatów do tych grup. Komunikaty opatrzone są tagami pozwalającymi na późniejsze selektywne odbieranie ich z kolejki w zależności od rodzaju.

Możliwa jest wymiana komunikatów w trybie non-blocking pozwalającym na jeszcze większe zrównoleglenie obliczeń

c) Zaawansowana komunikacja

Główną zaletą MPI przy bardziej złożonych schematach wymiany danych jest ukrywanie przed programistą szczegółów implementacyjnych oraz możliwość optymalizacji ścieżki przepływu danych. Wyobraźmy sobie przykładowo, że mamy 8 procesów i pierwszy z nich ma przekazać pewną porcję danych wszystkim pozostałym. Najprostsza możliwość:


jest oczywiście niezbyt optymalna - widać, że w ogólnym przypadku złożoność czasowa procesu przesyłania jest zależna liniowo od liczby procesów biorących udział w tej operacji.


Kolejny przykład jest już krokiem w dobrym kierunku - przesyłanie danych trwa dwukrotnie krócej niż poprzednio

Oczywiście i tak każdy wie, że optymalny schemat broadcastu danych w obrębie grupy procesów wygląda następująco :)


jednak zaletą MPI jest fakt, że takie decyzje (dotyczące wyboru dróg przesyłania danych) są ukryte przed programistą - może on po prostu założyć, że dane "kiedyś" i "jakoś" dotrą na miejsce przeznaczenia, a "kiedy" i "jak" - decyduje system i stara się to zrobić w sposób optymalny.

Wszystkie te rozważania można również przeprowadzić przeprowadzić w odwrotną stronę - dla agregacji danych z kilku procesorów. MPI definiuje zbiór funkcji służących do zbierania danych z kilku procesów. Typowy przykład to obliczanie sumy elementów wektora na podstawie zbioru sum częściowych (lub np. wyszukiwanie maksymalnego elementu).

4. Środowisko pracy

Z punktu widzenia programisty MPI składa się z dwóch części:

  • Biblioteki (do języka C lub Fortranu) zawierającej niezbędne funkcje i pliki nagłówkowe
  • Środowiska uruchamiania (runtime)
    (Można tu zauważyć pewną użytkową analogię np. z Javą - program mpirun jest tu odpowiednikiem polecenia java)

Najprostszy sposób kompilacji programu to użycie któregoś ze standardowych skryptów dostarczanych z MPI. Są to:

  • mpicc - dla programów w C
  • mpiCC - dla programów w C++
  • mpif77 - dla programów w Fortranie 77
  • mpif90 - dla programów w Fortranie 90

Poza opcjami specyficznymi dla MPI pozwalają one na przekazywanie standardowych opcji dla poszczególnych procesów kompilacji. Używanie tych skryptów jest zalecane, gdyż zwalnia nas z obowiązku ustawiania dużej liczby zmiennych systemowych zawierających parametry MPI - jedyne o czym musimy pamiętać to ustawienie zmiennej PATH.

Uruchamianie programów w MPI jest równie proste i sprowadza się do uruchomienia skryptu mpirun. Jego jedynym interesującym nas w tej chwili parametrem jest -np <n>. Określa on ilość równoległych procesów, jakie ma uruchomić system w celu wykonania obliczeń.

Przed uruchomieniem mpirun musimy upewnić się, że w naszym katalogu domowym na wszystkich maszynach potencjalnie mogących wchodzić w grę przy wykonywaniu naszego programu w odpowiednim katalogu znajduje się kopia pliku wykonywalnego programu. Najczęściej w takich przypadkach stosuje się montowanie katalogów przez NFS.

Szczegóły techniczne ukryte za mechanizmem rozsyłania procesów na różne maszyny w zasadzie nas tu nie interesują, warto może tylko wspomnieć o ADI (Abstract Device Interface). Jest to wewnętrzny standard mpich określający sposób komunikacji między procesami na niskim poziomie.

Zaletą używania takiego modelu jest oddzielenie części systemu zależnej od konkretnej architektury, od części identycznej dla wszystkich. Wynika z tego na przykład, że ten sam program uruchomiony na maszynie wieloprocesorowej może używać do komunikacji pamięci wspólnej (shared memory), a wykonywany na klastrze kilku silnych pecetów używa TCP/IP bez rekompilacji kodu!

5. Ważniejsze funkcje MPI

a) Podstawy

Podstawowe funkcje służące do inicjalizacji/zamykania programu oraz do wysyłania najprostszych komunikatów (typu Point-to-point) to:

  • int MPI_Init(int *argc, char ***argv);

    Funkcja inicjalizuje środowisko wykonywania programu, m.in. tworzy domyślny komunikator MPI_COMM_WORLD. Dopiero od momentu wywołania MPI_Init można używać pozostałych funkcji MPI.

  • int MPI_Finalize();

    Funkcja zwalnia zasoby używane przez MPI i przygotowuje system do zamknięcia.

  • int MPI_Comm_rank(MPI_Comm comm, int *rank);

    Funkcja pobiera numer aktualnego procesu (w obrębie komunikatora comm) i umieszcza go w zmiennej rank.

  • int MPI_Comm_size(MPI_Comm comm, int *size);

    Funkcja pobiera ilość procesów (w obrębie komunikatora comm i umieszcza ją w zmiennej size.

  • int MPI_Send(void *msg, int count, MPI_Datatype datatype,
                 int dest, int tag, MPI_Comm comm);

    Funkcja wysyła komunikat typu datatypedo procesu numer dest oznaczony znacznikiem tag w obrębie komunikatora comm.

    Typ komunikatu jest zawarty w zmiennej datatype i może to być któryś z predefiniowanych typów takich jak MPI_INT, MPI_FLOAT, MPI_DOUBLE, MPI_CHAR, lub inne (zob. instrukcja), jak i typy zdefiniowane przez użytkownika (o tym jeszcze będzie).

    Tag jest liczbą w zakresie [0..MPI_TAG_UB] i określa dodatkowy typ komunikatu wykorzystywany przy selektywnym odbiorze funkcją MPI_Recv.

  • int MPI_Recv(void *msg, int count, MPI_Datatype datatype,
                 int source, int tag, MPI_Comm comm,
                 MPI_Status *status);

    Funkcja odczytuje z kolejki komunikatora comm (z ewentualnym blokowaniem do czasu nadejścia) pierwszy komunikat od procesu source oznaczony znacznikiem tag typu datatype. Wynik umieszczany jest w buforze msg a status operacji w zmiennej status.

    Jeżeli proces ustawi source==MPI_ANY_SOURCE to odczytany będzie pierwszy komunikat od dowolnego procesu. Podobnie, dla tag==MPI_ANY_TAG nie będzie sprawdzany znacznik typu wiadomości.

    Bufor stanu status musi zostać uprzednio stworzony przez programistę. Dla C jego pojedynczy element składa się z trzech liczb całkowitych: MPI_SOURCE, MPI_TAG oraz MPI_STATUS. W ogólnym przypadku bowiem (przy odbieraniu komunikatów w których count>1) tablica status określa nam źródło i typ każdego komunikatu z osobna. Do pobierania ilości odebranych komunikatów na podstawie zmiennej stanu służy kolejna funkcja:

  • int MPI_Get_count(MPI_Status *status, MPI_Datatype datatype, int *count);

    która umieszcza szukaną ilość w zmiennej count.

b) Rozsyłanie/zbieranie danych

  • int MPI_Bcast(void *msg, int count,
                  MPI_Datatype datatype, int root, MPI_Comm comm);

    Funkcja rozsyła komunikat do wszystkich procesów w obrębie komunikatora comm poczynając od procesu root. Pozostałe argumenty - podobnie jak w MPI_Send().

  • int MPI_Reduce(void *operand, void *result,
                   int count, MPI_Datatype datatype, MPI_Op op,
                   int root, MPI_Comm comm);

    Bardzo ważna funkcja - pozwala wykonać na przykład sumowanie wszystkich częściowych wyników otrzymanych w procesach i umieszczenie wyniku w zmiennej. Argument root wskazuje dla którego procesu wynik ma być umieszczony w zmiennej result. Oto przykład użycia tej funkcji:

      MPI_Reduce(&suma_cz,&suma,1,MPI_FLOAT,MPI_SUM,0,MPI_COMM_WORLD);

    Przykładowe operatory to MPI_MAX, MPI_MIN, MPI_SUM - kompletna lista znajduje się w dokumentacji. Istnieje również możliwość definiowania własnych operatorów dla funkcji MPI_Reduce().

  • int MPI_Allreduce(void *operand, void *result,
                      int count, MPI_Datatype datatype, MPI_Op op,
                      MPI_Comm comm);

    Funkcja identyczna z poprzednią, różniąca się jedynie tym, że po jej wykonaniu wynik agregacji z użyciem operatora op znajduje się w zmiennej result we wszystkich procesach.

  • int MPI_Scatter(void *send_buf, int send_count,
                    MPI_Datatype send_type, void *recv_buf,
                    int recv_count, MPI_Datatype recv_type,
                    int root, MPI_Comm comm);

    Funkcja rozproszenia ("scatter") danych między procesami. Działa w ten sposób, że proces root rozsyła zawartość send_buff między wszystkie procesy. Jest ona dzielona na p segmentów, każdy składający się z send_count elementów. Pierwszy segment trafia do procesu 0, drugi do procesu 1 itp. Oczywiście argumenty, których nazwy zaczynają się na Send mają znaczenie tylko dla procesu który jest nadawcą.

  • int MPI_Gather(void *send_buf, int send_count,
                   MPI_Datatype send_type, void *recv_buf,
                   int recv_count, MPI_Datatype recv_type,
                   int root, MPI_Comm comm);
  • Każdy proces w grupie comm wysyła zawartość send_buff do procesu root. Ten proces układa przysłane dane w recv_buff w kolejności numerów procesów.

  • int MPI_Allgather(void *send_buf, int send_count,
                      MPI_Datatype send_type, void *recv_buf,
                      int recv_count, MPI_Datatype recv_type,
                      MPI_Comm comm);
  • Funkcja ta jest analogiczna do MPI_Gather z tą różnicą, że wynik jest umieszczany w recv_buff każdego procesu. Można tą funkcję traktować jako ciąg kolejnych wywołań MPI_Gather każdorazowo z innym numerem procesu root

    Zależność między trzema ostatnimi funkcjami ilustruje następujący rysunek:

c) Struktury danych

W zastosowaniach rzeczywistych najczęściej wysyłamy większą ilość danych, bądź to w strukturach, albo w postaci wektorów. Przesyłanie ich w postaci oddzielnych komunikatów powodowałoby powstanie dużego narzutu czasowego związanego z organizacją przesyłania danych. Potrzebna jest więc możliwość "pakowania" danych i wysyłania większej ich ilości za jednym razem.

W MPI jest to dość spory problem, gdyż najczęściej typ przesyłanych danych jest różny od standardowych typów zdefiniowanych w bibliotece. Zdecydowano się na rozwiązanie w którym programista ma możliwość tworzenia nowych typów danych w czasie wykonywania programu. Oto, zaczerpnięty z [1] przykład rozjaśniający nieco tą ideę:

#include "mpi.h"

...

/* Definiujemy typ bazowy */
typedef struct {
    float a;
    float b;
    int n;
} INDATA_TYPE;

...

MPI_Datatype   *message_type_ptr;  /* Wskaźnik do struktury typu */
int            block_lengths[3];   /* Długości bloków (tu równe 1) */
MPI_Aint       displacements[3];   /* Długości pól */
MPI_Aint       addresses[4];       /* Adresy pól */
MPI_Datatype   typelist[3];        /* Typy pojedynczych pól */

INDATA_TYPE    indata;

...

  typelist[0]=MPI_FLOAT;       /* Wypełniamy listę typów */
  typelist[1]=MPI_FLOAT;
  typelist[2]=MPI_INT;
  /* W tym przykładzie wszystkie pola są pojedyncze */
  block_lengths[0]=block_lengths[1]=block_lengths[2]=1;
  /* Ustalamy adresy pól */
  MPI_Address(indata, &adresses[0]);
  MPI_Address(&(indata->a), &adresses[1]);
  MPI_Address(&(indata->b), &adresses[2]);
  MPI_Address(&(indata->n), &adresses[3]);
  /* Obliczamy przesunięcia */ 
  displacements[0]=addresses[1]-addresses[0];
  displacements[1]=addresses[2]-addresses[1];
  displacements[2]=addresses[3]-addresses[2];
	
  /* Tworzymy typ i rejestrujemy go w MPI */
  MPI_Type_struct(3, block_lengths, displacements,
                  typelist, message_type_ptr);
  MPI_Type_commit(message_type_ptr);

...

  /* Nowego typu możemy używać tak jak innych wbudowanych */
  MPI_Bcast(indata, count, *message_type_ptr,
            root, MPI_COMM_WORLD);
...

Użyta w przykładzie funkcja MPI_Type_struct jest jednym z kilku konstruktorów typów. Pozostałe są podane poniżej.

  • int MPI_Address(void *data, MPI_Aint *address);

    Funkcja zapisuje adres zmiennej data do zmiennej address.

  • int MPI_Type_struct(int count,
                        int *array_of_block_lengths,
                        MPI_Aint *array_of_displacements,
                        MPI_Datatype *array_of_types,
                        MPI_Datatype *newtype);

    Funkcja tworzy nowy typ - strukturę o count składowych. Ilość elementów każdej składowej jest zapisana w tablicy array_of_block_lengths. Długości składowych (a dokładniej - offsety względem początku rekordu) należy umieścić w tablicy array_of_displacements. Typy pojedynczych danych składowych umieszczamy w array_of_types. Wynikowy typ MPI jest umieszczany w strukturze wskazywanej przez newtype.

  • int MPI_Type_vector(int count, int block_length,
                        int stride, MPI_Datatype element_type,
                        MPI_Datatype *newtype);

    Funkcja tworzy nowy typ - wektor długości count. Każdy element wektora zawiera block_length elementów typu element_type, elementy wektora są zaś rozdzielone dodatkowo stride elementami tego typu.

  • int MPI_Type_contiguous(int count, MPI_Datatype oldtype,
                            MPI_Datatype *newtype);

    Tworzenie prostszego (jednowymiarowego) wektora elementów typu oldtype długości count.

  • int MPI_Type_indexed(int count,
                         int *array_of_block_lengths,
                         int *array_of_displacements,
                         MPI_Datatype element_type,
                         MPI_Datatype *newtype);

    Jest to prostsza wersja funkcji MPI_Type_struct. Różni się tym, że wszystkie elementy struktury mają ten sam typ bazowy (element_type) i mogą być przesunięte względem początku o zadaną (tablica array_of_displacements) ilość elementów.

  • int MPI_Type_commit(MPI_Datatype *newtype);

    Tą funkcję należy wywołać po zdefiniowaniu nowego typu przy użyciu którejś z poprzednich funkcji - dopiero wtedy zacznie on być widziany przez MPI.

d) Grupowanie danych

Istnieje również prostsza metoda wysyłania większej ilości danych za jednym zamachem - polega ona na tym, że przed każdą operacją typu MPI_Send() ręcznie pakujemy wszystkie dane do jednego obszaru pamięci, a druga strona po odebraniu komunikatu może je rozpakować w analogiczny sposób. Odpowiednie funkcje mają następującą postać:

  • int MPI_Pack(void *pack_data, int in_count,
                 MPI_Datatype datatype, void *buffer,
                 int size, int *position_ptr, MPI_Comm comm);

    Parametr pack_data powinien zawierać in_count elementów typu datatype. Parametr position_ptr zawiera adres początkowy w buforze wyjściowym buffer i po wykonaniu funkcji jego wartość jest uaktualniana (dodawana jest długość zapisanych danych). Parametr size określa rozmiar obszaru pamięci wskazywanej przez buffer.

  • int MPI_Unpack(void *buffer, int size,
                   int *position_ptr, void *unpack_data, int count,
                   MPI_Datatype datatype, MPI_Comm comm);

    Funkcja odwrotna do poprzedniej - rozpakowuje count elementów typu datatype ze zmiennej buffer począwszy od pozycji position do obszaru pamięci wskazywanego przez unpack_data i odpowiednio uaktualnia zmienną position.

Oto przykład wykorzystania funkcji opisanych w tym punkcie:


 float a,b;
 int l;
 char buffer[100];
 int position;
 
 ...
 /* Proces wysyłający dane: */ 
 
   position=0;
   MPI_Pack(&a, 1, MPI_FLOAT, buffer, 100, &position, MPI_COMM_WORLD);
   MPI_Pack(&b, 1, MPI_FLOAT, buffer, 100, &position, MPI_COMM_WORLD);
   MPI_Pack(&l, 1, MPI_INT, buffer, 100, &position, MPI_COMM_WORLD);
   MPI_Bcast(buffer, 100, MPI_PACKED, root, MPI_COMM_WORLD);
 
 ...
 
 /* Proces odbierający dane: */ 
 	
   MPI_Bcast(buffer, 100, MPI_PACKED, root, MPI_COMM_WORLD);
   position=0;
   MPI_Unpack(buffer, 100, &position, &a, 1, MPI_FLOAT, MPI_COMM_WORLD);
   MPI_Unpack(buffer, 100, &position, &b, 1, MPI_FLOAT, MPI_COMM_WORLD);
   MPI_Unpack(buffer, 100, &position, &l, 1, MPI_INT, MPI_COMM_WORLD);

e) Synchronizacja

Podstawową (i najczęściej stosowaną) funkcją MPI służącą do synchronizacji procesów jest

  • int MPI_Barrier(MPI_Comm comm);

Wywołanie jej w pewnym miejscu programu powoduje, będzie on czekał, aż wszystkie pozostałe jego instancje dojdą do tego miejsca i dopiero potem ruszy dalej.

6. Pierwszy program

Najprostszy przykład użycia funkcji MPI to odpowiednik klasycznego programu "Hello World. Demonstruje on korzystanie z funkcji MPI_Send i MPI_Recv do najprostszej wymiany komunikatów.

   #include <stdio.h>
   #include "mpi.h"

   main(int argc, char **argv)
   {
     int my_rank;
     int p;
     int source;
     int dest;
     int tag=50;
     char message[100];
     MPI_Status status;

     MPI_Init(&argc, &argv);
     MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
     MPI_Comm_size(MPI_COMM_WORLD, &p);
   
     if (my_rank != 0)
     {
       sprintf(message, "Hello from process %d.", my_rank);
       dest = 0;
       MPI_Send(message, strlen(message)+1, MPI_CHAR, dest,
                tag, MPI_COMM_WORLD);
     }
     else
       for (source=1; source<p; source++)
       {
         MPI_Recv(message, 100, MPI_CHAR, source, tag,
                  MPI_COMM_WORLD, &status);
         printf("%s\n", message);
       }

     MPI_Finalize();
   }

Program ten działa inaczej, jeśli jest wykonywany przez proces numer 0, a inaczej w przeciwnym wypadku. Dla procesu 0 czekamy na (n-1) komunikatów i wyświetlamy ich treść na ekranie. Pozostałe procesy wysyłają po jednym komunikacie do procesu 0. Wynik działania programu jest zależny od ilości procesów zadeklarowanych przy uruchomieniu. Oto przykładowe wyniki:

   # ./mpirun -np 2 hello
   Hello from process 1.
   # ./mpirun -np 5 hello
   Hello from process 1.
   Hello from process 2.
   Hello from process 3.
   Hello from process 4.

7. Prosty program obliczeniowy (obliczanie liczby pi)

Przedstawię tu jeszcze jeden przykład, zaczerpnięty z dokumentacji MPICH i nieco uproszczony. Oblicza on liczbę korzystając ze wzoru:

Z technicznego punktu widzenia, całka jest liczona metodą trapezów, przy czym cały przedział całkowania jest rozdzielony równo między procesory, z których każdy jest odpowiedzialny za "swoją" część całki. Po skończonych obliczeniach częściowych wyniki są agregowane przy pomocy funkcji MPI_Reduce. Oto kod źródłowy programu:

   #include "mpi.h"
   #include <stdio.h>
   #include <math.h>

   void main(int argc, char *argv[])
   {
     int     myid,numprocs,i,n; 
     double  mypi,pi,h,sum,x;
	
     MPI_Init(&argc,&argv);              /* Pobierz informacje o systemie */
     MPI_Comm_size(MPI_COMM_WORLD,&numprocs);
     MPI_Comm_rank(MPI_COMM_WORLD,&myid);
	
     if (myid==0)    /* Wczytaj liczbę przedziałów (tylko proces numer 0) */
     {
       printf("Enter the number of intervals: ");
       scanf("%d",&n);
     }
     /* Wyślij liczbę przedziałów do pozostałych procesów w systemie */
     MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);
     h=1.0/(double)n;                           /* Oblicz krok całkowania */

     /* Licz całkę w zadanym przedziale z krokiem h (reguła trapezów) */
     for (sum=0.0, i=myid+1; i<=n; i+=numprocs)
     {
       x=h*((double)i-0.5);
       sum+=4.0/(1.0+x*x);
     }
     mypi=h*sum;              /* Wspólne podwyrażenie - na zewnątrz pętli */

     /* Oblicz sumę po wszystkiech procesach */
     MPI_Reduce(&mypi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
			
     if (myid == 0) printf("pi=%.16f\n",pi);   /* Wydruk - tylko proces 0 */
     MPI_Finalize();
   }

8. Zaawansowane możliwości

W tym punkcie opiszę jeszcze pokrótce kilka zaawansowanych możliwości MPICH-a, wykraczających poza ramy tego dokumentu, ale również wartych uwagi.

8.1. Biblioteka MPE

MPE to opracowana przez twórców MPICH biblioteka funkcji pomocniczych przydatnych przy pisaniu programów z użyciem MPI. Najważniejsze grupy zawartych w niej funkcji to:

  • Obsługa ekranu graficznego (bardzo wysokiego poziomu - w identyczny sposób programujemy pod X-Windows jak i w Windows NT).
  • Narzędzia do wizualizacji przepływu danych w obrębie programu - upshot, nupshot, Jumpshot. Przydają się przy optymalizacji kodu.
  • Funkcje wspomagające debugowanie programu przy pomocy mpigdb.

8.2 Topologie

Pojęcie topologii jest uogólnieniem komunikatora. MPI pozwala na zdefiniowanie dwóch podstawowych struktur procesów w obrębie jednego komunikatora. Pierwsza z nich to topologia kartezjańska - procesy ułożone są w macierz (niekoniecznie dwuwymiarową!) i mogą oprócz wszystkich standardowych operacji, dodatkowo uzyskiwać informacje o swoim położeniu w macierzy, sąsiednich procesach itp.

Druga, bardziej ogólna topologia - topologia grafu - działa na podobnej zasadzie, ale dla struktury grafu. Ważną cechą implementacyjną topologii jest fakt, że szczegóły techniczne numeracji procesów są ukryte przed programistą. Możliwa jest na przykład sytuacja, gdzie 9 procesów ułożonych w siatkę 3x3 w rzeczywistości ułożone jest następująco:

8.3 Komunikacja non-blocking

MPI umożliwia, oprócz klasycznej komunikacji typu MPI_Send/ MPI_Recv również przesyłanie wiadomości bez blokowania programu. W skrócie wygląda to tak, że po wywołaniu funkcji MPI_Isend program może wykonywać inne czynności (oczywiście nie zmieniające bufora wysyłanego komunikatu), natomiast w chwili, gdy chce się upewnić, że komunikat został już wysłany w całości, wywołuje MPI_Wait, lub MPI_Test.

W podobny sposób realizowany jest odbiór komunikatów bez blokowania - analogiczna funkcja nosi nazwę MPI_Irecv.

9. Zasoby sieciowe

10. Literatura

  1. Peter S. Pacheco - "A User's Guide to MPI", University of San Francisco, 1998.
  2. MPI Forum - "MPI: A Message-Passing Interface Standard", Message Passing Interface Forum, 1995.
  3. Praca zbiorowa - "MPI: The Complete Reference", The MIT Press, Cambridge, Massachusetts, 1996.
  4. Wiliam Gropp, Ewing Lusk - "User's Guide for mpich, a portable Implementatiom of MPI,
    Argonne National Labs, 1996.