Вордовские лекции (1115151), страница 27
Текст из файла (страница 27)
функцией MPI_Comm_group определяется группа, на которую указывает соответствующий коммуникатор;
на базе существующих групп функциями семейства MPI_Group_xxx создаются новые группы с нужным набором ветвей;
для итоговой группы функцией MPI_Comm_create создается коммуникатор. Она должна быть вызвана во всех ветвях-абонентах коммуникатора, передаваемого первым параметром;
все описатели созданных групп очищаются вызовами функции MPI_Group_free.
Такой механизм позволяет не только расщеплять группы подобно MPI_Comm_split, но и объединять их. Всего в MPI определено 7 разных функций конструирования групп.
9.2.11MPI и типы данных.
О типах передаваемых данных MPI должен знать постольку-поскольку при работе в сетях на разных машинах данные могут иметь разную разрядность (например, тип int - 4 или 8 байт), ориентацию (младший байт располагается в ОЗУ первым на процессорах Intel, последним - на всех остальных), и представление (это, в первую очередь, относится к размерам мантиссы и экспоненты для вещественных чисел). Поэтому все функции приемопередачи в MPI оперируют не количеством передаваемых байт, а количеством ячеек, тип которых задается параметром функции, следующим за количеством: MPI_INTEGER, MPI_REAL и т.д. Это переменные типа MPI_Datatype (тип "описатель типов", каждая его переменная описывает для MPI один тип). Они имеются для каждого базового типа, имеющегося в используемом языке программирования.
Однако, пользуясь базовыми описателями, можно передавать либо массивы, либо одиночные ячейки (как частный случай массива). А как передавать данные агрегатных типов, например, структуры? В MPI имеется механизм конструирования пользовательских описателей на базе уже имеющихся (как пользовательских, так и встроенных).
Более того, разработчики MPI создали механизм конструирования новых типов даже более универсальный, чем имеющийся в языке программирования. Действительно, во всех языках программирования ячейки внутри агрегатного типа (массива или структуры):
не налезают друг на друга,
не располагаются с разрывами (выравнивание полей в структурах не в счет).
В MPI сняты оба этих ограничения. Это позволяет весьма причудливо "вырезать", в частности, фрагменты матриц для передачи, и размещать принимаемые данные между собственных.
Выигрыш от использования механизма конструирования типов очевиден - лучше один раз вызвать функцию приемопередачи со сложным шаблоном, чем двадцать раз - с простыми.
9.2.12Зачем MPI знать тип передаваемых данных?
Действительно, зачем? Стандартные функции пересылки данных прекрасно обходятся без подобной информации - им требуется знать только размер в байтах. Вместо одного такого аргумента функции MPI получают два: количество элементов некоторого типа и символический описатель указанного типа (MPI_INT, и т.д.). Причин тому несколько:
-
Пользователю MPI позволяет описывать свои собственные типы данных, которые располагаются в памяти не непрерывно, а с разрывами, или наоборот, с "налезаниями" друг на друга. Переменная такого типа характеризуется не только размером, и эти характеристики MPI хранит в описателе типа.
-
Приложение MPI может работать на гетерогенном вычислительном комплексе (коллективе ЭВМ с разной архитектурой). Одни и те же типы данных на разных машинах могут иметь разное представление, например: на плавающую арифметику существует 3 разных стандарта (IEEE,IBM,Cray); тип char в терминальных приложениях Windows представлен альтернативной кодировкой ГОСТ, а в Юниксе - кодировкой KOI-8r ; ориентация байтов в многобайтовых числах на ЭВМ с процессорами Intel отличается от общепринятой (у Intel - младший байт занимает младший адрес, у всех остальных - наоборот). Если приложение работает в гетерогенной сети, через сеть задачи обмениваются данными в формате XDR (eXternal Data Representation), принятом в Internet. Перед отправкой и после приема данных задача конвертирует их в/из формата XDR. Естественно, при этом MPI должен знать не просто количество передаваемых байт, но и тип содержимого.
-
Обязательным требованием к MPI была поддержка языка Фортран в силу его инерционной популярности. Фортрановский тип CHARACTER требует особого обращения, поскольку переменная такого типа содержит не собственно текст, а адрес текста и его длину. Функция MPI, получив адрес переменной, должна извлечь из нее адрес текста и копировать сам текст. Это и произойдет, если в поле аргумента-описателя типа стоит MPI_CHARACTER. Ошибка в указании типа приведет: при отправке - к копированию служебных данных вместо текста, при приеме - к записи текста на место служебных данных. И то, и другое приводит к ошибкам времени выполнения.
-
Такие часто используемые в Си типы данных, как структуры, могут содержать в себе некоторое пустое пространство, чтобы все поля в переменной такого типа размещались по адресам, кратным некоторому четному числу (часто 2, 4 или 8) - это ускоряет обращение к ним. Причины тому чисто аппаратные. Выравнивание данных настраивается ключами компилятора. Разные задачи одного и того же приложения, выполняющиеся на одной и той же машине (даже на одном и том же процессоре), могут быть построены с разным выравниванием, и типы с одинаковым текстовым описанием будут иметь разное двоичное представление. MPI будет вынужден позаботиться о правильном преобразовании. Например, переменные такого типа могут занимать 9 или 16 байт:
typedef struct {
char c;
double d;
} CharDouble;
9.2.13Использование MPI.
MPI сам по себе является средством:
-
сложным: спецификация на MPI-1 содержит 300 страниц, на MPI-2 - еще 500 (причем это только отличия и добавления к MPI-1), и программисту для эффективной работы так или иначе придется с ними ознакомиться, помнить о наличии нескольких сотен функций, и о тонкостях их применения;
-
специализированным: это система связи.
Можно сказать, что сложность (т.е. многочисленность функций и обилие аргументов у большинства из них) является ценой за компромисс между эффективностью и универсальностью. С одной стороны, на SMP-машине должны существовать способы получить почти столь же высокую скорость при обмене данными между ветвями, как и при традиционном программировании через разделяемую память и семафоры. С другой стороны, все функции должны работать на любой платформе. Таким образом, программист заинтересован в инструментах, которые облегчали бы:
проведение декомпозиции,
запись ее в терминах MPI.
В данном случае это средства, генерирующие на базе входных данных текст программы на стандартном Си или Фортране, обладающей явным параллелизмом, выраженным в терминах MPI; содержащий вызовы MPI-процедур, наиболее эффективные в окружающем контексте. Такие средства делают написание программы не только легче, но и надежнее. Назовем некоторые перспективные типы такого инструментария, который лишал бы программиста необходимости вообще помнить о присутствии MPI.
Средства автоматической декомпозиции. Идеалом является такое оптимизирующее средство, которое на входе получает исходный текст некоего последовательного алгоритма, написанный на обычном языке программирования, и выдает на выходе исходный текст этого же алгоритма на этом же языке, но уже в распараллеленном на ветви виде, с вызовами MPI. Что ж, такие средства созданы, но никто не торопится раздавать их бесплатно. Кроме того, вызывает сомнение их эффективность.
Языки программирования. Это наиболее популярные на сегодняшний день средства полуавтоматической декомпозиции. В синтаксис универсального языка программирования (Си или Фортрана) вводятся дополнения для записи параллельных конструкций кода и данных. Препроцессор переводит текст в текст на стандартном языке с вызовами MPI. Примеры таких систем: mpC (massively parallel C) и HPF (High Performance Fortran).
Оптимизированные библиотеки для стандартных языков. В этом случае оптимизация вообще может быть скрыта от проблемного программиста. Чем больший объем работы внутри программы отводится подпрограммам такой библиотеки, тем большим будет итоговый выигрыш в скорости ее (программы) работы. Собственно же программа пишется на обычном языке программирования безо всяких упоминаний об MPI, и строится стандартным компилятором. От программиста потребуется лишь указать для компоновки имя библиотечного файла MPI, и запускать полученный в итоге исполняемый код не непосредственно, а через MPI-загрузчик. Популярные библиотеки обработки матриц, такие как Linpack, Lapack и ScaLapack, уже переписаны под MPI.
Средства визуального проектирования. Действительно, почему бы не расположить на экране несколько окон с исходным текстом ветвей, и пусть пользователь легким движением мыши протягивает стрелки от точек передачи к точкам приема - а визуальный построитель генерирует полный исходный текст?
Отладчики. Об отладчиках пока можно сказать только то, что они нужны. Должна быть возможность одновременной трассировки/просмотра нескольких параллельно работающих ветвей - что-либо более конкретное пока сказать трудно.
9.2.14MPI-1 и MPI-2.
В функциональности MPI есть пробелы, которые устранены в следующем проекте, MPI-2. Вкратце перечислим наиболее важные нововведения:
-
Взаимодействие между приложениями. Поддержка механизма "клиент-сервер".
-
Динамическое порождение ветвей.
-
Для работы с файлами создан архитектурно-независимый интерфейс.
-
Сделан шаг в сторону SMP-архитектуры. Теперь разделяемая память может быть не только каналом связи между ветвями, но и местом совместного хранения данных.
9.2.15Пример.
/*
* Простейшая приемопередача:
* MPI_Send, MPI_Recv
* Завершение по ошибке:
* MPI_Abort
*/
#include <mpi.h>
#include <stdio.h>
/* Идентификаторы сообщений */
#define tagFloatData 1
#define tagDoubleData 2
/* Этот макрос введен для удобства, */
/* он позволяет указывать длину массива в количестве ячеек */
#define ELEMS(x) ( sizeof(x) / sizeof(x[0]) )
int main( int argc, char **argv )
{
int size, rank, count;
float floatData[10];
double doubleData[20];
MPI_Status status;
/* Инициализируем библиотеку */
MPI_Init( &argc, &argv );
/* Узнаем количество задач в запущенном приложении */
MPI_Comm_size( MPI_COMM_WORLD, &size );
/* ... и свой собственный номер: от 0 до (size-1) */
MPI_Comm_rank( MPI_COMM_WORLD, &rank );
/* пользователь должен запустить ровно две задачи, иначе ошибка */
if( size != 2 ) {
/* задача с номером 0 сообщает пользователю об ошибке */
if( rank==0 )
printf("Error: two processes required instead of %d, abort\n",
size );
/* Все задачи-абоненты области связи MPI_COMM_WORLD
* будут стоять, пока задача 0 не выведет сообщение.
*/
MPI_Barrier( MPI_COMM_WORLD );
/* Без точки синхронизации может оказаться, что одна из задач
* вызовет MPI_Abort раньше, чем успеет отработать printf()
* в задаче 0, MPI_Abort немедленно принудительно завершит
* все задачи и сообщение выведено не будет
*/
/* все задачи аварийно завершают работу */
MPI_Abort(
MPI_COMM_WORLD, /* Описатель области связи, на которую */
/* распространяется действие ошибки */
MPI_ERR_OTHER ); /* Целочисленный код ошибки */
return -1;
}
if( rank==0 ) {
/* Задача 0 что-то такое передает задаче 1 */
MPI_Send(
floatData, /* 1) адрес передаваемого массива */
5, /* 2) сколько: 5 ячеек, т.е. floatData[0]..floatData[4] */
MPI_FLOAT, /* 3) тип ячеек */
1, /* 4) кому: задаче 1 */
tagFloatData, /* 5) идентификатор сообщения */
MPI_COMM_WORLD ); /* 6) описатель области связи, через которую */
/* происходит передача */
/* и еще одна передача: данные другого типа */
MPI_Send( doubleData, 6, MPI_DOUBLE, 1, tagDoubleData, MPI_COMM_WORLD );
} else {
/* Задача 1 что-то такое принимает от задачи 0 */
/* дожидаемся сообщения и помещаем пришедшие данные в буфер */
MPI_Recv(
doubleData, /* 1) адрес массива, куда складывать принятое */
ELEMS( doubleData ), /* 2) фактическая длина приемного */
/* массива в числе ячеек */
MPI_DOUBLE, /* 3) сообщаем MPI, что пришедшее сообщение */
/* состоит из чисел типа 'double' */
0, /* 4) от кого: от задачи 0 */
tagDoubleData, /* 5) ожидаем сообщение с таким идентификатором */
MPI_COMM_WORLD, /* 6) описатель области связи, через которую */
/* ожидается приход сообщения */
&status ); /* 7) сюда будет записан статус завершения приема */
/* Вычисляем фактически принятое количество данных */
MPI_Get_count(
&status, /* статус завершения */
MPI_DOUBLE, /* сообщаем MPI, что пришедшее сообщение */
/* состоит из чисел типа 'double' */
&count ); /* сюда будет записан результат */
/* Выводим фактическую длину принятого на экран */
printf("Received %d elems\n", count );
/* Аналогично принимаем сообщение с данными типа float
* Обратите внимание: задача-приемник имеет возможность
* принимать сообщения не в том порядке, в котором они
* отправлялись, если эти сообщения имеют разные идентификаторы
*/