4、GPU计算生态:CUDA、OpenCL、ROCm与Vulkan计算
聊到GPU计算,很多人第一反应就是CUDA。确实,NVIDIA把生态做得太成功了。但作为一个搞了十几年硬件的老兵,我得说,只看CUDA是不够的。你想想看,如果有一天你手里的板子不是NVIDIA的,或者你想在嵌入式设备上跑通用计算,怎么办?
这一章,我们就来掰扯清楚这几个主流计算框架。我不会只讲概念,我会结合我踩过的坑,告诉你每个框架到底适合什么场景。
4.1 CUDA:绕不开的霸主
CUDA,全称是Compute Unified Device Architecture。说白了,就是NVIDIA给自家GPU开的“后门”,让程序员可以直接用C语言写GPU程序。
我个人习惯把CUDA分成三层来看:
- 底层驱动层:负责和硬件打交道,你基本碰不到
- 运行时API:最常用的,管理内存、启动内核
- 库函数层:cuBLAS、cuFFT这些,拿来就用
先看一个最简单的向量加法例子:
__global__ void vecAdd(float *A, float *B, float *C, int N) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < N) {
C[i] = A[i] + B[i];
}
}
int main() {
float *d_A, *d_B, *d_C;
cudaMalloc(&d_A, N * sizeof(float));
cudaMalloc(&d_B, N * sizeof(float));
cudaMalloc(&d_C, N * sizeof(float));
cudaMemcpy(d_A, h_A, N * sizeof(float), cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, N * sizeof(float), cudaMemcpyHostToDevice);
vecAdd<<<256, 256>>>(d_A, d_B, d_C, N);
cudaMemcpy(h_C, d_C, N * sizeof(float), cudaMemcpyDeviceToHost);
return 0;
}
这段代码看着简单,但里面有个坑。我在项目中遇到过,blockIdx.x * blockDim.x + threadIdx.x这个索引计算,如果N不是blockSize的整数倍,就会越界。所以一定要加那个if (i < N)的判断。
4.2 OpenCL:跨平台的“万金油”
OpenCL是苹果发起的,后来交给Khronos Group管理。它的设计初衷很美好——一套代码,跑遍所有设备。但实际上呢?
嗯,这里要注意。OpenCL的跨平台是有代价的。它的抽象层次太高,导致性能往往不如原生框架。我在一个FPGA加速项目里用过OpenCL,说实话,调试起来比CUDA痛苦多了。
OpenCL的核心概念比CUDA多:
- 平台层:管理不同厂商的设备
- 上下文:一个逻辑容器,包含设备和内存
- 命令队列:提交任务的通道
- 内核:在设备上执行的函数
同样的向量加法,OpenCL写起来长不少:
const char *kernelSource =
"__kernel void vecAdd(__global float *A, __global float *B, __global float *C, int N) {"
" int i = get_global_id(0);"
" if (i < N) {"
" C[i] = A[i] + B[i];"
" }"
"}";
// 创建上下文、命令队列、程序对象...
// 编译内核、设置参数、执行...
// 读取结果、释放资源...
看到了吧?光是初始化代码就要写几十行。我曾经在一个项目里,OpenCL的初始化代码占了整个程序的三分之一。这就是跨平台的代价。
4.3 ROCm:AMD的“复仇者联盟”
ROCm是AMD推出的GPU计算平台,全称Radeon Open Compute。它的目标很明确——对标CUDA。
ROCm有几个关键组件:
| 组件 | 作用 | 对标CUDA |
|---|---|---|
| HIP | 类CUDA的编程接口 | CUDA Runtime |
| ROCclr | 运行时层 | CUDA Driver |
| MIOpen | 深度学习库 | cuDNN |
| rocBLAS | BLAS库 | cuBLAS |
HIP是ROCm里最值得关注的。它的语法和CUDA几乎一样,很多代码可以直接迁移。比如上面的向量加法,用HIP写:
__global__ void vecAdd(float *A, float *B, float *C, int N) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < N) {
C[i] = A[i] + B[i];
}
}
int main() {
// 和CUDA几乎一模一样的API
hipMalloc(&d_A, N * sizeof(float));
hipMemcpy(d_A, h_A, N * sizeof(float), hipMemcpyHostToDevice);
vecAdd<<<256, 256>>>(d_A, d_B, d_C, N);
hipMemcpy(h_C, d_C, N * sizeof(float), hipMemcpyDeviceToHost);
}
你看,除了cuda换成hip,其他基本没变。我建议团队做跨平台开发时,直接用HIP写,然后通过工具转成CUDA或ROCm代码。
4.4 Vulkan Compute:游戏引擎的“黑科技”
Vulkan是Khronos Group推出的新一代图形API,但它也支持通用计算。为什么游戏引擎要用Vulkan做计算?因为它的开销低、控制力强。
Vulkan计算的核心流程:
- 创建Vulkan实例和物理设备
- 创建逻辑设备和命令池
- 创建缓冲区并上传数据
- 编写并编译SPIR-V着色器
- 创建计算管线
- 提交命令并执行
- 读取结果
Vulkan的代码量比CUDA大得多,但它的优势在于:
- 显式控制:你可以精确控制内存布局和同步
- 多线程友好:可以在CPU端并行提交命令
- 跨平台:Windows、Linux、Android都能跑
不过说实话,Vulkan计算的门槛比较高。我建议只有在对性能有极致要求,或者需要在图形和计算之间频繁切换的场景下,才考虑用Vulkan。
4.5 如何选择?一张图看懂
为了让你更直观地理解这几个框架的定位,我画了一张对比图:
这张图应该能帮你快速定位。我个人建议,如果你是新手,先学CUDA。等把CUDA玩熟了,再去看其他框架,会发现很多概念都是相通的。
好了,这一章的内容就到这里。GPU计算生态确实复杂,但只要你抓住CUDA这个主线,其他框架学起来会快很多。记住,没有最好的框架,只有最适合你项目的框架。
公众号:蓝海资料掘金营,微信deep3321