【问题标题】:No speedup with OpenMp and MPI over MPI only仅在 MPI 上使用 OpenMp 和 MPI 没有加速
【发布时间】:2023-03-28 08:35:01
【问题描述】:

我已经阅读了我发现的所有相关问题,但我仍然找不到解决问题的方法,我有一个带有双 for 循环的函数,是我的瓶颈程序。

代码是用 MPI 设计的:

  1. 有一个大矩阵,我分散在 p 个进程中。
  2. 现在每个进程都有一个子矩阵。
  3. 每个进程都在循环调用update()
  4. 当循环终止时,主进程收集结果。

现在我想通过利用 update() 的双 for 循环来使用 OpenMp 来增强我的 MPI 代码以加快执行速度。

void update (int pSqrt, int id, int subN, float** gridPtr, float ** gridNextPtr)
{
        int i = 1, j = 1, end_i = subN - 1, end_j = subN - 1;
        if ( id / pSqrt == 0) {
                i = 2;
                end_i = subN - 1;
        } else if ( id / pSqrt == (pSqrt - 1) ) {
                i = 1;
                end_i = subN - 2;
        }
        #pragma omp parallel for
        for ( ; i < end_i; ++i) {
                if (id % pSqrt == 0) {
                        j = 2;
                        end_j = subN - 1; 
                } else if ((id + 1) % pSqrt == 0) {
                        j = 1;
                        end_j = subN - 2;
                }
                #pragma omp parallel for
                for ( ; j < end_j; ++j) {
                        gridNextPtr[i][j] = gridPtr[i][j]  +
                          parms.cx * (gridPtr[i+1][j] +
                          gridPtr[i-1][j] -
                          2.0 * gridPtr[i][j]) +
                          parms.cy * (gridPtr[i][j+1] +
                          gridPtr[i][j-1] -
                          2.0 * gridPtr[i][j]);
                }
        }
}

我在 2 台计算机上运行此程序,每台计算机都有 2 个 CPU。我正在使用 4 个进程。但是,无论有无 OpenMp,我都看不到任何加速。请问有什么想法吗?我正在使用-O1 优化标志进行编译。

【问题讨论】:

  • @HighPerformanceMark 不,不是。我在第一个 for 循环之前就有了它,但在我研究了相关问题之后,我决定把它移到那里。但是,我在执行时间上没有看到任何变化。
  • 好的,然后@HighPerformanceMark 我可能已经解释了我读错的答案,所以我将#pragma 移到了for 循环上方。但是,无论有没有它,它仍然给我同样的时间。
  • 我更新了我的问题@HighPerformanceMark。现在好点了吗?
  • 我不确定你在说什么@HighPerformanceMark。丢弃与 MPI 相关的所有内容?但我想说的是,MPI+OpenMp 并不比 MPI 版本快(如我所料)?
  • 对于少量内核,混合并行程序不比纯 MPI 快是很正常的。对于纯 MPI 已经存在问题的大量节点,您应该寻找不同的缩放比例。

标签: c parallel-processing mpi openmp distributed-computing


【解决方案1】:

高级分析

混合编程(例如 MPI+OpenMP)是一个好主意是一个常见的谬误。这种谬论得到了所谓的 HPC 专家的广泛支持,其中许多人是超级计算中心的论文推动者,并且不会编写太多代码。 MPI+Threads 谬误的专家驳斥是Exascale Computing without Threads

这并不是说平面 MPI 是最好的模型。例如,MPI 专家在 Bi-modal MPI and MPI+threads Computing on Scalable Multicore SystemsMPI + MPI: a new hybrid approach to parallel programming with MPI plus shared memory (free version) 中支持两级 MPI-only 方法。在所谓的 MPI+MPI 模型中,程序员使用 MPI 共享内存而不是 OpenMP 但使用 默认私有 数据模型来利用共享内存一致性域,这减少竞争条件的发生。此外,MPI+MPI 仅使用一个运行时系统,这使得资源管理和进程拓扑/亲和性更容易。相比之下,MPI+OpenMP 要求使用从根本上讲不可扩展的带有线程的 fork-join 执行模型(即在 OpenMP 并行区域之间进行 MPI 调用)或启用MPI_THREAD_MULTIPLE 以便在线程区域内进行 MPI 调用 - @ 987654329@ 在当今的平台上会产生明显的开销。

