MPI. Вводный курс

При создании этого документа помимо англоязычных пособий были использованы следующие авторские курсы на русском языке:


MPI (message passing interface) - стандартизованная и переносимая система передачи сообщений (библиотека функций). Стандарт определяет синтаксис и семантику библиотечных функций, используемых при написании переносимых программ с передачей сообщений на языках Fortran 77, С и С++.

Другими словами, MPI - это программный инструментарий для обеспечения связи между ветвями параллельного приложения.

Стандарт MPI-1 был принят группой порядка 80 исследователей из 40 организаций (как академических, так и промышленных) во время серии встреч в 1992-93 гг. В обсуждении стандарта могли принять участие все желающие. В июне 1994 была реализована версия 1.0

Существует несколько протестированных эффективных реализаций MPI, в том числе и для свободного использования (например, MPICH).  


Введение

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

Основная цель MPI - достигнуть переносимость разрабатываемого ПО для разных параллельных компьютеров. В том числе и для компьютеров с разделяемой памятью, если для них реализован MPI. Разработанный код также может выполняться на сети рабочих станций, либо, как множество процессов на одной станции. Это, в частности, дает возможность отлаживать программы на персональном компьютере.

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

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

Эффективность MPI достигается также за счет невыполнения ненужной работы по пересылке большого количества лишней информации с каждым сообщением или кодированию-декодированию заголовков сообщений. MPI был разработан так, чтобы поддерживатьодновременное выполнение вычислений и коммуникаций, чтобы использовать наличие сопроцессоров. Это достигается использованием неблокируемых коммуникационных вызовов, которые разделяют инициацию коммуникации и ее завершение. Для повышения скорости в MPI используются приемы,о которых прикладные программисты зачастую просто не задумываются. Например, встроенная буферизация позволяет избежать задержек при отправке данных - управление в передающую ветвь возвращается немедленно, даже если ветвь-получатель еще не подготовилась к приему. MPI использует многопоточность (multi-threading), вынося большую часть своей работы в потоки(threads) с низким приоритетом. Буферизация и многопоточность сводят к минимуму негативное влияние неизбежных простоев при пересылках на производительность прикладной программы. На передачу данных типа "один-всем" затрачивается время, пропорциональное не числу участвующих ветвей, а логарифму этого числа. И так далее...

Масштабируемость - основная цель параллельной обработки. MPI позволяет или поддерживает масштабируемость.  


Цели MPI

Разработчики ставили своей задачей:

 

Платформы

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

Интерфейс может использоваться в MIMD и MPMD программах (когда каждый процесс следует своим собственным путем в одном коде или даже выполняет свой код). Также может использоваться в SPMD программах. Хотя явная поддержка подпроцессов не обеспечивается, интерфейс разработан таким образом, чтобы не помешать их использованию. В следующей версии MPI обеспечивается динамическое порождение процессов.  


Что включается в MPI:

Что не включено:

В июне 1995 была реализована версия 1.1 (с изменениями, которые были действительно срочно необходимы).  


Процессы

MPI программа состоит из автономных процессов, выполняющих собственный код (Fortran 77, C, C++) в MIMD стиле. Коды, выполняемые каждым процессом, не обязаны быть идентичными. Разные процессы могут выполняться как на разных процессорах, так и на одном и том же - для программы это роли не играет, поскольку в обоих случаях механизм обмена данными одинаков. Процессы взаимодействуют посредством вызовов MPI примитивов. Обычно, каждый процесс выполняется над своим собственным адресным пространством, хотя возможны реализации MPI для разделяемой памяти.

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

MPI не обеспечивает механизмов для определения начального распределения процессов на физических процессорах. Этим занимается та система, на которой реализован MPI. Также стандарт MPI-1 не обеспечивает динамическое порождение и удаление процессов.

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

Особенность MPI: понятие области связи (communication domains). При запуске приложения все процессы помещаются в создаваемую для приложения общую область связи. При необходимости они могут создавать новые области связи на базе существующих. Все области связи имеют независимую друг от друга нумерацию процессов (нумерация начинается с 0). Программе пользователя в распоряжение предоставляется коммуникатор - описатель области связи. Многие функции MPI имеют среди входных аргументов коммуникатор, который ограничивает сферу их действия той областью связи, к которой он прикреплен. Для одной области связи может существовать несколько коммуникаторов. В этом случае приложение работает с такойобластью связи, как с несколькими разными областями. В исходных текстах примеров для MPI часто используется идентификатор MPI_COMM_WORLD. Это название коммуникатора, создаваемого библиотекой автоматически. Он описывает стартовую область связи, объединяющую все процессы приложения.

 


Типы вызовов (функций) MPI

Другими словами, локальные функции не инициируют пересылок данных между ветвями. Большинство информационных функций является локальными, т.к. копии системных данных уже хранятся в каждой ветви. Функция передачи MPI_Send и функция синхронизации MPI_Barrier НЕ являются локальными, поскольку производят пересылку. Следует заметить, что, к примеру, функция приема MPI_Recv (парная для MPI_Send) является локальной: она всего лишь пассивно ждет поступления данных, ничего не пытаясь сообщить другим ветвям.

 

Именованные константы

MPI процедуры иногда присваивают специальные значения аргументам. Например, tag - целочисленный аргумент операций процесс-процесс взаимодействий. Он может принимать специальное значение MPI_ANY_TAG.

Все именованные константы могут быть использованы при инициализации выражений. (искл-е - MPI_BOTTOM в Fortran). Эти константы не изменяют свое значение в процессе выполнения. В конце документа приведен список констант.  


Стиль

Все идентификаторы начинаются с префикса "MPI_". Это правило без исключений. Не рекомендуется заводить пользовательские идентификаторы, начинающиеся с этой приставки, а также с приставок "MPID_", "MPIR_" и "PMPI_", которые используются в служебных целях.

Если идентификатор сконструирован из нескольких слов, слова в нем разделяются подчерками: MPI_Get_count, MPI_Comm_rank. Иногда, однако, разделитель не используется: MPI_Sendrecv, MPI_Alltoall.

Порядок слов в составном идентификаторе выбирается по принципу "от общего к частному": сначала префикс "MPI_", потом название категории ( Type, Comm, Group, Attr, Errhandler и т.д.), потом название операции ( MPI_Errhandler_create, MPI_Errhandler_set, ...). Наиболее часто употребляемые функции выпадают из этой схемы: они имеют "анти-методические", но короткие и стереотипные названия, например MPI_Barrier, или MPI_Unpack.

Имена констант (и неизменяемых пользователем переменных) записываются полностью заглавными буквами: MPI_COMM_WORLD, MPI_FLOAT. В именах функций первая за префиксом буква - заглавная, остальные маленькие: MPI_Send, MPI_Comm_size.

