Каков канонический способ проверки ошибок с использованием API-интерфейса CUDA?
Просматривая ответы и комментарии по вопросам CUDA и в вики-тегах CUDA, я вижу, что часто предлагается проверить статус возврата каждого вызова API для ошибок. Документация API содержит такие функции, как cudaGetLastError
, cudaPeekAtLastError
и cudaGetErrorString
, но как лучше всего их собрать, чтобы надежно улавливать и сообщать об ошибках, не требуя большого количества дополнительного кода?
Ответы
Ответ 1
Вероятно, лучший способ проверить наличие ошибок в коде API времени выполнения - определить функцию-обработчик стиля assert и макрос-обертку, например:
#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);
}
}
Ответ 2
Ответ на talonmies выше - прекрасный способ прервать приложение в стиле assert
.
Иногда мы можем сообщать и восстанавливать из условия ошибки в контексте С++ как часть более крупного приложения.
Здесь достаточно сложный способ сделать это, выбросив исключение С++, полученное из 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
является 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;
}
Ответ 3
C++ -канонический способ: не проверять ошибки... используйте привязки C++, которые генерируют исключения.
Раньше меня раздражала эта проблема; и у меня было решение для функции macro-cum-wrapper-function, как в ответах Talonmies и Jared, но, если честно? Это делает использование CUDA Runtime API еще более уродливым и похожим на C.
Так что я подошел к этому по-другому и более фундаментально. Для примера результата, вот часть образца CUDA vectorAdd
- с полной проверкой ошибок каждого вызова 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...)
Опять же - все потенциальные ошибки проверяются и сообщаются через выброшенное исключение. Этот код использует мой
Thin Modern- C++ оболочки для библиотеки CUDA Runtime API (Github)
Обратите внимание, что после неудачного вызова исключения содержат как строковое объяснение, так и код состояния API времени выполнения CUDA.
Несколько ссылок на то, как ошибки CUDA автоматически проверяются с помощью этих оболочек:
Ответ 4
Решение, обсуждавшееся здесь, хорошо сработало для меня. Это решение использует встроенные функции 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;
}
Ответ 5
Я обычно делаю так:
#define CHK_ERROR if (erro != cudaSuccess) goto Error;
...
erro = cudaMalloc((void**)&d_image,sizeof(unsigned char)*nBlocks); CHK_ERROR
...
erro = cudaDeviceSynchronize(); CHK_ERROR
...
Error:
std::cerr << "Error on CUDA: " << cudaGetErrorString(erro);
cudaFree(d_image);
Я думаю, что он более читается