OpenMP на двухсоставной системе

Я делаю некоторые научные вычисления на С++ и пытаюсь использовать OpenMP для параллелизации некоторых из циклов. До сих пор это хорошо работало, например. на Intel i7-4770 с 8 потоками.

Настройка

У нас есть небольшая рабочая станция, которая состоит из двух процессоров Intel (E5-2680v2) на одной материнской плате. Код работает до тех пор, пока он работает на одном процессоре с таким количеством потоков, сколько мне нравится. Но как только я использую второй процессор, время от времени я наблюдаю неправильные результаты (примерно каждые 50-100-й раз я запускаю код). Это происходит даже тогда, когда я использую только 2 потока и назначаю их двум различным процессорам. Поскольку у нас есть 5 из этих рабочих станций (все одинаковы), я запускал код на каждом из них, и все это показывает эту проблему.

Рабочая станция работает на OpenSuse 13.1, ядро ​​3.11.10-7. Проблема существует с g++ 4.8.1 и 4.9.0 и с Intel icc 13.1.3.192 (хотя проблема не возникает часто с icc, но она по-прежнему существует).

Симптом

Симптом может быть описан следующим образом:

  • У меня есть большой массив std:: complex: std::complex<double>* mFourierValues;
  • В цикле я получаю доступ и устанавливаю каждый элемент. Каждая итерация обращается к другому элементу, поэтому у меня нет параллельных доступов (я проверил это): mFourierValues[idx] = newValue;
  • Если я сравню значение set-value со значением ввода-потом, примерно mFourierValues[idx] == newValue, эта проверка будет время от времени отказываться (хотя не каждый раз, когда результаты заканчиваются некорректно).

