Вы находитесь на странице: 1из 7

Лекция 6

Обмен сообщениями в MPI


В этой лекции мы ознакомимся с основными операциями обмена, реализованными в
спецификации MPI. Операции обмена бывают двухточечные и коллективные. Следует пони-
мать, что выполнение параллельных программ по своей сущности является недетерминиро-
ванным процессом. Действительно, в случае, если параллельная программа относится к
классу MPMD, то ее составные части могут иметь различные объемы кода и, понятно, вы-
полнение их будет занимать различное время. Если программа относится к классу SPMD, и
даже составные части ее имеют одинаковый код, то и в этом случае нельзя гарантировать,
что процессоры будут работать синхронно в общей шкале времени. Но для решения единой
задачи необходимо выполнять обмены данными в заданной последовательности, а иногда и в
заданные моменты времени. Функции библиотеки MPI не могут гарантировать, что сообще-
ния, отправленные двумя процессами в некоторой последовательности третьему процессу, в
такой же последовательности будут им получены. Но эти функции гарантируют, что два со-
общения, отправленные одним процессом другому, будут получены в той же последователь-
ности.
В начале изучим обобщенный механизм блокирующих и неблокирующих обменов, ко-
торые наглядно можно представить в виде схем, показанных на рис. 6.1. Блокирующий об-
мен – это обмен, при котором выполнение процесса-отправителя сообщения и/или процесса-
приемника может быть приостановлено до окончания передачи и/или приема. На рис.6.1.а
показан случай блокировки обоих процессов на время обмена. Неблокирующий обмен за-
вершается сразу для процесса-отправителя независимо от того, дошло ли сообщение до про-
цесса-приемника или нет. В этом случае для начала приема информации или анализа того,
пришла ли информация, необходимы специальные тестирующие функции. Отметим, что
реализации функций обмена могут быть разными.
В библиотеке имеется четыре функции блокирующей передачи, четыре функции не-
блокирующей передачи, одна функция блокирующего приема и одна функция неблокирую-
щего приема. Функции приема могут выполнять прием сообщений, реализованный любой из
функций передачи. Таким образом, возможно множество различных комбинаций функций
передачи и приема, которые можно применять в зависимости от необходимости и целей об-
мена данными в параллельной программе. Мы рассмотрим только наиболее часто применяе-
мые варианты обмена данными.
Мы уже рассматривали функцию передачи MPI_Send(). Кроме нее существуют еще
функции, которые можно условно обозначить так:

MPI_[I][R,S,B]Send();

При этом в языке С только первый символ после префикса MPI_ записывается с помощью
символа верхнего регистра, например MPI_Irsend(). Символы в квадратных скобках обо-
значают:

I – Immediate, неблокирующий обмен;


R – Ready, передача "по готовности";
S – Synchronous, синхронная передача;
B – Buffering, буферизованная передача.

Функции передачи и приема без этих символов, называются стандартными функциями пере-
дачи и приема.
2

Синхронная передача отличается от стандартной тем, что передача не завершается, по-


ка не будет получено подтверждение об окончании приема. Это подтверждение часто назы-
вают квитанцией.
Передача по готовности начинается, если получено сообщение от принимающей сторо-
ны о готовности к приему.
Буферизованная передача считается законченной, если данные переданы в системный
буфер.
Охарактеризуем кратко названные режимы передачи. Наиболее часто применяется
стандартный блокирующий режим передачи. Его применение обеспечивает надежный об-
мен, однако возможны тупиковые ситуации, если отправленное сообщение не достигло адре-
сата, процесс-приемник блокируется при использовании блокирующей функции приема. Бо-
лее надежный режим стандартный синхронной передачи, когда дальнейшая работа процесса-
передатчика продолжается, если получено уведомление о приеме сообщения. Но этот режим
более медленный. Наиболее быстрый режим – буферизованный, в отличие от стандартного,
он является асинхронным. В этом режиме процессы продолжают работу, как только данные
попали в системный буфер, но его можно применять, если буферы имеют достаточные раз-
меры, задача достаточно отлажена, исключены случаи приема "не своих" сообщений. Пере-
дача "по готовности" может обеспечить самой быстрый режим обмена, если прием только
лишь зарегистрирован (например, используется функция неблокирующего приема), в этом
случае передача заканчивается, как только сообщение отправлено в системный буфер.

