Вордовские лекции (1115151), страница 26
Текст из файла (страница 26)
gather: каждый-одному,
allgather: все-каждому,
alltoall: каждый-каждому.
На первый взгляд может показаться, что программисту легче будет в случае необходимости самому написать такую функцию, но при этом он, скорее всего, будет использовать функции типа MPI_Send и MPI_Recv, в то время как имеющиеся в MPI функции оптимизированы - не пользуясь функциями "точка-точка", они напрямую (на что, согласно идеологии MPI, программа пользователя права не имеет) обращаются к разделяемой памяти и семафорам и к TCP/IP при работе в сети. А если такая архитектурно-зависимая оптимизация невозможна, используется оптимизация архитектурно-независимая: передача производится не напрямую от одного ко всем (время передачи линейно зависит от количества ветвей-получателей), а по двоичному дереву (время передачи логарифмически зависит от количества). Как следствие, скорость работы повышается.
9.2.6Связь "точка-точка". Простейший набор. Пример.
Это самый простой тип связи между задачами: одна ветвь вызывает функцию передачи данных, а другая - функцию приема. В MPI это выглядит, например, так:
Задача 1 передает:
int buf[10];
MPI_Send( buf, 5, MPI_INT, 1, 0, MPI_COMM_WORLD );
Задача 2 принимает:
int buf[10];
MPI_Status status;
MPI_Recv( buf, 10, MPI_INT, 0, 0, MPI_COMM_WORLD, &status );
Аргументы функций:
Адрес буфера, из которого в задаче 1 берутся, а в задаче 2 помещаются данные. Наборы данных у каждой задачи свои, поэтому, например, используя одно и то же имя массива в нескольких задачах, указываете не одну и ту же область памяти, а разные, никак друг с другом не связанные.
Размер буфера. Задается не в байтах, а в количестве ячеек. Для MPI_Send указывает, сколько ячеек требуется передать (в примере передаются 5 чисел). В MPI_Recv означает максимальную емкость приемного буфера. Если фактическая длина пришедшего сообщения меньше - последние ячейки буфера останутся нетронутыми, если больше - произойдет ошибка времени выполнения.
Тип ячейки буфера. MPI_Send и MPI_Recv оперируют массивами однотипных данных. Для описания базовых типов Си в MPI определены константы MPI_INT, MPI_CHAR, MPI_DOUBLE и так далее, имеющие тип MPI_Datatype. Их названия образуются префиксом "MPI_" и именем соответствующего типа (int, char, double, ...), записанным заглавными буквами. Пользователь может "регистрировать" в MPI свои собственные типы данных, например, структуры, после чего MPI сможет обрабатывать их наравне с базовыми.
Номер задачи, с которой происходит обмен данными. Все задачи внутри созданной MPI группы автоматически нумеруются от 0 до (размер группы-1). В примере задача 0 передает задаче 1, задача 1 принимает от задачи 0.
Идентификатор сообщения. Это целое число от 0 до 32767, которое пользователь выбирает сам. Оно служит той же цели, что и, например, расширение файла - задача-приемник:
по идентификатору определяет смысл принятой информации ;
сообщения, пришедшие в неизвестном порядке, может извлекать из общего входного потока в нужном алгоритму порядке. Хорошим тоном является обозначение идентификаторов символьными именами посредством операторов "#define" или "const int".
Описатель области связи (коммуникатор). Обязан быть одинаковым для MPI_Send и MPI_Recv.
Статус завершения приема. Содержит информацию о принятом сообщении: его идентификатор, номер задачи-передатчика, код завершения и количество фактически пришедших данных.
9.2.7Коллективные функции.
Под термином "коллективные" в MPI подразумеваются три группы функций:
-
точки синхронизации, или барьеры;
-
функции коллективного обмена данными (о них уже упоминалось выше);
-
функции поддержки распределенных операций.
Коллективная функция одним из аргументов получает описатель области связи (коммуникатор). Вызов коллективной функции является корректным, только если произведен из всех процессов-абонентов соответствующей области связи, и именно с этим коммуникатором в качестве аргумента (хотя для одной области связи может иметься несколько коммуникаторов, подставлять их вместо друг друга нельзя). В этом и заключается коллективность: либо функция вызывается всем коллективом процессов, либо никем; третьего не дано.
Как поступить, если требуется ограничить область действия для коллективной функции только частью присоединенных к коммуникатору задач, или наоборот - расширить область действия? Нужно создавать временную группу/область связи/коммуникатор на базе существующих.
Основные особенности и отличия от коммуникаций типа "точка-точка":
-
на прием и/или передачу работают одновременно все задачи-абоненты указываемого коммуникатора;
-
коллективная функция выполняет одновременно и прием, и передачу; она имеет большое количество параметров, часть которых нужна для приема, а часть для передачи; в разных задачах та или иная часть игнорируется;
-
как правило, значения ВСЕХ параметров (за исключением адресов буферов) должны быть идентичными во всех задачах;
-
MPI назначает идентификатор для сообщений автоматически; кроме того, сообщения передаются не по указываемому коммуникатору, а по временному коммуникатору-дупликату; тем самым потоки данных коллективных функций надежно изолируются друг от друга и от потоков, созданных функциями "точка-точка".
MPI_Bcast рассылает содержимое буфера из задачи, имеющей в указанной области связи номер root, во все остальные:
MPI_Bcast( buf, count, dataType, rootRank, communicator );
MPI_Gather ("совок") собирает в приемный буфер задачи root передающие буфера остальных задач.
Векторный вариант "совка" - MPI_Gatherv - позволяет задавать разное количество отправляемых данных в разных задачах-отправителях. Соответственно, на приемной стороне задается массив позиций в приемном буфере, по которым следует размещать поступающие данные, и максимальные длины порций данных от всех задач. Оба массива содержат позиции/длины не в байтах, а в количестве ячеек типа recvCount.
MPI_Scatter ("разбрызгиватель") : выполняет обратную "совку" операцию - части передающего буфера из задачи root распределяются по приемным буферам всех задач
И ее векторный вариант - MPI_Scatterv, рассылающая части неодинаковой длины в приемные буфера неодинаковой длины.
MPI_Allgather аналогична MPI_Gather, но прием осуществляется не в одной задаче, а во ВСЕХ: каждая имеет специфическое содержимое в передающем буфере, и все получают одинаковое содержимое в буфере приемном. Как и в MPI_Gather, приемный буфер последовательно заполняется данными изо всех передающих. Вариант с неодинаковым количеством данных называется MPI_Allgatherv.
MPI_Alltoall : каждый процесс нарезает передающий буфер на куски и рассылает куски остальным процессам; каждый процесс получает куски от всех остальных и поочередно размещает их приемном буфере. Это "совок" и "разбрызгиватель" в одном флаконе. Векторный вариант называется MPI_Alltoallv.
Коллективные функции несовместимы с "точка-точка": недопустимым, например, является вызов в одной из принимающих широковещательное сообщение задач MPI_Recv вместо MPI_Bcast.
9.2.8Точки синхронизации, или барьеры.
Этим занимается всего одна функция:
int MPI_Barrier( MPI_Comm comm );
MPI_Barrier останавливает выполнение вызвавшей ее задачи до тех пор, пока не будет вызвана изо всех остальных задач, подсоединенных к указываемому коммуникатору. Гарантирует, что к выполнению следующей за MPI_Barrier инструкции каждая задача приступит одновременно с остальными.
Это единственная в MPI функция, вызовами которой гарантированно синхронизируется во времени выполнение различных ветвей! Некоторые другие коллективные функции в зависимости от реализации могут обладать, а могут и не обладать свойством одновременно возвращать управление всем ветвям; но для них это свойство является побочным и необязательным - если Вам нужна синхронность, используйте только MPI_Barrier.
Когда может потребоваться синхронизация? Например, синхронизация используется перед аварийным завершением.
Это утверждение непроверено, но: алгоритмическое необходимости в барьерах, как представляется, нет. Параллельный алгоритм для своего описания требует по сравнению с алгоритмом классическим всего лишь двух дополнительных операций - приема и передачи из ветви в ветвь. Точки синхронизации несут чисто технологическую нагрузку вроде той, что описана в предыдущем абзаце.
Иногда случается, что ошибочно работающая программа перестает врать, если ее исходный текст хорошенько нашпиговать барьерами. Однако программа начнет работать медленнее, например:
Без барьеров: 0 xxxx....xxxxxxxxxxxxxxxxxxxx
1 xxxxxxxxxxxx....xxxxxxxxxxxx
2 xxxxxxxxxxxxxxxxxxxxxx....xx
C барьерами: 0 xxxx....xx(xxxxxxxx(||||xxxxxxxx(||xx
1 xxxxxx(||||x....xxxxxxx(xxxxxxxx(||xx
2 xxxxxx(||||xxxxxxxx(||||..xxxxxxxx(xx
----------------------------- > Время
Обозначения:
x нормальное выполнение
. ветвь простаивает - процессорное время отдано под другие цели
( вызван MPI_Barrier
| MPI_Barrier ждет своего вызова в остальных ветвях
Так что "задавить" ошибку барьерами хорошо только в качестве временного решения на период отладки.
9.2.9Распределенные операции.
Идея проста: в каждой задаче имеется массив. Над нулевыми ячейками всех массивов производится некоторая операция (сложение/произведение/ поиск минимума/максимума и т.д.), над первыми ячейками производится такая же операция и т.д. Четыре функции предназначены для вызова этих операций и отличаются способом размещения результата в задачах.
MPI_Reduce : массив с результатами размещается в задаче с номером root:
int vector[16];
int resultVector[16];
MPI_Comm_rank( MPI_COMM_WORLD, &myRank );
for( i=0; i<16; i++ )
vector[i] = myRank*100 + i;
MPI_Reduce(
vector, /* каждая задача в коммуникаторе предоставляет вектор */
resultVector, /* задача номер 'root' собирает данные сюда */
16, /* количество ячеек в исходном и результирующем массивах */
MPI_INT, /* и тип ячеек */
MPI_SUM, /* описатель операции: поэлементное сложение векторов */
0, /* номер задачи, собирающей результаты в 'resultVector' */
MPI_COMM_WORLD /* описатель области связи */
);
if( myRank==0 )
/* печатаем resultVector, равный сумме векторов */
Предопределенных описателей операций в MPI насчитывается 12:
1. MPI_MAX и MPI_MIN ищут поэлементные максимум и минимум;
2. MPI_SUM вычисляет сумму векторов;
3. MPI_PROD вычисляет поэлементное произведение векторов;
4. MPI_LAND, MPI_BAND, MPI_LOR, MPI_BOR, MPI_LXOR, MPI_BXOR - логические и двоичные операции И, ИЛИ, исключающее ИЛИ;
5. MPI_MAXLOC, MPI_MINLOC - поиск индексированного минимума/максимума.
Количество поддерживаемых операциями типов для ячеек векторов строго ограничено вышеперечисленными. Никакие другие встроенные или пользовательские описатели типов использоваться не могут! Все операции являются ассоциативными ( "(a+b)+c = a+(b+c)" ) и коммутативными ( "a+b = b+a" ).
MPI_Allreduce : результат рассылается всем задачам, параметр 'root' убран.
MPI_Reduce_scatter : каждая задача получает не весь массив-результат, а его часть. Длины этих частей находятся в массиве-третьем параметре функции. Размер исходных массивов во всех задачах одинаков и равен сумме длин результирующих массивов.
MPI_Scan : аналогична функции MPI_Allreduce в том отношении, что каждая задача получает результрующий массив. Главное отличие: здесь содержимое массива-результата в задаче i является результатом выполнение операции над массивами из задач с номерами от 0 до i включительно.
Помимо встроенных, пользователь может вводить свои собственные операции.
9.2.10Создание коммуникаторов и групп.
Копирование. Самый простой способ создания коммуникатора - скопировать "один-в-один" уже имеющийся:
MPI_Comm tempComm;
MPI_Comm_dup( MPI_COMM_WORLD, &tempComm );
/* ... передаем данные через tempComm ... */
MPI_Comm_free( &tempComm );
Новая группа при этом не создается - набор задач остается прежним. Новый коммуникатор наследует все свойства копируемого.
Расщепление. Соответствующая коммуникатору группа расщепляется на непересекающиеся подгруппы, для каждой из которых заводится свой коммуникатор.
MPI_Comm_split(
existingComm, /* существующий описатель, например MPI_COMM_WORLD */
indexOfNewSubComm, /* номер подгруппы, куда надо поместить ветвь */
rankInNewSubComm, /* желательный номер в новой подгруппе */
&newSubComm ); /* описатель области связи новой подгруппы */
Эта функция имеет одинаковый первый параметр во всех ветвях, но разные второй и третий - и в зависимости от них разные ветви определяются в разные подгруппы; возвращаемый в четвертом параметре описатель будет принимать в разных ветвях разные значения (всего столько разных значений, сколько создано подгрупп). Если indexOfNewSubComm равен MPI_UNDEFINED, то в newSubComm вернется MPI_COMM_NULL, то есть ветвь не будет включена ни в какую из созданных групп.
Создание через группы. В предыдущих двух случаях коммуникатор создается от существующего коммуникатора напрямую, без явного создания группы: группа либо та же самая, либо создается автоматически. Самый же общий способ таков: