一、CUDA入门:GPU架构概述、CUDA编程模型、CUDA环境搭建与工具链
各位同学,欢迎来到《CUDA与TensorRT边缘计算部署实战》的第一章。
说实话,每次讲GPU架构,我都想起自己刚入行那会儿。那时候我对着NVIDIA的架构白皮书,看了三天三夜,愣是没搞明白SM和SP到底是个啥关系。后来在项目中调优一个图像处理算法,才真正体会到——不理解硬件,写出来的CUDA代码就是瞎蒙。
所以这一章,咱们先把地基打牢。我会用最直白的话,把GPU的底裤给你扒干净。
1.1 GPU架构概述:从“很多核”到“很多线程”
CPU和GPU最大的区别是什么?
CPU是“精英小团队”——几个大核,每个核都特别强,能处理各种复杂任务。GPU是“民工大部队”——成百上千个小核,每个核能力一般,但人多力量大,适合干重复性的体力活。
你想想看,渲染一张4K图片,需要计算几百万个像素的颜色值。每个像素的计算逻辑几乎一样,只是数据不同。这种场景,GPU就是天生的王者。
1.1.1 关键硬件概念
我们先搞清楚几个核心概念。这些词你以后会天天见。
| 术语 | 全称 | 通俗理解 |
|---|---|---|
| SM | Streaming Multiprocessor | GPU里的“计算单元组”,相当于一个车间 |
| SP / CUDA Core | Streaming Processor | SM里的“计算工人”,真正干活的 |
| Warp | 线程束 | 32个线程组成一队,一起执行指令 |
| Global Memory | 全局显存 | GPU的大仓库,所有线程都能访问,但慢 |
| Shared Memory | 共享内存 | SM内部的小仓库,快,但容量小 |
嗯,这里要注意:Warp是GPU调度的最小单位。什么意思?就是你写代码时觉得是“一个线程一个线程在执行”,但硬件层面其实是32个线程绑在一起跑的。这个特性会直接影响你的代码性能,后面我们会反复提到。
核心要点:GPU通过“大量线程 + 快速切换”来隐藏访存延迟。当一个Warp在等数据时,硬件立刻切换到另一个Warp执行。这就是GPU“人多不怕等”的底气。
1.1.2 我踩过的坑:架构理解不到位
我曾经在一个项目中,用GPU做视频编解码加速。一开始我天真地以为,只要把循环改成并行就完事了。结果跑起来发现,性能还不如CPU。
后来一分析,问题出在“线程发散”上。因为我的算法里有大量if-else分支,同一个Warp里的32个线程,有的走if,有的走else。硬件没办法,只能先执行if的线程,再执行else的线程。32个线程的活儿,硬是干了两遍。
所以记住:尽量让同一个Warp里的线程走相同的执行路径。这是GPU编程的第一条铁律。
1.2 CUDA编程模型:你只需要记住三个层次
CUDA的编程模型,说白了就是三个层次:Grid、Block、Thread。
我习惯这么理解:
- Grid:整个任务,比如“处理一张图片”
- Block:任务的一个子集,比如“处理图片的左上角区域”
- Thread:真正干活的单元,比如“处理一个像素”
每个Block会被分配到某个SM上执行。一个SM可以同时处理多个Block,但Block之间是独立的,不能互相通信(除非用全局显存,但那样很慢)。
1.2.1 第一个CUDA程序:向量加法
咱们直接上代码。这是CUDA的“Hello World”——向量加法。
// 文件名:vector_add.cu
#include <stdio.h>
// GPU核函数
__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() {
int N = 1024;
size_t size = N * sizeof(float);
// 1. 分配主机内存
float *h_A = (float*)malloc(size);
float *h_B = (float*)malloc(size);
float *h_C = (float*)malloc(size);
// 初始化数据
for (int i = 0; i < N; i++) {
h_A[i] = i * 1.0f;
h_B[i] = i * 2.0f;
}
// 2. 分配设备显存
float *d_A, *d_B, *d_C;
cudaMalloc(&d_A, size);
cudaMalloc(&d_B, size);
cudaMalloc(&d_C, size);
// 3. 把数据从主机拷贝到设备
cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice);
// 4. 配置核函数执行参数
int threadsPerBlock = 256;
int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;
// 5. 启动核函数
vecAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);
// 6. 把结果从设备拷贝回主机
cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);
// 验证结果
for (int i = 0; i < 10; i++) {
printf("C[%d] = %f\n", i, h_C[i]);
}
// 7. 清理
cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
free(h_A); free(h_B); free(h_C);
return 0;
}
个人习惯:我写CUDA代码时,一定会先算清楚 blocksPerGrid 和 threadsPerBlock。这两个参数直接决定了你的GPU利用率。一般 threadsPerBlock 取128或256比较稳妥,太少浪费SM,太多可能寄存器不够用。
1.2.2 代码里藏着什么?
你看这个核函数:
int i = blockIdx.x * blockDim.x + threadIdx.x;
这行代码是CUDA编程的灵魂。它把三维的线程索引(blockIdx, threadIdx)映射到一维的数据索引。为什么这么写?因为GPU的线程是三维组织的,但我们的数据通常是一维数组。
说白了,就是给每个线程分配一个“工号”,然后告诉它:“你负责处理第i个数据”。
1.3 CUDA环境搭建与工具链
光说不练假把式。咱们得把环境搭起来。
1.3.1 你需要什么?
- 硬件:一块NVIDIA显卡(GTX 1050以上都行,实在没有用云GPU)
- 驱动:NVIDIA Driver(去官网下载,版本别太老)
- CUDA Toolkit:包含nvcc编译器、CUDA库、调试工具
- cuDNN:深度学习加速库(后面TensorRT会用到)
1.3.2 安装步骤(Ubuntu 20.04为例)
我建议用Linux开发CUDA。Windows也能用,但坑比较多。
# 1. 检查显卡
lspci | grep -i nvidia
# 2. 安装驱动(推荐用官方runfile)
sudo apt purge nvidia-*
wget https://us.download.nvidia.com/XFree86/Linux-x86_64/550.54.14/NVIDIA-Linux-x86_64-550.54.14.run
sudo sh NVIDIA-Linux-x86_64-550.54.14.run
# 3. 安装CUDA Toolkit
wget https://developer.download.nvidia.com/compute/cuda/12.4.0/local_installers/cuda_12.4.0_550.54.14_linux.run
sudo sh cuda_12.4.0_550.54.14_linux.run
# 4. 配置环境变量
echo 'export PATH=/usr/local/cuda-12.4/bin:$PATH' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=/usr/local/cuda-12.4/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc
source ~/.bashrc
# 5. 验证安装
nvcc --version
nvidia-smi
避坑指南:我曾经在Ubuntu 22.04上装CUDA 11.8,结果死活编译不过。后来发现是GCC版本太高,CUDA 11.8最高只支持GCC 9。解决办法是装个低版本GCC:sudo apt install gcc-9 g++-9,然后设置 export CC=gcc-9 CXX=g++-9。
1.3.3 工具链一览
| 工具 | 用途 | 我的评价 |
|---|---|---|
| nvcc | CUDA编译器 | 核心工具,必须掌握 |
| nvidia-smi | 监控GPU状态 | 日常必备,看显存、温度、利用率 |
| nsys | 性能分析(Nsight Systems) | 调优神器,能看CPU/GPU时间线 |
| ncu | 内核分析(Nsight Compute) | 深入分析每个核函数的瓶颈 |
| cuda-gdb | GPU调试器 | 调试核函数里的bug |
我个人习惯是:先用 nvidia-smi 看一眼显存够不够,然后用 nsys 抓一下整体性能,最后用 ncu 定位具体问题。这套组合拳,基本能解决90%的性能问题。
1.4 本章小结
这一章我们干了三件事:
- 搞懂了GPU的硬件架构——SM、Warp、显存层次
- 学会了CUDA编程模型——Grid、Block、Thread三层结构
- 搭好了开发环境——驱动、CUDA Toolkit、工具链
下一章,我们会深入CUDA的内存管理。到时候我会告诉你,为什么 cudaMalloc 和 cudaMemcpy 用不好,你的程序会慢得像蜗牛。
嗯,今天就到这儿。记得把环境搭好,把向量加法的代码跑通。有问题随时交流。
公众号:蓝海资料掘金营,微信deep3321