CUDA从入门到放弃(九):CUDA错误处理(Error Handling)

在编写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编程入门

2 CUDA编程入门极简教程

3 CUDA C++ Programming Guide

4 CUDA C++ Best Practices Guide

5 NVIDIA CUDA初级教程视频

6 CUDA专家手册 [GPU编程权威指南]

7 CUDA并行程序设计:GPU编程指南

8 CUDA C编程权威指南

相关推荐
迅易科技44 分钟前
借助腾讯云质检平台的新范式,做工业制造企业质检的“AI慧眼”
人工智能·视觉检测·制造
古希腊掌管学习的神2 小时前
[机器学习]XGBoost(3)——确定树的结构
人工智能·机器学习
ZHOU_WUYI2 小时前
4.metagpt中的软件公司智能体 (ProjectManager 角色)
人工智能·metagpt
靴子学长3 小时前
基于字节大模型的论文翻译(含免费源码)
人工智能·深度学习·nlp
AI_NEW_COME4 小时前
知识库管理系统可扩展性深度测评
人工智能
海棠AI实验室4 小时前
AI的进阶之路:从机器学习到深度学习的演变(一)
人工智能·深度学习·机器学习
hunteritself4 小时前
AI Weekly『12月16-22日』:OpenAI公布o3,谷歌发布首个推理模型,GitHub Copilot免费版上线!
人工智能·gpt·chatgpt·github·openai·copilot
IT古董5 小时前
【机器学习】机器学习的基本分类-强化学习-策略梯度(Policy Gradient,PG)
人工智能·机器学习·分类
centurysee5 小时前
【最佳实践】Anthropic:Agentic系统实践案例
人工智能
mahuifa5 小时前
混合开发环境---使用编程AI辅助开发Qt
人工智能·vscode·qt·qtcreator·编程ai