Чтение глобального флага не работает для обмена данными CPU>GPU в CUDA

Я пытаюсь сделать простую однонаправленную связь между процессором и графическим процессором 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.

Любая идея, почему это не работает?

volatile не дает никаких гарантий относительно атомарности, видимости или порядка доступа между потоками — это не подходит для многопоточной синхронизации. См. Гарантирует ли «volatile» что-либо вообще в переносимом коде C для многоядерных систем? , или, точнее, Является изменчивым. Полезен с потоками
Andrew Henle 08.02.2023 13:45

Также прочитайте это: web.archive.org/web/20120210232850/http://software.intel.com‌​/… и это open-std.org/jtc1/sc22/wg21/docs/papers/ 2006/n2016.html, если вы не уверены, volatile "почти бесполезен для многопоточного программирования".

Andrew Henle 08.02.2023 13:58

@AndrewHenle Я знаю об этом. Я НЕ использую volatile для каких-либо многопоточных целей. (Я защищаю критический указатель логикой приложения и последовательным доступом). Я использую volatile только в его первоначальном назначении: сообщаю компилятору nvcc, что глобальная память графического процессора может измениться в любое время, поскольку к ней обращается отдельное оборудование, а именно ЦП, и он должен игнорировать кеши, а вместо этого выполнять дорогостоящий глобальный доступ к памяти. Это также рекомендуется в документации CUDA.

Silicomancer 08.02.2023 14:09

среди возможных других проблем, cudaMemcpy не может работать одновременно с вашим ядром, которое у вас есть в настоящее время. Это связано с потоками CUDA. Существует ряд концепций, необходимых для того, чтобы такая сигнализация работала, здесь пример, который охватывает большинство, хотя это не единственный способ. И да, я понимаю, что это сигнализация в другом направлении, я не предлагаю это полное решение. И если вы пытаетесь сделать это в Windows WDDM, это очень сложно.

Robert Crovella 08.02.2023 16:16

@RobertCrovella Я использую Linux. Не могли бы вы рассказать немного подробнее? Пример не имеет много пояснений или комментариев. Глядя на этот код, я не уверен, что является решающим моментом. Он использует такой же опрос памяти, как и мой код. Я вижу 3 отличия: 1) использование сопоставленной закрепленной памяти 2) вызовы __threadfence_system() 3) доступ atomicAdd().

Silicomancer 08.02.2023 22:19

2) кажется, применяется только в направлении GPU-> CPU (поскольку __threadfence_system() только для устройства) и 3) кажется, не применяется, так как мне нужна простая запись с одной стороны и простое чтение с другой стороны, а не RMW операция. Означает ли это, что сопоставленная закрепленная память является ключом?

Silicomancer 08.02.2023 22:20

Ваш вопрос, почему это не работает. Вы знаете что-нибудь о потоках CUDA? Это важно. Да, сопоставленная закрепленная память — это один из способов обойти ограничение, которое потоки CUDA накладывают на то, как вы пытаетесь общаться.

Robert Crovella 08.02.2023 22:59

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

Silicomancer 08.02.2023 23:25
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
8
79
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Фундаментальная проблема, которую я вижу в вашем подходе, заключается в том, что потоки cuda не позволят ему работать.

Потоки CUDA имеют два основных принципа:

  1. Элементы, выпущенные в один и тот же поток, не будут перекрываться; они будут сериализованы.
  2. Элементы, выдаваемые в отдельные созданные потоки, могут перекрываться; нет определенного порядка этих операций, предоставляемых 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
$

ПРИМЕЧАНИЯ:

  • Хотя это не очевидно из распечатки статического текста, программа тратит около 5 секунд до выхода. Если вы закомментируете такую ​​строку, как *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() для закрепленной памяти, где я ожидал параметр-указатель и простой доступ, имеет ли это смысл для вас?

Silicomancer 09.02.2023 13:11

Да, это имеет смысл для меня. Закрепленную память можно использовать несколькими способами; помимо того, что я показал здесь, у него есть и другие важные применения. Возможно, вам следует изучить онлайн-тренинг, на который я дал ссылку? Я не буду отвечать здесь на вопросы, которые уже обсуждались на онлайн-тренинге.

Robert Crovella 09.02.2023 15:53

Другие вопросы по теме