a) б)
Рис.6.1 – Блокирующий (а) и неблокирующий (б) обмены сообщениями
Таким образом, библиотека MPI предоставляет обширный "полигон" возможностей ор-
ганизации обмена между процессами, но их необходимо исследовать для каждой задачи, с
целью получить максимальную эффективность по быстродействию, надежности и исполь-
зуемой памяти. Для обеспечения всевозможных режимов в библиотеке имеются различные
дополнительные функции, которые лишь кратко перечислим.
Функции управления дополнительным буфером для буферизованного обмена:

MPI_Buffer_attach(void *buf,int size);


MPI_Buffer_detach(void *buf,int size);

Функции "пробники" для получения информации о сообщении до его помещения в бу-


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

Высокопроизводительные вычислительные системы и параллельное программирование. Кудерметов Р.К.


3

MPI_[I]Probe(int source,int tag,MPI_Comm comm, MPI_Status *status);

Функция блокировки процесса до завершения приема или передачи в неблокирующем


режиме:

MPI_Wait(MPI_Request *request,MPI_Status *status);

где параметр request – идентификатор операции обмена, специальная структура, заполняе-


мая неблокирующими операциями обмена.
Функция неблокирующей проверки завершения приема или передачи:

MPI_Test(MPI_Request *request,int flag, MPI_Status *status);

где параметр flag устанавливается в единицу, если операция, request заданная идентифи-
катором операции обмена, выполнена.
Имеется функция совместного приема-передачи:

MPI_Sendresv(void *sbuf,int scount,MPI_Datatype stype,int dest,


int stag,void rbuf, int rcount, MPI_Datatype,int source,int rtag,
MPI_Comm comm, MPI_Status *status);

Мы перечислили далеко не все функции спецификации MPI для двухточечного обмена


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

MPI_Bcast(void *buf,int count,MPI_Datatype type,int root,MPI_Comm comm);

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

Высокопроизводительные вычислительные системы и параллельное программирование. Кудерметов Р.К.


4

Рис. 6.2 – Работа функции MPI_Bcast()


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

MPI_Scatter(void *sbuf,int scount,MPI_Datatype stype,void *rbuf,int


rcount,MPI_Datatype rtype,int root,MPI_Comm comm);

MPI_Gather(void *sbuf,int scount,MPI_Datatype stype,void *rbuf,int


rcount,MPI_Datatype rtype,int root,MPI_Comm comm);

Функция распределения MPI_Scatter() рассылает равные части буфера sbuf процес-


са root всем процессам. При этом содержимое буфера процесса root разбивается на равные
части, каждая из которых состоит из scount элементов. Первая часть остается в отправляет-
ся в нулевой процесс, вторая в первый и т.д. Аргументы, относящиеся к передающей части
списка аргументов функции, имеют силу только для процесса root. На рис. 6.3. показано
действие функции MPI_Scatter().

Рис.6.3 – Работа функции MPI_Scatter()


Функция MPI_Gather() имеет обратное действие по сравнению с функцией
MPI_Scatter(), т.е. она принимает и располагает по порядку принятые из передающих
процессов данные (рис.6.4). При этом параметры приема действительны только для прини-
мающего процесса.

Высокопроизводительные вычислительные системы и параллельное программирование. Кудерметов Р.К.


5

Рис. 6.4 – Работа функции MPI_Gather()


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

MPI_Reduce(void *buf, void *result, int count, MPI_Datatype datatype,


MPI_Op op, int root, MPI_Comm comm);

