Коммуникационная библиотека MPI - общепризнанный стандарт в параллельном программировании с использованием механизма передачи сообщений. Полное и строгое описание среды программирования MPI можно найти в авторском описании разработчиков [1,2]. К сожалению, до настоящего времени не имеется перевода этого документа на русский язык. Большую подборку материалов по MPI можно найти на сервере НИВЦ МГУ [3].

1. ОБЩАЯ ОРГАНИЗАЦИЯ MPI
2.БАЗОВЫЕ ФУНКЦИИ MPI
3. КОММУНИКАЦИОННЫЕ ОПЕРАЦИИ ТИПА ТОЧКА-ТОЧКА
4. КОЛЛЕКТИВНЫЕ ОПЕРАЦИИ
5. ПРОИЗВОДНЫЕ ТИПЫ ДАННЫХ И ПЕРЕДАЧА УПАКОВАННЫХ ДАННЫХ
6. РАБОТА С ГРУППАМИ И КОММУНИКАТОРАМИ
7. ТОПОЛОГИЯ ПРОЦЕССОВ
8. ПРИМЕРЫ ПРОГРАММ
ЗАКЛЮЧЕНИЕ
ЛИТЕРАТУРА

1. ОБЩАЯ ОРГАНИЗАЦИЯ MPI

MPI-программа представляет собой набор независимых процессов, каждый из которых выполняет свою собственную программу (не обязательно одну и ту же), написанную на языке C или FORTRAN. Появились реализации MPI для C++, однако разработчики стандарта MPI за них ответственности не несут. Процессы MPI-программы взаимодействуют друг с другом посредством вызова коммуникационных процедур. Как правило, каждый процесс выполняется в своем собственном адресном пространстве, однако допускается и режим разделения памяти. MPI не специфицирует модель выполнения процесса - это может быть как последовательный процесс, так и многопотоковый. MPI не предоставляет никаких средств для распределения процессов по вычислительным узлам и для запуска их на исполнение. Эти функции возлагаются либо на операционную систему, либо на программиста. В частности, на nCUBE2 используется стандартная команда xnc, а на кластерах - специальный командный файл (скрипт) mpirun, который предполагает, что исполнимые модули уже каким-то образом распределены по компьютерам кластера. Описываемый в данном методическом пособии стандарт MPI 1.1 не содержит механизмов динамического создания и уничтожения процессов во время выполнения программы. MPI не накладывает каких-либо ограничений на то, как процессы будут распределены по процессорам, в частности, возможен запуск MPI программы с несколькими процессами на обычной однопроцессорной системе.

Для идентификации наборов процессов вводится понятие группы, объединяющей все или какую-то часть процессов. Каждая группа образует область связи, с которой связывается специальный объект - коммуникатор области связи. Процессы внутри группы нумеруются целым числом в диапазоне 0..groupsize-1. Все коммуникационные операции с некоторым коммуникатором будут выполняться только внутри области связи, описываемой этим коммуникатором. При инициализации MPI создается предопределенная область связи, содержащая все процессы MPI-программы, с которой связывается предопределенный коммуникатор MPI_COMM_WORLD. В большинстве случаев на каждом процессоре запускается один отдельный процесс, и тогда термины процесс и процессор становятся синонимами, а величина groupsize становится равной NPROCS - числу процессоров, выделенных задаче. В дальнейшем обсуждении мы будем понимать именно такую ситуацию и не будем очень уж строго следить за терминологией.

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

Набор функций библиотеки MPI далеко выходит за рамки набора функций, минимально необходимого для поддержки механизма передачи сообщений [1]. Однако сложность этой библиотеки не должна пугать пользователей, поскольку, в конечном итоге, все это множество функций предназначено для облегчения разработки эффективных параллельных программ. В конце концов, пользователю принадлежит право самому решать, какие средства из предоставляемого арсенала использовать, а какие нет. В принципе, любая параллельная программа может быть написана с использованием всего 6 MPI функций, а достаточно полную и удобную среду программирования составляет набор из 24 функций [5].

Каждая из MPI функций характеризуется способом выполнения:

  1. Локальная функция - выполняется внутри вызывающего процесса. Ее завершение не требует коммуникаций.
  2. Нелокальная функция - для ее завершения требуется выполнение MPI процедуры другим процессом.
  3. Глобальная функция - процедуру должны выполнять все процессы группы. Несоблюдение этого условия может приводить к зависанию задачи.
  4. Блокирующая функция - возврат управления из процедуры гарантирует возможность повторного использования параметров, участвующих в вызове. Никаких изменений в состоянии процесса, вызвавшего блокирующий запрос, до выхода из процедуры не может происходить.
  5. Неблокирующая функция - возврат из процедуры происходит немедленно, без ожидания окончания операции и до того, как будет разрешено повторное использование параметров, участвующих в запросе. Завершение неблокирующих операций осуществляется специальными функциями.
  6. Использование библиотеки MPI имеет некоторые отличия в языках C и FORTRAN.

    В языке C все процедуры являются функциями, и большинство из них возвращает код ошибки. При использовании имен подпрограмм и именованных констант необходимо строго соблюдать регистр символов. Массивы индексируются с 0. Логические переменные представляются типом int (true соответствует 1, а false - 0). Определение всех именованных констант, прототипов функций и определение типов выполняется подключением файла mpi.h. Введение собственных типов в MPI было продиктовано тем обстоятельством, что стандартные типы языков на разных платформах имеют различное представление. MPI допускает возможность запуска процессов параллельной программы на компьютерах различных платформ, обеспечивая при этом автоматическое преобразование данных при пересылках. В таблице 1 приведено соответствие предопределенных в MPI типов стандартным типам языка С.

    Таблица 1. Соответствие между MPI-типами и типами языка C

    тип MPI

    тип языка C

    MPI_CHAR

    signed char

    MPI_SHORT

    signed short int

    MPI_INT

    signed int

    MPI_LONG

    signed long int

    MPI_UNSIGNED_CHAR

    unsigned char

    MPI_UNSIGNED_SHORT

    unsigned short int

    MPI_UNSIGNED

    unsigned int

    MPI_UNSIGNED_LONG

    unsigned long int

    MPI_FLOAT

    float

    MPI_DOUBLE

    double

    MPI_LONG_DOUBLE

    long double

    MPI_BYTE

     

    MPI_PACKED

     

    В языке FORTRAN большинство MPI процедур являются подпрограммами (вызываются с помощью оператора CALL), а код ошибки возвращают через дополнительный последний параметр процедуры. Несколько процедур, оформленных в виде функций, код ошибки не возвращают. Не требуется строгого соблюдения регистра символов в именах подпрограмм и именованных констант. Массивы индексируются с 1. Объекты MPI, которые в языке C являются структурами, в языке FORTRAN представляются массивами целого типа. Определение всех именованных констант и определение типов выполняется подключением файла mpif.h. В таблице 2 приведено соответствие предопределенных в MPI типов стандартным типам языка FORTRAN.

    Таблица 2. Соответствие между MPI-типами и типами языка FORTRAN

    Тип MPI

    Тип языка FORTRAN

    MPI_INTEGER

    INTEGER

    MPI_REAL

    REAL

    MPI_DOUBLE_PRECISION

    DOUBLE PRECISION

    MPI_COMPLEX

    COMPLEX

    MPI_LOGICAL

    LOGICAL

    MPI_CHARACTER

    CHARACTER(1)

    MPI_BYTE

     

    MPI_PACKED

     

    В таблицах 1 и 2 перечислен обязательный минимум поддерживаемых стандартных типов, однако, если в базовой системе представлены и другие типы, то их поддержку будет осуществлять и MPI, например, если в системе есть поддержка комплексных переменных двойной точности DOUBLE COMPLEX, то будет присутствовать тип MPI_DOUBLE_COMPLEX. Типы MPI_BYTE и MPI_PACKED используется для передачи двоичной информации без какого-либо преобразования. Кроме того, программисту предоставляются средства создания собственных типов на базе стандартных (раздел 5.1).

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

    2. БАЗОВЫЕ ФУНКЦИИ MPI

    Любая прикладная MPI-программа (приложение) должна начинаться с вызова функции инициализации MPI (функция MPI_Init). В результате выполнения этой функции создается группа процессов, в которую помещаются все процессы приложения, и создается область связи, описываемая предопределенным коммуникатором MPI_COMM_WORLD. Эта область связи объединяет все процессы приложения. Процессы в группе упорядочены и пронумерованы от 0 до groupsize-1, где groupsize равно числу процессов в группе. Кроме этого, создается предопределенный коммуникатор MPI_COMM_SELF, описывающий свою область связи для каждого отдельного процесса.

    Синтаксис функции инициализации MPI_Init значительно отличается в языках C и FORTRAN:

    C:
    int MPI_Init(int *argc, char ***argv)
    FORTRAN:
    MPI_INIT(IERROR)
    INTEGER IERROR

    В программах на C каждому процессу при инициализации передаются аргументы функции main, полученные из командной строки. В программах на языке FORTRAN параметр IERROR является выходным и возвращает код ошибки.

    Функция завершения MPI программ MPI_Finalize.

    C:
    int MPI_Finalize(void)
    FORTRAN:
    MPI_FINALIZE(IERROR)
    INTEGER IERROR

    Функция закрывает все MPI-процессы и ликвидирует все области связи.

     

    Функция определения числа процессов в области связи MPI_Comm_size.

    C:
    int MPI_Comm_size(MPI_Comm comm, int *size)
    FORTRAN:
    MPI_COMM_SIZE(COMM, SIZE, IERROR)
    INTEGER COMM, SIZE, IERROR

    IN

    comm

    - коммуникатор;

    OUT

    size

    - число процессов в области связи коммуникатора comm.

    Функция возвращает количество процессов в области связи коммуникатора comm.

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

    Функция определения номера процесса MPI_Comm_rank.

    C:
    int MPI_Comm_rank(MPI_Comm comm, int *rank)
    FORTRAN:
    MPI_COMM_RANK(COMM, RANK, IERROR)
    INTEGER COMM, RANK, IERROR

    IN

    comm

    - коммуникатор;

    OUT

    rank

    - номер процесса, вызвавщего функцию.

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

    В минимальный набор следует включить также две функции передачи и приема сообщений.

    Функция передачи сообщения MPI_Send.

    C:
    int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest,
    int tag, MPI_Comm comm)
    FORTRAN:
    MPI_SEND(BUF, COUNT, DATATYPE, DEST, TAG, COMM, IERROR)
    <type> BUF(*)
    INTEGER COUNT, DATATYPE, DEST, TAG, COMM, IERROR

    IN buf

    -

    адрес начала расположения пересылаемых данных;

    IN count

    -

    число пересылаемых элементов;

    IN datatype

    -

    тип посылаемых элементов;

    IN dest

    -

    номер процесса-получателя в группе, связанной с коммуникатором comm;

    IN tag

    -

    идентификатор сообщения (аналог типа сообщения функций nread и nwrite PSE nCUBE2);

    IN comm

    -

    коммуникатор области связи.

    Функция выполняет посылку count элементов типа datatype сообщения с идентификатором tag процессу dest в области связи коммуникатора comm. Переменная buf - это, как правило, массив или скалярная переменная. В последнем случае значение count = 1.

    Функция приема сообщения MPI_Recv.

    C:
    int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source,
    int tag, MPI_Comm comm, MPI_Status *status)
    FORTRAN:
    MPI_RECV(BUF, COUNT, DATATYPE, SOURCE, TAG, COMM,
    STATUS, IERROR)
    <type> BUF(*)
    INTEGER COUNT, DATATYPE, SOURCE, TAG, COMM,
    STATUS(MPI_STATUS_SIZE), IERROR

    OUT

    buf

    -

    адрес начала расположения принимаемого сообщения;

    IN

    count

    -

    максимальное число принимаемых элементов;

    IN

    datatype

    -

    тип элементов принимаемого сообщения;

    IN

    source

    -

    номер процесса-отправителя;

    IN

    tag

    -

    идентификатор сообщения;

    IN

    comm

    -

    коммуникатор области связи;

    OUT

    status

    -

    атрибуты принятого сообщения.

     

     

     

     

    Функция выполняет прием count элементов типа datatype сообщения с идентификатором tag от процесса source в области связи коммуникатора comm.

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

    Функция отсчета времени (таймер) MPI_Wtime.

    C:
    double MPI_Wtime(void)
    FORTRAN:
    DOUBLE PRECISION MPI_WTIME()

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

    {
      double starttime, endtime;
      starttime = MPI_Wtime();
      ... хронометрируемый участок ...
      endtime = MPI_Wtime();
     printf("Выполнение заняло %f секунд\n", endtime-starttime);
    }

    Функция MPI_Wtick, имеющая точно такой же синтаксис, возвращает разрешение таймера (минимальное значение кванта времени).

    3. ОБЗОР КОММУНИКАЦИОННЫХ ОПЕРАЦИЙ ТИПА ТОЧКА-ТОЧКА

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

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

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

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

    Таблица 3. Список коммуникационных функций типа точка-точка

    Способ связи

    С блокировкой

    Без блокировки

    Стандартная посылка

    MPI_Send

    MPI_Isend

    Синхронная посылка

    MPI_Ssend

    MPI_Issend

    Буферизованная посылка

    MPI_Bsend

    MPI_Ibsend

    Согласованная посылка

    MPI_Rsend

    MPI_Irsend

    Прием информации

    MPI_Recv

    MPI_Irecv

    Из таблицы хорошо виден принцип формирования имен функций. К именам базовых функций Send/Recv добавляются различные префиксы.

    Префикс S

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

    Префикс B

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

    Префикс R

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

    Префикс I

    (immediate) - относится к неблокирующим операциям.

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

    3.1. Блокирующие коммуникационные операции

    Синтаксис базовых коммуникационных функций MPI_Send и MPI_Recv был рассмотрен в разделе 2, поэтому здесь мы рассмотрим только семантику этих операций.

    В стандартном режиме выполнение операции обмена включает три этапа:

  7. Передающая сторона формирует пакет сообщения, в который помимо передаваемой информации упаковываются адрес отправителя (source), адрес получателя (dest), идентификатор сообщения (tag) и коммуникатор (comm). Этот пакет передается отправителем в буфер, и на этом функция посылки сообщения заканчивается.
  8. Сообщение системными средствами передается адресату.
  9. Принимающий процессор извлекает сообщение из системного буфера, когда у него появится потребность в этих данных. Содержательная часть сообщения помещается в адресное пространство принимающего процесса (параметр buf), а служебная в параметр status.

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

