====================================== Übertragung eigener Datentypen mit MPI ====================================== Es lohnt sich, noch einmal einen Blick auf `MPI_Send` und `MPI_Recv` zu werfen: ---- CODE (type=cpp) ---------------------------------------------------------- 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 1:1-Nachrichtenaustausch-Operationen `MPI_Send` und `MPI_Recv` erlauben zwar die Übertragung von Arrays, bestehen aber darauf, dass die einzelnen Array-Elemente unmittelbar hintereinander im Speicher liegen. Somit können wir diese Operationen nicht unmittelbar nutzen, um einen beliebigen `DenseVectorView` oder einen `GeMatrixView` zu übertragen, da die Abstände, konfiguierbar durch `inc` bzw. `incRow` und `incCol`, beliebig sein können. MPI bietet aber vielfältige Möglichkeiten Datentypen selbst zu definieren und es bietet sich an, passende MPI_Datatype-Objekte für unsere Vektor- und Matrixtypen zu erzeugen. Es ist zuvor jedoch sinnvoll den Aufbau eines Datentyps in MPI anzusehen: Es gibt die Menge der Basistypen $BT$ in MPI, der beispielsweise `MPI_DOUBLE` oder `MPI_INT` angehören. Ein Datentyp $T$ mit der Kardinalität $n$ ist in der MPI-Bibliothek eine Sequenz von Tupeln $\left\{ (bt_1, o_1), (bt_2, o_2), \dots, (bt_n, o_n) \right\}$, mit $bt_i \in BT$ und den zugehörigen Offsets $o_i \in \mathbb{N}_0$ für $i = 1, \dots, n$. Die Offsets geben die relative Position der jeweiligen Basiskomponenten in Bytes zur Anfangsadresse an. Bezüglich der Kompatibilität bei `MPI_Send` und `MPI_Recv` werden zwei Datentypen $T_1$ und $T_2$ genau dann als kompatibel erachtet, falls die beiden Kardinalitäten $n_1$ und $n_2$ gleich sind und $bt_{1_i} = bt_{2_i}$ für alle $i = 1, \dots, n_1$ gilt. Bei \ident{MPI\_Send} sind Überlappungen zulässig, bei \ident{MPI\_Recv} haben sie einen undefinierten Effekt. Einige Beispiele hierzu: * Ein Zeilenvektor des Basistyps `MPI_DOUBLE` (8 Bytes) der Länge 4 hat den Datenyp $\left\{ (DOUBLE, 0), (DOUBLE, 8), (DOUBLE, 16), (DOUBLE, 24) \right\}$. * Ein Spaltenvektor der Länge 3 aus einer $5 \times 5$-Matrix hat den Datentyp $\left\{ (DOUBLE, 0), (DOUBLE, 40), (DOUBLE, 80) \right\}$. * Die Spur einer $3 \times 3$-Matrix hat den Datentyp $\left\{ (DOUBLE, 0), (DOUBLE, 32), (DOUBLE, 64) \right\}$. * Die obere Dreiecks-Matrix einer $3 \times 3$-Matrix: $\left\{ (DOUBLE, 0), (DOUBLE, 8), (DOUBLE, 16), (DOUBLE, 32), (DOUBLE, 40), (DOUBLE, 64) \right\}$. Da bezüglich der Kompatibilität die Offsets ignoriert werden, lässt sich bei der Übertragung problemlos ein Spalten- in einen Zeilenvektor konvertieren oder eine Matrix transponieren. Für die Konstruktion von Typen gibt es zahlreiche Funktionen in MPI, die letztlich immer die beschriebenen Tupel-Sequenzen erzeugen. Ein erstes Beispiel hierzu sei `MPI_Type_vector`: ---- CODE (type=cpp) ---------------------------------------------------------- int MPI_Type_vector(int count, int blocklength, int stride, MPI_Datatype oldtype, MPI_Datatype* newtype); ------------------------------------------------------------------------------- Der Elementtyp wird hier mit `oldtype` spezifiziert. Die `blocklength` spezifiziert, wieviel Elemente dieses Typs immer unmittelbar hintereinander liegen. Diese bilden einen Block. Der Offset zweier aufeinanderfolgenden Blöcke wird mit `stride` spezifiziert -- dieser Wert wird implizit mit der Größe des Elementtyps multipliziert. Insgesamt umfasst der Datentyp `count` Blöcke. Bevor ein neu erzeugter Datentyp bei einer Übertragung wie beispielsweise mit `MPI_Send` und `MPI_Recv` verwendet werden kann, muss der Datentyp intern in eine flache Repräsentierung entsprechend der Tupel-Sequenz konvertiert werden. Dies geschieht mit `MPI_Type_commit`: ---- CODE (type=cpp) ---------------------------------------------------------- int MPI_Type_commit(MPI_Datatype* datatype); ------------------------------------------------------------------------------- Hier wird ein Zeiger auf den Datentyp übergeben, d.h. `datatype` kann anschließend einen neuen Wert haben. Aufgabe ======= In der folgenden Vorlage ist ein kleiner Test zum Übertragen von Vektoren vorbereitet. Erzeugen Sie jeweils die notwendigen MPI-Datentypen, so dass jeweils einer der verwendeten Vektoren mit einer einzigen `MPI_Send`- bzw. `MPI_Recv`-Operation übertragen werden kann. Hierbei lohnt es sich, eine kleine Funktion zu schreiben, die den `MPI_Datatype` für einen Vektor bestimmt. So könnte eine entsprechende Template-Funktion aussehen: ---- CODE (type=cpp) ---------------------------------------------------------- template typename std::enable_if::value, MPI_Datatype>::type get_type(const Vector& vector) { using ElementType = typename Vector::ElementType; MPI_Datatype datatype; /* ... */ return datatype; } ------------------------------------------------------------------------------- Wenn Sie einen allgemeinen Weg wünschen, um dem Element-Typen wiederum den konkreten MPI-Datentyp zuzuordnen, empfiehlt sich die Verwendung von `#include `. Dort gibt es bereits eine `get_type`-Template-Funktion, die alle elementaren Datentypen von MPI unterstützt. So liefert `hpc::mpi::get_type(x)` für `double x` bereits `MPI_DOUBLE`. Die Vorlesungsbibliothek findet sich unter _/home/numerik/pub/hpc/session21_ auf unseren Rechnern. Vorlage ======= :import:session21/transfer_vectors.cpp :navigate: up -> doc:index next -> doc:session21/page02