Come si confronta il costo computazionale di un'operazione mpi_allgather con un'operazione di raccolta / dispersione?


11

Sto lavorando a un problema che può essere parallelizzato utilizzando una singola operazione mpi_allgather o un'operazione mpi_scatter e un'operazione mpi_gather. Queste operazioni vengono chiamate in un ciclo while, quindi possono essere chiamate più volte.

Nell'implementazione con uno schema MPI_allgather, sto raccogliendo un vettore distribuito su tutti i processi per la risoluzione di matrici duplicate. Nell'altra implementazione, raccolgo il vettore distribuito su un singolo processore (il nodo principale), risolvo il sistema lineare su questo processore e quindi disperdo il vettore della soluzione su tutti i processi.

Sono curioso di sapere se il costo di un'operazione di allgather è significativamente maggiore delle operazioni di dispersione e raccolta combinate. La lunghezza del messaggio gioca un ruolo significativo nella sua complessità? Varia tra le implementazioni di mpi?

Modificare:


Descrivi la struttura della comunicazione e le dimensioni coinvolte. Un MPI_Scatterseguito da MPI_Gathernon fornisce la stessa comunicazione semantica di MPI_Allgather. Forse c'è ridondanza quando si esprime l'operazione in entrambi i modi?
Jed Brown,

Paul, Jed ha ragione, intendevi un MPI_Gatherseguito da un MPI_Bcast?
Aron Ahmadia,

@JedBrown: ho aggiunto qualche informazione in più.
Paolo

@AronAhmadia: Non penso che dovrei usare un MPI_Bcast perché sto inviando una porzione del vettore, a ciascun processo, non l'intero vettore. La mia logica è che un messaggio più breve sarà più veloce da inviare rispetto a un messaggio più grande, in generale. Questo ha senso?
Paolo

La matrice è già distribuita in modo ridondante? È già preso in considerazione? Più processi condividono le stesse cache e bus di memoria? (Ciò influirebbe sulla velocità di risoluzione dei sistemi ridondanti.) Quanto sono grandi / costosi i sistemi? Perché risolvere in serie?
Jed Brown,

Risposte:


9

Innanzitutto, la risposta esatta dipende da: (1) utilizzo, ovvero argomenti di input della funzione, (2) qualità e dettagli dell'implementazione MPI e (3) l'hardware che si sta utilizzando. Spesso, (2) e (3) sono correlati, ad esempio quando il fornitore di hardware ottimizza MPI per la propria rete.

In generale, la fusione dei collettivi di MPI è migliore per i messaggi più piccoli, poiché i costi di avvio possono essere non banali e la sincronizzazione comportata dal blocco dei collettivi dovrebbe essere ridotta al minimo se si verificano variazioni nel tempo di calcolo tra le chiamate. Per i messaggi più grandi, l'obiettivo dovrebbe essere quello di ridurre al minimo la quantità di dati inviati.

Ad esempio, in teoria, MPI_Reduce_scatter_blockdovrebbe essere migliore di quello MPI_Reduceseguito MPI_Scatter, sebbene il primo sia spesso implementato in termini di secondo, in modo tale che non vi sia alcun vantaggio reale. Esiste una correlazione tra qualità dell'implementazione e frequenza d'uso nella maggior parte delle implementazioni di MPI, e ovviamente i fornitori ottimizzano quelle funzioni per le quali ciò è richiesto dal contratto con la macchina.

D'altra parte, se uno è su un Blue Gene, facendo MPI_Reduce_scatter_blockuso MPI_Allreduce, che fa più la comunicazione di MPI_Reducee MPI_Scattercombinati, in realtà è piuttosto un po 'più veloce. Questo è qualcosa che ho scoperto di recente ed è un'interessante violazione del principio di auto-coerenza delle prestazioni in MPI (questo principio è descritto più dettagliatamente in "Linee guida per le prestazioni MPI auto-coerenti" ).

Nel caso specifico di scatter + gather contro allgather, considera che nel primo, tutti i dati devono andare da e verso un singolo processo, il che ne fa il collo di bottiglia, mentre nell'allgather i dati possono fluire dentro e fuori da tutti i ranghi immediatamente , poiché tutti i gradi hanno alcuni dati da inviare a tutti gli altri gradi. Tuttavia, l'invio di dati da tutti i nodi contemporaneamente non è necessariamente una buona idea su alcune reti.

Infine, il modo migliore per rispondere a questa domanda è eseguire le seguenti operazioni nel codice e rispondere alla domanda mediante esperimento.

#ifdef TWO_MPI_CALLS_ARE_BETTER_THAN_ONE
  MPI_Scatter(..)
  MPI_Gather(..)
#else
  MPI_Allgather(..)
#endif

Un'opzione ancora migliore è che il tuo codice lo misuri sperimentalmente durante le prime due iterazioni, quindi usa quello che è più veloce per le restanti iterazioni:

const int use_allgather = 1;
const int use_scatter_then_gather = 2;

int algorithm = 0;
double t0 = 0.0, t1 = 0.0, dt1 = 0.0, dt2 = 0.0;

