Message Passing Interface
Message Passing Interface (MPI) ist ein Standard, der den Nachrichtenaustausch bei parallelen Berechnungen auf verteilten Computersystemen beschreibt. Er legt dabei eine Sammlung von Operationen und ihre Semantik, also eine Programmierschnittstelle fest, aber keine Implementierung. Eine MPI-Applikation besteht in der Regel aus mehreren miteinander kommunizierenden Prozessen, die alle zu Beginn der Programmausführung parallel gestartet werden. Alle diese Prozesse arbeiten dann gemeinsam an einem Problem und nutzen zum Datenaustausch Nachrichten, welche explizit von einem zum anderen Prozess geschickt werden. Ein Vorteil dieses Prinzips ist es, dass der Nachrichtenaustausch auch über Rechnergrenzen hinweg funktioniert. Parallele MPI-Programme sind somit sowohl auf PC-Clustern (hier funktioniert der Austausch der Nachrichten z. B. über TCP), als auch auf dedizierten Parallelrechnern ausführbar (hier läuft der Nachrichtenaustausch über ein Hochgeschwindigkeitsnetz wie InfiniBand oder Myrinet oder über den gemeinsamen Hauptspeicher). Geschichte1992 begann die Entwicklung des MPI 1.0 Standards mit Entwürfen (November 1992, Februar 1993, November 1993). Ausgangspunkt waren ältere Kommunikationsbibliotheken wie PVM, PARMACS, P4, Chameleon und Zipcode. Der Standard erschien am 5. Mai 1994 mit
Im Juni 1995 erfolgten Fehlerkorrekturen mit MPI 1.1. Am 18. Juli 1997 wurde die stabile Version MPI 1.2 veröffentlicht, die neben weiteren Fehlerkorrekturen eine Versionidentifikation erlaubt. Sie wird auch als MPI-1 bezeichnet. Am 30. Mai 2008 erschien MPI 1.3 mit weiteren Fehlerkorrekturen und Klarstellungen. Zeitgleich zur Version 1.2 wurde am 18. Juli 1997 auch der MPI 2.0 Standard verabschiedet. Dieser wird auch als MPI-2 bezeichnet und enthält unter anderem folgende Erweiterungen:
Am 23. Juni 2008 wurden die bisher separaten Teile MPI-1 und MPI-2 zu einem gemeinsamen Dokument vereint und als MPI 2.1 veröffentlicht. MPI Standard Version 2.2 ist vom 4. September 2009 und enthält weitere Verbesserungen und kleinere Erweiterungen. Am 21. September 2012 hat das MPI Forum MPI-3 veröffentlicht,[1] das neue Funktionalität einarbeitet wie beispielsweise nicht blockierende Kollektive, ein verbessertes einseitiges Kommunikationsmodell (RMA, Remote Memory Access), ein neues Fortran-Interface, topographiebezogene Kommunikation und nicht blockierende parallele Ein- und Ausgabe.[2] Am 9. Juni 2021 wurde MPI-4 veröffentlicht. Wesentliche Neuerungen sind Funktionsinterfaces, die Parameter mit einem größeren Wertebereich unterstützen. Zuvor waren durch den vorgegebenen 32-bit Datentyp wesentliche Größen, wie z. B. die Anzahl zu kommunizierender Datenelemente, auf etwas mehr als zwei Milliarden beschränkt. Persistente Kollektive eröffnen Möglichkeiten zur Optimierung wiederholt ausgeführter Kommunikation und sind zudem im Gegensatz zu den schon vorhandenen kollektiven Operationen in beliebiger Reihenfolge ausführbar. Zudem wurde die Fehlerbehandlung in vielen Punkten verbessert und ein neues Session-Modell zur dynamischen Nutzung der von MPI verwalteten Ressourcen eingeführt.[3] Zu den Hauptentwicklern gehört Bill Gropp. Punkt-zu-Punkt KommunikationDie grundlegendste Art der Kommunikation findet zwischen zwei Prozessen statt: ein Sendeprozess übermittelt dabei Informationen an einen Empfangsprozess. In MPI werden diese Informationen in sogenannte Nachrichten verpackt, die mit den Parametern Blockierendes Senden und EmpfangenDie einfachsten Operationen für eine Punkt-zu-Punkt Kommunikation sind senden und empfangen: int MPI_Send (void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)
int MPI_Recv (void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status* status)
Die beiden Operationen sind blockierend und asynchron. Das bedeutet:
Analog gilt:
ProgrammbeispielDie Verwendung von #include "mpi.h"
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int myrank, message_size=50, tag=42;
char message[message_size];
MPI_Status status;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myrank);
if (myrank == 0) {
MPI_Recv(message, message_size, MPI_CHAR, 1, tag, MPI_COMM_WORLD, &status);
printf("received \"%s\"\n", message);
}
else {
strcpy(message, "Hello, there");
MPI_Send(message, strlen(message)+1, MPI_CHAR, 0, tag, MPI_COMM_WORLD);
}
MPI_Finalize();
return 0;
}
Nichtblockierende KommunikationDie Effizienz einer parallelen Anwendung kann oftmals gesteigert werden, indem man Kommunikation mit Berechnung überlappt und/oder synchronisationsbedingte Wartezeiten vermeidet. Dazu definiert der MPI-Standard sogenannte nichtblockierende Kommunikation, bei der die Kommunikationsoperation lediglich angestoßen wird. Eine separate Funktion muss dann aufgerufen werden, um solch eine Operation zu beenden. Im Unterschied zur blockierenden Variante wird beim Starten der Operation ein int MPI_Isend (void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request* request)
int MPI_Irecv (void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request* request)
Fortschritt abfragenUm den Fortschritt einer dieser Operationen zu erfahren, wird folgende Operation verwendet: int MPI_Test (MPI_Request* request, int* flag, MPI_Status* status)
Wobei Blockierend wartenUm dennoch blockierend auf eine int MPI_Wait (MPI_Request* request, MPI_Status* status)
Synchronisierendes SendenFür die Sendeoperationen werden auch die synchronen Varianten Puffernde Varianten… Gruppen und KommunikatorenProzesse lassen sich in Gruppen zusammenfassen, wobei jedem Prozess eine eindeutige Nummer, der sogenannte Rang zugeordnet wird. Für den Zugriff auf eine Gruppe wird ein Kommunikator benötigt. Soll also eine globale Kommunikationsoperation auf eine Gruppe beschränkt werden, so muss der zur Gruppe gehörende Kommunikator angegeben werden. Der Kommunikator für die Menge aller Prozesse heißt Die zum Kommunikator int MPI_Comm_group (MPI_Comm comm, MPI_Group* group)
Für Prozessgruppen stehen die üblichen Mengenoperationen zur Verfügung. VereinigungZwei Gruppen
Die Prozesse aus SchnittmengeDie Schnittmenge zweier Gruppen erhält man mit
DifferenzDie Differenz zweier Gruppen erhält man mit
Globale KommunikationIn parallelen Anwendungen trifft man häufig spezielle Kommunikationsmuster an, bei denen mehrere oder gar alle MPI-Prozesse gleichzeitig beteiligt sind. Der MPI-Standard hat deswegen für die wichtigsten Muster eigene Operationen definiert. Diese unterscheidet man grob in drei Arten: Synchronisation (Barrier), Kommunikation (z.Bsp. Broadcast, Gather, Alltoall) und Kommunikation gekoppelt mit Berechnung (z. B. Reduce oder Scan). Manche dieser Operationen verwenden einen ausgewählten MPI-Prozess, der eine Sonderrolle einnimmt und der typischerweise mit Broadcast (ausstrahlen)Mit der Broadcast-Operation schickt ein ausgewählter MPI-Prozess
Der MPI-Prozess Gather (sammeln)Mit der Gather-Operation sammelt der MPI-Prozess
Vektorbasierte VarianteDie vektorbasierte Variante der Gather-Operation erlaubt eine prozessabhängige Anzahl von Elementen:
Bei den Feldern ist zu beachten, dass im Empfangspuffer zwar Lücken erlaubt sind aber keine Überlappungen.
Sollen also etwa von 3 Prozessen jeweils 1, 2 und 3 Elemente vom Typ Integer empfangen werden, so muss Scatter (streuen)Mit einer Scatter-Operation schickt der MPI Prozess
Vektorbasierte Variante
AkkumulationDie Akkumulation ist eine spezielle Form der Gather-Operation. Hierbei werden ebenfalls die Daten aller beteiligten Prozesse aufgesammelt, aber zusätzlich noch mittels einer festgelegten Reduktionsoperation zu einem Datum reduziert. Sei beispielsweise der Wert bei dem Prozess mit Rang , dann liefert Reduce(+) die Gesamtsumme aller Werte: .
Für den Parameter Logische Operationen
Arithmetische Operationen
Die Operationen Benutzerdefinierte OperationenZusätzlich zu den vordefinierten Reduktionsoperationen können auch eigene Reduktionsoperationen verwendet werden. Dazu wird eine frei programmierbare binäre Verknüpfungsoperation, die assoziativ sein muss und optional kommutativ sein kann, dem MPI bekanntgegeben:
Die dazugehörige Nutzerfunktion berechnet aus zwei Eingabewerten einen Ausgabewert und macht dies – aus Optimierungsgründen – nicht nur einmal mit Skalaren, sondern elementweise auf Vektoren beliebiger Länge:
PräfixreduzierungZusätzlich zur oben genannten Akkumulation, existiert auch eine Allreduce Variante – welche das gleiche Ergebnis allen MPI-Prozessen zur Verfügung stellt und nicht nur einem
Soll der eigene Wert nicht mit in die Berechnung eingehen (d. h., ausgeschlossen werden), so kann dies mit der exklusiven Scan Funktion AllgatherBei der Allgather-Operation schickt jeder Prozess an jeden anderen Prozess die gleichen Daten. Es handelt sich also um eine Multi-Broadcast-Operation, bei der es keinen gesonderten MPI-Prozess gibt.
All-to-all (Gesamtaustausch)Bei der All-to-all-Kommunikation werden – ähnlich wie bei der Allgather-Kommunikation – Daten zwischen allen Prozessen ausgetauscht. Dabei wird jedoch nur der i-te Teil des Sendebuffers an den i-ten Prozess gesendet. Daten, die vom Prozess mit dem Rang j kommen, werden entsprechend an j-ter Stelle im Empfangsbuffer gespeichert.
Des Weiteren gibt es noch die synchronisierende MPI_Barrier-Operation. Diese Funktion kehrt erst zurück, nachdem alle in der angegebenen Gruppe befindlichen MPI-Prozesse diesen Teil des Programmes erreicht haben. MPI-2Seit 1997 ist eine zweite Version des MPI-Standards verfügbar, die einige Erweiterungen zu dem weiterhin bestehenden MPI-1.1 Standard hinzufügt. Zu diesen Erweiterungen gehören unter anderem
Beispiel: Lesen einer nx(n+1)-Matrix mit paralleler Datei-Eingabe und size Prozessen mit den Nummern rank = 0 … size-1. Die Spalte n+1 enthält die rechte Seite des Gleichungssystems A * x = b in Form der erweiterten Matrix [A, b]. Die Zeilen der Matrix werden gleichmäßig auf die Prozessoren verteilt. Die Verteilung erfolgt zyklisch (Jeder Prozessor eine Zeile, nach size Zeilen wird wieder rank=0 bedient) und nicht blockweise (jeder Prozessor bekommt einen zusammenhängenden Block von n/size Zeilen): ndims = 1; /* dimensions */ aosi [0] = size * (n+1); /* array of sizes */ aoss [0] = n+1; /* array of subsizes */ aost [0] = rank * (n+1); /* array of starts */ order = MPI_ORDER_C; /* row or column order */ MPI_Type_create_subarray (ndims, aosi, aoss, aost, order, MPI_DOUBLE, &ft); MPI_Type_commit (&ft); MPI_File_open (MPI_COMM_WORLD, fn, MPI_MODE_RDONLY, MPI_INFO_NULL, &fh); MPI_File_set_view (fh, sizeof (int), MPI_DOUBLE, ft, „native“, MPI_INFO_NULL); for (i = rank; i < n; i+=size) { MPI_File_read (fh, rdbuffer, n+1, MPI_DOUBLE, &status); for (j = 0; j < n+1; j++) { A [i / size] [j] = rdbuffer [j]; /* nur die dem Prozess zugeordneten Zeilen */ } } MPI_File_close (&fh); Die Schnittstelle folgt mit leichten der Parallelität geschuldeten Änderungen dem POSIX-1003.1-Standard. Die Datei wird mit MPI_File_open zum gemeinsamen Lesen eröffnet. Die Blenden (views) für die einzelnen Prozesse werden mit MPI_File_set_view festgelegt. Hier wird die vorher definierte Variable ft (filetype) benötigt, in der in einem Block von size * (n+1) doubles eine Zeile mit n+1 doubles herausgepickt wird, die bei Position rank * (n+1) beginnt. Somit wird vom gesamten Block jedem Prozess sukzessive genau eine Zeile zugewiesen. Dieser Typ wird mit MPI_Type_create_subarray definiert und mit MPI_Type_commit im MPI-System bekanntgemacht. Jeder Prozess liest mit MPI_File_read „seine“ Zeilen mit den Nummern i = rank, rank + size, rank + 2*size, … bis die gesamte Matrix gelesen wurden. Das Argument size of (int) berücksichtigt die Größe der Matrix, die am Anfang der Datei als int gespeichert wird. Gewinn: In size Prozessoren kann eine Matrix verteilt gespeichert werden, die auf einem einzelnen Prozessor in dessen Speicher keinen Platz mehr hätte. Das rechtfertigt auch die Konvention, als Speicher einer Parallelanlage die Summe des Speichers der Einzelcores und Einzelknoten anzugeben. Dateiformat: n Zeile 0 (n+1) Zahlen für Prozess rank 0 Zeile 1 (n+1) Zahlen für Prozess rank 1 … Zeile r (n+1) Zahlen für Prozess rank r … Zeile size-1 (n+1) Zahlen für Prozess rank size-1 Zeile size (n+1) Zahlen für Prozess rank 0 Zeile size+1 (n+1) Zahlen für Prozess rank 1 … Zeile size+r (n+1) Zahlen für Prozess rank r … Zeile 2*size-1 (n+1) Zahlen für Prozess rank size-1 Zeile 2*size (n+1) Zahlen für Prozess rank 0 … … es folgen entsprechend der Zeilenzahl der Matrix ausreichend viele solcher Blöcke Das eigentliche Lesen erfolgt mit MPI_File_read. Jeder Prozess liest sequentiell nur die ihm zugeteilten Zeilen. Die kollektive Operation besteht darin, dass die MPI-Bibliothek das Lesen optimieren und parallelisieren kann. Nach Ende des Lesens muss die Datei wie üblich geschlossen werden. Das geschieht mit MPI_File_close. MPI verfügt für die Operationen über eigene Datentypen MPI_Datatype ft und MPI_File fh. Die Beschreibung des filetypes erfolgt mit normalen C-Variablen: int ndims; int aosi [1]; int aoss [1]; int aost [1]; int order; Weiteres in.[4] ImplementierungenC++, C und FortranDie erste Implementierung des MPI-1.x-Standards war MPICH vom Argonne National Laboratory und der Mississippi State University. Mittlerweile ist MPICH2 verfügbar, das den MPI-2.1-Standard implementiert. LAM/MPI vom Ohio Supercomputing Center war eine weitere freie Version, deren Weiterentwicklung inzwischen zugunsten von Open MPI eingestellt wurde. Ab der Version 1.35 der Boost Libraries gibt es Boost.MPI, eine C++-freundliche Schnittstelle zu verschiedenen MPI-Implementierungen. Auch andere Projekte, wie z. B. TPO++, bieten diese Möglichkeit und sind in der Lage, STL-Container zu versenden und zu empfangen. C#
Python
Java
PerlRHaskellSiehe auchLiteratur
Weblinks
Einzelnachweise
|