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).
Geschichte 1992 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 Punkt-zu-Punkt-Kommunikation globale Kommunikation Gruppen, Kontext und Kommunikatoren Umgebung Profiling-Schnittstelle Spracheinbindung für C und Fortran 77
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: parallele Datei-Ein-/Ausgabe dynamische Prozessverwaltung Zugriff auf Speicher anderer Prozesse zusätzliche Spracheinbindung von C++ und Fortran 90
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, 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.The Future of MPI. (PDF; 2,4 MB)
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.Changes in MPI-4.0
Zu den Hauptentwicklern gehört Bill Gropp.
Punkt-zu-Punkt Kommunikation Die 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 buffer, count, und datatype beschrieben werden. Zu jeder Sendeoperation muss eine passende Empfangsoperation existieren. Da in parallelen Anwendungen die bloße Reihenfolge der Abarbeitung von Operationen nicht immer ausreichend ist, bietet MPI zusätzlich den tag-Parameter an – nur wenn dieser Wert bei Sende- und Empfangsoperation identisch ist, dann passen beide zusammen.
Blockierendes Senden und Empfangen Die 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)
buf: Zeiger auf den Sendepuffer count: Zahl der Elemente im Sendepuffer datatype: Datentyp der Elemente im Sendepuffer dest: Rang des Zielprozesses tag: Markierung der Nachricht comm: Kommunikator der Prozessgruppe
int MPI_Recv (void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status* status)
buf: Zeiger auf einen Empfangspuffer ausreichender Größe count: Zahl der Elemente im Empfangspuffer datatype: Datentyp der Elemente im Empfangspuffer source: Rang des Quellprozesses (mit source=MPI_ANY_SOURCE wird von einem beliebigen Prozess empfangen) tag: erwartete Markierung der Nachricht (mit tag=MPI_ANY_TAG wird jede Nachricht empfangen) comm: Kommunikator der Prozessgruppe status: Zeiger auf eine Statusstruktur, in der Informationen über die empfangene Nachricht abgelegt werden sollen
Die beiden Operationen sind blockierend und asynchron. Das bedeutet: MPI_Recv kann ausgeführt werden, bevor das zugehörige MPI_Send gestartet wurde MPI_Recv blockiert, bis die Nachricht vollständig empfangen wurde Analog gilt: MPI_Send kann ausgeführt werden, bevor das zugehörige MPI_Recv gestartet wurde MPI_Send blockiert, bis der Sendepuffer wiederverwendet werden kann (d. h. die Nachricht vollständig übermittelt oder zwischengepuffert wurde)
Programmbeispiel Die Verwendung von MPI_Send und MPI_Recv wird im folgenden ANSI-C-Beispiel für 2 MPI-Prozesse veranschaulicht:
#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 Kommunikation Die 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 Request-Objekt erzeugt, mit dessen Hilfe auf die Beendigung dieser Operation geprüft oder gewartet werden kann.
int MPI_Isend (void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request* request)
… request: Adresse der Datenstruktur, die Informationen zur Operation enthält
int MPI_Irecv (void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request* request)
…
Fortschritt abfragen Um den Fortschritt einer dieser Operationen zu erfahren, wird folgende Operation verwendet:
int MPI_Test (MPI_Request* request, int* flag, MPI_Status* status)
Wobei flag=1 oder 0 gesetzt wird, je nachdem, ob die Operation abgeschlossen ist oder noch andauert.
Blockierend warten Um dennoch blockierend auf eine MPI_Isend- oder MPI_Irecv-Operation zu warten, wird folgende Operation benutzt:
int MPI_Wait (MPI_Request* request, MPI_Status* status)
Synchronisierendes Senden Für die Sendeoperationen werden auch die synchronen Varianten MPI_Ssend und MPI_Issend definiert. In diesem Modus wird das Senden erst dann beendet, wenn die zugehörige Empfangsoperation begonnen wurde.
Puffernde Varianten …
Gruppen und Kommunikatoren Prozesse 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 MPI_COMM_WORLD.
Die zum Kommunikator comm gehörende Gruppe erhält man mit
int MPI_Comm_group (MPI_Comm comm, MPI_Group* group)
Für Prozessgruppen stehen die üblichen Mengenoperationen zur Verfügung.
Vereinigung Zwei Gruppen group1 und group2 können zu einer neuen Gruppe new_group vereinigt werden:
int MPI_Group_union (MPI_Group group1, MPI_Group group2, MPI_Group* new_group)
Die Prozesse aus group1 behalten ihre ursprüngliche Nummerierung. Die aus group2, die nicht bereits in der ersten enthalten sind, werden fortlaufend weiter nummeriert.
Schnittmenge Die Schnittmenge zweier Gruppen erhält man mit
int MPI_Group_intersection (MPI_Group group1, MPI_Group group2, MPI_Group* new_group)
Differenz Die Differenz zweier Gruppen erhält man mit
int MPI_Group_difference (MPI_Group group1, MPI_Group group2, MPI_Group* new_group)
Globale Kommunikation In 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 root bezeichnet wird. Wo es sinnvoll ist, existieren zusätzlich zu den regulären Kommunikationsoperationen noch vektorbasierte Varianten (z. B. Scatterv), die unterschiedliche Argumente pro Prozess ermöglichen.
Broadcast (ausstrahlen) mini|hochkant|Die Broadcast-Operation für drei Prozesse
Mit der Broadcast-Operation schickt ein ausgewählter MPI-Prozess root allen anderen Prozessen in seiner Gruppe comm die gleichen Daten. Die dafür definierte Funktion ist dabei für alle beteiligten Prozesse identisch:
int MPI_Bcast (void *buffer, int count, MPI_Datatype type, int root, MPI_Comm comm)
Der MPI-Prozess root stellt in buffer seine Daten zur Verfügung, während die anderen Prozesse hier die Adresse ihres Empfangspuffers übergeben. Die restlichen Parameter müssen bei allen Prozessen gleich (bzw. gleichwertig) sein. Nachdem die Funktion zurückkehrt, befinden sich in allen Puffern die Daten, die ursprünglich nur bei root vorhanden waren.
Gather (sammeln) mini|Die Gather-Operation
Mit der Gather-Operation sammelt der MPI-Prozess root die Daten aller beteiligten Prozesse ein. Die Daten aller Sendepuffer werden dabei (nach Rang sortiert) hintereinander im Empfangspuffer abgelegt:
int MPI_Gather (void *sendbuf, int sendcount, MPI_Datatype sendtype, void *recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm)
Vektorbasierte Variante Die vektorbasierte Variante der Gather-Operation erlaubt eine prozessabhängige Anzahl von Elementen:
int MPI_Gatherv (void *sendbuf, int sendcount, MPI_Datatype sendtype, void *recvbuf, int *recvcounts, int *displs, MPI_Datatype recvtype, int root, MPI_Comm comm)
recvcounts: Feld, das die Zahl der Elemente enthält, die von den einzelnen Prozessen empfangen werden (nur für root relevant) displs: Feld, dessen Eintrag i die Verschiebung im Empfangspuffer festlegt, bei der die Daten von Prozess i abgelegt werden sollen (ebenfalls nur für root relevant)
Bei den Feldern ist zu beachten, dass im Empfangspuffer zwar Lücken erlaubt sind aber keine Überlappungen.