MPI程序性能优化:解决MPI_Allgather同步等待瓶颈
MPI异步通信优化方案(替代同步MPI_Allgather)
一、异步收集+异步广播的正确实现
你之前尝试主节点异步收集再广播失败,大概率是没正确管理非阻塞通信的请求句柄,或未确保数据就绪后再操作。以下是可运行的核心逻辑:
核心步骤
- 主节点(rank=0)完成KNN计算后,异步接收所有从节点的
new_cache数据;从节点计算完成后异步发送自身缓存到主节点。 - 主节点收齐所有缓存后,异步广播给所有从节点。
- 自定义
Diccionario结构需拆分成员(key_count、keys、values)分别通信,或序列化为连续字节流后传输。
代码示例
#include <mpi.h> #include <stdio.h> #include <stdlib.h> // 自定义Diccionario结构示例 typedef struct { int key_count; int* keys; float* values; } Diccionario; 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); Diccionario new_cache; // 省略:各节点加载CSV、执行KNN计算生成new_cache的代码 MPI_Request* reqs = NULL; Diccionario* all_caches = NULL; if (rank == 0) { // 主节点初始化接收请求与缓存存储数组 reqs = (MPI_Request*)malloc(2*(size-1)*sizeof(MPI_Request)); all_caches = (Diccionario*)malloc(size * sizeof(Diccionario)); all_caches[0] = new_cache; // 自身缓存直接复用 // 异步接收所有从节点的缓存 int req_idx = 0; for (int i = 1; i < size; i++) { // 先接收key_count,再分配内存接收keys/values MPI_Irecv(&all_caches[i].key_count, 1, MPI_INT, i, 0, MPI_COMM_WORLD, &reqs[req_idx++]); MPI_Wait(&reqs[req_idx-1], MPI_STATUS_IGNORE); all_caches[i].keys = (int*)malloc(all_caches[i].key_count * sizeof(int)); all_caches[i].values = (float*)malloc(all_caches[i].key_count * sizeof(float)); MPI_Irecv(all_caches[i].keys, all_caches[i].key_count, MPI_INT, i, 1, MPI_COMM_WORLD, &reqs[req_idx++]); MPI_Irecv(all_caches[i].values, all_caches[i].key_count, MPI_FLOAT, i, 2, MPI_COMM_WORLD, &reqs[req_idx++]); } MPI_Waitall(req_idx, reqs, MPI_STATUSES_IGNORE); // 异步广播所有缓存到从节点 free(reqs); reqs = (MPI_Request*)malloc(3*size*(size-1)*sizeof(MPI_Request)); req_idx = 0; for (int dest = 1; dest < size; dest++) { for (int i = 0; i < size; i++) { MPI_Isend(&all_caches[i].key_count, 1, MPI_INT, dest, 3+i, MPI_COMM_WORLD, &reqs[req_idx++]); MPI_Isend(all_caches[i].keys, all_caches[i].key_count, MPI_INT, dest, 4+i, MPI_COMM_WORLD, &reqs[req_idx++]); MPI_Isend(all_caches[i].values, all_caches[i].key_count, MPI_FLOAT, dest, 5+i, MPI_COMM_WORLD, &reqs[req_idx++]); } } MPI_Waitall(req_idx, reqs, MPI_STATUSES_IGNORE); } else { // 从节点异步发送自身缓存到主节点 reqs = (MPI_Request*)malloc(3*sizeof(MPI_Request)); MPI_Isend(&new_cache.key_count, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, &reqs[0]); MPI_Isend(new_cache.keys, new_cache.key_count, MPI_INT, 0, 1, MPI_COMM_WORLD, &reqs[1]); MPI_Isend(new_cache.values, new_cache.key_count, MPI_FLOAT, 0, 2, MPI_COMM_WORLD, &reqs[2]); MPI_Waitall(3, reqs, MPI_STATUSES_IGNORE); // 从节点异步接收主节点广播的所有缓存 free(reqs); reqs = (MPI_Request*)malloc(3*size*sizeof(MPI_Request)); all_caches = (Diccionario*)malloc(size * sizeof(Diccionario)); int req_idx = 0; for (int i = 0; i < size; i++) { MPI_Irecv(&all_caches[i].key_count, 1, MPI_INT, 0, 3+i, MPI_COMM_WORLD, &reqs[req_idx++]); MPI_Wait(&reqs[req_idx-1], MPI_STATUS_IGNORE); all_caches[i].keys = (int*)malloc(all_caches[i].key_count * sizeof(int)); all_caches[i].values = (float*)malloc(all_caches[i].key_count * sizeof(float)); MPI_Irecv(all_caches[i].keys, all_caches[i].key_count, MPI_INT, 0, 4+i, MPI_COMM_WORLD, &reqs[req_idx++]); MPI_Irecv(all_caches[i].values, all_caches[i].key_count, MPI_FLOAT, 0, 5+i, MPI_COMM_WORLD, &reqs[req_idx++]); } MPI_Waitall(req_idx, reqs, MPI_STATUSES_IGNORE); } // 省略:后续业务逻辑、内存释放代码 free(reqs); if (all_caches) { for (int i = 0; i < size; i++) { free(all_caches[i].keys); free(all_caches[i].values); } free(all_caches); } MPI_Finalize(); return 0; }
关键注意事项
- 所有非阻塞通信必须用
MPI_Wait/MPI_Waitall等待完成,避免访问未就绪数据。 - 通信标签(tag)需全局唯一,避免不同通信步骤冲突。
- 自定义结构无法直接通过MPI传输,必须拆分成员或序列化。
二、重叠计算与通信(进一步降低延迟)
由于KNN计算耗时约60秒,可将通信与计算重叠,减少等待时间:
- 在KNN计算接近完成时(比如完成90%迭代),提前初始化异步发送请求,待计算完全结束后直接触发传输。
- 示例代码片段:
int total_iterations = 1000; // 假设KNN总迭代数 for (int i = 0; i < total_iterations; i++) { // 执行KNN迭代计算 if (i == total_iterations * 0.9) { // 提前初始化发送请求,此时缓存已完成大部分填充 MPI_Isend(&new_cache.key_count, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, &send_req); } } // 计算完成后,确保缓存数据填充完毕,等待发送完成 MPI_Wait(&send_req, MPI_STATUS_IGNORE);
三、CSV加载优化建议
你提到广播CSV加载失败,可尝试以下方案:
- 主节点加载整个CSV文件为字节流或字符串数组,通过
MPI_Bcast广播给所有节点,各节点本地解析生成数据结构,避免重复磁盘IO。 - 大文件可拆分为多个块,用非阻塞广播提升传输效率。
四、替代方案:点对点异步通信(无需主节点中转)
若不想通过主节点中转,可让节点间直接异步交换缓存:
- 每个节点启动多个非阻塞发送/接收请求,与其他节点直接交换数据,快节点可先完成与其他快节点的通信,无需等待最慢节点。
- 核心逻辑片段:
MPI_Request* reqs = (MPI_Request*)malloc(2*(size-1)*sizeof(MPI_Request)); int req_idx = 0; all_caches = (Diccionario*)malloc(size * sizeof(Diccionario)); all_caches[rank] = new_cache; for (int peer = 0; peer < size; peer++) { if (peer != rank) { // 异步发送自身缓存给peer MPI_Isend(&new_cache.key_count, 1, MPI_INT, peer, 10+rank, MPI_COMM_WORLD, &reqs[req_idx++]); MPI_Isend(new_cache.keys, new_cache.key_count, MPI_INT, peer, 20+rank, MPI_COMM_WORLD, &reqs[req_idx++]); // 异步接收peer的缓存 MPI_Irecv(&all_caches[peer].key_count, 1, MPI_INT, peer, 10+peer, MPI_COMM_WORLD, &reqs[req_idx++]); MPI_Wait(&reqs[req_idx-1], MPI_STATUS_IGNORE); all_caches[peer].keys = (int*)malloc(all_caches[peer].key_count * sizeof(int)); all_caches[peer].values = (float*)malloc(all_caches[peer].key_count * sizeof(float)); MPI_Irecv(all_caches[peer].keys, all_caches[peer].key_count, MPI_INT, peer, 20+peer, MPI_COMM_WORLD, &reqs[req_idx++]); } } MPI_Waitall(req_idx, reqs, MPI_STATUSES_IGNORE);
内容的提问来源于stack exchange,提问作者Sebastian Piceda
相关产品推荐
相关产品推荐

