Почему графические процессоры 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.
для Pascal Titan время передачи памяти составляет около 202 мс, а запуск ядра - вокруг безумно длинного 8343 us, что делает меня поверьте, что-то не так.
Я далее изолировать проблему, заменив 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 США.
для Титана Pascal, время для передачи памяти около 5 мс в обоих направлениях, а запуск ядра-около 194 us, что на самом деле приводит к увеличению производительности, которое я надеюсь увидеть...
почему 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, произойдет ошибка страницы. Чистый эффект этой ошибки страницы:
- вызовите сбой кода ядра GPU (поток или потоки, которые получили доступ к странице) (до шага 2 complete)
- вызовите перенос этой страницы памяти с процессора на 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.