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