- PVSM.RU - https://www.pvsm.ru -

Обмен данными с использованием MPI. Работа с библиотекой MPI на примере Intel® MPI Library

Обмен данными с использованием MPI. Работа с библиотекой MPI на примере Intel® MPI Library - 1

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

Мы приведем краткое описание того, как организован обмен данными в параллельных приложениях на основе MPI, а также ссылки на внешние источники с более подробным описанием. В практической части вы найдете описание всех этапов разработки демонстрационного MPI-приложения «Hello World», начиная с настройки необходимого окружения и заканчивая запуском самой программы.

MPI (Message Passing Interface)

MPI [1] — интерфейс передачи сообщений между процессами, выполняющими одну задачу. Он предназначен, в первую очередь, для систем с распределенной памятью (MPP [2]) в отличие от, например, OpenMP [3]. Распределенная (кластерная) система, как правило, представляет собой набор вычислительных узлов, соединенных высокопроизводительными каналами связи (например, InfiniBand [4]).

MPI является наиболее распространенным стандартом интерфейса передачи данных в параллельном программировании. Стандартизацией [5] MPI занимается MPI Forum [6]. Существуют реализации MPI под большинство современных платформ, операционных систем и языков. MPI широко применяется при решении различных задач вычислительной физики, фармацевтики, материаловедения, генетики и других областей знаний.

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

Основная операция в MPI — это передача сообщений. В MPI реализованы практически все основные коммуникационные шаблоны: двухточечные (point-to-point), коллективные (collective) и односторонние (one-sided).

Работа с MPI

Рассмотрим на живом примере, как устроена типичная MPI-программа. В качестве демонстрационного приложения возьмем исходный код примера, поставляемого с библиотекой Intel® MPI Library. Прежде чем запустить нашу первую MPI-программу, необходимо подготовить и настроить рабочую среду для экспериментов.

Настройка кластерного окружения

Для экспериментов нам понадобится пара вычислительный узлов (желательно со схожими характеристиками). Если под руками нет двух серверов, всегда можно воспользоваться cloud-сервисами.

Для демонстрации я выбрал сервис Amazon Elastic Compute Cloud [7] (Amazon EC2). Новым пользователям Amazon предоставляет пробный год бесплатного использования [8] серверами начального уровня.

Работа с Amazon EC2 интуитивно понятна. В случае возникновения вопросов, можно обратиться к подробной документации [9] (на англ.). При желании можно использовать любой другой аналогичный сервис.

Создаем два рабочих виртуальных сервера. В консоли управления выбираем EC2 Virtual Servers in the Cloud, затем Launch Instance (под «Instance» подразумевается экземпляр виртуального сервера).

Следующим шагом выбираем операционную систему. Intel® MPI Library поддерживает как Linux, так и Windows. Для первого знакомства с MPI выберем OC Linux. Выбираем Red Hat Enterprise Linux 6.6 64-bit или SLES11.3/12.0.
Выбираем Instance Type (тип сервера). Для экспериментов нам подойдет t2.micro (1 vCPUs, 2.5 GHz, Intel® Xeon® processor family, 1 GiB оперативной памяти). Как недавно зарегистрировавшемуся пользователю, мне такой тип можно было использовать бесплатно — пометка «Free tier eligible». Задаем Number of instances: 2 (количество виртуальных серверов).

После того, как сервис предложит нам запустить Launch Instances (настроенные виртуальные сервера), сохраняем SSH-ключи, которые понадобятся для связи с виртуальными серверами извне. Состояние виртуальных серверов и IP адреса для связи с серверами локального компьютера можно отслеживать в консоли управления.

Важный момент: в настройках Network & Security / Security Groups необходимо создать правило, которым мы откроем порты для TCP соединений, — это нужно для менеджера MPI-процессов. Правило может выглядеть так:

Type: Custom TCP Rule
Protocol: TCP
Port Range: 1024-65535
Source: 0.0.0.0/0

