Wprowadzenie do MPI
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:
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
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
- Peter S. Pacheco - "A User's Guide to MPI",
University of San Francisco, 1998.
- MPI Forum - "MPI: A Message-Passing Interface Standard",
Message Passing Interface Forum, 1995.
- Praca zbiorowa - "MPI: The Complete Reference",
The MIT Press, Cambridge, Massachusetts, 1996.
- Wiliam Gropp, Ewing Lusk - "User's Guide for mpich, a portable
Implementatiom of MPI,
Argonne National Labs, 1996.
|