用于所有 mpi 进程发送和接收的时间高效设计模型:MPI all 2 all communication

Time efficient design model for sending to and receiving from all mpi processes: MPI all 2 all communication

我正在尝试从一个进程向所有 MPI 进程发送消息,并从一个进程中的所有这些进程接收消息。它基本上是一种全对全通信,其中每个进程向每个其他进程(自身除外)发送消息并从每个其他进程接收消息。

以下示例代码片段显示了我正在努力实现的目标。现在,MPI_Send 的问题在于它的行为,对于小消息大小它充当非阻塞但对于较大的消息(在我的机器 BUFFER_SIZE 16400 中)它阻塞。我知道这就是 MPI_Send 的行为方式。作为解决方法,我将下面的代码替换为阻塞 (send+recv),即 MPI_Sendrecv。示例代码是这样的 MPI_Sendrecv(intSendPack, BUFFER_SIZE, MPI_INT, processId, MPI_TAG, intReceivePack, BUFFER_SIZE, MPI_INT, processId, MPI_TAG, MPI_COMM_WORLD, MPI_STATUSES_IGNORE) 。我在每个等级的循环内对 MPI_COMM_WORLD 的所有进程进行上述调用,这种方法给了我想要实现的目标(所有通信)。但是,此调用会花费很多时间,我想通过一些省时的方法来减少这些时间。我已经尝试使用 mpi scatter 和 gather 来执行所有通信,但这里的一个问题是缓冲区大小 (16400) 在 MPI_all_to_all 函数调用的不同迭代中的实际实现中可能会有所不同。在这里,我使用 MPI_TAG 来区分不同迭代中的调用,我不能在分散和收集函数中使用它。

#define BUFFER_SIZE 16400

void MPI_all_to_all(int MPI_TAG)
{

    int size;
    int rank;
    MPI_Comm_size(MPI_COMM_WORLD, &size);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);

    int* intSendPack = new int[BUFFER_SIZE]();
    int* intReceivePack = new int[BUFFER_SIZE]();

    for (int prId = 0; prId < size; prId++) {
        if (prId != rank) {
            MPI_Send(intSendPack, BUFFER_SIZE, MPI_INT, prId, MPI_TAG,
            MPI_COMM_WORLD);
          }
    }

    for (int sId = 0; sId < size; sId++) {
        if (sId != rank) {
            MPI_Recv(intReceivePack, BUFFER_SIZE, MPI_INT, sId, MPI_TAG,
            MPI_COMM_WORLD, MPI_STATUSES_IGNORE);
        }
    }
}

我想知道是否有一种方法可以使用任何高效的通信模型执行所有对所有的通信。我不坚持 MPI_Send,如果有其他方法可以提供我想要实现的目标,我对此很满意。非常感谢任何帮助或建议。

这是一个基准,可以在 all-to-all 通信中比较集体通信与 point-to-point 通信的性能,

#include <iostream>
#include <algorithm>
#include <mpi.h>

#define BUFFER_SIZE 16384

void point2point(int*, int*, int, int);

int main(int argc, char *argv[])
{
    MPI_Init(&argc, &argv);

    int rank_id = 0, com_sz = 0;
    double t0 = 0.0, tf = 0.0;
    MPI_Comm_size(MPI_COMM_WORLD, &com_sz);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank_id);

    int* intSendPack = new int[BUFFER_SIZE]();
    int* result = new int[BUFFER_SIZE*com_sz]();
    std::fill(intSendPack, intSendPack + BUFFER_SIZE, rank_id);
    std::fill(result + BUFFER_SIZE*rank_id, result + BUFFER_SIZE*(rank_id+1), rank_id);

    // Send-Receive
    t0 = MPI_Wtime();
    point2point(intSendPack, result, rank_id, com_sz);
    MPI_Barrier(MPI_COMM_WORLD);
    tf = MPI_Wtime();
    if (!rank_id)
        std::cout << "Send-receive time: " << tf - t0 << std::endl;

    // Collective
    std::fill(result, result + BUFFER_SIZE*com_sz, 0);
    std::fill(result + BUFFER_SIZE*rank_id, result + BUFFER_SIZE*(rank_id+1), rank_id);
    t0 = MPI_Wtime();
    MPI_Allgather(intSendPack, BUFFER_SIZE, MPI_INT, result, BUFFER_SIZE, MPI_INT, MPI_COMM_WORLD);
    MPI_Barrier(MPI_COMM_WORLD);
    tf = MPI_Wtime();
    if (!rank_id)
        std::cout << "Allgather time: " << tf - t0 << std::endl;

    MPI_Finalize();
    delete[] intSendPack;
    delete[] result;
    return 0;
}

// Send/receive communication
void point2point(int* send_buf, int* result, int rank_id, int com_sz)
{
    MPI_Status status;
    // Exchange and store the data
    for (int i=0; i<com_sz; i++){
        if (i != rank_id){
            MPI_Sendrecv(send_buf, BUFFER_SIZE, MPI_INT, i, 0, 
                result + i*BUFFER_SIZE, BUFFER_SIZE, MPI_INT, i, 0, MPI_COMM_WORLD, &status);
        }
    }
}

这里每个 运行k 都将自己的数组 intSendPack 贡献给所有其他 运行k 上的数组 result,这些 运行k 应该在所有 运行ks。 result 是平坦的,每个 运行k 需要 BUFFER_SIZE 个条目,从它的 rank_id*BUFFER_SIZE 开始。 point-to-point 通信后,数组重置为原始形状。

时间是通过设置一个 MPI_Barrier 来衡量的,这将为您提供所有 运行ks 中的最大时间。

我 运行 Nersc Cori KNL using slurm 的 1 个节点上的基准测试。我 运行 每个案例几次只是为了确保值是一致的,我不是在看异常值,但你应该 运行 它可能 10 次左右以收集更适当的统计数据。

以下是一些想法:

  • 对于少量的进程 (5) 和较大的缓冲区大小 (16384),集体通信大约比 point-to-point 快两倍,但当移动到更大数量的 运行ks (64)。
  • 在此基准测试中,在该特定机器上推荐的 slurm 设置与默认设置之间的性能没有太大差异,但在实际中,具有更多通信的大型程序有一个非常重要的(工作 运行s不到一分钟,推荐 运行 20-30 分钟,默认情况下更长)。这一点是检查您的设置,它可能会有所不同。
  • 您在 Send/Receive 中看到的较大消息实际上是一个死锁。对于此基准测试中显示的消息大小,我也看到了它。万一你错过了那些,上面有两个值得一看的 SO 帖子:buffering explanation and a word on deadlocking

总而言之,调整此基准以更接近地代表您的代码并 运行 它在您的系统上,但在 all-to-all 或 one-to-all 情况下的集体通信应该更快,因为专用优化,例如用于通信参数 运行 的高级算法。 2-5 倍的加速是相当可观的,因为通信通常对总时间的贡献最大。