В целях безопасности можно задать и более строгое правило, но для нашего демонстрационного примера достаточно этого.

Здесь [10] можно прочитать инструкции о том, как связаться с виртуальными серверами с локального компьютера (на англ.).
Для связи с рабочими серверами c компьютера на Windows я использовал Putty [11], для передачи файлов — WinSCP [12]. Здесь [13] можно прочитать инструкции по их настройке для работы с сервисами Amazon (на англ.).

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

  1. На каждом из хостов запускаем утилиту ssh-keygen — она создаст в $HOME/.ssh директории пару из приватного и публичного ключей;
  2. Берем содержимое публичного ключа (файл с расширением .pub) с одного сервера и добавляем его в файл $HOME/.ssh/authorized_keys на другом сервере;
  3. Проделаем эту процедуру для обоих серверов;
  4. Попробуем присоединиться по SSH с одного сервера на другой и обратно, чтобы проверить корректность настройки SSH. При первом соединении может потребоваться добавить публичный ключ удаленного хоста в список $HOME/.ssh/known_hosts.

Настройка MPI библиотеки

Итак, рабочее окружение настроено. Время установить MPI.
В качестве демонстрационного варианта возьмем 30-дневную trial-версию Intel® MPI Library [14] (~300МБ). При желании можно использовать другие реализации MPI, например, MPICH [15]. Последняя доступная версия Intel® MPI Library на момент написания статьи 5.0.3.048, ее и возьмем для экспериментов.

Установим Intel® MPI Library, следуя инструкциям встроенного инсталлятора (могут потребоваться привилегии суперпользователя).

$ tar xvfz l_mpi_p_5.0.3.048.tgz
$ cd l_mpi_p_5.0.3.048
$ ./install.sh

Выполним установку на каждом из хостов с идентичным установочным путем на обоих узлах. Более стандартным способом развертывания MPI является установка в сетевое хранилище, доступное на каждом из рабочих узлов, но описание настройки подобного хранилища выходит за рамки статьи, поэтому ограничимся более простым вариантом.

Для компиляции демонстрационной MPI-программы воспользуемся GNU C компилятором (gcc).
В стандартном наборе программ RHEL образа от Amazon его нет, поэтому необходимо его установить:

$ sudo yum install gcc

В качестве демонстрационной MPI-программы возьмем test.c из стандартного набора примеров Intel® MPI Library (находится в папке intel/impi/5.0.3.048/test).
Для его компиляции первым шагом выставляем Intel® MPI Library окружение:

$. /home/ec2-user/intel/impi/5.0.3.048/intel64/bin/mpivars.sh

Далее компилируем нашу тестовую программу с помощью скрипта из состава Intel® MPI Library (все необходимые MPI зависимости при компиляции будут выставлены автоматически):

$ cd /home/ec2-user/intel/impi/5.0.3.048/test
$ mpicc -o test.exe ./test.c

Полученный test.exe копируем на второй узел:

$ scp test.exe ip-172-31-47-24:/home/ec2-user/intel/impi/5.0.3.048/test/

Прежде чем выполнять запуск MPI-программы, полезно будет сделать пробный запуск какой-нибудь стандартной Linux утилиты, например, 'hostname':

$ mpirun -ppn 1 -n 2 -hosts ip-172-31-47-25,ip-172-31-47-24 hostname
ip-172-31-47-25
ip-172-31-47-24

Утилита 'mpirun' — это программа из состава Intel® MPI Library, предназначенная для запуска MPI-приложений. Это своего рода «запускальщик». Именно эта программа отвечает за запуск экземляра MPI-программы на каждом из узлов, перечисленных в ее аргументах.

Касательно опций, '-ppn' — количество запускаемых процессов на каждый узел, '-n' — общее число запускаемых процессов, '-hosts' — список узлов, где будет запущено указанное приложение, последний аргумент — путь к исполняемому файлу (это может быть и приложение без MPI).

