阅读有关CUDA的问题的答案和评论,并在 CUDA标签wiki 中,我看到常常建议检查每个API调用的返回状态是否存在错误。 API文档包含诸如cudaGetLastError
、cudaPeekAtLastError
和cudaGetErrorString
等函数,但最可靠的方式是如何将它们组合起来以可靠地捕获和报告错误,而不需要大量的额外代码?
阅读有关CUDA的问题的答案和评论,并在 CUDA标签wiki 中,我看到常常建议检查每个API调用的返回状态是否存在错误。 API文档包含诸如cudaGetLastError
、cudaPeekAtLastError
和cudaGetErrorString
等函数,但最可靠的方式是如何将它们组合起来以可靠地捕获和报告错误,而不需要大量的额外代码?
检查运行时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);
}
}
您可以使用gpuErrchk
宏来包装每个API调用,该宏将处理其包装的API调用的返回状态,例如:
gpuErrchk( cudaMalloc(&a_d, size*sizeof(int)) );
如果调用中有错误,在您的代码中发生错误的文件和行将被输出到stderr
,并且应用程序将退出,同时会发出描述错误的文本消息。如果需要,在更复杂的应用程序中,您可以考虑修改gpuAssert
以引发异常而不是调用exit()
。
第二个相关问题是如何检查内核启动中的错误,这些错误无法像标准运行时API调用那样直接包装在宏调用中。对于内核,可以使用以下类似代码来实现:
kernel<<<1,1>>>(a);
gpuErrchk( cudaPeekAtLastError() );
gpuErrchk( cudaDeviceSynchronize() );
将首先检查无效的启动参数,然后强制主机等待直到内核停止并检查执行错误。如果您有后续的阻塞API调用,可以消除同步问题,例如:
将首先检查无效的启动参数,然后强制主机等待直到内核停止并检查执行错误。如果您有后续的阻塞API调用,可以消除同步问题,例如:
kernel<<<1,1>>>(a_d);
gpuErrchk( cudaPeekAtLastError() );
gpuErrchk( cudaMemcpy(a_h, a_d, size * sizeof(int), cudaMemcpyDeviceToHost) );
在这种情况下,cudaMemcpy
调用可能会返回内核执行期间发生的错误或者从内存复制本身引起的错误。这可能对初学者来说很困惑,我建议在调试期间在内核启动后使用显式同步,以便更容易地了解可能出现问题的位置。
请注意,在使用CUDA动态并行性时,应该将非常类似的方法应用于设备内核中CUDA运行时API的任何用法以及任何设备内核启动之后:
#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);
}
}
CUDA Fortran错误检查是类似的。请参见此处和此处,了解典型函数错误返回语法。与CUDA C++类似的方法用于收集与内核启动相关的错误。
cudaDeviceReset()
吗?还需要加上内存释放的语句吗? - AureliuscudaMemcpy()
或cudaDeviceSynchronize()
调用周围放置gpuErrchk()
是不足以捕获所有可能的错误条件的。我认为,在内核启动后立即调用cudaGetLastError()
而不是cudaPeekAtLastError()
更好,即使它们具有相同的效果,也可以帮助不知情的读者。 - teratalonmies的上面的回答是一种通过assert
方式中止应用程序的好方法。
偶尔我们可能希望在C++环境下报告并恢复错误条件,作为更大应用程序的一部分。
以下是一种相当简洁的方法,通过使用thrust::system_error
抛出从std::runtime_error
派生的C++异常来实现。
#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;
}
<thrust/system/cuda_error.h>
现在实际上是<thrust/system/cuda/error.h>
。 - chappjc我曾经为这个问题感到恼火;我曾经有一个类似于Talonmies和Jared答案中的宏包装函数解决方案,但是,说实话?它使得使用CUDA Runtime API更加丑陋和类C。
因此,我采用了一种不同而更基本的方法。以下是CUDA vectorAdd
示例的部分结果 - 对每个运行时API调用进行了完整的错误检查:
// (... prepare host-side buffers here ...)
auto current_device = cuda::device::current::get();
namespace cm = cuda::memory;
auto d_A = cm::device::make_unique<float[]>(current_device, numElements);
auto d_B = cm::device::make_unique<float[]>(current_device, numElements);
auto d_C = cm::device::make_unique<float[]>(current_device, numElements);
cm::copy(d_A.get(), h_A.get(), size);
cm::copy(d_B.get(), h_B.get(), size);
auto launch_config = cuda::launch_config_builder()
.overall_size(numElements)
.block_size(256)
.build();
cuda::launch(vectorAdd, launch_config,
d_A.get(), d_B.get(), d_C.get(), numElements);
cm::copy(h_C.get(), d_C.get(), size);
// (... verify results here...)
再次说明-所有潜在的错误都经过检查,如果出现错误则会抛出异常(注意:如果内核在启动后引起了一些错误,则将在尝试复制结果之后捕获,而不是之前;如果要确保内核成功,您需要同步设备或默认流)。
上面的代码使用了我的
CUDA Runtime API库的轻量级现代C++包装器 (Github)
请注意,这些异常除了包含字符串解释之外,还包含失败调用后的CUDA运行时API状态码。
以下是如何使用这些包装器自动检查CUDA错误的一些链接:
using namespace cuda::memory
。
每行超过三个::
真的让我远离C++。 - Dimitri Lesnoff.get()
吗?为什么?
我们能否获取这些数组对应的设备并在执行的其余部分中使用相同的数组?
我还将此make_unique
函数别名为cuda_make_unique_float
函数,以抽象掉所有这些作用域解析和模板实例化。
make_unique
将作用于数组,那么为什么我们需要指定方括号[]
?
该函数的模板定义可能会得到改进。我希望语法不要太严格。 - Dimitri Lesnoff这里讨论的解决方案对我很有效。该解决方案使用内置的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;
}
让我添加一个我最喜欢的CUDA错误检查宏。
#define CUDACHECK(err) do { cuda_check((err), __FILE__, __LINE__); } while(false)
inline void cuda_check(cudaError_t error_code, const char *file, int line)
{
if (error_code != cudaSuccess)
{
fprintf(stderr, "CUDA Error %d: %s. In file '%s' on line %d\n", error_code, cudaGetErrorString(error_code), file, line);
fflush(stderr);
exit(error_code);
}
}
将每个CUDA函数调用都包装在CUDACHECK()
宏中,在每次内核启动后,使用CUDACHECK(cudaPeekAtLastError())
。
do{...}while(false)
的存在主要是为了在宏后面强制加上分号。请参阅this article以获取有关为什么使用它的详细说明。
do { } while
正确地定义了宏,并加以解释。 - Mark Gates