Введение в программирование на кластерах

Коллективные обмены

Показывать лекцию целиком

Презентацию к данной лекции Вы можете скачать здесь.

Общая характеристика коллективных обменов

Общая характеристика коллективных обменов:

  • коллективные обмены не могут взаимодействовать с двухточечными. Коллективная
  • передача не может быть перехвачена двухточечной подпрограммой приема;
  • коллективные обмены могут выполняться как с синхронизацией, так и без нее;
  • все коллективные обмены являются блокирующими для инициировавшего их обмена;
  • теги сообщений в коллективных обменах назначаются системой.
  • Виды коллективных обменов:

  • широковещательная передача  - выполняется от одного процесса ко всем;
  • распределение данных;
  • сбор данных;
  • обмен с барьером - это форма синхронизации работы процессов, когда обмен сообщениями происходит только после того, как к соответствующей процедуре обратилось определенное число процессов;
  • операции сканирования;
  • операции приведения. Входными являются данные нескольких процессов, а результат  одно значение, которое становится доступным всем процессам, участвующим в обмене.
  • Широковещательная рассылка

    Широковещательная рассылка выполняется подпрограммой

    int MPI_Bcast(void *buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm)
    
    MPI_Bcast(buffer, count, datatype, root, comm, ierr)
    
      

    Параметры этой процедуры одновременно являются входными и выходными:

  • buffer - адрес буфера;
  • count - количество элементов данных в сообщении;
  • datatype - тип данных MPI;
  • root - ранг главного процесса, выполняющего широковещательную рассылку;
  • comm - коммуникатор.
  • Пример

    #include "mpi.h"
    #include <stdio.h>
    int main(int argc,char *argv[])
    {
    char data[24];
    int myrank, count = 25;
    MPI_Status status;
    MPI_Init(argc, argv);
    MPI_Comm_rank(MPI_COMM_WORLD, myrank);
    if (myrank == 0)
    {
     strcpy(data, "Hi, Parallel Programmer!");
     MPI_Bcast(data, count, MPI_BYTE, 0, MPI_COMM_WORLD);
     printf("send: %s\n", data);
    }
    else
    {
     MPI_Bcast(data, count, MPI_BYTE, 0, MPI_COMM_WORLD);
     printf("received: %s\n", data);
    }
     MPI_Finalize();
    return 0;
    }
        

    Пример

    #include "mpi.h"
    #include <stdio.h>
    
    int main(int argc, char *argv[])
    {
     int myrank;
     int root = 0;
     int count = 1;
     float a, b;
     int n;
    
     MPI_Init(argc, argv);
     MPI_Comm_rank(MPI_COMM_WORLD, myrank);
     if (myrank == 0)
    printf("Enter a, b, n\n");
    scanf("%f %f %i", a, b, n);
    MPI_Bcast(a, count, MPI_FLOAT, root, MPI_COMM_WORLD);
    MPI_Bcast(b, count, MPI_FLOAT, root, MPI_COMM_WORLD);
    MPI_Bcast(n, count, MPI_INT, root, MPI_COMM_WORLD);
    }
    else
    {
    MPI_Bcast(a, count, MPI_FLOAT, root, MPI_COMM_WORLD);
    MPI_Bcast(b, count, MPI_FLOAT, root, MPI_COMM_WORLD);
    MPI_Bcast(n, count, MPI_INT, root, MPI_COMM_WORLD);
    printf("%i Process got %f %f %i\n", myrank, a, b, n);
    }
     MPI_Finalize();
     return 0;
    } 
      

    Операции распределения и сбора данных

    Распределение данных

    Пример

    int MPI_Scatter(void *sendbuf, int sendcount, MPI_Datatype sendtype, 
    void *rcvbuf, int rcvcount, MPI_Datatype rcvtype, int root, MPI_Comm comm)
    
    MPI_Scatter(sendbuf, sendcount, sendtype, rcvbuf, rcvcount, rcvtype, root, comm, ierr)      
        

    Входные параметры:

  • sendbuf - адрес буфера передачи;
  • sendcount - количество элементов, пересылаемых каждому процессу;
  • sendtype - тип передаваемых данных;
  • rcvcount - количество элементов в буфере приема;
  • rcvtype - тип принимаемых данных;
  • root - ранг передающего процесса;
  • comm - коммуникатор.
  • Выходной параметр: rcvbuf - адрес буфера приема.

    Процесс с рангом root распределяет содержимое буфера передачи sendbuf среди всех процессов. Содержимое буфера передачи разбивается на несколько фрагментов, каждый из которых содержит sendcount элементов. Первый фрагмент передается процессу 0, второй процессу 1 и т. д. Аргументы send… имеют значение только на стороне распределяющего процесса root

    Сбор данных

    Сбор данных от остальных процессов в буфер главной задачи выполняется подпрограммой:

    int MPI_Gather(void *sendbuf, int sendcount, MPI_Datatype sendtype, 
    void *rcvbuf, int rcvcount, MPI_Datatype rcvtype, int root, MPI_Comm comm)
    
    MPI_Gather(sendbuf, sendcount, sendtype, rcvbuf, rcvcount, rcvtype, root, comm, ierr)
        

    Каждый процесс в коммуникаторе comm пересылает содержимое буфера передачи sendbuf процессу с рангом root. Процесс root склеивает полученные данные в буфере приема.

    Порядок склейки определяется рангами процессов: Аргументы rcvbuf, rcvcount и rcvtype играют роль только на стороне главного процесса. Аргумент rcvcount указывает количество элементов данных, полученных от каждого процесса (но НЕ суммарное их количество). При вызове подпрограмм MPI_Scatter и MPI_Gather из разных процессов следует использовать общий главный процесс.

    int MPI_Allgather(void *sendbuf, int sendcount, MPI_Datatype sendtype, 
    void *rcvbuf, int rcvcount, MPI_Datatype rcvtype, MPI_Comm comm)
    
    MPI_Allgather(sendbuf, sendcount, sendtype, rcvbuf, rcvcount, rcvtype, comm, ierr)
        

    Входные параметры:

  • sendbuf - начальный адрес буфера передачи;
  • sendcount - количество элементов в буфере передачи;
  • sendtype - тип передаваемых данных;
  • rcvcount - количество элементов, полученных от каждого процесса;
  • rcvtype - тип данных в буфере приема;
  • comm - коммуникатор.
  • Выходной параметр: rcvbuf - адрес буфера приема.

    Блок данных, переданный от j-го процесса, принимается каждым процессом и размещается в j-м блоке буфера приема recvbuf

    Операции приведения

    Операция приведения (редукции)

    Операция приведения, результат которой передается одному процессу

    int MPI_Reduce(void *buf, void *result, int count, 
    MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm)
    
    MPI_Reduce(buf, result, count, datatype, op, root, comm, ierr)
        

    Входные параметры:

  • buf - адрес буфера передачи;
  • count - количество элементов в буфере передачи;
  • datatype - тип данных в буфере передачи;
  • op - операция приведения;
  • root - ранг главного процесса;
  • comm - коммуникатор.
  • MPI_Reduce применяет операцию приведения к операндам из buf, а результат каждой операции помещается в буфер результата result. MPI_Reduce должна вызываться всеми процессами в коммуникаторе comm, а аргументы count, datatype и op в этих вызовах должны совпадать

    Пример

    #include "mpi.h"
    #include <stdio.h>
    int main(int argc,char *argv[])
    {
     int myrank, i;
     int count = 5, root = 1;
     MPI_Group MPI_GROUP_WORLD, subgroup;
     int ranks[4] = {1, 3, 5, 7};
     MPI_Comm subcomm;
     int sendbuf[5] = {1, 2, 3, 4, 5};
     int recvbuf[5];
    
     MPI_Init(argc, argv);
     MPI_Comm_group(MPI_COMM_WORLD, MPI_GROUP_WORLD);
     MPI_Group_incl(MPI_GROUP_WORLD, 4, ranks, subgroup);
     MPI_Group_rank(subgroup, myrank);
     MPI_Comm_create(MPI_COMM_WORLD, subgroup, subcomm);
    if(myrank != MPI_UNDEFINED)
    {
    MPI_Reduce(sendbuf, recvbuf, count, MPI_INT, MPI_SUM, root, subcomm);
    
    if(myrank == root) {
    printf("Reduced values");
    for(i = 0; i < count; i++){
    printf(" %i ", recvbuf[i]);}
    }
    printf("\n");
    
    MPI_Comm_free(subcomm);
    MPI_Group_free(MPI_GROUP_WORLD);
    MPI_Group_free(subgroup);
    }
     MPI_Finalize();
     return 0;
    }
     
          
        

    Пример. Использование операции приведения: вычисление числа$$\pi$$ методом Монте-Карло

    #include <stdlib.h>
    #include <stdio.h>
    #include "mpi.h"
    double dboard (int trials);
    
    #define trials 5000  
    #define ROUNDS 10   
    #define MASTER 0    
    
    main(int argc, char **argv) 
    {  
    double homepi, pisum, pi, avepi; 
    int mytid, nproc, rcode, ;
    
    MPI_Init(argc, argv);
    MPI_Comm_rank(MPI_COMM_WORLD, mytid);
    MPI_Comm_size(MPI_COMM_WORLD, nproc);
    printf ("MPI task ID = %d\n", mytid);
    srandom (mytid);
    
    avepi = 0;
    for (i = 0; i < ROUNDS; i++) {
      homepi = dboard(trials);
    
      rcode = MPI_Reduce(homepi, pisum, 1, MPI_DOUBLE,
       MPI_SUM, MASTER, MPI_COMM_WORLD);
      if (rcode != 0)
       printf("%d: failure on MPI_Reduce\n", mytid);
    
       if (mytid == MASTER) {
         pi = pisum/nproc;
         avepi = ((avepi * i) + pi)/(i + 1);  
         printf("   After %3d throws, average value of pi =  
         %10.8f\n", (trials * (i + 1)),avepi);
         }     
     }  
    MPI_Finalize();
    }
    #include <stdlib.h>
    #define sqr(x)  ((x)*(x))
    long random(void);
    double dboard(int trials)
         {
         double x_coord, y_coord, pi, r;
         int score, n;
         unsigned long cconst;
    
         cconst = 2 << (31 - 1);
         score = 0;
    
         for (n = 1; n <= trials; n++)  {
              r = (double)random()/cconst;
              x_coord = (2.0 * r) - 1.0;
              r = (double)random()/cconst;
              y_coord = (2.0 * r) - 1.0;
    
              if ((sqr(x_coord) + sqr(y_coord)) <= 1.0)
                   score++;
              }
    pi = 4.0 * (double)score/(double)trials;
         return(pi);
         } 
         
      

    Синхронизация

    Синхронизация с помощью "барьера" выполняется с помощью подпрограммы:

    int MPI_Barrier(MPI_Comm comm)
    
    MPI_Barrier(comm, ierr)
        

    Синхронизация с помощью "барьера" - простейшая форма синхронизации коллективных обменов. Она не требует пересылки данных. Обращение к подпрограмме MPI_Barrier блокирует выполнение каждого процесса из коммуникатора comm до тех пор, пока все процессы не вызовут эту подпрограмму.

    В общем случае барьер характеризуется "толщиной". "Толщина" – количество процессов, которые должны обратиться к подпрограмме барьерной синхронизации для того, чтобы барьер был преодолен.

    Векторная операция распределения данных

    Векторная подпрограмма распределения данных:

    int MPI_Scatterv(void *sendbuf, int *sendcounts, int *displs, MPI_Datatype sendtype, 
    void *rcvbuf, int rcvcount, MPI_Datatype rcvtype, int root, MPI_Comm comm)
    
    MPI_Scatterv(sendbuf, sendcounts, displs, sendtype, rcvbuf, rcvcount, rcvtype, root, comm, ierr)
        

    Входные параметры:

  • sendbuf - адрес буфера передачи;
  • sendcounts - целочисленный одномерный массив, содержащий количество элементов, передаваемых каждому процессу (индекс равен рангу адресата). Его длина равна количеству процессов в коммуникаторе;
  • displs - целочисленный массив, длина которого равна количеству процессов в коммуникаторе. Элемент с индексом i задает смещение относительно начала буфера передачи. Ранг адресата равен значению индекса i;
  • sendtype - тип данных в буфере передачи;
  • rcvcount - количество элементов в буфере приема;
  • rcvtype - тип данных в буфере приема;
  • root - ранг передающего процесса;
  • comm - коммуникатор.
  • Выходной параметр: rcvbuf - адрес буфера приема.

    Векторная операция сбора данных

    Сбор данных от всех процессов в заданном коммуникаторе и запись их в буфер приема с указанным смещением:

    int MPI_Gatherv(void *sendbuf, int sendcount, MPI_Datatype sendtype, void *recvbuf, 
    int *recvcounts, int *displs, MPI_Datatype recvtype, int root, MPI_Comm comm)
    
    MPI_Gatherv(sendbuf, sendcount, sendtype, recvbuf, recvcounts, displs, recvtype, root, comm, ierr)
        

    Список параметров у этой подпрограммы похож на список параметров подпрограммы MPI_Scatterv. В обменах, выполняемых подпрограммами MPI_Allgather и MPI_Alltoall, нет главного процесса. Детали отправки и приема важны для всех процессов, участвующих в обмене.

    Вернуться к учебному плану