主要内容

对集群上的独立作业进行基准测试

在本例中,我们将展示如何使用集群上的独立作业对应用程序进行基准测试,并详细分析结果。具体而言,我们:

  • 演示如何对顺序代码和任务并行代码的混合进行基准测试。

  • 解释强缩放和弱缩放。

  • 讨论客户端和集群上的一些潜在瓶颈。

注意:如果在大型集群上运行此示例,可能需要一个小时才能运行。

相关例子:

这个例子中显示的代码可以在这个函数中找到:

函数paralleldemo_distribjob_bench

检查集群配置文件

在与集群交互之前,我们验证MATLAB®客户端是否根据我们的需要进行了配置。调用parcluster将提供一个使用默认配置文件的集群,如果默认配置文件不可用,将抛出一个错误。

myCluster = parcluster;

时机

我们将所有操作分开计时,以便我们能够详细地检查它们。我们将需要所有这些详细的时间来了解时间花在哪里,并隔离潜在的瓶颈。为了示例的目的,我们测试的实际函数不是很重要;在这种情况下,我们模拟纸牌游戏21点或21的手。

我们把所有的运算都写得尽可能高效。例如,我们使用向量化的任务创建。我们使用抽搐而且toc用于测量所有操作的运行时间,而不是使用作业和任务属性CreateDateTimeStartDateTimeFinishDateTime等,因为抽搐而且toc给我们亚秒级的粒度。请注意,我们还对任务函数进行了检测,以便它返回执行基准计算所花费的时间。

函数[times, description] = timeJob(myCluster, numTasks, numHands)创建作业及其任务的代码按顺序在% MATLAB客户端从这里开始。我们首先衡量创造一个工作需要多长时间。。timingStart = tic;开始= tic;job = createJob(myCluster);次了。jobCreateTime = toc(start);描述。jobCreateTime =“创造就业时间”在一次调用createTask中创建所有任务,并测量多长时间%那需要。开始= tic;taskArgs = repmat({{numHands, 1}}, numTasks, 1);createTask(job, @pctdemo_task_blackjack, 2, taskkargs);次了。taskCreateTime = toc(start);描述。taskCreateTime =“任务创建时间”测量将作业提交到集群所需的时间。开始= tic;提交(工作);次了。submitTime = toc(start);描述。submitTime =“工作提交时间”一旦作业提交,我们希望它的所有任务都执行完毕。%平行。我们测量所有任务开始所需的时间%并运行到完成。开始= tic;等待(工作);次了。jobWaitTime = toc(start);描述。jobWaitTime =“工作等待时间”%任务现在已经完成,所以我们再次执行顺序代码%在MATLAB客户端。我们计算找回所有文件所需的时间工作的结果。开始= tic;results = fetchOutputs(job);次了。resultsTime = toc(start);描述。resultsTime ='结果检索时间'验证作业运行时没有任何错误。。如果~isempty([job. tasks . error]) taskErrorMsgs = pctdemo_helper_getUniqueErrors(job);删除(工作);错误(“pctexample: distribjobbench: JobErrored”...'在执行任务时发生以下错误'...“执行:\ n \ n % s”), taskErrorMsgs);结束获取任务的执行时间。我们的任务函数返回这个%作为第二个输出参数。次了。exeTime = max([results{:,2}]);描述。exeTime =“任务执行时间”测量删除作业及其所有任务所需的时间。开始= tic;删除(工作);times.deleteTime = toc(start);description.deleteTime =“工作删除时间”度量从创建作业到此为止所花费的总时间%点。次了。totalTime = toc(timingStart);描述。totalTime =的总时间;次了。numTasks = numTasks;描述。numTasks =“任务数量”结束

