1、CUDA初探:什么是GPU计算、CUDA的诞生背景、为什么选择CUDA

1.1 从一次加班说起

我记得那是2012年的一个深夜。

当时我在做图像处理的项目,CPU跑一个滤波算法,一张1080p的图要等好几秒。老板站在我身后,盯着屏幕上的进度条,一言不发。我心想:这玩意儿能不能快一点?

后来我接触到了GPU计算。嗯,说白了,就是把CPU干不了的脏活累活,扔给显卡去干。你想想看,CPU是那种什么都会一点的全能选手,但一次只能干一两件事。GPU呢?它是个偏科生,只会做简单的数学运算,但它有几千个核心,可以同时干几千件事。

这就是GPU计算的本质——用数量换速度。

核心概念:GPU计算(General-Purpose computing on Graphics Processing Units,简称GPGPU)就是利用GPU强大的并行计算能力,去处理原本由CPU执行的通用计算任务。

1.2 CUDA是怎么来的?

在CUDA出现之前,想让GPU干活,你得用OpenGL或者DirectX。什么意思呢?就是你要假装你的计算任务是画一个三角形,然后骗显卡去算。我刚开始学的时候,光是把两个数组加起来,就得写几百行图形API的代码。说实话,那感觉就像用筷子喝汤——能喝,但费劲。

2007年,NVIDIA推出了CUDA。这是一个划时代的东西。

CUDA的全称是Compute Unified Device Architecture,翻译过来就是“统一计算设备架构”。它让开发者可以直接用C语言写GPU程序,再也不用去学那些晦涩的图形API了。

我个人习惯把CUDA理解为:给GPU装上了Windows系统。以前你得懂底层硬件才能用GPU,现在你只要会写C代码,就能让几千个GPU核心为你工作。

一个小故事:我曾经给一个刚入行的同事解释CUDA,他说:“那不就是把CPU的for循环拆开,让每个GPU核心算一个元素吗?”我说:“对,但你要知道,拆开之后怎么管理这些核心、怎么传数据、怎么避免冲突,这些才是CUDA真正要解决的问题。”

1.3 为什么选CUDA?

你可能要问:现在市面上有OpenCL、有ROCm、有OneAPI,为什么偏偏选CUDA?

我直接说我的真实感受吧。

对比项 CUDA OpenCL ROCm
学习曲线 低(C语言扩展) 高(API复杂) 中(类似CUDA)
生态成熟度 极高(2007年至今) 一般 较低
性能优化工具 Nsight、nvcc、cuBLAS等 较少 有限
社区支持 海量教程、论坛、开源项目 较少 很少
硬件覆盖 仅NVIDIA GPU 多平台 仅AMD GPU

说白了,选CUDA就三个理由:

  1. 生态好——你遇到的大部分问题,网上都有人踩过坑了。我曾经在Stack Overflow上搜一个CUDA的bug,发现2010年就有人问过,下面还有NVIDIA工程师亲自回复。
  2. 工具链成熟——从编译到调试到性能分析,NVIDIA给你配齐了。我记得第一次用Nsight做性能分析时,一眼就看到了我的核函数哪里在“空转”,那种感觉就像医生拿到了CT片子。
  3. 性能上限高——CUDA是NVIDIA亲儿子,硬件特性都是最先支持的。比如Tensor Core、RT Core这些新东西,CUDA总是第一个能用上。

避坑指南:我曾经犯过一个错误——为了“跨平台”选了OpenCL。结果项目做到一半,发现性能调优根本找不到资料,最后不得不重写CUDA版本。如果你确定只用NVIDIA显卡,别犹豫,直接上CUDA。

1.4 一张图看懂CUDA的知识体系

下面这张图是我自己画的,把CUDA的核心知识点串了起来。你第一次看可能有些地方不懂,没关系,后面每一章都会详细讲。

CUDA 知识体系总览 硬件基础 GPU架构 → 流多处理器(SM) → CUDA核心 → 内存层次结构 (全局内存、共享内存、寄存器、常量内存、纹理内存) 编程模型 核函数(Kernel) → 线程层次(Grid/Block/Thread) → 线程同步 (<<>> 语法、线程ID计算、__syncthreads()) 内存管理 cudaMalloc → cudaMemcpy → 统一内存(UM) → 内存访问模式 (合并访问、Bank冲突、内存对齐) 性能优化 Occupancy → 指令级并行 → 流水线 → 异步操作 (Nsight性能分析、带宽利用率、计算与访存重叠)

这张图其实就概括了CUDA学习的四个阶段:先搞懂硬件怎么工作的,再学会怎么写核函数,然后学会怎么管内存,最后才是性能调优。别想着一步登天,我当年也是从“Hello World”级别的向量加法开始的。

1.5 你的第一个CUDA程序(预览)

虽然这一章只是入门,但我还是想让你看一眼真正的CUDA代码长什么样。别怕,现在看不懂没关系,后面会一行一行拆解。

// 一个简单的向量加法核函数
__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() {
    // ... 分配内存、拷贝数据、调用核函数、回收资源
    vecAdd<<<256, 256>>>(d_A, d_B, d_C, N);
    return 0;
}

看到那个 <<<256, 256>>> 了吗?这就是CUDA的“魔法”——告诉GPU用256个线程块,每个块256个线程,总共65536个线程同时干活。CPU要跑65536次的循环,GPU一次搞定。

一个小建议:刚开始学的时候,别纠结为什么是256不是128或者512。先跑起来,再谈优化。我见过太多人一开始就钻牛角尖,结果连第一个程序都没跑通。

1.6 本章小结

这一章我们聊了三件事:

  • GPU计算是什么——用几千个简单核心并行干活,适合数据密集型的任务
  • CUDA怎么来的——NVIDIA为了让GPU编程变得简单,搞出来的C语言扩展
  • 为什么选CUDA——生态好、工具全、性能高,是GPU计算的事实标准

下一章,我们会真正动手写第一个CUDA程序。到时候你会看到,从零到跑通一个核函数,其实没有想象中那么难。

想要更多资料,联系可可