在编写CUDA程序时,错误处理是确保程序正确性和稳定性的关键。CUDA错误主要分为编译阶段和运行阶段两类。编译阶段错误通过编译器检查,而运行阶段错误更为复杂,涉及运行时条件、内存访问和设备资源等。
对于运行阶段错误,特别要关注带返回码和不带返回码的函数。带返回码的函数,如CUDA API,应即时检查其返回的错误码。而不带返回码的函数则需要关注其他错误指示方式,如全局状态或日志信息。
通过实施有效的错误检查机制,我们可以及时发现并解决CUDA程序中的错误,确保程序的正确执行。
1 带返回码函数的错误检查
CUDA运行时API中的许多函数都遵循一种常见的模式,即执行某个操作后返回一个cudaError_t类型的错误码。这与常规的C/C++错误处理机制类似,通过检查返回的错误码,可以判断操作是否成功执行。cudaError_t是一个枚举类型,它包含了一系列可能的错误状态,其中一些常见的枚举值包括:
cudaSuccess:表示操作成功,其值为0。
cudaErrorInvalidValue:表示函数接收到了一个无效的值,其值为1。
cudaErrorMemoryAllocation:表示内存分配失败,其值为2。
cudaErrorInitializationError:表示初始化错误,其值为3。
cudaErrorCudartUnloading:表示CUDA运行时正在卸载,其值为4。
......
当调用这类带返回码的CUDA函数时,我们通常会将返回的错误码与cudaSuccess进行比较,以判断操作是否成功。然而,仅仅记录错误码可能并不足以帮助我们迅速定位问题,因为错误码本身是一个数字,要理解其背后的含义,通常需要查阅文档。
为了更直观地记录错误信息,CUDA提供了两个非常有用的函数 cudaGetErrorName 和 cudaGetErrorString:
cudaGetErrorName:
cpp
__host____device__const char *cudaGetErrorName(cudaError_t error)
cudaGetErrorName 函数接受一个cudaError_t类型的错误码作为参数,并返回与之对应的错误名称。如果传入的错误码不在CUDA定义的错误码列表中,它会返回字符串"unrecognized error code"。
cudaGetErrorString:
cpp
__host____device__const char *cudaGetErrorString(cudaError_t error)
cudaGetErrorString 与cudaGetErrorName类似,但它返回的是关于该错误码的详细描述信息。同样,如果错误码无法识别,它会返回"unrecognized error code"。
通过使用这两个函数,我们可以在记录错误日志时包含更详细的错误信息,从而更快速地定位和解决问题。因此,在编写涉及CUDA操作的代码时,建议对每个可能返回错误码的API调用都进行错误检查,并在发现错误时调用这些函数以获取更具体的错误信息。
使用示例:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <cuda_runtime.h>
cudaError_t cuda_check(cudaError_t error_code, int line)
{
if (error_code != cudaSuccess)
{
printf("line: %d, error_code: %d, error_name: %s, error_description: %s\n",
line, error_code, cudaGetErrorName(error_code), cudaGetErrorString(error_code));
exit(EXIT_FAILURE); // 如果出现错误,最好退出程序
}
return error_code;
}
int main()
{
// host上申请内存空间
float *p_host = (float *)malloc(4 * sizeof(float));
memset(p_host, 0, 4 * sizeof(float));
// device上申请相同大小空间
float *p_device;
cudaError_t error_code = cudaMalloc((void**)&p_device, 4 * sizeof(float));
cuda_check(error_code, __LINE__);
// 使用cudaMemset设置device内存为0
error_code = cudaMemset(p_device, 0, 4 * sizeof(float)); // 修正:确保清零整个内存块
cuda_check(error_code, __LINE__);
// host数据拷贝到device
error_code = cudaMemcpy(p_device, p_host, 4 * sizeof(float), cudaMemcpyHostToDevice); // 修正:使用正确的方向参数
cuda_check(error_code, __LINE__);
free(p_host); // 释放host内存
// 释放device内存
error_code = cudaFree(p_device);
cuda_check(error_code, __LINE__);
return 0;
}
2 不带返回码函数的错误检查
除了那些直接返回cudaError_t类型错误码的函数外,CUDA中确实存在一些不直接返回错误码的函数,特别是那些核函数(kernels),它们通常被设计为返回void类型。对于这类函数,如何进行错误检查就显得尤为重要。
对于熟悉Linux环境的开发者来说,$?这个变量应该不陌生,它用于获取上一个shell命令的退出状态码。在CUDA编程中,有一个类似的机制来检查不带返回码函数的执行结果,那就是cudaGetLastError 和 cudaPeekAtLastError函数, 这两函数不接受任何参数,并返回一个cudaError_t类型的错误码。如果自上次调用CUDA运行时函数以来没有发生错误,那么它将返回cudaSuccess。否则,它将返回与错误相对应的枚举值。
cudaGetLastError:
cpp
__host__ __device__ cudaError_t cudaGetLastError(void)
cudaGetLastError函数返回在主机线程中由CUDA Runtime库同一实例的任何运行时调用产生的最后一个错误,并将其重置为cudaSuccess。
cudaPeekAtLastError:
cpp
__host____device__cudaError_t cudaPeekAtLastError(void)
cudaGetLastError函数返回在主机线程中由CUDA Runtime库同一实例的任何运行时调用产生的最后一个错误。但是不会像cudaGetLastError()那样将错误重置为cudaSuccess。
为了有效使用cudaGetLastError进行错误检查,我们通常在执行不带返回码的函数后,立即调用它来检查是否有错误发生。如果有错误,我们可以使用之前提到的cudaGetErrorName和cudaGetErrorString函数来获取更详细的错误信息,以便于调试和解决问题。
使用示例:
cpp
#include <stdio.h>
#include <cuda_runtime.h>
__global__ void just_printf()
{
printf("hello from GPU\n");
}
cudaError_t cuda_check(cudaError_t error_code, int line)
{
if (error_code != cudaSuccess)
{
printf("line: %d, error_code: %d, error_name: %s, error_description: %s\n",
line, error_code, cudaGetErrorName(error_code), cudaGetErrorString(error_code));
exit(EXIT_FAILURE); // 检测到CUDA错误时退出程序
}
return error_code;
}
int main()
{
// 查看线程块的最大线程数
cudaDeviceProp prop;
cuda_check(cudaGetDeviceProperties(&prop, 0), __LINE__);
int maxThreadsPerBlock = prop.maxThreadsPerBlock; // 注意这里使用maxThreadsPerBlock而不是maxThreadsDim[0]
printf("maxThreadsPerBlock: %d\n", maxThreadsPerBlock);
// 启动内核,block_size不能超过GPU支持的最大线程数
int block_size = (maxThreadsPerBlock < 2048) ? maxThreadsPerBlock : 2048;
just_printf<<<1, block_size>>>();
// 同步设备,确保所有CUDA操作完成,以便检测错误
cuda_check(cudaDeviceSynchronize(), __LINE__);
return 0;
}
参考资料
1 CUDA编程入门