Почему графические процессоры NVIDIA Pascal медленнее работают с ядрами CUDA при использовании cudaMallocManaged

Я тестировал новый CUDA 8 вместе с графическим процессором Pascal Titan X и ожидаю ускорения для моего кода, но по какой-то причине он заканчивается медленнее. Я на Ubuntu 16.04.

Вот минимальный код, который может воспроизвести результат:

CUDASample.cuh

class CUDASample{
 public:
  void AddOneToVector(std::vector<int> &in);
};

CUDASample.cu

__global__ static void CUDAKernelAddOneToVector(int *data)
{
  const int x  = blockIdx.x * blockDim.x + threadIdx.x;
  const int y  = blockIdx.y * blockDim.y + threadIdx.y;
  const int mx = gridDim.x * blockDim.x;

  data[y * mx + x] = data[y * mx + x] + 1.0f;
}

void CUDASample::AddOneToVector(std::vector<int> &in){
  int *data;
  cudaMallocManaged(reinterpret_cast<void **>(&data),
                    in.size() * sizeof(int),
                    cudaMemAttachGlobal);

  for (std::size_t i = 0; i < in.size(); i++){
    data[i] = in.at(i);
  }

  dim3 blks(in.size()/(16*32),1);
  dim3 threads(32, 16);

  CUDAKernelAddOneToVector<<<blks, threads>>>(data);

  cudaDeviceSynchronize();

  for (std::size_t i = 0; i < in.size(); i++){
    in.at(i) = data[i];
  }

  cudaFree(data);
}

main.cpp

std::vector<int> v;

for (int i = 0; i < 8192000; i++){
  v.push_back(i);
}

CUDASample cudasample;

cudasample.AddOneToVector(v);

Единственное отличие - флаг NVCC, который для Pascal Titan X:

-gencode arch=compute_61,code=sm_61-std=c++11;

а для старого Maxwell Titan X:

-gencode arch=compute_52,code=sm_52-std=c++11;

EDIT: Вот результаты для работы с графическим профилем NVIDIA.

Для старого Maxwell Titan время передачи памяти составляет около 205 мс, а запуск ядра составляет около 268 нас. введите описание изображения здесь

Для Pascal Titan время для передачи памяти составляет около 202 мс, а запуск ядра - безумно длинный 8343 нас, что заставляет меня думать, что что-то не так. введите описание изображения здесь

Я также выделил проблему, заменив cudaMallocManaged на старый добрый cudaMalloc и сделал некоторые профилирования и наблюдал некоторый интересный результат.

CUDASample.cu

__global__ static void CUDAKernelAddOneToVector(int *data)
{
  const int x  = blockIdx.x * blockDim.x + threadIdx.x;
  const int y  = blockIdx.y * blockDim.y + threadIdx.y;
  const int mx = gridDim.x * blockDim.x;

  data[y * mx + x] = data[y * mx + x] + 1.0f;
}

void CUDASample::AddOneToVector(std::vector<int> &in){
  int *data;
  cudaMalloc(reinterpret_cast<void **>(&data), in.size() * sizeof(int));
  cudaMemcpy(reinterpret_cast<void*>(data),reinterpret_cast<void*>(in.data()), 
             in.size() * sizeof(int), cudaMemcpyHostToDevice);

  dim3 blks(in.size()/(16*32),1);
  dim3 threads(32, 16);

  CUDAKernelAddOneToVector<<<blks, threads>>>(data);

  cudaDeviceSynchronize();

  cudaMemcpy(reinterpret_cast<void*>(in.data()),reinterpret_cast<void*>(data), 
             in.size() * sizeof(int), cudaMemcpyDeviceToHost);

  cudaFree(data);
}

Для старого Maxwell Titan время для передачи памяти составляет около 5 мс в обоих направлениях, а запуск ядра составляет около 264 нас. введите описание изображения здесь

