Сравнительное тестирование A\b

Этот пример показывает, как протестировать решения в сравнении с эталоном линейной системы на кластере. Код MATLAB®, чтобы решить для x в A*x = b очень прост. Наиболее часто каждый использует матрицу, оставленную деление, также известное mldivide или оператор наклонной черты влево (\), чтобы вычислить x (то есть, x = A\b). Сравнительное тестирование производительности матрицы, оставленной деление на кластере, однако, не является столь же прямым.

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

Связанные примеры:

Код, показанный в этом примере, может быть найден в этой функции:

function results = paralleldemo_backslash_bench(memoryPerWorker)

Очень важно выбрать соответствующий матричный размер для кластера. Мы можем сделать это путем определения суммы системной памяти в Гбайт, доступном каждому рабочему как вход к этой функции, взятой в качестве примера. Значение по умолчанию очень консервативно; необходимо задать значение, которое подходит для системы.

if nargin == 0
    memoryPerWorker = 8.00; % In GB
%    warning('pctexample:backslashbench:BackslashBenchUsingDefaultMemory', ...
%            ['Amount of system memory available to each worker is ', ...
%             'not specified.  Using the conservative default value ', ...
%             'of %.2f gigabytes per worker.'], memoryPerWorker);
end

Предотвращение наверху

Чтобы получить точную меру нашей возможности решить линейные системы, мы должны удалить любой возможный источник издержек. Это включает получение текущего параллельного пула и временно отключение возможностей обнаружения мертвой блокировки.

p = gcp;
if isempty(p)
    error('pctexample:backslashbench:poolClosed', ...
        ['This example requires a parallel pool. ' ...
         'Manually start a pool using the parpool command or set ' ...
         'your parallel preferences to automatically start a pool.']);
end
poolSize = p.NumWorkers;
pctRunOnAll 'mpiSettings(''DeadlockDetection'', ''off'');'
Starting parallel pool (parpool) using the 'bigMJS' profile ... connected to 12 workers.

Функция сравнительного тестирования

Мы хотим протестировать в сравнении с эталоном матрицы, оставленной деление (\), а не стоимость ввода блока spmd, время, которое требуется, чтобы создать матрицу или другие параметры. Мы поэтому разделяем генерацию данных от решения линейной системы и измеряем только время, которое требуется, чтобы сделать последнего. Мы генерируем входные данные с помощью 2D циклического блоком codistributor, когда это - самая эффективная схема распределения решения линейной системы. Наше сравнительное тестирование затем состоит из измерения времени, требуются все рабочие, чтобы завершить решение линейной системы A*x = b. Снова, мы пытаемся удалить любой возможный источник издержек.

function [A, b] = getData(n)
    fprintf('Creating a matrix of size %d-by-%d.\n', n, n);
    spmd
        % Use the codistributor that usually gives the best performance
        % for solving linear systems.
        codistr = codistributor2dbc(codistributor2dbc.defaultLabGrid, ...
                                    codistributor2dbc.defaultBlockSize, ...
                                    'col');
        A = codistributed.rand(n, n, codistr);
        b = codistributed.rand(n, 1, codistr);
    end
end

function time = timeSolve(A, b)
    spmd
        tic;
        x = A\b; %#ok<NASGU> We don't need the value of x.
        time = gop(@max, toc); % Time for all to complete.
    end
    time = time{1};
end

Выбор проблемного размера

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

  • Несколько неэффективный для маленьких матриц

  • Довольно эффективный для больших матриц

  • Неэффективный, если матрицы являются слишком большими, чтобы поместиться в системную память и операционные системы, начинают подкачивать память диску

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

  • "Слишком маленькие" матрицы, чтобы иметь размер, 1000 на 1000

  • "Большие" матрицы, чтобы занять немного меньше чем 45% памяти, доступной каждому рабочему

  • "Слишком большие" матрицы занимают 50% или больше системной памяти, доступной каждому рабочему

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

Заметьте, что путем изменения проблемного размера согласно количеству рабочих, мы используем слабое масштабирование. Другие примеры сравнительного тестирования, такие как Простое Сравнительное тестирование PARFOR Используя Блэк джек и Сравнительное тестирование Независимых Заданий на Кластере, также используют слабое масштабирование. Когда те примеры тестируют вычислений параллели задачи в сравнении с эталоном, их слабое масштабирование состоит из создания количества итераций, пропорциональных количеству рабочих. Этот пример, однако, тестирует вычислений параллели данных в сравнении с эталоном, таким образом, мы связываем верхний предел размера матриц к количеству рабочих.

% Declare the matrix sizes ranging from 1000-by-1000 up to 45% of system
% memory available to each worker.
maxMemUsagePerWorker = 0.45*memoryPerWorker*1024^3; % In bytes.
maxMatSize = round(sqrt(maxMemUsagePerWorker*poolSize/8));
matSize = round(linspace(1000, maxMatSize, 5));

