Übertragen von Arrays / Klassen / Datensätzen zwischen Gebietsschemas

Dec 26 2020

In einer typischen N-Body-Simulation müsste am Ende jeder Epoche jedes Gebietsschema seinen eigenen Teil der Welt (dh alle Körper) mit den übrigen Gebietsschemas teilen . Ich arbeite daran mit einem lokalen Ansatz (dh unter Verwendung von on LocAnweisungen). Ich stieß auf einige seltsame Verhaltensweisen, aus denen ich keinen Sinn machen konnte, und beschloss, ein Testprogramm zu erstellen, in dem die Dinge komplizierter wurden. Hier ist der Code zum Replizieren des Experiments.

proc log(args...?n) {
    writeln("[locale = ", here.id, "] [", datetime.now(), "] => ", args);
}

const max: int = 50000;
record stuff {
    var x1: int;
    var x2: int;

    proc init() {
        this.x1 = here.id;
        this.x2 = here.id;
    }
}

class ctuff {
    var x1: int;
    var x2: int;

    proc init() {
        this.x1 = here.id;
        this.x2 = here.id;
    }
}

class wrapper {
    // The point is that total size (in bytes) of data in `r`, `c` and `a` are the same here, because the record and the class hold two ints per index.
    var r: [{1..max / 2}] stuff;
    var c: [{1..max / 2}] owned ctuff?;
    var a: [{1..max}] int;

    proc init() {
        this.a = here.id;
    }
}

proc test() {
    var wrappers: [LocaleSpace] owned wrapper?;
    coforall loc in LocaleSpace {
        on Locales[loc] {
            wrappers[loc] = new owned wrapper();
        }
    }

    // rest of the experiment further down.

}

Hier treten zwei interessante Verhaltensweisen auf.

1. Daten verschieben

Jetzt sollte jede Instanz von wrapperin Array wrappersin ihrem Gebietsschema leben. Insbesondere werden die Verweise ( wrapperswird) in locale Live 0, aber die internen Daten ( r, c, a) sollen in den jeweiligen locale leben. Deshalb versuchen wir, einige von Gebietsschema 1 nach Gebietsschema 3 zu verschieben:

on Locales[3] {
    var timer: Timer;
    timer.start();
    var local_stuff = wrappers[1]!.r;
    timer.stop();
    log("get r from 1", timer.elapsed());
    log(local_stuff);
}

on Locales[3] {
    var timer: Timer;
    timer.start();
    var local_c = wrappers[1]!.c;
    timer.stop();
    log("get c from 1", timer.elapsed());
}

on Locales[3] {
    var timer: Timer;
    timer.start();
    var local_a = wrappers[1]!.a;
    timer.stop();
    log("get a from 1", timer.elapsed());
}

Überraschenderweise zeigen meine Timings das

  1. Unabhängig von der Größe ( const max) ist die Zeit für das Senden des Arrays und der Aufzeichnung konstant, was für mich keinen Sinn ergibt. Ich habe sogar nachgefragt chplvis, und die Größe von nimmt GETtatsächlich zu, aber die Zeit bleibt gleich.

  2. Die Zeit zum Senden des Klassenfelds nimmt mit der Zeit zu, was sinnvoll ist, aber es ist ziemlich langsam und ich weiß nicht, welchem ​​Fall ich hier vertrauen soll.

2. Die Gebietsschemas direkt abfragen.

Um das Problem zu entmystifizieren, frage ich auch .locale.ideinige Variablen direkt ab. Zuerst fragen wir die Daten, von denen wir erwarten, dass sie in Gebietsschema 2 leben, aus Gebietsschema 2 ab:

on Locales[2] {
    var wrappers_ref = wrappers[2]!; // This is always 1 GET from 0, okay.
    log("array",
        wrappers_ref.a.locale.id,
        wrappers_ref.a[1].locale.id
    );
    log("record",
        wrappers_ref.r.locale.id,
        wrappers_ref.r[1].locale.id,
        wrappers_ref.r[1].x1.locale.id,
    );
    log("class",
        wrappers_ref.c.locale.id,
        wrappers_ref.c[1]!.locale.id,
        wrappers_ref.c[1]!.x1.locale.id
    );
}

Und das Ergebnis ist:

[locale = 2] [2020-12-26T19:36:26.834472] => (array, 2, 2)
[locale = 2] [2020-12-26T19:36:26.894779] => (record, 2, 2, 2)
[locale = 2] [2020-12-26T19:36:27.023112] => (class, 2, 2, 2)

Welches wird erwartet. Wenn wir jedoch das Gebietsschema derselben Daten in Gebietsschema 1 abfragen, erhalten wir:

[locale = 1] [2020-12-26T19:34:28.509624] => (array, 2, 2)
[locale = 1] [2020-12-26T19:34:28.574125] => (record, 2, 2, 1)
[locale = 1] [2020-12-26T19:34:28.700481] => (class, 2, 2, 2)