Параметр count (количество принимаемых элементов сообщения) в процедуре приема сообщения должен быть не меньше, чем длина принимаемого сообщения. При этом реально будет приниматься столько элементов, сколько находится в буфере. Такая реализация операции чтения связана с тем, что MPI допускает использование расширенных запросов для идентификаторов сообщений (MPI_ANY_TAG - читать сообщение с любым идентификатором) и для адресов отправителя (MPI_ANY_SOURCE - читать сообщение от любого отправителя). Не допускается расширенных запросов для коммуникаторов. Расширенные запросы возможны только в операциях чтения. Интересно отметить, что таким же образом организованы операции обмена в PSE nCUBE2 [1]. В этом отражается фундаментальное свойство механизма передачи сообщений - асимметрия операций передачи и приема сообщений, связанная с тем, что инициатива в организации обмена принадлежит передающей стороне.

Таким образом, после чтения сообщения некоторые параметры могут оказаться неизвестными, а именно: число считанных элементов, идентификатор сообщения и адрес отправителя. Эту информацию можно получить с помощью параметра status. Переменные status должны быть явно объявлены в MPI программе. В языке C status - это структура типа MPI_Status с тремя полями MPI_SOURCE, MPI_TAG, MPI_ERROR. В языке FORTRAN status - массив типа INTEGER размера MPI_STATUS_SIZE. Константы MPI_SOURCE, MPI_TAG и MPI_ERROR определяют индексы элементов. Назначение полей переменной status представлено в таблице 4.

Таблица 4. Назначение полей переменной status

Поля status

C

FORTRAN

Процесс-отправитель

status.MPI_SOURCE

status(MPI_SOURCE)

Идентификатора сообщения

status.MPI_TAG

status(MPI_TAG)

Код ошибки

status.MPI_ERROR

status(MPI_ERROR)

 

Как видно из таблицы 4, количество считанных элементов в переменную status не заносится. Для определения числа фактически полученных элементов сообщения необходимо использовать специальную функцию MPI_Get_count:

C:
int MPI_Get_count (MPI_Status *status, MPI_Datatype datatype, int *count)
FORTRAN:
MPI_GET_COUNT (STATUS, DATATYPE, COUNT, IERROR)
INTEGER STATUS (MPI_STATUS_SIZE), DATATYPE, COUNT, IERROR

IN

status

- атрибуты принятого сообщения;

IN

datatype

- тип элементов принятого сообщения;

OUT

count

- число полученных элементов.

Подпрограмма MPI_Get_count может быть вызвана либо после чтения сообщения (функциями MPI_Recv, MPI_Irecv), либо после опроса факта поступления сообщения (функциями MPI_Probe, MPI_Iprobe). Операция чтения безвозвратно уничтожает информацию в буфере приема. При этом попытка считать сообщение с параметром count меньше, чем число элементов в буфере, приводит к потере сообщения. Определить параметры полученного сообщения без его чтения можно с помощью функции MPI_Probe.

C:
int MPI_Probe (int source, int tag, MPI_Comm comm, MPI_Status *status)
FORTRAN:
MPI_PROBE (SOURCE, TAG, COMM, STATUS, IERROR)
INTEGER SOURCE, TAG, COMM, STATUS(MPI_STATUS_SIZE), IERROR

IN

source

- номер процесса-отправителя;

IN

tag

- идентификатор сообщения;

IN

comm

- коммуникатор;

OUT

status

- атрибуты опрошенного сообщения.

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

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

CALL MPI_COMM_RANK(comm, rank, ierr)
IF (rank.EQ.0) THEN
   CALL MPI_RECV(recvbuf, count, MPI_REAL, 1, tag, comm, status, ierr)
   CALL MPI_SEND(sendbuf, count, MPI_REAL, 1, tag, comm, ierr)
ELSE IF (rank.EQ.1) THEN
   CALL MPI_RECV(recvbuf, count, MPI_REAL, 0, tag, comm, status, ierr)
   CALL MPI_SEND(sendbuf, count, MPI_REAL, 0, tag, comm, ierr)
END IF

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

Приведем вариант правильной программы.

CALL MPI_COMM_RANK(comm, rank, ierr)
IF (rank.EQ.0) THEN
   CALL MPI_SEND(sendbuf, count, MPI_REAL, 1, tag, comm, ierr)
   CALL MPI_RECV(recvbuf, count, MPI_REAL, 1, tag, comm, status, ierr)
ELSE IF (rank.EQ.1) THEN
   CALL MPI_RECV(recvbuf, count, MPI_REAL, 0, tag, comm, status, ierr)
   CALL MPI_SEND(sendbuf, count, MPI_REAL, 0, tag, comm, ierr)
END IF

Другие комбинации операций SEND/RECV могут работать или не работать в зависимости от реализации MPI (буферизованный обмен или нет).

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

С:
int MPI_Sendrecv(void *sendbuf, int sendcount, MPI_Datatype sendtype,
int dest, int sendtag, void *recvbuf, int recvcount,
MPI_Datatype recvtype, int source, MPI_Datatypeа recvtag,
MPI_Comm comm, MPI_Status *status)
FORTRAN:
MPI_SENDRECV(SENDBUF, SENDCOUNT, SENDTYPE, DEST, SENDTAG, RECVBUF,
RECVCOUNT, RECVTYPE, SOURCE, RECVTAG, COMM, STATUS, IERROR)
<type> SENDBUF(*), RECVBUF(*)
INTEGER SENDCOUNT, SENDTYPE, DEST, SENDTAG, RECVCOUNT, RECVTYPE,
SOURCE, RECV TAG, COMM, STATUS(MPI_STATUS_SIZE), IERROR

IN

sendbuf

-

адрес начала расположения посылаемого сообщения;

IN

sendcount

-

число посылаемых элементов;

IN

sendtype

-

тип посылаемых элементов;

IN

dest

-

номер процесса-получателя;

IN

sendtag

-

идентификатор посылаемого сообщения;

OUT

recvbuf

-

адрес начала расположения принимаемого сообщения;

IN

recvcount

-

максимальное число принимаемых элементов;

IN

recvtype

-

тип элементов принимаемого сообщения;

IN

source

-

номер процесса-отправителя;

IN

recvtag

-

идентификатор принимаемого сообщения;

IN

comm

-

коммуникатор области связи;

OUT

status

-

атрибуты принятого сообщения.

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

С:
MPI_Sendrecv_replace(void* buf, int count, MPI_Datatype datatype,
int dest, int sendtag, int source, int recvtag,
MPI_Comm comm, MPI_Status *status)
FORTRAN:
MPI_SENDRECV_REPLACE(BUF, COUNT, DATATYPE, DEST,
SENDTAG, SOURCE, RECVTAG, COMM, STATUS, IERROR)
<type> BUF(*)
INTEGER COUNT, DATATYPE, DEST, SENDTAG, SOURCE, RECVTAG, COMM,
STATUS(MPI_STATUS_SIZE), IERROR

INOUT

buf

-

адрес начала расположения посылаемого и принимаемого сообщения;

IN

count

-

число передаваемых элементов;

IN

datatype

-

тип передаваемых элементов;

IN

dest

-

номер процесса-получателя;

IN

sendtag

-

идентификатор посылаемого сообщения;

IN

source

-

номер процесса-отправителя;

IN

recvtag

-

идентификатор принимаемого сообщения;

IN

comm

-

коммуникатор области связи;

OUT

status

-

атрибуты принятого сообщения.

В данной операции посылаемые данные из массива buf замещаются принимаемыми данными.

В качестве адресатов source и dest в операциях пересылки данных можно использовать специальный адрес MPI_PROC_NULL. Коммуникационные операции с таким адресом ничего не делают. Применение этого адреса бывает удобным вместо использования логических конструкций для анализа условий посылать/читать сообщение или нет. Этот прием будет использован нами далее в одном из примеров, а именно, в программе решения уравнения Лапласа методом Якоби.

3.2. Неблокирующие коммуникационные операции

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

Неблокирующие операции используют специальный скрытый (opaque) объект "запрос обмена" (request) для связи между функциями обмена и функциями опроса их завершения. Для прикладных программ доступ к этому объекту возможен только через вызовы MPI функций. Если операция обмена завершена, подпрограмма проверки снимает "запрос обмена", устанавливая его в значение MPI_REQUEST_NULL. Снять запрос без ожидания завершения операции можно подпрограммой MPI_Request_free.

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

C:
int MPI_Isend(void* buf, int count, MPI_Datatype datatype, int dest,
int tag, MPI_Comm comm, MPI_Request *request)
FORTRAN:
MPI_ISEND(BUF, COUNT, DATATYPE, DEST, TAG, COMM, REQUEST, IERROR)
<type> BUF(*)
INTEGER COUNT, DATATYPE, DEST, TAG, COMM, REQUEST, IERROR

IN

buf

- адрес начала расположения передаваемых данных;

IN

count

- число посылаемых элементов;

IN

datatype

- тип посылаемых элементов;

IN

dest

- номер процесса-получателя;

IN

tag

- идентификатор сообщения;

IN

comm

- коммуникатор;

OUT

request

- "запрос обмена".

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

Функция приема сообщения без блокировки MPI_Irecv.

C:
int MPI_Irecv(void* buf, int count, MPI_Datatype datatype, int source,
int tag, MPI_Comm comm, MPI_Request *request)
FORTRAN:
MPI_IRECV(BUF, COUNT, DATATYPE, SOURCE, TAG, COMM, REQUEST, IERROR)
<type> BUF(*)
INTEGER COUNT, DATATYPE, SOURCE, TAG, COMM, REQUEST, IERROR

OUT

buf

- адрес для принимаемых данных;

IN

count

- максимальное число принимаемых элементов;

IN

datatype

- тип элементов принимаемого сообщения;

IN

source

- номер процесса-отправителя;

IN

tag

- идентификатор сообщения;

IN

comm

- коммуникатор;

OUT

request

- "запрос обмена".

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

Как и в блокирующих операциях часто возникает необходимость опроса параметров полученного сообщения без его фактического чтения. Это делается с помощью функции MPI_Iprobe.

Неблокирующая функция чтения параметров полученного сообщения MPI_Iprobe.

C:
int MPI_Iprobe (int source, int tag, MPI_Comm comm, int *flag,
MPI_Status *status)
FORTRAN:
MPI_IPROBE (SOURCE, TAG, COMM, FLAG, STATUS, IERROR)
LOGICAL FLAG
INTEGER SOURCE, TAG, COMM, STATUS(MPI_STATUS_SIZE), IERROR

IN

source

- номер процесса-отправителя;

IN

tag

- идентификатор сообщения;

IN

comm

- коммуникатор;

OUT

flag

- признак завершенности операции;

OUT

status

- атрибуты опрошенного сообщения.

Если flag=true, то операция завершилась, и в переменной status находятся атрибуты этого сообщения.

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

  1. Операции ожидания завершения семейства WAIT блокируют работу процесса до полного завершения операции.
  2. Операции проверки завершения семейства TEST возвращают значения TRUE или FALSE в зависимости от того, завершилась операция или нет. Они не блокируют работу процесса и полезны для предварительного определения факта завершения операции.

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

C:
int MPI_Wait(MPI_Request *request, MPI_Status *status)
FORTRAN:
MPI_WAIT(REQUEST, STATUS, IERROR)
INTEGER REQUEST, STATUS(MPI_STATUS_SIZE), IERROR

INOUT

request

- запрос связи;

OUT

status

- атрибуты сообщения.

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

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

