精简CUDA教程——CUDA Driver API
tensorRT从零起步迈向高性能工业级部署(就业导向) 课程笔记,讲师讲的不错,可以去看原视频支持下。
Driver API概述
CUDA 的多级 API
CUDA 的 API 有多级(下图),详细可参考:CUDA环境详解。
- CUDA Driver API 是 CUDA 与 GPU 沟通的驱动级底层 API。早期 CUDA 与 GPU 沟通都是直接通过 Driver API。
cuCtxCreate()
等cu
开头的基本都是 Driver API。我们熟悉的nvidia-smi
命令就是调用的 Driver API。 - 后来发觉 Driver API 太过底层,细节太过复杂,故演变出了 Runtime API,Runtime API 是基于 Driver API 开发的,常见的
cudaMalloc()
等 API 都是 Runtime API。
CUDA Driver
环境相关
CUDA Driver 是随着显卡驱动发布,要与 cudatoolkit 分开看。
CUDA Driver 对应于 cuda.h
和 libcuda.so
两个文件。注意 cuda.h
会在安装 cudatoolkit 时包含,但是 libcuda.so
是随着显卡驱动安装的我们的系统中的(而不是也跟着 cudatooklit 安装)。因此,如果要直接复制移动 libcuda.so
文件时要注意驱动版本需要与之适配。
如何了解CUDA Driver
本精简课程对于底层的 Driver API 的理解,是为了有利于后续的 Runtime API 的学习与错误调试。Driver API 是理解 cudaRuntime 中上下文的关键。因此,本精简课程在 CUDA Driver 这部分的主要的知识点是:
- Context 的管理机制
- CUDA 系列接口的开发习惯(错误检查方法)
- 内存模型
关于context和内存的分类
关于context,有两种:
- 手动管理的 context:
cuCtxCreate
,手动管理,以堆栈的方式 push/pop - 自动管理的 context:
cuDevicePrimaryCtxRetain
,自动管理,Runtime API 以此为基础
关于内存,有两大类:
- CPU 内存,称之为 Host Memory
- Pageable Memory:可分页内存
- Page-Locked Memory:页锁定内存
- GPU 内存(显存),称之为 Device Memory
- Global Memory:全局内存
- Shared Memory:共享内存
- … 其他
以上内容之后会展开介绍。
cuIint 驱动初始化
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;
}
CUcontext
手动上下文管理
-
context 是一种上下文,关联对 GPU 的所有操作。
-
一个 context 与一块显卡关联,一块显卡可以被多个 context 关联。
-
每个线程都有一个栈结构存储 context,栈顶是当前使用的 context,对应有 push/pop 函数操作 context 的栈,所有 API 都以当前 context 为操作目标
试想一下,如果执行任何操作你都需要传递一个 device 决定送到哪个设备执行,得多麻烦。context 就是为了方便管理当前 API 是在哪个 device 上执行而提出的一种手段,而栈结构的使用则是为了保存之前的上下文中的 device,从而方便控制多个设备。
自动上下文管理
- 由于高频操作都是一个线程固定访问一个 device 不变,不经常会有同一个线程来回多次访问不同 device 的情况,且只会使用到一个 context,很少用到多 context。
- 即在多数情况下,
CreateContext
、PushCurrent
、PopCurrent
这种多 context 管理就显得很麻烦 - 因此就推出了
cuDevicePrimaryCtxRetain
,为设备关联主 context,这样分配、设置、释放、栈都不需要我们再去手动管理,是一种自动管理 context 的方式 primaryContext
:给我设备 id,给你 context 并设置好,此时一个 device 对应一个 primary context。不同线程,只要设备 id 相同,primary context 就相同,且 context 是线程安全的。- 在之后要介绍的 CUDA Runtime API 中,就是自动使用
cuDevicePrimaryCtxRetain
的。
DriverAPI 内存管理
- host memory 是计算机本身的内存,可以用 CUDA Driver API 来申请和释放,也可以用 C/C++ 的
malloc/free
和new/delete
来申请和释放。 - device memory 是显卡上的内存,即显存,有专用的 Driver API 来进行申请和释放。