OpenMp 和 MPI 仅通过 MPI 没有加速

No speedup with OpenMp and MPI over MPI only

我已经阅读了我发现的所有相关问题,但我仍然找不到解决我的问题的方法,我有一个函数,有一个双 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 优化标志进行编译。

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

请编译测试。如果效果更好,我会详细解释。 顺便说一句,使用一些更积极的编译器选项也可能有所帮助。

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. 循环索引 ij 是在 for 循环语句之外声明和初始化的。这是两个级别的 error-prone:1/ 它可能会强制声明它们的并行范围 (private),而将它们放在 for 语句中会自动为它们提供正确的范围;和 2/ 您可以通过在循环外错误地重用索引来获得 mix-up。将它们移动到 for 语句中很容易。
  3. 您无缘无故地更改了并行区域内 j 循环的边界。您将不得不声明 end_j 私有。此外,它是进一步开发的潜在限制(例如 collapse(2) 指令的潜在使用),因为它违反了 OpenMP 中定义的 规范循环形式 的规则标准。因此,在并行区域之外定义一些 beg_ibeg_j 是有意义的,节省了计算并简化了循环的形式,使它们保持 规范 .

现在,代码适合向量化,如果编译器无法通过本身。

High-level分析

混合编程(例如 MPI+OpenMP)是一个好主意,这是一个常见的谬误。这个谬论得到了 so-called HPC 专家的广泛支持,他们中的许多人都是超级计算中心的论文推手,并没有写太多代码。 MPI+线程谬误的专家take-down是Exascale Computing without Threads.

这并不是说平面 MPI 是最好的模型。例如,MPI 专家在 Bi-modal MPI and MPI+threads Computing on Scalable Multicore Systems and MPI + MPI: a new hybrid approach to parallel programming with MPI plus shared memory (free version 中支持 two-level MPI-only 方法。在 so-called MPI+MPI 模型中,程序员利用 shared-memory 一致性域使用 MPI shared-memory 而不是 OpenMP 但使用 默认私有 数据模型,这减少了竞争条件的发生。此外,MPI+MPI 仅使用一个运行时系统,这使得资源管理和处理 topology/affinity 更加容易。相比之下,MPI+OpenMP 要求使用具有线程的基本 non-scalable fork-join 执行模型(即在 OpenMP 并行区域之间进行 MPI 调用)或启用 MPI_THREAD_MULTIPLE 以使 MPI线程区域内的调用 - MPI_THREAD_MULTIPLE 在当今的平台上需要显着的开销。

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

减少 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]);
        }
    }
}

上面的示例代码适用于以下编译器:

  • 海湾合作委员会 5.3.0
  • Clang-OpenMP3.5.0
  • Cray C 8.4.2
  • 英特尔 16.0.1。

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