C:
int MPI_Test(MPI_Request *request, int *flag, MPI_Status *status)
FORTRAN:
MPI_TEST(REQUEST, FLAG, STATUS, IERROR)
LOGICAL FLAG
INTEGER REQUEST, STATUS(MPI_STATUS_SIZE), IERROR

INOUT

request

-

запрос связи;

OUT

flag

-

признак завершенности проверяемой операции;

OUT

status

-

атрибуты сообщения, если операция завершилась.

Это локальная неблокирующая операция. Если связанная с запросом request операция завершена, возвращается flag = true, а status содержит информацию о завершенной операции. Если проверяемая операция не завершена, возвращается flag = false, а значение status в этом случае не определено.

Рассмотрим пример использования неблокирующих операций и функции MPI_Wait.

CALL MPI_COMM_RANK(comm, rank, ierr)
IF(rank.EQ.0) THEN
   CALL MPI_ISEND(a(1), 10, MPI_REAL, 1, tag, comm, request, ierr)
   **** Выполнение вычислений во время передачи сообщения ****
   CALL MPI_WAIT(request, status, ierr)
ELSE
   CALL MPI_IRECV(a(1), 15, MPI_REAL, 0, tag, comm, request, ierr)
   **** Выполнение вычислений во время приема сообщения ****
   CALL MPI_WAIT(request, status, ierr)
END IF

Функция снятия запроса без ожидания завершения неблокирующей операции MPI_Request_free.

C:
int MPI_Request_free(MPI_Request *request)
FORTRAN:
MPI_REQUEST_FREE(REQUEST, IERROR)
INTEGER REQUEST, IERROR

INOUT request - запрос связи.

Параметр request устанавливается в значение MPI_REQUEST_NULL. Связанная с этим запросом операция не прерывается, однако проверить ее завершение с помощью MPI_Wait или MPI_Test уже нельзя. Для прерывания коммуникационной операции следует использовать функцию MPI_Cancel(MPI_Request *request).

В MPI имеется набор подпрограмм для одновременной проверки на завершение нескольких операций. Без подробного обсуждения приведем их перечень (таблица 5).

Таблица 5. Функции коллективного завершения неблокирующих операций

Выполняемая проверка

Функции ожидания (блокирующие)

Функции проверки (неблокирующие)

Завершились все операции

MPI_Waitall

MPI_Testall

Завершилась по крайней мере одна операция

MPI_Waitany

MPI_Testany

Завершилась одна из списка проверяемых

MPI_Waitsome

MPI_Testsome

Кроме того, MPI позволяет для неблокирующих операций формировать целые пакеты запросов на коммуникационные операции MPI_Send_init и MPI_Recv_init, которые запускаются функциями MPI_Start или MPI_Startall. Проверка на завершение выполнения производится обычными средствами с помощью функций семейства WAIT и TEST.

4. Обзор коллективных операций

Набор операций типа точка-точка является достаточным для программирования любых алгоритмов, однако MPI вряд ли бы завоевал такую популярность, если бы ограничивался только этим набором коммуникационных операций. Одной из наиболее привлекательных сторон MPI является наличие широкого набора коллективных операций, которые берут на себя выполнение наиболее часто встречающихся при программировании действий. Например, часто возникает потребность разослать некоторую переменную или массив из одного процессора всем остальным. Каждый программист может написать такую процедуру с использованием операций Send/Recv, однако гораздо удобнее воспользоваться коллективной операцией MPI_Bcast. Причем гарантировано, что эта операция будет выполняться гораздо эффективнее, поскольку MPI-функция реализована с использованием внутренних возможностей коммуникационной среды. Главное отличие коллективных операций от операций типа точка-точка состоит в том, что в них всегда участвуют все процессы, связанные с некоторым коммуникатором. Несоблюдение этого правила приводит либо к аварийному завершению задачи, либо к еще более неприятному зависанию задачи.

Набор коллективных операций включает:

Все коммуникационные подпрограммы, за исключением MPI_Bcast, представлены в двух вариантах:

Отличительные особенности коллективных операций:

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

Изучение коллективных операций начнем с рассмотрения двух функций, стоящих особняком: MPI_Barrier и MPI_Bcast.

Функция синхронизации процессов MPI_Barrier блокирует работу вызвавшего ее процесса до тех пор, пока все другие процессы группы также не вызовут эту функцию. Завершение работы этой функции возможно только всеми процессами одновременно (все процессы "преодолевают барьер" одновременно).

C:
int MPI_Barrier(MPI_Comm comm )
FORTRAN:
MPI_BARRIER(COMM, IERROR)
INTEGER COMM, IERROR

IN comm - коммуникатор.

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

Широковещательная рассылка данных выполняется с помощью функции MPI_Bcast. Процесс с номером root рассылает сообщение из своего буфера передачи всем процессам области связи коммуникатора comm.

С:
int MPI_Bcast(void* buffer, int count, MPI_Datatype datatype, int root,
MPI_Comm comm )
FORTRAN:
MPI_BCAST(BUFFER, COUNT, DATATYPE, ROOT, COMM, IERROR)
<type> BUFFER(*)
INTEGER COUNT, DATATYPE, ROOT, COMM, IERROR

INOUT

buffer

- адрес начала расположения в памяти рассылаемых данных;

IN

count

- число посылаемых элементов;

IN

datatype

- тип посылаемых элементов;

IN

root

- номер процесса-отправителя;

IN

comm

- коммуникатор.

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

4.1. Функции сбора блоков данных от всех процессов группы

Семейство функций сбора блоков данных от всех процессов группы состоит из четырех подпрограмм: MPI_Gather, MPI_Allgather, MPI_Gatherv, MPI_Allgatherv. Каждая из указанных подпрограмм расширяет функциональные возможности предыдущих.

Функция MPI_Gather производит сборку блоков данных, посылаемых всеми процессами группы, в один массив процесса с номером root. Длина блоков предполагается одинаковой. Объединение происходит в порядке увеличения номеров процессов-отправителей. То есть данные, посланные процессом i из своего буфера sendbuf, помещаются в i-ю порцию буфера recvbuf процесса root. Длина массива, в который собираются данные, должна быть достаточной для их размещения.

С:
int MPI_Gather(void* sendbuf, int sendcount, MPI_Datatype sendtype,
void* recvbuf, int recvcount, MPI_Datatype recvtype,
int root, MPI_Comm comm)
FORTRAN:
MPI_GATHER(SENDBUF, SENDCOUNT, SENDTYPE, RECVBUF,
RECVCOUNT, RECVTYPE, ROOT, COMM, IERROR)
<type> SENDBUF(*), RECVBUF(*)
INTEGER SENDCOUNT, SENDTYPE, RECVCOUNT, RECVTYPE, ROOT, COMM, IERROR

IN

sendbuf

-

адрес начала размещения посылаемых данных;

IN

sendcount

-

число посылаемых элементов;

IN

sendtype

-

тип посылаемых элементов;

OUT

recvbuf

-

адрес начала буфера приема (используется только в процессе-получателе root);

IN

recvcount

-

число элементов, получаемых от каждого процесса (используется только в процессе-получателе root);

IN

recvtype

-

тип получаемых элементов;

IN

root

-

номер процесса-получателя;

IN

comm

-

коммуникатор.

Тип посылаемых элементов sendtype должен совпадать с типом recvtype получаемых элементов, а число sendcount должно равняться числу recvcount. То есть, recvcount в вызове из процесса root - это число собираемых от каждого процесса элементов, а не общее количество собранных элементов.

Пример программы с использованием функции MPI_Gather.

MPI_Comm comm;
int array[100];
int root, *rbuf;
. . .
MPI_Comm_size(comm, &gsize);
rbuf = (int *) malloc( gsize * 100 * sizeof(int));
MPI_Gather(array, 100, MPI_INT, rbuf, 100, MPI_INT, root, comm);

Функция MPI_Allgather выполняется так же, как MPI_Gather, но получателями являются все процессы группы. Данные, посланные процессом i из своего буфера sendbuf, помещаются в i-ю порцию буфера recvbuf каждого процесса. После завершения операции содержимое буферов приема recvbuf у всех процессов одинаково.

C:
int MPI_Allgather(void* sendbuf, int sendcount, MPI_Datatype sendtype,
void* recvbuf, int recvcount, MPI_Datatype recvtype, MPI_Comm comm)
FORTRAN:
MPI_ALLGATHER(SENDBUF, SENDCOUNT, SENDTYPE, RECVBUF,
RECVCOUNT, RECVTYPE, COMM, IERROR)
<type> SENDBUF(*), RECVBUF(*)
INTEGER SENDCOUNT, SENDTYPE, RECVCOUNT, RECVTYPE, COMM, IERROR

IN

sendbuf

-

адрес начала буфера посылки;

IN

sendcount

-

число посылаемых элементов;

IN

sendtype

-

тип посылаемых элементов;

OUT

recvbuf

-

адрес начала буфера приема;

IN

recvcount

-

число элементов, получаемых от каждого процесса;

IN

recvtype

-

тип получаемых элементов;

IN

comm

-

коммуникатор.

 

Функция MPI_Gatherv позволяет собирать блоки с разным числом элементов от каждого процесса, так как количество элементов, принимаемых от каждого процесса, задается индивидуально с помощью массива recvcounts. Эта функция обеспечивает также большую гибкость при размещении данных в процессе-получателе, благодаря введению в качестве параметра массива смещений displs.

C:
int MPI_Gatherv(void* sendbuf, int sendcount, MPI_Datatype sendtype,
void* rbuf, int *recvcounts, int *displs, MPI_Datatype recvtype,
int root, MPI_Comm comm)
FORTRAN:
MPI_GATHERV(SENDBUF, SENDCOUNT, SENDTYPE, RBUF,
RECVCOUNTS, DISPLS, RECVTYPE, ROOT, COMM, IERROR)
<type> SENDBUF(*), RBUF(*)
INTEGER SENDCOUNT, SENDTYPE, RECVCOUNTS(*), DISPLS(*),
RECVTYPE, ROOT, COMM, IERROR

IN

sendbuf

-

адрес начала буфера передачи;

IN

sendcount

-

число посылаемых элементов;

IN

sendtype

-

тип посылаемых элементов;

OUT

rbuf

-

адрес начала буфера приема;

IN

recvcounts

-

целочисленный массив (размер равен числу процессов в группе), i-й элемент которого определяет число элементов, которое должно быть получено от процесса i;

IN

displs

-

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

IN

recvtype

-

тип получаемых элементов;

IN

root

-

номер процесса-получателя;

IN

comm

-

коммуникатор.

Сообщения помещаются в буфер приема процесса root в соответствии с номерами посылающих процессов, а именно, данные, посланные процессом i, размещаются в

 

адресном пространстве процесса root, начиная с адреса rbuf + displs[i].

Функция MPI_Allgatherv является аналогом функции MPI_Gatherv, но сборка выполняется всеми процессами группы. Поэтому в списке параметров отсутствует параметр root.

C:
int MPI_Allgatherv(void* sendbuf, int sendcount, MPI_Datatype sendtype,
void* rbuf, int *recvcounts, int *displs,
MPI_Datatype recvtype, MPI_Comm comm)
FORTRAN:
MPI_ALLGATHERV(SENDBUF, SENDCOUNT, SENDTYPE, RBUF, RECVCOUNTS,
DISPLS, RECVTYPE, COMM, IERROR)
<type> SENDBUF(*), RBUF(*)
INTEGER SENDCOUNT, SENDTYPE, RECVCOUNTS(*), DISPLS(*),
RECVTYPE, COMM, IERROR

IN

sendbuf

-

адрес начала буфера передачи;

IN

sendcount

-

число посылаемых элементов;

IN

sendtype

-

тип посылаемых элементов;

OUT

rbuf

-

адрес начала буфера приема;

IN

recvcounts

-

целочисленный массив (размер равен числу процессов в группе), содержащий число элементов, которое должно быть получено от каждого процесса;

IN

displs

-

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

IN

recvtype

-

тип получаемых элементов;

IN

comm

-

коммуникатор.

 

 

Рис 1. Графическая интерпретация операции Bcast.

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

   ...
   IF ( MYID .EQ. 0 ) THEN
   PRINT *, 'ВВЕДИТЕ ПАРАМЕТР N : '
   READ *, N
   END IF
   CALL MPI_BCAST(N, 1, MPI_INTEGER, 0, MPI_COMM_WORLD, IERR)

 

4.2. Функции распределения блоков данных по всем процессам группы

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

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

C:
int MPI_Scatter(void* sendbuf, int sendcount, MPI_Datatype sendtype,
void* recvbuf, int recvcount, MPI_Datatype recvtype,
int root, MPI_Comm comm)
FORTRAN:
MPI_SCATTER(SENDBUF, SENDCOUNT, SENDTYPE, RECVBUF,
RECVCOUNT, RECVTYPE, ROOT, COMM, IERROR)
<type> SENDBUF(*), RECVBUF(*)
INTEGER SENDCOUNT, SENDTYPE, RECVCOUNT, RECVTYPE, ROOT,
COMM, IERROR

IN

sendbuf

-

адрес начала размещения блоков распределяемых данных (используется только в процессе-отправителе root);

IN

sendcount

-