В нашем примере с запуском утилиты hostname мы должны получить ее вывод (название вычислительного узла) с обоих виртуальных серверов, тогда можно утверждать, что менеджер MPI-процессов работает корректно.

«Hello World» с использованием MPI

В качестве демонстрационного MPI-приложения мы взяли test.c из стандартного набора примеров Intel® MPI Library.

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

Рассмотрим подробнее основные составляющие типичной MPI-программы.

#include "mpi.h"

Подключение заголовочного файла mpi.h, который содержит объявления основных MPI-функций и констант.
Если для компиляции нашего приложения мы используем специальные скрипты из состава Intel® MPI Library (mpicc, mpiicc и т.д.), то путь до mpi.h прописывается автоматически. В противном случае, путь до папки include придется задать при компиляции.

MPI_Init (&argc, &argv);
...
MPI_Finalize ();

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

Чтобы описать остальные части нашей MPI-программы необходимо рассмотреть основные термины используемые в MPI-программировании.

MPI-программа — это набор процессов, которые могут посылать друг другу сообщения посредством различных MPI-функций. Каждый процесс имеет специальный идентификатор — ранг (rank). Ранг процесса может использоваться в различных операциях посылки MPI-сообщений, например, ранг можно указать в качестве идентификатора получателя сообщения.

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

Вернемся к test.c:

MPI_Comm_size (MPI_COMM_WORLD, &size);
MPI_Comm_rank (MPI_COMM_WORLD, &rank);

MPI_Comm_size() вызов запишет в переменную size (размер) текущего MPI_COMM_WORLD коммуникатора (общее количество процессов, которое мы указали с mpirun опцией '-n').
MPI_Comm_rank() запишет в переменную rank (ранг) текущего MPI-процесса в рамках коммуникатора MPI_COMM_WORLD.

MPI_Get_processor_name (name, &namelen);

Вызов MPI_Get_processor_name() запишет в переменную name строковой идентификатор (название) вычислительного узла, на котором был запущен соответствующий процесс.

Собранная информация (ранг процесса, размерность MPI_COMM_WORLD, название процессора) далее посылается со всех ненулевых рангов на нулевой с помощью функции MPI_Send():

MPI_Send (&rank, 1, MPI_INT, 0, 1, MPI_COMM_WORLD);
MPI_Send (&size, 1, MPI_INT, 0, 1, MPI_COMM_WORLD);
MPI_Send (&namelen, 1, MPI_INT, 0, 1, MPI_COMM_WORLD);
MPI_Send (name, namelen + 1, MPI_CHAR, 0, 1, MPI_COMM_WORLD);

MPI_Send() функция имеет следующий формат:

MPI_Send(buf, count, type, dest, tag, comm)
buf — адрес буфера памяти, в котором располагаются пересылаемые данные;
count — количество элементов данных в сообщении;
type — тип элементов данных пересылаемого сообщения;
dest — ранг процесса-получателя сообщения;
tag — специальный тег для идентификации сообщений;
comm — коммуникатор, в рамках которого выполняется посылка сообщения.

Более подробное описание функции MPI_Send() и ее аргументов, а также других MPI-функций можно найти в MPI-стандарте [5] (язык документации — английский).

На нулевом ранге принимаются сообщения, посланные остальными рангами, и печатаются на экран:

printf ("Hello world: rank %d of %d running on %sn", rank, size, name);

for (i = 1; i < size; i++) {
    MPI_Recv (&rank, 1, MPI_INT, i, 1, MPI_COMM_WORLD, &stat);
    MPI_Recv (&size, 1, MPI_INT, i, 1, MPI_COMM_WORLD, &stat);
    MPI_Recv (&namelen, 1, MPI_INT, i, 1, MPI_COMM_WORLD, &stat);
    MPI_Recv (name, namelen + 1, MPI_CHAR, i, 1, MPI_COMM_WORLD, &stat);
    printf ("Hello world: rank %d of %d running on %sn", rank, size, name);
}

