目录
- 前言
- 1. cuInit-驱动初始化
- 2. 返回值检查
- 总结
前言
杜老师推出的 tensorRT从零起步高性能部署 课程,之前有看过一遍,但是没有做笔记,很多东西也忘了。这次重新撸一遍,顺便记记笔记
本次课程学习精简 CUDA 教程-Driver API 案例初始化和检查的理解
课程大纲可看下面的思维导图
1. cuInit-驱动初始化
关于 cuInit 你需要了解:
- cuInit 的意义是初始化驱动 API,如果不执行,则所有 API 都将返回错误,全局执行一次即可
- 没有对应的 cuDestroy,不需要释放,程序销毁自动释放
cuInit驱动初始化案例的示例代码如下:
// CUDA驱动头文件cuda.h
#include <cuda.h>
#include <stdio.h> // 因为要使用printf
#include <string.h>
int main(){
/*
cuInit(int flags), 这里的flags目前必须给0;
对于cuda的所有函数,必须先调用cuInit,否则其他API都会返回CUDA_ERROR_NOT_INITIALIZED
https://docs.nvidia.com/cuda/archive/11.2.0/cuda-driver-api/group__CUDA__INITIALIZE.html
*/
CUresult code=cuInit(0); //CUresult 类型:用于接收一些可能的错误代码
if(code != CUresult::CUDA_SUCCESS){
const char* err_message = nullptr;
cuGetErrorString(code, &err_message); // 获取错误代码的字符串描述
// cuGetErrorName (code, &err_message); // 也可以直接获取错误代码的字符串
printf("Initialize failed. code = %d, message = %s\n", code, err_message);
return -1;
}
/*
测试获取当前cuda驱动的版本
显卡、CUDA、CUDA Toolkit
1. 显卡驱动版本,比如:Driver Version: 460.84
2. CUDA驱动版本:比如:CUDA Version: 11.2
3. CUDA Toolkit版本:比如自行下载时选择的10.2、11.2等;这与前两个不是一回事, CUDA Toolkit的每个版本都需要最低版本的CUDA驱动程序
三者版本之间有依赖关系, 可参照https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html
nvidia-smi显示的是显卡驱动版本和此驱动最高支持的CUDA驱动版本
*/
int driver_version = 0;
code = cuDriverGetVersion(&driver_version); // 获取驱动版本
printf("CUDA Driver version is %d\n", driver_version); // 若driver_version为11020指的是11.2
// 测试获取当前设备信息
char device_name[100]; // char 数组
CUdevice device = 0;
code = cuDeviceGetName(device_name, sizeof(device_name), device); // 获取设备名称、型号如:Tesla V100-SXM2-32GB // 数组名device_name当作指针
printf("Device %d name is %s\n", device, device_name);
return 0;
}
运行效果如下:
上述代码展示了使用 CUDA 驱动初始化函数 cuInit
并获取当前 CUDA 驱动版本和设备信息。
cuInit
函数的参数 flags
目前必须为 0,在使用 CUDA 的其它函数之前,必须先调用 cuInit
函数进行初始化,否则其它 CUDA API 函数会返回 CUDA_ERROR_NOT_INITIALIZED
错误。
代码通过调用 cuDriverGetVersion
函数获取当前 CUDA 驱动的版本,调用 cuDeviceGetName
函数获取当前设备的名称。
2. 返回值检查
关于返回值检查你需要知道:
- 正确友好的检查 cuda 函数的返回值,有利于程序的组织结构
- 使得代码可读性更好,错误更容易发现
我们在 cuInit 驱动初始化案例的基础上增加检查功能,示例代码如下:
// CUDA驱动头文件cuda.h
#include <cuda.h>
#include <stdio.h>
#include <string.h>
// 使用有参宏定义检查cuda driver是否被正常初始化, 并定位程序出错的文件名、行数和错误信息
// 宏定义中带do...while循环可保证程序的正确性
#define checkDriver(op) \
do{ \
auto code = (op); \
if(code != CUresult::CUDA_SUCCESS){ \
const char* err_name = nullptr; \
const char* err_message = nullptr; \
cuGetErrorName(code, &err_name); \
cuGetErrorString(code, &err_message); \
printf("%s:%d %s failed. \n code = %s, message = %s\n", __FILE__, __LINE__, #op, err_name, err_message); \
return -1; \
} \
}while(0)
int main(){
//检查cuda driver的初始化。虽然不初始化或错误初始化某些API不会报错(不信你试试),但安全起见调用任何API前务必检查cuda driver初始化
cuInit(2); // 正确的初始化应该给flag = 0
checkDriver(cuInit(0));
// 测试获取当前cuda驱动的版本
int driver_version = 0;
checkDriver(cuDriverGetVersion(&driver_version));
printf("Driver version is %d\n", driver_version);
// 测试获取当前设备信息
char device_name[100];
CUdevice device = 0;
checkDriver(cuDeviceGetName(device_name, sizeof(device_name), device));
printf("Device %d name is %s\n", device, device_name);
return 0;
}
运行效果如下:
我们将初始化代码屏蔽再来看运行效果:
可以看到打印的信息中可以定位出错的文件名、行数和信息,方便我们开发。
这段代码在 cuInit 驱动初始化案例基础上增加了检查功能,通过宏定义 checkDriver
对 CUDA 驱动初始化和其它 CUDA API 调用进行检查。
宏定义 checkDriver
接受一个操作 op
作为参数,在其内部使用 do...while(0)
循环,以确保宏定义的正确性。宏定义中的代码使用变量 code
存储操作 op
的返回结果,如果返回结果不等于 CUDA_SUCCESS
,即初始化或其它操作发生错误,就会打印出错误的文件名、函数‘操作名称’错误代码和错误信息,并返回 -1 表示程序执行失败。
关于
do...while(0)
的说明(from chatGPT)
do...while(0)
是一个常见的编程技巧,用于宏定义中的语法要求。尽管看起来有些奇怪,但它实际上是一个空循环,只会执行一次。在宏定义中,我们希望将多个语句作为一个整体来处理,以便能够在需要时以单个语句的形式使用。然而,C/C++ 的预处理器在处理宏时,要求宏的展开结果必须是一个完整的语句。
为了满足这一要求,我们使用
do...while(0)
结构,它的逻辑如下:
do
开始一个循环。在循环体内执行宏展开后的代码。
while(0)
结束循环,但由于条件始终为假,循环只会执行一次。通过使用
do...while(0)
,我们可以将多个语句组合成一个整体,并确保它们作为一个完整的语句被处理,而不影响程序的逻辑。
上述检查功能其实有一些问题,比如宏定义可读性差,返回值等。因此我们可以接着完善 check 功能
完善后的示例代码如下:
// CUDA驱动头文件cuda.h
#include <cuda.h>
#include <stdio.h>
#include <string.h>
// 很明显,这种代码封装方式,更加的便于使用
//宏定义 #define <宏名>(<参数表>) <宏体>
#define checkDriver(op) __check_cuda_driver((op), #op, __FILE__, __LINE__)
bool __check_cuda_driver(CUresult code, const char* op, const char* file, int line){
if(code != CUresult::CUDA_SUCCESS){
const char* err_name = nullptr;
const char* err_message = nullptr;
cuGetErrorName(code, &err_name);
cuGetErrorString(code, &err_message);
printf("%s:%d %s failed. \n code = %s, message = %s\n", file, line, op, err_name, err_message);
return false;
}
return true;
}
int main(){
// 检查cuda driver的初始化
// 实际调用的是__check_cuda_driver这个函数
checkDriver(cuInit(0));
// 测试获取当前cuda驱动的版本
int driver_version = 0;
if(!checkDriver(cuDriverGetVersion(&driver_version))){
return -1;
}
printf("Driver version is %d\n", driver_version);
// 测试获取当前设备信息
char device_name[100];
CUdevice device = 0;
checkDriver(cuDeviceGetName(device_name, sizeof(device_name), device));
printf("Device %d name is %s\n", device, device_name);
return 0;
}
运行效果如下:
这段代码对检查 CUDA 驱动的功能进行了进一步的完善。使用了函数封装的方式替代了宏定义,提供了代码的可读性和可维护性。
这种方式的好处是提供了一个统一的检查接口,可以方便地在需要检查操作结果的地方进行调用,并根据返回值判断检查是否成功,从而进行相应的处理。同时,通过函数的方式,避免了宏定义可能带来的可读性和调试的困扰。
官方实现的是上一个版本,这个版本是杜老师推荐的版本,以后 driver、runtime、kernel 都可以这样去做。更加的方便,封装性更好,逻辑性更好,更加友好
总结
本次课程学习了 Driver API 的几个案例,首先是 cuInit 驱动初始化,在使用 CUDA 的其它函数之前,必须调用 cuInit 进行初始化,否则会返回错误。然后学习了为 Driver API 函数添加 check 功能,方便定位错误信息,有利于后续开发,我们还对 check 功能进行了完善,用函数封装的方式替代了宏定义,这种方式更加的便于使用。