Trasferimento di array / classi / record tra locali

Dec 26 2020

In una tipica simulazione N-Body, alla fine di ogni epoca, ogni località dovrebbe condividere la propria porzione di mondo (cioè tutti i corpi) con il resto delle località. Sto lavorando su questo con un approccio di visualizzazione locale (cioè utilizzando on Locdichiarazioni). Ho riscontrato alcuni comportamenti strani da cui non riuscivo a dare un senso, quindi ho deciso di creare un programma di test, in cui le cose si complicassero. Ecco il codice per replicare l'esperimento.

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.

}

Qui avvengono due comportamenti interessanti.

1. Spostamento dei dati

Ora, ogni istanza di wrapperin array wrappersdovrebbe risiedere nella sua locale. Specificamente, i riferimenti ( wrappers) vivranno in locale 0, ma i dati interni ( r, c, a) dovrebbero vivere nel rispettivo locale. Quindi proviamo a spostarne alcuni dalla locale 1 alla locale 3, come tale:

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

Sorprendentemente, i miei tempi lo dimostrano

  1. Indipendentemente dalla dimensione ( const max), il tempo di invio dell'array e della registrazione è costante, il che non ha senso per me. Ho anche controllato chplvise la dimensione di GETaumenta effettivamente, ma il tempo rimane lo stesso.

  2. Il tempo per inviare il campo della classe aumenta con il tempo, il che ha senso, ma è piuttosto lento e non so in quale caso fidarmi qui.

2. Interrogare direttamente le impostazioni locali.

Per demistificare il problema, interrogo .locale.iddirettamente anche il di alcune variabili. Per prima cosa, interroghiamo i dati, che ci aspettiamo vivano nella locale 2, dalla 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
    );
}

E il risultato è:

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

Che è previsto. Tuttavia, se interroghiamo la lingua degli stessi dati sulla lingua 1, otteniamo:

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

Il che implica che wrappers_ref.r[1].x1.locale.idrisiede nella locale 1, anche se dovrebbe chiaramente essere nella lingua 2 . La mia unica ipotesi è che nel momento in cui .locale.idviene eseguito, i dati (ovvero .xil record) siano già stati spostati nella lingua della query (1).

Quindi, tutto sommato, la seconda parte dell'esperimento porta a una domanda secondaria, pur non rispondendo alla prima parte.


NOTA: tutti i esperimento sono gestiti con -nl 4in chapel/chapel-gasnetimmagine finestra mobile.

Risposte

4 Brad Dec 27 2020 at 23:49

Buone osservazioni, vediamo se riesco a far luce.

Come nota iniziale, qualsiasi tempistica presa con l'immagine Docker di gasnet dovrebbe essere presa con le pinze poiché tale immagine simula l'esecuzione su più nodi utilizzando il sistema locale anziché eseguire ciascuna locale sul proprio nodo di calcolo come previsto in Chapel. Di conseguenza, è utile per lo sviluppo di programmi di memoria distribuita, ma è probabile che le caratteristiche delle prestazioni siano molto diverse rispetto all'esecuzione su un cluster o un supercomputer effettivo. Detto questo, può ancora essere utile per ottenere tempi grossolani (ad esempio, la tua osservazione "ci vuole molto più tempo") o per contare le comunicazioni usando chplviso il modulo CommDiagnostics .

Per quanto riguarda le tue osservazioni sui tempi, osservo anche che il caso array-of-class è molto più lento e credo di poter spiegare alcuni comportamenti:

Innanzitutto, è importante capire che qualsiasi comunicazione tra nodi può essere caratterizzata utilizzando una formula come alpha + beta*length. Pensa alphaa rappresentare il costo di base per eseguire la comunicazione, indipendentemente dalla lunghezza. Questo rappresenta il costo della chiamata attraverso lo stack software per accedere alla rete, mettere i dati sul cavo, riceverli dall'altra parte e riportarli indietro attraverso lo stack software all'applicazione lì. Il valore preciso di alpha dipenderà da fattori come il tipo di comunicazione, la scelta dello stack software e l'hardware fisico. Nel frattempo, pensa a betacome rappresentare il costo per byte della comunicazione in cui, come intuisci, messaggi più lunghi costano necessariamente di più perché ci sono più dati da mettere in rete, o potenzialmente da memorizzare o copiare, a seconda di come la comunicazione è implementata.

Nella mia esperienza, il valore di alphatipicamente domina betaper la maggior parte delle configurazioni di sistema. Questo non vuol dire che sia libero di eseguire trasferimenti di dati più lunghi, ma che la varianza nel tempo di esecuzione tende ad essere molto più piccola per trasferimenti più lunghi rispetto a quelli più brevi rispetto a quando si esegue un singolo trasferimento rispetto a molti. Di conseguenza, quando scegli tra l'esecuzione di un trasferimento di nelementi e il ntrasferimento di 1 elemento, vorrai quasi sempre il primo.

