基于MPI_Send/Recv的C++矩阵分块并行代码并行性问询
C++ MPI并行矩阵块乘法代码问题分析
问题背景
- 实现目标:基于MPI编写分布式矩阵块乘法程序,其中rank 0作为纯协调节点不参与计算,负责向其余计算进程分发存储在向量A、B中的自定义矩阵块,待所有计算完成后收集结果输出全局矩阵。由于自定义矩阵类型无法直接使用
MPI_Scatter分发,代码采用MPI_Send/MPI_Recv点对点通信实现数据传输。 - 核心疑问:当每个计算进程分配的矩阵块数
local_N_blocks = N_blocks / (size - 1)大于1时,rank 0会连续向同一个目标进程发送多组矩阵块,此时是否需要等待目标进程完成上一块的乘法计算才能继续发送?现有代码是否存在并行设计缺陷、并行效率不足的问题?
原实现代码
// rank 0 sends the blocks to the other ranks, which compute the local // block products, then receive the partial results and prints the global // vector if (rank == 0) { // send data for (unsigned j = 0; j < N_blocks; ++j) { int dest = j / local_N_blocks + 1; // send number of rows unsigned n = A[j].rows(); MPI_Send(&n, 1, MPI_UNSIGNED, dest, 1, MPI_COMM_WORLD); // send blocks MPI_Send(A[j].data(), n*n, MPI_DOUBLE, dest, 2, MPI_COMM_WORLD); MPI_Send(B[j].data(), n*n, MPI_DOUBLE, dest, 3, MPI_COMM_WORLD); } // global vector std::vector<dense_matrix> C(N_blocks); for (unsigned j = 0; j < N_blocks; ++j) { int root = j / local_N_blocks + 1; // receive number of rows unsigned n; MPI_Recv(&n, 1, MPI_UNSIGNED, root, 4, MPI_COMM_WORLD, MPI_STATUS_IGNORE); // initialize blocks dense_matrix received(n,n); // receive blocks MPI_Recv(received.data(), n*n, MPI_DOUBLE, root, 5, MPI_COMM_WORLD, MPI_STATUS_IGNORE); // store block in the vector C[j] = received; } // print result print_matrix(C); } // all the other ranks receive the blocks and compute the local block // products, then send the results to rank 0 else { // local vector std::vector<dense_matrix> local_C(local_N_blocks); // receive data and compute products for (unsigned j = 0; j < local_N_blocks; ++j) { // receive number of rows unsigned n; MPI_Recv(&n, 1, MPI_UNSIGNED, 0, 1, MPI_COMM_WORLD, MPI_STATUS_IGNORE); // initialize blocks dense_matrix local_A(n,n); dense_matrix local_B(n,n); // receive blocks MPI_Recv(local_A.data(), n*n, MPI_DOUBLE, 0, 2, MPI_COMM_WORLD, MPI_STATUS_IGNORE); MPI_Recv(local_B.data(), n*n, MPI_DOUBLE, 0, 3, MPI_COMM_WORLD, MPI_STATUS_IGNORE); // compute product local_C[j] = local_A * local_B; } // send local results for (unsigned j = 0; j < local_N_blocks; ++j) { // send number of rows unsigned n = local_C[j].rows(); MPI_Send(&n, 1, MPI_UNSIGNED, 0, 4, MPI_COMM_WORLD); // send block MPI_Send(local_C[j].data(), n*n, MPI_DOUBLE, 0, 5, MPI_COMM_WORLD); } }
问题解答
连续发送不需要等待目标进程完成上一轮计算
MPI_Send的阻塞条件和接收方的计算进度完全无关,仅和通信缓冲区状态、接收方是否提交匹配的接收请求有关:
- 当消息大小在MPI eager协议阈值内时,数据会直接拷贝到MPI预留的内部通信缓冲区,
MPI_Send会立刻返回,rank 0可以直接执行后续发送逻辑,和接收方有没有完成上一块的计算没有任何关联。 - 当消息大小超过eager协议阈值时,MPI会切换到rendezvous通信模式,
MPI_Send会阻塞直到接收方调用了匹配源、匹配tag的MPI_Recv,且数据完成端到端拷贝才返回,但这个阻塞条件仅和接收方是否启动接收操作有关,和接收方是否完成上一块的乘法计算无关。
当前代码中计算进程的逻辑是先循环接收完所有分配到的矩阵块,再启动计算,因此只要计算进程提前提交了对应tag的MPI_Recv请求,rank 0的连续发送就可以正常推进,不需要额外添加等待逻辑。同时MPI保证同一对通信进程间、同tag的消息严格按照发送顺序匹配,不会出现A、B矩阵块错配的问题。
现有代码的并行设计缺陷与效率问题
当前实现在块数能被计算进程数整除、单块消息不触发异常阻塞的前提下可以跑出正确结果,但并行效率很低,还存在潜在的运行风险:
- 通信与计算完全串行,无时间重叠。现有流程严格拆分为「rank 0全量发送数据→计算进程全量接收数据→计算进程全量完成计算→计算进程全量回传结果→rank 0全量接收结果」五个串行阶段,通信时计算资源空转,计算时通信链路完全闲置,随着进程数增多、矩阵规模变大,整体开销会线性上涨。
- 存在潜在死锁风险。如果单块矩阵尺寸过大,超过MPI的eager消息阈值,rank 0连续向同一进程发送数据时可能占满通信缓冲区,此时
MPI_Send会阻塞等待接收方接收数据,但计算进程如果已经处理完当前提交的所有接收请求、进入计算阶段,而rank 0还卡在发送循环中,没有进入接收结果的逻辑,就会出现双方互相等待的死锁。另外代码中用整数除法计算local_N_blocks = N_blocks/(size-1),如果N_blocks不能被size-1整除,会出现剩余矩阵块无进程接收、部分进程等不到足够数据永久阻塞的问题。 - 启动阶段开销随进程数线性增长。rank 0单线程循环逐个向所有计算进程发送数据,进程数越多,所有计算进程等待数据的空等时间越长,完全没有利用到MPI通信的并行能力。
优化方向
- 调整计算进程逻辑为「收一块、算一块」,不需要等所有块都接收完成再启动计算,减少初始等待时间。
- 采用非阻塞通信接口
MPI_Isend/MPI_Irecv,在计算当前矩阵块的同时,后台异步发起下一块的接收、上一块结果的回传,实现通信和计算的时间重叠,把通信开销完全隐藏在计算过程中。 - 补充块分配的余数处理逻辑,保证所有矩阵块都能分配到对应进程,避免整数除法带来的阻塞问题。
- 如果矩阵类的内存布局连续固定,可以自定义对应MPI数据类型,直接用
MPI_Scatterv做批量数据分发,减少点对点通信的调用开销。
内容的提问来源于stack exchange,提问作者Fabio
相关产品推荐
相关产品推荐