число элементов, посылаемых каждому процессу;

IN

sendtype

-

тип посылаемых элементов;

OUT

recvbuf

-

адрес начала буфера приема;

IN

recvcount

-

число получаемых элементов;

IN

recvtype

-

тип получаемых элементов;

IN

root

-

номер процесса-отправителя;

IN

comm

-

коммуникатор.

Тип посылаемых элементов sendtype должен совпадать с типом recvtype получаемых элементов, а число посылаемых элементов sendcount должно равняться числу принимаемых recvcount. Следует обратить внимание, что значение sendcount в вызове из процесса root - это число посылаемых каждому процессу элементов, а не общее их количество. Операция Scatter является обратной по отношению к Gather. На Рис.5 представлена графическая интерпретация операции Scatter.

4.3. Функции распределения блоков данных по всем процессам группы

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

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

C:
int MPI_Scatter(void* sendbuf, int sendcount, MPI_Datatype sendtype,
void* recvbuf, int recvcount, MPI_Datatype recvtype,
int root, MPI_Comm comm)
FORTRAN:
MPI_SCATTER(SENDBUF, SENDCOUNT, SENDTYPE, RECVBUF,
RECVCOUNT, RECVTYPE, ROOT, COMM, IERROR)
<type> SENDBUF(*), RECVBUF(*)
INTEGER SENDCOUNT, SENDTYPE, RECVCOUNT, RECVTYPE, ROOT,
COMM, IERROR

IN

sendbuf

-

адрес начала размещения блоков распределяемых данных (используется только в процессе-отправителе root);

IN

sendcount

-

число элементов, посылаемых каждому процессу;

IN

sendtype

-

тип посылаемых элементов;

OUT

recvbuf

-

адрес начала буфера приема;

IN

recvcount

-

число получаемых элементов;

IN

recvtype

-

тип получаемых элементов;

IN

root

-

номер процесса-отправителя;

IN

comm

-

коммуникатор.

Тип посылаемых элементов sendtype должен совпадать с типом recvtype получаемых элементов, а число посылаемых элементов sendcount должно равняться числу принимаемых recvcount. Следует обратить внимание, что значение sendcount в вызове из процесса root - это число посылаемых каждому процессу элементов, а не общее их количество. представлена графическая интерпретация операции Scatter.

 

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

MPI_Comm comm;
int   rbuf[100], gsize;
int   root, *array;
. . . . . .
MPI_Comm_size(comm, &gsize);
array = (int *) malloc(gsize * 100 * sizeof(int));
. . . . . .
MPI_Scatter(array, 100, MPI_INT, rbuf, 100, MPI_INT, root, comm);

Функция MPI_Scaterv является векторным вариантом функции MPI_Scatter, позволяющим посылать каждому процессу различное количество элементов. Начало расположения элементов блока, посылаемого i-му процессу, задается в массиве смещений displs, а число посылаемых элементов в массиве sendcounts. Эта функция является обратной по отношению к функции MPI_Gatherv.

C:
int MPI_Scatterv(void* sendbuf, int *sendcounts, int *displs,
MPI_Datatype sendtype, void* recvbuf, int recvcount,
MPI_Datatype recvtype, int root, MPI_Comm comm)
FORTRAN:
MPI_SCATTERV(SENDBUF, SENDCOUNTS, DISPLS, SENDTYPE,
RECVBUF, RECVCOUNT, RECVTYPE, ROOT, COMM, IERROR)
<type> SENDBUF(*), RECVBUF(*)
INTEGER SENDCOUNTS(*), DISPLS(*), SENDTYPE, RECVCOUNT,
RECVTYPE, ROOT, COMM, IERROR

IN

sendbuf

-

адрес начала буфера посылки (используется только в процессе-отправителе root);

IN

sendcounts

-

целочисленный массив (размер равен числу процессов в группе), содержащий число элементов, посылаемых каждому процессу;

IN

displs

-

целочисленный массив (размер равен числу процессов в группе), i-ое значение определяет смещение относительно начала sendbuf для данных, посылаемых процессу i;

IN

sendtype

-

тип посылаемых элементов;

OUT

recvbuf

-

адрес начала буфера приема;

IN

recvcount

-

число получаемых элементов;

IN

recvtype

-

тип получаемых элементов;

IN

root

-

номер процесса-отправителя;

IN

comm

-

коммуникатор.

На Рис.6 представлена графическая интерпретация операции Scatterv.

4.4. Совмещенные коллективные операции

Функция MPI_Alltoall совмещает в себе операции Scatter и Gather и является по сути дела расширением операции Allgather, когда каждый процесс посылает различные данные разным получателям. Процесс i посылает j-ый блок своего буфера sendbuf процессу j, который помещает его в i-ый блок своего буфера recvbuf. Количество посланных данных должно быть равно количеству полученных данных для каждой пары процессов.

C:
int MPI_Alltoall(void* sendbuf, int sendcount, MPI_Datatype sendtype,
void* recvbuf, int recvcount, MPI_Datatype recvtype, MPI_Comm comm)
FORTRAN:
MPI_ALLTOALL(SENDBUF, SENDCOUNT, SENDTYPE, RECVBUF,
RECVCOUNT, RECVTYPE, COMM, IERROR)
<type> SENDBUF(*), RECVBUF(*)
INTEGER SENDCOUNT, SENDTYPE, RECVCOUNT, RECVTYPE,
COMM, IERROR

IN

sendbuf

-

адрес начала буфера посылки;

IN

sendcount

-

число посылаемых элементов;

IN

sendtype

-

тип посылаемых элементов;

OUT

recvbuf

-

адрес начала буфера приема;

IN

recvcount

-

число элементов, получаемых от каждого процесса;

IN

recvtype

-

тип получаемых элементов;

IN

comm

-

коммуникатор.

.

Функция MPI_Alltoallv реализует векторный вариант операции Alltoall, допускающий передачу и прием блоков различной длины с более гибким размещением передаваемых и принимаемых данных.

4.3. Глобальные вычислительные операции над распределенными данными

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

В MPI глобальные операции редукции представлены в нескольких вариантах:

Функция MPI_Reduce выполняется следующим образом. Операция глобальной редукции, указанная параметром op, выполняется над первыми элементами входного буфера, и результат посылается в первый элемент буфера приема процесса root. Затем то же самое делается для вторых элементов буфера и т.д.

С:
int MPI_Reduce(void* sendbuf, void* recvbuf, int count, MPI_Datatype datatype,
MPI_Op op, int root, MPI_Comm comm)
FORTRAN:
MPI_REDUCE(SENDBUF, RECVBUF, COUNT, DATATYPE, OP, ROOT,
COMM, IERROR)
<type> SENDBUF(*), RECVBUF(*)
INTEGER COUNT, DATATYPE, OP, ROOT, COMM, IERROR

IN

sendbuf

-

адрес начала входного буфера;

OUT

recvbuf

-

адрес начала буфера результатов (используется только в процессе-получателе root);

IN

count

-

число элементов во входном буфере;

IN

datatype

-

тип элементов во входном буфере;

IN

op

-

операция, по которой выполняется редукция;

IN

root

-

номер процесса-получателя результата операции;

IN

comm

-

коммуникатор.

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

Таблица 6. Предопределенные операции в функциях редукции MPI

Название

Операция

Разрешенные типы

MPI_MAX
MPI_MIN

Максимум
Минимум

C integer, FORTRAN integer,
Floating point</TD

 

 

 

MPI_SUM
MPI_PROD

Сумма
Произведение

C integer, FORTRAN integer,
Floating point, Complex</TD

 

 

 

MPI_LAND
MPI_LOR
MPI_LXOR

Логическое AND
Логическое OR
Логическое исключающее OR

C integer, Logical</TD

 

 

 

MPI_BAND
MPI_BOR
MPI_BXOR

Поразрядное AND
Поразрядное OR
Поразрядное исключающее OR

C integer, FORTRAN integer,
Byte</TD

 

 

 

MPI_MAXLOC

MPI_MINLOC

Максимальное значение
и его индекс
Минимальное значение
и его индекс

Специальные типы для этих функций</TD

 

 

 

В таблице используются следующие обозначения:

C integer:

MPI_INT, MPI_LONG, MPI_SHORT, MPI_UNSIGNED_SHORT, MPI_UNSIGNED, MPI_UNSIGNED_LONG

FORTRAN integer:

MPI_INTEGER

Floating point:

MPI_FLOAT, MPI_DOUBLE, MPI_REAL, MPI_DOUBLE_PRECISION, MPI_LONG_DOUBLE

Logical:

MPI_LOGICAL

Complex:

MPI_COMPLEX

Byte:

MPI_BYTE

Операции MAXLOC и MINLOC выполняются над специальными парными типами, каждый элемент которых хранит две величины: значение, по которым ищется максимум или минимум, и индекс элемента. В MPI имеется 9 таких предопределенных типов.

C:

 

MPI_FLOAT_INT

float

and

int

 

MPI_DOUBLE_INT

double

and

int

 

MPI_LONG_INT

long

and

int

 

MPI_2INT

int

and

int

 

MPI_SHORT_INT

short

and

int

 

MPI_LONG_DOUBLE_INT

long double

and

int

 

FORTRAN:

 

MPI_2REAL

REAL and REAL

 

MPI_2DOUBLE_PRECISION

DOUBLE PRECISION and DOUBLE PRECISION

 

MPI_2INTEGER

INTEGER and INTEGER

 

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

С:
int MPI_Allreduce(void* sendbuf, void* recvbuf, int count,
MPI_Datatype datatype, MPI_Op op, MPI_Comm comm)
FORTRAN:
MPI_ALLREDUCE (SENDBUF, RECVBUF, COUNT, DATATYPE, OP,
COMM, IERROR)
<type> SENDBUF(*), RECVBUF(*)
INTEGER COUNT, DATATYPE, OP, COMM, IERROR

IN

sendbuf

-

адрес начала входного буфера;

OUT

recvbuf

-

адрес начала буфера приема;

IN

count

-

число элементов во входном буфере;

IN

datatype

-

тип элементов во входном буфере;

IN

op

-

операция, по которой выполняется редукция;

IN

comm

-

коммуникатор.

Функция MPI_Reduce_scatter совмещает в себе операции редукции и распределения результата по процессам.

С:
MPI_Reduce_scatter(void* sendbuf, void* recvbuf, int *recvcounts,
MPI_Datatype datatype, MPI_Op op, MPI_Comm comm)
FORTRAN:
MPI_REDUCE_SCATTER(SENDBUF,RECVBUF,RECVCOUNTS,
DATATYPE, OP, COMM, IERROR)
<type> SENDBUF(*), RECVBUF(*)
INTEGER RECVCOUNTS(*), DATATYPE, OP, COMM, IERROR

IN

sendbuf

-

адрес начала входного буфера;

OUT

recvbuf

-

адрес начала буфера приема;

IN

revcount

-

массив, в котором задаются размеры блоков, посылаемых процессам;

IN

datatype

-

тип элементов во входном буфере;

IN

op

-

операция, по которой выполняется редукция;

IN

comm

-

коммуникатор.

Функция MPI_Reduce_scatter отличается от MPI_Allreduce тем, что результат операции разрезается на непересекающиеся части по числу процессов в группе, i-ая часть посылается i-ому процессу в его буфер приема. Длины этих частей задает третий параметр, являющийся массивом. На Рис. 10 представлена графическая интерпретация операции Reduce_scatter.

Функция MPI_Scan выполняет префиксную редукцию. Параметры такие же, как в MPI_Allreduce, но получаемые каждым процессом результаты отличаются друг от друга. Операция пересылает в буфер приема i-го процесса редукцию значений из входных буферов процессов с номерами 0, ... i включительно.

С:
int MPI_Scan(void* sendbuf, void* recvbuf, int count,
MPI_Datatype datatype, MPI_Op op, MPI_Comm comm)
FORTRAN:
MPI_SCAN(SENDBUF, RECVBUF, COUNT, DATATYPE, OP, COMM, IERROR)
<type>SENDBUF(*), RECVBUF(*)
INTEGER COUNT, DATATYPE, OP, COMM, IERROR

IN

sendbuf

-

адрес начала входного буфера

OUT

recvbuf

-

адрес начала буфера приема

IN

count

-

число элементов во входном буфере

IN

datatype

-

тип элементов во входном буфере

IN

op

-

операция, по которой выполняется редукция

IN

comm

-

коммуникатор

5. ПРОИЗВОДНЫЕ ТИПЫ ДАННЫХ И ПЕРЕДАЧА УПАКОВАННЫХ ДАННЫХ

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

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

 

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

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

Typemap = {(type0, disp0), ... , (typen-1, dispn-1)}

Значения смещений не обязательно должны быть неотрицательными, различными и упорядоченными по возрастанию. Отображение типа вместе с базовым адресом начала расположения данных buf определяет коммуникационный буфер обмена. Этот буфер будет содержать n элементов, а i-й элемент будет иметь адрес buf+disp и иметь базовый тип type. Стандартные типы MPI имеют предопределенные отображения типов. Например, MPI_INT имеет отображение {(int,0)}.

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