Заголовочный файл - mpi.h

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

Аргумены массивов нумеруются с 0.

0 - false, non-zero - true

При описании процедур MPI будем пользоваться словом OUT для обозначения "выходных" параметров, т.е. таких параметров, через которые процедура возвращает результаты.  


Общие процедуры MPI

int MPI_Init( int* argc, char*** argv)

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

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

MPI_Abort(MPI_comm comm, int errorcode)

Аварийное закрытие библиотеки. Вызывается, если пользовательская программа завершается по причине ошибок времени выполнения, связанных с MPI.

Вызов MPI_Abort из любой задачи принудительно завершает работу ВСЕХ задач, подсоединенных к заданной области связи. Если указан описатель MPI_COMM_WORLD, будет завершено всеприложение (все его задачи) целиком, что, по-видимому, и является наиболее правильным решением. Используйте код ошибки MPI_ERR_OTHER, если не знаете, как охарактеризовать ошибку в классификации MPI.

int MPI_Finalize( void )

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

Настоятельно рекомендуется не забывать вызывать эту функцию перед возвращением из программы. Для программы на языках C/C++ это:

 
int MPI_Comm_size( MPI_Comm comm, int* size )
Определение общего числа параллельных процессов в группе comm.
comm - идентификатор группы
OUT size - размер группы

int MPI_Comm_rank( MPI_comm comm, int* rank)
Определение номера процесса в группе comm.
Значение, возвращаемое по адресу &rank, лежит в диапазоне от 0 до size_of_group-1.
comm - идентификатор группы
OUT rank - номер вызывающего процесса в группе comm

double MPI_Wtime(void)
Функция возвращает астрономическое время в секундах (вещественное число), прошедшее с некоторого момента в прошлом. Гарантируется, что этот момент не будет изменен за время существования процесса.
 

Типы данных

Для описания базовых типов Си в MPI определены константы MPI_INT, MPI_CHAR, MPI_DOUBLE и так далее,имеющие тип MPI_Datatype. Их названия образуются префиксом "MPI_" и именем соответствующего типа (int, char, double, ...), записанным заглавными буквами (список констант, описывающих тип, приведен в конце документа). Пользователь может "регистрировать" в MPI свои собственные типы данных, например, структуры, после чего MPI сможет обрабатывать их наравне с базовыми. Процесс регистрации описывается ниже.  


Процесс-процесс взаимодействия

Основной механизм коммуникаций в MPI - передача данных между парой процессов, одна сторона посылает, другая - получает.

MPI обеспечивает набор функций посылки и приема, которые посылают данные определенного типа с ассоциированным msgtag. Тип нужен для неоднородных систем - чтобы правильно конвертировать. Msgtag (это целое число от 0 до 32767, которое пользователь выбирает сам) дает возможность выбирать при приеме - можно принимать сообщения только с определенным msgtag, либо с любым (MPI_ANY_TAG).

Блокирующие функции MPI_Send и MPI_Recv

buf можно переписать после того, как содержимое буфера будет востребовано

int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest, int msgtag, MPI_Comm comm)

buf - адрес начала буфера посылки сообщения
count - число передаваемых элементов в сообщении
datatype - тип передаваемых элементов
dest - номер процесса-получателя
msgtag - идентификатор сообщения
comm - идентификатор группы

Блокирующая посылка сообщения с идентификатором msgtag, состоящего из count элементов типа datatype, процессу с номером dest. Все элементы сообщения расположены подряд в буфере buf. Значение count может быть нулем. Тип передаваемых элементов datatype должен указываться с помощью предопределенных констант типа. Разрешается передавать сообщение самому себе.

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

int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source, int msgtag, MPI_comm comm, MPI_Status *status)

OUT buf - адрес начала буфера приема сообщения
count - максимальное число элементов в принимаемом сообщении
datatype - тип элементов принимаемого сообщения
source - номер процесса-отправителя
msgtag - идентификатор принимаемого сообщения
comm - идентификатор группы
OUT status - параметры принятого сообщения

Прием сообщения с идентификатором msgtag от процесса source с блокировкой. Число элементов в принимаемом сообщении не должно превосходить значения count. Если число принятых элементов меньше значения count, то гарантируется, что в буфере buf изменятся только элементы, соответствующие элементам принятого сообщения. Если нужно узнать точное число элементов в сообщении, то можно воспользоваться функцией MPI_Probe.

Блокировка гарантирует, что после возврата из подпрограммы все элементы сообщения приняты и расположены в буфере buf.

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

Если процесс посылает два сообщения другому процессу и оба эти сообщения соответствуют одному и тому же вызову MPI_Recv, то первым будет принято то сообщение, которое было отправлено раньше.

status содержит информацию о принятом сообщении: его идентификатор, номер задачи-передатчика, код завершения и количество фактически пришедших данных.

С одной стороны, мы передаем в MPI_Recv номер задачи, от которой ждем сообщение, и его идентификатор; а с другой - получаем их от MPI в структуре status? Это сделано потому, что MPI_Recv может быть вызвана с аргументами-джокерами ("принимай что угодно/от кого угодно"), и после такого приема данных программа узнает фактические номер/идентификатор, читая поля MPI_SOURCE и MPI_TAG из структуры status.

Поле MPI_ERROR, как правило, проверять необязательно - обработчик ошибок, устанавливаемый MPI по умолчанию, в случае сбоя завершит выполнение программы ДО возврата из MPI_Recv. Таким образом, после возврата из MPI_Recv поле status.MPI_ERROR может быть равно только 0 (MPI_SUCCESS);

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

int MPI_Get_Count( MPI_Status *status, MPI_Datatype datatype, int *count)

status - параметры принятого сообщения
datatype - тип элементов принятого сообщения
OUT count - число элементов сообщения

По значению параметра status данная подпрограмма определяет число уже принятых (после обращения к MPI_Recv) или принимаемых (после обращения к MPI_Probe или MPI_IProbe) элементов сообщения типа datatype.

int MPI_Probe( int source, int msgtag, MPI_Comm comm, MPI_Status *status)

source - номер процесса-отправителя или MPI_ANY_SOURCE
msgtag - идентификатор ожидаемого сообщения или MPI_ANY_TAG
comm - идентификатор группы
OUT status - параметры обнаруженного сообщения

