🚀 CUDA编程与API详解
本文系统地介绍了CUDA编程模型、Driver API和Runtime API的核心概念和使用方法,适合有一定编程基础的开发者快速入门GPU编程。
📖 一、概述
什么是CUDA?
CUDA(Compute Unified Device Architecture)是NVIDIA推出的并行计算平台和编程模型,让开发者能够使用NVIDIA GPU进行通用计算(GPGPU)。
为什么需要CUDA?
传统的CPU适合处理串行任务,而GPU拥有数千个计算核心,天生适合处理大规模并行任务。CUDA让开发者能够:
- ⚡ 加速计算密集型任务:深度学习、科学计算、图像处理
- 🔥 释放GPU算力:将GPU从单纯的图形渲染扩展到通用计算
- 💪 提升性能:相比纯CPU实现,可获得数十倍甚至上百倍加速
🧠 二、GPU编程模型
可以把GPU理解为一个"操作系统",拥有类似的概念体系:
GPU = 一个操作系统
├── Context(进程)
│ ├── Memory(显存)
│ ├── Stream(任务队列)
│ ├── Kernel(程序)
│ └── Event(同步工具)
2.1 Context(上下文)
Context是GPU上的"独立运行环境",类似CPU的进程。其主要功能是:
- ✅ 保证不同程序不相互干扰
- ✅ 资源管理(显存、kernel、stream)
所有GPU操作都必须属于某个Context,比如cuMemAlloc(显存)、cuLaunchKernel(计算)、NVDEC解码。
Context与线程的关系
每个要使用CUDA的线程,都必须绑定一个Context。
两种绑定模式对比:
| 维度 | 共享模式 | 独立模式 |
|---|---|---|
| 内存共享 | ✅ 可以直接共享GPU内存(同一地址空间) | ❌ 完全隔离(不同地址空间) ⚠️ 注意:对于NVDEC来说,解码出的数据是在GPU全局显存中,而不是某个context |
| 线程安全/稳定性 | ⚠️ 中等:容易出现context竞争、切换问题 | ✅ 高:线程之间完全隔离 |
| 性能 | ✅ 更好:无context切换,支持多stream并发 | ❌ 较差:context切换开销大 |
| 资源占用 | ✅ 较少:只需一个context | ❌ 较多:每个context都有独立资源 |
| NVDEC场景适配 | ⚠️ 容易出问题:多线程共享decoder/context风险高 | ✅ 推荐:每路流独立context,最稳定 |
💡 实战经验:在camera_detection项目中,每个解码子线程各自创建context,推理线程不需要设置任何context就能拿到各个解码子线程的数据。所有解码子线程共用一个context时,推理线程也需要设置相同的context,才能拿到解码子线程的数据,不然TensorRT会报数据无效的错误。
2.2 Stream(流)
Stream类似CPU里的"任务队列",在不同stream的CUDA操作能够同时运行,来自不同stream的CUDA操作可能相互交叉。Stream赋予GPU同时运行多个kernel的能力。
能够并行的操作:
- CUDA kernel
- 异步memcpy(如cudaMemcpyAsync)
- 硬件编解码(虽然没有显式指定stream,但内部也是排进某个stream)
默认流
当没有显式指定stream时,所有操作都会进入默认流:
cpp
kernel<<<grid, block>>>(...); // 没写 stream
cudaMemcpy(...); // 没写 stream
默认流的两种语义:
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 传统默认流 (Legacy Default Stream) | 全局同步:默认流会等待所有其他流执行完,其他流也会等待默认流执行完 | CUDA 7之前 |
| 新默认流 (Per-thread Default Stream) | 每个线程都有自己的默认流,默认流之间不再互相同步 | CUDA 7开始引入,推荐使用 |
编译控制:
bash
nvcc --default-stream per-thread # 使用新默认流
nvcc --default-stream legacy # 使用传统默认流
📚 参考链接:
2.3 Kernel(核函数)
运行在GPU上的函数,使用__global__关键字声明:
cpp
__global__ void kernel(...) {
// GPU上执行
}
// 启动方式
kernel<<<grid, block>>>(参数);
2.4 Memory(内存)
三种内存类型:
| 类型 | 函数 | 位置 | 特点 |
|---|---|---|---|
| Device Memory | cuMemAlloc() |
GPU显存 | GPU直接访问 |
| Host Memory | malloc() |
CPU内存 | 需要通过PCIe传输 |
| 锁页内存 | cudaHostAlloc() |
CPU内存 | 提高CPU↔GPU传输速度,不会被换出到磁盘 |
2.5 Event(事件)
Event是GPU上的同步工具,主要用于:
- ⏱️ 测量时间
- 🔒 控制同步
- 🔀 Stream之间协调
🔧 三、CUDA Driver API详解
Driver API是CUDA的底层原生接口,提供完全手动控制和更精细的资源管理。
- 📄 头文件 :
cuda.h - 🔤 函数前缀 :
cuXXX
3.1 核心特点
- ✅ 完全手动控制
- ✅ 更接近GPU驱动
- ✅ 更灵活
- ✅ 多线程、资源控制更精细
3.2 初始化流程
cpp
cuInit(0);
CUdevice dev;
cuDeviceGet(&dev, 0);
CUcontext ctx;
cuCtxCreate(&ctx, 0, dev);
3.3 常用API详解
cuInit - 初始化CUDA Driver
cpp
CUresult cuInit(unsigned int Flags);
描述:初始化CUDA Driver,必须在调用其他API之前调用,否则其他API调用会失败。对于一个进程,通常只需要全局调用一次。
参数:
Flags:目前必须是0
cuDeviceGetCount - 获取GPU数量
cpp
CUresult cuDeviceGetCount(int *count);
描述:获取当前系统中可用的CUDA设备数量。
参数:
count(输出):返回当前系统中可用GPU数量
返回值:
CUDA_SUCCESS:成功- 其他:错误码
cuDeviceGet - 获取GPU设备句柄
cpp
CUresult cuDeviceGet(CUdevice *device, int ordinal);
描述:从系统GPU列表中取得第ordinal张卡,返回设备句柄。可以理解为:告诉CUDA,我要使用哪一张显卡。
参数:
device:输出(GPU句柄)ordinal:GPU编号
cuCtxCreate - 创建Context
cpp
CUresult cuCtxCreate(CUcontext *pctx, unsigned int flags, CUdevice dev);
描述:在指定GPU上创建一个"进程环境"。
参数:
pctx:输出的contextflags:一般写成0dev:cuDeviceGet()选中的device
cuCtxDestroy - 销毁Context
cpp
CUresult cuCtxDestroy(CUcontext ctx);
描述:销毁Context,会执行以下操作:
- ✅ 释放GPU资源(显存、constant memory/texture资源、kernel相关资源)
- ✅ 销毁上下文状态(stream、event、module、kernel launch状态)
- ✅ 从当前线程解绑context
⚠️ 注意事项:
- 必须确保没有在用的操作,要求所有kernel已完成、没有正在执行的GPU操作
- 推荐先用
cuCtxSynchronize(),然后cuCtxDestroy - 多线程问题:最好在创建它的线程销毁,确保没有线程还在使用它
返回值错误码:
| 错误码 | 含义 |
|---|---|
| CUDA_SUCCESS | 成功 |
| CUDA_ERROR_INVALID_CONTEXT | context无效 |
| CUDA_ERROR_DEINITIALIZED | CUDA已关闭 |
| CUDA_ERROR_NOT_INITIALIZED | 未初始化 |
cuCtxSynchronize - 同步Context
cpp
CUresult cuCtxSynchronize(void);
描述:等待Context里所有已提交的GPU工作完成,包括:
- Kernel执行
- 内存操作(cuMemcpyHtoD、cuMemcpyDtoH等,尤其是async版本)
- Stream里的任务(所有stream:默认流+非默认流)
同步函数对比:
| API | 同步范围 | 推荐程度 |
|---|---|---|
| cuCtxSynchronize | 🚨 整个context | ❌ 不推荐频繁用 |
| cuStreamSynchronize | 单个stream | ✅ 推荐 |
| cuEventSynchronize | 某个事件 | ✅ 最精细 |
cuCtxPushCurrent vs cuCtxSetCurrent
这是两个容易混淆的API,对比如下:
| 特性 | cuCtxSetCurrent | cuCtxPushCurrent |
|---|---|---|
| 是否有栈 | ❌ 没有 | ✅ 有 |
| 是否可恢复 | ❌ 不可恢复 | ✅ 可恢复 |
| 行为 | 直接覆盖 | 压栈 |
| 使用场景 | 长期绑定 | 临时切换 |
cuCtxPushCurrent使用示例:
cpp
cuCtxPushCurrent(ctx);
// CUDA操作
cuCtxPopCurrent(&ctx);
必须和pop成对使用!
💡 NVIDIA官方解码示例中的策略:在回调函数中,每次CUDA操作前后都使用push/pop,而不是用cuCtxSetCurrent。原因:
- 不破坏调用者的context
- 支持嵌套调用,栈结构保证pop顺序正确
- 避免长时间占用context
cuStreamCreate - 创建Stream
cpp
CUresult cuStreamCreate(CUstream* phStream, unsigned int Flags);
描述:创建CUDA Stream。
参数:
phStream:输出参数,创建成功后得到streamFlags:CU_STREAM_DEFAULT:默认行为,和默认流有同步关系CU_STREAM_NON_BLOCKING:不和默认流同步,完全独立执行,更容易实现真正并行
cuStreamSynchronize - 同步Stream
cpp
CUresult cuStreamSynchronize(CUstream hStream);
描述:阻塞CPU,直到指定stream中的所有任务执行完成。
cuMemAlloc - 分配显存
cpp
CUresult cuMemAlloc(CUdeviceptr *dptr, size_t bytesize);
描述:在当前CUDA Context上分配一块GPU显存。
参数:
dptr:输出参数,返回GPU显存地址bytesize:要分配显存的大小(单位:字节)
cuMemAllocPitch - 分配对齐显存
cpp
CUresult cuMemAllocPitch(
CUdeviceptr *dptr,
size_t *pPitch,
size_t WidthInBytes,
size_t Height,
unsigned int ElementSizeBytes
);
描述:为二维数据(尤其是图像/视频帧)分配位对齐的显存,并返回行跨度(pitch)。
什么是pitch?
普通内存(宽 = 1920 bytes):
Row0: [1920 bytes]
Row1: [1920 bytes]
Pitch内存(宽 = 1920 bytes,pitch = 2048 bytes):
Row0: [1920 bytes + 128 padding]
Row1: [1920 bytes + 128 padding]
每一行变长了,但更利于GPU访问,提升内存访问效率。
参数:
dptr:输出参数,GPU内存地址pPitch:输出参数,每一行实际占用的字节数WidthInBytes:每一行有效数据的字节数(NV12: width, RGB: width * 3)Height:行数,即图像高度ElementSizeBytes:元素大小,表示申请显存的对齐位数(1、2、4、16)
🚀 四、CUDA Runtime API详解
Runtime API是对Driver API的高级封装,使用更简单,更适合大多数开发。
- 📄 头文件 :
cuda_runtime.h - 🔤 函数前缀 :
cudaXXX
4.1 核心特点
- ✅ 自动管理context
- ✅ 使用简单
- ✅ 更适合大多数开发
4.2 初始化(自动)
不需要写cuInit,第一次调用Runtime API时,自动创建context并绑定当前线程。
4.3 Kernel调用
cpp
kernel<<<grid, block, 0, stream>>>(args);
编译器自动帮你完成:
cuModuleLoadcuLaunchKernel
4.4 内存管理
cpp
cudaMalloc(&ptr, size);
cudaMemcpy(...);
4.5 cudaMemcpy详解
cpp
cudaError_t cudaMemcpy(
void* dst,
const void* src,
size_t count,
cudaMemcpyKind kind
);
描述:CUDA中最核心的内存拷贝函数,用于在CPU(Host)和GPU(Device)之间传输数据。
cudaMemcpyKind参数:
cudaMemcpyHostToDevice:CPU → GPUcudaMemcpyDeviceToHost:GPU → CPUcudaMemcpyDeviceToDevice:GPU → GPUcudaMemcpyHostToHost:CPU → CPU
4.6 cudaEvent相关API
Event主要用于:计时、同步、Stream之间协调。
可以把cudaEvent理解成GPU时间点标记,可以在某个stream上"插一个点"。
创建/销毁
cpp
// 创建
cudaEvent_t event;
cudaEventCreate(&event);
// 销毁
cudaEventDestroy(event);
记录事件
cpp
cudaEventRecord(event, stream);
含义:当stream执行到这里时,记录一个"时间点"。注意:是异步的,不会阻塞CPU。
同步等待
cpp
// CPU等GPU(阻塞)
cudaEventSynchronize(event);
// 查询是否完成(非阻塞)
cudaEventQuery(event);
// 返回:cudaSuccess(已完成)或 cudaErrorNotReady(还没完成)
经典用法:GPU计时
cpp
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
// 往stream中插入"标记操作"
cudaEventRecord(start, stream);
kernel<<<... , stream>>>();
cudaEventRecord(stop, stream);
// 等待stop完成
cudaEventSynchronize(stop);
float ms;
cudaEventElapsedTime(&ms, start, stop);
printf("time = %f ms\n", ms);
Stream之间同步
cpp
cudaStream_t decode_stream;
cudaStream_t infer_stream;
// 解码
nvdec -> decode_stream
// 推理
TensorRT -> infer_stream
cudaEventRecord(event, decode_stream);
// infer_stream必须等event完成才能继续执行
cudaStreamWaitEvent(infer_stream, event, 0);
📊 五、常用结构体
CUDA_MEMCPY2D
专门用于描述"二维显存拷贝"的结构体,配合cuMemcpy2D()、cuMemcpy2DAsync()使用。
cpp
typedef struct CUDA_MEMCPY2D_st {
unsigned int srcXInBytes; // 源数据:每一行内的起始偏移(字节)
unsigned int srcY; // 源数据:从第几行开始拷贝
CUmemorytype srcMemoryType; // 源内存类型(HOST/DEVICE/ARRAY)
const void* srcHost; // 源地址(srcMemoryType = HOST)
CUdeviceptr srcDevice; // 源设备指针(srcMemoryType = DEVICE)
CUarray srcArray; // 源CUDA Array(srcMemoryType = ARRAY)
unsigned int srcPitch; // 源数据每一行的实际跨度(stride)
unsigned int dstXInBytes; // 目标数据:每一行内的起始偏移
unsigned int dstY; // 目标数据:从第几行开始写入
CUmemorytype dstMemoryType; // 目标内存类型
void* dstHost; // 目标地址(dstMemoryType = HOST)
CUdeviceptr dstDevice; // 目标设备指针(dstMemoryType = DEVICE)
CUarray dstArray; // 目标CUDA Array(dstMemoryType = ARRAY)
unsigned int dstPitch; // 目标数据每一行的实际跨度
unsigned int WidthInBytes; // 每一行实际要拷贝的有效数据大小
unsigned int Height; // 要拷贝的行数
} CUDA_MEMCPY2D;
为什么需要CUDA_MEMCPY2D?
普通cuMemcpy()只能拷贝连续内存,不支持跨行跳跃。但实际场景中,一行有效数据是width bytes,实际间隔是pitch bytes(对齐后),存在padding。
如果直接memcpy,会把padding也拷进去。所以需要按行拷贝、每一行只拷width、行之间跳过padding,这就是CUDA_MEMCPY2D的意义。
💡 六、开发实践建议
以下内容为实际开发经验的补充
6.1 性能优化建议
- 使用非阻塞Stream :创建Stream时使用
CU_STREAM_NON_BLOCKING标志,避免与默认流的隐式同步 - 锁页内存 :频繁的Host-Device数据传输场景,使用
cudaHostAlloc分配锁页内存 - Stream并发:将独立的操作放到不同Stream中,实现真正的并行
- 避免过度同步 :优先使用
cuStreamSynchronize或cuEventSynchronize,而不是cuCtxSynchronize
6.2 多线程注意事项
-
Context管理:
- 独立模式:每个线程独立创建Context,隔离性好但资源开销大
- 共享模式:多线程共享Context,需注意同步问题
-
Context切换:
- 长期绑定用
cuCtxSetCurrent - 临时切换用
cuCtxPushCurrent/cuCtxPopCurrent成对使用
- 长期绑定用
-
资源释放:
- Context销毁前确保所有操作完成
- 最好在创建Context的线程中销毁
6.3 调试技巧
- 检查返回值:所有CUDA API调用后都应检查返回值
- 使用cuda-gdb:CUDA提供的调试器,支持断点、单步执行
- 错误处理宏:
cpp
#define CUDA_CHECK(call) \
do { \
CUresult err = call; \
if (err != CUDA_SUCCESS) { \
const char* errStr; \
cuGetErrorString(err, &errStr); \
printf("CUDA error: %s at %s:%d\n", errStr, __FILE__, __LINE__); \
} \
} while(0)
6.4 NVDEC开发建议
- NVDEC和TensorRT内部都是基于Driver API
- 解码出的数据在GPU全局显存中,不属于某个特定Context
- 多路解码场景推荐独立模式,每个线程创建独立Context
📚 七、参考资料
📝 总结
CUDA编程的核心在于理解GPU编程模型:
- Context 类似进程,是GPU资源的容器
- Stream 是任务队列,实现操作并行
- Event 是同步工具,用于计时和协调
- Driver API 提供底层精细控制,Runtime API 提供高级便捷封装
选择合适的API和编程模式,结合实际场景优化,才能充分发挥GPU的并行计算能力。
本文由印象笔记导出的学习笔记整理而成,保留了原有技术深度,适当补充了实践经验和最新信息。