Einführung in die MPI-Schnittstelle

MPI (\textit{Message Passing Interface}) ist ein Standard für eine Bibliotheksschnittstelle für parallele Programme. Im Gegensatz zu Threads und OpenMP leben die parallelen Ausführungsfäden in getrennten Prozessen und damit auch getrennten Adressräumen. Dies ermöglicht auch den Einsatz von MPI auf einem Cluster (wie beispielsweise der Pacioli) oder einem beliebigen Netzwerk von Rechnern.

1994 entstand die erste Fassung des Standards (1.0), 1995 die Version 1.2 und seit 1997 gibt es 2.0. Im September 2012 erschien die Version 3.0, die bei uns bislang nur auf der Thales unterstützt wird. Die Standards sind öffentlich unter http://www.mpi-forum.org/.

Der Standard umfasst die sprachspezifischen Schnittstellen für Fortran und C. In C++ wird entsprechend die C-Schnittstelle verwendet.

Es stehen mehrere Open-Source-Implementierungen zur Verfügung:

Wir verwenden im Rahmen dieser Vorlesung OpenMPI.

Bei Übersetzungen wird statt g++ das Kommando mpiCC oder mpic++ verwendet. MPI-Programme werden nicht direkt gestartet, sondern mit Hilfe des Programms mpirun, das sich um die Einrichtung der Ausführungsumgebung kümmert.

Auf welchen Rechnern wieviel Prozesse auf welche Weise gestartet werden, ist das Problem von mpirun. Das kann entsprechend konfiguriert werden, wobei insbesondere auch die Gesamtzahl der Prozesse festgelegt wird.

Zu Beginn starten alle Prozesse zeitgleich. Dies ist ein wesentlicher Unterschied zu allen bisherigen Vorgehensweisen, bei denen wir mit einem Ausführungsfaden begannen.

Ähnlich wie bei OpenMP kann jeder Prozess erfahren, wieviel Prozesse es gibt und welche Nummer der eigene Prozess hat (in MPI rank genannt). Der Prozess mit dem rank 0 hat dabei eine Sonderrolle. Er ist der einzige, der ggf. die Kommandozeile bearbeitet und auf der Standardausgabe etwas ausgeben kann. In einem Master/Worker-Pattern übernimmt dieser Prozess naturgemäß die Rolle des Masters.

Das wird am folgenden kleinen Beispiel demonstriert, bei dem die Worker jeweils mit MPI_Send ihre Prozessnummer an den Master schicken, während der Master diese mit MPI_Recv entgegennimmt und ausgibt:

#include <cstdio>
#include <mpi.h>

int main(int argc, char** argv) {
   MPI_Init(&argc, &argv);

   int rank; MPI_Comm_rank(MPI_COMM_WORLD, &rank);
   int nof_processes; MPI_Comm_size(MPI_COMM_WORLD, &nof_processes);

   if (rank) {
      MPI_Send(&rank, 1, MPI_INT, 0, 0, MPI_COMM_WORLD);
   } else {
      for (int i = 0; i + 1 < nof_processes; ++i) {
         MPI_Status status;
         int msg;
         MPI_Recv(&msg, 1, MPI_INT, MPI_ANY_SOURCE,
            0, MPI_COMM_WORLD, &status);
         int count;
         MPI_Get_count(&status, MPI_INT, &count);
         if (count == 1) {
            printf("%d\n", msg);
         }
      }
   }

   MPI_Finalize();
}

Zu beachten ist, dass n Prozesse dieses Programm gleichzeitig starten. MPI_Init muss vor allen anderen MPI-Funktion aufgerufen werden. Diese Funktion dient dazu, die Verbindung zur Ausführungsumgebung aufzunehmen. Analog ist am Ende MPI_Finalize aufzurufen, das den Prozess von der Ausführungsumgebung wieder trennt.

Mit MPI_Comm_rank wird die eigene Prozessnummer ermittelt, mit MPI_Comm_size die Zahl der Prozesse. Der erste Parameter spezifiziert hier jeweils die Kommunikationsdomäne. Das ist hier zu Beginn immer MPI_COMM_WORLD. Später können ggf. auch weitere Kommunikationsdomänen erzeugt werden.

Zur Kommunikation werden hier MPI_Send und MPI_Recv verwendet. Beide haben eine ähnliche Parameterfolge:

So lässt sich so ein Programm übersetzen und ausführen:

$shell> mpiCC -g -std=c++11 -o mpi_test mpi_test.cpp
$shell> mpirun -np 4 mpi_test
1
2
3

Aufgabe

Erweitern Sie das triviale Beispiel dahingehend, dass es mit Hilfe der Simpson-Regel ein numerisches Integral für die Funktion f auf dem Intervall \([a, b]\) unter Verwendung von \(n\) Teilintervallen berechnet:

\[S(f,a,b,n) ~=~ \frac{h}{3} \left( \frac{1}{2} f(x_0) + \sum_{k=1}^{n-1} f(x_k) + 2 \sum_{k=1}^{n} f\left(\frac{x_{k-1} + x_k}{2}\right) + \frac{1}{2} f(x_n) \right)\]

mit \(h ~=~ \frac{b-a}{n}\) und \( x_k ~=~ a + k \cdot h\).

Als Funktion könnten Sie beispielsweise \(\frac{4}{1 + x^2}\) über das Intervall \([0, 1]\) verwenden. Dann können Sie Ihr Resultat mit \(\pi\) vergleichen. Die Zahl der Teilintervalle können Sie der Einfachheit halber global als Konstante festlegen (100 genügt bei diesem Beispiel). Alle Prozesse (einschließlich rank 0) sollten das numerische Integral auf ihrem jeweiligen Teilintervall berechnen, dann sollten wie im Trivialbeispiel alle Prozesse mit rank größer 0 ihr Resultat an den Prozess mit rank 0 schicken, der dann die Teilergebnisse aufsummiert und ausgibt. Als MPI-Datentyp für double kann MPI_DOUBLE verwendet werden.