在大模型推理与训练日益普及的今天,GPU 已不再是游戏玩家的专属硬件,而是 AI 基础设施的核心。理解 CUDA 编程,是每一位希望深入 AI 系统优化的工程师必经之路。本文从零开始,带你掌握 GPU 并行计算的核心概念与实战代码。
一、为什么要用 GPU 计算?
CPU 与 GPU 的架构差异
CPU(中央处理器)和 GPU(图形处理器)的设计哲学截然不同。CPU 只有几个核心(通常 4~128 个),但每个核心都非常复杂,拥有大容量缓存、分支预测、乱序执行等机制,旨在最小化单线程延迟。GPU 则拥有数千个简单核心,旨在最大化整体吞吐量。
| 特性 | CPU | GPU |
|---|---|---|
| 核心数 | 少(4~128) | 多(数千至上万) |
| 核心复杂度 | 高 | 低 |
| 缓存策略 | 大容量乱序缓存 | 小缓存,共享内存可控 |
| 优化目标 | 低延迟 | 高吞吐量 |
| 适用场景 | 顺序控制、逻辑分支 | 数据并行、密集计算 |
GPU 的设计思路是:与其让少量核心飞快完成任务,不如让海量核心同时开工,每个核心处理一小部分数据。这种吞吐量导向的设计,在特定场景下可以带来数十倍甚至上百倍的加速。
GPU 何时能赢?
GPU 并非万能钥匙。它在以下场景表现优异:
- 数据并行问题:同一段计算逻辑应用于大量独立数据元素,如向量加法、矩阵乘法、图像卷积
- 高算术强度:计算量远大于数据搬移量,即每字节数据能支撑大量浮点运算
- 规则化的内存访问模式:连续、对齐的内存访问有利于合并(coalescing),减少带宽浪费
反之,如果任务中存在大量分支分歧、频繁的同步需求、或极低的算术强度,GPU 的优势会被抵消。
二、CUDA 线程层次结构
CUDA 的并行模型以线程网格(Grid) 为核心,采用多层级组织方式,允许多维度的并行度映射。
Grid → Block → Thread
CUDA 将线程组织为一个三维层次结构:
- Grid(网格):包含多个 Block,一次内核启动对应一个 Grid
- Block(块):包含多个 Thread,同一块内的线程可以通过共享内存协作,并通过
__syncthreads()同步 - Thread(线程):最基本的执行单元
每一级都可以是 1D、2D 或 3D。例如,处理二维图像时,使用 2D 的 Block 和 Grid 可以直观地将线程与像素坐标对应起来。
内置变量
CUDA 在核函数中提供了一组内置变量,用于确定当前线程的身份:
| 变量 | 含义 | 取值范围 |
|---|---|---|
threadIdx.x/y/z | 当前线程在 Block 内的索引 | 0 ~ blockDim-1 |
blockIdx.x/y/z | 当前 Block 在 Grid 内的索引 | 0 ~ gridDim-1 |
blockDim.x/y/z | 每个 Block 的维度大小 | 启动时指定 |
gridDim.x/y/z | Grid 的维度大小 | 启动时指定 |
全局线程索引的计算公式(以 1D 为例):
int globalIdx = blockIdx.x * blockDim.x + threadIdx.x;
Warp 与调度
NVIDIA GPU 以 Warp 为调度单位,一个 Warp 固定包含 32 个线程。同一 Warp 内的线程执行**单指令多线程(SIMT)**模式——它们在同一条指令上协同前进。Warp 调度器在每个时钟周期选择一个就绪的 Warp 执行,通过快速上下文切换隐藏内存访问延迟。
线程分歧(Thread Divergence)
当同一 Warp 内的线程遇到条件分支且执行路径不同时,就会发生线程分歧。硬件会串行化执行各路径,导致性能下降。编写核函数时应尽量保证 Warp 内线程走相同分支,避免 if (threadIdx.x % 2) 这类模式。
三、第一个 CUDA 程序
下面是一个完整的向量加法程序,涵盖了 CUDA 编程的完整流程:分配设备内存、传输数据、启动核函数、回收结果、释放资源。
#include <stdio.h>
#include <cuda_runtime.h>
// 错误检查宏——生产环境必备
#define CUDA_CHECK(call) \
do { \
cudaError_t err = call; \
if (err != cudaSuccess) { \
fprintf(stderr, "CUDA error at %s:%d: %s\n", \
__FILE__, __LINE__, cudaGetErrorString(err)); \
exit(EXIT_FAILURE); \
} \
} while (0)
// 核函数:每个线程计算 C[i] = A[i] + B[i]
__global__ void vectorAdd(const float *A, const 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() {
const int N = 1 << 20; // 1,048,576 个元素
size_t bytes = N * sizeof(float);
// 1. 分配主机内存并初始化
float *h_A = (float *)malloc(bytes);
float *h_B = (float *)malloc(bytes);
float *h_C = (float *)malloc(bytes);
for (int i = 0; i < N; i++) {
h_A[i] = 1.0f;
h_B[i] = 2.0f;
}
// 2. 分配设备内存
float *d_A, *d_B, *d_C;
CUDA_CHECK(cudaMalloc((void **)&d_A, bytes));
CUDA_CHECK(cudaMalloc((void **)&d_B, bytes));
CUDA_CHECK(cudaMalloc((void **)&d_C, bytes));
// 3. 将数据从主机拷贝到设备(H2D)
CUDA_CHECK(cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice));
// 4. 启动核函数
int threadsPerBlock = 256;
int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;
vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);
CUDA_CHECK(cudaGetLastError()); // 检查核函数启动错误
CUDA_CHECK(cudaDeviceSynchronize());
// 5. 将结果从设备拷贝回主机(D2H)
CUDA_CHECK(cudaMemcpy(h_C, d_C, bytes, cudaMemcpyDeviceToHost));
// 6. 验证结果
for (int i = 0; i < 5; i++) {
printf("C[%d] = %.1f\n", i, h_C[i]);
}
// 7. 释放资源
CUDA_CHECK(cudaFree(d_A));
CUDA_CHECK(cudaFree(d_B));
CUDA_CHECK(cudaFree(d_C));
free(h_A); free(h_B); free(h_C);
return 0;
}
编译与运行
nvcc -o vector_add vector_add.cu -O3
./vector_add
核函数启动语法解读
vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);
<<< >>> 是 CUDA 扩展语法,第一个参数是 Grid 维度,第二个参数是 Block 维度。两者都可以是 dim3 类型以支持 2D/3D。第三个参数可选,指定每个 Block 的动态共享内存大小;第四个参数可选,指定执行流(Stream)。
四、显存层次结构
理解 GPU 的显存层次,是写出高性能 CUDA 程序的关键。不同层级的显存拥有截然不同的容量、速度和可见范围。
| 层次 | 声明方式 | 容量 | 访问速度 | 可见范围 | 生命周期 |
|---|---|---|---|---|---|
| 寄存器(Register) | 局部变量自动分配 | ~256 KB/SM | 最快(1 cycle) | 单个线程 | 线程生命周期 |
| 共享内存(Shared Memory) | __shared__ | 极快(~L1,5 cycles) | 同 Block 内线程 | Block 生命周期 | |
| 常量内存(Constant Memory) | __constant__ | 64 KB | 缓存命中极快 | 全 Grid | 程序生命周期 |
| 纹理/缓存内存 | 纹理对象 | 受缓存限制 | 缓存命中快 | 全 Grid | 程序生命周期 |
| 全局内存(Global Memory) | cudaMalloc | 数 GB~数十 GB | 最慢(数百 cycles) | 全 Grid + 主机 | 手动管理 |
| 局部内存(Local Memory) | 寄存器溢出自动分配 | 全局内存子集 | 慢 | 单个线程 | 线程生命周期 |
共享内存优化示例
共享内存位于芯片上,速度接近 L1 缓存。经典的**分块矩阵乘法(Tiled Matrix Multiplication)**利用共享内存减少全局内存访问次数。
#define TILE_WIDTH 16
__global__ void matrixMulShared(const float *A, const float *B, float *C,
int width) {
__shared__ float s_A[TILE_WIDTH][TILE_WIDTH];
__shared__ float s_B[TILE_WIDTH][TILE_WIDTH];
int bx = blockIdx.x, by = blockIdx.y;
int tx = threadIdx.x, ty = threadIdx.y;
int row = by * TILE_WIDTH + ty;
int col = bx * TILE_WIDTH + tx;
float Cvalue = 0.0f;
for (int m = 0; m < (width + TILE_WIDTH - 1) / TILE_WIDTH; m++) {
// 协作加载 A 和 B 的子块到共享内存
int aCol = m * TILE_WIDTH + tx;
int bRow = m * TILE_WIDTH + ty;
s_A[ty][tx] = (row < width && aCol < width) ? A[row * width + aCol] : 0.0f;
s_B[ty][tx] = (bRow < width && col < width) ? B[bRow * width + col] : 0.0f;
__syncthreads(); // 等待全部加载完成
// 计算子块乘积
for (int k = 0; k < TILE_WIDTH; k++) {
Cvalue += s_A[ty][k] * s_B[k][tx];
}
__syncthreads(); // 等待计算完成再加载下一轮
}
if (row < width && col < width) {
C[row * width + col] = Cvalue;
}
}
全局内存访问优化要诀
- 合并访问(Coalesced Access):Warp 内线程访问连续对齐的内存地址,合并为一次事务
- 避免非对齐/步幅访问:间隔式访问大幅降低有效带宽
- 使用
__restrict__关键字:告知编译器指针无别名,启用更多优化
五、CUDA 流(Streams)与异步执行
默认情况下,CUDA 核函数和内存拷贝在流 0(默认流)上执行,且与主机端是同步的。通过创建非默认流,可以实现异步执行,使计算与数据传输重叠。
流的创建与使用
#include <stdio.h>
#include <cuda_runtime.h>
__global__ void scaleKernel(float *data, float scale, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
data[i] *= scale;
}
}
int main() {
const int N = 1 << 20;
size_t bytes = N * sizeof(float);
int numStreams = 4;
// 分配页锁定内存(允许异步传输)
float *h_data;
cudaMallocHost(&h_data, bytes);
float *d_data;
cudaMalloc(&d_data, bytes);
// 创建多个流
cudaStream_t streams[numStreams];
for (int i = 0; i < numStreams; i++) {
cudaStreamCreate(&streams[i]);
}
int chunkSize = N / numStreams;
int threadsPerBlock = 256;
int blocksPerGrid = (chunkSize + threadsPerBlock - 1) / threadsPerBlock;
// 分块异步执行:拷贝 → 计算 → 拷贝回
for (int i = 0; i < numStreams; i++) {
int offset = i * chunkSize;
size_t chunkBytes = chunkSize * sizeof(float);
// 异步 H2D 拷贝
cudaMemcpyAsync(d_data + offset, h_data + offset,
chunkBytes, cudaMemcpyHostToDevice, streams[i]);
// 异步启动核函数
scaleKernel<<<blocksPerGrid, threadsPerBlock, 0, streams[i]>>>
(d_data + offset, 2.0f, chunkSize);
// 异步 D2H 拷贝
cudaMemcpyAsync(h_data + offset, d_data + offset,
chunkBytes, cudaMemcpyDeviceToHost, streams[i]);
}
// 等待所有流完成
for (int i = 0; i < numStreams; i++) {
cudaStreamSynchronize(streams[i]);
}
// 清理
for (int i = 0; i < numStreams; i++) {
cudaStreamDestroy(streams[i]);
}
cudaFree(d_data);
cudaFreeHost(h_data);
return 0;
}
重叠执行的原理
现代 GPU 通常具备独立的拷贝引擎(Copy Engine)和计算引擎(Compute Engine)。通过 cudaMemcpyAsync 配合页锁定内存(Pinned Memory),主机到设备的传输可以异步进行。如果数据传输引擎和计算引擎互不干扰,理想情况下可以实现"一边传输下一块数据,一边计算当前块"的流水线,最大限度提升吞吐量。
⚠️ 异步拷贝必须使用页锁定主机内存(
cudaMallocHost或cudaHostAlloc),普通malloc内存无法发起真正的 DMA 传输。
六、GPU 架构纵览
Streaming Multiprocessor(SM)
SM 是 GPU 的核心计算单元。每个 SM 包含:
- 一组 CUDA Core(执行整数和浮点运算)
- 特殊功能单元(SFU,处理三角函数、开方等)
- Load/Store 单元
- 寄存器文件
- 共享内存 / L1 缓存
- Warp 调度器
一个 GPU 芯片通常集成数十到上百个 SM,例如 A100 有 108 个 SM,H100 有 132 个 SM。
CUDA Core 与 Tensor Core
- CUDA Core:通用标量计算单元,支持 FP32、FP64、INT 运算
- Tensor Core:专用矩阵乘加(MMA)加速器,从 Volta 架构引入。在 FP16/BF16/INT8 精度下提供极高吞吐,是深度学习训练与推理加速的核心
Tensor Core 支持 mma.sync.aligned 等指令,也可通过 cuBLAS、CUTLASS 等库间接调用。
各代架构演进
| 架构代号 | 代表显卡 | Compute Capability | 关键特性 |
|---|---|---|---|
| Volta | V100 | 7.0 | 首款 Tensor Core,独立 L1/共享内存,NVLink 2.0 |
| Ampere | A100, RTX 30 系 | 8.0 / 8.6 | 第三代 Tensor Core(支持稀疏性加速),MIG 多实例 |
| Hopper | H100 | 9.0 | 第四代 Tensor Core,Transformer Engine(FP8 自动混合精度),DPX 指令 |
| Blackwell | B200, RTX 50 系 | 10.0 | 第五代 Tensor Core,第二代 Transformer Engine,支持 FP4/FP6 |
Compute Capability(计算能力)
Compute Capability 以 x.y 格式标识 GPU 的硬件特性版本。主版本号对应架构世代(如 8.x = Ampere),次版本号代表特定 SKU 的小幅差异。开发时需关注目标硬件的 Compute Capability,以确定支持的指令集和最大资源限制。
# 查看本机 GPU 的 Compute Capability
nvidia-smi
/usr/local/cuda/extras/demo_suite/deviceQuery
总结
| 知识点 | 核心要点 |
|---|---|
| GPU 优势 | 吞吐量导向,数据并行场景 |
| 线程模型 | Grid/Block/Thread 三级层次,Warp 为调度单位 |
| 编程流程 | 分配 → 拷贝 → 核函数 → 拷贝回 → 释放 |
| 显存层次 | 寄存器 > 共享内存 > 常量内存 > 全局内存 |
| 流与异步 | 分块重叠计算与传输,使用页锁定内存 |
| 硬件演进 | Tensor Core 是深度学习加速关键,架构持续迭代 |
CUDA 编程的学习曲线并不陡峭,但要写出接近硬件极限的高性能代码,则需要深入理解 Warp 行为、显存层次、以及各代架构的微架构差异。本文提供的向量加法、分块矩阵乘法和流异步执行三个完整示例,构成了后续深入 CUDA 优化的坚实基础。建议读者在真实 GPU 上编译运行这些代码,通过 nvprof 或 Nsight Compute 分析工具观察性能瓶颈,逐步进阶到更高级的优化技法。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。