Przenoszenie tablic / klas / rekordów między lokalizacjami

Dec 26 2020

W typowej symulacji N-Body, pod koniec każdej epoki, każda lokalizacja musiałaby dzielić swoją własną część świata (tj. Wszystkie ciała) z pozostałymi lokalizacjami. Pracuję nad tym z podejściem lokalnego widoku (tj. Używając on Locinstrukcji). Spotkałem się z dziwnymi zachowaniami, z których nie mogłem zrozumieć, więc zdecydowałem się zrobić program testowy, w którym sprawy się skomplikowały. Oto kod do powtórzenia eksperymentu.

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.

}

Zachodzą tu dwa ciekawe zachowania.

1. Przenoszenie danych

Teraz każda instancja wrapperin array wrapperspowinna istnieć w swoim locale. W szczególności, (odniesienia wrappers) przeżyje w lokalizacji 0, ale dane wewnętrzne ( r, c, a) powinien być w danej lokalizacji. Więc staramy się przenieść część z locale 1 do locale 3, na przykład:

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());
}

O dziwo, moje czasy to pokazują

  1. Niezależnie od size ( const max), czas wysłania tablicy i rekordu jest stały, co nie ma dla mnie sensu. Sprawdziłem nawet u chplvisi rozmiar GETfaktycznie rośnie, ale czas pozostaje taki sam.

  2. Czas wysłania pola klasy wydłuża się z czasem, co ma sens, ale jest dość powolny i nie wiem, któremu przypadku tutaj ufać.

2. Bezpośrednie zapytania o lokalizacje.

Aby wyjaśnić problem, sprawdzam również .locale.idbezpośrednio niektóre zmienne. Najpierw sprawdzamy dane, które spodziewamy się żyć w locale 2, z locale 2:

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
    );
}

A wynik jest taki:

[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)

Co jest oczekiwane. Jeśli jednak zapytamy o ustawienia regionalne tych samych danych w locale 1, otrzymamy:

[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)

Oznacza to, że wrappers_ref.r[1].x1.locale.idżyje w języku 1, mimo że powinno być wyraźnie w języku 2 . Domyślam się tylko, że do czasu .locale.idwykonania dane (tj .x. Rekord) są już przeniesione do lokalizacji zapytania (1).

Podsumowując, druga część eksperymentu prowadzi do pytania drugorzędnego, nie udzielając odpowiedzi na część pierwszą.


UWAGA: wszystkie eksperymenty są uruchamiane -nl 4w chapel/chapel-gasnetobrazie dockera.

Odpowiedzi

4 Brad Dec 27 2020 at 23:49

Dobre obserwacje, zobaczę, czy mogę rzucić trochę światła.

Na wstępie należy zauważyć, że wszelkie czasy zrobione za pomocą obrazu Gasnet Docker powinny być traktowane z przymrużeniem oka, ponieważ ten obraz symuluje wykonanie na wielu węzłach przy użyciu lokalnego systemu, a nie uruchamia każde ustawienie regionalne na własnym węźle obliczeniowym, zgodnie z zamierzeniami w Chapel. W rezultacie jest to przydatne do tworzenia programów z pamięcią rozproszoną, ale charakterystyka wydajności może być bardzo różna niż w przypadku rzeczywistego klastra lub superkomputera. To powiedziawszy, nadal może być przydatne do uzyskiwania zgrubnych czasów (np. Obserwacja „to trwa znacznie dłużej”) lub do zliczania komunikacji za pomocą chplvislub modułu CommDiagnostics .

W odniesieniu do twoich obserwacji dotyczących czasów, zauważyłem również, że przypadek tablicy klas jest znacznie wolniejszy i wydaje mi się, że mogę wyjaśnić niektóre z zachowań:

Po pierwsze, ważne jest, aby zrozumieć, że każdą komunikację między węzłami można scharakteryzować za pomocą wzoru alpha + beta*length. Pomyśl o alphapodstawowym koszcie wykonania komunikacji, niezależnie od długości. Stanowi to koszt wywołania przez stos oprogramowania w celu uzyskania dostępu do sieci, umieszczenia danych w kablu, odebrania ich z drugiej strony i przesłania ich z powrotem przez stos oprogramowania do tamtejszej aplikacji. Dokładna wartość alfa będzie zależeć od takich czynników, jak typ komunikacji, wybór stosu oprogramowania i fizyczny sprzęt. W międzyczasie pomyśl o tym, że betareprezentuje koszt komunikacji w przeliczeniu na bajt, gdzie, jak intuicyjnie, dłuższe wiadomości z konieczności kosztują więcej, ponieważ jest więcej danych do przesłania lub potencjalnie do buforowania lub kopiowania, w zależności od tego, jak komunikacja jest zaimplementowana.

Z mojego doświadczenia wynika, że w większości konfiguracji systemowych wartość alphazazwyczaj dominuje beta. Nie oznacza to, że dłuższe transfery danych są darmowe, ale rozbieżność w czasie wykonania jest zwykle znacznie mniejsza w przypadku transferów dłuższych w porównaniu z krótszymi niż w przypadku pojedynczego transferu w porównaniu z wieloma. W rezultacie, wybierając między wykonaniem jednego transferu npierwiastków a ntransferem 1 elementu, prawie zawsze będziesz chciał tego pierwszego.

