Примеры с распределенными массивами

Введение

Распределение данных по процессам предназначено для ускорения счета и экономии памяти.
Параллельные вычисления с использованием распределенных массивов подробно описаны на сайте Matlab, в частности, в подразделе Working with Codistributed Arrays.

Распределенный массив состоит из сегментов (частей), каждый из которых размещен в рабочей области (workspace) соответствующего процесса (lab). При необходимости информацию о точном разбиении массива можно запросить с помощью функции getCodistributor (подробнее см. Obtaining information About the Array). Для просмотра реальных данных в локальном сегменте распределенного массива следует использовать функцию getLocalPart.

Возможен доступ к любому сегменту распределенного массива.
Доступ к локальному сегменту будет быстрее, чем к удаленному, поскольку последний требует посылки (функция labSend) и получения (функция labReceive) данных между процессами.
Содержимое распределенного массива можно собрать с помощью функции gather в один локальный массив, продублировав его на всех процессах или разместив только на одном процессе.

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

Существует 3 способа создания распределенного массива (см. Creating a Codistributed Array):
1) деление исходного массива на части,
2) построение из локальных частей (меньших массивов),
3) использование встроенных функций Matlab (типа rand, zeros, ...).

1) Деление массива на части может быть реализовано с помощью функции codistributed (Пример 1). При этом в рабочей области каждого процесса расположены исходный массив в своем полном объеме и соответствующий сегмент распределенного массива. Таким образом, этот способ хорош при наличии достаточного места в памяти для хранения тиражируемого (replicated) исходного массива.
Размерности исходного и распределенного массивов совпадают.

Деление массива может быть произведено по любому из его измерений.
По умолчанию в случае двумерного массива проводится горизонтальное разбиение, т.е. по столбцам, что выглядит естественно с учетом принятого в системе Matlab размещения матриц в памяти по столбцам (как в Фортране).

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

3) Для встроенных функций Matlab (типа rand, zeros, ...) можно создавать распределенный массив любого размера за один шаг (см. Using MATLAB Constructor Functions).

Вверх
Как можно использовать распределенные массивы

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

Даются возможные схемы решения таких задач с использованием распределенных массивов. Показан переход от исходной последовательной программы к параллельной с распределенными вычислениями. Соответствующие изменения выделены.
Замечание.
Хотя дистрибутивные массивы совместимы (в отличие от цикла parfor и GPU) с глобальными переменными, необходимо тщательно следить за тем, чтобы изменения глобальных переменных в процессе обработки одной порции данных не влияли на результаты обработки других порций, тем самым обеспечивая независимость вычислений частей дистрибутивных массивов.
Там, где допустимо, проще использовать цикл parfor или вычисления на GPU.

Вверх
Пример 1. Деление массива на части

Пусть требуется вычислить значения некоторой функции 10 вещественных переменных. Аргументом функции является 10-мерный вектор.
Значение функции необходимо вычислить для n точек (значений аргумента), заданных 10хn матрицей. Вычисление матрицы производится по некоторому заданному алгоритму и не требует много времени.

Обозначения.
my_func - вычисляемая функция
М - массив для аргументов функции my_func
F - массив для значений функции my_func
M_distr - распределенный массив для M (той же размерности)
F_distr - распределенный массив для F (той же размерности)

Исходная программа
Результирующая программа
function test_1()
% инициализация данных
n=1000;
...
% резервирование памяти
M = zeros(10,n);
F = zeros(1,n);
% заполнение матрицы М
...
function test_1()
% инициализация данных
n = 1000;
...
% резервирование памяти
M = zeros(10,n);
F = zeros(1,n);
% заполнение матрицы М
...
% создание распределенных массивов
% разбиение по умолчанию проводится
% по столбцам (2-му измерению)
M_distr = codistributed(M);
F_distr = codistributed(F);
% вычисление функции
for i = 1:n
  F(1,i) = my_func(M(:,i));
end
% вычисление функции
for i = drange(1:size(M_distr,2))
  F_distr(1,i) = my_func(M_distr(:,i));

end
% сбор результатов на 1-ом процессе
F = gather(F_distr,1);
save ('test_res_1.mat', 'F', 'M'); if labindex == 1
  save ('test_res_1.mat', 'F', 'M');
end
return
end
return
end

Вверх
Пример 2. Построение массива из частей.
Распределенный массив как объединение локальных массивов

Сформировать таблицу значений функции 2-х вещественных переменных ((x,y) --> F) на прямоугольной сетке размера 120x200, заданной векторами (x1:x2:x3 и y1:y2:y3). Существует алгоритм вычисления значения функции в точке , обозначенный my_func.

