OpenMP:嵌套并行化有啥好处?

Posted

技术标签:

【中文标题】OpenMP:嵌套并行化有啥好处?【英文标题】:OpenMP: What is the benefit of nesting parallelizations?OpenMP:嵌套并行化有什么好处? 【发布时间】:2011-05-18 01:58:38 【问题描述】:

据我了解,#pragma omp parallel 及其变体基本上在多个并发线程中执行以下块,这对应于 CPU 的数量。当有嵌套的并行化——parallel for within parallel for、parallel function within parallel function等——内部并行化会发生什么?

我是 OpenMP 的新手,我想到的情况可能相当简单 - 将向量与矩阵相乘。这是在两个嵌套的 for 循环中完成的。假设 CPU 的数量小于向量中的元素数量,尝试并行运行内部循环有什么好处吗?总线程数会大于CPU数,还是内层循环顺序执行?

【问题讨论】:

【参考方案1】:

对于像密集线性代数这样的东西,所有潜在的并行性都已经暴露在一个很好的宽 for 循环中的一个地方,你不需要嵌套的并行性——如果你确实想防止出现这种情况(比如说) 非常窄的矩阵,其中前导维度可能小于核心数,那么您所需要的只是 collapse 指令,它在概念上将多个循环扁平化为一个。

嵌套并行性适用于并行性并非一次全部暴露的情况——假设您想要同时进行 2 个函数评估,每个函数评估都可以有效地利用 4 个内核,并且您有一个 8 内核系统。您在并行部分中调用该函数,并且在函数定义中还有一个额外的,比如说,parallel for。

【讨论】:

vector*matrix 是一个针对一般问题的具体示例:当 OpenMP 在外部块中创建的线程数已经覆盖所有内核时,它是否会费心创建更多线程?如果是这样,它不会增加一些调度开销吗?如果没有,是否有任何理由创建嵌套的并行块? OpenMP 将使用环境变量 (OMP_NUM_THREADS) 或使用编译指示选项 #pragma omp parallel num_threads(2) 或使用函数调用 omp_set_num_threads() 创建尽可能多的线程。默认值通常是运行时看到可用的内核数,这通常是您想要进行实际工作的线程数。使用矩阵向量乘法,您只需要 omp 并行 - 使用循环的默认静态调度,它会将其分解为 OMP_NUM_THREADS 个线程(默认情况下,它是核心数),一切都很好。 嵌套并行适用于您正在执行的操作的顶层可用的并行量远小于内核数的情况,并且您希望利用较低级别的并行确保你所有的核心都在做真正的工作。例如,上面的示例在代码主体中只有两个函数调用(或一般代码部分)可以同时完成,但在每个函数调用或代码部分中可以利用更多的并行性。跨度> 更直接地回答这个问题“当它在外部块中创建的线程数已经覆盖所有内核时,OpenMP 是否会费心创建更多线程?” - 是的。 OpenMP 每次创建的线程数与您告诉它的线程数一样多;默认为无嵌套并使用 OMP_NUM_THREADS 线程;如果你不告诉它那是什么,那将是你系统上的核心数量。如果允许嵌套,则默认为在每个级别创建 OMP_NUM_THREADS 个线程,这将超额订阅。但是您可以通过环境变量、pragma 行上的指令或函数来控制它。【参考方案2】:

(1) OpenMP 中的嵌套并行: http://docs.oracle.com/cd/E19205-01/819-5270/aewbc/index.html

您需要通过设置OMP_NESTEDomp_set_nested 来开启嵌套并行,因为许多实现默认关闭此功能,甚至有些实现并不完全支持嵌套并行。如果打开,无论何时遇到parallel for,OpenMP 都会创建OMP_NUM_THREADS 中定义的线程数。因此,如果是 2 级并行,则线程总数为 N^2,其中 N = OMP_NUM_THREADS

这种嵌套并行会导致超额订阅(即繁忙线程的数量大于核心),这可能会降低加速比。在递归调用嵌套并行的极端情况下,线程可能会膨胀(例如,创建 1000 个线程),并且计算机只是浪费时间进行上下文切换。在这种情况下,您可以通过设置omp_set_dynamic来动态控制线程数。

(2) 矩阵向量乘法示例:代码如下:

// Input:  A(N by M), B(M by 1)
// Output: C(N by 1)
for (int i = 0; i < N; ++i)
  for (int j = 0; j < M; ++j)
     C[i] += A[i][j] * B[j];

一般来说,并行化内部循环而外部循环是可能的,因为线程的分叉/连接开销是不好的。 (虽然许多 OpenMP 实现预先创建了线程,但它仍然需要一些将任务分派给线程并在 parallel-for 结束时调用隐式屏障)

您关心的是 N

但是,如果 N 足够大,则代码会导致超额订阅。我只是在考虑以下解决方案:

更改循环结构,以便仅存在 1 级循环。 (看起来可行) 特化代码:如果 N 很小,则做嵌套并行,否则不做。 与omp_set_dynamic 的嵌套并行性。但是,请确定omp_set_dynamic 是如何控制线程数和线程活动的。实施可能会有所不同。

【讨论】:

处理小N而不降低并行度的方法就是使用collapse; #pragma omp parallel for collapse; for (int i=0; i&lt;N; ++i) 等。这在概念上合并了 i 和 j 循环。至于另一个问题,“但是,如果 N 足够大,那么代码会导致超额订阅。” ——不,不会。如果你在 i 之后放置并行 for,主线程将执行 i 循环,并且每次 i 迭代你将执行一个 fork,划分 j-work,然后加入。 (尽管正如您所说,大多数 OpenMP 实现现在都将使用公共线程池。) 可能有些混乱。我说的是嵌套并行:嵌套并行循环主要会导致超额订阅。如果 N 等于或大于核心数(例如 n),则将在外部 for-i 循环中创建 n 个线程。然后,每个线程会在遇到for-j循环时再fork另外n个线程。因此,n*n 线程在 n 个内核上工作。您可以使用系统实用程序轻松检查它。 好的,很公平,这就是他的要求。但当然不会那样做;这正是崩溃的目的——在两个循环上并行而不产生开销。【参考方案3】:

在外层使用 NUM_THREADS(num_groups) 子句来设置要使用的线程数。如果您的外部循环的计数为 N,并且处理器或内核的数量为 num_cores,请使用 num_groups = min(N,num_cores)。在内部级别,您需要为每个线程组设置子线程数,以便子线程的总数等于内核数。因此,如果 num_cores = 8,N = 4,则 num_groups = 4。在较低级别,每个子线程应该使用 2 个线程(因为 2+2+2+2 = 8)所以使用 NUM_THREADS(2) 子句。您可以将子线程的数量收集到一个数组中,每个外部区域线程一个元素(带有 num_groups 个元素)。

此策略始终可以最佳利用您的核心。当 N = num_cores 时,子线程计数数组包含所有 1,因此内部循环实际上是串行的。

【讨论】:

以上是关于OpenMP:嵌套并行化有啥好处?的主要内容,如果未能解决你的问题,请参考以下文章

在 OpenMP 中并行化嵌套循环并使用更多线程执行内部循环

使用 OpenMP 在 C、C++ 中并行化嵌套 for 循环的几种方法之间的区别

OpenMP 如何处理嵌套循环?

OpenMP 嵌套循环任务并行性,计数器未给出正确结果

OpenMP 矩阵乘法嵌套循环

openMP 嵌套并行 for 循环与内部并行 for