Aby zbadać czasy, ująłem fragmenty kodu czasowego z wywołaniami CommDiagnosticsmodułu w następujący sposób:

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

i stwierdziłeś, tak jak to zrobiłeś chplvis, że liczba komunikacji wymagana do zlokalizowania tablicy rekordów lub tablicy int jest stała, tak jak zmieniałem max, na przykład:

widownia otrzymać execute_on
0 0 0
1 0 0
2 0 0
3 21 1

Jest to zgodne z tym, czego oczekiwałbym od implementacji: że w przypadku tablicy typów wartości wykonujemy ustaloną liczbę komunikatów w celu uzyskania dostępu do metadanych tablicy, a następnie przekazujemy same elementy tablicy w pojedynczym transferze danych, aby zamortyzować koszty ogólne (unikaj płacenia wielu alphakosztów).

W przeciwieństwie do tego stwierdziłem, że liczba komunikatów służących do lokalizacji tablicy klas była proporcjonalna do rozmiaru tablicy. Na przykład dla domyślnej wartości 50 000 dla maxzobaczyłem:

widownia otrzymać położyć execute_on
0 0 0 0
1 0 0 0
2 0 0 0
3 25040 25000 1

Uważam, że powodem tego rozróżnienia jest fakt, że cjest to tablica ownedklas, w której tylko jedna zmienna klasy może „posiadać” dany ctuffobiekt na raz. W rezultacie podczas kopiowania elementów tablicy cz jednego ustawienia regionalnego do drugiego nie tylko kopiujesz surowe dane, jak w przypadku rekordów i liczb całkowitych, ale także wykonujesz przeniesienie własności na element. Zasadniczo wymaga to ustawienia zdalnej wartości nilpo skopiowaniu jej wartości do lokalnej zmiennej klasy. W naszej obecnej implementacji wydaje się, że odbywa się to za pomocą pilota getdo skopiowania wartości klasy zdalnej do lokalnej, a następnie zdalnego putdo ustawienia wartości zdalnej nil, stąd mamy polecenie get and put dla elementu tablicy, co daje O (n) komunikacja zamiast O (1), jak w poprzednich przypadkach. Przy dodatkowym wysiłku moglibyśmy potencjalnie zlecić kompilatorowi optymalizację tego przypadku, chociaż uważam, że zawsze będzie droższy niż inne ze względu na konieczność wykonania przeniesienia własności.

Przetestowałem hipotezę, że ownedklasy powodowały dodatkowe obciążenie, zmieniając ctuffobiekty z bycia ownedna unmanaged, co usuwa semantykę własności z implementacji. Kiedy to robię, widzę stałą liczbę komunikatów, jak w przypadku wartości:

widownia otrzymać execute_on
0 0 0
1 0 0
2 0 0
3 21 1

Uważam, że oznacza to fakt, że gdy język nie będzie musiał zarządzać własnością zmiennych klas, może po prostu ponownie przesłać ich wartości wskaźników w jednym transferze.

Poza tymi uwagami dotyczącymi wydajności ważne jest, aby zrozumieć kluczową różnicę semantyczną między klasami i rekordami przy wyborze tego, którego użyć. Obiekt klasy jest alokowany na stercie, a zmienna klasy jest zasadniczo odwołaniem lub wskaźnikiem do tego obiektu. Tak więc, gdy zmienna klasy jest kopiowana z jednego ustawienia narodowego do innego, kopiowany jest tylko wskaźnik, a oryginalny obiekt pozostaje tam, gdzie był (na dobre lub na złe). Natomiast zmienna rekordu reprezentuje sam obiekt i może być traktowana jako przydzielona „na miejscu” (np. Na stosie zmiennej lokalnej). Kiedy zmienna rekordu jest kopiowana z jednej lokalizacji do drugiej, kopiowany jest sam obiekt (tj. Wartości pól rekordu), w wyniku czego powstaje nowa kopia samego obiektu. Więcej szczegółów można znaleźć w tym pytaniu SO .

Przechodząc do drugiej obserwacji, uważam, że twoja interpretacja jest poprawna i że może to być błąd w implementacji (muszę trochę bardziej się nad tym dusić, aby być pewnym). W szczególności myślę, że masz rację, że to, co się dzieje, wrappers_ref.r[1].x1jest oceniane, a wynik jest przechowywany w zmiennej lokalnej, a .locale.idzapytanie jest stosowane do zmiennej lokalnej przechowującej wynik, a nie do oryginalnego pola. Przetestowałem tę teorię, zabierając a refdo pola, a następnie drukując locale.idten odnośnik w następujący sposób:

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

i wydawało się, że przyniosło to właściwy rezultat. Przyjrzałem się również wygenerowanemu kodowi, który wydawał się wskazywać, że nasze teorie są poprawne. Nie uważam, że implementacja powinna zachowywać się w ten sposób, ale zanim nabieram pewności, muszę się nad tym zastanowić. Jeśli chcesz otworzyć błąd przeciwko temu na stronie problemów Chapel na GitHubie , będziemy wdzięczni za dalszą dyskusję.