Обозначения.
my_func - вычисляемая функция 2-х переменных
F - массив для значений функции my_func
F_loc - локальный массив, который заполняется соответствующими значениями функции на каждом процессе и затем берется за основу (рассматривается как сегмент) при построении распределенного массива F_distr
F_distr - распределенный массив, содержимое которого собирается в массиве F на 1-ом процессе

Вариант 1.
Распараллеливание проводится по внешнему циклу (по x - первому индексу).
Предполагается, что число строк 120 кратно numlabs - числу процессов, заказанных при запуске программы на счет.

Исходная программа
Результирующая программа
function test_2()
% инициализация данных,
% в частности,
% x1, x2, x3, y1, y2, y3
...
function test_2()
% инициализация данных,
% в частности,
% x1, x2, x3, y1, y2, y3
...
m_x = x1:x2:x3;
n = 120/numlabs;
% выделение памяти
F = zeros(120,200);
% вычисление функции
i = 1;
for x = x1:x2:x3
  ...
% выделение памяти
F_loc = zeros(n,200);
% вычисление функции
% i = 1;
for i = 1:n
  k = (labindex-1)*n + i;
  x = m_x(k);

  ...
  j = 1;
  for y = y1:y2:y3
    F(i,j) = my_func(x,y);
    j = j + 1;
  end
  i = i + 1;
end
  j = 1;
  for y = y1:y2:y3
    F_loc(i,j) = my_func(x,y);
    j = j + 1;
  end
%   i = i + 1;
end
% распараллеливание проведено по строкам
% (1-му измерению) => codistributor1d(1, ...)
codist = codistributor1d(1, [], [120 200]);
F_distr = codistributed.build(F_loc, codist);
F = gather(F_distr, 1);
save ('test_res_2.mat', 'F'); if labindex == 1
  save ('test_res_2.mat', 'F');
end
return
end
return
end

Вариант 2.
Распараллеливание проводится по внутреннему циклу (по y - второму индексу).
Предполагается, что число столбцов 200 кратно numlabs - числу процессов, заказанных при запуске программы на счет.

Исходная программа
Результирующая программа
function test_2()
% инициализация данных,
% в частности,
% x1, x2, x3, y1, y2, y3
...
function test_2()
% инициализация данных,
% в частности,
% x1, x2, x3, y1, y2, y3
...
m_y = y1:y2:y3;
n = 200/numlabs;
% выделение памяти
F = zeros(120,200);
% вычисление функции
i = 1;
for x = x1:x2:x3
  ...
  j = 1;
  for y = y1:y2:y3
% выделение памяти
F_loc = zeros(120,n);
% вычисление функции
i = 1;
for x = x1:x2:x3
  ...
%   j = 1;
  for j = 1:n
    k = (labindex-1)*n + j;
    y = m_y(k);
    F(i,j) = my_func(x,y);
    j = j + 1;
  end
  i = i + 1;
end
    F_loc(i,j) = my_func(x,y);
%     j = j + 1;
  end
  i = i + 1;
end
% распараллеливание проведено по столбцам
% (2-му измерению) => codistributor1d(2, ...)
codist = codistributor1d(2, [], [120 200]);
F_distr = codistributed.build(F_loc, codist);
F = gather(F_distr, 1);
save ('test_res_2.mat', 'F'); if labindex == 1
  save ('test_res_2.mat', 'F');
end
return
end
return
end

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

Вверх
Как убедиться в работоспособности программы

Перед первым запуском программы-функции на кластере ее стоит проверить сначала в однопроцессорном варианте с отладчиком Debug (см.,например, Debug a MATLAB Program), а затем в параллельном режиме pmode. Поскольку политика использования вычислительных ресурсов ИММ УрО РАН не предполагает длительных вычислений на управляющем компьютере, запускать программу в режимах Debug и pmode следует с тестовым набором данных.

Итак, рекомендуется следующая последовательность выхода на счет:
    Debug
    pmode
    кластер

Запуск параллельной программы с отладчиком Debug (в однопроцессорном режиме) можно осуществить, например, предварительно открыв текст программы в редакторе (Editor), установив необходимые контрольные точки (щелкая, к примеру, левой клавишей мыши справа от номера нужной строки) и нажав клавишу F5 (см. пункт меню Debug).

Стартовать режим pmode на 4-х процессах (для наглядности) можно в окне Matlab (Command Window) с помощью команды
    pmode start 4

Затем в командной строке открывшегося параллельного окна (Parallel Command Window) вызвать свою программу.
Закрыть параллельный режим можно или из параллельного окна командой
    exit

или из окна Matlab командой
    pmode exit

Для контроля за данными полезна функция getLocalPart.

Запуск на кластере можно осуществить в окне Matlab с помощью функции imm_sch.