Сравнение производительности: гигафлопы

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

Путем генерации графиков, таких как они, мы можем ответить на вопросы, такие как:

  • Действительно ли наименьшие матрицы являются столь маленькими, что мы получаем низкую производительность?

  • Мы видим снижение производительности, когда матрица является столь большой, что она занимает 45% общей системной памяти?

  • Какова лучшая производительность, которой мы можем возможно достигнуть для данного количества рабочих?

  • Для которого матричные размеры делают 16 рабочих выполняют лучше, чем 8 рабочих?

  • Системная память ограничивает пиковую производительность?

Учитывая матричный размер, функция сравнительного тестирования создает матричный A и правую сторону b однажды, и затем решает A\b многократно, чтобы получить точную меру времени, которое требуется. Мы используем плавающее количество операций проблемы HPC, так, чтобы для n на n матрицы, мы считали операции с плавающей точкой как 2/3*n^3 + 3/2*n^2.

function gflops = benchFcn(n)
    numReps = 3;
    [A, b] = getData(n);
    time = inf;
    % We solve the linear system a few times and calculate the Gigaflops
    % based on the best time.
    for itr = 1:numReps
        tcurr = timeSolve(A, b);
        if itr == 1
            fprintf('Execution times: %f', tcurr);
        else
            fprintf(', %f', tcurr);
        end
        time = min(tcurr, time);
    end
    fprintf('\n');
    flop = 2/3*n^3 + 3/2*n^2;
    gflops = flop/time/1e9;
end

Выполнение сравнительных тестов

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

fprintf(['Starting benchmarks with %d different matrix sizes ranging\n' ...
         'from %d-by-%d to %d-by-%d.\n'], ...
        length(matSize), matSize(1), matSize(1), matSize(end), ...
        matSize(end));
gflops = zeros(size(matSize));
for i = 1:length(matSize)
    gflops(i) = benchFcn(matSize(i));
    fprintf('Gigaflops: %f\n\n', gflops(i));
end
results.matSize = matSize;
results.gflops = gflops;
Starting benchmarks with 5 different matrix sizes ranging
from 1000-by-1000 to 76146-by-76146.
Creating a matrix of size 1000-by-1000.
Analyzing and transferring files to the workers ...done.
Execution times: 1.038931, 0.592114, 0.575135
Gigaflops: 1.161756

Creating a matrix of size 19787-by-19787.
Execution times: 119.402579, 118.087116, 119.323904
Gigaflops: 43.741681

Creating a matrix of size 38573-by-38573.
Execution times: 552.256063, 549.088060, 555.753578
Gigaflops: 69.685485

Creating a matrix of size 57360-by-57360.
Execution times: 3580.232186, 3726.588242, 3113.261810
Gigaflops: 40.414533

Creating a matrix of size 76146-by-76146.
Execution times: 9261.720799, 9099.777287, 7968.750495
Gigaflops: 36.937936

Графический вывод производительности

Мы можем теперь построить результаты и выдержать сравнение с ожидаемым графиком, показанным выше.

fig = figure;
ax = axes('parent', fig);
plot(ax, matSize/1000, gflops);
lines = ax.Children;
lines.Marker = '+';
ylabel(ax, 'Gigaflops')
xlabel(ax, 'Matrix size in thousands')
titleStr = sprintf(['Solving A\\b for different matrix sizes on ' ...
                    '%d workers'], poolSize);
title(ax, titleStr, 'Interpreter', 'none');

Если результаты сравнительного теста не так хороши, как вы можете ожидать, вот некоторые вещи рассмотреть:

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

  • Если матрицы будут слишком маленькими или слишком большими для вашего кластера, получившаяся производительность будет плоха.

  • Если сетевые коммуникации будут медленными, на производительность сильно повлияют.

  • Если центральные процессоры и сетевые коммуникации оба очень быстры, но объем памяти ограничивается, возможно, что вы не можете протестировать в сравнении с эталоном с достаточно большими матрицами, чтобы полностью использовать доступные центральные процессоры и сетевую пропускную способность.

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

Сравнение различных количеств рабочих

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

Другие примеры, такие как Сравнительное тестирование Независимых Заданий на Кластере объяснили, что при сравнительном тестировании параллельных алгоритмов для различных количеств рабочих, каждый обычно использует слабое масштабирование. Таким образом, когда мы увеличиваем число рабочих, мы увеличиваем проблемный размер пропорционально. В случае покинутого деления матрицы мы должны показать дополнительный уход, потому что производительность деления зависит значительно от размера матрицы. Следующий код создает график производительности в Гигафлопах для всех матричных размеров, которые мы протестировали с и все различные количества рабочих, когда это дает нам самое подробное изображение показателей производительности матрицы, оставленной деление на этом конкретном кластере.

