MPI_Allgather является узким местом, как его можно сломать с помощью MPI_Send и MPI_Recv? - PullRequest
0 голосов
/ 14 марта 2019
       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 голосов
/ 14 марта 2019

Я думаю, что единственной причиной любой связи между задачами MPI является доступ к d_t[i-1] и i+1 в начале и конце сегмента соответственно.

Есливы не обменивались данными, вы читали устаревшие элементы, которые вам не удалось пересчитать.

Но вместо полной синхронизации каждая задача должна отправлять только началоего сегмента к задаче, которая работает над сегментом раньше.(И то же самое для конца его сегмента до следующего ранга).

Сделайте это с отправкой / получением.


Еще лучше, есть сегменты немного перекрываются в конце, чтобы вы могли общаться реже .«Неверные» данные будут распространяться по 1 элементу за каждую итерацию внешнего цикла, поэтому перекрытие 8 элементов (еще 1 32-байтовый вектор AVX) должно означать, что вам нужно общаться только каждые 8 ​​итераций или около того.

В идеале мы могли бы передавать сообщения, чтобы скрыть сетевую задержку .Вычисления выполняются быстро по сравнению с задержкой между машинами по гигабитному Ethernet (1 микросекунда = ~ 3000 тактовых циклов при 3GHz = ~ 48k операций FMA с плавающей запятой, при условии, что FMA по 2 на такт с 32-байтовыми векторами = теоретическая максимальная пропускная способность Haswell / Skyake).Поэтому я думаю, что иметь получателя, дублирующего некоторую работу над этими элементами, - хорошая ставка.

Если вы отправляете 16 элементов (каждый в начале / конце) каждые 12 итераций, но отправляете 2 итерации внешнего цикла перед вызовом получения, это будет на 2 итерации устаревшими, когда оно будет получено.(На практике разверните внешний цикл или используйте вложенный цикл, если вы можете избежать прерывания автоматической векторизации и автоматического распараллеливания OMP.)

Но это хорошо, по сравнению со всем размером сегмента, который требуется тривиальнымвремя для получающей стороны выполнить дополнительные 2 итерации над этими данными и перехватить их.Если задачи сохраняют свои 2 элемента от момента отправки, они могут комбинировать это с полученным блоком, чтобы потребовалось на 2 элемента меньше совпадений, чтобы в итоге>> 13 элементов массива были правильными на принимающей стороне.

Когдаотладка этого, вы, вероятно, захотите иметь как минимум 1 дополнительный элемент перекрытия, чтобы вы могли assert, чтобы избыточные элементы были == друг для друга.(Включая проверку того, что ваш код оптимизируется таким же образом, с сокращением умножения + добавления в FMA. Математика IEEE FP является детерминированной, но у компиляторов есть некоторая широта ...)

...