В этом примере показано, как выполнить сравнительный анализ решения линейной системы в кластере. Код 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 блок, время создания матрицы или другие параметры. Поэтому мы отделяем генерацию данных от решения линейной системы и измеряем только время, необходимое для выполнения последней. Мы производим входные данные, используя 2-й циклический блоком 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 с использованием Blackjack и бенчмаркинг независимых рабочих мест в кластере, также используют слабое масштабирование. Поскольку в этих примерах сравниваются параллельные вычисления задач, их слабое масштабирование состоит в том, чтобы сделать число итераций пропорциональным числу работников. Этот пример, однако, представляет собой параллельные вычисления данных сравнительного анализа, поэтому мы связываем верхний предел размера матриц с числом работников.
% 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 Challenge, так что для матрицы n-by-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, адаптированную для вашей сетевой настройки, и чтобы рабочие работали таким образом, чтобы как можно больше общения происходило через общую память. Однако объяснение того, как выявлять и решать эти типы проблем, выходит за рамки этого примера.
Теперь рассмотрим, как сравнивать различное количество работников, просматривая данные, полученные при выполнении этого примера, используя различное количество работников. Эти данные получены в кластере, отличном от указанного выше.
Другие примеры, такие как бенчмаркинг независимых рабочих мест в кластере, объясняют, что при бенчмаркинге параллельных алгоритмов для различного числа работников обычно используется слабое масштабирование. То есть по мере увеличения количества работников мы пропорционально увеличиваем размер проблемы. В случае левого деления матрицы мы должны проявлять дополнительную осторожность, потому что производительность деления в значительной степени зависит от размера матрицы. Следующий код создает график производительности в Gigaflops для всех размеров матрицы, которые мы проверили с и все различные числа работников, так как это дает нам наиболее подробную картину характеристик производительности матрицы левого деления в этом конкретном кластере.
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 Gigaflops. Таким образом, даже если бы у 4 рабочих было достаточно памяти для решения такой большой задачи, 64 работника тем не менее значительно превзошли бы их.
Глядя на наклон кривой для 4 работников, мы видим, что наблюдается лишь скромное увеличение производительности между тремя наибольшими размерами матрицы. Сравнение этого с более ранним графиком ожидаемой производительности A\b для различных размеров матрицы мы пришли к выводу, что мы довольно близки к достижению пиковой производительности для 4 работников с размером матрицы 7772-на-7772.
Глядя на кривую для 8 и 16 работников, мы видим, что производительность падает для наибольшего размера матрицы, что указывает на то, что мы близки или уже исчерпали доступную системную память. Однако мы видим, что увеличение производительности между вторым и третьим наибольшими размерами матрицы очень скромно, что указывает на некоторую стабильность. Поэтому мы предполагаем, что при работе с 8 или 16 работниками мы, скорее всего, не увидим значительного увеличения Gigaflops, если мы увеличим системную память и протестируем с большими размерами матрицы.
Рассматривая кривые для 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
Поэтому мы приходим к выводу, что при увеличении числа работников в 16 раз мы получаем ускорение приблизительно в 13,5 раза, увеличиваясь с 4 работников до 64. Как мы отметили выше, график производительности показывает, что мы можем увеличить производительность на 64 рабочих (и тем самым еще больше повысить скорость), увеличив системную память на кластерных компьютерах.
Эти данные были сгенерированы с помощью 16 двухпроцессорных восьмиядерных компьютеров, каждый с 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]