Який канонічний спосіб перевірити помилки за допомогою API виконання CUDA?


258

Переглядаючи відповіді та коментарі до питань CUDA та у вікі тегів CUDA , я бачу, що часто пропонується, щоб статус повернення кожного дзвінка API був перевірений на наявність помилок. Документація API містить такі функції, як cudaGetLastError, cudaPeekAtLastErrorі cudaGetErrorString, але який найкращий спосіб зібрати їх для надійного лову та повідомлення про помилки, не вимагаючи багато зайвого коду?


13
Зразки CUDA NVIDIA містять заголовок, helper_cuda.h, який має макроси, які називаються getLastCudaErrorі checkCudaErrors, які виконують майже все, що описано у прийнятій відповіді . Дивіться зразки для демонстрацій. Просто виберіть, щоб встановити зразки разом із інструментарієм, і у вас це буде.
chappjc

@chappjc Я не вважаю, що це питання і відповідь претендує на оригінальність, якщо це саме ви маєте на увазі, але це заслуга мати освічених людей, які використовують перевірку помилок CUDA.
JackOLantern

@JackOLantern Ні, це не те, що я мав на увазі. Це питання мені дуже допомогло, і його, звичайно, простіше знайти, ніж якийсь заголовок у SDK. Я вважав, що цінністю слід зазначити, що це також, як NVIDIA поводиться з цим і де шукати більше. Я хотів би пом’якшити тон свого коментаря, якби міг. :)
chappjc

Інструменти налагодження, що дозволяють "підходити" там, де починаються помилки, значно покращилися з 2012 року на CUDA. Я не працював з налагоджувачами на основі GUI, але у вікі тегів CUDA згадується командний рядок cuda-gdb. Це ДУЖЕ потужний інструмент, оскільки він дозволяє переглядати фактичні основи та нитки на самому графічному процесорі (хоча більшість часу потребує архітектури 2.0+)
зновуренко

@bluefeet: яка угода була з редакцією, яку ви скасували назад? Виглядало так, що насправді нічого не змінилося у відмітці, але це було прийнято як редагування. Чи було щось нахабне на роботі?
талонії

Відповіді:


304

Мабуть, найкращий спосіб перевірити помилки в коді API часу виконання - це визначити функцію обробника стилю затвердження та макрос обгортки, як це:

#define gpuErrchk(ans) { gpuAssert((ans), __FILE__, __LINE__); }
inline void gpuAssert(cudaError_t code, const char *file, int line, bool abort=true)
{
   if (code != cudaSuccess) 
   {
      fprintf(stderr,"GPUassert: %s %s %d\n", cudaGetErrorString(code), file, line);
      if (abort) exit(code);
   }
}

Потім ви можете обернути кожен виклик API gpuErrchkмакросом, який буде обробляти стан повернення API, який він називає, наприклад:

gpuErrchk( cudaMalloc(&a_d, size*sizeof(int)) );

Якщо у виклику сталася помилка, буде надсилатися текстове повідомлення, що описує помилку, і файл та рядок у вашому коді, де сталася помилка, stderrі програма вийде. Можна, можливо, змінити, gpuAssertщоб створити виняток, а не викликати exit()в більш досконалому додатку, якщо цього потрібно.

Другий пов'язаний питання полягає в тому, як перевірити помилки при запуску ядра, які не можуть бути безпосередньо загорнуті в макро-виклик, як стандартні дзвінки API виконання. Для ядер щось подібне:

kernel<<<1,1>>>(a);
gpuErrchk( cudaPeekAtLastError() );
gpuErrchk( cudaDeviceSynchronize() );

спочатку перевірять недійсний аргумент запуску, а потім змушують хоста чекати, поки ядро ​​зупиниться, і перевірять помилку виконання. Синхронізацію можна усунути, якщо у вас є наступний блокуючий виклик API, такий:

kernel<<<1,1>>>(a_d);
gpuErrchk( cudaPeekAtLastError() );
gpuErrchk( cudaMemcpy(a_h, a_d, size * sizeof(int), cudaMemcpyDeviceToHost) );

у такому випадку cudaMemcpyвиклик може повернути або помилки, які виникли під час виконання ядра, або помилки із самої копії пам'яті. Це може бентежити початківця, і я рекомендую використовувати явну синхронізацію після запуску ядра під час налагодження, щоб легше зрозуміти, де можуть виникати проблеми.

Зауважте, що при використанні динамічного паралелізму CUDA дуже схожа методологія може бути та повинна застосовуватися до будь-якого використання API виконання CUDA в ядрах пристрою, а також після запуску ядра пристрою:

#include <assert.h>
#define cdpErrchk(ans) { cdpAssert((ans), __FILE__, __LINE__); }
__device__ void cdpAssert(cudaError_t code, const char *file, int line, bool abort=true)
{
   if (code != cudaSuccess)
   {
      printf("GPU kernel assert: %s %s %d\n", cudaGetErrorString(code), file, line);
      if (abort) assert(0);
   }
}

8
@harism: Я не думаю, що так. Wiki Wiki призначений для запитань або відповідей, які часто редагуються. Це не одна з таких
талонії

1
чи не слід додавати і cudaDeviceReset()до виходу? І застереження про розсилку пам’яті?
Аврелій

2
@talonmies: Для виконання дзвінків під час виконання Async CUDA, таких як cudaMemsetAsync та cudaMemcpyAsync, чи потрібно також синхронізувати gpu-пристрій та хост-потік через виклик до gpuErrchk (cudaDeviceSynchronize ())?
нурабха

2
Зауважте, що явна синхронізація після запуску ядра не є помилковою, але може сильно змінити продуктивність виконання та переплетення семантики. Якщо ви використовуєте переплетення, то явна синхронізація для налагодження може приховати цілий клас помилок, які можуть бути важко відслідковувати в збірці Release.
masterxilo

Чи є спосіб отримати більш конкретні помилки для виконання ядра? Усі помилки, які я отримую, просто дають мені номер рядка з хостового коду, а не з ядра.
Азмісов

70

Відповідь талонії вище - це прекрасний спосіб перервати заявку в assertстилі.

Інколи, можливо, ми хочемо повідомити про стан помилки та відновити її в контексті C ++ як частину більшого додатка.

Ось досить розумний спосіб зробити це, кинувши виняток C ++, отриманий із std::runtime_errorвикористання thrust::system_error:

#include <thrust/system_error.h>
#include <thrust/system/cuda/error.h>
#include <sstream>

void throw_on_cuda_error(cudaError_t code, const char *file, int line)
{
  if(code != cudaSuccess)
  {
    std::stringstream ss;
    ss << file << "(" << line << ")";
    std::string file_and_line;
    ss >> file_and_line;
    throw thrust::system_error(code, thrust::cuda_category(), file_and_line);
  }
}

Тут буде включено ім’я файлу, номер рядка та опис англійської мови члена, який cudaError_tвикинув виняток .what():

#include <iostream>

int main()
{
  try
  {
    // do something crazy
    throw_on_cuda_error(cudaSetDevice(-1), __FILE__, __LINE__);
  }
  catch(thrust::system_error &e)
  {
    std::cerr << "CUDA error after cudaSetDevice: " << e.what() << std::endl;

    // oops, recover
    cudaSetDevice(0);
  }

  return 0;
}

Вихід:

$ nvcc exception.cu -run
CUDA error after cudaSetDevice: exception.cu(23): invalid device ordinal

За some_functionбажанням клієнт може відрізнити помилки CUDA від інших видів помилок:

try
{
  // call some_function which may throw something
  some_function();
}
catch(thrust::system_error &e)
{
  std::cerr << "CUDA error during some_function: " << e.what() << std::endl;
}
catch(std::bad_alloc &e)
{
  std::cerr << "Bad memory allocation during some_function: " << e.what() << std::endl;
}
catch(std::runtime_error &e)
{
  std::cerr << "Runtime error during some_function: " << e.what() << std::endl;
}
catch(...)
{
  std::cerr << "Some other kind of error during some_function" << std::endl;

  // no idea what to do, so just rethrow the exception
  throw;
}

Оскільки thrust::system_errorце a std::runtime_error, ми можемо альтернативно обробляти його таким же чином, як і широкий клас помилок, якщо нам не потрібна точність попереднього прикладу:

try
{
  // call some_function which may throw something
  some_function();
}
catch(std::runtime_error &e)
{
  std::cerr << "Runtime error during some_function: " << e.what() << std::endl;
}

1
Начебто упори були змінені. <thrust/system/cuda_error.h>зараз ефективно <thrust/system/cuda/error.h>.
chappjc

Джареде, я думаю, що моя бібліотека обгортки містить ваші запропоновані рішення - в основному, і вона досить легка, щоб можливо замінити її. (Дивіться мою відповідь)
einpoklum

27

C ++ - канонічний спосіб: не перевіряйте на помилки ... використовуйте прив'язки C ++, які викидають винятки.

Мене раніше ця проблема дратувала; і у мене раніше було рішення про функцію обгортання макрокоманди, як у відповідях Талонії та Джареда, але, чесно кажучи? Це робить використання CUDA Runtime API ще більш потворним та схожим на C.

Тож я підійшов до цього іншим і більш фундаментальним способом. Для вибірки результату, ось частина vectorAddзразка CUDA - з повною перевіркою помилок кожного виклику API виконання:

// (... prepare host-side buffers here ...)

auto current_device = cuda::device::current::get();
auto d_A = cuda::memory::device::make_unique<float[]>(current_device, numElements);
auto d_B = cuda::memory::device::make_unique<float[]>(current_device, numElements);
auto d_C = cuda::memory::device::make_unique<float[]>(current_device, numElements);

cuda::memory::copy(d_A.get(), h_A.get(), size);
cuda::memory::copy(d_B.get(), h_B.get(), size);

// (... prepare a launch configuration here... )

cuda::launch(vectorAdd, launch_config,
    d_A.get(), d_B.get(), d_C.get(), numElements
);    
cuda::memory::copy(h_C.get(), d_C.get(), size);

// (... verify results here...)

Знову ж таки - перевіряються всі потенційні помилки, і виняток, якщо сталася помилка (застереження: Якщо ядро ​​спричинило певну помилку після запуску, воно буде зафіксовано після спроби скопіювати результат, а не раніше; для забезпечення успішності ядра ви б потрібно перевірити наявність помилки між запуском і копією за допомогою cuda::outstanding_error::ensure_none()команди).

У наведеному вище коді використовується мій

Тонкі обгортки Modern-C ++ для бібліотеки API CUDA Runtime API (Github)

Зауважте, що винятки містять як рядкове пояснення, так і код статусу API виконання CUDA після невдалого виклику.

Кілька посилань на те, як автоматичні перевірки помилок CUDA за допомогою цих обгортків:


10

Розглянуте тут рішення добре працювало для мене. Це рішення використовує вбудовані функції cuda і дуже просте у виконанні.

Відповідний код скопійовано нижче:

#include <stdio.h>
#include <stdlib.h>

__global__ void foo(int *ptr)
{
  *ptr = 7;
}

int main(void)
{
  foo<<<1,1>>>(0);

  // make the host block until the device is finished with foo
  cudaDeviceSynchronize();

  // check for error
  cudaError_t error = cudaGetLastError();
  if(error != cudaSuccess)
  {
    // print the CUDA error message and exit
    printf("CUDA error: %s\n", cudaGetErrorString(error));
    exit(-1);
  }

  return 0;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.