Dies impliziert, dass es sich wrappers_ref.r[1].x1.locale.idin Gebietsschema 1 befindet, obwohl es sich eindeutig in Gebietsschema 2 befinden sollte . Ich vermute nur, dass zum Zeitpunkt der .locale.idAusführung die Daten (dh die .xdes Datensatzes) bereits in das abfragende Gebietsschema (1) verschoben wurden.

Alles in allem führte der zweite Teil des Experiments zu einer zweiten Frage, während der erste Teil nicht beantwortet wurde.


HINWEIS: Alle Experimente werden -nl 4im chapel/chapel-gasnetDocker-Image ausgeführt.

Antworten

4 Brad Dec 27 2020 at 23:49

Gute Beobachtungen, lassen Sie mich sehen, ob ich etwas Licht ins Dunkel bringen kann.

Zunächst sollten alle mit dem Gasnet Docker-Image aufgenommenen Timings mit einem Körnchen Salz aufgenommen werden, da dieses Image die Ausführung über mehrere Knoten unter Verwendung Ihres lokalen Systems simuliert, anstatt jedes Gebietsschema auf einem eigenen Rechenknoten auszuführen, wie in Chapel vorgesehen. Infolgedessen ist es nützlich für die Entwicklung verteilter Speicherprogramme, aber die Leistungsmerkmale unterscheiden sich wahrscheinlich stark von denen, die auf einem tatsächlichen Cluster oder Supercomputer ausgeführt werden. Das heißt, es kann immer noch nützlich sein, um grobe Timings zu erhalten (z. B. Ihre Beobachtung "Dies dauert viel länger") oder um die Kommunikation mit chplvisoder dem CommDiagnostics-Modul zu zählen .

In Bezug auf Ihre Beobachtungen zu Timings stelle ich auch fest, dass der Fall der Klassenreihenfolge viel langsamer ist, und ich glaube, ich kann einige der Verhaltensweisen erklären:

Zunächst ist es wichtig zu verstehen, dass jede knotenübergreifende Kommunikation mit einer Formel wie charakterisiert werden kann alpha + beta*length. Stellen Sie sich vor alpha, Sie stellen die Grundkosten für die Durchführung der Kommunikation dar, unabhängig von der Länge. Dies stellt die Kosten dar, die erforderlich sind, um über den Software-Stack zum Netzwerk zu gelangen, die Daten auf das Kabel zu übertragen, sie auf der anderen Seite zu empfangen und über den Software-Stack wieder zur dortigen Anwendung zu übertragen. Der genaue Wert von Alpha hängt von Faktoren wie der Art der Kommunikation, der Wahl des Software-Stacks und der physischen Hardware ab. Inzwischen denkt betaals Vertreter der pro-Byte - Kosten der Kommunikation , wo, wie Sie Intuit, längere Nachrichten unbedingt mehr kosten , weil es mehr Daten ist auf dem Draht zu setzen, oder potentiell zu puffern oder zu kopieren, je nachdem , wie die Kommunikation implementiert ist.

Nach meiner Erfahrung alphadominiert der Wert von normalerweise betafür die meisten Systemkonfigurationen. Das heißt nicht, dass es kostenlos ist, längere Datenübertragungen durchzuführen, aber dass die Varianz in der Ausführungszeit bei längeren und kürzeren Übertragungen tendenziell viel geringer ist als bei einer einzelnen Übertragung im Vergleich zu vielen. Wenn Sie also zwischen einer Übertragung von nElementen und einer nÜbertragung von 1 Element wählen , möchten Sie fast immer die erstere.

Um Ihre Timings zu untersuchen, habe ich Ihre zeitgesteuerten Codeteile mit Aufrufen des CommDiagnosticsModuls wie folgt eingeklammert :

resetCommDiagnostics();
startCommDiagnostics();
...code to time here...
stopCommDiagnostics();
printCommDiagnosticsTable();

und festgestellt, wie Sie es getan haben chplvis, dass die Anzahl der Kommunikationen, die erforderlich sind, um das Array von Datensätzen oder Ints zu lokalisieren, konstant war, als ich variierte max, zum Beispiel:

Gebietsschema erhalten execute_on
0 0 0
1 0 0
2 0 0
3 21 1

Dies steht im Einklang mit dem, was ich von der Implementierung erwarten würde: Für ein Array von Werttypen führen wir eine feste Anzahl von Kommunikationen durch, um auf Array-Metadaten zuzugreifen, und kommunizieren dann die Array-Elemente selbst in einer einzigen Datenübertragung, um die zu amortisieren Gemeinkosten (vermeiden Sie die Zahlung mehrerer alphaKosten).

Im Gegensatz dazu stellte ich fest, dass die Anzahl der Kommunikationen zum Lokalisieren des Arrays von Klassen proportional zur Größe des Arrays war. Zum Beispiel maxsah ich für den Standardwert von 50.000 für :

