MPI с C: пассивной синхронизацией RMA
Так как я до сих пор не нашел ответа на свой вопрос и вот-вот схожу с ума от этой проблемы, я просто задаю вопрос, мучающий мой разум;-)
Я работаю над распараллеливанием алгоритма исключения узлов, который я уже запрограммировал. Целевая среда - это кластер.
В моей параллельной программе я различаю основной процесс (в моем случае ранг 0) и рабочие рабы (каждый ранг, кроме 0). Моя идея такова, что мастер отслеживает, какие рабы доступны, и отправляет их, а затем работает. Поэтому и по некоторым другим причинам я пытаюсь установить рабочий процесс на основе пассивного RMA с последовательностями блокировка-разблокировка-разблокировка. Я использую целочисленный массив с именем schedule, в котором для каждой позиции в массиве, представляющей ранг, либо 0 для рабочего процесса, либо 1 для доступного процесса (поэтому, если schedule[1]=1, один доступен для работы). Если процесс завершается своей работой, он помещает в массив на ведущем устройстве 1 сигнал о его доступности. Код, который я пробовал для этого, выглядит следующим образом:
MPI_Win_lock(MPI_LOCK_EXCLUSIVE,0,0,win); // a exclusive window is locked on process 0
printf("Process %d:\t exclusive lock on process 0 started\n",myrank);
MPI_Put(&schedule[myrank],1,MPI_INT,0,0,1,MPI_INT,win); // the line myrank of schedule is put into process 0
printf("Process %d:\t put operation called\n",myrank);
MPI_Win_unlock(0,win); // the window is unlocked
Это работало отлично, особенно когда мастер-процесс был синхронизирован с барьером на конец блокировки, потому что тогда вывод мастера был сделан после операции put.
В качестве следующего шага я попытался позволить мастеру регулярно проверять, есть ли доступные рабы или нет. Поэтому я создал цикл while для повторения, пока каждый процесс не сигнализировал о своей доступности (повторяю, что это программа, обучающая меня принципам, я знаю, что реализация все еще не выполняет то, что я хочу). Цикл в базовом варианте просто печатает мое расписание массива и затем проверяет в функции fnz, есть ли другие рабочие процессы, кроме master:
while(j!=1){
printf("Process %d:\t following schedule evaluated:\n",myrank);
for(i=0;i<size;i++)printf("%d\t",schedule[i]);//print the schedule
printf("\n");
j=fnz(schedule);
}
И тогда концепция взорвалась. После инвертирования процесса и получения необходимой информации с помощью получения от ведомых устройств ведущим вместо того, чтобы передавать его с положением от подчиненных к ведущему устройству, я обнаружил, что моей основной проблемой является получение блокировки: команда разблокировки не выполняется, потому что в случае пут блокировки вообще не предоставляется, а в случае получения блокировка предоставляется только тогда, когда подчиненный процесс завершает свою работу и ожидает в барьере. На мой взгляд, в моем мышлении должна быть серьезная ошибка. Не может быть идеи пассивного RMA, что блокировка может быть достигнута только тогда, когда целевой процесс находится в барьере, синхронизирующем весь коммуникатор. Тогда я мог бы просто пойти вместе со стандартными операциями Send/Recv. Чего я хочу добиться, так это того, чтобы процесс 0 работал все время, делегируя работу и имея возможность RMA ведомых устройств определить, кому он может делегировать. Может ли кто-нибудь помочь мне и объяснить, как я могу сделать перерыв в процессе 0, чтобы другие процессы могли получить блокировки?
Заранее спасибо!
ОБНОВЛЕНИЕ: Я не уверен, что вы когда-либо работали с блокировкой, и просто хочу подчеркнуть, что я вполне могу получить обновленную копию окна удаленной памяти. Если я получаю доступность от рабов, блокировка предоставляется только тогда, когда рабы ожидают в барьере. Итак, что я получил, так это то, что процесс 0 выполняет блокировку-получение-разблокировку, в то время как процессы 1 и 2 имитируют работу так, что процесс 2 заметно дольше, чем один. В результате я ожидаю, что процесс 0 печатает расписание (0,1,0), потому что процесс 0 вообще не запрашивается, работает ли он, процесс 1 выполняется, а процесс 2 все еще работает. На следующем шаге, когда процесс 2 готов, я ожидаю вывод (0,1,1), так как оба ведомых устройства готовы к новой работе. Что я получаю, так это то, что подчиненные блокируют блокировку для процесса 0 только тогда, когда они ожидают в барьере, так что первый и единственный вывод, который я получаю, является последним, на который я рассчитываю, показывая, что блокировка была предоставлена для каждого отдельного пользователя. процесс первый, когда это было сделано с его работой. Поэтому, если кто-то может сказать мне, когда целевым процессом может быть предоставлена блокировка, вместо того, чтобы пытаться запутать мои знания о пассивном RMA, я был бы очень признателен
2 ответа
Во-первых, механизм пассивного RMA каким-то образом волшебным образом не засовывает память удаленного процесса, так как не многие транспорты MPI имеют реальные возможности RDMA, и даже те, которые имеют (например, InfiniBand), требуют значительного не пассивного участия цель, чтобы позволить пассивным операциям RMA произойти. Это объясняется в стандарте MPI, но в очень абстрактной форме открытых и закрытых копий памяти, предоставляемых через окно RMA.
Достижение рабочего и портативного пассивного RMA с MPI-2 включает в себя несколько этапов.
Шаг 1: Распределение окон в целевом процессе
Из соображений портативности и производительности память для окна должна быть выделена с использованием MPI_ALLOC_MEM
:
int size;
MPI_Comm_rank(MPI_COMM_WORLD, &size);
int *schedule;
MPI_Alloc_mem(size * sizeof(int), MPI_INFO_NULL, &schedule);
for (int i = 0; i < size; i++)
{
schedule[i] = 0;
}
MPI_Win win;
MPI_Win_create(schedule, size * sizeof(int), sizeof(int), MPI_INFO_NULL,
MPI_COMM_WORLD, &win);
...
MPI_Win_free(win);
MPI_Free_mem(schedule);
Шаг 2: Синхронизация памяти на цели
Стандарт MPI запрещает одновременный доступ к одному и тому же местоположению в окне (§11.3 из спецификации MPI-2.2):
Ошибочно иметь одновременный конфликтующий доступ к одной и той же ячейке памяти в окне; если местоположение обновляется операцией размещения или накопления, то это местоположение не может быть доступно с помощью нагрузки или другой операции RMA, пока операция обновления не будет завершена в целевом объекте.
Поэтому каждый доступ к schedule[]
в цели должна быть защищена блокировка (разделяемая, поскольку она только читает местоположение в памяти):
while (!ready)
{
MPI_Win_lock(MPI_LOCK_SHARED, 0, 0, win);
ready = fnz(schedule, oldschedule, size);
MPI_Win_unlock(0, win);
}
Другая причина для блокировки окна на цели состоит в том, чтобы предоставить записи в библиотеку MPI и, таким образом, облегчить продвижение локальной части операции RMA. MPI обеспечивает переносимый RMA даже при использовании транспорта, не поддерживающего RDMA, например, TCP/IP или совместно используемой памяти, и для этого требуется много активной работы (называемой прогрессией) на цели для поддержки "пассивного" RMA. Некоторые библиотеки предоставляют потоки асинхронной прогрессии, которые могут выполнять операции в фоновом режиме, например, Open MPI при настройке с --enable-opal-multi-threads
(по умолчанию отключено), но использование такого поведения приводит к непереносимым программам. Вот почему стандарт MPI допускает следующую смягченную семантику операции put (§11.7, с. 365):
6 Обновление путем вызова пут или накопления для копии открытого окна становится видимым в частной копии в памяти процесса не позднее, когда владелец окна выполняет в этом окне последующий вызов MPI_WIN_WAIT, MPI_WIN_FENCE или MPI_WIN_LOCK.
Если доступ на основе накопления или накопления был синхронизирован с блокировкой, то обновление копии открытого окна завершается, как только процесс обновления выполняет MPI_WIN_UNLOCK. С другой стороны, обновление частной копии в памяти процесса может быть отложено до тех пор, пока целевой процесс не выполнит вызов синхронизации в этом окне (6). Таким образом, обновления памяти процесса всегда могут быть отложены до тех пор, пока процесс не выполнит подходящий вызов синхронизации. Обновление копии открытого окна также может быть отложено до тех пор, пока владелец окна не выполнит синхронизирующий вызов, если используется забор или синхронизация после запуска-завершения-ожидания. Только когда используется синхронизация блокировки, становится необходимым обновить копию открытого окна, даже если владелец окна не выполняет никакого связанного вызова синхронизации.
Это также показано в примере 11.12 в том же разделе стандарта (стр. 367). И действительно, и Open MPI, и Intel MPI не обновляют значение schedule[]
если вызовы блокировки / разблокировки в коде мастера закомментированы. Стандарт MPI далее рекомендует (§11.7, с. 366):
Совет пользователям. Пользователь может писать правильные программы, следуя следующим правилам:
...
блокировка: обновления окна защищены эксклюзивными блокировками, если они могут конфликтовать. Бесконфликтный доступ (например, доступ только для чтения или накопленный доступ) защищен общими блокировками как для локального доступа, так и для доступа RMA.
Шаг 3: Предоставление правильных параметров MPI_PUT
в начале координат
MPI_Put(&schedule[myrank],1,MPI_INT,0,0,1,MPI_INT,win);
будет передавать все в первый элемент целевого окна. Корректный вызов, учитывая, что окно на цели было создано с disp_unit == sizeof(int)
является:
int one = 1;
MPI_Put(&one, 1, MPI_INT, 0, rank, 1, MPI_INT, win);
Местное значение one
Таким образом, передается в rank * sizeof(int)
байты, следующие за началом окна в цели. Если disp_unit
был установлен в 1, правильное значение будет:
MPI_Put(&one, 1, MPI_INT, 0, rank * sizeof(int), 1, MPI_INT, win);
Шаг 4: Работа со спецификой реализации
Приведенная выше подробная программа работает из коробки с Intel MPI. С открытым MPI нужно быть особенно внимательным. Библиотека построена вокруг набора структур и реализующих модулей. osc
(односторонняя связь) фреймворк поставляется в двух реализациях - rdma
а также pt2pt
, По умолчанию (в Open MPI 1.6.x и, возможно, ранее) это rdma
и по какой-то причине он не выполняет операции RMA на целевой стороне, когда MPI_WIN_(UN)LOCK
вызывается, что приводит к тупиковому поведению, если не сделан другой коммуникационный вызов (MPI_BARRIER
в твоем случае). С другой стороны pt2pt
Модуль выполняет все операции, как ожидалось. Поэтому с открытым MPI нужно запустить программу, как указано ниже, чтобы конкретно выбрать pt2pt
составная часть:
$ mpiexec --mca osc pt2pt ...
Ниже приводится полностью рабочий пример кода C99:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <mpi.h>
// Compares schedule and oldschedule and prints schedule if different
// Also displays the time in seconds since the first invocation
int fnz (int *schedule, int *oldschedule, int size)
{
static double starttime = -1.0;
int diff = 0;
for (int i = 0; i < size; i++)
diff |= (schedule[i] != oldschedule[i]);
if (diff)
{
int res = 0;
if (starttime < 0.0) starttime = MPI_Wtime();
printf("[%6.3f] Schedule:", MPI_Wtime() - starttime);
for (int i = 0; i < size; i++)
{
printf("\t%d", schedule[i]);
res += schedule[i];
oldschedule[i] = schedule[i];
}
printf("\n");
return(res == size-1);
}
return 0;
}
int main (int argc, char **argv)
{
MPI_Win win;
int rank, size;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &size);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
if (rank == 0)
{
int *oldschedule = malloc(size * sizeof(int));
// Use MPI to allocate memory for the target window
int *schedule;
MPI_Alloc_mem(size * sizeof(int), MPI_INFO_NULL, &schedule);
for (int i = 0; i < size; i++)
{
schedule[i] = 0;
oldschedule[i] = -1;
}
// Create a window. Set the displacement unit to sizeof(int) to simplify
// the addressing at the originator processes
MPI_Win_create(schedule, size * sizeof(int), sizeof(int), MPI_INFO_NULL,
MPI_COMM_WORLD, &win);
int ready = 0;
while (!ready)
{
// Without the lock/unlock schedule stays forever filled with 0s
MPI_Win_lock(MPI_LOCK_SHARED, 0, 0, win);
ready = fnz(schedule, oldschedule, size);
MPI_Win_unlock(0, win);
}
printf("All workers checked in using RMA\n");
// Release the window
MPI_Win_free(&win);
// Free the allocated memory
MPI_Free_mem(schedule);
free(oldschedule);
printf("Master done\n");
}
else
{
int one = 1;
// Worker processes do not expose memory in the window
MPI_Win_create(NULL, 0, 1, MPI_INFO_NULL, MPI_COMM_WORLD, &win);
// Simulate some work based on the rank
sleep(2*rank);
// Register with the master
MPI_Win_lock(MPI_LOCK_EXCLUSIVE, 0, 0, win);
MPI_Put(&one, 1, MPI_INT, 0, rank, 1, MPI_INT, win);
MPI_Win_unlock(0, win);
printf("Worker %d finished RMA\n", rank);
// Release the window
MPI_Win_free(&win);
printf("Worker %d done\n", rank);
}
MPI_Finalize();
return 0;
}
Пример вывода с 6 процессами:
$ mpiexec --mca osc pt2pt -n 6 rma
[ 0.000] Schedule: 0 0 0 0 0 0
[ 1.995] Schedule: 0 1 0 0 0 0
Worker 1 finished RMA
[ 3.989] Schedule: 0 1 1 0 0 0
Worker 2 finished RMA
[ 5.988] Schedule: 0 1 1 1 0 0
Worker 3 finished RMA
[ 7.995] Schedule: 0 1 1 1 1 0
Worker 4 finished RMA
[ 9.988] Schedule: 0 1 1 1 1 1
All workers checked in using RMA
Worker 5 finished RMA
Worker 5 done
Worker 4 done
Worker 2 done
Worker 1 done
Worker 3 done
Master done
Ответ Христо Лиева отлично работает, если я использую более новые версии библиотеки Open-MPI.
Однако в кластере, который мы используем в настоящее время, это невозможно, и для более старых версий было зафиксировано тупиковое поведение для последних вызовов разблокировки, как описано в Hhristo. Добавление опций --mca osc pt2pt
действительно в тупике, но MPI_Win_unlock
вызовы все еще не завершались, пока процесс, владеющий доступной переменной, не сделал свою собственную блокировку / разблокировку окна. Это не очень полезно, если у вас есть задания с разным временем завершения.
Поэтому с прагматической точки зрения, хотя, строго говоря, оставляя тему пассивной синхронизации RMA (за что я извиняюсь), я хотел бы указать на обходной путь, который использует внешние файлы для тех, кто застрял с использованием старых версий библиотека Open-MPI, поэтому им не нужно терять столько времени, сколько я:
Вы в основном создаете внешний файл, содержащий информацию о том, какой (подчиненный) процесс выполняет какую работу, вместо внутреннего массива. Таким образом, вам даже не нужно иметь основной процесс, предназначенный только для бухгалтерии рабов: он также может выполнять работу. В любом случае, каждый процесс может пойти посмотреть в этом файле, какая работа должна быть выполнена дальше, и, возможно, определить, что все сделано.
Важным моментом является то, что к этому информационному файлу не обращаются одновременно несколько процессов, так как это может привести к дублированию работы или к ухудшению. Эквивалент блокировки и разблокировки окна в MPI здесь имитируется проще всего с помощью файла блокировки: этот файл создается процессом, который в данный момент обращается к информационному файлу. Другие процессы должны ждать завершения текущего процесса, с небольшой задержкой проверяя, существует ли файл блокировки.
Полную информацию можно найти здесь.