Я пытаюсь сделать простую однонаправленную связь между процессором и графическим процессором K80 с помощью CUDA. Я хочу иметь флаг отмены bool
, который находится в глобальной памяти и опрашивается всеми запущенными потоками GPU/ядра. По умолчанию флаг должен быть установлен на false
и может быть установлен потоком ЦП/хоста на true
во время текущих вычислений. После этого потоки GPU/ядра должны выйти.
Это то, что я пробовал. У меня упрощенный код. Я удалил проверку ошибок и логику приложения (включая логику приложения, которая предотвращает одновременный доступ к cancelRequested
).
На стороне хоста глобальное определение (.cpp):
// Host side thread safety of this pointer is covered by application logic
volatile bool* cancelRequested = nullptr;
На стороне хоста в потоке вычислений (.cpp):
initialize(&cancelRequested);
compute(cancelRequested);
finalize(&cancelRequested);
На стороне хоста в основном потоке (.cpp):
cancel(cancelRequested); // Called after init is finished
Подпрограммы хоста (файл .cu):
void initialize(volatile bool** pCancelRequested)
{
cudaMalloc(const_cast<bool**>(pCancelRequested), sizeof(bool));
const bool aFalse = false;
cudaMemcpy(*const_cast<bool**>(pCancelRequested), &aFalse, sizeof(bool), cudaMemcpyHostToDevice);
}
void compute(volatile bool* pCancelRequested)
{
....
computeKernel<<<pBlocksPerGPU, aThreadsPerBlock>>>(pCancelRequested);
cudaDeviceSynchronize(); // Non-busy wait
....
}
void finalize(volatile bool** pCancelRequested)
{
cudaFree(*const_cast<bool**>(pCancelRequested));
*pCancelRequested = nullptr;
}
void cancel(volatile bool* pCancelRequested)
{
const bool aTrue = true;
cudaMemcpy(const_cast<bool*>(pCancelRequested), &aTrue, sizeof(bool), cudaMemcpyHostToDevice);
}
Подпрограммы устройства (файл .cu):
__global__ void computeKernel(volatile bool* pCancelRequested)
{
while (someCondition)
{
// Computation step here
if (*pCancelRequested)
{
printf("-> Cancel requested!\n");
return;
}
}
}
Код работает нормально. Но он никогда не входит в случай отмены. Я успешно прочитал значения false
и true
в initialize()
и cancel()
и проверил их с помощью gdb. т.е. запись в глобальный флаг работает нормально, по крайней мере, с точки зрения хоста. Однако ядра никогда не видят флаг отмены, установленный на true
, и нормально выходят из внешнего цикла while
.
Любая идея, почему это не работает?
Также прочитайте это: web.archive.org/web/20120210232850/http://software.intel.com/… и это open-std.org/jtc1/sc22/wg21/docs/papers/ 2006/n2016.html, если вы не уверены, volatile
"почти бесполезен для многопоточного программирования".
@AndrewHenle Я знаю об этом. Я НЕ использую volatile для каких-либо многопоточных целей. (Я защищаю критический указатель логикой приложения и последовательным доступом). Я использую volatile только в его первоначальном назначении: сообщаю компилятору nvcc, что глобальная память графического процессора может измениться в любое время, поскольку к ней обращается отдельное оборудование, а именно ЦП, и он должен игнорировать кеши, а вместо этого выполнять дорогостоящий глобальный доступ к памяти. Это также рекомендуется в документации CUDA.
среди возможных других проблем, cudaMemcpy
не может работать одновременно с вашим ядром, которое у вас есть в настоящее время. Это связано с потоками CUDA. Существует ряд концепций, необходимых для того, чтобы такая сигнализация работала, здесь пример, который охватывает большинство, хотя это не единственный способ. И да, я понимаю, что это сигнализация в другом направлении, я не предлагаю это полное решение. И если вы пытаетесь сделать это в Windows WDDM, это очень сложно.
@RobertCrovella Я использую Linux. Не могли бы вы рассказать немного подробнее? Пример не имеет много пояснений или комментариев. Глядя на этот код, я не уверен, что является решающим моментом. Он использует такой же опрос памяти, как и мой код. Я вижу 3 отличия: 1) использование сопоставленной закрепленной памяти 2) вызовы __threadfence_system() 3) доступ atomicAdd().
2) кажется, применяется только в направлении GPU-> CPU (поскольку __threadfence_system() только для устройства) и 3) кажется, не применяется, так как мне нужна простая запись с одной стороны и простое чтение с другой стороны, а не RMW операция. Означает ли это, что сопоставленная закрепленная память является ключом?
Ваш вопрос, почему это не работает. Вы знаете что-нибудь о потоках CUDA? Это важно. Да, сопоставленная закрепленная память — это один из способов обойти ограничение, которое потоки CUDA накладывают на то, как вы пытаетесь общаться.
Хорошо спасибо. Однажды я просмотрел потоки, но никогда их не использовал (т.е. всегда использовал поток по умолчанию), так как они мне не нужны для моего проекта. Еще меньше я знаю о отображенной закрепленной памяти. Я буду читать дальше.
Фундаментальная проблема, которую я вижу в вашем подходе, заключается в том, что потоки cuda не позволят ему работать.
Потоки CUDA имеют два основных принципа:
Даже если вы явно не используете потоки, вы работаете в «потоке по умолчанию», и применяется та же семантика потока.
Я не рассказываю все, что нужно знать о потоках, в этом кратком обзоре. Вы можете узнать больше о потоках CUDA в разделе 7 этой серии онлайн-обучения
Из-за потоков CUDA это:
computeKernel<<<pBlocksPerGPU, aThreadsPerBlock>>>(pCancelRequested);
и это:
cudaMemcpy(const_cast<bool*>(pCancelRequested), &aTrue, sizeof(bool), cudaMemcpyHostToDevice);
не могут перекрываться (они выпускаются в один и тот же поток CUDA «по умолчанию», поэтому правило 1 выше говорит, что они не могут перекрываться). Но перекрытие необходимо, если вы хотите «сигнализировать» работающему ядру. Мы должны разрешить выполнение операции cudaMemcpy
одновременно с запуском ядра.
Мы можем исправить это с помощью прямого применения потоков CUDA (принимая во внимание правило 2 выше), чтобы поместить операцию копирования и операцию вычисления (ядра) в отдельные созданные потоки, чтобы они могли перекрываться. Когда мы это делаем, все работает так, как хотелось бы:
$ cat t2184.cu
#include <iostream>
#include <unistd.h>
__global__ void k(volatile int *flag){
while (*flag != 0);
}
int main(){
int *flag, *h_flag = new int;
cudaStream_t s[2];
cudaStreamCreate(s+0);
cudaStreamCreate(s+1);
cudaMalloc(&flag, sizeof(h_flag[0]));
*h_flag = 1;
cudaMemcpy(flag, h_flag, sizeof(h_flag[0]), cudaMemcpyHostToDevice);
k<<<32, 256, 0, s[0]>>>(flag);
sleep(5);
*h_flag = 0;
cudaMemcpyAsync(flag, h_flag, sizeof(h_flag[0]), cudaMemcpyHostToDevice, s[1]);
cudaDeviceSynchronize();
}
$ nvcc -o t2184 t2184.cu
$ compute-sanitizer ./t2184
========= COMPUTE-SANITIZER
========= ERROR SUMMARY: 0 errors
$
ПРИМЕЧАНИЯ:
*h_flag = 0;
, то программа зависнет, указывая на то, что метод сигнала флага работает правильно.volatile
. Это необходимо указать компилятору, что любой доступ к этим данным должен быть фактическим доступом, компилятору не разрешено вносить изменения, которые препятствуют чтению или записи памяти в ожидаемом месте.Такое поведение сигнала хост->устройство также может быть реализовано без явного использования потоков, но с закрепленной хостом памятью в качестве местоположения сигнализации, поскольку она «видима» как хосту, так и коду устройства «одновременно». Вот пример:
#include <iostream>
#include <unistd.h>
__global__ void k(volatile int *flag){
while (*flag != 0);
}
int main(){
int *flag;
cudaHostAlloc(&flag, sizeof(flag[0]), cudaHostAllocDefault);
*flag = 1;
k<<<32, 256>>>(flag);
sleep(5);
*flag = 0;
cudaDeviceSynchronize();
}
Для других примеров передачи сигналов, таких как от устройства к хосту, других читателей может заинтересовать это.
Просто прочитал и нашел этот обзор: medium.com/analytics-vidhya/cuda-memory-model-823f02cef0bf Из вашего поста я понял, что закрепленная память хоста также видна устройству и что указатель может быть передан напрямую в и доступ к которому осуществляется ядром. Однако в приведенном выше обзоре автор использует явный cudaMemcpy() для закрепленной памяти, где я ожидал параметр-указатель и простой доступ, имеет ли это смысл для вас?
Да, это имеет смысл для меня. Закрепленную память можно использовать несколькими способами; помимо того, что я показал здесь, у него есть и другие важные применения. Возможно, вам следует изучить онлайн-тренинг, на который я дал ссылку? Я не буду отвечать здесь на вопросы, которые уже обсуждались на онлайн-тренинге.
volatile
не дает никаких гарантий относительно атомарности, видимости или порядка доступа между потоками — это не подходит для многопоточной синхронизации. См. Гарантирует ли «volatile» что-либо вообще в переносимом коде C для многоядерных систем? , или, точнее, Является изменчивым. Полезен с потоками