Стандартный сценарий определения и использования производных типов включает следующие шаги:

Любой тип данных в MPI имеет две характеристики: протяженность и размер, выраженные в байтах:

Для простых типов протяженность и размер совпадают.

Функция MPI_Type_extent определяет протяженность элемента некоторого типа.

C:
int MPI_Type_extent(MPI_Datatype datatype, MPI_Aint *extent)
FORTRAN:
MPI_TYPE_EXTENT(DATATYPE, EXTENT, IERROR)
INTEGER DATATYPE, EXTENT, IERROR

IN

datatype

- тип данных;

OUT

extent

- протяженность элемента заданного типа.

Функция MPI_Type_size определяет "чистый" размер элемента некоторого типа (за вычетом пустых промежутков).

C:
int MPI_Type_size(MPI_Datatype datatype, int *size)
FORTRAN:
MPI_TYPE_SIZE(DATATYPE, SIZE, IERROR)
INTEGER DATATYPE, SIZE, IERROR

IN

datatype

- тип данных;

OUT

size

- размер элемента заданного типа.

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

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

C:
int MPI_Type_contiguous(int count, MPI_Datatype oldtype,
MPI_Datatype *newtype)
FORTRAN:
MPI_TYPE_CONTIGUOUS(COUNT, OLDTYPE, NEWTYPE, IERROR)
INTEGER COUNT, OLDTYPE, NEWTYPE, IERROR

IN

count

- число элементов базового типа;

IN

oldtype

- базовый тип данных;

OUT

newtype

- новый производный тип данных.

 

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

C:
int MPI_Type_vector(int count, int blocklength, int stride,
MPI_Datatype oldtype, MPI_Datatype *newtype)
FORTRAN:
MPI_TYPE_VECTOR(COUNT, BLOCKLENGTH, STRIDE, OLDTYPE,
NEWTYPE, IERROR)
INTEGER COUNT, BLOCKLENGTH, STRIDE, OLDTYPE, NEWTYPE, IERROR

IN

count

-

число блоков;

IN

blocklength

-

число элементов базового типа в каждом блоке;

IN

stride

-

шаг между началами соседних блоков, измеренный числом элементов базового типа;

IN

oldtype

-

базовый тип данных;

OUT

newtype

-

новый производный тип данных.

Функция создает тип newtype, элемент которого состоит из count блоков, каждый из которых содержит одинаковое число blocklength элементов типа oldtype. Шаг stride между началом блока и началом следующего блока всюду одинаков и кратен протяженности представления базового типа.

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

C:
int MPI_Type_hvector(int count, int blocklength, MPI_Aint stride,
MPI_Datatype oldtype, MPI_Datatype *newtype)
FORTRAN:
MPI_TYPE_HVECTOR(COUNT, BLOCKLENGTH, STRIDE, OLDTYPE,
NEWTYPE, IERROR)
INTEGER COUNT, BLOCKLENGTH, STRIDE, OLDTYPE, NEWTYPE, IERROR

IN

count

-

число блоков;

IN

blocklength

-

число элементов базового типа в каждом блоке;

IN

stride

-

шаг между началами соседних блоков в байтах;

IN

oldtype

-

базовый тип данных;

OUT

newtype

-

новый производный тип данных.

Графическая интерпретация работы конструктора MPI_Type_hvector приведена на рис. 14.

Рис. 14. Графическая интерпретация работы конструктора MPI_Type_hvector.

Конструктор типа MPI_Type_indexed является более универсальным конструктором по сравнению с MPI_Type_vector, так как элементы создаваемого типа состоят из произвольных по длине блоков с произвольным смещением блоков от начала размещения элемента. Смещения измеряются в элементах старого типа.

C:
int MPI_Type_indexed(int count, int *array_of_blocklengths,
int *array_of_displacements, MPI_Datatype oldtype,
MPI_Datatype *newtype)
FORTRAN:
MPI_TYPE_INDEXED(COUNT, ARRAY_OF_BLOCKLENGTHS,
ARRAY_OF_DISPLACEMENTS, OLDTYPE, NEWTYPE, IERROR)
INTEGER COUNT, ARRAY_OF_BLOCKLENGTHS(*), ARRAY_OF_DISPLACEMENTS(*),
OLDTYPE, NEWTYPE, IERROR

IN

count

-

число блоков;

IN

array_of_blocklengths

-

массив, содержащий число элементов базового типа в каждом блоке;

IN

array_of_displacements

-

массив смещений каждого блока от начала размещения элемента нового типа, смещения измеряются числом элементов базового типа;

IN

oldtype

-

базовый тип данных;

OUT

newtype

-

новый производный тип данных.

Эта функция создает тип newtype, каждый элемент которого состоит из count блоков, где i-ый блок содержит array_of_blocklengths[i] элементов базового типа и смещен от начала размещения элемента нового типа на array_of_displacements[i] элементов базового типа.

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

C:
int MPI_Type_hindexed(int count, int *array_of_blocklengths,
MPI_Aint *array_of_displacements, MPI_Datatype oldtype,
MPI_Datatype *newtype)
FORTRAN:
MPI_TYPE_HINDEXED(COUNT, ARRAY_OF_BLOCKLENGTHS,
ARRAY_OF_DISPLACEMENTS, OLDTYPE, NEWTYPE, IERROR)
INTEGER COUNT, ARRAY_OF_BLOCKLENGTHS(*), ARRAY_OF_DISPLACEMENTS(*),
OLDTYPE, NEWTYPE, IERROR

IN

count

-

число блоков;

IN

array_of_blocklengths

-

массив, содержащий число элементов базового типа в каждом блоке;

IN

array_of_displacements

-

массив смещений каждого блока от начала размещения элемента нового типа, смещения измеряются в байтах;

IN

oldtype

-

базовый тип данных;

OUT

newtype

-

новый производный тип данных.

Элемент нового типа состоит из count блоков, где i-ый блок содержит array_of_blocklengths[i] элементов старого типа и смещен от начала размещения элемента нового типа на array_of_displacements[i] байт.

 

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

C:
int MPI_Type_struct(int count, int *array_of_blocklengths,
MPI_Aint *array_of_displacements, MPI_Datatype *array_of_types,
MPI_Datatype *newtype)
FORTRAN:
MPI_TYPE_STRUCT(COUNT, ARRAY_OF_BLOCKLENGTHS,
ARRAY_OF_DISPLACEMENTS, ARRAY_OF_TYPES, NEWTYPE, IERROR)
INTEGER COUNT, ARRAY_OF_BLOCKLENGTHS(*), ARRAY_OF_DISPLACEMENTS(*),
ARRAY_OF_TYPES(*), NEWTYPE, IERROR

IN

count

-

число блоков;

IN

array_of_blocklength

-

массив, содержащий число элементов одного из базовых типов в каждом блоке;

IN

array_of_displacements

-

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

IN

array_of_type

-

массив, содержащий тип элементов в каждом блоке;

OUT

newtype

-

новый производный тип данных.

Функция создает тип newtype, элемент которого состоит из count блоков, где i-ый блок содержит array_of_blocklengths[i] элементов типа array_of_types[i]. Смещение i-ого блока от начала размещения элемента нового типа измеряется в байтах и задается в array_of_displacements[i].

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

C:
int MPI_Type_commit(MPI_Datatype *datatype)
FORTRAN:
MPI_TYPE_COMMIT(DATATYPE, IERROR)
INTEGER DATATYPE, IERROR

INOUT datatype - новый производный тип данных.

Функция MPI_Type_free уничтожает описатель производного типа.

C:
int MPI_Type_free(MPI_Datatype *datatype)
FORTRAN:>
MPI_TYPE_FREE(DATATYPE, IERROR)
INTEGER DATATYPE, IERROR

INOUT datatype - уничтожаемый производный тип.

Функция MPI_Type_free устанавливает описатель типа в состояние MPI_DATATYPE_NULL. Это не повлияет на выполняющиеся в данный момент коммуникационные операции с этим типом данных и на производные типы, которые ранее были определены через уничтоженный тип.

Для определения длины сообщения используются две функции: MPI_Get_count и MPI_Get_elements. Для сообщений из простых типов они возвращают одинаковое число. Подпрограмма MPI_Get_count возвращает число элементов типа datatype, указанного в операции получения. Если получено не целое число элементов, то она возвратит константу MPI_UNDEFINED (функция MPI_Get_count рассматривалась в разделе 3.2, посвященном коммуникационным операциям типа точка-точка).

Функция MPI_Get_elements возвращает число элементов простых типов, содержащихся в сообщении.

C:
int MPI_Get_elements(MPI_Status *status, MPI_Datatype datatype,
int *count)
FORTRAN:
MPI_GET_ELEMENTS(STATUS, DATATYPE, COUNT, IERROR)
INTEGER STATUS(MPI_STATUS_SIZE), DATATYPE, COUNT, IERROR

IN

status

-

статус сообщения;

IN

datatype

-

тип элементов сообщения;

 

 

 

 

OUT

count

-

число элементов простых типов, содержащихся в сообщении.

 

 

 

 

5.2 Передача упакованных данных

Функция MPI_Pack упаковывает элементы предопределенного или производного типа MPI, помещая их побайтное представление в выходной буфер.

C:
int MPI_Pack(void* inbuf, int incount, MPI_Datatype datatype,
void *outbuf, int outsize, int *position, MPI_Comm comm)
FORTRAN:
MPI_PACK(INBUF, INCOUNT, DATATYPE, OUTBUF, OUTSIZE,
POSITION, COMM, IERROR)
<type> INBUF(*), OUTBUF(*)
INTEGER INCOUNT, DATATYPE, OUTSIZE, POSITION, COMM, IERROR

IN

inbuf

-

адрес начала области памяти с элементами, которые требуется упаковать;

IN

incount

-

число упаковываемых элементов;

IN

datatype

-

тип упаковываемых элементов;

OUT

outbuf

-

адрес начала выходного буфера для упакованных данных;

IN

outsize

-

размер выходного буфера в байтах;

INOUT

position

-

текущая позиция в выходном буфере в байтах;

IN

comm

-

коммуникатор.

Функция MPI_Pack упаковывает incount элементов типа datatype из области памяти с начальным адресом inbuf. Результат упаковки помещается в выходной буфер с начальным адресом outbuf и размером outsize байт. Параметр position указывает текущую позицию в байтах, начиная с которой будут размещаться упакованные данные. На выходе из подпрограммы значение position увеличивается на число упакованных байт, указывая на первый свободный байт. Параметр comm при последующей посылке упакованного сообщения будет использован как коммуникатор.

Функция MPI_Unpack извлекает заданное число элементов некоторого типа из побайтного представления элементов во входном массиве.

C:
Int MPI_Unpack(void* inbuf, int insize, int *position, void *outbuf,
int outcount, MPI_Datatype datatype, MPI_Comm comm)
FORTRAN:
MPI_UNPACK(INBUF, INSIZE, POSITION, OUTBUF, OUTCOUNT,
DATATYPE, COMM, IERROR)
<type> INBUF(*), OUTBUF(*)
INTEGER INSIZE, POSITION, OUTCOUNT, DATATYPE, COMM, IERROR

IN

inbuf

-

адрес начала входного буфера с упакованными данными;

IN

insize

-

размер входного буфера в байтах;

INOUT

position

-

текущая позиция во входном буфере в байтах;

OUT

outbuf

-

адрес начала области памяти для размещения распакованных элементов;

IN

outcount

-

число извлекаемых элементов;

IN

datatype

-

тип извлекаемых элементов;

IN

comm

-

коммуникатор.

Функция MPI_Unpack извлекает outcount элементов типа datatype из побайтного представления элементов в массиве inbuf, начиная с адреса position. После возврата из функции параметр position увеличивается на размер распакованного сообщения. Результат распаковки помещается в область памяти с начальным адресом outbuf.

Для посылки элементов разного типа из нескольких областей памяти их следует предварительно запаковать в один массив, последовательно обращаясь к функции упаковки MPI_Pack. При первом вызове функции упаковки параметр position, как правило, устанавливается в 0, чтобы упакованное представление размещалось с начала буфера. Для непрерывного заполнения буфера необходимо в каждом последующем вызове использовать значение параметра position, полученное из предыдущего вызова.

Упакованный буфер пересылается любыми коммуникационными операциями с указанием типа MPI_PACKED и коммуникатора comm, который использовался при обращениях к функции MPI_Pack.

Полученное упакованное сообщение распаковывается в различные массивы или переменные. Это реализуется последовательными вызовами функции распаковки MPI_Unpack с указанием числа элементов, которое следует извлечь при каждом вызове, и с передачей значения position, возвращенного предыдущим вызовом. При первом вызове функции параметр position следует установить в 0. В общем случае, при первом обращении должно быть установлено то значение параметра position, которое было использовано при первом обращении к функции упаковки данных. Очевидно, что для правильной распаковки данных очередность извлечения данных должна быть той же самой, как и при упаковке.

Функция MPI_Pack_size помогает определить размер буфера, необходимый для упаковки некоторого количества данных типа datatype.

