使用嵌套 'forall' 循环有什么好处或坏处

Are there any benefits or drawbacks to using nested 'forall' loops

我想知道使用嵌套 'forall' 循环的优缺点。我确实理解的一件事是 'forall' 将调用 'standalone' 或 'leader' 迭代器,这可能会或可能不会引发额外的并行性,即使跨多个语言环境也是如此。然而,生成的任务数量默认限制为 'here.maxTaskPar',因此我们只能获得这么多的并行度。如果两个 'forall' 循环都在分布式数据上,我可以看到支持使用嵌套 'forall' 语句的论点,但是如果它们都是本地的呢?当其中一个是本地的而另一个不是?

正如您所注意到的,这个问题的简短答案是 "it depends",因为 Chapel 的 forall 循环调用了任何人都可以编写的迭代器,因此可以做任何事情。但正如您还提到的那样,对于 Chapel 的许多标准类型,有一些控制执行策略的旋钮,如 Executing Chapel Programs::Controlling Degree of Data Parallelism 中所述,以及遵循的某些约定。我的其余答案将针对此类情况编写。

对于一个完全本地嵌套的 forall 循环,其中所有迭代都执行相似的工作量,您应该看不到使用嵌套 forall 循环之间的巨大差异:

forall i in 1..m do
  forall j in 1..n do
    var twoPi = 2*pi;

并为内部循环使用串行 for 循环:

forall i in 1..m do
  for j in 1..n do
    var twoPi = 2*pi;

正如我认为您所预料的那样,这样做的原因是外部 forall 循环将创建 dataParTasksPerLocale 个任务,其中此值默认为 here.numPUs()(当前语言环境或计算节点上的处理单元或核心)。然后,当每个内部循环开始 运行ning 时,如果 dataParIgnoreRunningTasksfalse,默认情况下,它的迭代器将注意到 dataParTasksPerLocale 已经是 运行 ning 等将避免创建额外的任务。结果是每个内部循环可能 运行 它的所有迭代都是连续的,因为它假设所有处理器内核都已经忙于 运行 宁任务。

现在,假设外循环的迭代负载极度不平衡,以至于一些外循环任务将在其他任务之前完成很长时间。例如,这是一个特别人为的循环,其中迭代的后半部分比前半部分做的工作少得多:

forall i in 1..m do
  if (i < m/2) then
    forall j in 1..n do
      var twoPi = 2*pi;

在这种情况下,任何迭代都在 m/2+1..m 范围内的任务很可能会先于那些迭代在 1..m/2 范围内的任务完成。假设这适用于一半的任务(这很可能适用于上述范围内的循环,其中任务往往被分配连续的迭代块)。这些任务应该很快完成。一旦发生这种情况,由另一半任务执行的每个内部循环 可能 看到少于 dataParTasksPerLocale / 2 的任务正在 运行ning 并创建额外的任务来执行他们的迭代。为什么我说 "may"?因为如果多个外循环任务同时 运行ning,就会有多个同时发生的内循环,每个内循环都会查询 运行ning 任务的数量并竞争创建 dataParTasksPerLocale - here.runningTasks() 个额外的任务, 因此有些人可能会并行执行其内部循环,而其他人则使用单个任务串行执行。

当然,这种 "inner loops may be parallelized" 行为甚至可能发生在比上述更真实的嵌套循环中,例如工作量可能因 i[=67 的值而发生巨大变化的嵌套循环=] 和 j:

forall i in 1..m do
  forall j in 1..n do
    computeForPoint(i,j);  // imagine the amount of work here varies significantly based on i and j

在任何不平衡的循环中,一些外循环任务可能先于其他任务完成,从而释放任务供后续内循环使用。在这种情况下,另一种选择是对外循环使用 Dynamic Iterator 以更好地保持外循环任务之间的工作平衡。请注意,即使在最平衡的循环中,也可能并非所有外循环任务都会同时完成,在这种情况下,最终的内循环实例可能会并行执行(这就是我在中使用 "likely" 的原因)最后一句话描述了我最初的平衡情况)。

在本地情况下,如果我只想使循环嵌套中的一个循环并行(并且两者都可以),我通常将其设置为外部循环,以最大程度地减少创建和销毁的任务数.也就是说,我通常会选择:

forall i in 1..m do
  for j in 1..n do
    ...

超过:

for i in 1..m do
  forall j in 1..n do
    ...

因为前者创建 ~dataParTasksPerLocale 任务而后者创建 ~m * dataParTasksPerLocale。或者,我可能会同时使用迭代器和 运行time 来避免创建过多的任务:

forall i in 1..m do
  forall j in 1..n do
    ...

但在许多情况下,"right" 的选择还可能取决于循环的行程次数、循环内的计算等。也就是说,不一定有一个放之四海而皆准的答案.

现在,转向分布式数据结构上的循环:从 Chapel 版本 1.17 开始,对于标准数组分布,这些数据结构上的串行循环始终在遇到循环的任务当前正在执行的当前语言环境中计算。相比之下,分布式数据结构上的 forall 循环在每个目标语言环境上至少创建一个任务,并且基于与上述本地情况相同的启发式方法,每个目标语言环境可能最多 dataParTasksPerLocale。因此,分布式数据结构上的循环通常应尽可能使用 forall 循环来优化局部性并提高创建可扩展代码的机会。