Per esaminare i tuoi tempi, ho messo tra parentesi le tue porzioni di codice temporizzato con le chiamate al CommDiagnosticsmodulo come segue:

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

e ho scoperto, come hai fatto con chplvis, che il numero di comunicazioni richieste per localizzare l'array di record o l'array di int era costante al variare max, ad esempio:

locale ottenere execute_on
0 0 0
1 0 0
2 0 0
3 21 1

Ciò è coerente con ciò che mi aspetterei dall'implementazione: che per un array di tipi di valore, eseguiamo un numero fisso di comunicazioni per accedere ai metadati dell'array e quindi comunichiamo gli elementi dell'array stessi in un singolo trasferimento di dati per ammortizzare il spese generali (evitare di pagare più alphacosti).

Al contrario, ho scoperto che il numero di comunicazioni per la localizzazione dell'array di classi era proporzionale alla dimensione dell'array. Ad esempio, per il valore predefinito di 50.000 per max, ho visto:

locale ottenere mettere execute_on
0 0 0 0
1 0 0 0
2 0 0 0
3 25040 25000 1

Credo che la ragione di questa distinzione sia legata al fatto che cè un array di ownedclassi, in cui solo una singola variabile di classe può "possedere" un dato ctuffoggetto alla volta. Di conseguenza, quando si copiano gli elementi di array cda una locale a un'altra, non si copiano solo i dati grezzi, come con i casi record e interi, ma si esegue anche un trasferimento di proprietà per elemento. Ciò richiede essenzialmente l'impostazione del valore remoto su nildopo aver copiato il suo valore nella variabile di classe locale. Nella nostra attuale implementazione, questo sembra essere fatto utilizzando un telecomando getper copiare il valore della classe remota su quello locale, seguito da un telecomando putsu cui impostare il valore remoto nil, quindi abbiamo un elemento get e put per array, risultante in O (n) comunicazioni anziché O (1) come nei casi precedenti. Con uno sforzo aggiuntivo, potremmo potenzialmente fare in modo che il compilatore ottimizzi questo caso, anche se credo che sarà sempre più costoso degli altri a causa della necessità di eseguire il trasferimento di proprietà.

Ho testato l'ipotesi che le ownedclassi risultassero in un sovraccarico aggiuntivo modificando i tuoi ctuffoggetti da essere owneda unmanaged, il che rimuove qualsiasi semantica di proprietà dall'implementazione. Quando lo faccio, vedo un numero costante di comunicazioni, come nei casi di valore:

locale ottenere execute_on
0 0 0
1 0 0
2 0 0
3 21 1

Credo che questo rappresenti il ​​fatto che una volta che il linguaggio non ha bisogno di gestire la proprietà delle variabili di classe, può semplicemente trasferire di nuovo i valori del puntatore in un unico trasferimento.

Oltre a queste note sulle prestazioni, è importante comprendere una differenza semantica chiave tra classi e record quando si sceglie quale utilizzare. Un oggetto di classe viene allocato sull'heap e una variabile di classe è essenzialmente un riferimento o un puntatore a quell'oggetto. Pertanto, quando una variabile di classe viene copiata da una lingua a un'altra, viene copiato solo il puntatore e l'oggetto originale rimane dov'era (nel bene o nel male). Al contrario, una variabile record rappresenta l'oggetto stesso e può essere pensata come allocata "sul posto" (ad esempio, sullo stack per una variabile locale). Quando una variabile record viene copiata da una locale all'altra, è l'oggetto stesso (cioè i valori dei campi del record) che viene copiato, risultando in una nuova copia dell'oggetto stesso. Vedi questa domanda SO per ulteriori dettagli.

Passando alla tua seconda osservazione, credo che la tua interpretazione sia corretta e che questo potrebbe essere un bug nell'implementazione (devo soffermarmi un po 'di più per essere sicuro). In particolare, penso che tu abbia ragione sul fatto che ciò che sta accadendo è che wrappers_ref.r[1].x1viene valutato, con il risultato memorizzato in una variabile locale e che la .locale.idquery viene applicata alla variabile locale che memorizza il risultato piuttosto che il campo originale. Ho testato questa teoria portando refa sul campo e poi stampando locale.idquel riferimento, come segue:

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

e questo sembrava dare il giusto risultato. Ho anche esaminato il codice generato che sembrava indicare che le nostre teorie erano corrette. Non credo che l'implementazione debba comportarsi in questo modo, ma è necessario pensarci un po 'di più prima di essere fiducioso. Se desideri aprire un bug contro questo problema nella pagina dei problemi di GitHub di Chapel , per ulteriori discussioni lì, lo apprezzeremmo.