C:
int MPI_Pack_size(int incount, MPI_Datatype datatype,
MPI_Comm comm, int *size)
FORTRAN:
MPI_PACK_SIZE(INCOUNT, DATATYPE, COMM, SIZE, IERROR)
INTEGER INCOUNT, DATATYPE, COMM, SIZE, IERROR

IN

incount

-

число элементов, подлежащих упаковке;

IN

datatype

-

тип элементов, подлежащих упаковке;

IN

comm

-

коммуникатор;

OUT

size

-

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

Первые три параметра функции MPI_Pack_size такие же, как у функции MPI_Pack. После обращения к функции параметр size будет содержать размер сообщения в байтах после его упаковки.

Рассмотрим пример рассылки разнотипных данных из 0-го процесса с использованием функций MPI_Pack и MPI_Unpack.

char buff[100];
double x, y;
int position, a[2];
{
MPI_Comm_rank(MPI_COMM_WORLD, &myrank);
if (myrank == 0)
{  /* Упаковка данных*/
position = 0;
MPI_Pack(&x, 1, MPI_DOUBLE, buff, 100, &position, MPI_COMM_WORLD);
MPI_Pack(&y, 1, MPI_DOUBLE, buff, 100, &position, MPI_COMM_WORLD);
MPI_Pack(a, 2, MPI_INT, buff, 100, &position, MPI_COMM_WORLD);
}
   /* Рассылка упакованного сообщения */
MPI_Bcast(buff, position, MPI_PACKED, 0, MPI_COMM_WORLD);
   /* Распаковка сообщения во всех процессах */
if (myrank != 0)
position = 0;
MPI_Unpack(buff, 100, &position, &x, 1, MPI_DOUBLE, MPI_COMM_WORLD);
MPI_Unpack(buff, 100, &position, &y, 1, MPI_DOUBLE, MPI_COMM_WORLD);
MPI_Unpack(buff, 100, &position, a,  2, MPI_INT, MPI_COMM_WORLD);
}

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

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

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

MPI_GROUP_EMPTY

-

группа, не содержащая ни одного процесса;

MPI_GROUP_NULL

-

значение возвращаемое, когда группа не может быть создана.

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

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

В MPI существует два типа коммуникаторов:

intracommunicator

-

описывает область связи некоторой группы процессов;

intercommunicator

-

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

Тип коммуникатора можно определить с помощью специальной функции MPI_Comm_test_inter.

С:
MPI_Comm_test_inter(MPI_Comm comm, int *flag)
FORTRAN:
MPI_COMM_TEST_INTER(COMM, FLAG, IERROR)
INTEGER COMM, IERROR
LOGICAL FLAG

IN

comm

-

коммуникатор;

OUT

flag

-

возвращает true, если comm - intercommunicator.

Функция возвращает значение "истина", если коммуникатор является интеркоммуникатором.

При инициализации MPI создается два предопределенных коммуникатора:

MPI_COMM_WORLD

- описывает область связи, содержащую все процессы;

MPI_COMM_SELF

- описывает область связи, состоящую из одного процесса.

6.1. Функции работы с группами

Функция определения числа процессов в группе MPI_Group_size.

С:
MPI_Group_size(MPI_Group group, int *size)
FORTRAN:
MPI_GROUP_SIZE(GROUP, SIZE, IERROR)
INTEGER GROUP, SIZE, IERROR

IN

group

- группа;

OUT

size

- число процессов в группе.

Функция возвращает число процессов в группе. Если group = MPI_GROUP_EMPTY, тогда size = 0.

Функция определения номера процесса в группе MPI_Group_rank.

С:
MPI_Group_rank(MPI_Group group, int *rank)
FORTRAN:
MPI_GROUP_RANK(GROUP, RANK, IERROR)
INTEGER GROUP, RANK, IERROR

IN

group

- группа;

OUT

rank

- номер процесса в группе.

Функция MPI_Group_rank возвращает номер процесса в группе, вызвавшего функцию. Если процесс не является членом группы, то возвращается значение MPI_UNDEFINED.

Функция установки соответствия между номерами процессов в двух группах MPI_Group_translate_ranks.

С:
MPI_Group_translate_ranks (MPI_Group group1, int n, int *ranks1,
MPI_Group group2, int *ranks2)
FORTRAN:
MPI_GROUP_TRANSLATE_RANKS(GROUP1,N, RANKS1, GROUP2,
RANKS2, IERROR)
INTEGER GROUP1, N, RANKS1(*), GROUP2, RANKS2(*), IERROR

IN

group1

-

группа1;

IN

n

-

число процессов, для которых устанавливается соответствие;

IN

ranks1

-

массив номеров процессов из 1-й группы;

IN

group2

-

группа2;

OUT

ranks2

-

номера тех же процессов во второй группе.

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

Для создания новых групп в MPI имеется 8 функций. Группа может быть создана либо с помощью коммуникатора, либо с помощью операций над множествами процессов других групп.

Функция создания группы с помощью коммуникатора MPI_Comm_group.

С:
MPI_Comm_group(MPI_Comm comm, MPI_Group *group)
FORTRAN:
MPI_COMM_GROUP(COMM, GROUP, IERROR)
INTEGER COMM, GROUP, IERROR

IN

comm

- коммуникатор;

OUT

group

- группа.

Функция создает группу group для множества процессов, входящих в область связи коммуникатора comm.

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

С:
MPI_Group_union(MPI_Group group1, MPI_Group group2,
MPI_Group *newgroup)
MPI_Group_intersection(MPI_Group group1, MPI_Group group2,
MPI_Group *newgroup)
MPI_Group_difference(MPI_Group group1, MPI_Group group2,
MPI_Group *newgroup)
FORTRAN:
MPI_GROUP_UNION(GROUP1, GROUP2, NEWGROUP, IERROR)
MPI_GROUP_INTERSECTION(GROUP1, GROUP2, NEWGROUP, IERROR)
MPI_GROUP_DIFFERENCE(GROUP1, GROUP2, NEWGROUP, IERROR)
INTEGER GROUP1, GROUP2,NEWGROUP, IERROR

IN

group1

- первая группа;

IN

group2

- вторая группа;

OUT

newgroup

- новая группа.

Операции определяются следующим образом:

Union

-

формирует новую группу из элементов 1-й группы и из элементов 2-й группы, не входящих в 1-ю (объединение множеств).

Intersection

-

новая группа формируется из элементов 1-й группы, которые входят также и во 2-ю. Упорядочивание как в 1-й группе (пересечение множеств).

Difference

-

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

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

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

С:
MPI_Group_incl(MPI_Group group, int n, int *ranks,
MPI_Group *newgroup)
MPI_Group_excl(MPI_Group group, int n, int *ranks,
MPI_Group *newgroup)
FORTRAN:
MPI_GROUP_INCL(GROUP, N, RANKS, NEWGROUP, IERROR)
MPI_GROUP_EXCL(GROUP, N, RANKS, NEWGROUP, IERROR)
INTEGER GROUP, N, RANKS(*), NEWGROUP, IERROR

IN

group

- существующая группа;

IN

n

- число элементов в массиве ranks;

IN

ranks

- массив номеров процессов;

OUT

newgroup

- новая группа.

Функция MPI_Group_incl создает новую группу, которая состоит из процессов существующей группы, перечисленных в массиве ranks. Процесс с номером i в новой группе есть процесс с номером ranks[i] в существующей группе. Каждый элемент в массиве ranks должен иметь корректный номер в группе group, и среди этих элементов не должно быть совпадающих.

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

Две следующие функции по смыслу совпадают с предыдущими, но используют более сложное формирование выборки. Массив ranks заменяется двумерным массивом ranges, представляющим собой набор триплетов для задания диапазонов процессов.

С:
MPI_Group_range_incl(MPI_Group group, int n, int ranges[][3],
MPI_Group *newgroup)
MPI_Group_range_excl(MPI_Group group, int n, int ranges[][3],
MPI_Group *newgroup)
FORTRAN:
MPI_GROUP_RANGE_INCL(GROUP, N, RANGES, NEWGROUP, IERROR)
MPI_GROUP_RANGE_EXCL(GROUP, N, RANGES, NEWGROUP, IERROR)
INTEGER GROUP, N, RANGES(3,*), NEWGROUP, IERROR

Каждый триплет имеет вид: нижняя граница, верхняя граница, шаг.

Уничтожение созданных групп выполняется функцией MPI_Group_free.

С:
MPI_Group_free(MPI_Group *group)
FORTRAN:
MPI_GROUP_FREE(GROUP, IERROR)
INTEGER GROUP, IERROR

INOUT group - уничтожаемая группа.

6.2. Функции работы с коммуникаторами

В данном подразделе рассматриваются функции работы с коммуникаторами. Они разделяются на функции доступа к коммуникаторам и функции создания коммуникаторов. Функции доступа являются локальными и не требуют коммуникаций, в отличие от функций создания, которые являются коллективными и могут потребовать межпроцессорных коммуникаций. Две основных функции доступа к коммуникатору, MPI_Comm_size - опрос числа процессов в области связи и MPI_Comm_rank - опрос идентификатора (номера) процесса в области связи, были рассмотрены в самом начале среди базовых функций MPI (раздел 2). Кроме них, имеется функция сравнения двух коммуникаторов MPI_Comm_compare.

С:
MPI_Comm_compare(MPI_Comm comm1,MPI_Comm comm2, int *result)
FORTRAN:
MPI_COMM_COMPARE(COMM1, COMM2, RESULT, IERROR)
INTEGER COMM1, COMM2, RESULT, IERROR

IN

comm1

- первый коммуникатор;

IN

comm2

- второй коммуникатор;

OUT

result

- результат сравнения.

Возможные значения результата сравнения:

MPI_IDENT

-

коммуникаторы идентичны, представляют один и тот же объект;

MPI_CONGRUENT

-

коммуникаторы конгруэнтны, две области связи с одними и теми же атрибутами группы;

MPI_SIMILAR

-

коммуникаторы подобны, группы содержат одни и те же процессы, но другое упорядочивание;

MPI_UNEQUAL

-

во всех других случаях.

Создание нового коммуникатора возможно с помощью одной из трех функций: MPI_Comm_dup, MPI_Comm_create, MPI_Comm_split.

Функция дублирования коммуникатора MPI_Comm_dup.

С:
MPI_Comm_dup(MPI_Comm comm, MPI_Comm *newcomm)
FORTRAN:
MPI_COMM_DUP(COMM, NEWCOMM, IERROR)
INTEGER COMM, NEWCOMM, IERROR

IN

comm

- коммуникатор;

OUT

newcomm

- копия коммуникатора.

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

Функция создания коммуникатора MPI_Comm_create.

С:
MPI_Comm_create(MPI_Comm comm, MPI_Group group,
MPI_Comm *newcomm)
FORTRAN:
MPI_COMM_CREATE(COMM, GROUP, NEWCOMM, IERROR)
INTEGER COMM, GROUP, NEWCOMM, IERROR

IN

comm

-

родительский коммуникатор;

IN

group

-

группа, для которой создается коммуникатор;

OUT

newcomm

-

новый коммуникатор.

Эта функция создает коммуникатор для группы group. Для процессов, которые не являются членами группы, возвращается значение MPI_COMM_NULL. Функция возвращает код ошибки, если группа group не является подгруппой родительского коммуникатора.

Функция расщепления коммуникатора MPI_Comm_split.

С:
MPI_Comm_split(MPI_Comm comm, int color, int key,
MPI_Comm *newcomm)
FORTRAN:
MPI_COMM_SPLIT(COMM, COLOR, KEY, NEWCOMM, IERROR)
INTEGER COMM, COLOR, KEY, NEWCOMM, IERROR

IN

comm

-

родительский коммуникатор;

IN

color

-

признак подгруппы;

IN

key

-

управление упорядочиванием;

OUT

newcomm

-

новый коммуникатор.

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

Приведем алгоритм расщепления группы из восьми процессов на три подгруппы и его графическую интерпретацию (Рис.18).

MPI_comm comm, newcomm;
int myid, color;
 . . . . . .
MPI_Comm_rank(comm, &myid);
color = myid%3;
MPI_Comm_split(comm, color, myid, &newcomm);

 

В данном примере первую подгруппу образовали процессы, номера которых делятся на 3 без остатка, вторую - для которых остаток равен 1 и третью - для которых остаток равен 2. Отметим, что после выполнения функции MPI_Comm_split значения коммуникатора newcomm в процессах разных подгрупп будут отличаться.

Функция уничтожения коммуникатораа MPI_Comm_free.

С:
MPI_Comm_free(MPI_Comm *comm)
FORTRAN:
MPI_COMM_FREE(COMM, IERROR)
INTEGER COMM, IERROR

IN comm - уничтожаемый коммуникатор.

Примечание: За рамками данного руководства мы оставим обсуждение inter коммуникаторов и вопросы, связанные с изменением или добавлением новых атрибутов коммуникаторов.

7. Топология процессов.