Для Pascal Titan время для передачи памяти составляет около 5 мс в обоих направлениях, а запуск ядра - около 194, что на самом деле приводит к увеличению производительности. Я надеюсь увидеть... введите описание изображения здесь

Почему Pascal GPU так медленно работает при запуске ядер CUDA, когда используется cudaMallocManaged? Это будет пародия, если мне нужно вернуть весь мой существующий код, который использует cudaMallocManaged в cudaMalloc. Этот эксперимент также показывает, что время передачи памяти с использованием cudaMallocManaged намного медленнее, чем использование cudaMalloc, что также кажется неправильным. Если использование этого результата приводит к медленному времени выполнения, даже код проще, это должно быть неприемлемым, поскольку вся цель использования CUDA вместо простого С++ - ускорить процесс. Что я делаю неправильно и почему я наблюдаю этот результат?

Ответ 1

В CUDA 8 с графическими процессорами Pascal миграция данных управляемой памяти в режиме унифицированной памяти (UM) обычно происходит иначе, чем в предыдущих архитектурах, и вы испытываете последствия этого. (Также см. Примечание в конце об обновленном поведении CUDA 9 для Windows.)

В предыдущих архитектурах (например, Maxwell) управляемые выделения, используемые определенным вызовом ядра, будут мигрированы сразу после запуска ядра примерно так, как если бы вы вызывали cudaMemcpy для перемещения данных самостоятельно.

В графических процессорах CUDA 8 и Pascal миграция данных происходит через пейджинг по требованию. По умолчанию при запуске ядра никакие данные не переносятся на устройство явным образом (*). Когда код устройства GPU пытается получить доступ к данным на определенной странице, которая не находится в памяти GPU, произойдет сбой страницы. Общий эффект от этой ошибки страницы:

  1. Заставить код ядра графического процессора (поток или потоки, которые обращались к странице) зависнуть (до завершения шага 2)
  2. Заставить эту страницу памяти мигрировать из CPU в GPU

Этот процесс будет повторяться по мере необходимости, поскольку код графического процессора затрагивает различные страницы данных. Последовательность операций, выполняемых на шаге 2 выше, включает некоторую задержку при обработке ошибки страницы в дополнение к времени, потраченному на фактическое перемещение данных. Поскольку этот процесс будет перемещать данные на страницу за раз, он может быть значительно менее эффективным, чем перемещение всех данных за один раз, либо с использованием cudaMemcpy либо через механизм UM, предшествующий Pascal, который вызывал перемещение всех данных при запуске ядра (будь то это было нужно или нет, и независимо от того, когда код ядра действительно нуждался в этом).

Оба подхода имеют свои плюсы и минусы, и я не хочу обсуждать достоинства или различные мнения или точки зрения. Процесс подкачки по требованию предоставляет множество важных функций и возможностей для графических процессоров Pascal.

Этот конкретный пример кода, однако, не приносит пользы. Это ожидалось, и поэтому рекомендуемое использование для приведения поведения в соответствие с предыдущим поведением/производительностью (например, maxwell) должно предшествовать запуску ядра cudaMemPrefetchAsync().

Вы бы использовали семантику потока CUDA, чтобы принудительно завершить этот вызов до запуска ядра (если при запуске ядра не указан поток, вы можете передать NULL для параметра потока, чтобы выбрать поток по умолчанию). Я полагаю, что другие параметры для этого вызова функции довольно очевидны.

С помощью этого вызова функции перед вызовом ядра, охватывающего рассматриваемые данные, вы не должны наблюдать каких-либо сбоев страниц в случае Паскаля, а поведение профиля должно быть аналогично случаю Максвелла.