我们来看看我们正在测量的一些细节:

  • 新增就业时间:创建一个工作所需的时间。对于MATLAB作业调度器集群,这涉及远程调用,并且MATLAB作业调度器在其数据库中分配空间。对于其他类型的集群,作业创建涉及将一些文件写入磁盘。

  • 任务创建时间:创建并保存任务信息所需的时间。MATLAB作业调度器将其保存在其数据库中,而其他集群类型将其保存在文件系统上的文件中。

  • 工作提交时间:提交作业所需的时间。对于MATLAB作业调度器集群,我们告诉它开始执行其数据库中的作业。我们要求其他集群类型执行我们创建的所有任务。

  • 作业等待时间:从作业提交到作业完成的等待时间。这包括在作业提交和作业完成之间发生的所有活动,例如:集群可能需要启动所有的worker并向worker发送任务信息;工作人员读取任务信息,执行任务功能。在MATLAB Job Scheduler集群的情况下,worker然后将任务结果发送到MATLAB Job Scheduler,后者将任务结果写入其数据库,而对于其他集群类型,worker将任务结果写入磁盘。

  • 任务执行时间:模拟21点所用的时间。我们使用任务函数来精确测量这个时间。此时间也包含在作业等待时间中。

  • 结果检索时间:将作业结果输入MATLAB客户端所需的时间。对于MATLAB作业调度器,我们从它的数据库中获取它们。对于其他类型的集群,我们从文件系统中读取它们。

  • 删除作业时间:删除所有作业和任务信息所需的时间。MATLAB作业调度器将其从数据库中删除。对于其他类型的集群,我们从文件系统中删除文件。

  • 总时间:执行以上所有操作所需的时间。

选择问题大小

我们知道,大多数集群都是为批量执行中期或长期运行的作业而设计的,因此我们故意让基准计算落在这个范围内。然而,我们不希望这个示例花费数小时来运行,因此我们选择问题大小,以便每个任务在我们的硬件上花费大约1分钟,然后我们重复计时测量几次以提高准确性。根据经验,如果任务中的计算时间远远少于一分钟,则应该考虑是否parfor比作业和任务更好地满足您的低延迟需求。

numHands = 1.2e6;numReps = 5;

我们通过运行不同数量的工人来探索加速,从1、2、4、8、16等开始,以尽可能多的工人结束。在这个例子中,我们假设我们有对集群进行基准测试的专用访问,并且集群的NumWorkers属性已正确设置。假设是这样,每个任务将立即在一个专用的worker上执行,因此我们可以将提交的任务数量与执行它们的worker数量等同起来。

numWorkers = myCluster。NumWorkers;如果isinf(numWorkers) || (numWorkers == 0) error(“pctexample: distribjobbench: InvalidNumWorkers”...“无法从集群中推断出工人的数量。”...将默认配置文件中的NumWorkers设置为...'一个非0或无穷大的值。']);结束numTasks = [pow2(0:ceil(log2(numWorkers) - 1)), numWorkers];

弱尺度测量

我们改变作业中的任务数量,并让每个任务执行固定数量的工作。这叫做弱扩展,这是我们真正最关心的,因为我们通常会扩展到集群来解决更大的问题。应该将其与本例后面显示的强伸缩性基准进行比较。基于弱伸缩的加速也称为按比例缩小的加速

流([“开始较弱的缩放时机。”...'总共提交了%d个作业。\n'), numReps *长度(numTasks));j = 1:长度(numTasks) n = numTasks(j);itr = 1:numReps [rep(itr), description] = timeJob(myCluster, n, numHands);% #好< AGROW >结束保留总时间最少的迭代。totalTime = [rep.totalTime];最快= find(totalTime == min(totalTime), 1);弱(j) = rep(最快);% #好< AGROW >流('包含%d个任务的任务等待时间:%f秒\n'...n,弱(j) .jobWaitTime);结束
启动弱缩放计时。总共提交了45个作业。包含1个任务的任务等待时间(s): 59.631733秒包含2个任务的任务等待时间(s): 60.717059秒包含4个任务的任务等待时间(s): 61.343568秒包含8个任务的任务等待时间(s): 60.759119秒包含16个任务的任务等待时间(s): 63.016560秒包含32个任务的任务等待时间(s): 64.615484秒包含64个任务等待时间(s): 66.581806秒包含128个任务的任务等待时间(s): 91.043285秒包含256个任务等待时间(s): 150.411704秒

顺序执行

我们测量计算的顺序执行时间。请注意,只有当它们具有相同的硬件和软件配置时,才应该将此时间与集群上的执行时间进行比较。

