Вордовские лекции (1115151), страница 25
Текст из файла (страница 25)
Обычно создание любой параллельной программы включает в себя следующие основных стадии:
-
последовательный алгоритм подвергается декомпозиции (распараллеливанию), т.е. разбивается на независимо работающие ветви;
-
для взаимодействия в ветви вводятся две дополнительных нематематических операции: прием и передача данных.
-
распараллеленный алгоритм записывается в виде программы, в которой операции приема и передачи записываются в терминах конкретной системы связи между ветвями.
Система связи, в свою очередь, включает в себя два компонента: программный и аппаратный. С точки же зрения программиста данные могут передаваться:
-
через разделяемую память, синхронизация доступа ветвей к такой памяти происходит посредством семафоров
-
в виде сообщений
Первый метод является базовым для SMP-машин, второй - для сетей всех типов. Хотя можно реализовать любой метод на любой архитектуре, к примеру, разделяемая память на не-SMP-комплексах посредством организации единого виртуального адресного пространства или на SMP-машине вырожденным каналом связи для передачи сообщений служит разделяемая память.
9.2.2Крaткая характеристика MPI.
MPI представляет собой программный инструмент, предназначенных для поддержки работы параллельных процессов в терминах передачи сообщений для обеспечения связи между ветвями параллельного приложения. MPI предоставляет программисту единый механизм взаимодействия ветвей внутри параллельного приложения независимо от машинной архитектуры (однопроцессорные / многопроцессорные с общей/раздельной памятью), взаимного расположения ветвей (на одном процессоре / на разных) и API операционной системы. Параллельное приложение состоит из нескольких ветвей, или процессов, или задач, выполняющихся одновременно. Разные процессы могут выполняться как на разных процессорах, так и на одном и том же - для программы это роли не играет, поскольку в обоих случаях механизм обмена данными одинаков. При программировании на MPI программа должна содержать код всех ветвей сразу. MPI-загрузчиком запускается указываемое количество экземпляров программы. Каждый экземпляр определяет свой порядковый номер в запущенном коллективе, и в зависимости от этого номера и размера коллектива выполняет ту или иную ветку алгоритма. Такая модель параллелизма называется Single program/Multiple data ( SPMD ), и является частным случаем модели Multiple instruction/Multiple data ( MIMD ). Каждая ветвь имеет пространство данных, полностью изолированное от других ветвей. Процессы обмениваются друг с другом данными в виде сообщений. Сообщения проходят под идентификаторами, которые позволяют программе и библиотеке связи отличать их друг от друга. Для совместного проведения тех или иных расчетов процессы внутри приложения объединяются в группы. Каждый процесс может узнать у библиотеки связи свой номер внутри группы, и, в зависимости от номера приступает к выполнению соответствующей части расчетов. Количество ветвей фиксировано - в ходе работы порождение новых ветвей невозможно. Если MPI-приложение запускается в сети, запускаемый файл приложения должен быть построен на каждой машине.
В состав MPI входят, как правило, два обязательных компонента: библиотека программирования для языков Си, Си++ и Фортран, загрузчик исполняемых файлов. Кроме того, может присутствовать справочная система, командные файлы для облегчения компиляции/компоновки программ и др. в зависимости от версии.
9.2.3Коммуникаторы, группы и области связи.
Группа - это некое множество ветвей. Одна ветвь может быть членом нескольких групп. В распоряжение программиста предоставлен тип MPI_Group и набор функций, работающих с переменными и константами этого типа. Констант, собственно, две: MPI_GROUP_EMPTY может быть возвращена, если группа с запрашиваемыми характеристиками в принципе может быть создана, но пока не содержит ни одной ветви; MPI_GROUP_NULL возвращается, когда запрашиваемые характеристики противоречивы. Согласно концепции MPI, после создания группу нельзя дополнить или усечь - можно создать только новую группу под требуемый набор ветвей на базе существующей.
Область связи ("communication domain") - это нечто абстрактное: в распоряжении программиста нет типа данных, описывающего непосредственно области связи, как нет и функций по управлению ими. Области связи автоматически создаются и уничтожаются вместе с коммуникаторами. Абонентами одной области связи являются ВСЕ задачи либо одной, либо двух групп.
Коммуникатор, или описатель области связи - это верхушка трехслойного пирога (группы, области связи, описатели областей связи), в который "запечены" задачи: именно с коммуникаторами программист имеет дело, вызывая функции пересылки данных, а также подавляющую часть вспомогательных функций. Одной области связи могут соответствовать несколько коммуникаторов. Коммуникаторы являются "несообщающимися сосудами": если данные отправлены через один коммуникатор, ветвь-получатель сможет принять их только через этот же самый коммуникатор, но ни через какой-либо другой.
Зачем вообще нужны разные группы, разные области связи и разные их описатели?
По существу, они служат той же цели, что и идентификаторы сообщений - помогают ветви-приемнику и ветви-получателю надежнее определять друг друга, а также содержимое сообщения. Ветви внутри параллельного приложения могут объединяться в подколлективы для решения промежуточных задач - посредством создания групп, и областей связи над группами. Пользуясь описателем этой области связи, ветви гарантированно ничего не примут извне подколлектива, и ничего не отправят наружу. Параллельно при этом они могут продолжать пользоваться любым другим имеющимся в их распоряжении коммуникатором для пересылок вне подколлектива, например, MPI_COMM_WORLD для обмена данными внутри всего приложения. Коллективные функции создают дубликат от полученного аргументом коммуникатора, и передают данные через дубликат, не опасаясь, что их сообщения будут случайно перепутаны с сообщениями функций "точка-точка", распространямыми через оригинальный коммуникатор. Программист с этой же целью в разных кусках кода может передавать данные между ветвями через разные коммуникаторы, один из которых создан копированием другого. Коммуникаторы распределяются автоматически (функциями семейства "Создать новый комуникатор"), и для них не существует джокеров ("принимай через какой угодно коммуникатор") - вот еще два их существенных достоинства перед идентификаторами сообщений. Идентификаторы (целые числа) распределяются пользователем вручную, и это служит источником двух частых ошибок вследствие путаницы на приемной стороне:
-
сообщениям, имеющим разный смысл, вручную по ошибке назначается один и тот же идентификатор;
-
функция приема с джокером сгребает все подряд, в том числе и те сообщения, которые должны быть приняты и обработаны в другом месте ветви.
9.2.4Обрамляющие функции. Начало и завершение.
Существует несколько функций, которые используются в любом, даже самом коротком приложении MPI. Занимаются они не столько собственно передачей данных, сколько ее обеспечением:
-
Инициализация библиотеки. Одна из первых инструкций в функции main (главной функции приложения):
MPI_Init( &argc, &argv );
Она получает адреса аргументов, стандартно получаемых самой main от операционной системы и хранящих параметры командной строки. В конец командной строки программы MPI-загрузчик mpirun добавляет ряд информационных параметров, которые требуются MPI_Init. (Пример №1).
-
Аварийное закрытие библиотеки. Вызывается, если пользовательская программа завершается по причине ошибок времени выполнения, связанных с MPI:
MPI_Abort( описатель области связи, код ошибки MPI );
Вызов MPI_Abort из любой задачи принудительно завершает работу ВСЕХ задач, подсоединенных к заданной области связи. Если указан описатель MPI_COMM_WORLD, будет завершено все приложение (все его задачи) целиком, что, по-видимому, и является наиболее правильным решением. Используйте код ошибки MPI_ERR_OTHER, если не знаете, как охарактеризовать ошибку в классификации MPI.
-
Нормальное закрытие библиотеки:
MPI_Finalize();
Рекомендуется не забывать вписывать эту инструкцию перед возвращением из программы, то есть:
-
перед вызовом стандартной функции Си exit ;
-
перед каждым после MPI_Init оператором return в функции main ;
-
если функции main назначен тип void, и она не заканчивается оператором return, то MPI_Finalize() следует поставить в конец main.
-
Две информационных функции: сообщают размер группы (то есть общее количество задач, подсоединенных к ее области связи) и порядковый номер вызывающей задачи:
int size, rank;
MPI_Comm_size( MPI_COMM_WORLD, &size );
MPI_Comm_rank( MPI_COMM_WORLD, &rank );
Использование MPI_Init, MPI_Finalize, MPI_Comm_size и MPI_Comm_rank. – пример:
/*
* Начало и завершение:
* MPI_Init, MPI_Finalize
* Определение количества задач в приложениии и своего порядкового номера:
* MPI_Comm_size, MPI_Comm_rank
*/
#include <mpi.h>
#include <stdio.h>
int main( int argc, char **argv )
{
int size, rank, i;
/* Инициализируем библиотеку */
MPI_Init( &argc, &argv );
/* Узнаем количество задач в запущенном приложении */
MPI_Comm_size( MPI_COMM_WORLD, &size );
/* ... и свой собственный номер: от 0 до (size-1) */
MPI_Comm_rank( MPI_COMM_WORLD, &rank );
/* задача с номером 0 сообщает пользователю размер группы,
* к которой прикреплена область связи,
* к которой прикреплен описатель (коммуникатор) MPI_COMM_WORLD,
* т.е. число процессов в приложении!!
*/
if( rank==0 )
printf("Total processes count = %d\n", size );
/* Каждая задача выводит пользователю свой номер */
printf("Hello! My rank in MPI_COMM_WORLD = %d\n", rank );
/* Точка синхронизации, затем задача 0 печатает
* аргументы командной строки. В командной строке
* могут быть параметры, добавляемые загрузчиком MPIRUN.
*/
MPI_Barrier( MPI_COMM_WORLD );
if( rank == 0 )
for( puts ("Command line of process 0:"), i=0; i<argc;i++)
printf( "%d: \"%s\"\n", i, argv[i] );
/* Все задачи завершают выполнение */
MPI_Finalize();
return 0;
}
9.2.5Функции пересылки данных.
Хотя с теоретической точки зрения ветвям для организации обмена данными достаточно всего двух операций (прием и передача), на практике все обстоит гораздо сложнее. Одними только коммуникациями "точка-точка" (т.е. такими, в которых ровно один передающий процесс и ровно один принимающий) занимается порядка 40 функций. Пользуясь ими, программист имеет возможность выбрать:
-
способ зацепления процессов - в случае неодновременного вызова двумя процессами парных функций приема и передачи могут быть произведены:
-
автоматический выбор одного из трех нижеприведенных вариантов;
-
буферизация на передающей стороне - функция передачи заводит временный буфер, копирует в него сообщение и возвращает управление вызвавшему процессу. Содержимое буфера будет передано в фоновом режиме;
-
ожидание на приемной стороне, завершение с кодом ошибки на передающей стороне;
-
ожидание на передающей стороне, завершение с кодом ошибки на приемной стороне.
-
способ взаимодействия коммуникационного модуля MPI с вызывающим процессом:
-
блокирующий - управление вызывающему процессу возвращается только после того, как данные приняты или переданы (или скопированы во временный буфер);
-
неблокирующий - управление возвращается немедленно (т.е. процесс блокируется до завершения операции), и фактическая приемопередача происходит в фоне. Функция неблокирующего приема имеет дополнительный параметр типа "квитанция". Процесс не имеет права производить какие-либо действия с буфером сообщения, пока квитанция не будет "погашена";
персистентный - в отдельные функции выделены:
-
создание "канала" для приема/передачи сообщения,
-
инициация приема/передачи,
-
закрытие канала.
Такой способ эффективен, к примеру, если приемопередача происходит внутри цикла, а создание/закрытие канала вынесены за его границы.
Две простейшие (но и самые медленные) функции - MPI_Recv и MPI_Send - выполняют блокирующую приемопередачу с автоматическим выбором зацепления (кстати сказать, все функции приема совместимы со всеми функциями передачи). Таким образом, MPI - весьма разветвленный инструментарий. То, что в конкурирующих пакетах типа PVM реализовано одним-единственным способом, в MPI может быть сделано несколькими, про которые говорится: способ А прост в использовании, но не очень эффективен; способ Б сложнее, но эффективнее; а способ В сложнее и эффективнее при определенных условиях. Замечание о разветвленности относится и к коллективным коммуникациям (при которых получателей и/или отправителей несколько): в MPI эта категория представлена 9 функциями 5 типов:
broadcast: один-всем,
scatter: один-каждому,