Опубликован: 28.10.2009 | Уровень: специалист | Доступ: платный
Лекция 4:

Параллельное программирование на основе MPI

6.2.1.3. Передача сообщений

Для передачи сообщения процесс-отправитель должен выполнить функцию:

int MPI_Send(void *buf, int count, MPI_Datatype type, int dest,
  int tag, MPI_Comm comm),

где

  • buf - адрес буфера памяти, в котором располагаются данные отправляемого сообщения,
  • count - количество элементов данных в сообщении,
  • type - тип элементов данных пересылаемого сообщения,
  • dest - ранг процесса, которому отправляется сообщение,
  • tag - значение-тег, используемое для идентификации сообщений,
  • comm - коммуникатор, в рамках которого выполняется передача данных.

Для указания типа пересылаемых данных в MPI имеется ряд базовых типов, полный список которых приведен в табл. 6.1.

Таблица 6.1. Базовые (пpедопpеделенные) типы данных MPI для алгоритмического языка C
MPI_Datatype C Datatype
MPI_BYTE
MPI_CHAR signed char
MPI_DOUBLE double
MPI_FLOAT float
MPI_INT int
MPI_LONG long
MPI_LONG_DOUBLE long double
MPI_PACKED
MPI_SHORT short
MPI_UNSIGNED_CHAR unsigned char
MPI_UNSIGNED unsigned int
MPI_UNSIGNED_LONG unsigned long
MPI_UNSIGNED_SHORT unsigned short

Следует отметить:

  1. Отправляемое сообщение определяется через указание блока памяти ( буфера ), в котором это сообщение располагается. Используемая для указания буфера триада
    ( buf, count, type )

    входит в состав параметров практически всех функций передачи данных.

  2. Процессы, между которыми выполняется передача данных, в обязательном порядке должны принадлежать коммуникатору, указываемому в функции MPI_Send.
  3. Параметр tag используется только при необходимости различения передаваемых сообщений, в противном случае в качестве значения параметра может быть использовано произвольное целое число (см. также описание функции MPI_Recv ).

Сразу же после завершения функции MPI_Send процесс-отправитель может начать повторно использовать буфер памяти, в котором располагалось отправляемое сообщение. Вместе с этим, следует понимать, что в момент завершения функции MPI_Send состояние самого пересылаемого сообщения может быть совершенно различным - сообщение может располагаться в процессе-отправителе, может находиться в процессе передачи, может храниться в процессе-получателе или же может быть принято процессом-получателем при помощи функции MPI_Recv. Тем самым, завершение функции MPI_Send означает лишь, что операция передачи начала выполняться и пересылка сообщения будет рано или поздно будет выполнена.

Пример использования функции будет представлен после описания функции MPI_Recv.

6.2.1.4. Прием сообщений

Для приема сообщения процесс-получатель должен выполнить функцию:

int MPI_Recv(void *buf, int count, MPI_Datatype type, int source,
  int tag, MPI_Comm comm, MPI_Status *status),

где

  • buf, count, type - буфер памяти для приема сообщения, назначение каждого отдельного параметра соответствует описанию в MPI_Send,
  • source - ранг процесса, от которого должен быть выполнен прием сообщения,
  • tag - тег сообщения, которое должно быть принято для процесса,
  • comm - коммуникатор, в рамках которого выполняется передача данных,
  • status - указатель на структуру данных с информацией о результате выполнения операции приема данных.

Следует отметить:

  1. Буфер памяти должен быть достаточным для приема сообщения, а тип элементов передаваемого и принимаемого сообщения должны совпадать; при нехватке памяти часть сообщения будет потеряна и в коде завершения функции будет зафиксирована ошибка переполнения,
  2. При необходимости приема сообщения от любого процесса-отправителя для параметра source может быть указано значение MPI_ANY_SOURCE,
  3. При необходимости приема сообщения с любым тегом для параметра tag может быть указано значение MPI_ANY_TAG,
  4. Параметр status позволяет определить ряд характеристик принятого сообщения:
    • status.MPI_SOURCE - ранг процесса-отправителя принятого сообщения,
    • status.MPI_TAG - тег принятого сообщения.