seqTime = inf;itr = 1:numReps start = tic;pctdemo_task_blackjack (numHands, 1);seqTime = min(seqTime, toc(start));结束流('顺序执行时间:%f秒\n', seqTime);
顺序执行时间:84.771630秒

基于弱伸缩和总执行时间的加速

我们首先看一下在不同数量的worker上运行所实现的总体加速。加速是基于用于计算的总时间,因此它包括代码的顺序部分和并行部分。

此加速曲线表示多个项目的能力,每个项目的权重未知:集群硬件、集群软件、客户端硬件、客户端软件以及客户端与集群之间的连接。因此,加速曲线并不代表其中任何一种,而是全部放在一起。

如果加速曲线满足您想要的性能目标,您就知道上述所有因素在这个特定的基准测试中都能很好地协同工作。然而,如果加速曲线没有达到你的目标,你不知道上面列出的许多因素中哪一个是罪魁祸首。甚至可能是应用程序并行化所采用的方法,而不是其他软件或硬件。

通常情况下,新手认为这张图就能完整地反映集群硬件或软件的性能。事实并非如此,人们总是需要意识到,这个图表并不能让我们得出任何关于潜在性能瓶颈的结论。

标题str = sprintf(['基于总执行时间\n的加速'...“注意:此图表不表示性能”...“瓶颈”]);pctdemo_plot_distribjob (“加速”(weak.numTasks),(弱。totalTime],...(1)疲软。totalTime titleStr);

详细图表,第1部分

我们稍微深入一点,看看在代码的各个步骤中所花费的时间。我们对弱扩展进行了基准测试,也就是说,我们创建的任务越多,我们执行的工作就越多。因此,随着任务数量的增加,任务输出数据的大小也会增加。考虑到这一点,我们希望我们创建的任务越多,需要花费的时间就越长:

  • 任务创建

  • 作业输出参数的检索

  • 工作破坏时间

我们没有理由相信,随着任务数量的增加,以下因素会增加:

  • 新增就业时间

毕竟,作业是在定义它的任何任务之前创建的,因此它没有理由随着任务的数量而变化。我们可能期望在就业创造时间中只看到一些随机波动。

pctdemo_plot_distribjob (“字段”,弱,描述,...“jobCreateTime”“taskCreateTime”“resultsTime”“deleteTime”},...“时间以秒为单位”);

归一化时代

我们已经得出结论,随着任务数量的增加,任务创建时间预计会增加,检索作业输出参数和删除作业的时间也会增加。然而,这种增长是由于我们在增加工人/任务数量时执行了更多的工作。因此,通过查看执行这些操作所花费的时间来衡量这三个活动的效率,并通过任务数量来规范化它是有意义的。通过这种方式,我们可以查看以下时间是否随着任务数量的变化而保持不变、增加或减少:

  • 创建单个任务所需的时间

  • 从单个任务检索输出参数所需的时间

  • 删除作业中的任务所需的时间

图中的归一化时间表示MATLAB客户端的能力以及可能与之交互的集群硬件或软件的部分。如果这些曲线保持平坦,通常被认为是好的,如果它们在下降,则是极好的。

pctdemo_plot_distribjob (“normalizedFields”,弱,描述,...“taskCreateTime”“resultsTime”“deleteTime”});

这些图表有时显示,随着任务数量的增加,检索每个任务结果所花费的时间会减少。这无疑是件好事:我们做的工作越多,效率就越高。如果操作的开销是固定的,并且作业中的每个任务花费的时间是固定的,就可能发生这种情况。

我们不能指望基于总执行时间的加速曲线看起来特别好,如果它包括花在如上顺序活动上的大量时间,其中所花的时间随着任务数量的增加而增加。在这种情况下,一旦有足够多的任务,顺序活动将占主导地位。

详细图表,第2部分

以下每个步骤所花费的时间可能会随着任务的数量而变化,但我们希望不会这样:

  • 作业提交时间。

  • 任务执行时间。这是模拟21点所花费的时间。不多不少。

在这两种情况下,我们都要查看经过的时间,也称为挂钟时间。我们既不查看集群上的总CPU时间,也不查看规范化时间。

pctdemo_plot_distribjob (“字段”,弱,描述,...“submitTime”“exeTime”});

