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

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

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

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

Благодаря https://stackoverflow.com/a/9938666/2965879 я смог воспользоваться этим, заказав блоки в обратном порядке, что побуждает MATLAB начинать сначала с самых больших.

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

Мое решение состоит в том, чтобы делать самые большие блоки последовательно (но распараллеливать на уровне записей!), что нормально, если накладные расходы на итерацию не имеют большого значения, соответственно. блоки не становятся слишком маленькими. Остальные блоки я потом делаю парфором. В идеале я бы позволил MATLAB решить, как с этим справиться, но поскольку вложенный цикл parfor теряет свой параллелизм, это не работает. Кроме того, объединить обе петли в одну (почти) невозможно.

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

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

Для 12 ядер (соответственно конфигурации вычислительного сервера, с которым я работал) я нашел разумный параметр в 100 записей на одного рабочего в качестве ограничения, но это не работает, когда количество ядер не меньше по сравнению с количеством блоков (например, 64 против 200).

Я пытался уменьшить количество ядер с разной мощностью (например, 1/2, 3/4), но это также не всегда работает. Затем я попытался сгруппировать блоки в пакеты и определить отсечку, когда записи превышают среднее значение для пакета, соответственно. количество партий, в которых они находятся от конца:

logical_sml = true(1,num_core); i = 0;
while all(logical_sml)
    i = i+1;
    m = mean(num_entr_asc(1:min(i*num_core,end))); % "asc" ~ ascending order 
    logical_sml = num_entr_asc(i*num_core+(1:num_core)) < i^(3/4)*m;  
        % if the small blocks were parallelised perfectly, i.e. all  
        % cores take the same time, the time would be proportional to  
        % i*m. To try to discount the different sizes (and imperfect  
        % parallelisation), we only scale with a power of i less than  
        % one to not end up with a few blocks which hold up the rest  
end  
num_block_big = num_block - (i+1)*num_core + sum(~logical_sml);

(Примечание: этот код не работает для векторов num_entr_asc, длина которых не кратна num_core, но я решил опустить конструкции min(...,end) для удобочитаемости.)

Я также опустил < max(...,...) для объединения обоих условий (т. е. вместе с минимальным количеством записей на одного работника), что необходимо, чтобы отсечка не была обнаружена слишком рано. Я немного думал о том, чтобы как-то использовать и дисперсию, но пока все попытки были неудовлетворительны.

Я был бы очень признателен, если бы у кого-то была хорошая идея, как решить эту проблему.
Спасибо, что прочитали этот очень длинный вопрос,
С уважением,
Аксель

Пс. Поскольку мой «Уважаемый stackoverflow», кажется, отфильтрован, позвольте мне поблагодарить за то, что я уже много раз находил здесь решение своего вопроса.


person Axel    schedule 07.11.2013    source источник
comment
Как говорится, лучшая благодарность - это проголосовать за ответ (я уверен, что вы делали это в тех случаях) :-)   -  person Luis Mendo    schedule 07.11.2013
comment
Не ответ, но большая часть вашего вопроса может быть сведена к следующему: «Мой вопрос сейчас о том, как лучше всего определить это отключение между последовательным и параллельным режимом [в циклах].   -  person coneyhelixlake    schedule 07.11.2013
comment
@ijkilchenko: Вы абсолютно правы, возможно, я слишком буквально воспринял совет быть конкретным... ;-)   -  person Axel    schedule 27.11.2013


Ответы (1)


Я придумал несколько удовлетворительное решение, поэтому, если кому-то интересно, я решил поделиться им. Я все еще был бы признателен за комментарии о том, как улучшить/доработать подход.

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

function c=est_cost_para(cost_blocks,cost_it,num_cores)
% Estimate cost of parallel computation

% Inputs:
%   cost_blocks: Estimate of cost per block in arbitrary units. For
%       consistency with the other code this must be in the reverse order
%       that the scheduler is fed, i.e. cost should be ascending!
%   cost_it:     Base cost of iteration (regardless of number of entries)
%       in the same units as cost_blocks.
%   num_cores:   Number of cores
%
% Output:
%   c: Estimated cost of parallel computation

num_blocks=numel(cost_blocks);
c=zeros(num_cores,1);

i=min(num_blocks,num_cores);
c(1:i)=cost_blocks(end-i+1:end)+cost_it;
while i<num_blocks
    i=i+1;
    [~,i_min]=min(c); % which core finished first; is fed with next block
    c(i_min)=c(i_min)+cost_blocks(end-i+1)+cost_it;
end

c=max(c);

end

Параметр cost_it для пустой итерации представляет собой грубую смесь множества различных побочных эффектов, которые предположительно можно разделить: стоимость пустой итерации в for/parfor-цикле (также может быть разной для каждого блока), а также начало -время работы соотв. передача данных parfor-контура (и, возможно, больше). Основная причина, по которой я собираю все вместе, заключается в том, что я не хочу оценивать/определять более детальные затраты.

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

% function i=cutoff_ser_para(cost_blocks,cost_it,num_cores)
% Determine cut-off between serial an parallel regime

% Inputs:
%   cost_blocks: Estimate of cost per block in arbitrary units. For
%       consistency with the other code this must be in the reverse order
%       that the scheduler is fed, i.e. cost should be ascending!
%   cost_it:     Base cost of iteration (regardless of number of entries)
%       in the same units as cost_blocks.
%   num_cores:   Number of cores
%
% Output:
%   i: Number of blocks to be calculated serially

num_blocks=numel(cost_blocks);
cost=zeros(num_blocks+1,2);

for i=0:num_blocks
    cost(i+1,1)=sum(cost_blocks(end-i+1:end))/num_cores + i*cost_it;
    cost(i+1,2)=est_cost_para(cost_blocks(1:end-i),cost_it,num_cores);
end

[~,i]=min(sum(cost,2));
i=i-1;

end

В частности, я не завышаю/не изменяю значение est_cost_para, которое предполагает (помимо cost_it) наиболее оптимистичное планирование. Я оставляю все как есть в основном потому, что не знаю, что сработает лучше всего. Чтобы быть консервативным (то есть избегать подачи слишком больших блоков в параллельный цикл), можно, конечно, добавить некоторый процент в качестве буфера или даже использовать мощность> 1, чтобы увеличить параллельную стоимость.

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

По сравнению с подходом в моем многословном вопросе я вижу два основных преимущества:

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

Конечно, асимптотическая сложность выше при постоянном вызове est_cost_para с его циклом while, но в моем случае (num_blocks<500) это абсолютно ничтожно мало.

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

person Axel    schedule 27.11.2013