В этом примере мы покажем, как бенчмарить приложение с помощью независимых заданий на кластере, и довольно подробно проанализируем результаты. В частности, мы:
Покажите, как сравнить смесь последовательного кода и параллельного кода задачи.
Объясните сильное и слабое масштабирование.
Обсудите некоторые потенциальные узкие места как на клиенте, так и на кластере.
Примечание.Если запустить этот пример на большом кластере, то запуск может занять час.
Похожие примеры:
Код, показанный в этом примере, можно найти в этой функции:
function paralleldemo_distribjob_bench
Прежде чем взаимодействовать с кластером, мы проверяем, что клиент MATLAB ® сконфигурирован в соответствии с нашими потребностями. Вызывающие parcluster
выдаст нам кластер, использующий профиль по умолчанию, или выдаст ошибку, если значение по умолчанию недопустимо.
myCluster = parcluster;
Мы проводим все операции отдельно, чтобы мы могли их детально осмотреть. Нам понадобятся все эти подробные сроки, чтобы понять, где тратится время, и изолировать потенциальные узкие места. Для целей примера фактическая функция, которую мы проверяем, не очень важна; в этом случае мы моделируем руки блэкджека карточной игры или 21.
Мы пишем все операции, чтобы быть максимально эффективными. Для примера мы используем векторизованное создание задачи. Используем tic
и toc
для измерения прошедшего времени всех операций вместо использования свойств задания и задачи CreateTime
, StartTime
, FinishTime
и т.д., потому что tic
и toc
придать нам субсекундную зернистость. Обратите внимание, что мы также инструментализировали функцию задачи так, чтобы она возвращала время, потраченное на выполнение наших расчетов.
function [times, description] = timeJob(myCluster, numTasks, numHands) % The code that creates the job and its tasks executes sequentially in % the MATLAB client starts here. % We first measure how long it takes to create a job. timingStart = tic; start = tic; job = createJob(myCluster); times.jobCreateTime = toc(start); description.jobCreateTime = 'Job creation time'; % Create all the tasks in one call to createTask, and measure how long % that takes. start = tic; taskArgs = repmat({{numHands, 1}}, numTasks, 1); createTask(job, @pctdemo_task_blackjack, 2, taskArgs); times.taskCreateTime = toc(start); description.taskCreateTime = 'Task creation time'; % Measure how long it takes to submit the job to the cluster. start = tic; submit(job); times.submitTime = toc(start); description.submitTime = 'Job submission time'; % Once the job has been submitted, we hope all its tasks execute in % parallel. We measure how long it takes for all the tasks to start % and to run to completion. start = tic; wait(job); times.jobWaitTime = toc(start); description.jobWaitTime = 'Job wait time'; % Tasks have now completed, so we are again executing sequential code % in the MATLAB client. We measure how long it takes to retrieve all % the job results. start = tic; results = fetchOutputs(job); times.resultsTime = toc(start); description.resultsTime = 'Result retrieval time'; % Verify that the job ran without any errors. if ~isempty([job.Tasks.Error]) taskErrorMsgs = pctdemo_helper_getUniqueErrors(job); delete(job); error('pctexample:distribjobbench:JobErrored', ... ['The following error(s) occurred during task ' ... 'execution:\n\n%s'], taskErrorMsgs); end % Get the execution time of the tasks. Our task function returns this % as its second output argument. times.exeTime = max([results{:,2}]); description.exeTime = 'Task execution time'; % Measure how long it takes to delete the job and all its tasks. start = tic; delete(job); times.deleteTime = toc(start); description.deleteTime = 'Job deletion time'; % Measure the total time elapsed from creating the job up to this % point. times.totalTime = toc(timingStart); description.totalTime = 'Total time'; times.numTasks = numTasks; description.numTasks = 'Number of tasks'; end
Мы рассмотрим некоторые детали того, что мы измеряем:
Время создания рабочих мест: время это берет, чтобы создать работу. Для кластера планировщика заданий MATLAB это включает удаленный вызов, и планировщик заданий MATLAB выделяет пространство в своей основе данных. Для других типов кластеров создание заданий включает запись нескольких файлов на диск.
Время создания задачи: Время, необходимое для создания и сохранения информации о задаче. MATLAB Job Scheduler сохраняет это в своей основе данных, тогда как другие типы кластеров сохраняют его в файлах файловой системы.
Время подачи задания: Время отправки задания. Для кластера MATLAB Job Scheduler, мы говорим ему, чтобы он начал выполнять задание, которое он имеет в своей основе данных. Мы просим другие типы кластеров выполнить все задачи, которые мы создали.
Время ожидания работы: Время ожидания после подачи работы до завершения работы. Это включает в себя все действия, которые происходят между представлением задания и завершением задания, такие как: кластер может потребоваться запустить всех работников и отправить работникам информацию о задаче; рабочие считывают информацию о задаче и выполняют функцию задачи. В случае кластера планировщика заданий MATLAB рабочие лица затем отправляют результаты задания в планировщик заданий MATLAB, который записывает их в свою основу данных, в то время как для других типов кластеров рабочие записывают результаты задания на диск.
Время выполнения задачи: Время, потраченное на симуляцию блэкджека. Мы инструментуем функцию задачи, чтобы точно измерить это время. Это время также включается во время ожидания задания.
Время извлечения результатов: время, необходимое для переноса результатов задания в клиент MATLAB. Для планировщика заданий MATLAB мы получаем их из его основы данных. Для других типов кластеров мы читаем их из файла системы.
Время удаления задания: Время удаления всей информации о задании и задаче. Планировщик заданий MATLAB удаляет его из своей основы данных. Для других типов кластеров мы удаляем файлы из файловой системы.
Общее время: Время выполнения всего вышеперечисленного.
Мы знаем, что большинство кластеров предназначены для пакетного выполнения средних или длительных заданий, поэтому мы намеренно стараемся, чтобы наши эталонные расчеты попадали в эту область значений. Тем не менее, мы не хотим, чтобы этот пример взял часы, чтобы запустить, поэтому мы выбираем размер задачи, так что каждая задача занимает приблизительно 1 минуту на нашем оборудовании, и затем мы повторяем временные измерения несколько раз для повышения точности. Как правило, если ваши вычисления в задаче занимают гораздо меньше минуты, следует подумать, parfor
удовлетворяет ваши потребности с низкой задержкой лучше, чем задания и задачи.
numHands = 1.2e6; numReps = 5;
Мы исследуем ускорение, работая на другом количестве работников, начиная с 1, 2, 4, 8, 16 и т.д., и заканчивая таким количеством работников, которое мы можем использовать. В этом примере мы предполагаем, что у нас есть специальный доступ к кластеру для бенчмаркинга, и что NumWorkers
кластера свойство установлено правильно. Если предположить, что это так, каждая задача будет выполняться сразу на выделенном работнике, поэтому мы можем приравнять количество задач, которые мы отправляем, к количеству работников, которые их выполняют.
numWorkers = myCluster.NumWorkers ; if isinf(numWorkers) || (numWorkers == 0) error('pctexample:distribjobbench:InvalidNumWorkers', ... ['Cannot deduce the number of workers from the cluster. ' ... 'Set the NumWorkers on your default profile to be ' ... 'a value other than 0 or inf.']); end numTasks = [pow2(0:ceil(log2(numWorkers) - 1)), numWorkers];
Мы варьируем количество задач в задании, и должны, чтобы каждая задача выполняла фиксированный объем работы. Это называется слабым масштабированием, и это то, что нам действительно важно, потому что мы обычно масштабируемся до кластера, чтобы решить большие задачи. Его следует сравнить с сильными тестами масштабирования, показанными ниже в этом примере. Ускорение, основанное на слабом масштабировании, также известно как масштабированное ускорение.
fprintf(['Starting weak scaling timing. ' ... 'Submitting a total of %d jobs.\n'], numReps*length(numTasks)); for j = 1:length(numTasks) n = numTasks(j); for itr = 1:numReps [rep(itr), description] = timeJob(myCluster, n, numHands); %#ok<AGROW> end % Retain the iteration with the lowest total time. totalTime = [rep.totalTime]; fastest = find(totalTime == min(totalTime), 1); weak(j) = rep(fastest); %#ok<AGROW> fprintf('Job wait time with %d task(s): %f seconds\n', ... n, weak(j).jobWaitTime); end
Starting weak scaling timing. Submitting a total of 45 jobs. Job wait time with 1 task(s): 59.631733 seconds Job wait time with 2 task(s): 60.717059 seconds Job wait time with 4 task(s): 61.343568 seconds Job wait time with 8 task(s): 60.759119 seconds Job wait time with 16 task(s): 63.016560 seconds Job wait time with 32 task(s): 64.615484 seconds Job wait time with 64 task(s): 66.581806 seconds Job wait time with 128 task(s): 91.043285 seconds Job wait time with 256 task(s): 150.411704 seconds
Измеряем последовательное время выполнения расчетов. Обратите внимание, что это время должно сравниваться со временем выполнения в кластере, только если они имеют одинаковое оборудование и программное строение.
seqTime = inf; for itr = 1:numReps start = tic; pctdemo_task_blackjack(numHands, 1); seqTime = min(seqTime, toc(start)); end fprintf('Sequential execution time: %f seconds\n', seqTime);
Sequential execution time: 84.771630 seconds
Мы сначала рассмотрим общее ускорение, достигнутое за счет бега на разное количество рабочих. Ускорение основано на общем времени, используемом для расчетов, поэтому оно включает как последовательные так и параллельные фрагменты нашего кода.
Эта кривая ускорения представляет возможности нескольких элементов с неизвестными весами, сопоставленными с каждым из них: Оборудование кластера, программное обеспечение кластера, клиентское оборудование, клиентское программное обеспечение и соединение между клиентом и кластером. Поэтому кривая скорости не представляет какой-либо из этих, а все вместе взятые.
Если кривая скорости соответствует вашим желаемым эффективности целям, вы знаете, что все вышеупомянутые факторы хорошо работают вместе в этом конкретном бенчмарке. Однако, если кривая ускорения не достигает ваших целей, вы не знаете, какой из многих факторов, перечисленных выше, является наиболее виноватым. Может даже быть, что подход, принятый при параллелизации приложения, виноват, а не другое программное обеспечение или оборудование.
Слишком часто новички считают, что этот одиночный график даёт полное представление о эффективности их кластерного оборудования или программного обеспечения. Это действительно не так, и всегда нужно знать, что этот график не позволяет нам делать какие-либо выводы о потенциальных узких местах эффективности.
titleStr = sprintf(['Speedup based on total execution time\n' ... 'Note: This graph does not identify performance ' ... 'bottlenecks']); pctdemo_plot_distribjob('speedup', [weak.numTasks], [weak.totalTime], ... weak(1).totalTime, titleStr);
Мы копаем немного бит глубже и смотрим на время, проведенное в различных шагах нашего кода. Мы сопоставили слабое масштабирование, то есть чем больше задач мы создаем, тем больше работы мы выполняем. Поэтому размер выходных данных задачи увеличивается, когда мы увеличиваем количество задач. Учитывая это, мы ожидаем, что следующее займет больше времени, чем больше задач мы создаем:
Создание задач
Поиск выходных аргументов задания
Время уничтожения задания
У нас нет оснований полагать, что с количеством задач увеличивается следующее:
Время создания рабочих мест
Ведь задание создается до того, как мы определим любую из его задач, поэтому нет причин, почему оно должно варьироваться от количества задач. Мы можем ожидать увидеть только некоторые случайные колебания времени создания рабочих мест.
pctdemo_plot_distribjob('fields', weak, description, ... {'jobCreateTime', 'taskCreateTime', 'resultsTime', 'deleteTime'}, ... 'Time in seconds');
Мы уже пришли к выводу, что время создания задачи, как ожидается, увеличится, когда мы увеличим количество задач, как и время для извлечения выходных аргументов задания и удаления задания. Однако это увеличение связано с тем, что мы выполняем больше работы по мере увеличения количества работников/задач. Поэтому важно измерить эффективность этих трех видов деятельности путем анализа времени, необходимого для выполнения этих операций, и нормализовать их по количеству задач. Таким образом, мы можем посмотреть, остается ли любой из следующих раз постоянным, увеличиваем или уменьшаем, изменяя количество задач:
Время создания одной задачи
Время, необходимое для извлечения выходных аргументов из одной задачи
Время удаления задачи в задании
Нормированное время в этом графике представляет возможности клиента MATLAB и фрагмент оборудования или программного обеспечения кластера, с которыми он может взаимодействовать. Обычно считается хорошим, если эти кривые остаются плоскими, и превосходным, если они уменьшаются.
pctdemo_plot_distribjob('normalizedFields', weak, description, ... {'taskCreateTime', 'resultsTime', 'deleteTime'});
Эти графики иногда показывают, что время, потраченное на получение результатов по заданию, уменьшается, когда количество задач увеличивается. Это, несомненно, хорошо: Мы становимся более эффективными, чем больше работы мы выполняем. Это может произойти, если для операции существует фиксированный объем накладных расходов и требуется фиксированное количество времени на задачу в задании.
Мы не можем ожидать, что кривая ускорения, основанная на общем времени выполнения, будет выглядеть особенно хорошо, если она включает значительное количество времени, затраченного на последовательные действия, такие как выше, где время, затраченное на увеличения с количеством задач. В этом случае последовательная деятельность будет доминировать, когда будет достаточно много задач.
Не исключено, что время, проведенное в каждом из следующих шагов, варьируется от количества задач, но мы надеемся, что это не так:
Время подачи задания.
Время выполнения задачи. Это фиксирует время, потраченное на симуляцию блэкджека. Ничего больше, ничего меньше.
В обоих случаях мы смотрим на истекшее время, также называемое стенкой настенного синхроимпульса. Мы не смотрим ни на общее время центрального процессора в кластере, ни на нормированное время.
pctdemo_plot_distribjob('fields', weak, description, ... {'submitTime', 'exeTime'});
Существуют ситуации, когда каждый из показанных выше случаев мог бы увеличиться с количеством задач. Для примера:
С некоторыми типами сторонних кластеров отправка задания включает один системный вызов для каждой задачи в задании или отправка задания включает копирование файлов по сети. В этих случаях время представления задания может линейно увеличиться с количеством задач.
График времени выполнения задачи является наиболее вероятным, чтобы показать аппаратные ограничения и конкуренцию ресурсов. Например, время выполнения задачи может увеличиться, если мы выполняем несколько рабочих процессов на одном компьютере, из-за конкуренции за ограниченную пропускную способность памяти. Другой пример искажения ресурса - если функция задачи состояла в том, чтобы считать или записать большие файлы данных с помощью одной, общей файловой системы. Функция задачи в этом примере, однако, не получает доступа к файловой системе вообще. Эти типы оборудования ограничений подробно описаны в примере «Искажение ресурса в параллельных задачах задачи».
Теперь, когда мы рассмотрели время, проведенное на различных этапах нашего кода, мы хотим создать ускоренную кривую, которая более точно отражает возможности нашего оборудования и программного обеспечения кластера. Мы делаем это путем вычисления кривой скорости на основе времени ожидания задания.
При вычислении этой кривой скорости на основе времени ожидания задания мы сначала сравниваем ее с временем, которое требуется для выполнения задания с одной задачей в кластере.
titleStr = 'Speedup based on job wait time compared to one task'; pctdemo_plot_distribjob('speedup', [weak.numTasks], [weak.jobWaitTime], ... weak(1).jobWaitTime, titleStr);
Время ожидания задания может включать время запуска всех работников MATLAB. Поэтому возможно, что это время ограничено возможностями ввода-вывода совместно используемой файловой системы. Время ожидания задания также включает среднее время выполнения задачи, поэтому здесь также применяются любые обнаруженные там недостатки. Если у нас нет специального доступа к кластеру, мы можем ожидать, что кривая ускорения, основанная на времени ожидания задания, значительно пострадает.
Затем сравниваем время ожидания задания со временем последовательного выполнения, принимая, что оборудование клиентского компьютера сопоставимо с вычислительными узлами. Если клиент не сопоставим с узлами кластера, это сравнение абсолютно бессмысленно. Если ваш кластер имеет существенную задержку во времени при назначении задач работникам, например, путем назначения задач работникам только один раз в минуту, этот график будет сильно затронут, потому что время последовательного выполнения не страдает от этой задержки. Обратите внимание, что этот график будет иметь ту же форму, что и предыдущий график, они будут отличаться только постоянным мультипликативным фактором.
titleStr = 'Speedup based on job wait time compared to sequential time'; pctdemo_plot_distribjob('speedup', [weak.numTasks], [weak.jobWaitTime], ... seqTime, titleStr);
Как мы уже упоминали, время ожидания задания состоит из времени выполнения задачи плюс планирования, времени ожидания в очереди кластера, времени запуска MATLAB и т.д. В бездействующем кластере различие между временем ожидания задания и временем выполнения задачи должна оставаться постоянной, по крайней мере, для небольшого числа задач. Поскольку число задач увеличивается до десятков, сотен или тысяч, мы должны в конечном счете столкнуться с некоторыми ограничениями. Для примера, если у нас достаточно много задач/работников, кластер не может сказать всем работникам одновременно начать выполнять свою задачу, или если Работники MATLAB все используют один и тот же файл систему, они могут в конечном итоге насытить файл сервер.
titleStr = 'Difference between job wait time and task execution time'; pctdemo_plot_distribjob('barTime', [weak.numTasks], ... [weak.jobWaitTime] - [weak.exeTime], titleStr);
Теперь мы измеряем время выполнения задачи фиксированного размера, изменяя при этом количество работников, которых мы используем, чтобы решить проблему. Это называется сильным масштабированием, и хорошо известно, что, если приложение имеет какие-либо последовательные части, существует верхний предел скорости, которая может быть достигнута при сильном масштабировании. Это оформлено в законе Амдаля, который широко обсуждался и обсуждался на протяжении многих лет.
Вы можете легко столкнуться с пределами ускорения с сильным масштабированием при отправке заданий в кластер. Если выполнение задачи имеет фиксированные накладные расходы (которые оно обычно делает), даже если это составляет всего одну секунду, время выполнения нашего приложения никогда не опустится ниже одной секунды. В нашем случае мы начинаем с приложения, которое выполняется примерно за 60 секунд на одном работнике MATLAB. Если мы разделим расчеты на 60 рабочих, это может занять всего одну секунду для каждого рабочего, чтобы вычислить его фрагмент общей задачи. Однако гипотетические накладные расходы на выполнение задачи в одну секунду стали основным вкладчиком в общее время выполнения.
Если ваше приложение не работает в течение длительного времени, задания и задачи обычно не являются способом достичь хороших результатов при сильном масштабировании. Если накладные расходы на выполнение задачи близки ко времени выполнения вашего приложения, необходимо выяснить, parfor
ли соответствует вашим требованиям. Даже в случае
parfor
, существует фиксированное количество накладных расходов, хотя и намного меньшее, чем при обычных задачах и задачах, и эти пределы накладных расходов на ускорение, которые могут быть достигнуты при сильном масштабировании. Размер вашей задачи относительно размера кластера может быть настолько большим, что вы испытываете эти ограничения.
Как общее правило, можно достичь только сильного масштабирования небольших проблем на большом количестве процессоров со специализированным оборудованием и больших усилий по программированию.
fprintf(['Starting strong scaling timing. ' ... 'Submitting a total of %d jobs.\n'], numReps*length(numTasks)) for j = 1:length(numTasks) n = numTasks(j); strongNumHands = ceil(numHands/n); for itr = 1:numReps rep(itr) = timeJob(myCluster, n, strongNumHands); end ind = find([rep.totalTime] == min([rep.totalTime]), 1); strong(n) = rep(ind); %#ok<AGROW> fprintf('Job wait time with %d task(s): %f seconds\n', ... n, strong(n).jobWaitTime); end
Starting strong scaling timing. Submitting a total of 45 jobs. Job wait time with 1 task(s): 60.531446 seconds Job wait time with 2 task(s): 31.745135 seconds Job wait time with 4 task(s): 18.367432 seconds Job wait time with 8 task(s): 11.172390 seconds Job wait time with 16 task(s): 8.155608 seconds Job wait time with 32 task(s): 6.298422 seconds Job wait time with 64 task(s): 5.253394 seconds Job wait time with 128 task(s): 5.302715 seconds Job wait time with 256 task(s): 49.428909 seconds
Как мы уже обсуждали, кривые ускорения, которые изображают сумму времени, потраченного на выполнение последовательного кода в клиенте MATLAB, и время, выполняемое параллельным кодом в кластере, могут быть очень вводящими в заблуждение. Следующий график показывает эту информацию в худшем сценарии сильного масштабирования. Мы намеренно выбрали исходную задачу, чтобы быть настолько маленькой относительно нашего размера кластера, что кривая ускорения будет выглядеть плохо. Ни оборудование кластера, ни программное обеспечение не были разработаны с учетом такого рода использования.
titleStr = sprintf(['Speedup based on total execution time\n' ... 'Note: This graph does not identify performance ' ... 'bottlenecks']); pctdemo_plot_distribjob('speedup', [strong.numTasks], ... [strong.totalTime].*[strong.numTasks], strong(1).totalTime, titleStr);
Сильные результаты масштабирования не выглядели хорошо, потому что мы намеренно использовали задания и задачи для выполнения вычислений короткой длительности. Теперь мы рассмотрим, как parfor
применяется к этой же проблеме. Обратите внимание, что мы не включаем время, необходимое для открытия пула в наши измерения времени.
pool = parpool(numWorkers); parforTime = inf; strongNumHands = ceil(numHands/numWorkers); for itr = 1:numReps start = tic; r = cell(1, numWorkers); parfor i = 1:numWorkers r{i} = pctdemo_task_blackjack(strongNumHands, 1); %#ok<PFOUS> end parforTime = min(parforTime, toc(start)); end delete(pool);
Starting parallel pool (parpool) using the 'bigMJS' profile ... connected to 256 workers. Analyzing and transferring files to the workers ...done.
Исходные, последовательные вычисления заняли примерно одну минуту, поэтому каждому рабочему нужно выполнить всего несколько секунд расчетов на большом кластере. Поэтому мы ожидаем, что высокая эффективность масштабирования будет намного лучше с parfor
чем с заданиями и задачами.
fprintf('Execution time with parfor using %d workers: %f seconds\n', ... numWorkers, parforTime); fprintf(['Speedup based on strong scaling with parfor using ', ... '%d workers: %f\n'], numWorkers, seqTime/parforTime);
Execution time with parfor using 256 workers: 1.126914 seconds Speedup based on strong scaling with parfor using 256 workers: 75.224557
Мы видели различие между слабым и сильным масштабированием и обсуждали, почему мы предпочитаем смотреть на слабое масштабирование: Это измеряет нашу способность решать большие задачи на кластере (больше симуляций, больше итераций, больше данных и т.д.). Большое количество графиков и количество деталей в этом примере должны также быть свидетельством того, что бенчмарки не могут быть сведены к одному числу или одному графику. Нужно посмотреть всю картину, чтобы понять, можно ли отнести эффективность приложения к приложению, оборудованию или программному обеспечению кластера или к их комбинации.
Мы также видели, что для кратких вычислений, parfor
может быть отличной альтернативой рабочим местам и задачам. Для получения дополнительных результатов бенчмаркинга с помощью parfor
, см. пример Простой бенчмаркинг PARFOR с использованием Blackjack.
end