在某些情况下,上面所示的每一个时间都可能随着任务数量的增加而增加。例如:

  • 对于某些第三方集群类型,作业提交涉及到作业中每个任务的一个系统调用,或者作业提交涉及到通过网络复制文件。在这些情况下,作业提交时间可能会随着任务数量线性增加。

  • 任务执行时间图最有可能暴露硬件限制和资源争用。例如,如果我们在同一台计算机上执行多个工作,由于争夺有限的内存带宽,任务执行时间可能会增加。资源争用的另一个例子是,如果任务函数使用单个共享文件系统读取或写入大型数据文件。但是,本例中的任务函数根本不访问文件系统。本例中详细介绍了这些类型的硬件限制任务并行问题中的资源争用

基于弱伸缩和作业等待时间的加速

现在我们已经剖析了代码的各个阶段所花费的时间,我们想要创建一个加速曲线,以更准确地反映集群硬件和软件的能力。我们通过计算基于作业等待时间的加速曲线来实现这一点。

在根据作业等待时间计算此加速曲线时,我们首先将其与在集群上执行单个任务的作业所需的时间进行比较。

titleStr =“基于与单个任务相比的任务等待时间的加速”;pctdemo_plot_distribjob (“加速”(weak.numTasks),(弱。jobWaitTime],...(1)疲软。jobWaitTime titleStr);

作业等待时间可能包括启动所有MATLAB工作线程的时间。因此,这个时间可能受到共享文件系统IO能力的限制。作业等待时间还包括平均任务执行时间,因此这里看到的任何不足也适用于此。如果我们没有对集群的专用访问,我们可以预期基于作业等待时间的加速曲线会受到显著影响。

接下来,我们将作业等待时间与顺序执行时间进行比较,假设客户端计算机的硬件与计算节点相当。如果客户端不能与集群节点进行比较,那么这种比较是毫无意义的。如果您的集群在向工人分配任务时有很大的时间延迟,例如,每分钟只向工人分配一次任务,则此图将受到严重影响,因为顺序执行时间不会受到这种延迟。请注意,这张图将具有与前一张图相同的形状,它们只会因一个常数的乘法因子而不同。

titleStr =与顺序时间相比,基于作业等待时间的加速;pctdemo_plot_distribjob (“加速”(weak.numTasks),(弱。jobWaitTime],...seqTime titleStr);

比较任务等待时间和任务执行时间

如前所述,作业等待时间由任务执行时间加上调度、集群队列中的等待时间、MATLAB启动时间等组成。在空闲集群中,任务等待时间和任务执行时间之间的差值应该保持不变,至少对于少量任务是这样。随着任务数量增长到数十个、数百个或数千个,我们最终必然会遇到一些限制。例如,一旦我们有足够多的任务/worker,集群就不能同时告诉所有的worker开始执行他们的任务,或者如果MATLAB worker都使用相同的文件系统,它们可能最终会使文件服务器饱和。

