В этом примере показано, как тестировать приложение с помощью независимых заданий в кластере, и мы анализируем результаты достаточно подробно. В частности, мы:
Демонстрационный ролик о тестировании комбинации последовательного кода и параллельного кода задачи.
Объясните сильное и слабое масштабирование.
Обсудите некоторые потенциальные узкие места как на клиенте, так и на кластере.
Примечание.Запуск этого примера в большом кластере может занять час.
Связанные примеры:
Код, показанный в этом примере, можно найти в следующей функции:
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 сохраняет его в своей базе данных, в то время как другие типы кластеров сохраняют его в файлах файловой системы.
Время отправки задания: время, необходимое для отправки задания. Для кластера планировщика заданий MATLAB мы говорим ему начать выполнение задания, которое у него есть в базе данных. Мы просим другие типы кластеров выполнять все созданные задачи.
Время ожидания задания: время ожидания после отправки задания до его завершения. Сюда входят все действия, которые выполняются между отправкой задания и завершением задания, например: кластеру может потребоваться запустить всех работников и отправить им информацию о задании; работники считывают информацию о задаче и выполняют функцию задачи. В случае кластера планировщика заданий 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'});

Бывают ситуации, когда каждый из указанных выше моментов времени может увеличиваться с увеличением количества задач. Например:
При некоторых типах кластеров сторонних производителей отправка задания включает в себя по одному системному вызову для каждой задачи в задании, или отправка задания включает копирование файлов по сети. В этих случаях время подачи заданий может увеличиваться линейно с числом задач.
График времени выполнения задачи наиболее вероятен для раскрытия аппаратных ограничений и конфликтов ресурсов. Например, время выполнения задачи может увеличиться, если мы выполняем несколько работников на одном компьютере из-за конкуренции за ограниченную пропускную способность памяти. Другим примером конкуренции за ресурсы является то, должна ли функция задачи считывать или записывать большие файлы данных с использованием одной общей файловой системы. Однако функция задачи в этом примере вообще не имеет доступа к файловой системе. Эти типы аппаратных ограничений подробно описаны в примере Конкуренция за ресурсы в задачах Parallel Problems.
Теперь, когда мы рассекли время, проведенное на различных стадиях нашего кода, мы хотим создать кривую ускорения, которая более точно отражает возможности нашего кластерного аппаратного и программного обеспечения. Мы делаем это, рассчитывая кривую ускорения на основе времени ожидания задания.
Рассчитывая эту кривую ускорения на основе времени ожидания задания, мы сначала сравниваем ее со временем, которое требуется для выполнения задания с одной задачей в кластере.
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