Gebietsschema erhalten stellen execute_on
0 0 0 0
1 0 0 0
2 0 0 0
3 25040 25000 1

Ich glaube, der Grund für diese Unterscheidung liegt in der Tatsache, dass ces sich um ein Array von ownedKlassen handelt, in denen jeweils nur eine einzelne Klassenvariable ein bestimmtes ctuffObjekt "besitzen" kann . Wenn Sie also die Elemente eines Arrays cvon einem Gebietsschema in ein anderes kopieren, kopieren Sie nicht nur Rohdaten wie in den Fällen "Datensatz" und "Ganzzahl", sondern führen auch eine Eigentumsübertragung pro Element durch. Dies erfordert im Wesentlichen das Setzen des Remote-Werts auf, nilnachdem sein Wert in die lokale Klassenvariable kopiert wurde. In unserer aktuellen Implementierung scheint dies mit einer Fernbedienung zu geschehen, getum den Wert der entfernten Klasse in die lokale zu kopieren, gefolgt von einer Fernbedienung put, um den Wert für die entfernte Klasse festzulegen. nilDaher haben wir ein Get and Put pro Array-Element, was zu O führt (n) Kommunikation anstelle von O (1) wie in den vorherigen Fällen. Mit zusätzlichem Aufwand könnte der Compiler diesen Fall möglicherweise optimieren, obwohl ich glaube, dass er aufgrund der Notwendigkeit, die Eigentumsübertragung durchzuführen, immer teurer sein wird als die anderen.

Ich habe die Hypothese getestet, dass ownedKlassen zu zusätzlichem Overhead führen, indem Sie Ihre ctuffObjekte von "Sein" ownedin " Ändern" ändern unmanaged, wodurch jegliche Besitzersemantik aus der Implementierung entfernt wird. Wenn ich das mache, sehe ich eine konstante Anzahl von Kommunikationen, wie in den Wertefällen:

Gebietsschema erhalten execute_on
0 0 0
1 0 0
2 0 0
3 21 1

Ich glaube, dies stellt die Tatsache dar, dass die Sprache, sobald sie den Besitz der Klassenvariablen nicht mehr verwalten muss, ihre Zeigerwerte einfach in einer einzigen Übertragung erneut übertragen kann.

Über diese Leistungshinweise hinaus ist es wichtig, einen wichtigen semantischen Unterschied zwischen Klassen und Datensätzen zu verstehen, wenn Sie auswählen, welche verwendet werden sollen. Ein Klassenobjekt wird auf dem Heap zugewiesen, und eine Klassenvariable ist im Wesentlichen eine Referenz oder ein Zeiger auf dieses Objekt. Wenn also eine Klassenvariable von einem Gebietsschema in ein anderes kopiert wird, wird nur der Zeiger kopiert, und das ursprüngliche Objekt bleibt dort, wo es war (zum Guten oder Schlechten). Im Gegensatz dazu stellt eine Datensatzvariable das Objekt selbst dar und kann als "an Ort und Stelle" zugewiesen werden (z. B. auf dem Stapel für eine lokale Variable). Wenn eine Datensatzvariable von einem Gebietsschema in das andere kopiert wird, wird das Objekt selbst (dh die Feldwerte des Datensatzes) kopiert, was zu einer neuen Kopie des Objekts selbst führt. Siehe diese SO-Frage für weitere Details.

Wenn Sie zu Ihrer zweiten Beobachtung übergehen, glaube ich, dass Ihre Interpretation korrekt ist und dass dies ein Fehler in der Implementierung sein kann (ich muss etwas mehr darüber schmoren, um sicher zu sein). Insbesondere denke ich, dass Sie Recht haben, dass das, was passiert wrappers_ref.r[1].x1, ausgewertet wird, wobei das Ergebnis in einer lokalen Variablen gespeichert wird und dass die .locale.idAbfrage auf die lokale Variable angewendet wird, in der das Ergebnis und nicht auf das ursprüngliche Feld gespeichert sind. Ich habe diese Theorie getestet, indem ich a refauf das Feld gebracht und dann locale.iddiese Referenz wie folgt gedruckt habe :

ref x1loc = wrappers_ref.r[1].x1;
...wrappers_ref.c[1]!.x1.locale.id...

und das schien das richtige Ergebnis zu liefern. Ich habe mir auch den generierten Code angesehen, der darauf hindeutet, dass unsere Theorien korrekt sind. Ich glaube nicht, dass sich die Implementierung so verhalten sollte, aber ich muss ein bisschen mehr darüber nachdenken, bevor ich zuversichtlich bin. Wenn Sie auf der GitHub-Themenseite von Chapel einen Fehler dagegen öffnen möchten , würden wir uns freuen, wenn Sie dort weitere Diskussionen führen.