CUDA编程与API详解

🚀 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的能力。

能够并行的操作:

  1. CUDA kernel
  2. 异步memcpy(如cudaMemcpyAsync)
  3. 硬件编解码(虽然没有显式指定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:输出的context
  • flags:一般写成0
  • dev: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。原因:

  1. 不破坏调用者的context
  2. 支持嵌套调用,栈结构保证pop顺序正确
  3. 避免长时间占用context

cuStreamCreate - 创建Stream
cpp 复制代码
CUresult cuStreamCreate(CUstream* phStream, unsigned int Flags);

描述:创建CUDA Stream。

参数

  • phStream:输出参数,创建成功后得到stream
  • Flags
    • 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);

编译器自动帮你完成:

  • cuModuleLoad
  • cuLaunchKernel

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 → GPU
  • cudaMemcpyDeviceToHost:GPU → CPU
  • cudaMemcpyDeviceToDevice:GPU → GPU
  • cudaMemcpyHostToHost: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 性能优化建议

  1. 使用非阻塞Stream :创建Stream时使用CU_STREAM_NON_BLOCKING标志,避免与默认流的隐式同步
  2. 锁页内存 :频繁的Host-Device数据传输场景,使用cudaHostAlloc分配锁页内存
  3. Stream并发:将独立的操作放到不同Stream中,实现真正的并行
  4. 避免过度同步 :优先使用cuStreamSynchronizecuEventSynchronize,而不是cuCtxSynchronize

6.2 多线程注意事项

  1. Context管理

    • 独立模式:每个线程独立创建Context,隔离性好但资源开销大
    • 共享模式:多线程共享Context,需注意同步问题
  2. Context切换

    • 长期绑定用cuCtxSetCurrent
    • 临时切换用cuCtxPushCurrent/cuCtxPopCurrent成对使用
  3. 资源释放

    • Context销毁前确保所有操作完成
    • 最好在创建Context的线程中销毁

6.3 调试技巧

  1. 检查返回值:所有CUDA API调用后都应检查返回值
  2. 使用cuda-gdb:CUDA提供的调试器,支持断点、单步执行
  3. 错误处理宏
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开发建议

  1. NVDEC和TensorRT内部都是基于Driver API
  2. 解码出的数据在GPU全局显存中,不属于某个特定Context
  3. 多路解码场景推荐独立模式,每个线程创建独立Context

📚 七、参考资料


📝 总结

CUDA编程的核心在于理解GPU编程模型:

  • Context 类似进程,是GPU资源的容器
  • Stream 是任务队列,实现操作并行
  • Event 是同步工具,用于计时和协调
  • Driver API 提供底层精细控制,Runtime API 提供高级便捷封装

选择合适的API和编程模式,结合实际场景优化,才能充分发挥GPU的并行计算能力。


本文由印象笔记导出的学习笔记整理而成,保留了原有技术深度,适当补充了实践经验和最新信息。

相关推荐
新缸中之脑2 小时前
用Gemma 4构建自托管OCR
人工智能·ocr
ai_xiaogui2 小时前
凌晨3点的重构局:从遗漏“用户中心”看AI客户端前后端分离架构的深水区
人工智能·aistarter·panelai·ai客户端架构设计·桌面端前后端分离·本地大模型api接入·独立开发者踩坑实录
探物 AI2 小时前
虾破苍穹(一):RTX 3060 养一只本地“呆呆”龙虾 [特殊字符]
人工智能·ai编程
俊哥V2 小时前
每日 AI 研究简报 · 2026-04-12
人工智能·ai
拥抱AGI2 小时前
Qwen3.5开源矩阵震撼发布!从0.8B到397B,不同规模模型性能、显存、速度深度对比与选型指南来了!
人工智能·学习·程序员·开源·大模型·大模型训练·qwen3.5
哈喽天空2 小时前
win10原生安装openclaw
人工智能
geinvse_seg2 小时前
开源实战——手把手教你搭建AI量化分析平台:从Docker部署到波浪理论实战
人工智能·docker·开源·蓝耘元生代·蓝耘maas
永霖光电_UVLED2 小时前
Marvell 与 Mojo Vision共同开发基于 micro-LED光学互连解决方案
人工智能