В вычислительных
Решение всех перечисленных вопросов и обеспечивает интерфейс передачи данных ( message passing interface - MPI ).
Подобный способ организации параллельных вычислений получил наименование модели "одна программа множество процессов" ( single program multiple processes or SPMP ) ).
Следует отметить, что попытки создания программных средств передачи данных между процессорами начались предприниматься практически сразу с появлением локальных компьютерных сетей - ряд таких средств, представлен, например, в Воеводин В.В. и Воеводин Вл.В. (2002), Buyya (1999), Andrews (2000) и многих других. Однако подобные средства часто были неполными и, самое главное, являлись несовместимыми. Таким образом, одна из самых серьезных проблем в программировании - переносимость программ при переводе программного обеспечения на другие компьютерные системы - проявлялась при разработке параллельных программ в самой
Итак, теперь можно пояснить, что означает понятие
Вопросы, связанные с разработкой параллельных программ с использованием
Рассмотрим ряд понятий и определений, являющихся основополагающими для стандарта
Под параллельной программой в рамках
Каждый процесс параллельной программы порождается на основе копии одного и того же программного кода ( модель SPMP ). Данный программный код, представленный в виде исполняемой программы, должен быть доступен в момент запуска параллельной программы на всех используемых процессорах. Исходный программный код для исполняемой программы разрабатывается на
Количество процессов и число используемых процессоров определяется в момент запуска параллельной программы средствами среды исполнения np-1, где np есть общее количество процессов. Номер процесса именуется рангом процесса.
Основу point-to-point ) операции между двумя процессами и коллективные ( collective ) коммуникационные действия для одновременного взаимодействия нескольких процессов.
Для выполнения парных операций могут использоваться разные режимы передачи, среди которых синхронный, блокирующий и др. - полное рассмотрение возможных
Как уже отмечалось ранее, стандарт
Процессы параллельной программы объединяются в группы. Под коммуникатором в
Как правило, парные
В ходе вычислений могут создаваться новые и удаляться существующие MPI_COMM_WORLD.
При необходимости передачи данных между процессами из разных групп необходимо создавать глобальный ).
Подробное рассмотрение возможностей
При выполнении операций передачи сообщений для указания передаваемых или получаемых данных в функциях
Подробное рассмотрение возможностей
Как уже отмечалось ранее, парные
Вместе с этим (и это уже отмечалось в разделе 3), для изложения и последующего анализа ряда
В
Кроме того, в
И, наконец, последний ряд замечаний перед началом рассмотрения
MPI -1 ); дополнительные свойства стандарта версии 2.0 буду представлены в п. 6.8.3.Приступая к изучению
Приведем минимально-необходимый набор функций
Первой вызываемой функцией
int MPI_Init ( int *agrc, char ***argv ).
для инициализации среды выполнения
Последней вызываемой функцией
int MPI_Finalize (void)
Как результат, можно отметить, что структура параллельной программы, разработанная с использованием
#include "mpi.h"
int main ( int argc, char *argv[] ) {
<программный код без использования MPI функций>
MPI_Init ( agrc, argv );
<программный код с использованием MPI функций >
MPI_Finalize();
<программный код без использования MPI функций >
return 0;
}
Следует отметить:
mpi .h содержит определения MPI_Init и MPI_Finalize являются обязательными и должны быть выполнены (и только один раз) каждым процессом параллельной программы,MPI_Init может быть использована функция MPI_Initialized для определения того, был ли ранее выполнен вызов MPI_Init.Рассмотренные примеры функций дают представление синтаксиса именования функций в
Определение количества процессов в выполняемой параллельной программе осуществляется при помощи функции:
int MPI_Comm_size ( MPI_Comm comm, int *size ).
Для определения ранга процесса используется функция:
int MPI_Comm_rank ( MPI_Comm comm, int *rank ).
Как правило, вызов функций MPI_Comm_size и MPI_Comm_rank выполняется сразу после MPI_Init:
#include "mpi.h"
int main ( int argc, char *argv[] ) {
int ProcNum, ProcRank;
<программный код без использования MPI функций>
MPI_Init ( agrc, argv );
MPI_Comm_size ( MPI_COMM_WORLD, ProcNum);
MPI_Comm_rank ( MPI_COMM_WORLD, ProcRank);
<программный код с использованием MPI функций >
MPI_Finalize();
<программный код без использования MPI функций >
return 0;
}
Следует отметить:
MPI_COMM_WORLD, как отмечалось ранее, создается по умолчанию и представляет все процессы выполняемой параллельной программы,MPI_Comm_rank, является рангом процесса, выполнившего вызов этой функции, т.е. переменная ProcRank будет принимать различные значения в разных процессах.Для передачи сообщения процесс-отправитель должен выполнить функцию:
int MPI_Send(void *buf, int count, MPI_Datatype type, int dest, int tag, MPI_Comm comm),
где
buf - адрес буфера памяти, в котором располагаются данные отправляемого сообщения,count - количество элементов данных в сообщении,type - тип элементов данных пересылаемого сообщения,dest - ранг процесса, которому отправляется сообщение,tag - значение-тег, используемое для идентификации сообщений,comm - Для указания типа пересылаемых данных в имеется ряд базовых типов, полный список которых приведен в табл. 6.1.
MPI_Datatype
| C
|
|---|---|
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 |
|
MPI_UNSIGNED |
|
MPI_UNSIGNED_LONG |
|
MPI_UNSIGNED_SHORT |
|
Следует отметить:
( buf, count, type )
входит в состав параметров практически всех функций передачи данных.
MPI_Send.tag используется только при необходимости различения передаваемых сообщений, в противном случае в качестве значения параметра может быть использовано произвольное целое число (см. также описание функции MPI_Recv ).Сразу же после завершения функции MPI_Send процесс-отправитель может начать повторно использовать буфер памяти, в котором располагалось отправляемое сообщение. Вместе с этим, следует понимать, что в момент завершения функции MPI_Send состояние самого пересылаемого сообщения может быть совершенно различным - сообщение может располагаться в процессе-отправителе, может находиться в процессе передачи, может храниться в процессе-получателе или же может быть принято процессом-получателем при помощи функции MPI_Recv. Тем самым, завершение функции MPI_Send означает лишь, что операция передачи начала выполняться и
Пример использования функции будет представлен после описания функции MPI_Recv.
Для приема сообщения процесс-получатель должен выполнить функцию:
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 - указатель на структуру данных с информацией о результате выполнения операции приема данных.Следует отметить:
source может быть указано значение MPI_ANY_SOURCE,tag может быть указано значение MPI_ANY_TAG,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 является блокирующей для процесса-получателя, т.е. его выполнение приостанавливается до завершения работы функции. Таким образом, если по каким-то причинам ожидаемое для приема сообщение будет отсутствовать, выполнение параллельной программы будет блокировано.
Рассмотренный набор функций оказывается достаточным для разработки параллельных программ ). Приводимая ниже программа является стандартным начальным примером для
#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;
}
Как следует из текста программы, каждый процесс определяет свой ранг, после чего действия в программе разделяются. Все процессы, кроме процесса с рангом 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_Send исполняется только процессом с рангом 0, второй участок с функцией приема MPI_Recv используется всеми процессами, за исключением нулевого процесса.
Для разделения фрагментов кода между процессами обычно используется подход, примененный в только что рассмотренной программе - при помощи функции MPI_Comm_rank определяется ранг процесса, а затем в соответствии с рангом выделяются необходимые для процесса участки программного кода. Наличие в одной и той же программе фрагментов кода разных процессов также значительно усложняет понимание и, в целом, разработку
MPI_Comm_rank(MPI_COMM_WORLD, ProcRank);
if ( ProcRank == 0 ) DoProcess0();
else if ( ProcRank == 1 ) DoProcess1();
else if ( ProcRank == 2 ) DoProcess2();
Во многих случаях, как и в рассмотренном примере, выполняемые действия являются отличающимися только для процесса с рангом 0. В этом случае общая схема
MPI_Comm_rank(MPI_COMM_WORLD, ProcRank);
if ( ProcRank == 0 ) DoManagerProcess();
else DoWorkerProcesses();
В завершение обсуждения примера поясним использованный в MPI_SUCCESS. Другие значения
MPI_ERR_BUFFER - неправильный указатель на буфер,MPI_ERR_COMM - неправильный MPI_ERR_RANK - неправильный ранг процесса,и др. - полный список констант для проверки .
Практически сразу же после разработки первых параллельных программ возникает необходимость определения времени выполнения вычислений для оценки достигаемого ускорения процессов решения задач за счет использования параллелизма. Используемые обычно средства для измерения времени работы программ зависят, как правило, от аппаратной платформы, операционной системы,
Получение времени текущего момента выполнения программы обеспечивается при помощи функции:
double MPI_Wtime(void),
результат вызова которой есть количество секунд, прошедшее от некоторого определенного момента времени в прошлом. Этот момент времени в прошлом, от которого происходит отсчет секунд, может зависеть от среды реализации библиотеки MPI_Wtime следует использовать только для определения длительности выполнения тех или иных фрагментов кода параллельных программ. Возможная схема применения функции MPI_Wtime может состоять в следующем:
double t1, t2, dt;
t1 = MPI_Wtime();
…
t2 = MPI_Wtime();
dt = t2 - t1;
Точность измерения времени также может зависеть от среды выполнения параллельной программы. Для определения текущего значения точности может быть использована функция:
double MPI_Wtick(void),
позволяющая определить время в секундах между двумя последовательными показателями времени аппаратного таймера используемой компьютерной системы.
Функции MPI_Send и MPI_Recv, рассмотренные в п. 6.2.1, обеспечивают возможность выполнения парных операций передачи данных между двумя процессами параллельной программы. Для выполнения коммуникационных коллективных операций, в которых принимают участие все процессы
Для демонстрации примеров применения рассматриваемых функций x (см. подраздел 2.5):
Разработка
Первая проблема при выполнении рассмотренного x всем процессам параллельной программы. Конечно, для решения этой проблемы можно воспользоваться рассмотренными ранее функциями парных
MPI_Comm_size(MPI_COMM_WORLD,ProcNum); for (i=1; i<ProcNum; i++) MPI_Send(x,n,MPI_DOUBLE,i,0,MPI_COMM_WORLD);
Однако такое решение будет крайне неэффективным, поскольку повторение операций передачи приводит к суммированию затрат (log2p итераций передачи данных.
Достижение эффективного выполнения
int MPI_Bcast(void *buf,int count,MPI_Datatype type,int root,MPI_Comm comm),
где
buf, count, type - буфер памяти с отправляемым сообщением (для процесса с рангом 0), и для приема сообщений для всех остальных процессов,root - ранг процесса, выполняющего рассылку данных,comm - Функция MPI_Bcast осуществляет рассылку данных из буфера buf, содержащего count элементов типа type с процесса, имеющего номер root, всем процессам, входящим в (см. рис. 6.1).
(рис 6.1) Общая схема операции передачи данных от одного процесса всем процессамСледует отметить:
MPI_Bcast определяет коллективную операцию и, тем самым, при выполнении необходимых рассылок данных вызов функции MPI_Bcast должен быть осуществлен всеми процессами указываемого MPI_Bcast буфер памяти имеет различное назначение в разных процессах. Для процесса с рангом root, с которого осуществляется рассылка данных, в этом буфере должно находиться рассылаемое сообщение. Для всех остальных процессов указываемый буфер предназначен для приема передаваемых данных.Приведем программу для решения учебной задачи суммирования элементов вектора с использованием рассмотренной функции.
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "mpi.h"
int main(int argc, char* argv[]){
double x[100], TotalSum, ProcSum = 0.0;
int ProcRank, ProcNum, N=100;
MPI_Status Status;
// инициализация
MPI_Init(argc,argv);
MPI_Comm_size(MPI_COMM_WORLD,ProcNum);
MPI_Comm_rank(MPI_COMM_WORLD,ProcRank);
// подготовка данных
if ( ProcRank == 0 ) DataInitialization(x,N);
// рассылка данных на все процессы
MPI_Bcast(x, N, MPI_DOUBLE, 0, MPI_COMM_WORLD);
// вычисление частичной суммы на каждом из процессов
// на каждом процессе суммируются элементы вектора x от i1 до i2
int k = N / ProcNum;
int i1 = k * ProcRank;
int i2 = k * ( ProcRank + 1 );
if ( ProcRank == ProcNum-1 ) i2 = N;
for ( int i = i1; i < i2; i++ )
ProcSum = ProcSum + x[i];
// сборка частичных сумм на процессе с рангом 0
if ( ProcRank == 0 ) {
TotalSum = ProcSum;
for ( int i=1; i < ProcNum; i++ ) {
MPI_Recv(ProcSum, 1, MPI_DOUBLE, MPI_ANY_SOURCE, 0, MPI_COMM_WORLD,
Status);
TotalSum = TotalSum + ProcSum;
}
}
else // все процессы отсылают свои частичные суммы
MPI_Send(ProcSum, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD);
// вывод результата
if ( ProcRank == 0 )
printf("\nTotal Sum = %10.2f",TotalSum);
MPI_Finalize();
}
В приведенной программе функция DataInitialization осуществляет подготовку начальных данных. Необходимые данные могут быть введены с клавиатуры, прочитаны из файла или сгенерированы при помощи датчика случайных чисел - подготовка этой функции предоставляется как задание для самостоятельной разработки.
В рассмотренной программе суммирования числовых значений имеющаяся процедура сбора и последующего суммирования данных является примером часто выполняемой коллективной операции передачи данных от всех процессов одному процессу. В этой операции над собираемыми значениями осуществляется та или иная обработка данных (для подчеркивания последнего момента данная операция еще именуется операцией редукции данных ). Как и ранее, реализация операции
int MPI_Reduce(void *sendbuf, void *recvbuf,int count,MPI_Datatype type, MPI_Op op,int root,MPI_Comm comm),
где
sendbuf - буфер памяти с отправляемым сообщением,recvbuf - буфер памяти для результирующего сообщения (только для процесса с рангом root),count - количество элементов в сообщениях,type - тип элементов сообщений,op - операция, которая должна быть выполнена над данными,root - ранг процесса, на котором должен быть получен результат,comm - В качестве операций
| Операция | Описание |
|---|---|
MPI_MAX |
Определение максимального значения |
MPI_MIN |
Определение минимального значения |
MPI_SUM |
Определение суммы значений |
MPI_PROD |
Определение произведения значений |
MPI_LAND |
Выполнение логической операции "И" над значениями сообщений |
MPI_BAND |
Выполнение |
MPI_LOR |
Выполнение логической операции "ИЛИ" над значениями сообщений |
MPI_BOR |
Выполнение |
MPI_LXOR |
Выполнение логической операции исключающего "ИЛИ" над значениями сообщений |
MPI_BXOR |
Выполнение |
MPI_MAXLOC |
Определение максимальных значений и их индексов |
MPI_MINLOC |
Определение минимальных значений и их индексов |
Помимо данного стандартного набора операций могут быть определены и новые дополнительные операции непосредственно самим пользователем библиотеки
Общая схема выполнения операции сбора и обработки данных на одном процессоре показана на рис. 6.2. Элементы получаемого сообщения на процессе root представляют собой результаты обработки соответствующих элементов передаваемых процессами сообщений, т.е.
где $$\otimes$$ есть операция, задаваемая при вызове функции MPI_Reduce (для пояснения на рис. 6.3. показан пример выполнения операции
(рис 6.2) Общая схема операции сбора и обработки на одном процессе данных от всех процессов Следует отметить:
MPI_Reduce определяет коллективную операцию и, тем самым, вызов функции должен быть выполнен всеми процессами указываемого count, type, op , root, comm ,root,Применим полученные знания для переработки ранее рассмотренной программы суммирования - как можно увидеть, весть программный код, выделенный двойной рамкой, может быть теперь заменен на вызов одной лишь функции MPI_Reduce:
// сборка частичных сумм на процессе с рангом 0 MPI_Reduce(ProcSum,TotalSum, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
В ряде ситуаций независимо выполняемые в процессах вычисления необходимо синхронизировать. Так, например, для измерения времени начала работы параллельной программы необходимо, чтобы для всех процессов одновременно были завершены все подготовительные действия, перед окончанием работы программы все процессы должны завершить свои вычисления и т.п.
Синхронизация процессов, т.е. одновременное достижение процессами тех или иных точек процесса вычислений, обеспечивается при помощи функции
int MPI_Barrier(MPI_Comm comm);
Функция MPI_Barrier определяет коллективную операции и, тем самым, при использовании должна вызываться всеми процессами используемого MPI_Barrier выполнение процесса блокируется, продолжение вычислений процесса произойдет только после вызова функции MPI_Barrier всеми процессами
Продолжим начатое в п. 6.2.1 изучение функций
Рассмотренная ранее функция MPI_Send обеспечивает так называемый стандартный ( Standard ) режим отправки сообщений, при котором (см. также п. 6.2.1.3):
MPI_Recv.Кроме стандартного режима в
Для именования функций отправки сообщения для разных режимов выполнения в MPI_Send, к которому как префикс добавляется начальный символ названия соответствующего режима работы, т.е.
MPI_Ssend - функция отправки сообщения в синхронном режиме,MPI_Bsend - функция отправки сообщения в буферизованном режиме,MPI_Rsend - функция отправки сообщения в режиме по готовности.Список параметров всех перечисленных функций совпадает с составом параметров функции MPI_Send.
Для использования буферизованного режима передачи должны быть создан и передан
int MPI_Buffer_attach(void *buf, int size),
где
buf - буфер памяти для буферизации сообщений,size - размер буфера.После завершения работы с буфером он должен быть отключен от
int MPI_Buffer_detach(void *buf, int *size).
По практическому использованию режимов можно привести следующие рекомендации:
В заключение отметим, что для функции приема MPI_Recv не существует различных режимов работы.
Все рассмотренные ранее функции отправки и приема сообщений являются блокирующими, т.е. приостанавливающими выполнение процессов до момента завершения работы вызванных функций. В то же время при выполнении параллельных вычислений часть сообщений может быть отправлена и принята заранее до момента реальной потребности в пересылаемых данных. В таких ситуациях было бы крайне желательным иметь возможность выполнения функций обмена данными без блокировки процессов для совмещения процессов передачи сообщений и вычислений. Такой неблокирующий способ выполнения обменов является, конечно, более сложным для использования, но при правильном применении мог бы в значительной степени уменьшить потери эффективности параллельных вычислений из-за медленных (по сравнению с быстродействием процессоров) коммуникационных операций.
I ( Immediate ). Список параметров неблокирующих функций содержит весь набор параметров исходных функций и один дополнительный параметр request с типом MPI_Request (в функции MPI_Irecv отсутствует также параметр status ):
int MPI_Isend(void *buf, int count, MPI_Datatype type, int dest, int tag, MPI_Comm comm, MPI_Request *request) int MPI_Issend(void *buf, int count, MPI_Datatype type, int dest, int tag, MPI_Comm comm, MPI_Request *request) int MPI_Ibsend(void *buf, int count, MPI_Datatype type, int dest, int tag, MPI_Comm comm, MPI_Request *request) int MPI_Irsend(void *buf, int count, MPI_Datatype type, int dest, int tag, MPI_Comm comm, MPI_Request *request) int MPI_Irecv(void *buf, int count, MPI_Datatype type, int source, int tag, MPI_Comm comm, MPI_Request *request)
Вызов неблокирующей функции приводит к request, которая далее может использоваться для проверки завершения инициированной операции обмена.
Проверка состояния выполняемой неблокирующей
int MPI_Test( MPI_Request *request, int *flag, MPI_status *status),
где
request - дескриптор операции, определенный при вызове неблокирующей функции,flag - результат проверки (=true, если операция завершена),status - результат выполнения операции обмена (только для Операция проверки является неблокирующей, т.е. процесс может проверить состояние неблокирующей операции обмена и продолжить далее свои вычисления, если по результатам проверки окажется, что операция все еще не завершена. Возможная схема совмещения вычислений и выполнения неблокирующей операции обмена может состоять в следующем:
MPI_Isend(buf,count,type,dest,tag,comm,request);
…
do {
…
MPI_Test(request,flag,status)
} while ( !flag );
Если при выполнении неблокирующей операции окажется, что продолжение вычислений невозможно без получения передаваемых данных, то может быть использована блокирующая операция ожидания завершения операции:
int MPI_Wait( MPI_Request *request, MPI_status *status).
Кроме рассмотренных,
MPI_Testall - проверка завершения всех перечисленных операций обмена,MPI_Waitall - ожидание завершения всех операций обмена,MPI_Testany - проверка завершения хотя бы одной из перечисленных операций обмена,MPI_Waitany - ожидание завершения любой из перечисленных операций обмена,MPI_Testsome - проверка завершения каждой из перечисленных операций обмена,MPI_Waitsome - ожидание завершения хотя бы одной из перечисленных операций обмена и оценка состояния по всем операциям.Приведение простого примера использования неблокирующих функций достаточно затруднительно. Хорошей возможностью для освоения рассмотренных функций могут служить, например,
Одной из часто выполняемых форм информационного взаимодействия в параллельных программах является обмен данными между процессами, когда для продолжения вычислений процессам необходимо отправить данные одним процессам и, в то же время, получить сообщения от других процессов. Простейший вариант этой ситуации состоит, например, в обмене данными между двумя процессами. Реализация таких обменов при помощи обычных парных
Достижение эффективного и гарантированного одновременного выполнения операций передачи и приема данных может быть обеспечено при помощи функции
int MPI_Sendrecv(void *sbuf,int scount,MPI_Datatype stype,int dest, int stag,
void *rbuf,int rcount,MPI_Datatype rtype,int source,int rtag,
MPI_Comm comm, MPI_Status *status),
где
sbuf, scount, stype, dest, stag - параметры передаваемого сообщения,rbuf, rcount, rtype, source, rtag - параметры принимаемого сообщения,comm - status - структура данных с информацией о результате выполнения операции.Как следует из описания, функция MPI_Sendrecv передает сообщение, описываемое параметрами ( sbuf, scount, stype, dest, stag ), процессу с рангом dest и принимает сообщение в буфер, определяемый параметрами ( rbuf, rcount, rtype, source, rtag ), от процесса с рангом source.
В функции MPI_Sendrecv для передачи и приема сообщений применяются разные буфера. В случае же, когда сообщения имеют одинаковый тип, в
int MPI_Sendrecv_replace (void *buf, int count, MPI_Datatype type, int dest, int stag, int source, int rtag, MPI_Comm comm, MPI_Status* status).
Пример использование функций для одновременного выполнения операций передачи и приема приведен в разделе 8 при разработке параллельных программ матричного умножения.
Как уже отмечалось ранее, под коллективными операциями в
Рассмотрим далее оставшиеся базовые коллективные
Обобщенная
int MPI_Scatter(void *sbuf,int scount,MPI_Datatype stype,
void *rbuf,int rcount,MPI_Datatype rtype,
int root, MPI_Comm comm),
где
sbuf, scount, stype - параметры передаваемого сообщения ( scount определяет
количество элементов, передаваемых на каждый процесс),rbuf, rcount, rtype - параметры сообщения, принимаемого в процессах,root - ранг процесса, выполняющего рассылку данных,comm -
(рис 6.4) Общая схема операции обобщенной передачи данных от одного процесса всем процессамПри вызове этой функции процесс с рангом root произведет передачу данных всем другим процессам в scount элементов. Процесс с рангом 0 получит блок данных из sbuf из элементов с индексами от 0 до scount-1, процессу с рангом 1 будет отправлен блок из элементов с индексами от scount до 2* scount-1 и т.д. Тем самым, общий размер отправляемого сообщения должен быть равен scount * p элементов, где p есть количество процессов в .
Следует отметить, поскольку функция MPI_Scatter определяет коллективную операцию, вызов этой функции при выполнении рассылки данных должен быть обеспечен в каждом процессе
Отметим также, что функция MPI_Scatter передает всем процессам сообщения одинакового размера. Выполнение более общего варианта операции распределения данных, когда MPI_Scatterv.
Пример использования функции MPI_Scatter рассматривается в разделе 7 при разработке параллельных программ
Операция обобщенной передачи данных от всех процессоров одному процессу ( сбор данных ) является обратной к процедуре распределения данных (см. рис. 6.5). Для выполнения этой операции в
int MPI_Gather(void *sbuf,int scount,MPI_Datatype stype,
void *rbuf,int rcount,MPI_Datatype rtype,
int root, MPI_Comm comm),
где
sbuf, scount, stype - параметры передаваемого сообщения,rbuf, rcount, rtype - параметры принимаемого сообщения,root - ранг процесса, выполняющего сбор данных,comm -
(рис 6.5) Общая схема операции обобщенной передачи данных от всех процессов одному процессуПри выполнении функции MPI_Gather каждый процесс в sbuf на процесс с рангом root. Процесс с рангом root собирает все получаемые данные в буфере rbuf (размещение данных в буфере осуществляется в соответствии с рангами процессов-rbuf должен быть равен scount * p элементов, где p есть количество процессов в .
Функция MPI_Gather также определяет коллективную операцию, и ее вызов при выполнении сбора данных должен быть обеспечен в каждом процессе
Следует отметить, что при использовании функции MPI_Gather сборка данных осуществляется только на одном процессе. Для получения всех собираемых данных на каждом из процессов функцию сбора и рассылки:
int MPI_Allgather(void *sbuf, int scount, MPI_Datatype stype,
void *rbuf, int rcount, MPI_Datatype rtype, MPI_Comm comm).
Выполнение общего варианта операции сбора данных, когда размеры передаваемых процессами сообщений могут быть различны, обеспечивается при помощи функций MPI_Gatherv и MPI_Allgatherv.
Пример использования функции MPI_Gather рассматривается в разделе 7 при разработке параллельных программ
Передача данных от всех процессов всем процессам является наиболее общей операцией передачи данных (см. рис. 6.6). Выполнение данной операции может быть обеспечено при помощи функции:
int MPI_Alltoall(void *sbuf,int scount,MPI_Datatype stype,
void *rbuf,int rcount,MPI_Datatype rtype,MPI_Comm comm),
где
sbuf, scount, stype - параметры передаваемых сообщений,rbuf, rcount, rtype - параметры принимаемых сообщенийcomm -
(рис 6.6) Общая схема операции передачи данных от всех процессов всем процессам (сообщения показываются обозначениями вида i в j, где i и j есть ранги передающих и принимающих процессов соответственноПри выполнении функции MPI_Alltoall каждый процесс в scount элементов каждому процессу (общий размер отправляемых сообщений в процессах должен быть равен scount * p элементов, где p есть количество процессов в ) и принимает сообщения от каждого процесса.
Вызов функции MPI_Alltoall при выполнении операции общего обмена данными должен быть выполнен в каждом процессе
Вариант операции общего обмена данных, когда размеры передаваемых процессами сообщений могут быть различны, обеспечивается при помощи функций MPI_Alltoallv.
Пример использования функции MPI_Alltoall рассматривается в разделе 7 при разработке параллельных программ
Рассмотренная в п. 6.2.3.2 функция MPI_Reduce обеспечивает получение результатов функцию :
int MPI_Allreduce(void *sendbuf, void *recvbuf,int count,MPI_Datatype type, MPI_Op op,MPI_Comm comm).
Функция MPI_AllReduce выполняет рассылку между процессами всех результатов операции MPI_Reduce_scatter.
И еще один вариант операции сбора и обработки данных, при котором обеспечивается получение и всех
int MPI_Scan(void *sendbuf, void *recvbuf,int count,MPI_Datatype type, MPI_Op op,MPI_Comm comm).
Общая схема выполнения функции MPI_Scan показана на рис. 6.7. Элементы получаемых сообщений представляют собой результаты обработки соответствующих элементов передаваемых процессами сообщений, при этом для получения результатов на процессе с рангом i, 0 i<n,используются данные от процессов, ранг которых меньше или равен i,т.е.
где $$\otimes$$ есть операция, задаваемая при вызове функции MPI_Scan.
(рис 6.7) Общая схема операции редукции с получением частичных результатов обработки данных
Для
| Вид коллективной операции | Общее описание и оценка сложности | Функция MPI | Примеры использования |
|---|---|---|---|
| Передача от одного процесса всем процессам ( широковещательная рассылка ) | п.4.2.5 | MPI_Bcast
п. 6.2.3.1 |
п. 6.2.3.1 |
| Сбор и обработка данных на одном процессе от всех процессов ( редукция данных ) | пп. 4.2.5, 4.2.6 | MPI_Reduce
п. 6.2.3.2 |
п. 6.2.3.2 |
| - то же с рассылкой результатов всем процессам | пп. 4.2.5, 4.2.6 | MPI_Allreduce
MPI_Reduce_scatter
п. 6.4.4 |
|
| - то же с получением |
пп. 4.2.5, 4.2.6 | MPI_Scan
п. 6.4.4 |
|
| Обобщенная передача от одного процесса всем процессам ( распределение данных ) | п. 4.2.7 | MPI_Scatter
MPI_Scatterv
п. 6.4.1 |
Раздел 7 |
| Обобщенная передача от всех процессов одному процессу ( сбор данных ) | п. 4.2.7 | MPI_Gather
MPI_Gatherv
п. 6.4.2 |
Раздел 7 |
| - то же с рассылкой результатов всем процессам | п. 4.2.7 | MPI_Allgather
MPI_Allgatherv
п. 6.4.2 |
|
| Общая передача данных от всех процессов всем процессам | п. 4.2.8 | MPI_Alltoall
MPI_Alltoallv
п. 6.4.3 |
Раздел 7 |
Во всех ранее рассмотренных примерах использования функций передачи данных предполагалось, что сообщения представляют собой некоторый непрерывный вектор элементов предусмотренного в
Для обеспечения больших возможностей при определении состава передаваемых сообщений в
В самом общем виде под производным типом данных в type map ) в виде последовательности описаний входящих в тип значений, каждое отдельное значение описывается указанием типа и смещения адреса месторасположения от некоторого базового адреса, т.е.
TypeMap = {(type0,disp0),… , (typen-1,dispn-1)}.
Часть карты типа с указанием только типов значений именуется в
TypeSignature = {type0,… , typen-1}
Сигнатура типа описывает, какие
Поясним рассмотренные понятия на следующем примере. Пусть в сообщение должны входить значения переменных:
double a; /* адрес 24 */ double b; /* адрес 40 */ int n; /* адрес 48 */
Тогда
{(MPI_DOUBLE,0),
(MPI_DOUBLE,16),
(MPI_INT,24)
}
Дополнительно для
lb(TypeMap) = minj(dispj)
extent (TypeMap) = ub(TypeMap)-lb(TypeMap).Согласно определению нижняя граница есть смещение для первого байта значений рассматриваемого типа данных. Соответственно верхняя граница представляет собой смещение для байта, располагающегося вслед за последним элементом рассматриваемого типа данных. При этом величина смещения для верхней границы может быть округлена вверх с учетом требований выравнивания адресов. Так, одно из самых общих требований, которые налагают реализации языков C и Fortran, состоит в том, чтобы адрес элемента был кратен длине этого элемента в байтах. Например, если тип int занимает четыре байта, то адрес на элемент типа int должен нацело делиться на четыре. Именно это требование и отражается в определении верхней границы типа данных a,b и n, для которого нижняя граница равна 0, а верхняя граница принимает значение 32 (величина округления 6 или 4 в зависимости от размера типа int ). Здесь следует отметить, что требуемое выравнивание определяется по типу первого элемента данных в карте типа.
Следует также указать на различие понятий протяженности и размера типа. Протяженность - это размер памяти в байтах, который нужно отводить для одного элемента int занимает четыре байта).
Для получения значения протяженности и размера типа в
int MPI_Type_extent ( MPI_Datatype type, MPI_Aint *extent ), int MPI_Type_size ( MPI_Datatype type, MPI_Aint *size ).
Определение нижней и верхней границ типа может быть выполнено при помощи функций:
int MPI_Type_lb ( MPI_Datatype type, MPI_Aint *disp ), int MPI_Type_ub ( MPI_Datatype type, MPI_Aint *disp ).
Важной и необходимой при конструировании
int MPI_Address ( void *location, MPI_Aint *address )
(следует отметить, что данная функция является переносимым вариантом средств получения адресов в
Для снижения сложности в
Далее перечисленные способы конструирования
При непрерывном способе конструирования
int MPI_Type_contiguous(int count,MPI_Data_type oldtype,MPI_Datatype *newtype).
Как следует из описания, новый тип newtype создается как count элементов исходного типа oldtype. Например, если исходный тип данных имеет карту типа
{ (MPI_INT,0),(MPI_DOUBLE,8) },
то вызов функции MPI_Type_contiguous с параметрами
MPI_Type_contiguous (2, oldtype, newtype);
приведет к созданию типа данных с картой типа
{ (MPI_INT,0),(MPI_DOUBLE,8),(MPI_INT,16),(MPI_DOUBLE,24) }.
В определенном плане наличие непрерывного способа конструирования является избыточным, поскольку использование аргумента count в процедурах
При векторном способе конструирования
int MPI_Type_vector ( int count, int blocklen, int stride, MPI_Data_type oldtype, MPI_Datatype *newtype ),
где
count - количество блоков,blocklen - размер каждого блока,stride - количество элементов, расположенных между двумя соседними блокамиoldtype - исходный тип данных,newtype - новый определяемый тип данных.int MPI_Type_hvector ( int count, int blocklen, MPI_Aint stride, MPI_Data_type oldtype, MPI_Datatype *newtype ).
Отличие способа конструирования, определяемого функцией MPI_Type_hvector, состоит лишь в том, что параметр stride для определения интервала между блоками задается в байтах, а не в элементах исходного типа данных.
Как следует из описания, при векторном способе новый
n x n:MPI_Type_vector ( n/2, n, 2*n, StripRowType, ElemType ),
n x n:MPI_Type_vector ( n, 1, n, ColumnType, ElemType ),
n x n:MPI_Type_vector ( n, 1, n+1, DiagonalType, ElemType ).
С учетом характера приводимых примеров можно упомянуть имеющуюся в
int MPI_Type_create_subarray ( int ndims, int *sizes, int *subsizes, int *starts, int order, MPI_Data_type oldtype, MPI_Datatype *newtype ),
где
ndims - sizes - количество элементов в каждой размерности исходного массива,subsizes - количество элементов в каждой размерности определяемого подмассива,starts - индексы начальных элементов в каждой размерности определяемого подмассива,order - параметр для указания необходимости переупорядочения,oldtype - тип данных элементов исходного массива,newtype - новый тип данных для описания подмассива.При индексном способе конструирования
int MPI_Type_indexed ( int count, int blocklens[], int indices[], MPI_Data_type oldtype, MPI_Datatype *newtype ),
где
count - количество блоков,blocklens - количество элементов в каждов блоке,indices - смещение каждого блока от начала типа (в количестве элементов исходного типа),oldtype - исходный тип данных,newtype - новый определяемый тип данных.int MPI_Type_hindexed ( int count, int blocklens[], MPI_Aint indices[], MPI_Data_type oldtype, MPI_Datatype *newtype )
Как следует из описания, при индексном способе новый n x n:
// конструирование типа для описания верхней треугольной матрицы
for ( i=0, i<n; i++ ) {
blocklens[i] = n - i;
indices[i] = i * n + i;
}
MPI_Type_indexed ( n, blocklens, indices, UTMatrixType, ElemType ).
Как и ранее, способ конструирования, определяемый функцией MPI_Type_hindexed, отличается тем, что элементы для определения интервалов между блоками задаются в байтах, а не в элементах исходного типа данных.
Следует отметить, что существует еще одна дополнительная функция MPI_Type_create_indexed_block индексного способа конструирования для определения типов с блоками одинакового размера (данная функция предусматривается стандартом
Как отмечалось ранее,
int MPI_Type_struct ( int count, int blocklens[], MPI_Aint indices[], MPI_Data_type oldtypes[], MPI_Datatype *newtype ),
где
count - количество блоков,blocklens - количество элементов в каждов блоке,indices - смещение каждого блока от начала типа (в байтах),oldtypes - исходные типы данных в каждом блоке в отдельности,newtype - новый определяемый тип данных.Как следует из описания,
Рассмотренные в предыдущем пункте функции конструирования позволяют определить
int MPI_Type_commit (MPI_Datatype *type ).
При завершении использования
int MPI_Type_free (MPI_Datatype *type ).
Наряду с рассмотренными в п. 4.5.2 методами конструирования
Для использования данного подхода должен быть определен буфер памяти достаточного размера для сборки сообщения. Входящие в состав сообщения данные должны быть упакованы в буфер при помощи функции:
int MPI_Pack ( void *data, int count, MPI_Datatype type, void *buf, int bufsize, int *bufpos, MPI_Comm comm),
где
data - буфер памяти с элементами для упаковки,count - количество элементов в буфере,type - тип данных для упаковываемых элементов,buf - буфер памяти для упаковки,buflen - размер буфера в байтах,bufpos - позиция для начала записи в буфер (в байтах от начала буфера),comm - Функция MPI_Pack упаковывает count элементов из буфера data в буфер упаковки buf, начиная с позиции bufpos. Общая схема процедуры упаковки показана на рис. 6.8а.
(рис 6.8) Общая схема упаковки и распаковки данныхНачальное значение переменной bufpos должно быть сформировано до начала упаковки и далее устанавливается функцией MPI_Pack. Вызов функции MPI_Pack осуществляется последовательно для упаковки всех необходимых данных. Так, для ранее рассмотренного примера набора переменных a,b и n, для их упаковки необходимо выполнить:
bufpos = 0; MPI_Pack(a,1,MPI_DOUBLE,buf,buflen,bufpos,comm); MPI_Pack(b,1,MPI_DOUBLE,buf,buflen,bufpos,comm); MPI_Pack(n,1,MPI_INT,buf,buflen,bufpos,comm);
Для определения необходимого размера буфера для упаковки может быть использована функция:
int MPI_Pack_size (int count, MPI_Datatype type, MPI_Comm comm, int *size),
которая в параметре size указывает необходимый размер буфера для упаковки count элементов типа type.
После упаковки всех необходимых данных подготовленный буфер может быть использован в функциях передачи данных с указанием типа MPI_PACKED.
После получения сообщения с типом MPI_PACKED данные могут быть распакованы при помощи функции:
int MPI_Unpack (void *buf, int bufsize, int *bufpos, void *data, int count, MPI_Datatype type, MPI_Comm comm),
где
buf - буфер памяти с упакованными данными,buflen - размер буфера в байтах,bufpos - позиция начала данных в буфере (в байтах от начала буфера),data - буфер памяти для распаковываемых данных,count - количество элементов в буфере,type - тип распаковываемых данных,comm - Функция MPI_Unpack распаковывает начиная с позиции bufpos очередную порцию данных из буфера buf и помещает распакованные данные в буфер data. Общая схема процедуры распаковки показана на рис. 6.8б.
Начальное значение переменной bufpos должно быть сформировано до начала распаковки и далее устанавливается функцией MPI_Unpack. Вызов функции MPI_Unpack осуществляется последовательно для распаковки всех упакованных данных, при этом порядок распаковки должен соответствовать порядку упаковки. Так, для ранее рассмотренного примера упаковки для распаковки упакованных данных необходимо выполнить:
bufpos = 0; MPI_Pack(buf,buflen,bufpos,a,1,MPI_DOUBLE,comm); MPI_Pack(buf,buflen,bufpos,b,1,MPI_DOUBLE,comm); MPI_Pack(buf,buflen,bufpos,n,1,MPI_INT,comm);
В заключение выскажем ряд рекомендаций по использованию упаковки для формирования сообщений. Поскольку такой подход приводит к появлению дополнительных действий по упаковке и распаковке данных, то данный способ может быть оправдан при сравнительно небольших
Рассмотрим теперь возможности
Для изложения последующего материала напомним ряд понятий и определений, приведенных в начале данного раздела.
Процессы параллельной программы объединяются в группы. В группу могут входить все процессы параллельной программы; с другой стороны, в группе может находиться только часть имеющихся процессов. Соответственно, один и тот же процесс может принадлежать нескольким группам. Управление
Под коммуникатором в контекст ), используемых при выполнении
Все имеющиеся в параллельной программе процессы входят в состав создаваемого по умолчанию MPI_COMM_WORLD.
При необходимости передачи данных между процессами из разных групп необходимо создавать глобальный ). Взаимодействие между процессами разных групп оказывается необходимым в достаточно редких ситуациях, в данном учебном материале не рассматривается и может служить темой для самостоятельно изучения - см., например, Немнюгин и Стесик (2002), Group, et al. (1994), Pacheco (1996).
MPI_COMM_WORLD.
Для получения группы, связанной с существующим
int MPI_Comm_group ( MPI_Comm comm, MPI_Group *group ).
Далее, на основе существующих групп, могут быть созданы новые группы:
newgroup из существующей группы oldgroup, которая будет включать в себя n процессов, ранги которых перечисляются в массиве ranks :int MPI_Group_incl(MPI_Group oldgroup,int n, int *ranks,MPI_Group *newgroup),
newgroup из группы oldgroup, которая будет включать в себя n процессов, ранги которых не совпадают с рангами, перечисленными в массиве ranks :int MPI_Group_excl(MPI_Group oldgroup,int n, int *ranks,MPI_Group *newgroup).
Для получения новых групп над имеющимися
newgroup как объединения групп group1 и group2:int MPI_Group_union(MPI_Group group1, MPI_Group group2, MPI_Group *newgroup);
newgroup как пересечения групп group1 и group2:int MPI_Group_intersection ( MPI_Group group1, MPI_Group group2, MPI_Group *newgroup ),
newgroup как разности групп group1 и group2:int MPI_Group_difference ( MPI_Group group1, MPI_Group group2, MPI_Group *newgroup ).
При конструировании групп может оказаться полезной специальная пустая группа MPI_COMM_EMPTY.
Ряд функций
int MPI_Group_size ( MPI_Group group, int *size ),
int MPI_Group_rank ( MPI_Group group, int *rank ).
После завершения использования группа должна быть удалена:
int MPI_Group_free ( MPI_Group *group )
(выполнение данной операции не затрагивает
Отметим прежде всего, что в данном пункте рассматривается управление интракоммуникаторами, используемыми для
Для создания новых
int MPI_Comm_dup ( MPI_Comm oldcom, MPI_comm *newcomm ),
int MPI_comm_create (MPI_Comm oldcom, MPI_Group group, MPI_Comm *newcomm).
Дублирование
Следует отметить также, что операция создания
Для пояснения рассмотренных функций можно привести пример создания MPI_COMM_WORLD (такой
MPI_Group WorldGroup, WorkerGroup; MPI_Comm Workers; int ranks[1]; ranks[0] = 0; // получение группы процессов в MPI_COMM_WORLD MPI_Comm_group(MPI_COMM_WORLD, WorldGroup); // создание группы без процесса с рангом 0 MPI_Group_excl(WorldGroup, 1, ranks, WorkerGroup); // Создание коммуникатора по группе MPI_Comm_create(MPI_COMM_WORLD,WorkerGroup,Workers); ... MPI_Group_free(WorkerGroup); MPI_Comm_free(Workers);
Быстрый и полезный способ одновременного создания нескольких
int MPI_Comm_split ( MPI_Comm oldcomm, int split, int key, MPI_Comm *newcomm ),
где
oldcomm - исходный split - номер key - порядок ранга процесса в создаваемом newcomm - создаваемый Создание MPI_Comm_split должен быть выполнен в каждом процессе oldcomm. В результате выполнения функции процессы разделяются на непересекающиеся группы с одинаковыми значениями параметра split. На основе сформированных групп создается набор key (процесс с большим значением параметра key должен иметь больший ранг).
В качестве примера можно рассмотреть задачу представления набора процессов в виде двумерной решетки. Пусть p=q*q есть общее количество процессов, следующий далее фрагмент программы обеспечивает получение
MPI_Comm comm; int rank, row; MPI_Comm_rank(MPI_COMM_WORLD,rank); row = rank/q; MPI_Comm_split(MPI_COMM_WORLD,row,rank,comm);
При выполнении данного примера, например, при p=9, процессы с рангами (0,1,2) образуют первый
После завершения использования
int MPI_Comm_free ( MPI_Comm *comm ).
Под топологией вычислительной системы обычно понимается структура узлов сети и линий связи между этими узлами. Топология может быть представлена в виде графа, в котором вершины есть процессоры (процессы) системы, а дуги соответствуют имеющимся линиям (каналам) связи.
Как уже отмечалось ранее, парные
Понятно, что физическая топология системы является аппаратно реализуемой и изменению не подлежит (хотя существуют и программируемые средства построения сетей). Но, оставляя неизменной физическую основу, мы можем организовать логическое представление любой необходимой виртуальной топологии. Для этого достаточно, например, сформировать тот или иной механизм дополнительной
Использование виртуальных процессов может оказаться полезным в силу ряда разных причин.
В
Декартовы топологии, в которых множество процессов представляется в виде прямоугольной решетки (см. п. 1.4.1 и рис. 6.7), а для указания процессов используется декартова система координат, широко применяются во многих задачах для описания структуры имеющихся информационных зависимостей. В числе примеров таких задач - матричные алгоритмы (см. разделы 7 и 8) и сеточные методы
Для создания декартовой топологии (решетки) в
int MPI_Cart_create(MPI_Comm oldcomm, int ndims, int *dims, int *periods, int reorder, MPI_Comm *cartcomm),
где:
oldcomm - исходный ndims - размерность декартовой решетки,dims - массив длины ndims, задает количество процессов в каждом измерении решетки,periods - массив длины ndims, определяет, является ли решетка периодической вдоль каждого измерения,reorder - параметр допустимости изменения нумерации процессов,cartcomm - создаваемый Операция создания топологии является коллективной и, тем самым, должна выполняться всеми процессами исходного
Для пояснения назначения параметров функции MPI_Cart_create рассмотрим пример создания двухмерной решетки 4x4, в которой строки и столбцы имеют кольцевую структуру (за последним процессом следует первый процесс):
// создание двухмерной решетки 4x4 MPI_Comm GridComm; int dims[2], periods[2], reorder = 1; dims[0] = dims[1] = 4; periods[0] = periods[1] = 1; MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, reoreder, GridComm);
Следует отметить, что в силу кольцевой структуры измерений сформированная в рамках примера топология является тором.
Для определения декартовых координат процесса по его рангу можно воспользоваться функцией:
int MPI_Card_coords(MPI_Comm comm,int rank,int ndims,int *coords),
где:
comm - rank - ранг процесса, для которого определяются декартовы координаты,ndims - размерность решетки,coords - возвращаемые функцией декартовы координаты процесса.Обратное действие - определение ранга процесса по его декартовым координатам - обеспечивается при помощи функции:
int MPI_Cart_rank(MPI_Comm comm, int *coords, int *rank),
где
comm - coords - декартовы координаты процесса,rank - возвращаемый функцией ранг процесса.Полезная во многих приложениях процедура разбиения решетки на подрешетки меньшей размерности обеспечивается при помощи функции:
int MPI_Card_sub(MPI_Comm comm, int *subdims, MPI_Comm *newcomm),
где:
comm - исходный subdims - массив для указания, какие измерения должны остаться в создаваемой подрешетке,newcomm - создаваемый Операция создания подрешеток также является коллективной и, тем самым, должна выполняться всеми процессами исходного MPI_Cart_sub определяет
Для пояснения функции MPI_Cart_sub дополним ранее рассмотренный пример создания двухмерной решетки и определим
// создание коммуникаторов для каждой строки и столбца решетки MPI_Comm RowComm, ColComm; int subdims[2]; // создание коммуникаторов для строк subdims[0] = 0; // фиксации измерения subdims[1] = 1; // наличие данного измерения в подрешетке MPI_Cart_sub(GridComm, subdims, RowComm); // создание коммуникаторов для столбцов subdims[0] = 1; subdims[1] = 0; MPI_Cart_sub(GridComm, subdims, ColComm);
В приведенном примере для решетки размером 4х4 создаются 8 RowComm и ColComm соответствуют строке и столбцу процессов, к которым данный процесс принадлежит.
Дополнительная функция MPI_Cart_shift обеспечивает поддержку процедуры последовательной передачи данных по одному из измерений решетки ( операция сдвига данных - см. раздел 3). В зависимости от периодичности измерения решетки, по которому выполняется сдвиг, различаются два типа данной операции:
k элементов вдоль измерения решетки - в этой операции данные от процесса i пересылаются процессу (i+k) mod dim , где dim есть размер измерения, вдоль которого производится сдвиг,k позиций вдоль измерения решетки - в этом варианте операции данные от процессора i пересылаются процессору i+k (если таковой существует).Функция MPI_Cart_shift обеспечивает получение рангов процессов, с которыми текущий процесс (процесс, вызвавший функцию MPI_Cart_shift ) должен выполнить обмен данными:
int MPI_Card_shift(MPI_Comm comm, int dir, int disp, int *source, int *dst),
где:
comm - dir - номер измерения, по которому выполняется сдвиг,disp - величина сдвига ( <0 - сдвиг к началу измерения),source - ранг процесса, от которого должны быть получены данные,dst - ранг процесса которому должны быть отправлены данные.Следует отметить, что функция MPI_Cart_shift только определяет ранги процессов, между которыми должен быть выполнен обмен данными в ходе MPI_Sendrecv.
Сведения по функциям
Для создания
int MPI_Graph_create(MPI_Comm oldcomm, int nnodes, int *index, int *edges, int reorder, MPI_Comm *graphcomm),
где:
oldcomm - исходный nnodes - количество вершин графа,index - количество исходящих дуг для каждой вершины,edges - последовательный список reorder - параметр допустимости изменения нумерации процессов,cartcomm - создаваемый Операция создания топологии является коллективной и, тем самым, должна выполняться всеми процессами исходного
(рис 6.9) Пример графа для топологии типа звездаДля примера создадим топологию графа со структурой, представленной на рис. 6.9. В этом случае количество процессов равно 5, порядки вершин (количества исходящих дуг) принимают значения (4,1,1,1,1), а
Процессы Линии связи 0 1, 2, 3, 4 1 0 2 0 3 0 4 0
Для создания топологии с графом данного вида необходимо выполнить следующий программный код:
// создание топологии типа звезда
int index[] = { 4,1,1,1,1 };
int edges[] = { 1,2,3,4,0,0,0,0 };
MPI_Comm StarComm;
MPI_Graph_create(MPI_COMM_WORLD, 5, index, edges, 1, StarComm);
Приведем еще две полезные функции для работы с топологиями графа. Количество соседних процессов, в которых от проверяемого процесса есть выходящие дуги, может быть получено при помощи функции:
int MPI_Graph_neighbors_count(MPI_Comm comm,int rank, int *nneighbors).
Получение рангов соседних вершин обеспечивается функцией:
int MPI_Graph_neighbors(MPI_Comm comm,int rank,int mneighbors, int *neighbors),
где mneighbors есть размер массива .
При разработке параллельных программ с использованием
CALL,status является массивом целого типа из MPI_STATUS_SIZE элементов,MPI_Comm и MPI_Datatype представлены целых типом INTEGER.В качестве принятых соглашений при разработке программ на языке Fortran рекомендуется записывать имена подпрограмм с использованием прописных символов.
В качестве примера приведем вариант программы из п. 4.2.1.5 на
PROGRAM MAIN
include 'mpi.h'
INTEGER PROCNUM, PROCRANK, RECVRANK, IERR
INTEGER STATUS(MPI_STATUS_SIZE)
CALL MPI_Init(IERR)
CALL MPI_Comm_size(MPI_COMM_WORLD, PROCNUM, IERR)
CALL MPI_Comm_rank(MPI_COMM_WORLD, PROCRANK IERR)
IF ( PROCRANK.EQ.0 )THEN
! Действия, выполняемые только процессом с рангом 0
PRINT *,"Hello from process ", PROCRANK
DO i = 1, PROCNUM-1
CALL MPI_RECV(RECVRANK, 1, MPI_INT, MPI_ANY_SOURCE,
MPI_ANY_TAG, MPI_COMM_WORLD, STATUS, IERR)
PRINT *,"Hello from process ", RECVRANK
END DO
ELSE ! Сообщение, отправляемое всеми процессами,
! кроме процесса с рангом 0
CALL MPI_SEND(PROCRANK,1,MPI_INT,0,0,MPI_COMM_WORLD,IERR)
END IF
MPI_FINALIZE(IERR);
STOP
END
Для проведения параллельных вычислений в вычислительной системе должна быть установлена среда выполнения
Здесь, к сожалению, стандартизация заканчивается. Существует несколько различных сред выполнения
Запуск mpirun. В числе возможных параметров этой команды:
-localonly. При выполнении параллельной программы в Существует и значительное количество других параметров, но они обычно используются при разработке достаточно сложных параллельных программ - их описание может быть получено в справочной информации по соответствующей среде выполнения
При запуске программы на нескольких компьютерах исполняемый файл программы должен быть скопирован на все эти компьютеры или же должен находиться на общем доступном для всех компьютеров ресурсе.
Как уже отмечалось, стандарт
Для знакомства со стандартом
Данный раздел посвящен рассмотрению методов параллельного программирования для вычислительных систем с распределенной памятью с использованием
В самом начале раздела отмечается, что ) - является в настоящий момент времени одним из основных подходов для разработки параллельных программ для вычислительных систем с распределенной памятью. Использование
В подразделе 6.1 рассматривается ряд понятий и определений, являющихся основополагающими для стандарта
В подразделе 6.2 проводится быстрое и простое введение в разработку параллельных программ с использованием
В подразделе 6.3 излагается материал, связанный с операциями передачи данных между двумя процессами. В данном подразделе подробно излагаются имеющиеся в
В подразделе 6.4 рассматриваются коллективные операции передачи данных. Изложение материала соответствует последовательности изучения коммуникационных операций, использованной в разделе 3. Основной результат данного подраздела состоит в том, что
В подразделе 6.5 излагается материал, связанный с использованием в
В подразделе 6.6 обсуждаются вопросы управления процессами и коммуникаторами. Рассматриваемые в подразделе возможности
В подразделе 6.7 рассматриваются возможности
В подразделе 6.8 приводятся дополнительные сведения о
Имеется ряд источников, в которых может быть получена информация о
Среди опубликованных изданий могут быть рекомендованы работы Group, et al. (1994), Pacheco (1996), Snir, et al. (1996), Group, et al. (1999a). Описание стандарта
Следует отметить также работу Quinn (2003), в которой изучение
Подраздел 6.2.
n байт. Выполните эксперименты и оцените зависимость времени выполнения операции данных от длины сообщения. Сравните с теоретическими оценками, построенными по модели Хокни.Подраздел 6.3.
Подраздел 6.4.
Подраздел 6.5.
Подраздел 6.7.
В вычислительных
Решение всех перечисленных вопросов и обеспечивает интерфейс передачи данных ( message passing interface - MPI ).
Подобный способ организации параллельных вычислений получил наименование модели "одна программа множество процессов" ( single program multiple processes or SPMP ) ).
Следует отметить, что попытки создания программных средств передачи данных между процессорами начались предприниматься практически сразу с появлением локальных компьютерных сетей - ряд таких средств, представлен, например, в Воеводин В.В. и Воеводин Вл.В. (2002), Buyya (1999), Andrews (2000) и многих других. Однако подобные средства часто были неполными и, самое главное, являлись несовместимыми. Таким образом, одна из самых серьезных проблем в программировании - переносимость программ при переводе программного обеспечения на другие компьютерные системы - проявлялась при разработке параллельных программ в самой
Итак, теперь можно пояснить, что означает понятие
Вопросы, связанные с разработкой параллельных программ с использованием
Рассмотрим ряд понятий и определений, являющихся основополагающими для стандарта
Под параллельной программой в рамках
Каждый процесс параллельной программы порождается на основе копии одного и того же программного кода ( модель SPMP ). Данный программный код, представленный в виде исполняемой программы, должен быть доступен в момент запуска параллельной программы на всех используемых процессорах. Исходный программный код для исполняемой программы разрабатывается на
Количество процессов и число используемых процессоров определяется в момент запуска параллельной программы средствами среды исполнения np-1, где np есть общее количество процессов. Номер процесса именуется рангом процесса.
Основу point-to-point ) операции между двумя процессами и коллективные ( collective ) коммуникационные действия для одновременного взаимодействия нескольких процессов.
Для выполнения парных операций могут использоваться разные режимы передачи, среди которых синхронный, блокирующий и др. - полное рассмотрение возможных
Как уже отмечалось ранее, стандарт
Процессы параллельной программы объединяются в группы. Под коммуникатором в
Как правило, парные
В ходе вычислений могут создаваться новые и удаляться существующие MPI_COMM_WORLD.
При необходимости передачи данных между процессами из разных групп необходимо создавать глобальный ).
Подробное рассмотрение возможностей
При выполнении операций передачи сообщений для указания передаваемых или получаемых данных в функциях
Подробное рассмотрение возможностей
Как уже отмечалось ранее, парные
Вместе с этим (и это уже отмечалось в разделе 3), для изложения и последующего анализа ряда
В
Кроме того, в
И, наконец, последний ряд замечаний перед началом рассмотрения
MPI -1 ); дополнительные свойства стандарта версии 2.0 буду представлены в п. 6.8.3.Приступая к изучению
Приведем минимально-необходимый набор функций
Первой вызываемой функцией
int MPI_Init ( int *agrc, char ***argv ).
для инициализации среды выполнения
Последней вызываемой функцией
int MPI_Finalize (void)
Как результат, можно отметить, что структура параллельной программы, разработанная с использованием
#include "mpi.h"
int main ( int argc, char *argv[] ) {
<программный код без использования MPI функций>
MPI_Init ( agrc, argv );
<программный код с использованием MPI функций >
MPI_Finalize();
<программный код без использования MPI функций >
return 0;
}
Следует отметить:
mpi .h содержит определения MPI_Init и MPI_Finalize являются обязательными и должны быть выполнены (и только один раз) каждым процессом параллельной программы,MPI_Init может быть использована функция MPI_Initialized для определения того, был ли ранее выполнен вызов MPI_Init.Рассмотренные примеры функций дают представление синтаксиса именования функций в
Определение количества процессов в выполняемой параллельной программе осуществляется при помощи функции:
int MPI_Comm_size ( MPI_Comm comm, int *size ).
Для определения ранга процесса используется функция:
int MPI_Comm_rank ( MPI_Comm comm, int *rank ).
Как правило, вызов функций MPI_Comm_size и MPI_Comm_rank выполняется сразу после MPI_Init:
#include "mpi.h"
int main ( int argc, char *argv[] ) {
int ProcNum, ProcRank;
<программный код без использования MPI функций>
MPI_Init ( agrc, argv );
MPI_Comm_size ( MPI_COMM_WORLD, ProcNum);
MPI_Comm_rank ( MPI_COMM_WORLD, ProcRank);
<программный код с использованием MPI функций >
MPI_Finalize();
<программный код без использования MPI функций >
return 0;
}
Следует отметить:
MPI_COMM_WORLD, как отмечалось ранее, создается по умолчанию и представляет все процессы выполняемой параллельной программы,MPI_Comm_rank, является рангом процесса, выполнившего вызов этой функции, т.е. переменная ProcRank будет принимать различные значения в разных процессах.Для передачи сообщения процесс-отправитель должен выполнить функцию:
int MPI_Send(void *buf, int count, MPI_Datatype type, int dest, int tag, MPI_Comm comm),
где
buf - адрес буфера памяти, в котором располагаются данные отправляемого сообщения,count - количество элементов данных в сообщении,type - тип элементов данных пересылаемого сообщения,dest - ранг процесса, которому отправляется сообщение,tag - значение-тег, используемое для идентификации сообщений,comm - Для указания типа пересылаемых данных в имеется ряд базовых типов, полный список которых приведен в табл. 6.1.
MPI_Datatype
| C
|
|---|---|
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 |
|
MPI_UNSIGNED |
|
MPI_UNSIGNED_LONG |
|
MPI_UNSIGNED_SHORT |
|
Следует отметить:
( buf, count, type )
входит в состав параметров практически всех функций передачи данных.
MPI_Send.tag используется только при необходимости различения передаваемых сообщений, в противном случае в качестве значения параметра может быть использовано произвольное целое число (см. также описание функции MPI_Recv ).Сразу же после завершения функции MPI_Send процесс-отправитель может начать повторно использовать буфер памяти, в котором располагалось отправляемое сообщение. Вместе с этим, следует понимать, что в момент завершения функции MPI_Send состояние самого пересылаемого сообщения может быть совершенно различным - сообщение может располагаться в процессе-отправителе, может находиться в процессе передачи, может храниться в процессе-получателе или же может быть принято процессом-получателем при помощи функции MPI_Recv. Тем самым, завершение функции MPI_Send означает лишь, что операция передачи начала выполняться и
Пример использования функции будет представлен после описания функции MPI_Recv.
Для приема сообщения процесс-получатель должен выполнить функцию:
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 - указатель на структуру данных с информацией о результате выполнения операции приема данных.Следует отметить:
source может быть указано значение MPI_ANY_SOURCE,tag может быть указано значение MPI_ANY_TAG,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 является блокирующей для процесса-получателя, т.е. его выполнение приостанавливается до завершения работы функции. Таким образом, если по каким-то причинам ожидаемое для приема сообщение будет отсутствовать, выполнение параллельной программы будет блокировано.
Рассмотренный набор функций оказывается достаточным для разработки параллельных программ ). Приводимая ниже программа является стандартным начальным примером для
#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;
}
Как следует из текста программы, каждый процесс определяет свой ранг, после чего действия в программе разделяются. Все процессы, кроме процесса с рангом 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_Send исполняется только процессом с рангом 0, второй участок с функцией приема MPI_Recv используется всеми процессами, за исключением нулевого процесса.
Для разделения фрагментов кода между процессами обычно используется подход, примененный в только что рассмотренной программе - при помощи функции MPI_Comm_rank определяется ранг процесса, а затем в соответствии с рангом выделяются необходимые для процесса участки программного кода. Наличие в одной и той же программе фрагментов кода разных процессов также значительно усложняет понимание и, в целом, разработку
MPI_Comm_rank(MPI_COMM_WORLD, ProcRank);
if ( ProcRank == 0 ) DoProcess0();
else if ( ProcRank == 1 ) DoProcess1();
else if ( ProcRank == 2 ) DoProcess2();
Во многих случаях, как и в рассмотренном примере, выполняемые действия являются отличающимися только для процесса с рангом 0. В этом случае общая схема
MPI_Comm_rank(MPI_COMM_WORLD, ProcRank);
if ( ProcRank == 0 ) DoManagerProcess();
else DoWorkerProcesses();
В завершение обсуждения примера поясним использованный в MPI_SUCCESS. Другие значения
MPI_ERR_BUFFER - неправильный указатель на буфер,MPI_ERR_COMM - неправильный MPI_ERR_RANK - неправильный ранг процесса,и др. - полный список констант для проверки .
Практически сразу же после разработки первых параллельных программ возникает необходимость определения времени выполнения вычислений для оценки достигаемого ускорения процессов решения задач за счет использования параллелизма. Используемые обычно средства для измерения времени работы программ зависят, как правило, от аппаратной платформы, операционной системы,
Получение времени текущего момента выполнения программы обеспечивается при помощи функции:
double MPI_Wtime(void),
результат вызова которой есть количество секунд, прошедшее от некоторого определенного момента времени в прошлом. Этот момент времени в прошлом, от которого происходит отсчет секунд, может зависеть от среды реализации библиотеки MPI_Wtime следует использовать только для определения длительности выполнения тех или иных фрагментов кода параллельных программ. Возможная схема применения функции MPI_Wtime может состоять в следующем:
double t1, t2, dt;
t1 = MPI_Wtime();
…
t2 = MPI_Wtime();
dt = t2 - t1;
Точность измерения времени также может зависеть от среды выполнения параллельной программы. Для определения текущего значения точности может быть использована функция:
double MPI_Wtick(void),
позволяющая определить время в секундах между двумя последовательными показателями времени аппаратного таймера используемой компьютерной системы.
Функции MPI_Send и MPI_Recv, рассмотренные в п. 6.2.1, обеспечивают возможность выполнения парных операций передачи данных между двумя процессами параллельной программы. Для выполнения коммуникационных коллективных операций, в которых принимают участие все процессы
Для демонстрации примеров применения рассматриваемых функций x (см. подраздел 2.5):
Разработка
Первая проблема при выполнении рассмотренного x всем процессам параллельной программы. Конечно, для решения этой проблемы можно воспользоваться рассмотренными ранее функциями парных
MPI_Comm_size(MPI_COMM_WORLD,ProcNum); for (i=1; i<ProcNum; i++) MPI_Send(x,n,MPI_DOUBLE,i,0,MPI_COMM_WORLD);
Однако такое решение будет крайне неэффективным, поскольку повторение операций передачи приводит к суммированию затрат (log2p итераций передачи данных.
Достижение эффективного выполнения
int MPI_Bcast(void *buf,int count,MPI_Datatype type,int root,MPI_Comm comm),
где
buf, count, type - буфер памяти с отправляемым сообщением (для процесса с рангом 0), и для приема сообщений для всех остальных процессов,root - ранг процесса, выполняющего рассылку данных,comm - Функция MPI_Bcast осуществляет рассылку данных из буфера buf, содержащего count элементов типа type с процесса, имеющего номер root, всем процессам, входящим в (см. рис. 6.1).
(рис 6.1) Общая схема операции передачи данных от одного процесса всем процессамСледует отметить:
MPI_Bcast определяет коллективную операцию и, тем самым, при выполнении необходимых рассылок данных вызов функции MPI_Bcast должен быть осуществлен всеми процессами указываемого MPI_Bcast буфер памяти имеет различное назначение в разных процессах. Для процесса с рангом root, с которого осуществляется рассылка данных, в этом буфере должно находиться рассылаемое сообщение. Для всех остальных процессов указываемый буфер предназначен для приема передаваемых данных.Приведем программу для решения учебной задачи суммирования элементов вектора с использованием рассмотренной функции.
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "mpi.h"
int main(int argc, char* argv[]){
double x[100], TotalSum, ProcSum = 0.0;
int ProcRank, ProcNum, N=100;
MPI_Status Status;
// инициализация
MPI_Init(argc,argv);
MPI_Comm_size(MPI_COMM_WORLD,ProcNum);
MPI_Comm_rank(MPI_COMM_WORLD,ProcRank);
// подготовка данных
if ( ProcRank == 0 ) DataInitialization(x,N);
// рассылка данных на все процессы
MPI_Bcast(x, N, MPI_DOUBLE, 0, MPI_COMM_WORLD);
// вычисление частичной суммы на каждом из процессов
// на каждом процессе суммируются элементы вектора x от i1 до i2
int k = N / ProcNum;
int i1 = k * ProcRank;
int i2 = k * ( ProcRank + 1 );
if ( ProcRank == ProcNum-1 ) i2 = N;
for ( int i = i1; i < i2; i++ )
ProcSum = ProcSum + x[i];
// сборка частичных сумм на процессе с рангом 0
if ( ProcRank == 0 ) {
TotalSum = ProcSum;
for ( int i=1; i < ProcNum; i++ ) {
MPI_Recv(ProcSum, 1, MPI_DOUBLE, MPI_ANY_SOURCE, 0, MPI_COMM_WORLD,
Status);
TotalSum = TotalSum + ProcSum;
}
}
else // все процессы отсылают свои частичные суммы
MPI_Send(ProcSum, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD);
// вывод результата
if ( ProcRank == 0 )
printf("\nTotal Sum = %10.2f",TotalSum);
MPI_Finalize();
}
В приведенной программе функция DataInitialization осуществляет подготовку начальных данных. Необходимые данные могут быть введены с клавиатуры, прочитаны из файла или сгенерированы при помощи датчика случайных чисел - подготовка этой функции предоставляется как задание для самостоятельной разработки.
В рассмотренной программе суммирования числовых значений имеющаяся процедура сбора и последующего суммирования данных является примером часто выполняемой коллективной операции передачи данных от всех процессов одному процессу. В этой операции над собираемыми значениями осуществляется та или иная обработка данных (для подчеркивания последнего момента данная операция еще именуется операцией редукции данных ). Как и ранее, реализация операции
int MPI_Reduce(void *sendbuf, void *recvbuf,int count,MPI_Datatype type, MPI_Op op,int root,MPI_Comm comm),
где
sendbuf - буфер памяти с отправляемым сообщением,recvbuf - буфер памяти для результирующего сообщения (только для процесса с рангом root),count - количество элементов в сообщениях,type - тип элементов сообщений,op - операция, которая должна быть выполнена над данными,root - ранг процесса, на котором должен быть получен результат,comm - В качестве операций
| Операция | Описание |
|---|---|
MPI_MAX |
Определение максимального значения |
MPI_MIN |
Определение минимального значения |
MPI_SUM |
Определение суммы значений |
MPI_PROD |
Определение произведения значений |
MPI_LAND |
Выполнение логической операции "И" над значениями сообщений |
MPI_BAND |
Выполнение |
MPI_LOR |
Выполнение логической операции "ИЛИ" над значениями сообщений |
MPI_BOR |
Выполнение |
MPI_LXOR |
Выполнение логической операции исключающего "ИЛИ" над значениями сообщений |
MPI_BXOR |
Выполнение |
MPI_MAXLOC |
Определение максимальных значений и их индексов |
MPI_MINLOC |
Определение минимальных значений и их индексов |
Помимо данного стандартного набора операций могут быть определены и новые дополнительные операции непосредственно самим пользователем библиотеки
Общая схема выполнения операции сбора и обработки данных на одном процессоре показана на рис. 6.2. Элементы получаемого сообщения на процессе root представляют собой результаты обработки соответствующих элементов передаваемых процессами сообщений, т.е.
где $$\otimes$$ есть операция, задаваемая при вызове функции MPI_Reduce (для пояснения на рис. 6.3. показан пример выполнения операции
(рис 6.2) Общая схема операции сбора и обработки на одном процессе данных от всех процессов Следует отметить:
MPI_Reduce определяет коллективную операцию и, тем самым, вызов функции должен быть выполнен всеми процессами указываемого count, type, op , root, comm ,root,Применим полученные знания для переработки ранее рассмотренной программы суммирования - как можно увидеть, весть программный код, выделенный двойной рамкой, может быть теперь заменен на вызов одной лишь функции MPI_Reduce:
// сборка частичных сумм на процессе с рангом 0 MPI_Reduce(ProcSum,TotalSum, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
В ряде ситуаций независимо выполняемые в процессах вычисления необходимо синхронизировать. Так, например, для измерения времени начала работы параллельной программы необходимо, чтобы для всех процессов одновременно были завершены все подготовительные действия, перед окончанием работы программы все процессы должны завершить свои вычисления и т.п.
Синхронизация процессов, т.е. одновременное достижение процессами тех или иных точек процесса вычислений, обеспечивается при помощи функции
int MPI_Barrier(MPI_Comm comm);
Функция MPI_Barrier определяет коллективную операции и, тем самым, при использовании должна вызываться всеми процессами используемого MPI_Barrier выполнение процесса блокируется, продолжение вычислений процесса произойдет только после вызова функции MPI_Barrier всеми процессами
Продолжим начатое в п. 6.2.1 изучение функций
Рассмотренная ранее функция MPI_Send обеспечивает так называемый стандартный ( Standard ) режим отправки сообщений, при котором (см. также п. 6.2.1.3):
MPI_Recv.Кроме стандартного режима в
Для именования функций отправки сообщения для разных режимов выполнения в MPI_Send, к которому как префикс добавляется начальный символ названия соответствующего режима работы, т.е.
MPI_Ssend - функция отправки сообщения в синхронном режиме,MPI_Bsend - функция отправки сообщения в буферизованном режиме,MPI_Rsend - функция отправки сообщения в режиме по готовности.Список параметров всех перечисленных функций совпадает с составом параметров функции MPI_Send.
Для использования буферизованного режима передачи должны быть создан и передан
int MPI_Buffer_attach(void *buf, int size),
где
buf - буфер памяти для буферизации сообщений,size - размер буфера.После завершения работы с буфером он должен быть отключен от
int MPI_Buffer_detach(void *buf, int *size).
По практическому использованию режимов можно привести следующие рекомендации:
В заключение отметим, что для функции приема MPI_Recv не существует различных режимов работы.
Все рассмотренные ранее функции отправки и приема сообщений являются блокирующими, т.е. приостанавливающими выполнение процессов до момента завершения работы вызванных функций. В то же время при выполнении параллельных вычислений часть сообщений может быть отправлена и принята заранее до момента реальной потребности в пересылаемых данных. В таких ситуациях было бы крайне желательным иметь возможность выполнения функций обмена данными без блокировки процессов для совмещения процессов передачи сообщений и вычислений. Такой неблокирующий способ выполнения обменов является, конечно, более сложным для использования, но при правильном применении мог бы в значительной степени уменьшить потери эффективности параллельных вычислений из-за медленных (по сравнению с быстродействием процессоров) коммуникационных операций.
I ( Immediate ). Список параметров неблокирующих функций содержит весь набор параметров исходных функций и один дополнительный параметр request с типом MPI_Request (в функции MPI_Irecv отсутствует также параметр status ):
int MPI_Isend(void *buf, int count, MPI_Datatype type, int dest, int tag, MPI_Comm comm, MPI_Request *request) int MPI_Issend(void *buf, int count, MPI_Datatype type, int dest, int tag, MPI_Comm comm, MPI_Request *request) int MPI_Ibsend(void *buf, int count, MPI_Datatype type, int dest, int tag, MPI_Comm comm, MPI_Request *request) int MPI_Irsend(void *buf, int count, MPI_Datatype type, int dest, int tag, MPI_Comm comm, MPI_Request *request) int MPI_Irecv(void *buf, int count, MPI_Datatype type, int source, int tag, MPI_Comm comm, MPI_Request *request)
Вызов неблокирующей функции приводит к request, которая далее может использоваться для проверки завершения инициированной операции обмена.
Проверка состояния выполняемой неблокирующей
int MPI_Test( MPI_Request *request, int *flag, MPI_status *status),
где
request - дескриптор операции, определенный при вызове неблокирующей функции,flag - результат проверки (=true, если операция завершена),status - результат выполнения операции обмена (только для Операция проверки является неблокирующей, т.е. процесс может проверить состояние неблокирующей операции обмена и продолжить далее свои вычисления, если по результатам проверки окажется, что операция все еще не завершена. Возможная схема совмещения вычислений и выполнения неблокирующей операции обмена может состоять в следующем:
MPI_Isend(buf,count,type,dest,tag,comm,request);
…
do {
…
MPI_Test(request,flag,status)
} while ( !flag );
Если при выполнении неблокирующей операции окажется, что продолжение вычислений невозможно без получения передаваемых данных, то может быть использована блокирующая операция ожидания завершения операции:
int MPI_Wait( MPI_Request *request, MPI_status *status).
Кроме рассмотренных,
MPI_Testall - проверка завершения всех перечисленных операций обмена,MPI_Waitall - ожидание завершения всех операций обмена,MPI_Testany - проверка завершения хотя бы одной из перечисленных операций обмена,MPI_Waitany - ожидание завершения любой из перечисленных операций обмена,MPI_Testsome - проверка завершения каждой из перечисленных операций обмена,MPI_Waitsome - ожидание завершения хотя бы одной из перечисленных операций обмена и оценка состояния по всем операциям.Приведение простого примера использования неблокирующих функций достаточно затруднительно. Хорошей возможностью для освоения рассмотренных функций могут служить, например,
Одной из часто выполняемых форм информационного взаимодействия в параллельных программах является обмен данными между процессами, когда для продолжения вычислений процессам необходимо отправить данные одним процессам и, в то же время, получить сообщения от других процессов. Простейший вариант этой ситуации состоит, например, в обмене данными между двумя процессами. Реализация таких обменов при помощи обычных парных
Достижение эффективного и гарантированного одновременного выполнения операций передачи и приема данных может быть обеспечено при помощи функции
int MPI_Sendrecv(void *sbuf,int scount,MPI_Datatype stype,int dest, int stag,
void *rbuf,int rcount,MPI_Datatype rtype,int source,int rtag,
MPI_Comm comm, MPI_Status *status),
где
sbuf, scount, stype, dest, stag - параметры передаваемого сообщения,rbuf, rcount, rtype, source, rtag - параметры принимаемого сообщения,comm - status - структура данных с информацией о результате выполнения операции.Как следует из описания, функция MPI_Sendrecv передает сообщение, описываемое параметрами ( sbuf, scount, stype, dest, stag ), процессу с рангом dest и принимает сообщение в буфер, определяемый параметрами ( rbuf, rcount, rtype, source, rtag ), от процесса с рангом source.
В функции MPI_Sendrecv для передачи и приема сообщений применяются разные буфера. В случае же, когда сообщения имеют одинаковый тип, в
int MPI_Sendrecv_replace (void *buf, int count, MPI_Datatype type, int dest, int stag, int source, int rtag, MPI_Comm comm, MPI_Status* status).
Пример использование функций для одновременного выполнения операций передачи и приема приведен в разделе 8 при разработке параллельных программ матричного умножения.
Как уже отмечалось ранее, под коллективными операциями в
Рассмотрим далее оставшиеся базовые коллективные
Обобщенная
int MPI_Scatter(void *sbuf,int scount,MPI_Datatype stype,
void *rbuf,int rcount,MPI_Datatype rtype,
int root, MPI_Comm comm),
где
sbuf, scount, stype - параметры передаваемого сообщения ( scount определяет
количество элементов, передаваемых на каждый процесс),rbuf, rcount, rtype - параметры сообщения, принимаемого в процессах,root - ранг процесса, выполняющего рассылку данных,comm -
(рис 6.4) Общая схема операции обобщенной передачи данных от одного процесса всем процессамПри вызове этой функции процесс с рангом root произведет передачу данных всем другим процессам в scount элементов. Процесс с рангом 0 получит блок данных из sbuf из элементов с индексами от 0 до scount-1, процессу с рангом 1 будет отправлен блок из элементов с индексами от scount до 2* scount-1 и т.д. Тем самым, общий размер отправляемого сообщения должен быть равен scount * p элементов, где p есть количество процессов в .
Следует отметить, поскольку функция MPI_Scatter определяет коллективную операцию, вызов этой функции при выполнении рассылки данных должен быть обеспечен в каждом процессе
Отметим также, что функция MPI_Scatter передает всем процессам сообщения одинакового размера. Выполнение более общего варианта операции распределения данных, когда MPI_Scatterv.
Пример использования функции MPI_Scatter рассматривается в разделе 7 при разработке параллельных программ
Операция обобщенной передачи данных от всех процессоров одному процессу ( сбор данных ) является обратной к процедуре распределения данных (см. рис. 6.5). Для выполнения этой операции в
int MPI_Gather(void *sbuf,int scount,MPI_Datatype stype,
void *rbuf,int rcount,MPI_Datatype rtype,
int root, MPI_Comm comm),
где
sbuf, scount, stype - параметры передаваемого сообщения,rbuf, rcount, rtype - параметры принимаемого сообщения,root - ранг процесса, выполняющего сбор данных,comm -
(рис 6.5) Общая схема операции обобщенной передачи данных от всех процессов одному процессуПри выполнении функции MPI_Gather каждый процесс в sbuf на процесс с рангом root. Процесс с рангом root собирает все получаемые данные в буфере rbuf (размещение данных в буфере осуществляется в соответствии с рангами процессов-rbuf должен быть равен scount * p элементов, где p есть количество процессов в .
Функция MPI_Gather также определяет коллективную операцию, и ее вызов при выполнении сбора данных должен быть обеспечен в каждом процессе
Следует отметить, что при использовании функции MPI_Gather сборка данных осуществляется только на одном процессе. Для получения всех собираемых данных на каждом из процессов функцию сбора и рассылки:
int MPI_Allgather(void *sbuf, int scount, MPI_Datatype stype,
void *rbuf, int rcount, MPI_Datatype rtype, MPI_Comm comm).
Выполнение общего варианта операции сбора данных, когда размеры передаваемых процессами сообщений могут быть различны, обеспечивается при помощи функций MPI_Gatherv и MPI_Allgatherv.
Пример использования функции MPI_Gather рассматривается в разделе 7 при разработке параллельных программ
Передача данных от всех процессов всем процессам является наиболее общей операцией передачи данных (см. рис. 6.6). Выполнение данной операции может быть обеспечено при помощи функции:
int MPI_Alltoall(void *sbuf,int scount,MPI_Datatype stype,
void *rbuf,int rcount,MPI_Datatype rtype,MPI_Comm comm),
где
sbuf, scount, stype - параметры передаваемых сообщений,rbuf, rcount, rtype - параметры принимаемых сообщенийcomm -
(рис 6.6) Общая схема операции передачи данных от всех процессов всем процессам (сообщения показываются обозначениями вида i в j, где i и j есть ранги передающих и принимающих процессов соответственноПри выполнении функции MPI_Alltoall каждый процесс в scount элементов каждому процессу (общий размер отправляемых сообщений в процессах должен быть равен scount * p элементов, где p есть количество процессов в ) и принимает сообщения от каждого процесса.
Вызов функции MPI_Alltoall при выполнении операции общего обмена данными должен быть выполнен в каждом процессе
Вариант операции общего обмена данных, когда размеры передаваемых процессами сообщений могут быть различны, обеспечивается при помощи функций MPI_Alltoallv.
Пример использования функции MPI_Alltoall рассматривается в разделе 7 при разработке параллельных программ
Рассмотренная в п. 6.2.3.2 функция MPI_Reduce обеспечивает получение результатов функцию :
int MPI_Allreduce(void *sendbuf, void *recvbuf,int count,MPI_Datatype type, MPI_Op op,MPI_Comm comm).
Функция MPI_AllReduce выполняет рассылку между процессами всех результатов операции MPI_Reduce_scatter.
И еще один вариант операции сбора и обработки данных, при котором обеспечивается получение и всех
int MPI_Scan(void *sendbuf, void *recvbuf,int count,MPI_Datatype type, MPI_Op op,MPI_Comm comm).
Общая схема выполнения функции MPI_Scan показана на рис. 6.7. Элементы получаемых сообщений представляют собой результаты обработки соответствующих элементов передаваемых процессами сообщений, при этом для получения результатов на процессе с рангом i, 0 i<n,используются данные от процессов, ранг которых меньше или равен i,т.е.
где $$\otimes$$ есть операция, задаваемая при вызове функции MPI_Scan.
(рис 6.7) Общая схема операции редукции с получением частичных результатов обработки данных
Для
| Вид коллективной операции | Общее описание и оценка сложности | Функция MPI | Примеры использования |
|---|---|---|---|
| Передача от одного процесса всем процессам ( широковещательная рассылка ) | п.4.2.5 | MPI_Bcast
п. 6.2.3.1 |
п. 6.2.3.1 |
| Сбор и обработка данных на одном процессе от всех процессов ( редукция данных ) | пп. 4.2.5, 4.2.6 | MPI_Reduce
п. 6.2.3.2 |
п. 6.2.3.2 |
| - то же с рассылкой результатов всем процессам | пп. 4.2.5, 4.2.6 | MPI_Allreduce
MPI_Reduce_scatter
п. 6.4.4 |
|
| - то же с получением |
пп. 4.2.5, 4.2.6 | MPI_Scan
п. 6.4.4 |
|
| Обобщенная передача от одного процесса всем процессам ( распределение данных ) | п. 4.2.7 | MPI_Scatter
MPI_Scatterv
п. 6.4.1 |
Раздел 7 |
| Обобщенная передача от всех процессов одному процессу ( сбор данных ) | п. 4.2.7 | MPI_Gather
MPI_Gatherv
п. 6.4.2 |
Раздел 7 |
| - то же с рассылкой результатов всем процессам | п. 4.2.7 | MPI_Allgather
MPI_Allgatherv
п. 6.4.2 |
|
| Общая передача данных от всех процессов всем процессам | п. 4.2.8 | MPI_Alltoall
MPI_Alltoallv
п. 6.4.3 |
Раздел 7 |
Во всех ранее рассмотренных примерах использования функций передачи данных предполагалось, что сообщения представляют собой некоторый непрерывный вектор элементов предусмотренного в
Для обеспечения больших возможностей при определении состава передаваемых сообщений в
В самом общем виде под производным типом данных в type map ) в виде последовательности описаний входящих в тип значений, каждое отдельное значение описывается указанием типа и смещения адреса месторасположения от некоторого базового адреса, т.е.
TypeMap = {(type0,disp0),… , (typen-1,dispn-1)}.
Часть карты типа с указанием только типов значений именуется в
TypeSignature = {type0,… , typen-1}
Сигнатура типа описывает, какие
Поясним рассмотренные понятия на следующем примере. Пусть в сообщение должны входить значения переменных:
double a; /* адрес 24 */ double b; /* адрес 40 */ int n; /* адрес 48 */
Тогда
{(MPI_DOUBLE,0),
(MPI_DOUBLE,16),
(MPI_INT,24)
}
Дополнительно для
lb(TypeMap) = minj(dispj)
extent (TypeMap) = ub(TypeMap)-lb(TypeMap).Согласно определению нижняя граница есть смещение для первого байта значений рассматриваемого типа данных. Соответственно верхняя граница представляет собой смещение для байта, располагающегося вслед за последним элементом рассматриваемого типа данных. При этом величина смещения для верхней границы может быть округлена вверх с учетом требований выравнивания адресов. Так, одно из самых общих требований, которые налагают реализации языков C и Fortran, состоит в том, чтобы адрес элемента был кратен длине этого элемента в байтах. Например, если тип int занимает четыре байта, то адрес на элемент типа int должен нацело делиться на четыре. Именно это требование и отражается в определении верхней границы типа данных a,b и n, для которого нижняя граница равна 0, а верхняя граница принимает значение 32 (величина округления 6 или 4 в зависимости от размера типа int ). Здесь следует отметить, что требуемое выравнивание определяется по типу первого элемента данных в карте типа.
Следует также указать на различие понятий протяженности и размера типа. Протяженность - это размер памяти в байтах, который нужно отводить для одного элемента int занимает четыре байта).
Для получения значения протяженности и размера типа в
int MPI_Type_extent ( MPI_Datatype type, MPI_Aint *extent ), int MPI_Type_size ( MPI_Datatype type, MPI_Aint *size ).
Определение нижней и верхней границ типа может быть выполнено при помощи функций:
int MPI_Type_lb ( MPI_Datatype type, MPI_Aint *disp ), int MPI_Type_ub ( MPI_Datatype type, MPI_Aint *disp ).
Важной и необходимой при конструировании
int MPI_Address ( void *location, MPI_Aint *address )
(следует отметить, что данная функция является переносимым вариантом средств получения адресов в
Для снижения сложности в
Далее перечисленные способы конструирования
При непрерывном способе конструирования
int MPI_Type_contiguous(int count,MPI_Data_type oldtype,MPI_Datatype *newtype).
Как следует из описания, новый тип newtype создается как count элементов исходного типа oldtype. Например, если исходный тип данных имеет карту типа
{ (MPI_INT,0),(MPI_DOUBLE,8) },
то вызов функции MPI_Type_contiguous с параметрами
MPI_Type_contiguous (2, oldtype, newtype);
приведет к созданию типа данных с картой типа
{ (MPI_INT,0),(MPI_DOUBLE,8),(MPI_INT,16),(MPI_DOUBLE,24) }.
В определенном плане наличие непрерывного способа конструирования является избыточным, поскольку использование аргумента count в процедурах
При векторном способе конструирования
int MPI_Type_vector ( int count, int blocklen, int stride, MPI_Data_type oldtype, MPI_Datatype *newtype ),
где
count - количество блоков,blocklen - размер каждого блока,stride - количество элементов, расположенных между двумя соседними блокамиoldtype - исходный тип данных,newtype - новый определяемый тип данных.int MPI_Type_hvector ( int count, int blocklen, MPI_Aint stride, MPI_Data_type oldtype, MPI_Datatype *newtype ).
Отличие способа конструирования, определяемого функцией MPI_Type_hvector, состоит лишь в том, что параметр stride для определения интервала между блоками задается в байтах, а не в элементах исходного типа данных.
Как следует из описания, при векторном способе новый
n x n:MPI_Type_vector ( n/2, n, 2*n, StripRowType, ElemType ),
n x n:MPI_Type_vector ( n, 1, n, ColumnType, ElemType ),
n x n:MPI_Type_vector ( n, 1, n+1, DiagonalType, ElemType ).
С учетом характера приводимых примеров можно упомянуть имеющуюся в
int MPI_Type_create_subarray ( int ndims, int *sizes, int *subsizes, int *starts, int order, MPI_Data_type oldtype, MPI_Datatype *newtype ),
где
ndims - sizes - количество элементов в каждой размерности исходного массива,subsizes - количество элементов в каждой размерности определяемого подмассива,starts - индексы начальных элементов в каждой размерности определяемого подмассива,order - параметр для указания необходимости переупорядочения,oldtype - тип данных элементов исходного массива,newtype - новый тип данных для описания подмассива.При индексном способе конструирования
int MPI_Type_indexed ( int count, int blocklens[], int indices[], MPI_Data_type oldtype, MPI_Datatype *newtype ),
где
count - количество блоков,blocklens - количество элементов в каждов блоке,indices - смещение каждого блока от начала типа (в количестве элементов исходного типа),oldtype - исходный тип данных,newtype - новый определяемый тип данных.int MPI_Type_hindexed ( int count, int blocklens[], MPI_Aint indices[], MPI_Data_type oldtype, MPI_Datatype *newtype )
Как следует из описания, при индексном способе новый n x n:
// конструирование типа для описания верхней треугольной матрицы
for ( i=0, i<n; i++ ) {
blocklens[i] = n - i;
indices[i] = i * n + i;
}
MPI_Type_indexed ( n, blocklens, indices, UTMatrixType, ElemType ).
Как и ранее, способ конструирования, определяемый функцией MPI_Type_hindexed, отличается тем, что элементы для определения интервалов между блоками задаются в байтах, а не в элементах исходного типа данных.
Следует отметить, что существует еще одна дополнительная функция MPI_Type_create_indexed_block индексного способа конструирования для определения типов с блоками одинакового размера (данная функция предусматривается стандартом
Как отмечалось ранее,
int MPI_Type_struct ( int count, int blocklens[], MPI_Aint indices[], MPI_Data_type oldtypes[], MPI_Datatype *newtype ),
где
count - количество блоков,blocklens - количество элементов в каждов блоке,indices - смещение каждого блока от начала типа (в байтах),oldtypes - исходные типы данных в каждом блоке в отдельности,newtype - новый определяемый тип данных.Как следует из описания,
Рассмотренные в предыдущем пункте функции конструирования позволяют определить
int MPI_Type_commit (MPI_Datatype *type ).
При завершении использования
int MPI_Type_free (MPI_Datatype *type ).
Наряду с рассмотренными в п. 4.5.2 методами конструирования
Для использования данного подхода должен быть определен буфер памяти достаточного размера для сборки сообщения. Входящие в состав сообщения данные должны быть упакованы в буфер при помощи функции:
int MPI_Pack ( void *data, int count, MPI_Datatype type, void *buf, int bufsize, int *bufpos, MPI_Comm comm),
где
data - буфер памяти с элементами для упаковки,count - количество элементов в буфере,type - тип данных для упаковываемых элементов,buf - буфер памяти для упаковки,buflen - размер буфера в байтах,bufpos - позиция для начала записи в буфер (в байтах от начала буфера),comm - Функция MPI_Pack упаковывает count элементов из буфера data в буфер упаковки buf, начиная с позиции bufpos. Общая схема процедуры упаковки показана на рис. 6.8а.
(рис 6.8) Общая схема упаковки и распаковки данныхНачальное значение переменной bufpos должно быть сформировано до начала упаковки и далее устанавливается функцией MPI_Pack. Вызов функции MPI_Pack осуществляется последовательно для упаковки всех необходимых данных. Так, для ранее рассмотренного примера набора переменных a,b и n, для их упаковки необходимо выполнить:
bufpos = 0; MPI_Pack(a,1,MPI_DOUBLE,buf,buflen,bufpos,comm); MPI_Pack(b,1,MPI_DOUBLE,buf,buflen,bufpos,comm); MPI_Pack(n,1,MPI_INT,buf,buflen,bufpos,comm);
Для определения необходимого размера буфера для упаковки может быть использована функция:
int MPI_Pack_size (int count, MPI_Datatype type, MPI_Comm comm, int *size),
которая в параметре size указывает необходимый размер буфера для упаковки count элементов типа type.
После упаковки всех необходимых данных подготовленный буфер может быть использован в функциях передачи данных с указанием типа MPI_PACKED.
После получения сообщения с типом MPI_PACKED данные могут быть распакованы при помощи функции:
int MPI_Unpack (void *buf, int bufsize, int *bufpos, void *data, int count, MPI_Datatype type, MPI_Comm comm),
где
buf - буфер памяти с упакованными данными,buflen - размер буфера в байтах,bufpos - позиция начала данных в буфере (в байтах от начала буфера),data - буфер памяти для распаковываемых данных,count - количество элементов в буфере,type - тип распаковываемых данных,comm - Функция MPI_Unpack распаковывает начиная с позиции bufpos очередную порцию данных из буфера buf и помещает распакованные данные в буфер data. Общая схема процедуры распаковки показана на рис. 6.8б.
Начальное значение переменной bufpos должно быть сформировано до начала распаковки и далее устанавливается функцией MPI_Unpack. Вызов функции MPI_Unpack осуществляется последовательно для распаковки всех упакованных данных, при этом порядок распаковки должен соответствовать порядку упаковки. Так, для ранее рассмотренного примера упаковки для распаковки упакованных данных необходимо выполнить:
bufpos = 0; MPI_Pack(buf,buflen,bufpos,a,1,MPI_DOUBLE,comm); MPI_Pack(buf,buflen,bufpos,b,1,MPI_DOUBLE,comm); MPI_Pack(buf,buflen,bufpos,n,1,MPI_INT,comm);
В заключение выскажем ряд рекомендаций по использованию упаковки для формирования сообщений. Поскольку такой подход приводит к появлению дополнительных действий по упаковке и распаковке данных, то данный способ может быть оправдан при сравнительно небольших
Рассмотрим теперь возможности
Для изложения последующего материала напомним ряд понятий и определений, приведенных в начале данного раздела.
Процессы параллельной программы объединяются в группы. В группу могут входить все процессы параллельной программы; с другой стороны, в группе может находиться только часть имеющихся процессов. Соответственно, один и тот же процесс может принадлежать нескольким группам. Управление
Под коммуникатором в контекст ), используемых при выполнении
Все имеющиеся в параллельной программе процессы входят в состав создаваемого по умолчанию MPI_COMM_WORLD.
При необходимости передачи данных между процессами из разных групп необходимо создавать глобальный ). Взаимодействие между процессами разных групп оказывается необходимым в достаточно редких ситуациях, в данном учебном материале не рассматривается и может служить темой для самостоятельно изучения - см., например, Немнюгин и Стесик (2002), Group, et al. (1994), Pacheco (1996).
MPI_COMM_WORLD.
Для получения группы, связанной с существующим
int MPI_Comm_group ( MPI_Comm comm, MPI_Group *group ).
Далее, на основе существующих групп, могут быть созданы новые группы:
newgroup из существующей группы oldgroup, которая будет включать в себя n процессов, ранги которых перечисляются в массиве ranks :int MPI_Group_incl(MPI_Group oldgroup,int n, int *ranks,MPI_Group *newgroup),
newgroup из группы oldgroup, которая будет включать в себя n процессов, ранги которых не совпадают с рангами, перечисленными в массиве ranks :int MPI_Group_excl(MPI_Group oldgroup,int n, int *ranks,MPI_Group *newgroup).
Для получения новых групп над имеющимися
newgroup как объединения групп group1 и group2:int MPI_Group_union(MPI_Group group1, MPI_Group group2, MPI_Group *newgroup);
newgroup как пересечения групп group1 и group2:int MPI_Group_intersection ( MPI_Group group1, MPI_Group group2, MPI_Group *newgroup ),
newgroup как разности групп group1 и group2:int MPI_Group_difference ( MPI_Group group1, MPI_Group group2, MPI_Group *newgroup ).
При конструировании групп может оказаться полезной специальная пустая группа MPI_COMM_EMPTY.
Ряд функций
int MPI_Group_size ( MPI_Group group, int *size ),
int MPI_Group_rank ( MPI_Group group, int *rank ).
После завершения использования группа должна быть удалена:
int MPI_Group_free ( MPI_Group *group )
(выполнение данной операции не затрагивает
Отметим прежде всего, что в данном пункте рассматривается управление интракоммуникаторами, используемыми для
Для создания новых
int MPI_Comm_dup ( MPI_Comm oldcom, MPI_comm *newcomm ),
int MPI_comm_create (MPI_Comm oldcom, MPI_Group group, MPI_Comm *newcomm).
Дублирование
Следует отметить также, что операция создания
Для пояснения рассмотренных функций можно привести пример создания MPI_COMM_WORLD (такой
MPI_Group WorldGroup, WorkerGroup; MPI_Comm Workers; int ranks[1]; ranks[0] = 0; // получение группы процессов в MPI_COMM_WORLD MPI_Comm_group(MPI_COMM_WORLD, WorldGroup); // создание группы без процесса с рангом 0 MPI_Group_excl(WorldGroup, 1, ranks, WorkerGroup); // Создание коммуникатора по группе MPI_Comm_create(MPI_COMM_WORLD,WorkerGroup,Workers); ... MPI_Group_free(WorkerGroup); MPI_Comm_free(Workers);
Быстрый и полезный способ одновременного создания нескольких
int MPI_Comm_split ( MPI_Comm oldcomm, int split, int key, MPI_Comm *newcomm ),
где
oldcomm - исходный split - номер key - порядок ранга процесса в создаваемом newcomm - создаваемый Создание MPI_Comm_split должен быть выполнен в каждом процессе oldcomm. В результате выполнения функции процессы разделяются на непересекающиеся группы с одинаковыми значениями параметра split. На основе сформированных групп создается набор key (процесс с большим значением параметра key должен иметь больший ранг).
В качестве примера можно рассмотреть задачу представления набора процессов в виде двумерной решетки. Пусть p=q*q есть общее количество процессов, следующий далее фрагмент программы обеспечивает получение
MPI_Comm comm; int rank, row; MPI_Comm_rank(MPI_COMM_WORLD,rank); row = rank/q; MPI_Comm_split(MPI_COMM_WORLD,row,rank,comm);
При выполнении данного примера, например, при p=9, процессы с рангами (0,1,2) образуют первый
После завершения использования
int MPI_Comm_free ( MPI_Comm *comm ).
Под топологией вычислительной системы обычно понимается структура узлов сети и линий связи между этими узлами. Топология может быть представлена в виде графа, в котором вершины есть процессоры (процессы) системы, а дуги соответствуют имеющимся линиям (каналам) связи.
Как уже отмечалось ранее, парные
Понятно, что физическая топология системы является аппаратно реализуемой и изменению не подлежит (хотя существуют и программируемые средства построения сетей). Но, оставляя неизменной физическую основу, мы можем организовать логическое представление любой необходимой виртуальной топологии. Для этого достаточно, например, сформировать тот или иной механизм дополнительной
Использование виртуальных процессов может оказаться полезным в силу ряда разных причин.
В
Декартовы топологии, в которых множество процессов представляется в виде прямоугольной решетки (см. п. 1.4.1 и рис. 6.7), а для указания процессов используется декартова система координат, широко применяются во многих задачах для описания структуры имеющихся информационных зависимостей. В числе примеров таких задач - матричные алгоритмы (см. разделы 7 и 8) и сеточные методы
Для создания декартовой топологии (решетки) в
int MPI_Cart_create(MPI_Comm oldcomm, int ndims, int *dims, int *periods, int reorder, MPI_Comm *cartcomm),
где:
oldcomm - исходный ndims - размерность декартовой решетки,dims - массив длины ndims, задает количество процессов в каждом измерении решетки,periods - массив длины ndims, определяет, является ли решетка периодической вдоль каждого измерения,reorder - параметр допустимости изменения нумерации процессов,cartcomm - создаваемый Операция создания топологии является коллективной и, тем самым, должна выполняться всеми процессами исходного
Для пояснения назначения параметров функции MPI_Cart_create рассмотрим пример создания двухмерной решетки 4x4, в которой строки и столбцы имеют кольцевую структуру (за последним процессом следует первый процесс):
// создание двухмерной решетки 4x4 MPI_Comm GridComm; int dims[2], periods[2], reorder = 1; dims[0] = dims[1] = 4; periods[0] = periods[1] = 1; MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, reoreder, GridComm);
Следует отметить, что в силу кольцевой структуры измерений сформированная в рамках примера топология является тором.
Для определения декартовых координат процесса по его рангу можно воспользоваться функцией:
int MPI_Card_coords(MPI_Comm comm,int rank,int ndims,int *coords),
где:
comm - rank - ранг процесса, для которого определяются декартовы координаты,ndims - размерность решетки,coords - возвращаемые функцией декартовы координаты процесса.Обратное действие - определение ранга процесса по его декартовым координатам - обеспечивается при помощи функции:
int MPI_Cart_rank(MPI_Comm comm, int *coords, int *rank),
где
comm - coords - декартовы координаты процесса,rank - возвращаемый функцией ранг процесса.Полезная во многих приложениях процедура разбиения решетки на подрешетки меньшей размерности обеспечивается при помощи функции:
int MPI_Card_sub(MPI_Comm comm, int *subdims, MPI_Comm *newcomm),
где:
comm - исходный subdims - массив для указания, какие измерения должны остаться в создаваемой подрешетке,newcomm - создаваемый Операция создания подрешеток также является коллективной и, тем самым, должна выполняться всеми процессами исходного MPI_Cart_sub определяет
Для пояснения функции MPI_Cart_sub дополним ранее рассмотренный пример создания двухмерной решетки и определим
// создание коммуникаторов для каждой строки и столбца решетки MPI_Comm RowComm, ColComm; int subdims[2]; // создание коммуникаторов для строк subdims[0] = 0; // фиксации измерения subdims[1] = 1; // наличие данного измерения в подрешетке MPI_Cart_sub(GridComm, subdims, RowComm); // создание коммуникаторов для столбцов subdims[0] = 1; subdims[1] = 0; MPI_Cart_sub(GridComm, subdims, ColComm);
В приведенном примере для решетки размером 4х4 создаются 8 RowComm и ColComm соответствуют строке и столбцу процессов, к которым данный процесс принадлежит.
Дополнительная функция MPI_Cart_shift обеспечивает поддержку процедуры последовательной передачи данных по одному из измерений решетки ( операция сдвига данных - см. раздел 3). В зависимости от периодичности измерения решетки, по которому выполняется сдвиг, различаются два типа данной операции:
k элементов вдоль измерения решетки - в этой операции данные от процесса i пересылаются процессу (i+k) mod dim , где dim есть размер измерения, вдоль которого производится сдвиг,k позиций вдоль измерения решетки - в этом варианте операции данные от процессора i пересылаются процессору i+k (если таковой существует).Функция MPI_Cart_shift обеспечивает получение рангов процессов, с которыми текущий процесс (процесс, вызвавший функцию MPI_Cart_shift ) должен выполнить обмен данными:
int MPI_Card_shift(MPI_Comm comm, int dir, int disp, int *source, int *dst),
где:
comm - dir - номер измерения, по которому выполняется сдвиг,disp - величина сдвига ( <0 - сдвиг к началу измерения),source - ранг процесса, от которого должны быть получены данные,dst - ранг процесса которому должны быть отправлены данные.Следует отметить, что функция MPI_Cart_shift только определяет ранги процессов, между которыми должен быть выполнен обмен данными в ходе MPI_Sendrecv.
Сведения по функциям
Для создания
int MPI_Graph_create(MPI_Comm oldcomm, int nnodes, int *index, int *edges, int reorder, MPI_Comm *graphcomm),
где:
oldcomm - исходный nnodes - количество вершин графа,index - количество исходящих дуг для каждой вершины,edges - последовательный список reorder - параметр допустимости изменения нумерации процессов,cartcomm - создаваемый Операция создания топологии является коллективной и, тем самым, должна выполняться всеми процессами исходного
(рис 6.9) Пример графа для топологии типа звездаДля примера создадим топологию графа со структурой, представленной на рис. 6.9. В этом случае количество процессов равно 5, порядки вершин (количества исходящих дуг) принимают значения (4,1,1,1,1), а
Процессы Линии связи 0 1, 2, 3, 4 1 0 2 0 3 0 4 0
Для создания топологии с графом данного вида необходимо выполнить следующий программный код:
// создание топологии типа звезда
int index[] = { 4,1,1,1,1 };
int edges[] = { 1,2,3,4,0,0,0,0 };
MPI_Comm StarComm;
MPI_Graph_create(MPI_COMM_WORLD, 5, index, edges, 1, StarComm);
Приведем еще две полезные функции для работы с топологиями графа. Количество соседних процессов, в которых от проверяемого процесса есть выходящие дуги, может быть получено при помощи функции:
int MPI_Graph_neighbors_count(MPI_Comm comm,int rank, int *nneighbors).
Получение рангов соседних вершин обеспечивается функцией:
int MPI_Graph_neighbors(MPI_Comm comm,int rank,int mneighbors, int *neighbors),
где mneighbors есть размер массива .
При разработке параллельных программ с использованием
CALL,status является массивом целого типа из MPI_STATUS_SIZE элементов,MPI_Comm и MPI_Datatype представлены целых типом INTEGER.В качестве принятых соглашений при разработке программ на языке Fortran рекомендуется записывать имена подпрограмм с использованием прописных символов.
В качестве примера приведем вариант программы из п. 4.2.1.5 на
PROGRAM MAIN
include 'mpi.h'
INTEGER PROCNUM, PROCRANK, RECVRANK, IERR
INTEGER STATUS(MPI_STATUS_SIZE)
CALL MPI_Init(IERR)
CALL MPI_Comm_size(MPI_COMM_WORLD, PROCNUM, IERR)
CALL MPI_Comm_rank(MPI_COMM_WORLD, PROCRANK IERR)
IF ( PROCRANK.EQ.0 )THEN
! Действия, выполняемые только процессом с рангом 0
PRINT *,"Hello from process ", PROCRANK
DO i = 1, PROCNUM-1
CALL MPI_RECV(RECVRANK, 1, MPI_INT, MPI_ANY_SOURCE,
MPI_ANY_TAG, MPI_COMM_WORLD, STATUS, IERR)
PRINT *,"Hello from process ", RECVRANK
END DO
ELSE ! Сообщение, отправляемое всеми процессами,
! кроме процесса с рангом 0
CALL MPI_SEND(PROCRANK,1,MPI_INT,0,0,MPI_COMM_WORLD,IERR)
END IF
MPI_FINALIZE(IERR);
STOP
END
Для проведения параллельных вычислений в вычислительной системе должна быть установлена среда выполнения
Здесь, к сожалению, стандартизация заканчивается. Существует несколько различных сред выполнения
Запуск mpirun. В числе возможных параметров этой команды:
-localonly. При выполнении параллельной программы в Существует и значительное количество других параметров, но они обычно используются при разработке достаточно сложных параллельных программ - их описание может быть получено в справочной информации по соответствующей среде выполнения
При запуске программы на нескольких компьютерах исполняемый файл программы должен быть скопирован на все эти компьютеры или же должен находиться на общем доступном для всех компьютеров ресурсе.
Как уже отмечалось, стандарт
Для знакомства со стандартом
Данный раздел посвящен рассмотрению методов параллельного программирования для вычислительных систем с распределенной памятью с использованием
В самом начале раздела отмечается, что ) - является в настоящий момент времени одним из основных подходов для разработки параллельных программ для вычислительных систем с распределенной памятью. Использование
В подразделе 6.1 рассматривается ряд понятий и определений, являющихся основополагающими для стандарта
В подразделе 6.2 проводится быстрое и простое введение в разработку параллельных программ с использованием
В подразделе 6.3 излагается материал, связанный с операциями передачи данных между двумя процессами. В данном подразделе подробно излагаются имеющиеся в
В подразделе 6.4 рассматриваются коллективные операции передачи данных. Изложение материала соответствует последовательности изучения коммуникационных операций, использованной в разделе 3. Основной результат данного подраздела состоит в том, что
В подразделе 6.5 излагается материал, связанный с использованием в
В подразделе 6.6 обсуждаются вопросы управления процессами и коммуникаторами. Рассматриваемые в подразделе возможности
В подразделе 6.7 рассматриваются возможности
В подразделе 6.8 приводятся дополнительные сведения о
Имеется ряд источников, в которых может быть получена информация о
Среди опубликованных изданий могут быть рекомендованы работы Group, et al. (1994), Pacheco (1996), Snir, et al. (1996), Group, et al. (1999a). Описание стандарта
Следует отметить также работу Quinn (2003), в которой изучение
Подраздел 6.2.
n байт. Выполните эксперименты и оцените зависимость времени выполнения операции данных от длины сообщения. Сравните с теоретическими оценками, построенными по модели Хокни.Подраздел 6.3.
Подраздел 6.4.
Подраздел 6.5.
Подраздел 6.7.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.