Как я упоминал в комментариях, если бы вы создали тестовый пример, который включал два последовательных вызова ядра, вы бы заметили, что второй вызов выполняется примерно на полной скорости даже в случае Паскаля, поскольку все данные уже перенесены на стороне GPU через первое выполнение ядра. Следовательно, использование этой функции предварительной выборки не должно считаться обязательным или автоматическим, а должно использоваться продуманно. Существуют ситуации, когда графический процессор может в некоторой степени скрыть задержку сбоя страницы, и, очевидно, данные, уже находящиеся в графическом процессоре, не требуют предварительной выборки.

Обратите внимание, что "остановка", упомянутая на шаге 1 выше, может вводить в заблуждение Доступ к памяти сам по себе не вызывает остановку. Но если запрашиваемые данные действительно необходимы для операции, например, умножения, то деформация будет останавливаться при операции умножения, пока необходимые данные не станут доступными. С этим связано то, что подкачка по требованию данных с хоста на устройство таким способом - это просто еще одна "задержка", которую GPU может скрыть в своей архитектуре, скрывающей задержку, если имеется достаточно другой доступной "работы" для участия. к.

В качестве дополнительного примечания, в CUDA 9 режим подкачки по требованию для паскаля и выше доступен только в Linux; предыдущая поддержка Windows, объявленная в CUDA 8, была прекращена. Смотрите здесь На окнах, даже для устройств Pascal и выше, в CUDA 9 режим единой системы обмена сообщениями такой же, как у maxwell и предыдущих устройств; данные переносятся в GPU в массовом порядке при запуске ядра.

(*) Здесь предполагается, что данные являются "резидентными" на хосте, то есть уже "затронуты" или инициализированы в коде ЦП после вызова управляемого выделения. Само управляемое распределение создает страницы данных, связанные с устройством, и когда код ЦП "касается" этих страниц, среда выполнения CUDA запрашивает страницу, необходимую для размещения в памяти хоста, чтобы процессор мог их использовать. Если вы выполняете выделение, но никогда не "касаетесь" данных в коде ЦП (странная ситуация, вероятно), тогда они фактически уже будут "резидентными" в памяти устройства при запуске ядра, и наблюдаемое поведение будет другим. Но это не тот случай для данного конкретного примера/вопроса.

Дополнительная информация доступна в этой статье блога.

Ответ 2

Я могу воспроизвести это в трех программах на 1060 и 1080. В качестве примера я использую визуализацию voulme с процедурной передаточной функцией, которая была почти интерактивной в реальном времени на 960, но на 1080 это небольшое шоу. Все данные хранятся только в текстурах только для чтения, и только мои передаточные функции находятся в управляемой памяти. В отличие от моего другого кода рендеринг объема выполняется особенно медленно, это зависит от моего другого кода, который передаёт мои передачи от ядра к другим методам устройства.

Я верю, что это не только вызов ядер с данными cudaMallocManaged. Моя экспирация идет к тому, что каждый вызов ядра или метода устройства имеет такое поведение, и эффект складывается. Кроме того, основой для рендеринга объема являются части предоставленные CudaSample без управляемой памяти, которые работают, как ожидалось, на Maxwell с графическим процессором pascal (1080, 1060,980Ti, 980,960).

Я только вчера нашел эту ошибку, потому что мы изменили все системы reurechure на pascal. Я буду профилировать свое программное обеспечение в следующие дни на 980 в comapre до 1080. Я еще не уверен, что я должен сообщить об ошибке в зоне разработки NVIDIA.

Ответ 3

это ОШИБКА NVIDIA в системах Windows, которая возникает с архитектурой PASCAL.

Я знаю это с нескольких дней, но не мог написать его здесь, потому что я был в отпуске без подключения к интернету.

Подробнее см. комментарии: https://devblogs.nvidia.com/parallelforall/unified-memory-cuda-beginners/ где Марк Харрис из NVIDIA подтверждает ошибку. Он должен быть исправлен с помощью CUDA 9. Он также сообщает, что он должен быть передан Microsoft, чтобы помочь причину. Но до сих пор я не нашел подходящую страницу отчета об ошибках Microsoft.