Параллельное программирование на основе MPI
5.2.2. Определение времени выполнение MPI-программы
Практически сразу же после разработки первых параллельных программ возникает необходимость определения времени выполнения вычислений для оценки достигаемого ускорения процессов решения задач за счет использования параллелизма. Используемые обычно средства для измерения времени работы программ зависят, как правило, от аппаратной платформы, операционной системы, алгоритмического языка и т.п. Стандарт MPI включает определение специальных функций для измерения времени, применение которых позволяет устранить зависимость от среды выполнения параллельных программ.
Получение текущего момента времени обеспечивается при помощи функции:
double MPI_Wtime(void),
результат ее вызова есть количество секунд, прошедшее от некоторого определенного момента времени в прошлом. Этот момент времени в прошлом, от которого происходит отсчет секунд, может зависеть от среды реализации библиотеки MPI, и, тем самым, для ухода от такой зависимости функцию MPI_Wtime следует использовать только для определения длительности выполнения тех или иных фрагментов кода параллельных программ. Возможная схема применения функции MPI_Wtime может состоять в следующем:
double t1, t2, dt; t1 = MPI_Wtime(); ѕ t2 = MPI_Wtime(); dt = t2 – t1;
Точность измерения времени также может зависеть от среды выполнения параллельной программы. Для определения текущего значения точности может быть использована функция:
double MPI_Wtick(void),
позволяющая определить время в секундах между двумя последовательными показателями времени аппаратного таймера примененной компьютерной системы.
5.2.3. Начальное знакомство с коллективными операциями передачи данных
Функции MPI_Send и MPI_Recv, рассмотренные в п. 5.2.1, обеспечивают возможность выполнения парных операций передачи данных между двумя процессами параллельной программы. Для выполнения коммуникационных коллективных операций, в которых принимают участие все процессы коммуникатора, в MPI предусмотрен специальный набор функций. В данном подразделе будут рассмотрены три такие функции, широко применяемые даже при разработке сравнительно простых параллельных программ ; полное же представление коллективных операций будет дано в подразделе 5.4.
Для демонстрации применения рассматриваемых функций MPI будет использоваться учебная задача суммирования элементов вектора x (см. подраздел 2.5):
Разработка параллельного алгоритма для решения данной задачи не вызывает затруднений: необходимо разделить данные на равные блоки, передать эти блоки процессам, выполнить в процессах суммирование полученных данных, собрать значения вычисленных частных сумм на одном из процессов и сложить значения частичных сумм для получения общего результата решаемой задачи. При последующей разработке демонстрационных программ данный рассмотренный алгоритм будет несколько упрощен: процессам программы будет передаваться весь суммируемый вектор, а не отдельные блоки этого вектора.
5.2.3.1. Передача данных от одного процесса всем процессам программы
Первая задача при выполнении рассмотренного параллельного алгоритма суммирования состоит в необходимости передачи значений вектора x всем процессам параллельной программы. Конечно, для решения этой задачи можно воспользоваться рассмотренными ранее функциями парных операций передачи данных:
MPI_Comm_size(MPI_COMM_WORLD, &ProcNum); for (int i = 1; i < ProcNum; i++) MPI_Send(&x, n, MPI_DOUBLE, i, 0, MPI_COMM_WORLD);
Однако такое решение будет крайне неэффективным, поскольку повторение операций передачи приводит к суммированию затрат (латентностей) на подготовку передаваемых сообщений. Кроме того, как показано в "Оценка коммуникационной трудоемкости параллельных алгоритмов" , данная операция может быть выполнена за log2p итераций передачи данных.
Достижение эффективного выполнения операции передачи данных от одного процесса всем процессам программы ( широковещательная рассылка данных ) может быть обеспечено при помощи функции MPI:
int MPI_Bcast(void *buf, int count, MPI_Datatype type, int root, MPI_Comm comm),
где
- buf, count, type — буфер памяти с отправляемым сообщением (для процесса с рангом 0 ) и для приема сообщений (для всех остальных процессов );
- root — ранг процесса, выполняющего рассылку данных;
- comm — коммуникатор, в рамках которого выполняется передача данных.
Функция MPI_Bcast осуществляет рассылку данных из буфера buf, содержащего count элементов типа type, с процесса, имеющего номер root, всем процессам, входящим в коммуникатор comm (см. рис. 5.1).
Следует отметить:
- функция MPI_Bcast определяет коллективную операцию, и, тем самым, при выполнении необходимых рассылок данных вызов функции MPI_Bcast должен быть осуществлен всеми процессами указываемого коммуникатора (см. далее пример программы);
- указываемый в функции MPI_Bcast буфер памяти имеет различное назначение у разных процессов: для процесса с рангом root, которым осуществляется рассылка данных, в этом буфере должно находиться рассылаемое сообщение, а для всех остальных процессов указываемый буфер предназначен для приема передаваемых данных;
- все коллективные операции "несовместимы" с парными операциями — так, например, принять широковещательное сообщение, отосланное с помощью MPI_Bcast, функцией MPI_Recv нельзя, для этого можно задействовать только MPI_Bcast.
Приведем программу для решения учебной задачи суммирования элементов вектора с использованием рассмотренной функции.
Программа 5.2. Параллельная программа суммирования числовых значений
#include <math.h> #include <stdio.h> #include <stdlib.h> #include "mpi.h" int main(int argc, char* argv[]){ double x[100], TotalSum, ProcSum = 0.0; int ProcRank, ProcNum, N=100, k, i1, i2; MPI_Status Status; // Инициализация MPI_Init(&argc,&argv); MPI_Comm_size(MPI_COMM_WORLD,&ProcNum); MPI_Comm_rank(MPI_COMM_WORLD,&ProcRank); // Подготовка данных if ( ProcRank == 0 ) DataInitialization(x,N); // Рассылка данных на все процессы MPI_Bcast(x, N, MPI_DOUBLE, 0, MPI_COMM_WORLD); // Вычисление частичной суммы на каждом из процессов // на каждом процессе суммируются элементы вектора x от i1 до i2 k = N / ProcNum; i1 = k * ProcRank; i2 = k * ( ProcRank + 1 ); if ( ProcRank == ProcNum-1 ) i2 = N; for ( int i = i1; i < i2; i++ ) ProcSum = ProcSum + x[i]; // Сборка частичных сумм на процессе с рангом 0 if ( ProcRank == 0 ) { TotalSum = ProcSum; for ( int i=1; i < ProcNum; i++ ) { MPI_Recv(&ProcSum,1,MPI_DOUBLE,MPI_ANY_SOURCE,0, MPI_COMM_WORLD, &Status); TotalSum = TotalSum + ProcSum; } } else // Все процессы отсылают свои частичные суммы MPI_Send(&ProcSum, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); // Вывод результата if ( ProcRank == 0 ) printf("\nTotal Sum = %10.2f",TotalSum); MPI_Finalize(); return 0; }5.2.
В приведенной программе функция DataInitialization осуществляет подготовку начальных данных. Необходимые данные могут быть введены с клавиатуры, прочитаны из файла или сгенерированы при помощи датчика случайных чисел – подготовка этой функции предоставляется как задание для самостоятельной разработки.
5.2.3.2. Передача данных от всех процессов одному процессу. Операция редукции
В рассмотренной программе суммирования числовых значений имеющаяся процедура сбора и последующего суммирования данных является примером часто выполняемой коллективной операции передачи данных от всех процессов одному процессу. В этой операции над собираемыми значениями осуществляется та или иная обработка данных (для подчеркивания последнего момента данная операция еще именуется операцией редукции данных ). Как и ранее, реализация операции редукции при помощи обычных парных операций передачи данных является неэффективной и достаточно трудоемкой. Для наилучшего выполнения действий, связанных с редукцией данных, в MPI предусмотрена функция:
int MPI_Reduce(void *sendbuf, void *recvbuf, int count, MPI_Datatype type, MPI_Op op, int root, MPI_Comm comm),
где
- sendbuf — буфер памяти с отправляемым сообщением;
- recvbuf — буфер памяти для результирующего сообщения (только для процесса с рангом root );
- count — количество элементов в сообщениях;
- type — тип элементов сообщений;
- op — операция, которая должна быть выполнена над данными;
- root — ранг процесса, на котором должен быть получен результат;
- comm — коммуникатор, в рамках которого выполняется операция.
В качестве операций редукции данных могут быть использованы предопределенные в MPI операции – см. табл. 5.2.
Помимо данного стандартного набора операций могут быть определены и новые дополнительные операции непосредственно самим пользователем библиотеки MPI – см., например, [ [ 4 ] , [ 40 ] – [ 42 ] , [ 57 ] ].
Общая схема выполнения операции сбора и обработки данных на одном процессе показана на табл. 5.2. Элементы получаемого сообщения на процессе root представляют собой результаты обработки соответствующих элементов передаваемых процессами сообщений, т.е.:
где есть операция, задаваемая при вызове функции MPI_Reduce (для пояснения на рис. 5.3 показан пример выполнения функции редукции данных).Следует отметить:
- функция MPI_Reduce определяет коллективную операцию, и, тем самым, вызов функции должен быть выполнен всеми процессами указываемого коммуникатора. При этом все вызовы функции должны содержать одинаковые значения параметров count, type, op, root, comm ;
- передача сообщений должна быть выполнена всеми процессами, результат операции будет получен только процессом с рангом root ;
- выполнение операции редукции осуществляется над отдельными элементами передаваемых сообщений. Так, например, если сообщения содержат по два элемента данных и выполняется операция суммирования MPI_SUM, то результат также будет состоять из двух значений, первое из которых будет содержать сумму первых элементов всех отправленных сообщений, а второе значение будет равно сумме вторых элементов сообщений соответственно;
- не все сочетания типа данных type и операции op возможны, разрешенные сочетания перечислены в табл. 5.3.
Операции | Допустимый тип операндов для алгоритмического языка C |
---|---|
MPI_MAX, MPI_MIN, MPI_SUM, MPI_PROD | Целый, вещественный |
MPI_LAND, MPI_LOR, MPI_LXOR | Целый |
MPI_BAND, MPI_BOR, MPI_BXOR | Целый, байтовый |
MPI_MINLOC, MPI_MAXLOC | Целый, вещественный |
Рис. 5.3. Пример выполнения операции редукции при суммировании пересылаемых данных для трех процессов (в каждом сообщении 4 элемента, сообщения собираются на процессе с рангом 2)
Применим полученные знания для переработки ранее рассмотренной программы суммирования: как можно увидеть, весь программный код ("Сборка частичных сумм на процессе с рангом 0"), может быть теперь заменен на вызов одной лишь функции MPI_Reduce:
// Сборка частичных сумм на процессе с рангом 0 MPI_Reduce(&ProcSum, &TotalSum, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
5.2.3.3. Синхронизация вычислений
В ряде ситуаций независимо выполняемые в процессах вычисления необходимо синхронизировать. Так, например, для измерения времени начала работы параллельной программы необходимо, чтобы для всех процессов одновременно были завершены все подготовительные действия, перед окончанием работы программы все процессы должны завершить свои вычисления и т.п.
Синхронизация процессов, т.е. одновременное достижение процессами тех или иных точек процесса вычислений, обеспечивается при помощи функции MPI:
int MPI_Barrier(MPI_Comm comm),
где
- comm — коммуникатор, в рамках которого выполняется операция.
Функция MPI_Barrier определяет коллективную операцию, и, тем самым, при использовании она должна вызываться всеми процессами используемого коммуникатора. При вызове функции MPI_Barrier выполнение процесса блокируется, продолжение вычислений процесса произойдет только после вызова функции MPI_Barrier всеми процессами коммуникатора.
5.2.3.4. Аварийное завершение параллельной программы
Для корректного завершения параллельной программы в случае непредвиденных ситуаций необходимо использовать функцию:
int MPI_Abort(MPI_Comm comm, int errorcode),
где
- comm — коммуникатор, процессы которого необходимо аварийно остановить;
- errorcode — код возврата из параллельной программы.
Эта функция корректно прерывает выполнение параллельной программы, оповещая об этом событии среду MPI, в отличие от функций стандартной библиотеки алгоритмического языка C, таких, как abort или terminate. Обычное ее использование заключается в следующем:
MPI_Abort(MPI_COMM_WORLD, MPI_ERR_OTHER);