Производительность сильно снижается при использовании mpirun для выполнения моей программы - PullRequest
3 голосов
/ 13 марта 2020

Я новичок в области MPI. Я пишу свою программу с использованием Intel Math Kernel Library и хочу вычислить умножение матрицы на матрицу по блокам, что означает, что я разбил большую матрицу X на множество маленьких матриц вдоль столбца, как показано ниже. Моя матрица большая, поэтому каждый раз я вычисляю только (N, M) x (M, N), где я могу установить M вручную.

XX^T = X_1X_1^T + X_2X_2^T + ... + X_nX_n^T

Сначала я устанавливаю общее количество потоков равным 16, а M равняется 1024. Затем я запускаю свою программу следующим образом. Я проверяю состояние своего процессора и обнаруживаю, что загрузка процессора составляет 1600%, что является нормальным.

./MMNET_MPI --block 1024 --numThreads 16

Однако я попытался запустить свою программу, используя MPI, как показано ниже. Тогда я обнаружил, что загрузка процессора составляет всего 200-300%. Странно, я изменяю номер блока на 64, и я могу немного улучшить производительность при использовании процессора на 1200%.

mpirun -n 1 --bind-to none ./MMNET_MPI --block 1024 --numThreads 16

Я не знаю, в чем проблема. Кажется, что mpirun делает некоторые настройки по умолчанию, которые влияют на мою программу. Следующее является частью моего кода умножения матриц. Команда #pragma omp parallel for предназначена для извлечения маленькой матрицы N by M из формата сжатия параллельно. После этого я использую clubs_dgemv для вычисления умножения матрицы на матрицу.

#include "MemoryUtils.h"
#include "Timer.h"
#include "omp.h"
#include <mpi.h>
#include <mkl.h>

#include <iostream>

using namespace std;

int main(int argc, char** argv) {
  omp_set_num_threads(16);
  Timer timer;
  double start_time = timer.get_time();

  MPI_Init(&argc, &argv);

  int total_process;
  int id;
  MPI_Comm_size(MPI_COMM_WORLD, &total_process);
  MPI_Comm_rank(MPI_COMM_WORLD, &id);

  if (id == 0) {
    cout << "========== Testing MPI properties for MMNET ==========" << endl;
  }

  cout << "Initialize the random matrix ..." << endl;

  unsigned long N = 30000;
  unsigned long M = 500000;
  unsigned long snpsPerBlock = 1024;

  auto* matrix = ALIGN_ALLOCATE_DOUBLES(N*M);
  auto* vector = ALIGN_ALLOCATE_DOUBLES(N);
  auto* result = ALIGN_ALLOCATE_DOUBLES(M);
  auto *temp1 = ALIGN_ALLOCATE_DOUBLES(snpsPerBlock);
  memset(result, 0, sizeof(double) * M);

  cout << "Time for allocating is " << timer.update_time() << " sec" << endl;

  memset(matrix, 1.1234, sizeof(double) * N * M);
  memset(vector, 1.5678, sizeof(double) * N);
  // #pragma omp parallel for
  // for (unsigned long row = 0; row < N * M; row++) {
  //     matrix[row] = (double)rand() / RAND_MAX;
  // }

  // #pragma omp parallel for
  // for (unsigned long row = 0; row < N; row++) {
  //     vector[row] = (double)rand() / RAND_MAX;
  // }

  cout << "Time for generating data is " << timer.update_time() << " sec" << endl;

  cout << "Starting calculating..." << endl;

  for (unsigned long m0 = 0; m0 < M; m0 += snpsPerBlock) {
    uint64 snpsPerBLockCrop = std::min(M, m0 + snpsPerBlock) - m0;
    auto* snpBlock = matrix + m0 * N;

    MKL_INT row = N;
    MKL_INT col = snpsPerBLockCrop;
    double alpha = 1.0;
    MKL_INT lda = N;
    MKL_INT incx = 1;
    double beta = 0.0;
    MKL_INT incy = 1;
    cblas_dgemv(CblasColMajor, CblasTrans, row, col, alpha, snpBlock, lda, vector, incx, beta, temp1, incy);

    // compute XA
    double beta1 = 1.0;
    cblas_dgemv(CblasColMajor, CblasNoTrans, row, col, alpha, snpBlock, lda, temp1, incx, beta1, result, incy);
  }

  cout << "Time for computation is " << timer.update_time() << " sec" << endl;
  ALIGN_FREE(matrix);
  ALIGN_FREE(vector);
  ALIGN_FREE(result);
  ALIGN_FREE(temp1);
  return 0;
}