Функция

MPI_Get_count(MPI_Status *status, MPI_Datatype type, int *count)

возвращает в переменной count количество элементов типа type в принятом сообщении.

Вызов функции MPI_Recv не должен согласовываться со временем вызова соответствующей функции передачи сообщения MPI_Send - прием сообщения может быть инициирован до момента, в момент или после момента начала отправки сообщения.

По завершении функции MPI_Recv в заданном буфере памяти будет располагаться принятое сообщение. Принципиальный момент здесь состоит в том, что функция MPI_Recv является блокирующей для процесса-получателя, т.е. его выполнение приостанавливается до завершения работы функции. Таким образом, если по каким-то причинам ожидаемое для приема сообщение будет отсутствовать, выполнение параллельной программы будет блокировано.

6.2.1.5. Первая параллельная программа с использованием MPI

Рассмотренный набор функций оказывается достаточным для разработки параллельных программ ). Приводимая ниже программа является стандартным начальным примером для алгоритмического языка C.

#include <stdio.h>
  #include "mpi.h"
  int main(int argc, char* argv[]){
    int ProcNum, ProcRank, RecvRank;
    MPI_Status Status;
    MPI_Init(&argc, &argv);
    MPI_Comm_size(MPI_COMM_WORLD, &ProcNum);
    MPI_Comm_rank(MPI_COMM_WORLD, &ProcRank);
    if ( ProcRank == 0 ){
      // Действия, выполняемые только процессом с рангом 0
      printf ("\n Hello from process %3d", ProcRank);
      for ( int i=1; i<ProcNum; i++ ) {
        MPI_Recv(&RecvRank, 1, MPI_INT, MPI_ANY_SOURCE, 
          MPI_ANY_TAG, MPI_COMM_WORLD, &Status);
        printf("\n Hello from process %3d", RecvRank);
      }
    } 
    else // Сообщение, отправляемое всеми процессами, 
         // кроме процесса с рангом 0
      MPI_Send(&ProcRank,1,MPI_INT,0,0,MPI_COMM_WORLD);
    MPI_Finalize();
    return 0;
  }
6.1. Первая параллельная программа с использованием MPI

Как следует из текста программы, каждый процесс определяет свой ранг, после чего действия в программе разделяются. Все процессы, кроме процесса с рангом 0, передают значение своего ранга нулевому процессу. Процесс с рангом 0 сначала печатает значение своего ранга, а далее последовательно принимает сообщения с рангами процессов и также печатает их значения. При этом важно отметить, что порядок приема сообщений заранее не определен и зависит от условий выполнения параллельной программы (более того, этот порядок может изменяться от запуска к запуску). Так, возможный вариант результатов печати процесса 0 может состоять в следующем (для параллельной программы из четырех процессов):

Hello from process 0
   Hello from process 2
   Hello from process 1
   Hello from process 3

Такой "плавающий" вид получаемых результатов существенным образом усложняет разработку, тестирование и отладку параллельных программ, т.к. в этом случае исчезает один из основных принципов программирования - повторяемость выполняемых вычислительных экспериментов. Как правило, если это не приводит к потере эффективности, следует обеспечивать однозначность расчетов и при использовании параллельных вычислений. Так, для рассматриваемого простого примера можно восстановить постоянство получаемых результатов при помощи задания ранга процесса-отправителя в операции приема сообщения:

MPI_Recv(&RecvRank, 1, MPI_INT, i, MPI_ANY_TAG, MPI_COMM_WORLD, &Status).