titleStr =“任务等待时间和任务执行时间之间的差异”;pctdemo_plot_distribjob (“barTime”(weak.numTasks),...(弱。-[弱。exeTime], titleStr);

强尺度测量

我们现在测量一个固定大小问题的执行时间,同时改变我们用来解决问题的worker的数量。这叫做强大的扩展,并且众所周知,如果应用程序有任何连续的部分,那么通过强大的伸缩性可以实现的加速是有上限的。这是正式的Amdahl法则多年来,这个问题一直被广泛讨论和争论。

在向集群提交作业时,由于具有强大的伸缩性,您很容易遇到加速的限制。如果任务执行有固定的开销(通常是这样),即使只有1秒,应用程序的执行时间也不会低于1秒。在我们的例子中,我们从一个应用程序开始,该应用程序在一个MATLAB worker上执行大约60秒。如果我们把计算分配给60个工人,每个工人可能只需要一秒钟就能计算出整个问题中属于自己的部分。然而,一秒钟的假设任务执行开销已经成为总体执行时间的主要贡献者。

除非您的应用程序运行了很长时间,否则作业和任务通常都不能通过强大的可伸缩性获得良好的结果。如果任务执行的开销接近应用程序的执行时间,则应该调查是否parfor满足您的要求。即使在这种情况下parfor,有固定数量的开销,尽管比常规的作业和任务要小得多,并且这些开销限制了通过强大的扩展可以实现的加速。您的问题大小相对于您的集群大小可能大到您会遇到这些限制,也可能不会。

一般的经验法则是,只有通过专门的硬件和大量的编程工作,才能在大量的处理器上实现小问题的强大伸缩性。

流([“开始了强劲的扩张时机。”...'总共提交了%d个作业。\n'), numReps *长度(numTasks))j = 1:长度(numTasks) n = numTasks(j);strongNumHands = cell (numHands/n);itr = 1:numReps rep(itr) = timeJob(myCluster, n, strongNumHands);结束找到;找到;totalTime] == min([rep.totalTime]), 1);强(n) =代表(ind);% #好< AGROW >流('包含%d个任务的任务等待时间:%f秒\n'...n,强(n) .jobWaitTime);结束
开始强缩放时机。总共提交了45个作业。包含1个任务的任务等待时间(s): 60.531446秒包含2个任务的任务等待时间(s): 31.745135秒包含4个任务的任务等待时间(s): 18.367432秒包含8个任务的任务等待时间(s): 11.172390秒包含16个任务的任务等待时间(s): 8.155608秒包含32个任务的任务等待时间(s): 6.298422秒包含64个任务的任务等待时间(s): 5.302715秒包含256个任务的任务等待时间(s): 49.428909秒

基于强伸缩性和总执行时间的加速

正如我们已经讨论过的,描述在MATLAB客户端中执行顺序代码所花费的时间和在集群上执行并行代码所花费的时间之和的加速曲线可能非常容易误导人。下图显示了在最坏情况下的强伸缩情况下的信息。我们故意选择原始问题相对于我们的簇大小非常小,这样加速曲线看起来就会很糟糕。集群硬件和软件的设计都没有考虑到这种用途。

标题str = sprintf(['基于总执行时间\n的加速'...“注意:此图表不表示性能”...“瓶颈”]);pctdemo_plot_distribjob (“加速”(strong.numTasks),...[strong.totalTime]。*[强劲。numTasks],强大的(1)。totalTime titleStr);

短任务的替代方案:PARFOR

强伸缩结果看起来不太好,因为我们故意使用作业和任务来执行短时间的计算。现在我们来看看是如何做到的parfor适用于同样的问题。请注意,我们在时间测量中不包括打开池所需的时间。

pool = parpool(numWorkers);parforTime = inf;strongNumHands = cell (numHands/numWorkers);itr = 1:numReps start = tic;r = cell(1, numWorkers);parfori = 1:numWorkers r{i} = pctdemo_task_blackjack(strongNumHands, 1);% #好< PFOUS >结束parforTime = min(parforTime, toc(start));结束删除(池);
启动并行池(parpool)使用'bigMJS'配置文件…与256名员工相连。分析文件并将文件传输给工作人员…完成。

基于PARFOR的强伸缩加速

最初的顺序计算大约需要一分钟,因此每个工作人员只需在大型集群上执行几秒钟的计算。因此,我们预计强大的缩放性能会更好parfor而不是工作和任务。

流('使用%d个工人的执行时间:%f秒\n'...numWorkers parforTime);流([“加速基于使用parfor的强缩放”...'%d工人:%f\n'], numWorkers, seqTime/parforTime);
使用256个工作人员的parr的执行时间:1.126914秒使用256个工作人员的parr的强伸缩加速:75.224557

总结

我们已经看到了弱伸缩和强伸缩之间的区别,并讨论了为什么我们更喜欢研究弱伸缩:它衡量了我们在集群上解决更大问题的能力(更多模拟、更多迭代、更多数据等)。本例中大量的图表和大量的细节也证明了一个事实,即基准测试不能归结为单个数字或单个图表。我们需要全面地了解应用程序性能是否可以归因于应用程序、集群硬件或软件,或者两者的组合。

我们还看到,对于短期计算,parfor可以是工作和任务的一个很好的选择。更多的基准测试结果使用parfor,见示例PARFOR使用21点的简单基准测试

结束