Получение информации о структуре ожидаемого сообщения с блокировкой. Возврата из подпрограммы не произойдет до тех пор, пока сообщение с подходящим идентификатором и номером процесса-отправителя не будет доступно для получения. Атрибуты доступного сообщения можно определить обычным образом с помощью параметра status. Следует обратить внимание, что подпрограмма определяет только факт прихода сообщения, но реально его не принимает. Стандарт MPI гарантирует, что следующий за MPI_Probe вызов MPI_Recv с теми же параметрами (имеются в виду номер задачи-передатчика, идентификатор сообщения и коммуникатор) поместит в буфер пользователя именно то сообщение, которое было принято функцией MPI_Probe. MPI_Probe нужна в двух случаях:

  1. Когда задача-приемник не знает заранее длины ожидаемого сообщения. Пользовательский буфер заводится в динамической памяти:
    	MPI_Probe( MPI_ANY_SOURCE, tagMessageInt, MPI_COMM_WORLD, &status );
    	/* MPI_Probe вернет управление после того как примет */
    	/* данные в системный буфер */
    	MPI_Get_count( &status, MPI_INT, &bufElems );
    	buf = malloc( sizeof(int) * bufElems );
    	MPI_Recv( buf, bufElems, MPI_INT, ...
    	/* ... дальше параметры у MPI_Recv такие же, как в MPI_Probe ); */
    	/* MPI_Recv останется просто скопировать */
    	/* данные из системного буфера в пользовательский */ 
    

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

  2. Когда задача-приемник собирает сообщения от разных отправителей с содержимым разных типов. Без MPI_Probe порядок извлечения сообщений в буфер пользователя должен быть задан в момент компиляции:
	MPI_Recv( floatBuf, floatBufSize, MPI_FLOAT, MPI_ANY_SOURCE, tagFloatData, ... );
	MPI_Recv( intBuf,intBufSize,MPI_INT,MPI_ANY_SOURCE, tagIntData,... );
	MPI_Recv( charBuf,charBufSize,MPI_CHAR,MPI_ANY_SOURCE, tagCharData,... ); 

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

for( i=0; i<3; i++ ) {
	MPI_Probe( MPI_ANY_SOURCE,MPI_ANY_TAG,MPI_COMM_WORLD,&status );
	switch( status.MPI_TAG ) {
		case tagFloatData:
			MPI_Recv( floatBuf, floatBufSize, MPI_FLOAT, ... );
			break;
		case tagIntData:
			MPI_Recv( intBuf, intBufSize, MPI_INT, ... );
			break;
		case tagCharData:
			MPI_Recv( charBuf, charBufSize, MPI_CHAR, ... );
			break;
	} /* конец switch */
} /* конец for */ 

Многоточия здесь означают, что последние 4 параметра у MPI_Recv такие же, как и у предшествующей им MPI_Probe.

Неблокирующие функции

MPI обеспечивает также неблокирующие send и receive функции, которые позволяют совмещать вычисления с коммуникациями или несколько передач сообщений.

int MPI_ISend(void *buf, int count, MPI_Datatype datatype, int dest, int msgtag, MPI_Comm comm, MPI_Request *request)

buf - адрес начала буфера посылки сообщения
count - число передаваемых элементов в сообщении
datatype - тип передаваемых элементов
dest - номер процесса-получателя
msgtag - идентификатор сообщения
comm - идентификатор группы
OUT request - идентификатор асинхронной передачи

Передача сообщения, аналогичная MPI_Send, однако возврат из подпрограммы происходит сразу после инициализации процесса передачи без ожидания обработки всего сообщения, находящегося в буфере buf. Это означает, что нельзя повторно использовать данный буфер для других целей без получения дополнительной информации о завершении данной посылки. Окончание процесса передачи (т.е. того момента, когда можно переиспользовать буфер buf без опасения испортить передаваемое сообщение) можно определить с помощью параметра request и процедур MPI_Wait и MPI_Test.

Сообщение, отправленное любой из процедур MPI_Send и MPI_ISend, может быть принято любой из процедур MPI_Recv и MPI_IRecv.

int MPI_IRecv(void *buf, int count, MPI_Datatype datatype, int source, int msgtag, MPI_comm comm, MPI_Request *request)

OUT buf - адрес начала буфера приема сообщения
count - максимальное число элементов в принимаемом сообщении
datatype - тип элементов принимаемого сообщения
source - номер процесса-отправителя
msgtag - идентификатор принимаемого сообщения
comm - идентификатор группы
OUT request - идентификатор асинхронного приема сообщения

Прием сообщения, аналогичный MPI_Recv, однако возврат из подпрограммы происходит сразу после инициализации процесса приема без ожидания получения сообщения в буфере buf. Окончание процесса приема можно определить с помощью параметра request и процедур MPI_Wait и MPI_Test.

int MPI_Wait( MPI_Request *request, MPI_Status *status)

request - идентификатор асинхронного приема или передачи
OUT status - параметры сообщения

Ожидание завершения асинхронных процедур MPI_ISend или MPI_IRecv, ассоциированных с идентификатором request. В случае приема, атрибуты и длину полученного сообщения можно определить обычным образом с помощью параметра status.

int MPI_WaitAll( int count, MPI_Request *requests, MPI_Status *statuses)

count - число идентификаторов
requests - массив идентификаторов асинхронного приема или передачи
OUT statuses - параметры сообщений

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

int MPI_WaitAny( int count, MPI_Request *requests, int *index, MPI_Status *status)

count - число идентификаторов
requests - массив идентификаторов асинхронного приема или передачи
OUT index - номер завершенной операции обмена
OUT status - параметры сообщений

Выполнение процесса блокируется до тех пор, пока какая-либо операция обмена, ассоциированная с указанными идентификаторами, не будет завершена. Если несколько операций могут быть завершены, то случайным образом выбирается одна из них. Параметр index содержит номер элемента в массиве requests, содержащего идентификатор завершенной операции.

int MPI_WaitSome( int incount, MPI_Request *requests, int *outcount, int *indexes, MPI_Status *statuses)

incount - число идентификаторов
requests - массив идентификаторов асинхронного приема или передачи
OUT outcount - число идентификаторов завершившихся операций обмена
OUT indexes - массив номеров завершившихся операции обмена
OUT statuses - параметры завершившихся сообщений

Выполнение процесса блокируется до тех пор, пока по крайней мере одна из операций обмена, ассоциированных с указанными идентификаторами, не будет завершена. Параметр outcount содержит число завершенных операций, а первые outcount элементов массива indexes содержат номера элементов массива requests с их идентификаторами. Первые outcount элементов массива statuses содержат параметры завершенных операций.

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

request - идентификатор асинхронного приема или передачи
OUT flag - признак завершенности операции обмена
OUT status - параметры сообщения

Проверка завершенности асинхронных процедур MPI_ISend или MPI_IRecv, ассоциированных с идентификатором request. В параметре flag возвращает значение 1, если соответствующая операция завершена, и значение 0 в противном случае. Если завершена процедура приема, то атрибуты и длину полученного сообщения можно определить обычным образом с помощью параметра status.

int MPI_TestAll( int count, MPI_Request *requests, int *flag, MPI_STatus *statuses)

count - число идентификаторов
requests - массив идентификаторов асинхронного приема или передачи
OUT flag - признак завершенности операций обмена
OUT statuses - параметры сообщений

В параметре flag возвращает значение 1, если все операции, ассоциированные с указанными идентификаторами, завершены (с указанием параметров сообщений в массиве statuses). В противном случае возвращается 0, а элементы массива statuses неопределены.

int MPI_TestAny(int count, MPI_Request *requests, int *index, int *flag, MPI_Status *status)

count - число идентификаторов
requests - массив идентификаторов асинхронного приема или передачи
OUT index - номер завершенной операции обмена
OUT flag - признак завершенности операции обмена
OUT status - параметры сообщения

Если к моменту вызова подпрограммы хотя бы одна из операций обмена завершилась, то в параметре flag возвращается значение 1, index содержит номер соответствующего элемента в массиве requests, а status - параметры сообщения.

int MPI_TestSome( int incount, MPI_Request *requests, int *outcount, int *indexes, MPI_Status *statuses)

incount - число идентификаторов
requests - массив идентификаторов асинхронного приема или передачи
OUT outcount - число идентификаторов завершившихся операций обмена
OUT indexes - массив номеров завершившихся операции обмена
OUT statuses - параметры завершившихся операций

Данная подпрограмма работает так же, как и MPI_TestSome, за исключением того, что возврат происходит немедленно. Если ни одна из указанных операций не завершилась, то значение outcount будет равно нулю.

int MPI_Iprobe( int source, int msgtag, MPI_Comm comm, int *flag, MPI_Status *status)

source - номер процесса-отправителя или MPI_ANY_SOURCE
msgtag - идентификатор ожидаемого сообщения или MPI_ANY_TAG
comm - идентификатор группы
OUT flag - признак завершенности операции обмена
OUT status - параметры обнаруженного сообщения

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

Объединение запросов на взаимодействие

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

int MPI_Send_Init( void *buf, int count, MPI_Datatype datatype, int dest, int msgtag, MPI_Comm comm, MPI_Request *request)

buf - адрес начала буфера посылки сообщения
count - число передаваемых элементов в сообщении
datatype - тип передаваемых элементов
dest - номер процесса-получателя
msgtag - идентификатор сообщения
comm - идентификатор группы
OUT request - идентификатор асинхронной передачи

Формирование запроса на выполнение пересылки данных. Все параметры точно такие же, как и у подпрограммы MPI_ISend, однако в отличие от нее пересылка не начинается до вызова подпрограммы MPI_StartAll.

int MPI_Recv_Init( void *buf, int count, MPI_Datatype datatype, int source, int msgtag, MPI_Comm comm, MPI_Request *request)

OUT buf - адрес начала буфера приема сообщения
count - число принимаемых элементов в сообщении
datatype - тип принимаемых элементов
source - номер процесса-отправителя
msgtag - идентификатор сообщения
comm - идентификатор группы
OUT request - идентификатор асинхронного приема

Формирование запроса на выполнение приема данных. Все параметры точно такие же, как и у подпрограммы MPI_IReceive, однако в отличие от нее реальный прием не начинается до вызова подпрограммы MPI_StartAll.

MPI_Start_All( int count, MPI_Request *requests)

count - число запросов на взаимодействие
OUT requests - массив идентификаторов приема/передачи

Запуск всех отложенных взаимодействий, ассоциированных вызовами подпрограмм MPI_Send_Init и MPI_Recv_Init с элементами массива запросов requests. Все взаимодействия запускаются в режиме без блокировки, а их завершение можно определить обычным образом с помощью процедур MPI_Wait и MPI_Test.  

Совмещенные прием/передача сообщений

Некоторые конструкции с приемо-передачей применяются очень часто:

Обмен данными с соседями по группе (в группе четное количество ветвей!):

MPI_Comm_size( MPI_COMM_WORLD, &size );
MPI_Comm_rank( MPI_COMM_WORLD, &rank );
if( rank % 2 ) {
	/* Ветви с четными номерами сначала
	* передают следующим нечетным ветвям,
	* потом принимают от предыдущих
	*/
	MPI_Send(..., ( rank+1 ) % size ,...);
	MPI_Recv(..., ( rank+size-1 ) % size ,...);
} 
else {
	/* Нечетные ветви поступают наоборот:
	* сначала принимают от предыдущих ветвей,
	* потом передают следующим.
	*/
	MPI_Recv(..., ( rank-1 ) % size ,...);
	MPI_Send(..., ( rank+1 ) % size ,...);
} 

Посылка данных и получение подтверждения:

MPI_Send(..., anyRank ,...);/* Посылаем данные */
MPI_Recv(..., anyRank ,...);/* Принимаем подтверждение */ 

Ситуация настолько распространенная, что в MPI специально введены две функции, осуществляющие одновременно посылку одних данных и прием других. Первая из них - MPI_Sendrecv. Ее прототип содержит 12 параметров: первые 5 параметров такие же, как у MPI_Send, остальные 7 параметров такие же как у MPI_Recv. Один ее вызов проделывает те же действия, для которых в первом фрагменте требуется блок IF-ELSE с четырьмя вызовами. Следует учесть, что:

int MPI_Sendrecv( void *sbuf, int scount, MPI_Datatype stype, int dest, int stag, void *rbuf, int rcount, MPI_Datatype rtype, int source, MPI_DAtatype rtag, MPI_Comm comm, MPI_Status *status)

sbuf - адрес начала буфера посылки сообщения
scount - число передаваемых элементов в сообщении
stype - тип передаваемых элементов
dest - номер процесса-получателя
stag - идентификатор посылаемого сообщения
OUT rbuf - адрес начала буфера приема сообщения
rcount - число принимаемых элементов сообщения
rtype - тип принимаемых элементов
source - номер процесса-отправителя
rtag - идентификатор принимаемого сообщения
comm - идентификатор группы
OUT status - параметры принятого сообщения

MPI_Sendrecv_replace помимо общего коммуникатора использует еще и общий для приема-передачи буфер. Не очень удобно, что параметр count получает двойное толкование: это и количество отправляемых данных, и предельная емкость входного буфера. Показания к применению:

MPI_Sendrecv_replace так же гарантированно не вызывает клинча.

Что такое клинч? Дальше следует краткая иллюстрация этой ошибки, очень распространенной там, где для пересылок используется разделяемая память.

Вариант 1:

-- Ветвь 1 -- -- Ветвь 2 --

Recv( из ветви 2 ) Recv( из ветви 1 )

Send( в ветвь 2 ) Send( в ветвь 1 )

Вариант 1 вызовет клинч, какой бы инструментарий не использовался: функция приема не вернет управления до тех пор, пока не получит данные; поэтому функция передачи не может приступить к отправке данных; поэтому функция приема, и так далее …

Вариант 2:

-- Ветвь 1 -- -- Ветвь 2 --

Send( в ветвь 2 ) Send( в ветвь 1 )

Recv( из ветви 2 ) Recv( из ветви 1 )

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

Однако при использовании MPI зависания во втором варианте не произойдет! MPI_Send, если на приемной стороне нет готовности (не вызван MPI_Recv), не станет ее дожидаться, а положит данные во временный буфер и вернет управление программе НЕМЕДЛЕННО. Когда MPI_Recv будет вызван, данные он получит не из пользовательского буфера напрямую, а из промежуточного системного. Буферизация - дело громоздкое - может быть, и не всегда сильно экономит время (особенно на SMP-машинах), зато повышает надежность: делает программу более устойчивой к ошибкам программиста.

MPI_Sendrecv и MPI_Sendrecv_replace также делают программу более устойчивой: с их использованием программист лишается возможности перепутать варианты 1 и 2.  


Типы данных

Зачем MPI знать тип передаваемых данных?

Стандартные функции пересылки данных, например, memcpy, обходятся без подобной информации - им требуется знать только размер в байтах. Вместо одного такого аргумента функции MPI получают два: количество элементов некоторого типа и символический описатель указанного типа (MPI_INT, и т.д.). Причин тому несколько:

  1. Пользователю MPI позволяет описывать свои собственные типы данных, которые располагаются в памяти не непрерывно, а с разрывами, или, наоборот, с "налезаниями" друг на друга. Переменная такого типа характеризуется не только размером, и эти характеристики MPI хранит в описателе типа.

  2. Приложение MPI может работать на гетерогенном вычислительном комплексе (коллективе ЭВМ с разной архитектурой). Одни и те же типы данных на разных машинах могут иметь разное представление. Если приложение работает в гетерогенной сети, через сеть задачи обмениваются данными в формате XDR (eXternal Data Representation), принятом в Internet. Перед отправкой и после приема данных задача конвертирует их в/из формата XDR. Естественно, при этом MPI должен знать не просто количество передаваемых байт, но и тип содержимого.

  3. Обязательным требованием к MPI была поддержка языка Фортран в силу его инерционной популярности. Фортрановский тип CHARACTER требует особого обращения, поскольку переменная такого типа содержит не собственно текст, а адрес текста и его длину. Функция MPI, получив адрес переменной, должна извлечь из нее адрес текста и копировать сам текст. Это и произойдет, если в поле аргумента-описателя типа стоит MPI_CHARACTER. Ошибка в указании типа приведет: при отправке - к копированию служебных данных вместо текста, при приеме - к записи текста на место служебных данных. И то, и другое приводит к ошибкам времени выполнения.

  4. Такие часто используемые в Си типы данных, как структуры, могут содержать в себе некоторое пустое пространство, чтобы все поля в переменной такого типа размещались по адресам, кратным некоторому четному числу (часто 2, 4 или 8) - это ускоряет обращение к ним. Причины тому чисто аппаратные. Выравнивание данных настраивается ключами компилятора. Разные задачи одного и того же приложения, выполняющиеся на одной и той же машине (даже на одном и том же процессоре), могут быть построены с разным выравниванием, и типы с одинаковым текстовым описанием будут иметь разное двоичное представление. MPI будет вынужден позаботиться о правильном преобразовании. Например, переменные такого типа могут занимать 9 или 16 байт:
typedef struct {
	charc;
	doubled;
} CharDouble; 
 

Передача разнотипных данных

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

Способ 1. Каждый элемент в разнотипном наборе данных посылается отдельно:

	#define msgTag 10
	struct {
		inti;
		float f[4];
		charc[8];
	} s;

	MPI_Send(&s.i, 1, MPI_INT,targetRank, msgTag,MPI_COMM_WORLD );
	MPI_Send( s.f, 4, MPI_FLOAT, targetRank, msgTag+1, MPI_COMM_WORLD );
	MPI_Send( s.c, 8, MPI_CHAR,targetRank, msgTag+2, MPI_COMM_WORLD ); 

... и на приемной стороне столько же раз вызывается MPI_Recv.

Этот способ является крайне не эффективным, так как каждая операция пересылки требует инициализации.

Способ 2 ("классический"). Функция приема/передачи вызывается один раз, но до/после нее многократно вызывается функция упаковки/распаковки:

Передача:

	int bufPos = 0;
	char tempBuf[ sizeof(s) ];
	MPI_Pack(&s.i,1,MPI_INT,tempBuf,sizeof(tempBuf),&bufPos,MPI_COMM_WORLD);
	MPI_Pack(s.f,4,MPI_FLOAT,tempBuf,sizeof(tempBuf),&bufPos,MPI_COMM_WORLD);
	MPI_Pack(s.c,8,MPI_CHAR,tempBuf,sizeof(tempBuf),&bufPos,MPI_COMM_WORLD );
	MPI_Send(tempBuf, bufPos,MPI_BYTE,targetRank,msgTag,MPI_COMM_WORLD );

Прием:

	int bufPos = 0;
	char tempBuf[ sizeof(s) ];
	MPI_Recv( tempBuf, sizeof(tempBuf), MPI_BYTE, sourceRank, msgTag,
	MPI_COMM_WORLD, &status );
	MPI_Unpack(tempBuf,sizeof(tempBuf),&bufPos,&s.i,1,MPI_INT,MPI_COMM_WORLD);
	MPI_Unpack(tempBuf,sizeof(tempBuf),&bufPos,s.f,4,MPI_FLOAT,MPI_COMM_WORLD);
	MPI_Unpack(tempBuf,sizeof(tempBuf),&bufPos,s.c,8,MPI_CHAR,MPI_COMM_WORLD); 

Этот способ пришел в MPI из PVM, где предлагается в качестве единственного. Он прост в понимании. Замечания по применению:

1.MPI_BYTE - это особый описатель типа; который не описывает тип данных для конкретного языка программирования (в Си он ближе всего к unsigned char). Использование MPI_BYTE означает, что содержимое соответствующего массива НЕ ДОЛЖНО подвергаться НИКАКИМ преобразованиям - и на приемной, и на передающей стороне массив будет иметь одну и ту же длину и одинаковое ДВОИЧНОЕ представление.

2.Зачем функциям упаковки/распаковки требуется описатель области связи? Описатель, помимо прочего, несет в себе информацию о распределении подсоединенных к области связи задач по процессорам и компьютерам. Если процессоры одинаковые, или задачи выполняются на одном и том же процессоре, данные просто копируются, иначе происходит их преобразование в/из формата XDR (eXternal Data Representation - разработан фирмой Sun Microsystems, используется в Интернете для взаимодействия разнотипных машин). Учтите, что коммуникаторы у функций упаковки/распаковки и у соответствующей функции передачи/приема должны совпадать, иначе произойдет ошибка;

3.По мере того как во временный буфер помещаются данные или извлекаются оттуда, MPI сохраняет текущую позицию в переменной, которая в приведенном примере названа bufPos. Не забудьте проинициализировать ее нулем перед тем как начинать упаковывать/извлекать. Естественно, что передается она не по значению, а по ссылке. Первый же аргумент - "адрес временного буфера" - во всех вызовах остается неизменным;

4.В примере НЕКОРРЕКТНО выбран размер временного буфера: использовалось НЕВЕРНОЕ предположение, что в XDR-формате данные займут места не больше, чем в формате используемого ветвью процессора; или что XDR-преобразование заведомо не будет применено. Правильным же решением будет для определения необходимого размера временного буфера на приемной стороне использовать связку MPI_Probe / MPI_Get_count / MPI_Recv, а на передающей - функцию MPI_Pack_size:

	int bufSize = 0;
	void *tempBuf;
	MPI_Pack_size( 1, MPI_INT,MPI_COMM_WORLD, &bufSize );
	MPI_Pack_size( 4, MPI_FLOAT, MPI_COMM_WORLD, &bufSize );
	MPI_Pack_size( 8, MPI_CHAR,MPI_COMM_WORLD, &bufSize );
	tempBuf = malloc( bufSize );
	/* ... теперь можем упаковывать, не опасаясь переполнения */ 

Однако и второй способ замедляет работу: по сравнению с единственным вызовом memcpy на SMP-машине или одном процессоре несколько раз производится упаковка/распаковка - дело весьма небыстрое!

Способ 3 ("жульнический"). Если есть уверенность, что одни и те же типы данных в обеих ветвях приложения имеют одинаковое двоичное представление, то:

	Передача:MPI_Send( &s, sizeof(s), MPI_BYTE, ... );
	Прием:MPI_Recv( &s, sizeof(s), MPI_BYTE, ... ); 

Способ 4. Создание и использование собственных типов данных.

Общие правила:

Конструкторы типа.

MPI_Type_contiguous : самый простой конструктор типа, он создает описание массива. В следующем примере оба вызова MPI_Send делают одно и то же.

	int a[16];
	MPI_Datatype intArray16;
	MPI_Type_contiguous( 16, MPI_INT, &intArray16 );
	MPI_Type_commit( &intArray16 );

	MPI_Send( a, 16, MPI_INT, ... );
	MPI_Send( a, 1, intArray16, ... );

	MPI_Type_free( &intArray16 ); 

Функция MPI_Type_count возвращает количество ячеек в переменной составного типа: после MPI_Type_count( intArray16, &count ) значение count станет равным 16. Как правило, прямой необходимости использовать эти функции нет, и тем не менее.

MPI_Type_vector(int count,int blocklength,int stride,MPI_Datatype oldtype,MPI_Datatype &newtype)

служит для описания множества однотипных равноудаленных в памяти массивов данных. Позволяет весьма изощренные манипуляции с данными. Он создает описание для не-непрерывной последовательности элементов, которые, в свою очередь, составлены из непрерывной последовательности ячеек базового (уже определенного) типа
count - количество элементов в новом типе
blocklength - количество ячеек базового типа в одном элементе
stride - расстояние между НАЧАЛАМИ эл-тов, в числе ячеек
oldtype - описатель базового типа, т.е. типа ячейки
&newtype - cсылка на новый описатель

То есть:

Функция MPI_Type_hvector полностью ей аналогична, за одним исключением: расстояние между элементами задается не в количестве ячеек базового типа, а в байтах.

MPI_Type_indexed : расширение "векторного" описателя; длины массивов и расстояния между ними теперь не фиксированы, а у каждого массива свои. Соответственно, второй и третий аргументы здесь - не переменные, а массивы: массив длин и массив позиций.

Пример: создание шаблона для выделения верхней правой части матрицы.

	#defineSIZE100
	float a[ SIZE ][ SIZE ];
	int pos[ SIZE ]
	int len[ SIZE ];
	MPI_Datatype upper;
	...
	for( i=0; i<SIZE; i++ ) {/*xxxxxx*/
		pos[i] = SIZE*i + i;/*.xxxxx*/
		len[i] = SIZE - i; /*..xxxx*/
	}/*...xxx*/

	MPI_Type_indexed(
	SIZE,/* количество массивов в переменной нового типа */
	len,/* длины этих массивов */
	pos,/* их позиции от начала переменной, */
	/* отсчитываемые в количестве ячеек */
	MPI_FLOAT,/* тип ячейки массива */
	&upper );
	
	MPI_Type_commit( &upper );

	/* Поступающий поток чисел типа 'float' будет
	* размещен в верхней правой части матрицы 'a'
	*/
	MPI_Recv( a, 1, upper, .... ); 

Аналогично работает функция MPI_Type_hindexed, но позиции массивов от начала переменной задаются не в количестве ячеек базового типа, а в байтах.

MPI_Type_struct(int count,int *len,MPI_Aint *pos,MPI_Datatype *types,MPI_Datatype *newtype )

создает описатель структуры.
count - количество полей
*len - массив с длинами полей (на тот случай, если это массивы)
*pos - массив со смещениями полей от начала структуры, в байтах
*types - массив с описателями типов полей
*newtype - ссылка на создаваемый тип

Здесь используется тип MPI_Aint: это просто скалярный тип, переменная которого имеет одинаковый с указателем размер. Введен он исключительно для единообразия с Фортраном, в котором нет типа "указатель". По этой же причине имеется и функция MPI_Address: в Си она не нужна (используются оператор вычисления адреса & и основанный на нем макрос offsetof() ); а в Фортране оператора вычисления адреса нет, и используется MPI_Address.

Пример создания описателя типа "структура":

	#include <stddef.h>/* подключаем макрос 'offsetof()' */

	typedef struct {
		inti;
		double d[3];
		longl[8];
		charc;
	} AnyStruct;

	AnyStruct st;

	MPI_Datatype anyStructType;

	intlen[5] = { 1, 3, 8, 1, 1 };
	MPI_Aintpos[5] = { offsetof(AnyStruct,i), offsetof(AnyStruct,d),
		offsetof(AnyStruct,l), offsetof(AnyStruct,c),
		sizeof(AnyStruct) };
	MPI_Datatype typ[5] = { MPI_INT,MPI_DOUBLE,MPI_LONG,MPI_CHAR,MPI_UB };

	MPI_Type_struct( 5, len, pos, typ, &anyStructType );
	MPI_Type_commit( &anyStructType );
	/* подготовка закончена */

	MPI_Send( st, 1, anyStructType, ... ); 

Обратите внимание: структура в примере содержит 4 поля, а массивы для ее описания состоят из 5 элементов. Сделано это потому, что MPI должен знать не только смещения полей, но и размер всей структуры. Для этого и служит псевдотип MPI_UB ("upper bound"). Адрес начала структуры и адрес ее первого поля, как правило, совпадают, но если это не так, то нулевым элементом массива typ должен быть MPI_LB.

MPI_Type_extent и MPI_Type_size : важные информационные функции.

Можно сказать, что MPI_Type_extent сообщает, сколько места переменная типа занимает при хранении в памяти, а MPI_Type_size - какой МИНИМАЛЬНЫЙ размер она будет иметь при передаче (ужатая за счет неиспользуемого пространства). В Фортране их придется использовать постоянно ввиду отсутствия функции sizeof.  

Групповые (коллективные) взаимодействия

Под термином "коллективные" в MPI подразумеваются три группы функций:

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

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

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

Функции коллективного обмена данными

Основные особенности и отличия от коммуникаций типа "точка-точка":

 

int MPI_Bcast( void *buf, int count, MPI_Datatype datatype, int source, MPI_Comm comm)

OUT buf - адрес начала буфера посылки сообщения
count - число передаваемых элементов в сообщении
datatype - тип передаваемых элементов
source - номер рассылающего процесса
comm - идентификатор группы

Рассылка сообщения от процесса source всем процессам, включая рассылающий процесс. При возврате из процедуры содержимое буфера buf процесса source будет скопировано в локальный буфер процесса. Значения параметров count, datatype и source должны быть одинаковыми у всех процессов.

int MPI_Gather( void *sbuf, int scount, MPI_Datatype stype, void *rbuf, int rcount, MPI_Datatype rtype, int dest, MPI_Comm comm)

sbuf - адрес начала буфера посылки
scount - число элементов в посылаемом сообщении
stype - тип элементов отсылаемого сообщения
OUT rbuf - адрес начала буфера сборки данных
rcount - число элементов в принимаемом сообщении
rtype - тип элементов принимаемого сообщения
dest - номер процесса, на котором происходит сборка данных
comm - идентификатор группы
OUT ierror - код ошибки

Функции поддержки распределенных операций

Сборка данных со всех процессов в буфере rbuf процесса dest. Каждый процесс, включая dest, посылает содержимое своего буфера sbuf процессу dest. Собирающий процесс сохраняет данные в буфере rbuf, располагая их в порядке возрастания номеров процессов. Параметр rbuf имеет значение только на собирающем процессе и на остальных игнорируется, значения параметров count, datatype и dest должны быть одинаковыми у всех процессов.

int MPI_AllReduce( void *sbuf, void *rbuf, int count, MPI_Datatype datatype, MPI_Op op, MPI_Comm comm)

sbuf - адрес начала буфера для аргументов
OUT rbuf - адрес начала буфера для результата
count - число аргументов у каждого процесса
datatype - тип аргументов
op - идентификатор глобальной операции
comm - идентификатор группы

Выполнение count глобальных операций op с возвратом count результатов во всех процессах в буфере rbuf. Операция выполняется независимо над соответствующими аргументами всех процессов. Значения параметров count и datatype у всех процессов должны быть одинаковыми. Из соображений эффективности реализации предполагается, что операция op обладает свойствами ассоциативности и коммутативности.

Идентификаторы глобальных операций:

int MPI_Reduce( void *sbuf, void *rbuf, int count, MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm)

sbuf - адрес начала буфера для аргументов
OUT rbuf - адрес начала буфера для результата
count - число аргументов у каждого процесса
datatype - тип аргументов
op - идентификатор глобальной операции
root - процесс-получатель результата
comm - идентификатор группы

Функция аналогична предыдущей, но результат будет записан в буфер rbuf только у процесса root.

Пример использования функции MPI_Reduce:

int vector[16];
int resultVector[16];
MPI_Comm_rank( MPI_COMM_WORLD, &myRank );
for( i=0; i<16; i++ )
	vector[i] = myRank*100 + i;
MPI_Reduce(
	vector,  /* каждая задача в коммуникаторе предоставляет вектор */
	resultVector,   /* задача номер 'root' собирает данные сюда */
	16,/* количество ячеек в исходном и результирующем массивах */
	MPI_INT, /* и тип ячеек */
	MPI_SUM, /* описатель операции: поэлементное сложение векторов */
	0,/* номер задачи, собирающей результаты в 'resultVector' */
	MPI_COMM_WORLD  /* описатель области связи */
);
if( myRank==0 )
	/* печатаем resultVector, равный сумме векторов */ 

Синхронизация процессов

int MPI_Barrier( MPI_Comm comm)

comm - идентификатор группы

Блокирует работу процессов, вызвавших данную процедуру, до тех пор, пока все оставшиеся процессы группы comm также не выполнят эту процедуру.  


Коммуникаторы, группы и области связи.

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

Область связи ("communication domain") - это нечто абстрактное: в распоряжении программиста нет типа данных, описывающего непосредственно области связи, как нет и функций по управлению ими. Области связи автоматически создаются и уничтожаются вместе с коммуникаторами. Абонентами одной области связи являются ВСЕ задачи либо одной, либо двух групп.

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

Зачем вообще нужны разные группы, разные области связи и разные их описатели?

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

Важно помнить, что ВСЕ функции, создающие коммуникатор, являются КОЛЛЕКТИВНЫМИ! Именно это качество позволяет таким функциям возвращать в разные ветви ОДИН И ТОТ ЖЕ описатель. Коллективность, напомню, заключется в следующем:

 

Создание коммуникаторов и групп

Копирование. Самый простой способ создания коммуникатора - скопировать "один-в-один" уже имеющийся:

	MPI_Comm tempComm;
	MPI_Comm_dup( MPI_COMM_WORLD, &tempComm );
	 /* ... передаем данные через tempComm ... */
	MPI_Comm_free( &tempComm ); 

Новая группа при этом не создается - набор задач остается прежним. Новый коммуникатор наследует все свойства копируемого.

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

	MPI_Comm_split(
	existingComm,   /* существующий описатель, например MPI_COMM_WORLD */
	indexOfNewSubComm,   /* номер подгруппы, куда надо поместить ветвь */
	rankInNewSubComm,/* желательный номер в новой подгруппе */
	&newSubComm );/* описатель области связи новой подгруппы */ 

Эта функция имеет одинаковый первый параметр во всех ветвях, но разные второй и третий - и в зависимости от них разные ветви определяются в разные подгруппы; возвращаемый в четвертом параметре описатель будет принимать в разных ветвях разные значения (всего столько разных значений, сколько создано подгрупп). Если indexOfNewSubComm равен MPI_UNDEFINED, то в newSubComm вернется MPI_COMM_NULL, то есть ветвь не будет включена ни в какую из созданных групп.

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

  1. функцией MPI_Comm_group определяется группа, на которую указывает соответствующий коммуникатор;
  2. на базе существующих групп функциями семейства MPI_Group_xxx создаются новые группы с нужным набором ветвей;
  3. для итоговой группы функцией MPI_Comm_create создается коммуникатор; не забудьте, что она должна быть вызвана во ВСЕХ ветвях-абонентах коммуникатора, передаваемого первымпараметром;
  4. все описатели созданных групп очищаются вызовами функции MPI_Group_free.

Такой механизм позволяет, в частности, не только расщеплять группы подобно MPI_Comm_split, но и объединять их. Всего в MPI определено 7 разных функций конструирования групп.

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

Полезная нагрузка коммуникатора: атрибуты.

Помимо характеристик области связи, тело коммуникатора содержит в себе некие дополнительные данные (атрибуты). Механизм хранения атрибутов называется "caching". Атрибуты могут быть системные и пользовательские; в системных, в частности, хранятся:

Атрибуты идентифицируются целыми числами, которые MPI назначает автоматически. Некоторые константы для описания системных атрибутов: MPI_TAG_UB, MPI_HOST, MPI_IO, MPI_WTIME_IS_GLOBAL. К этим атрибутам программист обращается редко, и менять их не может; а для таких часто используемых атрибутов, как обработчик ошибок или описание топологии, существуют персональные наборы функций, например, MPI_Errhandler_xxx.

Атрибуты - удобное место хранения совместно используемой информации; помещенная в атрибут одной из ветвей, такая информация становится доступной всем использующим коммуникатор ветвям БЕЗ пересылки сообщений (вернее, на MPP-машине, к примеру, сообщения будут, но на системном уровне, т.е. скрытые от глаз программиста).

Пользовательские атрибуты создаются и уничтожаются функциями MPI_Keyval_create и MPI_Keyval_free; модифицируются функциями MPI_Attr_put, MPI_Attr_get и MPI_Attr_delete.

При создании коммуникатора на базе существующего атрибуты из последнего тем или иным образом копируются или нет в зависимости от функции копирования типа MPI_Copy_function, адрес которой является параметром функции создания атрибута.

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

Корректное удаление отслуживших описателей.

Здесь имеются в виду ВСЕ типы системных данных, для которых предусмотрена функция MPI_Xxx_free (и константа MPI_XXX_NULL):

1.коммуникаторы;

2.группы;

3.типы данных;

4.распределенные операции;

5.квитанции (request's);

6.атрибуты коммуникаторов;

7.обработчики ошибок (errhandler's).

Далее приводится описание на примере коммуникаторов и групп, но изложенная схема является общей для всех типов ресурсов.

Не играет роли, в каком порядке уничтожать взаимосвязанные описатели. Главное - не забыть вызвать функцию удаления ресурса MPI_Xxx_free вовсе. Соответствующий ресурс не будет удален немедленно, он прекратит существование только если будут выполнены два условия:

  1. программе пользователя никогда не предоставлялись ссылки на ресурс, или всепользовательские ссылки очищены вызовами MPI_Xxx_free ;
  2. ресурс перестает использоваться другими ресурсами MPI, то есть удаляются все системные ссылки.

Взаимосвязанными описателями являются описатели коммуникатора и группы (коммуникатор ссылается на группу); или описатели типов, если один создан на базе другого (порожденный ссылается на исходный).

Пример:

	MPI_Comm subComm;
	MPI_Group subGroup;
	int rank;
	MPI_Comm_rank( MPI_COMM_WORLD, &rank );

	MPI_Comm_split( MPI_COMM_WORLD, rank / 3, rank % 3, &subComm );
	 /* Теперь создан коммуникатор subComm, и автоматически создана
	  * группа, на которую распространяется его область действия.
	  * На коммуникатор заведена ссылка из программы - subComm.
	  * На группу заведена системная ссылка из коммуникатора.
	  */

	MPI_Comm_group( subComm, &subGroup );
	 /* Теперь на группу имеется две ссылки - системная
	  * из коммуникатора, и пользовательская subGroup.
	  */

	MPI_Group_free( &subGroup );
	 /* Пользовательская ссылка на группу уничтожена,
	  * subGroup сброшен в MPI_GROUP_NULL.
	  * Собственно описание группы из системных данных не удалено,
	  * так как на него еще ссылается коммуникатор.
	  */

	MPI_Comm_free( &subComm );
	 /* Удалена пользовательская ссылка на коммуникатор,
	  * subComm сброшен в MPI_COMM_NULL. Так как других ссылок
	  * на коммуникатор нет, его описание удаляется из системных данных.
	  * Вместе с коммуникатором удалена системная ссылка на группу.
	  * Так как других ссылок на группу нет, ее описание удаляется
	  * из системных данных.
	  */

Еще раз: для MPI не играет роли, в каком порядке будут вызваны завершающие вызовы MPI_Xxx_free, это дело программы.

И не пытайтесь уничтожать константные описатели вроде MPI_COMM_WORLD или MPI_CHAR: их создание и уничтожение - дело самого MPI.  


Предопределенные константы

Предопределенные константы типа элементов сообщений

Константы MPI		Тип в C 
MPI_CHAR			signed char 
MPI_SHORT			signed int 
MPI_INT			signed int 
MPI_LONG			signed long int 
MPI_UNSIGNED_CHAR		unsigned char 
MPI_UNSIGNED_SHORT	unsigned int 
MPI_UNSIGNED		unsigned int 
MPI_UNSIGNED_LONG		unsigned long int 
MPI_FLOAT			float 
MPI_DOUBLE			double 
MPI_LONG_DOUBLE		long double 

Другие предопределенные типы

MPI_Status - структура; атрибуты сообщений; содержит три обязательных поля: 
MPI_Source (номер процесса отправителя) 
MPI_Tag (идентификатор сообщения) 
MPI_Error (код ошибки) 

MPI_Request - системный тип; идентификатор операции посылки-приема сообщения 

MPI_Comm - системный тип; идентификатор группы (коммуникатора) 

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

Константы-пустышки

MPI_COMM_NULL 
MPI_DATATYPE_NULL 
MPI_REQUEST_NULL 

Константа неопределенного значения

MPI_UNDEFINED 

Глобальные операции

MPI_MAX 
MPI_MIN 
MPI_SUM 
MPI_PROD 

Любой процесс/идентификатор

MPI_ANY_SOURCE 
MPI_ANY_TAG 

Код успешного завершения процедуры

MPI_SUCCESS