Указание ранга процесса-отправителя регламентирует порядок приема сообщений, и, как результат, строки печати будут появляться строго в порядке возрастания рангов процессов (повторим, что такая регламентация в отдельных ситуациях может приводить к замедлению выполняемых параллельных вычислений).

Следует отметить еще один важный момент - разрабатываемая с использованием MPI программа как в данном частном варианте, так и в самом общем случае используется для порождения всех процессов параллельной программы и, как результат, должна определять вычисления, выполняемые во всех этих процессах. Можно сказать, что MPI-программа является некоторым " макро-кодом ", различные части которого используются разными процессами. Так, например, в приведенном примере программы выделенные двойной рамкой участки программного кода не выполняются одновременно ни в одном процессе. Первый выделенный участок с функцией приема MPI_Send исполняется только процессом с рангом 0, второй участок с функцией приема MPI_Recv используется всеми процессами, за исключением нулевого процесса.

Для разделения фрагментов кода между процессами обычно используется подход, примененный в только что рассмотренной программе - при помощи функции MPI_Comm_rank определяется ранг процесса, а затем в соответствии с рангом выделяются необходимые для процесса участки программного кода. Наличие в одной и той же программе фрагментов кода разных процессов также значительно усложняет понимание и, в целом, разработку MPI-программы. Как результат, можно рекомендовать при увеличении объема разрабатываемых программ выносить программный код разных процессов в отдельные программные модули (функции). Общая схема MPI программы в этом случае будет иметь вид:

MPI_Comm_rank(MPI_COMM_WORLD, &ProcRank);
    if ( ProcRank == 0 ) DoProcess0();
    else if ( ProcRank == 1 ) DoProcess1();
    else if ( ProcRank == 2 ) DoProcess2();

Во многих случаях, как и в рассмотренном примере, выполняемые действия являются отличающимися только для процесса с рангом 0. В этом случае общая схема MPI программы принимает более простой вид:

MPI_Comm_rank(MPI_COMM_WORLD, &ProcRank);
    if ( ProcRank == 0 ) DoManagerProcess();
    else DoWorkerProcesses();

В завершение обсуждения примера поясним использованный в MPI подход для контроля правильности выполнения функций - все функции MPI возвращают в качестве своего значения код завершения. При успешном выполнении функции возвращаемый код равен MPI_SUCCESS. Другие значения кода завершения свидетельствуют об обнаружении тех или иных ошибочных ситуаций в ходе выполнения функций. Для выяснения типа обнаруженной ошибки используются предопределенные именованные константы, среди которых:

  • MPI_ERR_BUFFER - неправильный указатель на буфер,
  • MPI_ERR_COMM - неправильный коммуникатор,
  • MPI_ERR_RANK - неправильный ранг процесса,

и др. - полный список констант для проверки кода завершения содержится в файле mpi.h.

6.2.2. Определение времени выполнение MPI-программы

Практически сразу же после разработки первых параллельных программ возникает необходимость определения времени выполнения вычислений для оценки достигаемого ускорения процессов решения задач за счет использования параллелизма. Используемые обычно средства для измерения времени работы программ зависят, как правило, от аппаратной платформы, операционной системы, алгоритмического языка и т.п. Стандарт MPI включает определение специальных функций для измерения времени, использование которых позволяет устранить зависимость от среды выполнения параллельных программ.

Получение времени текущего момента выполнения программы обеспечивается при помощи функции:

double MPI_Wtime(void),

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

double t1, t2, dt;
    t1 = MPI_Wtime();
    …
    t2 = MPI_Wtime();
    dt = t2 - t1;

Точность измерения времени также может зависеть от среды выполнения параллельной программы. Для определения текущего значения точности может быть использована функция:

double MPI_Wtick(void),

позволяющая определить время в секундах между двумя последовательными показателями времени аппаратного таймера используемой компьютерной системы.

Алексей Николаев
Алексей Николаев
Россия, г. Саранск
Рамиль Ариков
Рамиль Ариков
Россия, Республика Мордовия