使用 mpirun 执行我的程序,性能下降很多
Performance degrades a lot by using mpirun to execute my program
我是 MPI 领域的新手。我使用英特尔数学核心函数库编写我的程序,我想按块计算矩阵矩阵乘法,这意味着我将大矩阵 X 沿列拆分为许多小矩阵,如下所示。我的矩阵很大,所以每次我只计算 (N, M) x (M, N) 我可以手动设置 M。
XX^T = X_1X_1^T + X_2X_2^T + ... + X_nX_n^T
我先设置总线程数为16,M等于1024,然后运行我的程序直接如下。我检查我的 cpu 状态,我发现 cpu 使用率为 1600%,这是正常的。
./MMNET_MPI --block 1024 --numThreads 16
但是,我尝试 运行 我的程序使用 MPI,如下所示。然后我发现 cpu 使用率只有 200-300%。奇怪的是,我将块号更改为 64,我可以将性能提高到 cpu 使用率 1200%。
mpirun -n 1 --bind-to none ./MMNET_MPI --block 1024 --numThreads 16
不知道是什么问题。 mpirun
似乎做了一些默认设置,这对我的程序有影响。以下是我的矩阵乘法代码的一部分。命令 #pragma omp parallel for
旨在从并行压缩格式中提取 N×M 的小矩阵。之后我使用 clubs_dgemv
来计算矩阵-矩阵乘法。
#include "MemoryUtils.h"
#include "Timer.h"
#include "omp.h"
#include <mpi.h>
#include <mkl.h>
#include <iostream>
using namespace std;
int main(int argc, char** argv) {
omp_set_num_threads(16);
Timer timer;
double start_time = timer.get_time();
MPI_Init(&argc, &argv);
int total_process;
int id;
MPI_Comm_size(MPI_COMM_WORLD, &total_process);
MPI_Comm_rank(MPI_COMM_WORLD, &id);
if (id == 0) {
cout << "========== Testing MPI properties for MMNET ==========" << endl;
}
cout << "Initialize the random matrix ..." << endl;
unsigned long N = 30000;
unsigned long M = 500000;
unsigned long snpsPerBlock = 1024;
auto* matrix = ALIGN_ALLOCATE_DOUBLES(N*M);
auto* vector = ALIGN_ALLOCATE_DOUBLES(N);
auto* result = ALIGN_ALLOCATE_DOUBLES(M);
auto *temp1 = ALIGN_ALLOCATE_DOUBLES(snpsPerBlock);
memset(result, 0, sizeof(double) * M);
cout << "Time for allocating is " << timer.update_time() << " sec" << endl;
memset(matrix, 1.1234, sizeof(double) * N * M);
memset(vector, 1.5678, sizeof(double) * N);
// #pragma omp parallel for
// for (unsigned long row = 0; row < N * M; row++) {
// matrix[row] = (double)rand() / RAND_MAX;
// }
// #pragma omp parallel for
// for (unsigned long row = 0; row < N; row++) {
// vector[row] = (double)rand() / RAND_MAX;
// }
cout << "Time for generating data is " << timer.update_time() << " sec" << endl;
cout << "Starting calculating..." << endl;
for (unsigned long m0 = 0; m0 < M; m0 += snpsPerBlock) {
uint64 snpsPerBLockCrop = std::min(M, m0 + snpsPerBlock) - m0;
auto* snpBlock = matrix + m0 * N;
MKL_INT row = N;
MKL_INT col = snpsPerBLockCrop;
double alpha = 1.0;
MKL_INT lda = N;
MKL_INT incx = 1;
double beta = 0.0;
MKL_INT incy = 1;
cblas_dgemv(CblasColMajor, CblasTrans, row, col, alpha, snpBlock, lda, vector, incx, beta, temp1, incy);
// compute XA
double beta1 = 1.0;
cblas_dgemv(CblasColMajor, CblasNoTrans, row, col, alpha, snpBlock, lda, temp1, incx, beta1, result, incy);
}
cout << "Time for computation is " << timer.update_time() << " sec" << endl;
ALIGN_FREE(matrix);
ALIGN_FREE(vector);
ALIGN_FREE(result);
ALIGN_FREE(temp1);
return 0;
}
我的cpu信息如下
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 44
On-line CPU(s) list: 0-43
Thread(s) per core: 1
Core(s) per socket: 22
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel(R) Xeon(R) Gold 6152 CPU @ 2.10GHz
Stepping: 4
CPU MHz: 1252.786
CPU max MHz: 2101.0000
CPU min MHz: 1000.0000
BogoMIPS: 4200.00
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 1024K
L3 cache: 30976K
NUMA node0 CPU(s): 0-21
NUMA node1 CPU(s): 22-43
默认情况下,MKL 实现了一些智能动态选择要使用的线程数。这是由变量 MKL_DYNAMIC
控制的,默认设置为 TRUE
。 MKL 的文档说明:
If you [sic] are able to detect the presence of MPI, but cannot determine if it has been called in a thread-safe mode (it is impossible to detect this with MPICH 1.2.x, for instance), and MKL_DYNAMIC
has not been changed from its default value of TRUE
, Intel MKL will run one thread.
由于您调用 MPI_Init()
而不是 MPI_Init_thread()
来初始化 MPI,您实际上是在请求单线程 MPI 级别 (MPI_THREAD_SINGLE
)。该库可以免费为您提供任何线程级别,并且会保守地坚持 MPI_THREAD_SINGLE
。您可以通过在初始化后调用 MPI_Query_thread(&provided)
来检查输出值是否大于 MPI_THREAD_SINGLE
.
由于您将 OpenMP 和线程化 MKL 与 MPI 混合使用,因此您真的应该通过调用 MPI_Init_thread()
:
告诉 MPI 在更高的线程支持级别进行初始化
int provided;
MPI_Init_thread(NULL, NULL, MPI_THREAD_MULTIPLE, &provided);
// This ensures that MPI actually provides MPI_THREAD_MULTIPLE
if (provided < MPI_THREAD_MULTIPLE) {
// Complain
}
(从技术上讲,如果您不从主线程外部进行 MPI 调用,您需要 MPI_THREAD_FUNNNELED
,但这不是 MKL 理解的线程安全模式)
即使您向 MPI 请求某些线程支持级别,也不能保证您一定会得到它,这就是为什么您必须检查所提供的级别的原因。此外,较旧的 Open MPI 版本必须显式地使用此类支持进行构建 - 默认情况下不支持 MPI_THREAD_MULTIPLE
进行构建,因为某些网络模块不是线程安全的。您可以通过 运行ning ompi_info
并查找与此类似的行来检查是否是这种情况:
Thread support: posix (MPI_THREAD_MULTIPLE: yes, OPAL support: yes, OMPI progress: no, ORTE progress: yes, Event lib: yes)
现在,即使 MPI 不提供比 MPI_THREAD_SINGLE
更高级别的线程支持,大多数不在主线程外进行 MPI 调用的线程软件 运行 也完全没问题,即,对于大多数 MPI 实现,MPI_THREAD_SINGLE
等同于 MPI_THREAD_FUNNELED
。在这种情况下,将 MKL_DYNAMIC
设置为 FALSE
应该会使 MKL 的行为与 运行 没有 mpirun
时的行为相同:
mpirun -x MKL_DYNAMIC=FALSE ...
无论如何,由于您的程序接受线程数作为参数,因此只需同时调用 mkl_set_num_threads()
和 omp_set_num_threads()
,不要依赖神奇的默认机制。
编辑:启用全线程支持会产生后果 - 延迟增加并且一些网络模块可能拒绝工作,例如旧版 Open MPI 中的 InfiniBand 模块,导致库悄悄切换到较慢的传输,例如 TCP/IP.更好地请求 MPI_THREAD_FUNNELED
并明确设置 MKL 和 OpenMP 线程的数量。
我是 MPI 领域的新手。我使用英特尔数学核心函数库编写我的程序,我想按块计算矩阵矩阵乘法,这意味着我将大矩阵 X 沿列拆分为许多小矩阵,如下所示。我的矩阵很大,所以每次我只计算 (N, M) x (M, N) 我可以手动设置 M。
XX^T = X_1X_1^T + X_2X_2^T + ... + X_nX_n^T
我先设置总线程数为16,M等于1024,然后运行我的程序直接如下。我检查我的 cpu 状态,我发现 cpu 使用率为 1600%,这是正常的。
./MMNET_MPI --block 1024 --numThreads 16
但是,我尝试 运行 我的程序使用 MPI,如下所示。然后我发现 cpu 使用率只有 200-300%。奇怪的是,我将块号更改为 64,我可以将性能提高到 cpu 使用率 1200%。
mpirun -n 1 --bind-to none ./MMNET_MPI --block 1024 --numThreads 16
不知道是什么问题。 mpirun
似乎做了一些默认设置,这对我的程序有影响。以下是我的矩阵乘法代码的一部分。命令 #pragma omp parallel for
旨在从并行压缩格式中提取 N×M 的小矩阵。之后我使用 clubs_dgemv
来计算矩阵-矩阵乘法。
#include "MemoryUtils.h"
#include "Timer.h"
#include "omp.h"
#include <mpi.h>
#include <mkl.h>
#include <iostream>
using namespace std;
int main(int argc, char** argv) {
omp_set_num_threads(16);
Timer timer;
double start_time = timer.get_time();
MPI_Init(&argc, &argv);
int total_process;
int id;
MPI_Comm_size(MPI_COMM_WORLD, &total_process);
MPI_Comm_rank(MPI_COMM_WORLD, &id);
if (id == 0) {
cout << "========== Testing MPI properties for MMNET ==========" << endl;
}
cout << "Initialize the random matrix ..." << endl;
unsigned long N = 30000;
unsigned long M = 500000;
unsigned long snpsPerBlock = 1024;
auto* matrix = ALIGN_ALLOCATE_DOUBLES(N*M);
auto* vector = ALIGN_ALLOCATE_DOUBLES(N);
auto* result = ALIGN_ALLOCATE_DOUBLES(M);
auto *temp1 = ALIGN_ALLOCATE_DOUBLES(snpsPerBlock);
memset(result, 0, sizeof(double) * M);
cout << "Time for allocating is " << timer.update_time() << " sec" << endl;
memset(matrix, 1.1234, sizeof(double) * N * M);
memset(vector, 1.5678, sizeof(double) * N);
// #pragma omp parallel for
// for (unsigned long row = 0; row < N * M; row++) {
// matrix[row] = (double)rand() / RAND_MAX;
// }
// #pragma omp parallel for
// for (unsigned long row = 0; row < N; row++) {
// vector[row] = (double)rand() / RAND_MAX;
// }
cout << "Time for generating data is " << timer.update_time() << " sec" << endl;
cout << "Starting calculating..." << endl;
for (unsigned long m0 = 0; m0 < M; m0 += snpsPerBlock) {
uint64 snpsPerBLockCrop = std::min(M, m0 + snpsPerBlock) - m0;
auto* snpBlock = matrix + m0 * N;
MKL_INT row = N;
MKL_INT col = snpsPerBLockCrop;
double alpha = 1.0;
MKL_INT lda = N;
MKL_INT incx = 1;
double beta = 0.0;
MKL_INT incy = 1;
cblas_dgemv(CblasColMajor, CblasTrans, row, col, alpha, snpBlock, lda, vector, incx, beta, temp1, incy);
// compute XA
double beta1 = 1.0;
cblas_dgemv(CblasColMajor, CblasNoTrans, row, col, alpha, snpBlock, lda, temp1, incx, beta1, result, incy);
}
cout << "Time for computation is " << timer.update_time() << " sec" << endl;
ALIGN_FREE(matrix);
ALIGN_FREE(vector);
ALIGN_FREE(result);
ALIGN_FREE(temp1);
return 0;
}
我的cpu信息如下
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 44
On-line CPU(s) list: 0-43
Thread(s) per core: 1
Core(s) per socket: 22
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel(R) Xeon(R) Gold 6152 CPU @ 2.10GHz
Stepping: 4
CPU MHz: 1252.786
CPU max MHz: 2101.0000
CPU min MHz: 1000.0000
BogoMIPS: 4200.00
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 1024K
L3 cache: 30976K
NUMA node0 CPU(s): 0-21
NUMA node1 CPU(s): 22-43
默认情况下,MKL 实现了一些智能动态选择要使用的线程数。这是由变量 MKL_DYNAMIC
控制的,默认设置为 TRUE
。 MKL 的文档说明:
If you [sic] are able to detect the presence of MPI, but cannot determine if it has been called in a thread-safe mode (it is impossible to detect this with MPICH 1.2.x, for instance), and
MKL_DYNAMIC
has not been changed from its default value ofTRUE
, Intel MKL will run one thread.
由于您调用 MPI_Init()
而不是 MPI_Init_thread()
来初始化 MPI,您实际上是在请求单线程 MPI 级别 (MPI_THREAD_SINGLE
)。该库可以免费为您提供任何线程级别,并且会保守地坚持 MPI_THREAD_SINGLE
。您可以通过在初始化后调用 MPI_Query_thread(&provided)
来检查输出值是否大于 MPI_THREAD_SINGLE
.
由于您将 OpenMP 和线程化 MKL 与 MPI 混合使用,因此您真的应该通过调用 MPI_Init_thread()
:
int provided;
MPI_Init_thread(NULL, NULL, MPI_THREAD_MULTIPLE, &provided);
// This ensures that MPI actually provides MPI_THREAD_MULTIPLE
if (provided < MPI_THREAD_MULTIPLE) {
// Complain
}
(从技术上讲,如果您不从主线程外部进行 MPI 调用,您需要 MPI_THREAD_FUNNNELED
,但这不是 MKL 理解的线程安全模式)
即使您向 MPI 请求某些线程支持级别,也不能保证您一定会得到它,这就是为什么您必须检查所提供的级别的原因。此外,较旧的 Open MPI 版本必须显式地使用此类支持进行构建 - 默认情况下不支持 MPI_THREAD_MULTIPLE
进行构建,因为某些网络模块不是线程安全的。您可以通过 运行ning ompi_info
并查找与此类似的行来检查是否是这种情况:
Thread support: posix (MPI_THREAD_MULTIPLE: yes, OPAL support: yes, OMPI progress: no, ORTE progress: yes, Event lib: yes)
现在,即使 MPI 不提供比 MPI_THREAD_SINGLE
更高级别的线程支持,大多数不在主线程外进行 MPI 调用的线程软件 运行 也完全没问题,即,对于大多数 MPI 实现,MPI_THREAD_SINGLE
等同于 MPI_THREAD_FUNNELED
。在这种情况下,将 MKL_DYNAMIC
设置为 FALSE
应该会使 MKL 的行为与 运行 没有 mpirun
时的行为相同:
mpirun -x MKL_DYNAMIC=FALSE ...
无论如何,由于您的程序接受线程数作为参数,因此只需同时调用 mkl_set_num_threads()
和 omp_set_num_threads()
,不要依赖神奇的默认机制。
编辑:启用全线程支持会产生后果 - 延迟增加并且一些网络模块可能拒绝工作,例如旧版 Open MPI 中的 InfiniBand 模块,导致库悄悄切换到较慢的传输,例如 TCP/IP.更好地请求 MPI_THREAD_FUNNELED
并明确设置 MKL 和 OpenMP 线程的数量。