这个话题可能涉及很多页,我暂时没有时间写,所以请查看引用的链接。

减少 OpenMP 运行时开销

MPI+OpenMP 性能不如纯 MPI 的一个原因是 OpenMP 运行时开销往往出现在太多地方。一种不必要的运行时开销来自嵌套并行。当一个嵌套在omp parallel 上的构造嵌套在另一个构造中时,就会发生嵌套并行。大多数程序员不知道并行区域是一种相对昂贵的结构,应该尽量减少它们。此外,omp parallel for 是两种结构的融合——parallelfor——人们应该真正尝试独立思考这些结构。理想情况下,您创建一个包含许多工作共享结构的并行区域,例如forsections 等。

下面是您的代码修改为仅使用一个并行区域和跨两个for 循环的并行性。因为collapse 需要完美的嵌套(两个for 循环之间没有任何东西),所以我不得不在里面移动一些代码。但是,没有什么能阻止编译器在 OpenMP 降低之后将这个循环不变量提升回来(这是一个编译器概念,您可以忽略它),因此代码可能仍然只执行 end_i 次,而不是 end_i*end_j 次。

更新:我已经修改了另一个答案中的代码来演示 collapse

有多种方法可以使用 OpenMP 并行化这两个循环。您可以在下面看到四个版本,它们都与 OpenMP 4 兼容。版本 1 可能是最好的,至少在当前的编译器中是这样。版本 2 使用折叠但不使用 simd(它与 Open 3 兼容)。版本 3 可能是最好的,但在理想情况下更难实现,并且不会导致使用某些编译器生成 SIMD 代码。版本 4 仅并行化外循环。

您应该试验一下,看看哪些选项对您的应用程序来说是最快的。

#if VERSION==1
#define OUTER _Pragma("omp parallel for")
#define INNER _Pragma("omp simd")
#elif VERSION==2
#define OUTER _Pragma("omp parallel for collapse(2)")
#define INNER
#elif VERSION==3
#define OUTER _Pragma("omp parallel for simd collapse(2)")
#define INNER
#elif VERSION==4
#define OUTER _Pragma("omp parallel for simd")
#define INNER
#else
#error Define VERSION
#define OUTER
#define INNER
#endif


struct {
    float cx;
    float cy;
} parms;

void update (int pSqrt, int id, int subN, const float * restrict gridPtr[restrict], float * restrict gridNextPtr[restrict])
{
    int beg_i = 1, beg_j = 1;
    int end_i = subN - 1, end_j = subN - 1;
    if ( id / pSqrt == 0 ) {
        beg_i = 2;
    } else if ( id / pSqrt == (pSqrt - 1) ) {
        end_i = subN - 2;
    }
    if (id % pSqrt == 0) {
        beg_j = 2;
    } else if ((id + 1) % pSqrt == 0) {
        end_j = subN - 2;
    }
    OUTER
    for ( int i = beg_i; i < end_i; ++i ) {
        INNER
        for ( int j = beg_j; j < end_j; ++j ) {
            gridNextPtr[i][j] = gridPtr[i][j] + parms.cx * (gridPtr[i+1][j] + gridPtr[i-1][j] - 2 * gridPtr[i][j])
                                              + parms.cy * (gridPtr[i][j+1] + gridPtr[i][j-1] - 2 * gridPtr[i][j]);
        }
    }
}

上面的示例代码在以下编译器中是正确的:

  • GCC 5.3.0
  • Clang-OpenMP 3.5.0
  • 克雷 C 8.4.2
  • 英特尔 16.0.1。

由于[restrict](将其替换为[] 就足够了)和OpenMP simd 子句,它不会与PGI 11.7 一起编译。与this presentation 不同,此编译器缺乏对C99 的完全支持。鉴于它是在 2011 年发布的,因此它不兼容 OpenMP 也就不足为奇了。不幸的是,我无法访问更新的版本。

