MPI中8×8矩阵2×2块循环分发至4进程的问题排查
块循环矩阵分发的MPI实现问题解析与修复
问题根源分析
你的核心问题在于混淆了MPI_Type_create_darray与MPI_Scatter、MPI_File_Set_view的工作逻辑差异:
MPI_File_Set_view的自动协调特性
当配合文件操作时,MPI_File_Set_view会让每个进程基于自身的darray类型,直接从文件中定位并读取属于自己的全局数据块,MPI底层会自动处理进程间的偏移计算和数据映射,因此无需额外处理就能正常工作。MPI_Scatter的固有局限性
MPI_Scatter是根进程将单个连续全局缓冲区拆分为等份发送给各进程的接口,你直接用每个进程自己的darray类型作为发送/接收类型时,会出现两个关键问题:- 初始代码中,
blockcyclic_darray描述的是当前rank在全局数组中的数据布局,根进程调用MPI_Scatter时,发送类型是自身的darray(仅包含进程0的数据),因此只有进程0能正确接收匹配的数据,其他进程因接收类型不匹配无法获取数据。 - 手动修改
extent后,虽然强制让数据分发到了所有进程,但统一设置的4*extent跨度没有考虑不同rank的darray类型的固有偏移,导致非0进程的接收数据出现位移错误。
- 初始代码中,
正确实现方案
方案1:使用MPI_Scatterv配合进程专属darray类型
这种方式利用MPI_Scatterv支持为每个进程指定不同发送位移和类型的特性,让根进程正确拆分全局数组到各进程:
#include <stdio.h> #include <stdlib.h> #include "mpi.h" int main(int argc, char** argv) { MPI_Init(&argc, &argv); int rank, size; MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Comm_size(MPI_COMM_WORLD, &size); const int global_dims[2] = {8, 8}; const int proc_dims[2] = {2, 2}; const int distribs[2] = {MPI_DISTRIBUTE_CYCLIC, MPI_DISTRIBUTE_CYCLIC}; const int dargs[2] = {2, 2}; const int local_count = 16; // 8*8 / 4 = 16元素/进程 float* input = NULL; float* output = malloc(local_count * sizeof(float)); // 根进程初始化全局矩阵 if (rank == 0) { input = malloc(global_dims[0] * global_dims[1] * sizeof(float)); for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { int proc_row = (i / 2) % 2; int proc_col = (j / 2) % 2; input[i * 8 + j] = proc_row * 2 + proc_col; } } } // 每个进程创建描述自身全局数据区域的darray类型 MPI_Datatype global_darray; MPI_Type_create_darray(size, rank, 2, global_dims, distribs, dargs, proc_dims, MPI_ORDER_C, MPI_FLOAT, &global_darray); MPI_Type_commit(&global_darray); // 创建本地连续数组的类型(用于接收数据) MPI_Datatype local_contiguous; MPI_Type_contiguous(local_count, MPI_FLOAT, &local_contiguous); MPI_Type_commit(&local_contiguous); if (rank == 0) { MPI_Aint* displs = malloc(size * sizeof(MPI_Aint)); int* counts = malloc(size * sizeof(int)); // 为每个进程计算全局数据的起始位移 for (int r = 0; r < size; r++) { MPI_Datatype temp_darray; MPI_Type_create_darray(size, r, 2, global_dims, distribs, dargs, proc_dims, MPI_ORDER_C, MPI_FLOAT, &temp_darray); MPI_Type_commit(&temp_darray); MPI_Aint lb, extent; MPI_Type_get_extent(temp_darray, &lb, &extent); displs[r] = lb; counts[r] = 1; MPI_Type_free(&temp_darray); } // 用Scatterv分发数据 MPI_Scatterv(input, counts, displs, global_darray, output, 1, local_contiguous, 0, MPI_COMM_WORLD); free(displs); free(counts); } else { MPI_Scatterv(NULL, NULL, NULL, MPI_DATATYPE_NULL, output, 1, local_contiguous, 0, MPI_COMM_WORLD); } // 验证输出(示例:打印前4个元素) printf("Rank %d: ", rank); for (int i = 0; i < 4; i++) { printf("%.0f ", output[i]); } printf("\n"); // 释放资源 MPI_Type_free(&global_darray); MPI_Type_free(&local_contiguous); free(output); if (rank == 0) free(input); MPI_Finalize(); return 0; }
方案2:手动计算块位置,用MPI_Send/Recv分发
对于进程数较少的场景,也可以手动遍历全局矩阵,收集每个进程对应的块循环数据后直接发送:
// 仅核心分发逻辑,其余初始化/释放代码同方案1 if (rank == 0) { for (int dest_rank = 0; dest_rank < 4; dest_rank++) { int proc_row = dest_rank / 2; int proc_col = dest_rank % 2; float* send_buf = malloc(local_count * sizeof(float)); int idx = 0; // 遍历所有2x2块,收集目标进程的块 for (int i = 0; i < 8; i += 2) { for (int j = 0; j < 8; j += 2) { int block_row = (i / 2) % 2; int block_col = (j / 2) % 2; if (block_row == proc_row && block_col == proc_col) { // 复制2x2块到发送缓冲区 for (int di = 0; di < 2; di++) { for (int dj = 0; dj < 2; dj++) { send_buf[idx++] = input[(i + di) * 8 + (j + dj)]; } } } } } MPI_Send(send_buf, local_count, MPI_FLOAT, dest_rank, 0, MPI_COMM_WORLD); free(send_buf); } } else { MPI_Recv(output, local_count, MPI_FLOAT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); }
关键总结
MPI_Type_create_darray生成的类型是全局数组中当前rank的数据区域描述,不能直接用于MPI_Scatter的默认连续拆分逻辑。MPI_Scatterv是适配非连续数据分发的正确接口,它允许为每个进程指定独立的发送位移和数据类型。- MPI文件操作接口会自动处理全局数据的进程间映射,这是之前文件读取正常的核心原因。
内容的提问来源于stack exchange,提问作者G. Ianni
相关产品推荐
相关产品推荐

