3.2 Основы программирования в рамках стандарта MPI

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

Каждый процесс параллельной программы порождается на основе копии одного и того же программного кода (модель SPMP - Single Programm Multiple Processes). Данный программный код, представленный в виде исполняемой программы, должен быть доступен в момент запуска параллельной программы на всех используемых процессорах. Исходный программный код для исполняемой программы разрабатывается на алгоритмических языках C или Fortran с использованием той или иной реализации библиотеки MPI.

Количество процессов и число используемых процессоров определяется в момент запуска параллельной программы средствами среды исполнения MPI-программ и в ходе вычислений меняться не может (в стандарте MPI-2 предусматривается возможность динамического изменения количества процессов). Все процессы программы последовательно перенумерованы от 0 до p-1, где p есть общее количество процессов. Номер процесса именуется рангом процесса.

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

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

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

Простейшая MPI программа.

Рассмотрим параллельный вариант примера широко используемой демонстрационной программы “Hello world”. В дополнение к обычному сообщению каждый процесс будет печатать свой номер и общее количество процессов.

Первая программа:

#include <stdio.h> 
#include "mpi.h" 
int main(int argc, char* argv[]) 
{ 
   int procs_rank, procs_count; 
   MPI_Init(&argc, &argv); 
   // инициализация MPI-библиотеки 
   MPI_Comm_size(MPI_COMM_WORLD, &procs_count); 
   // определяем количество процессов 
   MPI_Comm_rank(MPI_COMM_WORLD, &procs_rank); 
   // узнаем ранг процесса 
   printf ("\n Hello, World from process %3d of %3d", procs_rank, procs_count); 
   MPI_Finalize(); 
   // закрываем MPI-библиотеку 
   return 0; 
}

Разберем детально пример:

Сначала подключается заголовочный файл библиотеки "mpi.h", содержащий определения функций, типов и констант MPI. Этот файл необходимо включать во все модули, использующие MPI.

Первым в каждой MPI программе должен быть вызов MPI_Init, который должен присутствовать в каждой программе MPI и предшествует всем другим вызовам MPI. Он устанавливает “среду” (environment) MPI. Только одно обращение к MPI_Init допускается в исполнении программы. Его аргументами являются количество аргументов командной строки процесса и собственно командная строка процесса.

int  MPI_Init (int* argc,      char*** argv)

Функция MPI_Comm_size возвращает в procs_count число запущенных для данной программы процессов. Каким способом пользователь запускает эти процессы – зависит от реализации, но любая программа может определить число запущенных процессов с помощью данного вызова. Значение procs_count – это, по сути, размер группы, связанной с коммуникатором MPI_COMM_WORLD. Процессы каждой группы пронумерованы целыми числами, начиная с 0, которые называются рангами (rank). Каждый процесс определяет свой номер в группе, связанной с данным коммуникатором, с помощью MPI_Comm_rank. Таким образом, каждый процесс получает одно и то же число в procs_count, но разные числа в rank. Каждый процесс печатает свой ранг и общее количество запущенных процессов, затем все процессы выполняют MPI_Finalize. Эта функция должна быть выполнена каждым процессом MPI и приводит к ликвидации “среды” MPI. Никакие вызовы MPI не могут быть осуществлены процессом после вызова MPI_Finalize (повторный MPI_Init также невозможен).

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

int MPI_Comm_size(MPI_Comm comm, int *size)

где comm – коммуникатор, size – число процессов в области связи коммуникатора comm.

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

int MPI_Comm_rank(MPI_Comm comm, int *rank)

где comm – коммуникатор, rank – номер процесса, вызвавщего функцию.

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

int  MPI_Finalize(void)

Рассмотрим результат работы этой программы для двух процессов.


Вариант 1. Вариант 2.
Hello, World from process 0 of 2Hello, World from process 1 of 2
Hello, World from process 1 of 2Hello, World from process 0 of 2

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

Определение времени выполнения MPI программы

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

double MPI_Wtime(void);

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

{ 
    double starttime, endtime; 
    starttime = MPI_Wtime(); 
    // Некоторые операции 
    endtime = MPI_Wtime(); 
    printf("Work time %f sec\n", endtime-starttime); 
}

Точность измерения времени также может зависеть от среды выполнения параллельной программы. Для определения текущего значения точности может быть использована функция:

double MPI_Wtick(void);

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