В этом примере показано, как протестировать решения в сравнении с эталоном линейной системы в кластере. Код 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]