8.1 Message Passing Interface (MPI)

Im November 1992 begannen Teilnehmer aus Industrie, Forschungslabors und Universitäten mit der Diskussion über eine Standardisierung eines Modells zum Nachrichtenaustausch zwischen Hochleistungscomputern. Ziel war es, aus einer Vielzahl von Konzepten, die für verschiedene Kommunikationshardware entstanden waren, eine gemeinsame Teilmenge zu finden, die aber doch von allen ohne größere Nachteile genutzt werden kann.

1994 wurde dann die offizielle Spezifikation MPI 1.0 veröffentlicht. Neben der Weiterentwicklung von MPI in den Versionen 1.1 und 1.2 wurde Mitte 1995 mit der Diskussion von MPI 2.0 begonnen, die 1997 mit der Veröffentlichung der Spezifikation abgeschlossen wurde.

MPI 1.0 wird wie schon erwähnt von allen Herstellern von Parallelrechnern oder Supercomputern unterstützt. Daneben gibt es mit MPICH und LAM/MPI zwei `freie' Implementierungen [ACG$^+$00,SMML00], die besonders im (Linux-) PC-Cluster-Umfeld weite Verbreitung haben. Von MPI 2.0 gibt es derzeit noch keine vollständige Implementierung; allerdings sind mehrere Projekte dazu im Gange.

Wir legen im Folgenden MPICH in der Version 1.2.1 zugrunde, das MPI 1.2 und schon einige Erweiterungen von MPI 2.0 implementiert. In dieser Version umfasst MPI gut 300 Bibliotheksfunktionen, mit denen jedes gewünschte Detail des Kommunikationsverhaltens eingestellt werden kann.

`Sechs-Funktionen'-MPI

Für den Anfang reicht aber ein `Sechs-Funktionen'-MPI.

Die ersten beiden Funktionen dienen dazu, den Kontakt mit MPI auf- und abzubauen:

MPI.Init() und MPI.Finalize().

Zwei weitere Funktionen sind erforderlich, um den eigenen Platz (Rank) innerhalb der gesamten MPI-Umgebung (Size) zu finden:

MPI.COMM_WORLD.Size() und MPI.COMM_WORLD.Rank().

Mit diesen vier Funktionen können wir channel implementieren.

Die letzten beiden Funktionen dienen zum Senden und Empfangen von Daten:

MPI.COMM_WORLD.Send() und MPI.COMM_WORLD.Recv().

Damit können wir send und receive implementieren.

Alternativ zu Send und Receive kann man oft auch bequemer mit Broadcast und Reduction sein Ziel erreichen:

MPI.COMM_WORLD.Bcast() und MPI.COMM_WORLD.Reduce().

Bcast kann sowohl Daten senden als auch Daten empfangen, wodurch es Send und Recv auf dem `Hinweg' der Daten gänzlich ersetzen kann. Mit Reduce können, wie wir im Kapitel 7 (OpenMP) schon gesehen haben, sehr elegant die Ergebnisse der verschiedenen Prozesse eingesammelt und verarbeitet werden. Damit kann es Send und Recv auf dem `Rückweg' der Daten ersetzen.

Bei paralleler Ausführung von vielen Prozessen sind Bcast und Reduce wesentlich effizienter als Send und Recv. Wenn p die Anzahl der beteiligten Prozesse ist (d.h. p=MPI.COMM_WORLD.Size()), dann benötigen sowohl Bcast als auch Reduce nur log_2(p) parallele Send- und Recv-Operationen, um die Daten mit allen Partnern auszutauschen. Ohne Bcast bzw. Reduce wären immer p solche Operationen erforderlich, um die Daten mit allen Partnern auszutauschen. Bei 100 Prozessen sind das nur 7=log_2(100) Operationen, bei 8 Prozessen aber noch 3.

Kommunikatoren

Ein zentrales Konzept von MPI sind die Kommunikatoren (Communicators). Jeder Nachrichtenaustausch findet in einem Kontext statt. In der obigen Zusammenstellung ist das der globale Kontext MPI.COMM_WORLD. Kommunikatoren können mit MPI-Routinen erzeugt, geklont und manipuliert werden. Die Kommunikatoren ermöglichen damit virtuelle Kanäle zwischen den verschiedenen Kommunikationspartnern. Damit wird es auch gefahrlos möglich, parallele Programmbibliotheken auf Basis von MPI zu entwickeln. Die Bibliotheksprogramme können sich mittels eines eigenen Kommunikators von störenden Nachrichten zwischen anderen Programmen abschotten. Die Überwachung leistet das MPI-Laufzeitsystem und die Programme können nicht versehentlich Nachrichten erhalten, die nicht für sie gedacht sind. Mit Hilfe von Kommunikatoren lassen sich auch Untergruppen von Prozessen definieren, die nur untereinander kommunizieren.

In der C++- und Java-Bindung von MPI wird schon durch die Notation klar, dass die Kommunikationsoperationen Methoden des Kontextes MPI.COMM_WORLD sind. In der FORTRAN- und C-Bindung von MPI wird der Communicator als Parameter bei allen Funktionsaufrufen mitgeführt.

Eine aktuelle Forschungsrichtung ist die Untersuchung des Einsatzes von Jini zur Verwaltung des Kontextes [BC99]. MPI-Prozesse könnten über Jini ihre Fähigkeiten publizieren. Die beteiligten MPI-Prozesse könnten diese Fähigkeiten leasen. Falls ein Prozess abstürzt, würde der Leasing-Contract auslaufen und die Partner könnten dies sicher erkennen und darauf reagieren. Das Erkennen von Abstürzen und das Aufräumen von Ressourcen und nicht abgeholten Nachrichten ist in praktisch allen MPI-Implementierungen ein Schwachpunkt.