Итак, симптом выглядит так, что я обращаюсь к элементам одновременно без каких-либо синхронизаций. Однако, когда я сохраняю индексы в std::vector (с правильным #pragma omp critical), все знаки уникальны и находятся в правильном диапазоне.

Вопросы

После нескольких дней отладки мое подозрение растет, что происходит еще что-то, и что мой код верен. Для меня это выглядит как-то странно, когда CPU синхронизируют кеши с основной памятью.

Поэтому мои вопросы:

  • Можно ли использовать OpenMP для такой системы? (Я не нашел источник, который говорит "нет".)
  • Известны ли ошибки для такой ситуации (я не нашел их в контролерах ошибок)?
  • Где проблема, вероятно, расположена, на ваш взгляд?
    • Мой код (который, кажется, работает нормально на 1 CPU с несколькими ядрами!),
    • компиляторы (gcc, icc оба!),
    • ОС,
    • аппаратное обеспечение (дефект на всех 5 рабочих станциях?)

Код

[Изменить: старый код удален, см. ниже]

Редактировать с помощью минимального примера

ОК, наконец, я смог создать более короткий (и самосогласованный) пример кода.

О коде

  • зарезервировать некоторое пространство памяти. Для массива в стеке это будет доступно, например: complex<double> mAllElements[tensorIdx][kappa1][kappa2][kappa3]. То есть У меня 3 ранга-3-тензора (tensorIdx). Каждый тензор представляет собой трехмерный массив, индексированный kappa1, kappa2 и kappa3.
  • У меня есть 4 вложенных цикла (по всем 4 индексам), тогда как цикл kappa1 - это тот, который получает parallized (и является самым внешним). Они расположены в DoComputation().
  • В main() я вызываю DoComputation() один раз, чтобы получить некоторые ссылочные значения, а затем я вызываю его несколько раз и сравниваю результаты. Они должны соответствовать точно, но иногда они этого не делают.

К сожалению, код по-прежнему составляет около 190 строк. Я попытался упростить его (только 1 тензор ранга 1 и т.д.), Но потом я никогда не смог воспроизвести проблему. Я думаю, это происходит потому, что обращения к памяти не выровнены (цикл выше tensorIdx является самым внутренним) (я знаю, это далеко не оптимально).

Кроме того, некоторые задержки были необходимы в соответствующих местах, чтобы воспроизвести ошибку. В этом причина вызовов nops(). Без них код работает намного быстрее, но пока не показал проблему.

Обратите внимание, что я снова проверил критическую часть, CalcElementIdx(), и считаю ее правильной (каждый элемент обращается один раз). Я также запускал valgrind memcheck, helgrind и drd (с надлежащим перекомпилированным libgomp), и все трое не выдавали ошибок.

Выход

Каждые секунды до третьего запуска программы я получаю один или два несоответствия. Пример вывода:

41      Is exactly 0
42      Is exactly 0
43      Is exactly 0
44      Is exactly 0
45      348496
46      Is exactly 0
47      Is exactly 0
48      Is exactly 0
49      Is exactly 0

Это верно для gcc и icc.

Мой вопрос

Мой вопрос: Подходит ли код ниже для вас? (Помимо очевидных недостатков дизайна.) (Если он слишком длинный, я попытаюсь уменьшить его дальше, но, как описано выше, я провалился до сих пор.)

Код

Код был скомпилирован с помощью

g++ main.cc -O3 -Wall -Wextra -fopenmp

или

icc main.cc -O3 -Wall -Wextra -openmp

Обе версии показывают описанную проблему при запуске на двух процессорах с общим количеством 40 потоков. Я не мог заметить ошибку на 1 CPU (и столько потоков, сколько мне нравится).

// File: main.cc
#include <cmath>
#include <iostream>
#include <fstream>
#include <complex>
#include <cassert>
#include <iomanip>
#include <omp.h>

using namespace std;


// If defined: We add some nops in certain places, to get the timing right.
// Without them, I haven't observed the bug.
#define ENABLE_NOPS

// The size of each of the 3 tensors is: GRID_SIZE x GRID_SIZE x GRID_SIZE
static const int GRID_SIZE = 60;

//=============================================
// Produces several nops. Used to get correct "timings".

//----
template<int N> __attribute__((always_inline)) inline void nop()
{
    nop<N-1>();
    asm("nop;");
}

//----
template<> inline void nop<0>() { }

//----
__attribute__((always_inline)) inline void nops()
{
    nop<500>(); nop<500>(); nop<500>(); nop<500>(); nop<500>(); nop<500>(); nop<500>(); nop<500>(); nop<500>();
}




//=============================================
/*
Memory layout: We have 3 rank-3-tensors, i.e. 3 arrays of dimension 3.
The layout looks like this: complex<double> allElements[tensorIdx][kappa1][kappa2][kappa3];
The kappas represent the indices into a certain tensor, and are all in the interval [0; GRID_SIZE-1].
*/
class MemoryManagerFFTW
{
public:
  //---------- Constructor ----------
  MemoryManagerFFTW()
  {
    mAllElements = new complex<double>[GetTotalNumElements()];
  }

  //---------- Destructor ----------
  ~MemoryManagerFFTW() 
  { 
    delete[] mAllElements; 
  }

  //---------- SetElement ----------
  void SetElement(int tensorIdx, int kappa1, int kappa2, int kappa3, const complex<double>& newVal)
  {
    // Out-of-bounds error checks are done in this function.
    const size_t idx = CalcElementIdx(tensorIdx, kappa1, kappa2, kappa3);

    // These nops here are important to reproduce the bug.
#if defined(ENABLE_NOPS)
    nops();
    nops();
#endif

    // A flush makes the bug appear more often.
    // #pragma omp flush
    mAllElements[idx] = newVal;

    // This was never false, although the same check is false in DoComputation() from time to time.
    assert(newVal == mAllElements[idx]);
  }

  //---------- GetElement ----------
  const complex<double>& GetElement(int tensorIdx, int kappa1, int kappa2, int kappa3)const
  {  
    const size_t idx = CalcElementIdx(tensorIdx, kappa1, kappa2, kappa3);
    return mAllElements[idx];
  }


  //---------- CalcElementIdx ----------
  size_t CalcElementIdx(int tensorIdx, int kappa1, int kappa2, int kappa3)const
  {
    // We have 3 tensors (index by "tensorIdx"). Each tensor is of rank 3. In memory, they are placed behind each other.
    // tensorStartIdx is the index of the first element in the tensor.
    const size_t tensorStartIdx = GetNumElementsPerTensor() * tensorIdx;

    // Index of the element relative to the beginning of the tensor. A tensor is a 3dim. array of size GRID_SIZE x GRID_SIZE x GRID_SIZE
    const size_t idxInTensor = kappa3 + GRID_SIZE * (kappa2 + GRID_SIZE * kappa1);

    const size_t finalIdx = tensorStartIdx + idxInTensor;
    assert(finalIdx < GetTotalNumElements());

    return finalIdx;
  }


  //---------- GetNumElementsPerTensor & GetTotalNumElements ----------
  size_t GetNumElementsPerTensor()const { return GRID_SIZE * GRID_SIZE * GRID_SIZE; }
  size_t GetTotalNumElements()const { return NUM_TENSORS * GetNumElementsPerTensor(); }



public:
  static const int NUM_TENSORS = 3; // The number of tensors.
  complex<double>* mAllElements; // All tensors. An array [tensorIdx][kappa1][kappa2][kappa3]
};




//=============================================
void DoComputation(MemoryManagerFFTW& mSingleLayerManager)
{
  // Parallize outer loop.
  #pragma omp parallel for
  for (int kappa1 = 0; kappa1 < GRID_SIZE; ++kappa1)
  {
    for (int kappa2 = 0; kappa2 < GRID_SIZE; ++kappa2)
    {
      for (int kappa3 = 0; kappa3 < GRID_SIZE; ++kappa3)
      {    
#ifdef ENABLE_NOPS
        nop<50>();
#endif
        const double k2 = kappa1*kappa1 + kappa2*kappa2 + kappa3*kappa3;
        for (int j = 0; j < 3; ++j)
        {
          // Compute and set new result.
          const complex<double> curElement = mSingleLayerManager.GetElement(j, kappa1, kappa2, kappa3);
          const complex<double> newElement = exp(-k2) * k2 * curElement;

          mSingleLayerManager.SetElement(j, kappa1, kappa2, kappa3, newElement);

          // Check if the results has been set correctly. This is sometimes false, but _not_ always when the result is incorrect.
          const complex<double> test = mSingleLayerManager.GetElement(j, kappa1, kappa2, kappa3);
          if (test != newElement)
            printf("Failure: (%g, %g) != (%g, %g)\n", test.real(), test.imag(), newElement.real(), newElement.imag());
        }
      }
    }
  }
}



//=============================================
int main()
{
  cout << "Max num. threads: " << omp_get_max_threads() << endl;

  // Call DoComputation() once to get a reference-array.
  MemoryManagerFFTW reference;
  for (size_t i = 0; i < reference.GetTotalNumElements(); ++i)
    reference.mAllElements[i] = complex<double>((double)i, (double)i+0.5);
  DoComputation(reference);

  // Call DoComputation() several times, and each time compare the result to the reference.
  const size_t NUM = 1000;
  for (size_t curTry = 0; curTry < NUM; ++curTry)
  {
    MemoryManagerFFTW mSingleLayerManager;
    for (size_t i = 0; i < mSingleLayerManager.GetTotalNumElements(); ++i)
      mSingleLayerManager.mAllElements[i] = complex<double>((double)i, (double)i+0.5);
    DoComputation(mSingleLayerManager);

    // Get the max. difference. This *should* be 0, but isn't from time to time.
    double maxDiff = -1;
    for (size_t i = 0; i < mSingleLayerManager.GetTotalNumElements(); ++i)
    {
      const complex<double> curDiff = mSingleLayerManager.mAllElements[i] - reference.mAllElements[i];
      maxDiff = max(maxDiff, max(curDiff.real(), curDiff.imag()));
    }

    if (maxDiff != 0)
      cout << curTry << "\t" << maxDiff << endl;
    else
      cout << curTry << "\t" << "Is exactly 0" << endl;
  }

  return 0;
}

Изменить

Как видно из комментариев и ответа Zboson ниже, в ядре 3.11.10-7 произошла ошибка. После обновления до 3.15.0-1 моя проблема исчезла, и код работает так, как должен.

Ответы

Ответ 1

Проблема возникла из-за ошибки в ядре ядра ядра ядра 3.11.10-7. Ошибка может быть связана с тем, как ядро ​​обрабатывает недействительность кэша TLB, как отметил Христо Ильев. Я догадался, что ядро ​​может быть проблемой, потому что я читал, что в Linux Kernel 3.15 для систем NUMA будут внесены некоторые улучшения, поэтому я решил, что ядро версия важна для систем NUMA.

Когда OP обновил ядро ​​Linux своей системы NUMA до 3.15.0-1, проблема исчезла.