s = load('pctdemo_data_backslash.mat', 'workers4', 'workers8', ...
         'workers16', 'workers32', 'workers64');
fig = figure;
ax = axes('parent', fig);
plot(ax, s.workers4.matSize./1000, s.workers4.gflops, ...
     s.workers8.matSize./1000, s.workers8.gflops, ...
     s.workers16.matSize./1000, s.workers16.gflops, ...
     s.workers32.matSize./1000, s.workers32.gflops, ...
     s.workers64.matSize./1000, s.workers64.gflops);
lines = ax.Children;
set(lines, {'Marker'}, {'+'; 'o'; 'v'; '.'; '*'});
ylabel(ax, 'Gigaflops')
xlabel(ax, 'Matrix size in thousands')
title(ax, ...
      'Comparison data for solving A\\b on different numbers of workers');
legend('4 workers', '8 workers', '16 workers', '32 workers',  ...
       '64 workers', 'location', 'NorthWest');

Первая вещь, которую мы замечаем при рассмотрении графика выше, состоит в том, что 64 рабочих позволяют нам решать намного большие линейные системы уравнений, чем возможно только с 4 рабочими. Кроме того, мы видим, что, даже если бы можно было бы работать с матрицей размера 60,000 60,000 на 4 рабочих, мы получили бы производительность приблизительно только 10 гигафлопов. Таким образом, даже если бы у этих 4 рабочих была достаточная память, чтобы решить такую большую проблему, 64 рабочих, тем не менее, значительно превзошли бы их по характеристикам.

Смотря на наклон кривой для 4 рабочих, мы видим, что существует только скромное увеличение производительности между тремя самыми большими матричными размерами. При сравнении этого с более ранним графиком ожидаемой производительности A\b для различных матричных размеров мы приходим к заключению, что мы вполне близко к достижению пиковой производительности для 4 рабочих с матричным размером 7772 7772.

Смотря на кривую для 8 и 16 рабочих, мы видим, что производительность понижается для самого большого матричного размера, указывая, что мы рядом или уже исчерпали доступную системную память. Однако мы видим, что увеличение производительности между вторыми и третьими по величине матричными размерами очень скромно, указывая на какую-то устойчивость. Мы поэтому предугадываем, что при работе с 8 или 16 рабочими, скорее всего, не видели бы значительное увеличение Гигафлопов, если бы мы увеличили системную память и протестировали с большими матричными размерами.

Смотря на кривые для 32 и 64 рабочих, мы видим, что существует значительное увеличение производительности между вторыми и третьими по величине матричными размерами. Для 64 рабочих между двумя самыми большими матричными размерами существует также значительное увеличение производительности. Мы поэтому предугадываем, что у нас заканчивается системная память для 32 и 64 рабочих, прежде чем мы достигли пиковой производительности. Если это правильно, то добавление большей памяти компьютерам и позволило бы нам решать большие проблемы и выполнять лучше в тех больших матричных размерах.

Ускорение

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

peakPerf = [max(s.workers4.gflops), max(s.workers8.gflops), ...
            max(s.workers16.gflops), max(s.workers32.gflops), ...
            max(s.workers64.gflops)];
disp('Peak performance in Gigaflops for 4-64 workers:')
disp(peakPerf)

disp('Speedup when going from 4 workers to 8, 16, 32 and 64 workers:')
disp(peakPerf(2:end)/peakPerf(1))
Peak performance in Gigaflops for 4-64 workers:
   10.9319   23.2508   40.7157   73.5109  147.0693

Speedup when going from 4 workers to 8, 16, 32 and 64 workers:
    2.1269    3.7245    6.7244   13.4532

Мы поэтому приходим к заключению, что получаем ускорение приблизительно 13,5 при увеличении числа рабочих 16 сгибов, движении от 4 рабочих к 64. Как мы отметили выше, график производительности указывает, что мы можем смочь увеличить производительность на 64 рабочих (и таким образом улучшить ускорение еще больше) путем увеличения системной памяти на кластерных компьютерах.

Используемый кластер

Эти данные были сгенерированы с помощью 16 двухпроцессорных, octa-базовых компьютеров, каждого с 64 Гбайт памяти, соединенной с GigaBit Ethernet. При использовании 4 рабочих они были всеми на одиночном компьютере. Мы использовали 2 компьютера для 8 рабочих, 4 компьютера для 16 рабочих, и т.д.

Перевключение обнаружения мертвой блокировки

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

pctRunOnAll 'mpiSettings(''DeadlockDetection'', ''on'');'
end
ans = 

  struct with fields:

    matSize: [1000 19787 38573 57360 76146]
     gflops: [1.1618 43.7417 69.6855 40.4145 36.9379]