OpenMp 和 MPI 仅通过 MPI 没有加速
No speedup with OpenMp and MPI over MPI only
我已经阅读了我发现的所有相关问题,但我仍然找不到解决我的问题的方法,我有一个函数,有一个双 for 循环, 是瓶颈 我的程序。
代码是根据 MPI 设计的:
- 有一个大矩阵,我分散在 p 个进程中。
- 每个进程现在都有一个子矩阵。
- 每个进程都在循环中调用
update()
。
- 当循环终止时,主进程收集结果。
现在,我想利用 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]);
}
}
}
编辑:关于我对代码所做的一些解释...
- 初始版本毫无理由地使用嵌套并行性(
parrallel
区域嵌套在另一个 parallel
区域中)。这可能会适得其反,我只是将其删除。
- 循环索引
i
和 j
是在 for
循环语句之外声明和初始化的。这是两个级别的 error-prone:1/ 它可能会强制声明它们的并行范围 (private
),而将它们放在 for
语句中会自动为它们提供正确的范围;和 2/ 您可以通过在循环外错误地重用索引来获得 mix-up。将它们移动到 for
语句中很容易。
- 您无缘无故地更改了并行区域内
j
循环的边界。您将不得不声明 end_j
私有。此外,它是进一步开发的潜在限制(例如 collapse(2)
指令的潜在使用),因为它违反了 OpenMP 中定义的 规范循环形式 的规则标准。因此,在并行区域之外定义一些 beg_i
和 beg_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
是两个结构的融合 - parallel
和 for
- 人们应该真正尝试独立思考它们。理想情况下,您创建一个包含许多工作共享结构的并行区域,例如 for
、sections
等
下面是您的代码修改为仅使用一个并行区域和跨两个 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 并不奇怪。不幸的是,我无法访问更新的版本。
我已经阅读了我发现的所有相关问题,但我仍然找不到解决我的问题的方法,我有一个函数,有一个双 for 循环, 是瓶颈 我的程序。
代码是根据 MPI 设计的:
- 有一个大矩阵,我分散在 p 个进程中。
- 每个进程现在都有一个子矩阵。
- 每个进程都在循环中调用
update()
。 - 当循环终止时,主进程收集结果。
现在,我想利用 update()
.
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]);
}
}
}
编辑:关于我对代码所做的一些解释...
- 初始版本毫无理由地使用嵌套并行性(
parrallel
区域嵌套在另一个parallel
区域中)。这可能会适得其反,我只是将其删除。 - 循环索引
i
和j
是在for
循环语句之外声明和初始化的。这是两个级别的 error-prone:1/ 它可能会强制声明它们的并行范围 (private
),而将它们放在for
语句中会自动为它们提供正确的范围;和 2/ 您可以通过在循环外错误地重用索引来获得 mix-up。将它们移动到for
语句中很容易。 - 您无缘无故地更改了并行区域内
j
循环的边界。您将不得不声明end_j
私有。此外,它是进一步开发的潜在限制(例如collapse(2)
指令的潜在使用),因为它违反了 OpenMP 中定义的 规范循环形式 的规则标准。因此,在并行区域之外定义一些beg_i
和beg_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
是两个结构的融合 - parallel
和 for
- 人们应该真正尝试独立思考它们。理想情况下,您创建一个包含许多工作共享结构的并行区域,例如 for
、sections
等
下面是您的代码修改为仅使用一个并行区域和跨两个 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 并不奇怪。不幸的是,我无法访问更新的版本。