Топология процессов является одним из необязательных атрибутов коммуникатора. Такой атрибут может быть присвоен только intra коммуникатору. По умолчанию предполагается линейная топология, в которой процессы пронумерованы в диапазоне от 0 до n-1, где n - число процессов в группе. Однако для многих задач линейная топология неадекватно отражает логику коммуникационных связей между процессами. MPI предоставляет средства для создания достаточно сложных "виртуальных" топологий в виде графов, где узлы являются процессами, а грани - каналами связи между процессами. Конечно же, следует различать логическую топологию процессов, которую позволяет формировать MPI, и физическую топологию процессоров.

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

7.1. Декартова топология

Обобщением линейной и матричной топологий на произвольное число измерений является декартова топология. Для создания коммуникатора с декартовой топологией используется функция MPI_Cart_create. С помощью этой функции можно создавать топологии с произвольным числом измерений, причем по каждому измерению в отдельности можно накладывать периодические граничные условия. Таким образом, для одномерной топологии мы можем получить или линейную структуру, или кольцо - в зависимости от того, какие граничные условия будут наложены. Для двумерной топологии, соответственно, либо прямоугольник, либо цилиндр, либо тор. Заметим, что не требуется специальной поддержки гиперкубовой структуры, поскольку она представляет собой n-мерный тор с двумя процессами вдоль каждого координатного направления.

Функция создания коммуникатора с декартовой топологией.

С:
MPI_Cart_create(MPI_Comm comm_old, int ndims, int *dims,
int *periods, int reorder, MPI_Comm *comm_cart)
FORTRAN:
MPI_CART_CREATE(COMM_OLD, NDIMS, DIMS, PERIODS,
REORDER, COMM_CART, IERROR)
INTEGER COMM_OLD, NDIMS, DIMS(*), COMM_CART, IERROR
LOGICAL PERIODS(*), REORDER

IN

comm_old

-

родительский коммуникатор;

IN

ndims

-

число измерений;

IN

dims

-

массив размера ndims, в котором задается число процессов вдоль каждого измерения;

IN

periods

-

логический массив размера ndims для задания граничных условий (true - периодические, false - непериодические);

IN

reorder

-

логическая переменная, указывает, производить перенумерацию процессов (true) или нет (false);

OUT

comm_cart

-

новый коммуникатор.

Функция является коллективной, т.е. должна запускаться на всех процессах, входящих в группу коммуникатора comm_old. При этом, если какие-то процессы не попадают в новую группу, то для них возвращается результат MPI_COMM_NULL. В случае, когда размеры заказываемой сетки больше имеющегося в группе числа процессов, функция завершается аварийно. Значение параметра reorder=false означает, что идентификаторы всех процессов в новой группе будут такими же, как в старой группе. Если reorder=true, то MPI будет пытаться перенумеровать их с целью оптимизации коммуникаций.

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

Функция определения оптимальной конфигурации сетки.

С:
MPI_Dims_create(int nnodes, int ndims, int *dims)
FORTRAN:
MPI_DIMS_CREATE(NNODES, NDIMS, DIMS, IERROR)
INTEGER NNODES, NDIMS, DIMS(*), IERROR

IN

nnodes

-

общее число узлов в сетке;

IN

ndims

-

число измерений;

INOUT

dims

-

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

На входе в процедуру в массив dims должны быть занесены целые неотрицательные числа. Если элементу массива dims[i] присвоено положительное число, то для этой размерности вычисление не производится (число процессов вдоль этого направления считается заданным). Вычисляются только те компоненты dims[i], для которых перед обращением к процедуре были присвоены значения 0. Функция стремится создать максимально равномерное распределение процессов вдоль направлений, выстраивая их по убыванию, т.е. для 12 процессов она построит трехмерную сетку 4 х 3 х 1. Результат работы этой процедуры может использоваться в качестве входного параметра для процедуры MPI_Cart_create.

Функция опроса числа измерений декартовой топологии MPI_Cartdim_get.

С:
MPI_Cartdim_get(MPI_Comm comm, int *ndims)
FORTRAN:
MPI_CARTDIM_GET(COMM, NDIMS, IERROR)
INTEGER COMM, NDIMS, IERROR

IN

comm

-

коммуникатор с декартовой топологией;

OUT

ndim

-

число измерений в декартовой топологии.

Функция возвращает число измерений в декартовой топологии ndims для коммуникатора comm. Результат может быть использован в качестве параметра для вызова функции MPI_Cart_get, которая служит для получения более детальной информации.

С:
MPI_Cart_get(MPI_Comm comm, int ndims, int *dims,
int *periods, int *coords)
FORTRAN:
MPI_CART_GET(COMM, NDIMS, DIMS, PERIODS, COORDS, IERROR)
INTEGER COMM, NDIMS, DIMS(*), COORDS(*), IERROR
LOGICAL PERIODS(*)

IN

comm

-

коммуникатор с декартовой топологией;

IN

ndims

-

число измерений;

OUT

dims

-

массив размера ndims, в котором возвращается число процессов вдоль каждого измерения;

OUT

periods

-

логический массив размера ndims, в котором возвращаются наложенные граничные условия; (true - периодические, false - непериодические);

OUT

coords

-

координаты в декартовой сетке вызывающего процесса.

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

Функция получения идентификатора процесса по его координатам MPI_Cart_rank.

С:
MPI_Cart_rank(MPI_Comm comm, int *coords, int *rank)
FORTRAN:
MPI_CART_RANK(COMM, COORDS, RANK, IERROR)
INTEGER COMM, COORDS(*), RANK, IERROR

IN

comm

-

коммуникатор с декартовой топологией;

IN

coords

-

координаты в декартовой системе;

OUT

rank

-

идентификатор процесса.

Для измерений с периодическими граничными условиями будет выполняться приведение к основной области определения 0 <= coords(i) < dims(i).

Функция определения координат процесса по его идентификатору MPI_Cart_coords.

С:
MPI_Cart_coords(MPI_Comm comm, int rank, int ndims, int *coords)
FORTRAN:
MPI_CART_COORDS(COMM, RANK, NDIMS, COORDS, IERROR)
INTEGER COMM, RANK, NDIMS, COORDS(*), IERROR

IN

comm

-

коммуникатор с декартовой топологией;

IN

rank

-

идентификатор процесса;

IN

ndim

-

число измерений;

OUT

coords

-

координаты процесса в декартовой топологии.

Во многих численных алгоритмах используется операция сдвига данных вдоль каких-то направлений декартовой решетки. В MPI существует специальная функция MPI_Cart_shift, реализующая эту операцию. Точнее говоря, сдвиг данных осуществляется с помощью функции MPI_Sendrecv, а функция MPI_Cart_shift вычисляет для каждого процесса параметры для функции MPI_Sendrecv (source и dest).

Функция сдвига данных MPI_Cart_shift.

С:
MPI_Cart_shift(MPI_Comm comm, int direction, int disp,
int *rank_source, int *rank_dest)
FORTRAN:
MPI_CART_SHIFT(COMM, DIRECTION, DISP, RANK_SOURCE,
RANK_DEST, IERROR)
INTEGER COMM, DIRECTION, DISP, RANK_SOURCE,
RANK_DEST, IERROR

IN

comm

-

коммуникатор с декартовой топологией;

IN

direction

-

номер измерения, вдоль которого выполняется сдвиг;

IN

disp

-

величина сдвига (может быть как положительной, так и отрицательной);

OUT

rank_sourceа

-

номер процесса, от которого должны быть получены данные;

OUT

rank_dest

-

номер процесса, которому должны быть посланы данные.

Номер измерения и величина сдвига не обязаны быть одинаковыми для всех процессов. В зависимости от граничных условий сдвиг может быть либо циклический, либо с учетом граничных процессов. В последнем случае для граничных процессов возвращается MPI_PROC_NULL либо для переменной rank_source, либо для rank_dest. Это значение также может быть использовано при обращении к функции MPI_sendrecv.

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

Функция выделения подпространства в декартовой топологии MPI_Cart_sub.

С:
MPI_Cart_sub(MPI_Comm comm, int *remain_dims,
MPI_Comm *newcomm)
FORTRAN:
MPI_CART_SUB(COMM, REMAIN_DIMS, NEWCOMM, IERROR)
INTEGER COMM, NEWCOMM, IERROR
LOGICAL REMAIN_DIMS(*)

IN

comm

-

коммуникатор с декартовой топологией;

IN

remain_dims

-

логический массив размера ndims, указывающий, входит ли i-e измерение в новую подрешетку (remain_dims[i] = true);

OUT

newcomm

-

новый коммуникатор, описывающий подрешетку, содержащую вызывающий процесс.

Функция является коллективной. Действие функции проиллюстрируем следующим примером. Предположим, что имеется декартова решетка 2 х 3 х 4, тогда обращение к функции MPI_Cart_sub с массивом remain_dims (true, false, true) создаст три коммуникатора с топологией 2 х 4. Каждый из коммуникаторов будет описывать область связи, состоящую из 1/3 процессов, входивших в исходную область связи.

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

Для определения топологии коммуникатора служит функция MPI_Topo_test.

С:
MPI_Topo_test(MPI_Comm comm, int *status)
FORTRAN:
MPI_TOPO_TEST(COMM, STATUS, IERROR)
INTEGER COMM, STATUS, IERROR

IN

comm

- коммуникатор;

OUT

status

- топология коммуникатора.

Функция MPI_Topo_test возвращает через переменную status топологию коммуникатора comm. Возможные значения:

MPI_GRAPH

- топология графа;

MPI_CART

- декартова топология;

MPI_UNDEFINED

- топология не задана.

 

8.1. Вычисление числа

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

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

program calc_pi
include 'mpif.h'
integer i, n
double precision w,
gsum, sum
double precision v
integer np, myid, ierr
real*8 time, mflops, time1, time2, dsecnd
с инициализация MPI и определение процессорной конфигурации
call MPI_INIT( ierr )
call MPI_COMM_RANK( MPI_COMM_WORLD, myid, ierr )
call MPI_COMM_SIZE( MPI_COMM_WORLD, np, ierr )
с информацию с клавиатуры считывает 0-й процессор
if ( myid .eq. 0 ) then
print *, 'Введите число точек разбиения интервала : '
read *
, n
time1 = MPI_Wtime()
endif
с рассылка числа точек разбиения всем процессорам
call MPI_BCAST(n, 1, MPI_INTEGER, 0, MPI_COMM_WORLD, ierr)
с вычисление частичной суммы на процессоре
w = 1.0 / n
sum = 0.0d0

do i = myid+1, n, np
v = (i - 0.5d0 ) * w
v = 4.0d0 / (1.0d0 + v * v)
sum = sum + v
end do
с суммирование частичных сумм с сохранением результата в 0-м
с процессоре
call MPI_REDUCE(sum, gsum, 1, MPI_DOUBLE_PRECISION,
$ MPI_SUM, 0, MPI_COMM_WORLD, ierr)
с печать выходной информации с 0-го процессора
if (myid .eq. 0) then
time2 = MPI_Wtime()
time = time2 - time1
mflops = 9 * n / (1000000.0 * time)
print *, 'pi is approximated with ',
gsum *w
print *, 'time = ', time, ' seconds'
print *, 'mflops = ', mflops, ' on ', np, ' processors'
print *, 'mflops = ', mflops/np, ' for one processor'
endif
с закрытие MPI
call MPI_FINALIZE(ierr)
end

8.2. Перемножение матриц

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

Для обсуждения проблемы приведем сначала однопроцессорный вариант программы:

    PROGRAM MATMULT
    IMPLICIT NONE
    INTEGER I,J,K,N,NM
    PARAMETER ( NM=2000)
    REAL*8 A(NM,NM),B(NM,NM),C(NM,NM)
    REAL*8 TIME
    WRITE(*,*) СInput NТ
    READ(*,*) N
C начальная инициализация массивов
    DO 1 I = 1,N
    DO 1 J = 1,N
    A(I,J) = DBLE(I)
    B(I,J) = 1./DBLE(J)
 1  CONTINUE
    WRITE(*,*) 'N = ',N
C включаем таймер
    TIME = SECOND()
C блок вычисления
    DO 2 I = 1,N
    DO 2 J = 1,N
    C(I,J) = 0.0
    DO 3 K = 1,N
 3  C(I,J) = C(I,J) + A(I,K)*B(K,J)
 2  CONTINUE
C фиксируем время выполнения программы и печатаем контрольную
C информацию (угловые матричные элементы результирующей матрицы
C С(1,1) = C(N,N) = N, С(1,N)= 1, C(N,1)= N*N)
    TIME = SECOND() - TIME
    WRITE(*,10) C(1,1),C(1,N),C(N,1),C(N,N)
 10 FORMAT(2X,2F16.6)
    WRITE(*,*) ' TIME CALCULATION: ', TIME
    END

Следует отметить, что представленный вариант программы неэффективно работает на современных вычислительных системах. Простая модификация блока вычислений с введением промежуточного массива для хранения строки матрицы А значительно увеличивает скорость работы программы (на компьютерах Pentium III примерно в 2 раза, а на Alpha DS20E в 10 раз!):

    DO 1 I = 1,N
    DO L = 1,N
    ROW(L) = A(I,L)
    END DO
    DO 1 J = 1,N
    C(I,J) = 0.0D0
    DO 3 K = 1,N
 3  C(I,J) = C(I,J) + ROW(K)*B(K,J)
 1  CONTINUE

