© Георгиевский Анатолий, 26.10.2005

Задание 2: Hello MPI

Создать шаблон MPI программы, откомпилировать и запустить тестовое приложение, преодолеть языковой барьер.

Пара слов об MPI

Если мы зададимся вопросом, как организовать выполнение каких-либо вычислений на параллельном компьютере с распределенной памятью (а именно таким и является любой кластер), довольно скоро станет понятно, что нам необходимо придумать какой-либо алгоритм передачи данных от одного процесса к другому. Стандарт MPI (message passing interface) - "протокол передачи сообщений" как раз и определяет правила передачи данных которые должны соблюдать пользователь (программист) и система. MPI поддерживает работу с наиболее распространенными языками программирования: Fortran и C.

Для реализациии передачи данных вообще говоря достаточно лишь пяти различных команд:

1. Инициализация параллельной части программы.

2. Определение уникальных адресов процессов.

3. Передача сообщения (массива заданного размера элементов заданного типа) от процесса с адресом "адрес-1" процессу с адресом "адрес-2".

4. Прием сообщения (массива заданного размера элементов заданного типа) от процесса с адресом "адрес-1" процессом с адресом "адрес-2".

5. Завершение параллельной части программы.

В стандарте MPI реализованы все эти команды (и еще многие другие).

Соглашение об именовании процедур

Здесь нет никаких жестких ограничений. Мы хотим поделиться опытом, как это бывает удобно делать.

В основе системы именования процедур мы закладываем два тезиса. Имена процедур должны отражать иерархию понятий. Имя должно содержать название пакета, название контекста (класса), название действия.

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

Создание и компиляция первой программы под MPI

Традиционно, всякое знакомство с языком программирование начинанется с приветствия. В случае параллельного программирования, надо чтобы каждый процесс выдал на терминал свое приветствие. Очевидно процесс должен себя идентифицировать, чтобы приветствия хоть как-то различались. Кроме того до последнего момента не ясно, куда направится вывод сообщений, каким чудесным образом функция printf будет выводить сообщения на удаленный терминал.

#include "mpi.h"
#include <stdio.h>
int main(int argc, char * argv[]){
int proc_id, num_proc;
    MPI_Init(&argc, &argv);
    MPI_Comm_size(MPI_COMM_WORLD, &num_proc);
    MPI_Comm_rank(MPI_COMM_WORLD, &proc_id);
// ...
    printf ("Hello, MPI! Process %u of %u\n", 
                proc_id, num_proc);

    MPI_Finalize();
    return 0;
}

Задание простое: воспроизвести программку и дождаться результатов её выполнения на десяти процессорах (копьютерах в калстере).

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

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

Графическое описание процесса

Выбор начальных условий

Дальнейшее продолжение идеи параллельной обработки входных данных требует использования индексируемого файла конфигурации на входе, чтобы по номеру процесса, proc_id, можно было получить начальные условия для каждой ветви расчета. Начальные условия делятся по группам. Нам подойдет синтаксис файлов инициализации (.ini).

# общие параметры
delta=1.2
eps=0.1
[process1]
# группа параметров для процесса 1
X1=1.2
X2=2.4
[process2]
# группа параметров для процесса 2
X1=2.4
X2=3.6
[process3]
...
[process4]
...

Необходимую функциональность для разбора файлов инициализации можно найти в библиотеке GLib.

GLib - универсальная библиотека утилит, предлагает множество полезных типов, функций для преобразования типов, функций работы со строками, управления файлами, обработки и хранения данных. Библиотека одинаково работает на большинстве UNIX-платформ, равно как и на Windows. Мы предлагаем использовать GLib для разработки платформо-незвисимых приложений. GLib распространяется свободно под лицензией GNU Library General Public License (GNU LGPL).

Ниже приведен фрагмент процедуры разбора файла инициализации с использованием GLib

#include <glib.h>
...
GKeyFile * keyfile = g_key_file_new();
GError * error = NULL;
gchar *value;
gchar *group;
double X1, X2;

g_key_file_load_from_file(keyfile, "config.ini", 0, &error);

group = g_strdup_printf ("process%i", proc_id);
value = g_key_file_get_value(keyfile, group, "X1", &error);
X1 = g_strtod(value,NULL);

Такое решение эффективно работает, когда количество входных данных не велико, и данные не структурированы. Когда данные укладываются в иерархическую структуру, удобно оказывается использовать формат XML. Когда количество записей существенно велико надо думать об использовании базы данных с выборкой записей по индексу.

Параметры запуска MPI-программы

При запуске приложения из командной строки параметры командной строки могут передаваться в программу через аргументы функции main(argc, argv). Аргумент argc - содержит число параметров, а argv - список строк. Через параметры командной строки бывает удобно пердавать название файлов для обработки.

На самом деле не очевидно, что с параметрами делает загрузчик MPI, поэтому, для начала мы протестировали список параметров.

#include "mpi.h"
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char * argv[]){
int proc_id, num_proc;
int i;
    MPI_Init(&argc, &argv);
    MPI_Comm_size(MPI_COMM_WORLD, &num_proc);
    MPI_Comm_rank(MPI_COMM_WORLD, &proc_id);

    printf ("Hello, MPI! Process %u of %u\n", 
                proc_id, num_proc);

    for(i=0; i<argc; i++)
        printf("%d=%s\n", i, argv[i]);

    MPI_Finalize();
    return 0;
}

Результат тестирования виден на картинке. Загрузчик благополучно разбирает отдельные ключи и строки.

Разбор параметров командной строки

Постановка задачи такова: имя параметра (ключа) начинается с симовла '-', за которым следует имя, через пробел ожидается значение параметра. Параметры могут быть без значения, если ключ указан - значение параметра "истина", если не указан "ложь". Строка должна быть обработана и представлена в виде объекта "список".

Пример процедуры разбора параметров:

#include <glib.h>
...
GKeyFile *KeyFile = g_key_file_new();
GError *error = NULL;
gchar group[]="cli";

for(i=1; i<argc; i++){
    if (argv[i][0] == '-'){
        gchar *key = &argv[i][1];
        if(((i+1) < argc) && 
            argv[i+1][0] != '-')
        {
            g_key_file_set_value(KeyFile, group, key, argv[i+1]);
            i++;
        } else {
            g_key_file_set_boolean(KeyFile, group, key, TRUE);
        } 
    } else {
        printf("invalid argument %s\n", argv[i]);
    }
}
// пример использования параметра 
if (g_key_file_has_key(KeyFile, group, "p", &error)){
    gchar *value = g_key_file_get_value(KeyFile, group, "p", &error);
    printf ("p = %s\n\n",value);
}
// пример вывода форматированного списка параметров
int length=0;
gchar *data = g_key_file_to_data(KeyFile, &length, &error);
fwrite ( data, sizeof(gchar), length, stdout);

В примере для систематизации параметров использованы функции из библиотеки Glib.


Полезные ссылки

  • GLib - библиотека расхожих типов и утилит, под Windows поставляется в комплекте с GTK+ и glade

    ()

  •