Fireball

Node Level Parallelism

Un petit cours de comm ?

Adrien Merlini & Serge Guelton

Systèmes distribués : ils sont partout

Data Center, où, quand, comment, pourquoi

https://www.google.com/about/datacenters

À toutes les échelles

Top 500

https://www.top500.org/

June 1993: Rpeak 131 GFlops, 1,024 Cores, USA June 2003: RPeak 40,960 GFlops, 5,120 Cores, Japan June 2013: RPeak 54,902 GFlops, 3,120,000 Cores, China June 2020: RPeak 513,584 GFlops, 7,299,072 Cores, Japan

Green 500

https://www.top500.org/lists/green500/

June 2013: 3208.8 MFlops/watts June 2020: 21.108 GFlops/watts, 393 in TOP500

SETI@Home (et ses successeurs)

https://setiathome.berkeley.edu/

> analyse observationnelle cherchant à détecter de la vie intelligente non terrestre Depuis 1999 !

Objectif officieux: prouver la viabilité d'une grille distribuée à base de machines sur étagères

successeur : https://boinc.berkeley.edu/

L'ancètre : MPI

https://www.mpi-forum.org/docs/

> Message Passing Interface

Tellement important que les constructeurs fournissent leur propre versions (e.g. Intel, IBM…)

Une tendance : Celery

https://docs.celeryproject.org/

> Distributed Task Queue

L'ordonnanceur : Slurm

https://www.schedmd.com/

> workload manager

Le modeste : Make

https://www.gnu.org/software/make/

> build automation

Une question de grain

Surcoûts possibles :

Qui s'ajoutent au temps d'exécution de la tâche…

De la taille des tuyaux

Aux débuts de l'informatique, les processeurs étaient lents :

→ on optimise le nombre d'instruction, pas les transferts mémoires

au 21ème siècle, les processeurs sont rapides :

→ on optimise les accès mémoires

Mais tout dépend du système ! MPI ≠ OpenMP

Communication entre architectures différentes

Que se passe-t-il quand un processeur arm32 bit envoie un tableau de int à un processeur amd64 ?

Écueils possibles :

MPI API : compilation et exécution

MPI API : contexte

Création et fermeture de contexte :

Les autres appels MPI ne sont valides qu'entre ces deux appels

Exemple

int main(int argc, char **argv)
{
    MPI_Init(&argc, &argv);
    sleep(random());
    MPI_Finalize();
    return 0;
}

Q: Quand se termine le programme principal ?

MPI API : communicateurs

communicateur = ensemble de processus + contexte de comm prédéfini: MPI_Comm MPI_COMM_WORLD

Exemple

int main(int argc, char **argv)
{
    MPI_Init(&argc, &argv);

    int my_rank, num_procs;
    MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
    MPI_Comm_size(MPI_COMM_WORLD, &num_procs);
    printf("%d/%d\n", my_rank, num_procs);
    MPI_Finalize();
    return 0;
}

Q: Quel stdout est utilisé ?

MPI API : envoi d'un message

int MPI_Send(void *buf, // données à envoyer
             int count, // nombre d'élément données
             MPI_Datatype datatype, // type d'un élément
             int dest, // étiquette du destinataire
             int tag, // identifiant du message
             MPI_Comm comm,  // communicateur choisi
);

MPI API : réception d'un message

int MPI_Recv(void *buf,  // adresse de réception
             int count,  // nombre max. d'éléments
             MPI_Datatype datatype, // type d'un élément
             int source,  // rang de la source
             int tag, // étiquette du message attendu
             MPI_Comm comm, // communicateur choisi
             MPI_Status *status  // descripteur de la com.
);

Exemple

int main(int argc, char **argv)
{
    MPI_Init(&argc, &argv);
    int my_rank, num_procs, from_rank;
    MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
    MPI_Comm_size(MPI_COMM_WORLD, &num_procs);
    MPI_Send(&my_rank, 1, MPI_INT,
             (my_rank + 1) % num_procs, 42, MPI_COMM_WORLD);

    MPI_Recv(&from_rank, 1, MPI_INT,
             (my_rank - 1) % num_procs, 42, MPI_COMM_WORLD,
             MPI_STATUS_IGNORE);

    assert(from_rank == (my_rank - 1) % num_procs);

    MPI_Finalize();
    return 0;
}

MPI API : statut d'un message

struct MPIStatus{
    int MPI_SOURCE ; // rang de la source, cf. MPI_ANY_SOURCE
    int MPI_TAG ; //  ́étiquette du message recu, cf. MPI_ANY_TAG
    int MPI_ERROR ; // code d'erreur
};

int MPI_Get_count(MPI_Status *status,  // statut à inspecter
                  MPI_Datatype datatype,  // type des éléments transférés
                  int *count  // nombre d'éléments reçus
);

Exemple

char buffer[512];
MPI_Status status;
MPI_Recv(buffer, 512, MPI_CHAR,
         MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD,
         &status);
int buffer_count;
MPI_Get_count(&status, MPI_CHAR, &buffer_count);

printf("recieved message from %d with tag %d\n",
       status.MPI_SOURCE,
       status.MPI_TAG);

for(int i = 0; i < buffer_count; ++i)
    stuff(buffer[i]);

MPI API : inspection d'un message

int MPI_Probe(int source, // rang de la source
              int tag, // étiquette du message attendu
              MPI_Comm comm, // communicateur choisi
              MPI_Status *status  // descripteur de la com.
);

Bloquant, pratique pour dimensionner un tampon avant un MPI_Recv.

MPI API : transferts non bloquants

int MPI_Isend(..., // mêmes arguments que MPI_Send
              MPI_Request* req // requète interrogeable
);

int MPI_Wait(MPI_Request* req, // requète à interroger
             MPI_Status* status // status de la requète
);

int MPI_Test(MPI_Request *req, // requète à interroger
             int *flag, // positionné à 0 si envoi non terminé
             MPI_Status *status  // statut à maj
);

MPI API : broadcasting

int MPI_Bcast(void *buffer, // cf. MPI_Send / MPI_Recv
              int count, // cf. MPI_Send / MPI_Recv
              MPI_Datatype datatype, // cf. MPI_Send / MPI_Recv
              int root, // rang de l'envoyeur
              MPI_Comm comm // cf. MPI_Send
);

[!] les nœuds de rang ≠ root reçoivent les données à l'issue de cet appel

MPI API: scatter / gather

                SCATTER            GATHER

A:               +-- 0         0 -- +
B:               +-- 1         1 -- +
C:  [0,1,2,3] > -|                  | - > [0, 1, 2, 3]
D:               +-- 2         2 -- +
E:               +-- 3         3 -- +

MPI API: scatter

 int MPI_Scatter(
     // description des données envoyées aux autres nœuds
     const void *sendbuf, int sendcount, MPI_Datatype sendtype,
     // description desdonnées reçues par chaque autre nœud
     void *recvbuf, int recvcount, MPI_Datatype recvtype,
     int root, // rang de l'envoyeur
     MPI_Comm comm // …
 );
[!] recvcount * comm_size = sendcount

MPI API: gather

// idem
int MPI_Gather(
    const void * sendbuf, int sendcount, MPI_Datatype sendtype,
    void * recvbuf, int recvcount, MPI_Datatype recvtype,
    int root,
    MPI_Comm comm)
[!] recvcount = sendcount * comm_size

Q: comment gérer les I/O

+/- : performance, #communication, portabilité ?

Un aperçu de MPI I/O

API pour accéder de manières distribuée aux données

1