【讨论】:

  • 感谢杰夫的精彩回答。但是,我正在使用 2 个进程执行此代码,但似乎还没有完成。有什么想法吗?
  • 代码可能需要private(i,j,end_i,end_j) 或类似的东西。不提供MCVE 的缺陷之一是第三方无法对其进行 V(erify) 或对其进行任何修改。
  • 我认识 Jeff,但代码很大。我在第二个 for 的旁边放置了一个 printf(),我得到了 i = 1, j = 3, end_i = 2049, end_j = 2049 i = 1, j = 3, end_i = 2049, end_j = 2049,我似乎在 [1, 3] 中获得了值并循环。我应该把private()放在哪里?就在collapse(2)旁边?
  • 是的。对于它的价值,LLNL tutorial 非常好。这是我对 OpenMP 语法的默认参考。
  • 随意将代码放在 Github 上,而不是试图在这里内联。
【解决方案2】:

这个版本(未测试)怎么样?

请编译并测试它。如果效果更好,我会解释更多。 顺便说一句,使用一些更激进的编译器选项也可能会有所帮助。

void update (int pSqrt, int id, int subN, float** gridPtr, float ** gridNextPtr)
{
    int beg_i = 1, beg_j = 1;
    int end_i = subN - 1, end_j = subN - 1;
    if ( id / pSqrt == 0 ) {
        beg_i = 2;
    } else if ( id / pSqrt == (pSqrt - 1) ) {
        end_i = subN - 2;
    }
    if (id % pSqrt == 0) {
        beg_j = 2;
    } else if ((id + 1) % pSqrt == 0) {
        end_j = subN - 2;
    }
    #pragma omp parallel for schedule(static)
    for ( int i = beg_i; i < end_i; ++i ) {
        #pragma omp simd
        for ( int j = beg_j; j < end_j; ++j ) {
            gridNextPtr[i][j] = gridPtr[i][j] +
                parms.cx * (gridPtr[i+1][j] +
                        gridPtr[i-1][j] -
                        2.0 * gridPtr[i][j]) +
                parms.cy * (gridPtr[i][j+1] +
                        gridPtr[i][j-1] -
                        2.0 * gridPtr[i][j]);
        }
    }
}

编辑:关于我对代码所做的一些解释...

  1. 最初的版本无缘无故地使用嵌套并行(parrallel 区域嵌套在另一个 parallel 区域中)。这可能是非常反效果的,我只是将其删除。
  2. 循环索引ijfor 循环语句之外声明和初始化。这在两个层面上容易出错: 1/ 它可能会强制声明它们的并行范围(private),而将它们放在for 语句中会自动给它们正确的范围;和 2/ 你可以通过错误地重用循环之外的索引来混淆。将它们移入 for 语句很容易。
  3. 您无缘无故地更改了并行区域内j 循环的边界。您必须将end_j 声明为私有。此外,它是进一步开发的潜在限制(例如可能使用 collapse(2) 指令),因为它违反了 OpenMP 标准中定义的规范循环形式的规则。因此,在并行区域之外定义一些 beg_ibeg_j 是有意义的,可以节省计算并简化循环的形式,保持它们规范

从那里开始,代码适合矢量化,如果编译器无法自行查看可能的矢量化,则在 j 循环上添加一个简单的 simd 指令将强制执行它。

【讨论】:

  • 感谢您的回答。我编译喜欢:mpicc -O1 -o myheat myheat.c -lm
  • 你用什么编译器?
  • 我使用了-std=c99 标志,现在可以了。我再也看不到加速了!
  • 您是否尝试过使用(错误的)schedule(static,1)?如果是这样,请尝试不使用。并尝试-O3 -mtune=native -march=native(假设您使用 GCC)
  • 谢谢 Gilles,你能解释一下吗?
猜你喜欢
  • 2018-09-29
  • 1970-01-01
  • 2012-06-16
  • 2011-01-10
  • 2015-03-26
  • 2012-02-04
  • 2012-10-20
  • 2021-03-01
  • 2016-02-08
相关资源
最近更新 更多