Это объясняется более эффективным использованием быстродействующей кэш-памяти. В языке программирования ФОРТРАН двумерные массивы располагаются в памяти компьютера по столбцам, поэтому выборка элементов из массива А выполняется из далеко расположенных друг от друга ячеек памяти. Вынесение этой операции из самых внутренних циклов значительно ускоряет работу программы. В языке С аналогичную процедуру следует применить к массиву В.

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

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

Отсюда возникает 64 возможных вариантов решения этой задачи. Большинство из этих вариантов плохо отражают специфику алгоритма, и, соответственно, заведомо неэффективны. Тот или иной способ распределения матриц однозначно определяет, какие из трех циклов вычислительного блока должны быть подвержены процедуре редукции. Ниже предлагается вариант программы решения этой задачи, который в достаточно полной мере учитывает специфику алгоритма. Поскольку для вычисления каждого матричного элемента матрицы С необходимо выполнить скалярное произведение строки матрицы А на столбец матрицы В, то матрица А разложена на одномерную сетку процессоров по строкам, а матрица В - по столбцам.

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

    PROGRAM PMATMULT
    INCLUDE 'mpif.h'
C параметры:
С NM    - полная размерность матриц;
С NPMIN - минимальное число процессоров для решения задачи;
С NPS   - размерность локальной части матриц
    PARAMETER (NM = 500, NPMIN=4, NPS=NM/NPMIN+1)
    REAL*8 A(NPS,NM), B(NM,NPS), C(NPS,NM), COL(NM)
    REAL*8 TIME
С в массивах NB и NS информация о декомпозиции матриц:
С NB  - число строк матрицы в каждом процессоре;
С NS  - номер строки, начиная с которой хранится матрица в данном
С       процессоре;
С предполагается, что процессоров не больше 64.
    INTEGER NB(0:63), NS(0:63)
С инициализация MPI
    CALL MPI_INIT(IERR)
    CALL MPI_COMM_RANK(MPI_COMM_WORLD, IAM, IERR)
    CALL MPI_COMM_SIZE(MPI_COMM_WORLD, NPROCS, IERR)
    IF(IAM.EQ.0) WRITE(*,*) 'NM = ',NM,' NPROCS = ',NPROCS
C вычисление параметров декомпозиции матриц
С алгоритм реализует максимально равномерное распределение
    NB1 = NM/NPROCS
    NB2 = MOD(NM,NPROCS)
    DO I = 0,NPROCS-1
    NB(I) = NB1
    END DO
    DO I = 0,NB2-1
    NB(I)= NB(I)+1
    END DO

    NS(0)=0
    DO I = 1,NPROCS-1
    NS(I)= NS(I-1) + NB(I-1)
    END DO
   
C заполнение матрицы А, значения матричных элементов определяются
С глобальным индексом строки. Здесь IAM - номер процессора
    DO J = 1,NM
    DO I = 1,NB(IAM)
    A(I,J) = DBLE(I+NS(IAM))
    END DO
    END DO
С заполнение матрицы В
    DO I = 1,NM
    DO J = 1,NB(IAM)
    B(I,J) =1./DBLE(J+NS(IAM))
    END DO
    END DO
C включение таймера
    TIME = MPI_WTIME()
С Блок вычисления,
С циклы по строкам и по столбцам переставлены местами и цикл по
С столбцам разбит на две части: по процессорам J1 и по элементам
С внутри процессора J2. Это сделано для того, чтобы не вычислять,
С в каком процессоре находится данный столбец. Переменная J выполняет
С сквозную нумерацию столбцов.   

С цикл по столбцам
    J = 0
    DO J1 = 0,NPROCS-1
    DO J2 = 1,NB(J1)
    J = J + 1
С процессор, хранящий очередной столбец, рассылает его всем остальным
С процессорам
    IF(IAM.EQ.J1) THEN
    DO N = 1,NM
    COL(N) = B(N,J2)
    END DO
    END IF
    CALL MPI_BCAST(COL, NM, MPI_DOUBLE_PRECISION, J1,
    *MPI_COMM_WORLD,IERR)
С цикл по строкам (именно он укорочен)
    DO I = 1,NB(IAM)
    C(I,J) = 0.0
С внутренний цикл
    DO K = 1,NM
    C(I,J) = C(I,J) + A(I,K)*COL(K)
    END DO
    END DO
    END DO
    END DO
    TIME = MPI_WTIME() - TIME
С печать контрольных матричных элементов из тех процессоров, где
С они хранятся
    IF(IAM.EQ.0) WRITE(*,*) IAM,C(1,1),C(1,NM)
    IF(IAM.EQ.NPROCS-1)
    *WRITE(*,*) IAM, C(NB(NPROCS-1),1), C(NB(NPROCS-1), NM)
    IF(IAM.EQ.0) WRITE(*,*) ' TIME CALCULATION: ', TIME
    CALL MPI_FINALIZE(IERR)
    END

В отличие от программы вычисления числа p , в этой программе практически невозможно выделить изменения по сравнению с однопроцессорным вариантом. По сути дела - это совершенно новая программа, имеющая очень мало общего с прототипом. Распараллеливание в данной программе заключается в том, что каждый процессор вычисляет свой блок матрицы С, который составляет приблизительно 1/NPROCS часть полной матрицы. Нетрудно заметить, что пересылки данных не потребовались бы, если бы матрица В не распределялась по процессорам, а целиком хранилась в каждом процессоре. В некоторых случаях такая асимметрия в распределении матриц бывает очень выгодна.

8.3. Решение краевой задачи методом Якоби

Приведем пример программы решения уравнения Лапласа методом Якоби на двумерной регулярной сетке. В программе декомпозиция области выполнена не по строкам, как на рисунке, а по столбцам. Так удобнее при программировании на языке FORTRAN, на C удобнее разбиение производить по строкам (это определяется способом размещения матриц в памяти компьютера).

С Программа решения уравнения Лапласа методом Якоби с
С использованием функции MPI_Sendrecv и NULL процессов
    PROGRAM JACOBI
    IMPLICIT NONE
    INCLUDE 'mpif.h'
    INTEGER n,m,npmin,nps,itmax
C параметры:
С n      - количество точек области в каждом направлении;
С npmin  - минимальное число процессоров для решения задачи;
С npsаа  - число столбцов локальной части матрицы. Этот параметр
С          введен в целях экономии памяти;
С itmax  - максимальное число итераций, если сходимость не будет
С          достигнута.
    PARAMETER (n = 400, npmin=1, nps=n/npmin+1, itmax = 1000)
    REAL*8 A(0:n+1,0:nps+1), B(n,nps)
    REAL*8 diffnorm, gdiffnorm, eps, time
    INTEGER left, right, i, j, k, itcnt, status(0:3), tag
    INTEGER IAM, NPROCS, ierr
    LOGICAL converged
С определение числа процессоров, выделенных задаче (NPROCS),
C и номера текущего процессора (IAM)
    CALL MPI_INIT(IERR)
    CALL MPI_COMM_SIZE(MPI_COMM_WORLD, NPROCS, ierr)
    CALL MPI_COMM_RANK(MPI_COMM_WORLD, IAM, ierr)
С установка критерия достижения сходимости
    eps = 0.01
С вычисление числа столбцов, обрабатываемых процессором
    m = n/NPROCS
    IF (IAM.LT.(n-NPROCS*m)) THEN
    m = m+1
    END IF
    time = MPI_Wtime()
С задание начальных и граничных значений
    do j = 0,m+1
    do i = 0,n+1
     a(i,j) = 0.0
    end do
    end do

    do j = 0,m+1
    A(0,j) = 1.0
    A(n+1,j) = 1.0
    end do
    IF(IAM.EQ.0) then
    do i = 0,n+1
    A(i,0) = 1.0
    end do
    end if
    IF(IAM.EQ.NPROCS-1) then
    do i = 0,n+1
    A(i,m+1) = 1.0
    end do
    end if
С определение номеров процессоров слева и справа. Если таковые
С отсутствуют, то им присваивается значение MPI_PROC_NULL
С (для них коммуникационные операцииа игнорируются)
    IF (IAM.EQ.0) THEN
    left = MPI_PROC_NULL
    ELSE
    left = IAM - 1
    END +IF
    IF (IAM.EQ.NPROCS-1) THEN
    right = MPI_PROC_NULL
    ELSE
    right = IAM+1
    END IF
    tag = 100
    itcnt = 0
    converged = .FALSE.
С цикл по итерациям
    DO k = 1,itmax
    diffnorm = 0.0
    itcnt = itcnt + 1
С вычисление новых значений функции по 5-точечной схеме
    DO j = 1, m
    DO i = 1, n
    B(i,j)=0.25*(A(i-1,j)+A(i+1,j)+A(i,j-1)+A(i,j+1))
    diffnorm = diffnorm + (B(i,j)-A(i,j))**2
    END DO
    END DO
С переприсваивание внутренних точек области
    DO j = 1, m
    DO i = 1, n
    A(i,j) = B(i,j)
    END DO
    END DO

С пересылка граничных значений в соседние процессоры
    CALL MPI_SENDRECV(B(1,1), n, MPI_DOUBLE_PRECISION, left, tag,
    $ A(1,0), n, MPI_DOUBLE_PRECISION, left, tag, MPI_COMM_WORLD,
    $ status, ierr)

    CALL MPI_SENDRECV(B(1,m), n, MPI_DOUBLE_PRECISION, right,
    $ tag, A(1,m+1), n, MPI_DOUBLE_PRECISION, right, tag,
    $ MPI_COMM_WORLD, status, ierr)
С вычисление невязки и проверка условия достижения сходимости
    CALL MPI_Allreduce( diffnorm, gdiffnorm, 1, MPI_DOUBLE_PRECISION,
    $ MPI_SUM, MPI_COMM_WORLD, ierr )
    gdiffnorm = sqrt( gdiffnorm)
    converged = gdiffnorm.LT.eps
    if(converged) goto 777
    END DO
 777 continue

    time = MPI_Wtime() - time
    IF(IAM.EQ.0) then
    WRITE(*,*) ' At iteration ', itcnt, 'а diff is ', gdiffnorm
    WRITE(*,*) ' Time calculation: ', time
    END IF   
    CALL MPI_Finalize(ierr)
    stop
    end

ЗАКЛЮЧЕНИЕ

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

В последние годы ситуация значительно изменилась благодаря появлению достаточно эффективных библиотек подпрограмм для решения широкого круга задач. Такие библиотеки избавляют программистов от рутинной работы по написанию подпрограмм для решения стандартных задач численных методов и позволяют сконцентрироваться на предметной области. Однако использование этих библиотек не избавляет от необходимости ясного понимания принципов параллельного программирования и требует выполнения достаточно объемной подготовительной работы.

ЛИТЕРАТУРА

  1. Воеводин Вл.В. Курс лекций "Параллельная обработка данных".
    http://parallel.ru/parallel/vvv
  2. Воеводин Вл.В. Лекция об архитектуре векторно-конвейерных супер-ЭВМ CRAY C90.
    http://parallel.ru/vvv/lec2.html
  3. The Cost Effective Computing Array (COCOA).
    http://cocoa.aero.psu.edu
  4. "ScaLAPACK Users Guide" http://www.netlib.org/scalapack/slug/index.html
  5. OpenMP Application Program Interface (API), http://www.openmp.org/
  6. Андреев А.Н. Что такое OpenMP? http://parallel.ru/tech/tech_dev/openmp.html
  7. Message Passing Interface Forum. MPI: A Message-Passing Interface Standard, 1994.
  8. High Performance Fortran. Language Specification. High Performance Fortran Forum. http://dacnet.rice.edu/Depts/CRPC/HPFF/versions/hpf2/hpf-v20/index.html
  9. nCUBE Corporation. nCUBE 2 Programmers Guide, r2.0, December 1990.
  10. G. Amdahl. Validity of the single-processor approach to achieving large-scale computing capabilities.
    In Proc. 1967 AFIPS Conf., volume 30, page 483. AFIPS Press, 1967.
  11. Ian Foster. Designing and Building Parallel Programs.
    http://www.hensa.ac.uk/parallel/books/addison-wesley/dbpp/index.html http://rsusu1.rnd.runnet.ru/ncube/design/dbpp/book-info.html
  12. Дацюк В.Н., Букатов А.А., Жегуло А.И. Методическое пособие по курсу "Многопроцессорные системы и параллельное программирование" Часть I,II Введение в организацию и методы программирования многопроцессорных вычислительных систем. Ростов-на-Дону, 2000. http://rsusu1.rnd.runnet.ru/ncube/koi8/method/index.html
  13. Message Passing Interface Forum. MPI: A Message-Passing Interface Standard, 1994.
  14. MPI : The Complete Reference. http://rsusu1.rnd.runnet.ru/ncube/mpi/mpibook/mpi-book.html
  15. MPI: The Message Passing Interface. http://parallel.ru/tech/tech_dev/mpi.html