while (..)
{
    if ( (iteration==0 && algorithm==0) || algorithm==use_scatter_then_gather )
    {
        t0 = MPI_Wtime();
        MPI_Scatter(..);
        MPI_Gather(..);
        t1 = MPI_Wtime();
        dt1 = t1-t0;
    } 
    else if ( (iteration==1 && algorithm==0) || algorithm==use_allgather)
    {
        t0 = MPI_Wtime();
        MPI_Allgather(..);
        t1 = MPI_Wtime();
        dt2 = t1-t0;
    }

    if (iteration==1)
    {
       dt2<dt1 ? algorithm=use_allgather : algorithm=use_scatter_then_gather;
    }
}

Non è una cattiva idea ... cronometra entrambi e determina quale è più veloce.
Paolo

La maggior parte dell'hardware degli ambienti HPC moderni ottimizza molte chiamate MPI. A volte questo porta a incredibili accelerazioni, altre volte a comportamenti estremamente opachi. Stai attento!
meawoppl

@Jeff: mi sono appena reso conto di aver lasciato fuori un dettaglio importante ... Sto lavorando con un cluster presso il Texas Advanced Computing Center, dove usano una rete di topologia ad albero grasso. Ciò influirebbe sulla differenza di prestazioni tra gli approcci di raccolta e trasmissione?
Paolo

@Paul La topologia non è il fattore dominante qui, ma un albero grasso ha una larghezza di banda di bisection sostanziale, che dovrebbe rendere il tutto economico. Tuttavia, raccogliere dovrebbe essere sempre più economico di Allgather. Per i messaggi più grandi, tuttavia, potrebbe essere inferiore a un fattore 2.
Jeff

5

Jeff ha assolutamente ragione sul fatto che l'unico modo per essere sicuri è misurare - dopo tutto siamo scienziati, e questa è una domanda empirica - e fornisce ottimi consigli su come implementare tali misurazioni. Consentitemi ora di offrire una visione contraria (o forse complementare).

C'è una distinzione da fare tra la scrittura di un codice da utilizzare ampiamente e la regolazione di un fine specifico. In generale, stiamo realizzando il primo - costruendo il nostro codice in modo che a) possiamo utilizzarlo su un'ampia varietà di piattaforme eb) il codice sia mantenibile ed estendibile per gli anni a venire. Ma a volte stiamo facendo l'altro: abbiamo un anno di allocazione su una grande macchina, e ci stiamo arrampicando su alcune serie di simulazioni di grandi dimensioni e abbiamo bisogno di una certa base di prestazioni per ottenere ciò di cui abbiamo bisogno durante il tempo dell'assegnazione concessa.

Quando stiamo scrivendo codice, renderlo ampiamente utilizzabile e gestibile è molto più importante della rasatura di qualche percento del tempo di esecuzione su una determinata macchina. In questo caso, la cosa giusta da fare è quasi sempre di usare la routine che meglio descrive ciò che vuoi fare: questa è generalmente la chiamata più specifica che puoi fare che fa quello che vuoi. Ad esempio, se un allgather dritto o un allgatherv fa quello che vuoi, dovresti usarlo piuttosto che tirare fuori le tue operazioni scatter / gatter. Le ragioni sono che:

  • Il codice ora rappresenta più chiaramente ciò che stai cercando di fare, rendendolo più comprensibile per la persona successiva che verrà al tuo codice l'anno successivo senza avere idea di cosa dovrebbe fare il codice (quella persona potresti essere tu);
  • Sono disponibili ottimizzazioni a livello di MPI per questo caso più specifico che non sono nel caso più generale, quindi la tua libreria MPI può aiutarti; e
  • Cercare di lanciare il tuo probabilmente si ritorcerà contro; anche se funziona meglio sulla macchina X con l'implementazione MPI Y.ZZ, potrebbe comportare molto peggio quando ci si sposta su un altro computer o si aggiorna l'implementazione MPI.

In questo caso abbastanza comune, se scopri che alcuni collettivi MPI funzionano irragionevolmente lentamente sul tuo computer, la cosa migliore da fare è presentare una segnalazione di bug al fornitore di mpi; non vuoi complicare il tuo software cercando di aggirare il codice dell'applicazione, cosa dovrebbe essere corretto a livello di libreria MPI.

Tuttavia . Se sei in modalità "tuning" - hai un codice funzionante, devi salire su scale molto grandi in un breve periodo di tempo (ad esempio, un'assegnazione annuale) e hai profilato il tuo codice e ho scoperto che questa particolare parte del codice è un collo di bottiglia, quindi ha senso iniziare a eseguire queste accordature molto specifiche. Speriamo che non facciano parte del tuo codice a lungo termine - idealmente queste modifiche rimarranno in qualche ramo specifico del progetto del tuo repository - ma potresti doverle fare. In tal caso, la codifica di due diversi approcci distinti dalle direttive del preprocessore o un approccio di "autotuning" per un modello di comunicazione specifico - può avere molto senso.

Quindi non sono in disaccordo con Jeff, voglio solo aggiungere un po 'di contesto su quando dovresti preoccuparti abbastanza di tali domande relative alle prestazioni da modificare il tuo codice per affrontarlo.


Penso di essere più interessato alla portabilità che all'ottimizzazione a questo punto, ma sono sempre curioso di sapere se esiste un'altra implementazione che sia ugualmente portatile ma più veloce :)
Paul
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.