MPI_Allgather是这里的瓶颈,如何用MPI_Send和MPI_Recv打破它?

时间:2019-03-14 07:18:26

标签: c++ optimization mpi intel openmpi

       float * simulate(const float alpha, const long n_segments, const int n_steps, float *d_buf1, float *d_buf2, const int rank, const int world_size, const long segments_per_process) {

      float* d_t  = d_buf1;  // buffer for d(*, t)
      float* d_t1 = d_buf2;  // buffer for d(*, t+1)

      const long start_segment = segments_per_process*((long)rank)   +1L;
      const long last_segment  = segments_per_process*((long)rank+1L)+1L;

      const float dx = 1.0f/(float)n_segments;
      const float phase = 0.5f;

      MPI_Status stat;
      for(int t = 0; t < n_steps; t++) {
    #pragma omp parallel for simd
        for(long i = start_segment; i < last_segment; i++) {
          const float L_x = L(alpha,phase,i*dx);
          d_t1[i] = L_x*(d_t[i+1] + d_t[i-1])
                    +2.0f*(1.0f-L_x)*(d_t[i]) 
                    - d_t1[i]; // The algorithm calls for d(i, t-1) here, but that is currently contained in d_t1
        }

        float* temp = d_t1; d_t1 = d_t; d_t = temp;
        MPI_Allgather(MPI_IN_PLACE, 0, MPI_DATATYPE_NULL, &d_t[1], segments_per_process, MPI_FLOAT, MPI_COMM_WORLD);

      }
      return d_t;
    }

这是一个使用MPI计算弦振动的程序,在这个程序中,我们必须使用给定的等级用MPI_Send和MPI_Recv完成任务。这样可以更有效地执行


这是我为实现@Peter Cordes的答案所做的修改。它没有给出正确的输出,您能看看我做错了什么吗?

float * simulate(const float alpha, const long n_segments, const int n_steps, float *d_buf1, float *d_buf2, const int rank, const int world_size, const long segments_per_process) {

  float* d_t  = d_buf1;  // buffer for d(*, t)
  float* d_t1 = d_buf2;  // buffer for d(*, t+1)

  const long start_segment = segments_per_process*((long)rank)   +1L;
  const long last_segment  = segments_per_process*((long)rank+1L)+1L;

  const float dx = 1.0f/(float)n_segments;
  const float phase = 0.5f;

  MPI_Status stat;
  for(int t = 0; t < n_steps; t++) {
      MPI_Barrier(MPI_COMM_WORLD);
#pragma omp parallel for simd
    for(long i = start_segment; i < last_segment; i++) {
      const float L_x = L(alpha,phase,i*dx);
      d_t1[i] = L_x*(d_t[i+1] + d_t[i-1])
                +2.0f*(1.0f-L_x)*(d_t[i]) 
                - d_t1[i]; // The algorithm calls for d(i, t-1) here, but that is currently contained in d_t1
    }

    float* temp = d_t1; d_t1 = d_t; d_t = temp;

    /*MPI_Bcast(&d_t,1,
    MPI_Allgather(MPI_IN_PLACE, 0, MPI_DATATYPE_NULL, &d_t[1], segments_per_process, MPI_FLOAT, MPI_COMM_WORLD);
    */

    MPI_Send(&d_t, 1, MPI_FLOAT, rank - 1, 0, MPI_COMM_WORLD);
    MPI_Send(&d_t[segments_per_process-1], 1, MPI_FLOAT, rank + 1, 1, MPI_COMM_WORLD);

    MPI_Recv(&d_t, 1, MPI_FLOAT, rank, 0, MPI_COMM_WORLD,&stat);
    MPI_Recv(&d_t[segments_per_process-1], 1, MPI_FLOAT, rank, 1, MPI_COMM_WORLD, &stat);
  }
  return d_t;
}

1 个答案:

答案 0 :(得分:0)

我认为MPI任务之间需要进行任何通信的唯一原因是分别在该段的开头和结尾处访问d_t[i-1]i+1

如果您不交换数据,那么您将读取该任务没有重新计算的陈旧元素。

但是,每个任务只需要将其分段的开始发送到之前处理该分段的任务,而不是全部同步。 (并且该段末尾到下一个排名也是如此)。

通过发送/接收来完成。


更好的是,将段 overlap 放在末尾,这样您就可以减少交流次数。 “错误”数据将在每个外循环迭代中传播1个元素,因此8个元素的重叠(还有1个32字节AVX向量)将意味着您仅需要每8个迭代进行一次通讯。

理想情况下,我们可以通过管道传递消息来隐藏网络延迟。与千兆位以太网上的机器之间的延迟相比,计算速度飞快(假设1 GHz = 32字节向量的每时钟2个FMA = Haswell / Skyake的理论最大吞吐量),千兆位以太网上的机器之间的延迟(1微秒=〜3000个时钟周期=〜48k浮点FMA操作)。因此,我认为让接收方在这些元素上重复一些工作是不错的选择。

如果每12个迭代发送16个元素(每个元素在开始/结束时),但是在调用receive之前发送2个外循环迭代,则接收到的元素将过期2个迭代。 (在实践中,如果可以避免破坏自动矢量化和OMP自动并行化,请展开外部循环或使用嵌套循环。)

但这很好,与段的整个大小相比,接收方需要花费很少的时间在该数据上运行额外的2次迭代并赶上它。如果任务从发送时保存其2个元素,则可以将其与接收到的块结合起来,以减少2个重叠元素,最终在接收端得到> = 13个正确的数组元素。

进行调试时,您可能希望至少有1个额外的重叠元素,因此可以assert使得冗余元素彼此==。 (包括使用FMA收缩乘法和加法来验证代码是否以相同的方式进行了优化。IEEEFP数学是确定性的,但编译器具有一定的自由度...)