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

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

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

CUDASample.византийским

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;

а для старого Максвелла Titan X это:

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

EDIT: вот результаты для запуска визуального профилирования NVIDIA.

для старого Maxwell Titan время передачи памяти составляет около 205 мс, а запуск ядра-около 268 us. enter image description here

для Pascal Titan время передачи памяти составляет около 202 мс, а запуск ядра - вокруг безумно длинного 8343 us, что делает меня поверьте, что-то не так. enter image description here

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

CUDASample.ку

__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 США. enter image description here

для Титана Pascal, время для передачи памяти около 5 мс в обоих направлениях, а запуск ядра-около 194 us, что на самом деле приводит к увеличению производительности, которое я надеюсь увидеть... enter image description here

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

3 ответов


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

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

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

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

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

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

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

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

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

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

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

в качестве дополнительного примечания в CUDA 9 режим подкачки спроса для pascal и за его пределами доступен только в linux; предыдущая поддержка Windows, объявленная в CUDA 8, была отменена. См.здесь. В windows, даже для устройств Pascal и за ее пределами, начиная с CUDA 9, режим UM такой же, как у maxwell и предыдущих устройств; данные переносятся на GPU En-masse при запуске ядра.

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


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

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

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


Это ошибка NVIDIA в системах Windows, ведьма происходит с архитектурой PASCAL.

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

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