Ich möchte 2d Datenblöcke mit MPI_GATHER senden. Zum Beispiel habe ich 2x3 Arrays auf jedem Knoten und ich möchte 8x3 Array auf Root, wenn ich 4 Knoten habe. Für 1D-Arrays sortiert MPI_GATHER Daten nach MPI-Rang, aber für 2d-Daten schafft es Chaos !. Was ist der saubere Weg, um Brocken in Ordnung zu bringen?
Ich habe die Ausgabe dieses Codes erwartet:
%Vor%um so etwas zu sein:
%Vor%aber es sieht so aus:
%Vor%Die folgende wörtliche Fortran-Übersetzung von diese Antwort . Ich hatte gedacht, dass dies unnötig war, aber die vielen Unterschiede bei der Array-Indizierung und dem Speicherlayout könnten bedeuten, dass es sich lohnt, eine Fortran-Version zu machen.
Lassen Sie mich zunächst sagen, dass Sie das eigentlich nicht wirklich tun wollen - zerstreuen und riesige Datenmengen von einem "Master" -Prozess sammeln. Normalerweise möchten Sie, dass jede Aufgabe an ihrem eigenen Teil des Puzzles herumtüftelt, und Sie sollten darauf abzielen, dass niemals ein Prozessor eine "globale Ansicht" der gesamten Daten benötigt; Sobald Sie dies benötigen, begrenzen Sie die Skalierbarkeit und die Problemgröße. Wenn Sie dies für I / O tun - ein Prozess liest die Daten, dann streut sie, und sammelt sie wieder zum Schreiben, Sie wollen schließlich in MPI-IO schauen.
Wenn Sie jedoch zu Ihrer Frage kommen, hat MPI sehr gute Möglichkeiten, beliebige Daten aus dem Speicher zu ziehen und sie zu einer Reihe von Prozessoren zu streuen / zu sammeln. Leider erfordert dies eine große Anzahl von MPI-Konzepten - MPI-Typen, Extents und kollektive Operationen. Viele der grundlegenden Ideen werden in der Antwort auf diese Frage diskutiert - MPI_Type_create_suarray und MPI_Gather .
Betrachten Sie ein globales 1d-Integer-Array, das Aufgabe 0 hat, das Sie an eine Anzahl von MPI-Tasks verteilen möchten, so dass sie jeweils ein Stück in ihrem lokalen Array erhalten. Angenommen, Sie haben 4 Aufgaben und das globale Array ist [0,1,2,3,4,5,6,7]. Sie könnten Aufgabe 0 vier Nachrichten (einschließlich einer an sich selbst) senden lassen, um diese zu verteilen, und wenn es Zeit ist, sie wieder zusammenzusetzen, erhalten Sie vier Nachrichten, um sie wieder zu bündeln. aber das wird bei einer großen Anzahl von Prozessen offensichtlich sehr zeitaufwendig. Für diese Art von Operationen gibt es optimierte Routinen - Scatter / Gather-Operationen. Also würden Sie in diesem Fall so etwas tun:
%Vor%Danach würden die Daten der Prozessoren aussehen wie
%Vor%Das heißt, die Streuoperation nimmt das globale Array und sendet zusammenhängende 2-int-Chunks an alle Prozessoren.
Um das Array neu zu assemblieren, verwenden wir die Operation MPI_Gather (), die genauso funktioniert, aber umgekehrt:
%Vor%Und jetzt sehen die Arrays wie folgt aus:
%Vor%Gather bringt alle Daten zurück.
Was passiert, wenn die Anzahl der Datenpunkte die Anzahl der Prozesse nicht gleichmäßig verteilt und wir für jeden Prozess eine unterschiedliche Anzahl von Elementen senden müssen? Dann benötigen Sie eine verallgemeinerte Version von Scatter, MPI_Scatterv
, die Hier können Sie die Anzahl für jeden Prozessor und die Verschiebungen angeben - wobei im globalen Array dieses Datenelement beginnt. Nehmen wir an, mit den gleichen 4 Aufgaben hätten Sie eine Reihe von Zeichen [a, b, c, d, e, f, g, h, i] mit 9 Zeichen, und Sie würden jedem Prozess zwei Zeichen außer dem letzten zuweisen , das hat drei. Dann würdest du
Jetzt sehen die Daten wie
aus %Vor%Sie haben jetzt scatterv verwendet, um die unregelmäßigen Datenmengen zu verteilen. Die Verschiebung in jedem Fall ist zwei * Rang (gemessen in Zeichen; die Verschiebung ist in der Einheit der Typen gesendet für eine Streuung oder erhalten für eine Versammlung; es ist nicht allgemein in Bytes oder etwas) vom Start des Arrays, und die Zählungen sind [2,2,2,3]. Wenn es der erste Prozessor gewesen wäre, bei dem wir 3 Zeichen haben wollten, hätten wir Zählimpulse = [3,2,2,2] gesetzt und Verschiebungen wären [0,3,5,7] gewesen. Gatherv arbeitet wieder genauso, aber umgekehrt; Die Arrays "counts" und "displs" würden gleich bleiben.
Nun, für 2D ist das ein bisschen schwieriger. Wenn wir 2d-Sublocks eines 2d-Arrays senden wollen, sind die Daten, die wir jetzt senden, nicht mehr zusammenhängend. Wenn wir 3x3 Unterblöcke eines 6x6-Arrays an 4 Prozessoren senden, enthalten die Daten, die wir senden, Löcher:
%Vor%(Beachten Sie, dass bei allen Hochleistungsrechnern nur das Layout der Daten im Speicher verstanden wird.)
Wenn wir die mit "1" markierten Daten an Aufgabe 1 senden wollen, müssen wir drei Werte überspringen, drei Werte senden, drei Werte überspringen, drei Werte senden, drei Werte überspringen, drei Werte senden. Eine zweite Komplikation ist, wo die Subregionen aufhören und anfangen; Beachten Sie, dass Region "1" nicht beginnt, wo Region "0" stoppt; Nach dem letzten Element der Region "0" befindet sich der nächste Ort im Speicher auf halbem Weg durch die Region "1".
Lassen Sie uns zuerst das erste Layoutproblem angehen - wie man nur die Daten herauszieht, die wir senden wollen. Wir könnten immer nur alle "0" Regionsdaten in ein anderes, zusammenhängendes Array kopieren und senden; Wenn wir es sorgfältig genug planen würden, könnten wir das sogar so machen, dass wir MPI_Scatter über die Ergebnisse aufrufen könnten. Aber wir müssen unsere gesamte Hauptdatenstruktur nicht so transponieren.
Bisher sind alle von uns verwendeten MPI-Datentypen einfach - MPI_INTEGER gibt (sagen wir) 4 Bytes hintereinander an.Mit MPI können Sie jedoch eigene Datentypen erstellen, die beliebig komplexe Datenlayouts im Speicher beschreiben. Und dieser Fall - rechteckige Teilbereiche eines Arrays - ist häufig genug, dass da ist spezifische Aufforderung dafür . Für den 2-dimensionalen Fall, den wir oben beschreiben,
%Vor%Dies erzeugt einen Typ, der nur die Region "0" aus dem globalen Array auswählt. Beachten Sie, dass selbst in Fortran der Startparameter als offset (z. B. 0-basiert) vom Start des Arrays und nicht als Index (z. B. 1-basiert) angegeben wird.
Wir könnten jetzt gerade diese Daten an einen anderen Prozessor senden
%Vor%und der empfangende Prozess könnte es in einem lokalen Array empfangen. Beachten Sie, dass der empfangende Prozess, wenn er nur in einem 3x3-Array empfangen wird, nicht beschreiben kann, was er als eine Art newtype erhält; das beschreibt nicht mehr das Speicherlayout, da zwischen dem Ende einer Zeile und dem Beginn der nächsten keine großen Überspringungen vorhanden sind. Stattdessen erhält es nur einen Block von 3 * 3 = 9 ganzen Zahlen:
%Vor%Beachten Sie, dass wir dies auch für andere Unterregionen tun könnten, indem Sie entweder einen anderen Typ (mit anderem Start-Array) für die anderen Blöcke erstellen oder indem Sie ab der ersten Position des bestimmten Blocks senden:
%Vor%Jetzt, da wir verstehen, wie man Subregionen spezifiziert, gibt es nur noch eine Sache, die man diskutieren sollte, bevor man Scatter / Gather-Operationen verwendet, und das ist die "Größe" dieser Typen. Wir könnten MPI_Scatter () (oder sogar scatterv) nicht einfach mit diesen Typen verwenden, da diese Typen eine Ausdehnung von 15 ganzen Zahlen haben; Das heißt, wo sie enden, sind 15 ganze Zahlen, nachdem sie beginnen - und wo sie enden nicht gut mit dem nächsten Block beginnen, also können wir nicht einfach Streuung verwenden - es würde den falschen Ort auswählen, um mit dem Senden von Daten zu beginnen zum nächsten Prozessor.
Natürlich könnten wir MPI_Scatterv () verwenden und die Verschiebungen selbst bestimmen, und das werden wir tun - außer dass die Verschiebungen in Einheiten der Größe des Sendetyps liegen, und das hilft uns auch nicht; Die Blöcke beginnen bei Offsets von (0,3,18,21) ganzen Zahlen vom Anfang des globalen Arrays, und die Tatsache, dass ein Block 15 ganze Zahlen von dem Punkt an, an dem er beginnt, lässt uns diese Verschiebungen nicht in ganzzahligen Vielfachen ausdrücken .
Um dies zu umgehen, können Sie mit MPI den Umfang des Typs für diese Berechnungen festlegen. Der Typ wird nicht abgeschnitten. Es wird nur verwendet, um herauszufinden, wo das nächste Element mit dem letzten Element beginnt. Für Typen wie diese mit Löchern in ihnen, ist es oft praktisch, die Ausdehnung auf etwas kleiner als die Distanz im Speicher zum tatsächlichen Ende des Typs zu setzen.
Wir können den Umfang so festlegen, dass er für uns praktisch ist. Wir könnten einfach die Ganzzahl extent 1 setzen und dann die Verschiebungen in Einheiten von ganzen Zahlen setzen. In diesem Fall setze ich den Umfang jedoch gerne auf 3 ganze Zahlen - die Größe einer Unterspalte -, so beginnt der Block "1" unmittelbar nach dem Block "0" und der Block "3" beginnt unmittelbar nach dem Block. " 2 ". Leider funktioniert es nicht ganz so gut, wenn man von Block "2" zu Block "3" springt, aber das kann nicht geholfen werden.
Um die Unterblöcke in diesem Fall zu streuen, machen wir folgendes:
%Vor% Hier haben wir den gleichen Blocktyp wie zuvor erstellt, aber wir haben die Größe geändert; Wir haben nicht geändert, wo der Typ "startet" (die 0), aber wir haben geändert, wo es "endet" (3 ganze Zahlen). Wir haben dies vorher nicht erwähnt, aber die MPI_Type_commit
wird benötigt, um den Typ verwenden zu können; Sie müssen jedoch nur den endgültigen Typ festlegen, den Sie tatsächlich verwenden, keine Zwischenschritte. Sie verwenden MPI_Type_free
, um den Committed-Typ freizugeben, wenn Sie fertig sind.
Nun können wir endlich die Blöcke zerstreuen: Die obigen Datenmanipulationen sind ein wenig kompliziert, aber sobald es fertig ist, sieht das scatterv genau wie vorher aus:
%Vor%Und jetzt sind wir fertig, nach einer kleinen Tour durch Streuung, sammeln und MPI abgeleitete Typen.
Es folgt ein Beispielcode, der sowohl die Erfassung als auch die Streuoperation mit Zeichenfeldern zeigt. Ausführen des Programms:
%Vor%und der Code folgt:
%Vor%Das ist also die allgemeine Lösung. Für Ihren speziellen Fall, wo wir nur durch Reihen anhängen, brauchen wir keine Gatherv, wir können einfach eine Gather verwenden, weil in diesem Fall alle Verschiebungen die gleichen sind - vorher, im 2d Block Fall wir hatte eine Verschiebung "nach unten" gehen, und springt dann in dieser Verschiebung, als Sie "über" zur nächsten Spalte von Blöcken ging. Hier ist die Verschiebung immer eine Ausdehnung von der vorherigen, so dass wir Verschiebungen nicht explizit angeben müssen. Ein abschließender Code sieht also so aus:
%Vor%Wenn Sie dies mit 3 Prozessen ausführen, erhalten Sie:
%Vor%