Для наглядности нулевой ранг дополнительно печатает свои данные наподобие тех, что он принял с удаленных рангов.

MPI_Recv() функция имеет следующий формат:

MPI_Recv(buf, count, type, source, tag, comm, status)
buf, count, type — буфер памяти для приема сообщения;
source — ранг процесса, от которого должен быть выполнен прием сообщения;
tag — тег принимаемого сообщения;
comm — коммуникатор, в рамках которого выполняется прием данных;
status — указатель на специальную MPI-структуру данных, которая содержит информацию о результате выполнения операции приема данных.

В данной статье мы не будем углубляться в тонкости работы функций MPI_Send()/MPI_Recv(). Описание различных типов MPI-операций и тонкостей их работы — тема отдельной статьи. Отметим только, что нулевой ранг в нашей программе будет принимать сообщения от других процессов строго в определенной последовательности, начиная с первого ранга и по нарастающей (это определяется полем source в функции MPI_Recv(), которое изменяется от 1 до size).

Описанные функции MPI_Send()/MPI_Recv() — это пример так называемых двухточечных (point-to-point) MPI-операций. В таких операциях один ранг обменивается сообщениями с другим в рамках определенного коммуникатора. Существуют также коллективные (collective) MPI-операции, в которых в обмене данными могут участвовать более двух рангов. Коллективные MPI-операции — это тема для отдельной (и, возможно, не одной) статьи.

В результате работы нашей демонстрационной MPI-программы мы получим:

$ mpirun -ppn 1 -n 2 -hosts ip-172-31-47-25,ip-172-31-47-24 /home/ec2-user/intel/impi/5.0.3.048/test/test.exe
Hello world: rank 0 of 2 running on ip-172-31-47-25
Hello world: rank 1 of 2 running on ip-172-31-47-24

Вас заинтересовало рассказанное в этом посте и вы хотели бы принять участие в развитии технологии MPI? Команда разработчиков Intel® MPI Library (г.Нижний Новгород) в данный момент активно ищет инженеров-соратников. Дополнительную информацию можно посмотреть на официальном сайте компании Intel [16] и на сайте BrainStorage [17].

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

Автор: iTemko

Источник [18]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/85440

Ссылки в тексте:

[1] MPI: https://ru.wikipedia.org/wiki/Message_Passing_Interface

[2] MPP: https://ru.wikipedia.org/wiki/%D0%9C%D0%B0%D1%81%D1%81%D0%BE%D0%B2%D0%BE-%D0%BF%D0%B0%D1%80%D0%B0%D0%BB%D0%BB%D0%B5%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F_%D0%B0%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0

[3] OpenMP: https://ru.wikipedia.org/wiki/OpenMP

[4] InfiniBand: https://ru.wikipedia.org/wiki/InfiniBand

[5] Стандартизацией: http://www.mpi-forum.org/docs/mpi-3.0/mpi30-report.pdf

[6] MPI Forum: http://www.mpi-forum.org

[7] Amazon Elastic Compute Cloud: http://aws.amazon.com/ec2/

[8] пробный год бесплатного использования: http://aws.amazon.com/free/

[9] документации: http://aws.amazon.com/documentation/ec2/

[10] Здесь: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AccessingInstances.html

[11] Putty: http://www.chiark.greenend.org.uk/~sgtatham/putty/

[12] WinSCP: http://winscp.net/eng/download.php

[13] Здесь: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/putty.html

[14] Intel® MPI Library: https://software.intel.com/en-us/intel-mpi-library

[15] MPICH: http://www.mpich.org

[16] официальном сайте компании Intel: http://www.intel.ru/jobs

[17] BrainStorage: http://brainstorage.me/jobs/27551

[18] Источник: http://habrahabr.ru/post/251357/