Здесь op - операция приведения, которая может иметь предопределенные значения, такие


как MPI_SUM, MPI_PROD, MPI_LAND, MPI_BAND, MPI_MAX и т.п. Всего 12 операций. Кроме
того можно определить собственные операции приведения с помощью функции
MPI_Op_create(). Работа функции понятна из рис. 6.5.

Рис. 6.5 – Работа функции MPI_Reduce()


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

Рис.6.6 – Работа функции MPI_Scan()

Высокопроизводительные вычислительные системы и параллельное программирование. Кудерметов Р.К.


6

Синтаксис функции имеет вид:

MPI_Scan(void *sbuf,void rbuf, int count, MPI_Datatype datatype,


MPI_Op op, MPI_Comm comm);

Ниже в примере показано использование перечисленных функций, с помощью коммен-


тариев приведены результаты для случая запуска программы на четырех процессах.

#include "mpi.h"
#include <stdio.h>

int main(int argc,char *argv[])


{
float *x,*y;
float sum=0.0,total=0.0;
int i,j,myrank,P,n,M;
int err;

MPI_Init(&argc,&argv);
MPI_Comm_size(MPI_COMM_WORLD,&P);
MPI_Comm_rank(MPI_COMM_WORLD,&myrank);

if(myrank==0)
{
puts("Enter size of array");
scanf("%d",&n);
x=(float *)malloc(n*sizeof(float));
if(x==NULL) MPI_Abort(MPI_COMM_WORLD,err);
for(i=0;i<n;i++)
x[i]=(float)i;//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
M=n/P;
}

MPI_Bcast(&M,1,MPI_INT,0,MPI_COMM_WORLD);
if(myrank!=0)
{
x=(float *)malloc(M*sizeof(float));
if(x==NULL) MPI_Abort(MPI_COMM_WORLD,err);
}
y=(float *)calloc(M,sizeof(float));
if(y==NULL) MPI_Abort(MPI_COMM_WORLD,err);

MPI_Scatter(x,M,MPI_FLOAT,x,M,MPI_FLOAT,0,MPI_COMM_WORLD);

for(i=0;i<M;i++)
sum+=x[i];
MPI_Barrier(MPI_COMM_WORLD);
MPI_Reduce(&sum,&total,1,MPI_FLOAT,MPI_SUM,0,MPI_COMM_WORLD);
MPI_Reduce(x,y,M,MPI_FLOAT,MPI_SUM,0,MPI_COMM_WORLD);
if(myrank==0)
{
printf("Reduce total=%.0f\n",total); //Reduce 120
printf("Reduce: ");
for(i=0;i<M;i++)
printf("y[%d]=%.0f ",i,y[i]); //Reduce 24 28 32 36
printf("\n");
}
MPI_Gather(&sum,1,MPI_FLOAT,y,1,MPI_FLOAT,0,MPI_COMM_WORLD);
Высокопроизводительные вычислительные системы и параллельное программирование. Кудерметов Р.К.
7

if(myrank==0)
{
printf("Gather: ");
for(i=0;i<M;i++)
printf("y[%d]=%.0f ",i,y[i]); //Gather 6 22 38 54
printf("\n");
}

MPI_Scan(x,y,M,MPI_FLOAT,MPI_SUM,MPI_COMM_WORLD);
for(i=0;i<P;i++)
if(myrank==i)
{
printf("Scan:\n");
printf("#%d: ",myrank);
for(j=0;j<M;j++)
printf("y[%d]=%.0f ",j,y[j]);// Scan #0 0 1 2 3
printf("\n"); // #1 4 6 8 10
} // #2 12 15 18 21
free(y); // #3 24 28 32 36
free(x);
MPI_Finalize();
return 0;
}

Высокопроизводительные вычислительные системы и параллельное программирование. Кудерметов Р.К.

Вам также может понравиться