Моя информация о процессоре выглядит следующим образом.

Architecture:        x86_64
CPU op-mode(s):      32-bit, 64-bit
Byte Order:          Little Endian
CPU(s):              44
On-line CPU(s) list: 0-43
Thread(s) per core:  1
Core(s) per socket:  22
Socket(s):           2
NUMA node(s):        2
Vendor ID:           GenuineIntel
CPU family:          6
Model:               85
Model name:          Intel(R) Xeon(R) Gold 6152 CPU @ 2.10GHz
Stepping:            4
CPU MHz:             1252.786
CPU max MHz:         2101.0000
CPU min MHz:         1000.0000
BogoMIPS:            4200.00
Virtualization:      VT-x
L1d cache:           32K
L1i cache:           32K
L2 cache:            1024K
L3 cache:            30976K
NUMA node0 CPU(s):   0-21
NUMA node1 CPU(s):   22-43

1 Ответ

2 голосов
/ 14 марта 2020

MKL по умолчанию реализует некоторую интеллектуальную динамику c выбора количества используемых потоков. Это контролируется переменной MKL_DYNAMIC, которая по умолчанию установлена ​​на TRUE. Документация для MKL гласит:

Если вы [sic] способны обнаружить наличие MPI, но не можете определить, был ли он вызван в поточно-безопасном режиме ( например, это невозможно обнаружить с помощью MPICH 1.2.x), и значение MKL_DYNAMIC не изменилось со значения по умолчанию TRUE, Intel MKL будет запускать один поток.

Поскольку Вы вызываете MPI_Init(), а не MPI_Init_thread() для инициализации MPI, вы фактически запрашиваете однопоточный уровень MPI (MPI_THREAD_SINGLE). Библиотека может предоставить вам любой уровень потоков и будет консервативно придерживаться MPI_THREAD_SINGLE. Вы можете проверить это, вызвав MPI_Query_thread(&provided) после инициализации и посмотреть, не превышает ли выходное значение MPI_THREAD_SINGLE.

Поскольку вы смешиваете OpenMP и MKL с многопоточностью с MPI, вы должны сказать MPI инициализировать в более высокий уровень поддержки потоков путем вызова MPI_Init_thread():

int provided;

MPI_Init_thread(NULL, NULL, MPI_THREAD_MULTIPLE, &provided);
// This ensures that MPI actually provides MPI_THREAD_MULTIPLE
if (provided < MPI_THREAD_MULTIPLE) {
  // Complain
}

(технически вам необходимо MPI_THREAD_FUNNNELED, если вы не делаете вызовы MPI извне основного потока, но это не потокобезопасный режим как понимает MKL)

Даже если вы запрашиваете определенный уровень поддержки потоков у MPI, нет никакой гарантии, что вы его получите, поэтому вам нужно проверить предоставленный уровень. Кроме того, старые версии Open MPI должны быть явно собраны с такой поддержкой - по умолчанию не производится сборка с поддержкой MPI_THREAD_MULTIPLE, так как некоторые сетевые модули не являются поточно-ориентированными. Вы можете проверить, так ли это, запустив ompi_info и выполнив поиск строки, похожей на эту:

Thread support: posix (MPI_THREAD_MULTIPLE: yes, OPAL support: yes, OMPI progress: no, ORTE progress: yes, Event lib: yes)

Теперь реальность такова, что большинство многопоточных программ не выполняет вызовы MPI вне основного потока. работает отлично, даже если MPI не обеспечивает более высокий уровень поддержки потоков, чем MPI_THREAD_SINGLE, т. е. в большинстве реализаций MPI MPI_THREAD_SINGLE эквивалентно MPI_THREAD_FUNNELED. В этом случае установка MKL_DYNAMIC в FALSE должна заставить MKL вести себя так же, как при запуске без mpirun:

mpirun -x MKL_DYNAMIC=FALSE ...

В любом случае, поскольку ваша программа принимает число потоков в качестве аргумента, просто Вызовите mkl_set_num_threads() и omp_set_num_threads() и не полагайтесь на магические механизмы по умолчанию.

Редактировать: Включение поддержки полной нити имеет последствия - повышенная задержка и некоторые сетевые модули могут отказываться работать, например, модуль InfiniBand в старые версии Open MPI, в результате чего библиотека тихо переключается на более медленные транспорты, такие как TCP / IP. Лучше запрос MPI_THREAD_FUNNELED и явно установить количество потоков MKL и OpenMP.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...