tensorRT从零起步迈向高性能工业级部署(就业导向) 课程笔记,讲师讲的不错,可以去看原视频支持下。
CUDA 的 API 有多级(下图),详细可参考:CUDA环境详解。
cuCtxCreate()
等 cu
开头的基本都是 Driver API。我们熟悉的 nvidia-smi
命令就是调用的 Driver API。cudaMalloc()
等 API 都是 Runtime API。环境相关
CUDA Driver 是随着显卡驱动发布,要与 cudatoolkit 分开看。
CUDA Driver 对应于 cuda.h
和 libcuda.so
两个文件。注意 cuda.h
会在安装 cudatoolkit 时包含,但是 libcuda.so
是随着显卡驱动安装的我们的系统中的(而不是也跟着 cudatooklit 安装)。因此,如果要直接复制移动 libcuda.so
文件时要注意驱动版本需要与之适配。
本精简课程对于底层的 Driver API 的理解,是为了有利于后续的 Runtime API 的学习与错误调试。Driver API 是理解 cudaRuntime 中上下文的关键。因此,本精简课程在 CUDA Driver 这部分的主要的知识点是:
关于context,有两种:
cuCtxCreate
,手动管理,以堆栈的方式 push/popcuDevicePrimaryCtxRetain
,自动管理,Runtime API 以此为基础关于内存,有两大类:
以上内容之后会展开介绍。
cuInit
的意义是,初始化驱动 API,全局执行一次即可,如果不执行,则所有 API 都将返回错误。cuDestroy
,不需要释放,程序销毁自动释放。版本一
正确友好地检查 cuda 函数的返回值,有利于程序的组织结构,使得代码的可读性更好,错误更容易发现。
我们知道 cuInit
返回的类型是 CUresult
,该返回值会告诉程序员函数成功还是失败,失败的原因是什么。
官方版本的检查的逻辑,如下:
// 使用有参宏定义检查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)
是一个宏定义,我们在调用其他 API 的时候,对函数的返回值进行检查,并在出错时将错误码和报错信息打印出来,方便调试。比如:
checkDriver(cuDeviceGetName(device_name, sizeof(device_name), device));
如果有未初始化等错误,报错信息会被清晰地打印出来。
这个版本一也是 Nvidia 官方使用的版本,但是存在一些问题,比如代码可读性较差,直接返回 int 型错误码等。推荐使用版本二。
版本二
// 很明显,这种代码封装方式,更加的便于使用
//宏定义 #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;
}
很明显的,版本二的返回值、代码可读性、封装性等都相较版本一好了很多。使用的方式是一样的:
checkDriver(cuDeviceGetName(device_name, sizeof(device_name), device));
// 或加一个判断,遇到错误即退出
if (!checkDriver(cuDeviceGetName(device_name, sizeof(device_name), device))) {
return -1;
}
context 是一种上下文,关联对 GPU 的所有操作。
一个 context 与一块显卡关联,一块显卡可以被多个 context 关联。
每个线程都有一个栈结构存储 context,栈顶是当前使用的 context,对应有 push/pop 函数操作 context 的栈,所有 API 都以当前 context 为操作目标
试想一下,如果执行任何操作你都需要传递一个 device 决定送到哪个设备执行,得多麻烦。context 就是为了方便管理当前 API 是在哪个 device 上执行而提出的一种手段,而栈结构的使用则是为了保存之前的上下文中的 device,从而方便控制多个设备。
CreateContext
、PushCurrent
、PopCurrent
这种多 context 管理就显得很麻烦cuDevicePrimaryCtxRetain
,为设备关联主 context,这样分配、设置、释放、栈都不需要我们再去手动管理,是一种自动管理 context 的方式primaryContext
:给我设备 id,给你 context 并设置好,此时一个 device 对应一个 primary context。不同线程,只要设备 id 相同,primary context 就相同,且 context 是线程安全的。cuDevicePrimaryCtxRetain
的。malloc/free
和 new/delete
来申请和释放。