MPI bietet sehr viele Optionen und Funktionen, um die Sende- oder Empfangspuffer und das Blockierungsverhalten der Kommunikationsoperationen auszuwählen oder zu steuern. Die Beschreibung all dieser Möglichkeiten würde aber den Umfang dieses Buches sprengen. Wir verweisen daher auf die entsprechende Literatur, zum Beispiel [GLS95]. Soweit wir es feststellen konnten, sind aber all diese Varianten in mpiJava vollständig implementiert.

Starten und Stoppen

Wie schon erwähnt, bietet MPI 1.0 keine Spezifikation, wie MPI-Prozesse gestartet, überwacht und beendet werden sollen. MPICH und einige kommerzielle MPI-Versionen bieten aber ein Programm (Shell-Script) namens mpirun an, das MPI-Programme startet.

   mpirun -np <#processes> <program and arguments>
In seiner einfachsten Form erwartet es den Parameter `-np n', mit dem die Anzahl `n' der gewünschten gleichzeitig laufenden Programme angegeben wird, gefolgt von dem Namen des MPI-Programms, gefolgt von Argumenten für das Programm. Zum Beispiel startet
   mpirun -np 2 HelloWorld
das MPI-Programm `HelloWorld' zwei Mal. mpirun greift auf ein machine-File zurück, in dem die Hostnamen der verfügbaren Rechner eingetragen werden. (Die Datei entspricht dem host-File von PVM.) Entsprechend dem `-np n'-Parameter werden die ersten n Rechner aus der Datei ausgewählt. mpirun versucht mittels rsh eine Verbindung zu den Rechnern aufzubauen und startet dann dort das gleiche MPI-Programm. Dazu muss der Benutzer die entsprechenden Zugangsrechte auf dem Rechner besitzen und dieses Programm (nebst MPI) muss dort verfügbar sein (zum Beispiel per NFS). mpirun sammelt auf allen beteiligten Rechnern die Ausgabe der MPI-Programme ein und leitet diese an den Ausgangsrechner zurück. Dadurch wird die Fehlersuche in den verteilten MPI-Programmen etwas vereinfacht. MPI verwendet im Unterschied zu PVM (Parallel Virtual Machine) keinen Daemon, der Programme dynamisch starten kann.

mpiJava verfügt zusätzlich über ein Skript prunjava, mit ähnlichen Parametern wie mpirun. So startet etwa

   prunjava 2 HelloWorld
die mpiJava-Klasse `HelloWorld' zwei Mal. prunjava führt intern im Wesentlichen das Kommando
   mpirun -np 2 java -classpath ... HelloWorld
aus. Neben dem Pfad zu den mpiJava-Klassen wird an MPICH die Progammbibliothek libmpijava.so weitergegeben, mit der mpiJava über JNI die MPICH-Funktionen aufruft. Da mpirun neben dem MPICH-Laufzeitsystem zusätzlich noch eine JVM starten muss, sind die Startup-Zeiten von mpiJava-Programmen natürlich größer als die reiner MPI-Programme.

Der Programmierstil, in dem viele MPI-Programme geschrieben werden, ist SPMD (Single Programm Multiple Data). Das heißt, es wird nur ein Programm geschrieben, das intern über Fallunterscheidungen feststellt, welche Rolle es annimmt: die eines Masters oder die der Gehilfen. MPI liefert dazu über MPI.COMM_WORLD.Rank() eine Zahl, die zur Fallunterscheidung genutzt wird. Zum Beispiel bezeichnet 0 meist den Master, während die anderen Zahlen für die Gehilfen stehen.

Klassenorganisation

Abbildung 8.1: MPI-Klassenorganisation
\begin{figure}{\tt\begin{itemize}
\item class java.lang.Object
\begin{itemize}
...
...temize} \item class mpi.User\_function
\end{itemize}\end{itemize}}
\end{figure}

Den Aufbau der Java-Klassenhierarchie von MPI nach [KLL$^+$99] (sie entspricht der C++-Klassenhierarchie von MPI 2.0) zeigt Abbildung 8.1. Wir verwenden direkt nur die Klasse mpi.MPI. Sie besteht nur aus statischen Feldern und Methoden (soweit nicht von Object geerbt) und sie existiert u.a. auch aus Kompatibilitätsgründen zur FORTRAN- und C-Version von MPI. In mpi.MPI gibt es ein statisches Feld COMM_WORLD vom Typ mpi.Intracomm, welche von mpi.Comm abgeleitet ist. In mpi.Comm sind alle Sende- und Empfangsmethoden definiert. mpi.Intracomm erweitert mpi.Comm um Methoden zur kollektiven Kommunikation wie Barrier, Broadcast, Reduce, Scatter und Gather. mpi.Intercomm erweitert mpi.Comm um Methoden zum Aufbau von Verbindungen zwischen verschiedenen Gruppen mit unterschiedlichen Kommunikatoren. Durch mpi.Comm.clone() kann man sich einen Satz von neuen virtuellen Kanälen zu den gleichen Partnern erzeugen. Mit

    mpi.MPI.COMM_WORLD.Send( ... )
    mpi.MPI.COMM_WORLD.Bcast( ... )
verwenden wir also die Methode Send() von mpi.Comm bzw. die Methode Bcast() von mpi.Intracomm.


Heinz Kredel
2002-04-05