GPU 与 CUDA 编程基础 — 从硬件到核函数
#GPU · #CUDA · #TensorCore · #显存层次 · #并行计算 · #性能优化
LLM 推理和训练的性能瓶颈几乎都在 GPU 上。理解 GPU 架构和 CUDA 编程模型,是理解 Flash Attention、量化、TensorRT 等优化的前提。本文从 GPU 硬件架构入手,逐步深入到 CUDA 编程实战。
GPU 硬件架构
CPU vs GPU 架构差异
mermaid
graph TD
subgraph "CPU (Intel i9)"
CTRL_CPU["复杂控制单元<br/>分支预测/乱序执行"] --> ALU_CPU["少量 ALU<br/>大核心 × 16-24 核"]
CACHE_CPU["大容量缓存<br/>L1 64KB | L2 2MB | L3 36MB"]
end
subgraph "GPU (NVIDIA H100)"
CTRL_GPU["简单控制单元<br/>Warp Scheduler"] --> ALU_GPU["海量 ALU<br/>小核心 × 18432 核"]
CACHE_GPU["小容量缓存<br/>L1 256KB | L2 50MB"]
end
NOTE["CPU 擅长: 复杂控制流、低延迟单线程<br/>GPU 擅长: 数据并行、高吞吐量计算"]| 特性 | CPU | GPU |
|---|---|---|
| 核心数 | 16-64 | 数千~上万 |
| 单核性能 | 极高 (高频 + 超标量) | 较低 (简化核心) |
| 吞吐量 | 低 | 极高 |
| 缓存 | 大 (三级缓存 ~100MB) | 小 (L2 ~50MB) |
| 控制逻辑 | 复杂 (分支预测/乱序执行) | 简单 (SIMT) |
| 适用 | 操作系统、Web 服务、数据库 | 矩阵运算、图像处理、深度学习 |
NVIDIA GPU 演进
| GPU | 年份 | 显存 | FP16 TFLOPS | Tensor Core | 关键特性 |
|---|---|---|---|---|---|
| V100 | 2017 | 32GB HBM2 | 125 | 第 1 代 | Volta 架构 |
| A100 | 2020 | 80GB HBM2e | 312 | 第 3 代 | 稀疏化 2x |
| H100 | 2023 | 80GB HBM3 | 989 | 第 4 代 | FP8, Transformer Engine |
| H200 | 2024 | 141GB HBM3e | 989 | 第 4 代 | 更大显存 |
| B200 | 2025 | 192GB HBM3e | ~2250 | 第 5 代 | Blackwell, FP4 |
GPU 内存层次
mermaid
graph TD
subgraph "GPU 内存层次 (延迟从小到大)"
REG["寄存器 (Register)<br/>每个线程私有<br/>最快 (~0 cycles)<br/>容量: 每 SM 65536 个 32-bit"]
SMEM["共享内存 (Shared Memory)<br/>线程块内共享<br/>极快 (~20 cycles)<br/>容量: 每 SM 最大 228KB (H100)"]
L1["L1 缓存<br/>每 SM 私有<br/>快 (~30 cycles)<br/>容量: 256KB/SM"]
L2["L2 缓存<br/>所有 SM 共享<br/>较慢 (~200 cycles)<br/>容量: 50MB (H100)"]
GMEM["全局显存 (Global Memory / HBM)<br/>所有 SM 和 CPU 可访问<br/>慢 (~400-800 cycles)<br/>容量: 80-192GB (HBM3e)"]
end
REG --> SMEM --> L1 --> L2 --> GMEM关键洞察:共享内存比全局显存快 20-40 倍。Flash Attention 的核心优化——分块计算中所有中间结果都放在 SRAM(共享内存+L1) 中而非写回 HBM。
内存带宽
| GPU | 带宽 | 带宽利用率 80% |
|---|---|---|
| A100 80GB | 2.0 TB/s | 1.6 TB/s |
| H100 80GB | 3.35 TB/s | 2.68 TB/s |
| H200 141GB | 4.8 TB/s | 3.84 TB/s |
| RTX 4090 | 1.0 TB/s | 0.8 TB/s |
LLM 推理的 Decode 阶段是 Memory-bound:kernels 等数据的时间远多于计算时间。因此带宽是推理吞吐的天花板。
CUDA 编程模型
线程层级
mermaid
graph TD
GRID["Grid (网格)<br/>一次 kernel 启动"] --> BLOCK0["Block 0<br/>(0,0)"]
GRID --> BLOCK1["Block 1<br/>(0,1)"]
GRID --> BLOCK2["Block 2<br/>(1,0)"]
GRID --> BLOCK3["Block 3<br/>(1,1)"]
BLOCK0 --> THR00["Thread (0,0)"]
BLOCK0 --> THR01["Thread (0,1)"]
BLOCK0 --> THR_MORE["..."]
BLOCK0 --> THR15["Thread (3,3)"]
WARP["32 个线程 = 1 个 Warp<br/>最小调度单元"]| 层级 | 标识 | 数量限制 | 通信方式 |
|---|---|---|---|
| Thread | threadIdx | 每 Block 最多 1024 | 寄存器 |
| Warp | — | 固定 32 线程 | __shfl_sync |
| Block | blockIdx | 1D/2D/3D | shared 内存 + __syncthreads |
| Grid | — | 1D/2D/3D | 全局内存 (需 kernel 结束) |
第一个 CUDA 程序
cuda
// vector_add.cu — 向量加法
#include <cuda_runtime.h>
#include <stdio.h>
// GPU 核函数 (Kernel)
// __global__ 表示 CPU 调用、GPU 执行
__global__ void vector_add(float *a, float *b, float *c, int n) {
// 计算全局线程 ID
int idx = blockIdx.x * blockDim.x + threadIdx.x;
// 边界检查
if (idx < n) {
c[idx] = a[idx] + b[idx];
}
}
int main() {
int n = 1 << 20; // 1M 元素
size_t bytes = n * sizeof(float);
// 1. 分配主机内存 (CPU)
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. 分配设备内存 (GPU)
float *d_a, *d_b, *d_c;
cudaMalloc(&d_a, bytes);
cudaMalloc(&d_b, bytes);
cudaMalloc(&d_c, bytes);
// 3. 拷贝数据到 GPU
cudaMemcpy(d_a, h_a, bytes, cudaMemcpyHostToDevice);
cudaMemcpy(d_b, h_b, bytes, cudaMemcpyHostToDevice);
// 4. 启动 Kernel
int threads_per_block = 256;
int blocks_per_grid = (n + threads_per_block - 1) / threads_per_block;
vector_add<<<blocks_per_grid, threads_per_block>>>(d_a, d_b, d_c, n);
// 5. 拷贝结果回 CPU
cudaMemcpy(h_c, d_c, bytes, cudaMemcpyDeviceToHost);
// 6. 验证
float max_error = 0.0f;
for (int i = 0; i < n; i++) {
max_error = fmax(max_error, fabs(h_c[i] - 3.0f));
}
printf("Max error: %f\n", max_error);
// 7. 释放内存
cudaFree(d_a); cudaFree(d_b); cudaFree(d_c);
free(h_a); free(h_b); free(h_c);
return 0;
}bash
# 编译运行
nvcc -o vector_add vector_add.cu
./vector_add线程索引计算
cuda
// 1D Grid + 1D Block
int tid = blockIdx.x * blockDim.x + threadIdx.x;
// 2D Grid + 2D Block (图像处理常用)
int col = blockIdx.x * blockDim.x + threadIdx.x; // 图像宽度方向
int row = blockIdx.y * blockDim.y + threadIdx.y; // 图像高度方向Tensor Core — 矩阵乘法的加速器
工作原理
mermaid
graph TD
subgraph "Tensor Core: D = A × B + C"
A["矩阵 A<br/>FP16 (4×4)"] --> TC["Tensor Core<br/>单周期完成<br/>D = A×B + C"]
B["矩阵 B<br/>FP16 (4×4)"] --> TC
C["矩阵 C<br/>FP32 (4×4)"] --> TC
TC --> D["矩阵 D<br/>FP32 (4×4)"]
end
NOTE["H100 Tensor Core:<br/>FP16: 989 TFLOPS<br/>FP8: 1979 TFLOPS<br/>FP4: ~4000 TFLOPS<br/>(稀疏化 2x 后)"]| 精度 | 相对速度 | 适用阶段 |
|---|---|---|
| FP32 | 1× (基准) | 训练中的梯度累积 |
| TF32 | 8× | 训练前向/反向的主要精度 |
| FP16/BF16 | 16× | 推理 + 混合精度训练 |
| FP8 | 32× | H100+ 推理加速 |
| INT8 | 32× | 推理量化 |
| INT4 | 64× | 极端推理量化 |
用 CUDA 调用 Tensor Core
cuda
// 通过 warp-level matrix multiply (wmma) 使用 Tensor Core
#include <cuda_fp16.h>
#include <mma.h>
using namespace nvcuda;
__global__ void tensor_core_matmul(
half *A, half *B, float *C, int M, int N, int K
) {
// 声明 warp-level 操作的 fragment
wmma::fragment<wmma::matrix_a, 16, 16, 16, half, wmma::row_major> a_frag;
wmma::fragment<wmma::matrix_b, 16, 16, 16, half, wmma::col_major> b_frag;
wmma::fragment<wmma::accumulator, 16, 16, 16, float> c_frag;
// 初始化累加器
wmma::fill_fragment(c_frag, 0.0f);
// 加载并计算
wmma::load_matrix_sync(a_frag, A, K);
wmma::load_matrix_sync(b_frag, B, K);
wmma::mma_sync(c_frag, a_frag, b_frag, c_frag);
// 存储结果
wmma::store_matrix_sync(C, c_frag, N, wmma::mem_row_major);
}共享内存优化实战
Bank Conflict
共享内存被组织为 32 个 bank(对应 32 线程的 warp)。同一 warp 内多个线程访问同一 bank 的不同地址时,会产生 bank conflict → 串行化。
cuda
// ❌ 坏: bank conflict — 线程 i 访问 bank[i*2]
__shared__ float smem[128];
int idx = threadIdx.x;
float val = smem[idx * 2]; // threads 0,1 都访问 bank 0 → 冲突!
// ✅ 好: 无 bank conflict — 线程 i 访问 bank[i]
float val = smem[idx]; // threads 0,1 访问 bank 0,1 → 无冲突矩阵乘法分块 (Tiling)
cuda
// 使用共享内存的分块矩阵乘法 (简化版)
#define TILE_SIZE 32
__global__ void matmul_tiled(
float *A, float *B, float *C, int M, int N, int K
) {
__shared__ float As[TILE_SIZE][TILE_SIZE];
__shared__ float Bs[TILE_SIZE][TILE_SIZE];
int row = blockIdx.y * TILE_SIZE + threadIdx.y;
int col = blockIdx.x * TILE_SIZE + threadIdx.x;
float sum = 0.0f;
// 遍历所有 tile
for (int t = 0; t < (K + TILE_SIZE - 1) / TILE_SIZE; t++) {
// 协作加载 A 和 B 的 tile 到共享内存
int a_col = t * TILE_SIZE + threadIdx.x;
As[threadIdx.y][threadIdx.x] = (row < M && a_col < K) ? A[row * K + a_col] : 0.0f;
int b_row = t * TILE_SIZE + threadIdx.y;
Bs[threadIdx.y][threadIdx.x] = (b_row < K && col < N) ? B[b_row * N + col] : 0.0f;
__syncthreads(); // 确保所有线程加载完毕
// 在共享内存上计算
for (int k = 0; k < TILE_SIZE; k++) {
sum += As[threadIdx.y][k] * Bs[k][threadIdx.x];
}
__syncthreads(); // 确保所有线程计算完毕再加载下一 tile
}
if (row < M && col < N) {
C[row * N + col] = sum;
}
}性能对比
| 实现方式 | 相对速度 | 瓶颈 |
|---|---|---|
| 朴素实现 | 1× | 全局内存带宽 |
| 共享内存分块 | 10-20× | 共享内存延迟 |
| cuBLAS | 50-100× | Tensor Core 理论极限 |
GPU 性能分析工具
nvidia-smi
bash
# 实时监控 GPU 利用率、温度、功耗
nvidia-smi dmon -s pucvmet -d 1
# 输出:
# gpu pwr gtemp mtemp sm mem enc dec mclk pclk
# 0 320 65 65 98 85 0 0 9501 1980
# sm=98% → 计算利用率饱和
# mem=85% → 显存带宽利用率Nsight Systems / Compute
bash
# 性能分析: 查看 kernel 耗时和内存传输
nsys profile --stats=true ./my_cuda_program
# 深入分析单个 kernel 的瓶颈
ncu --set full -o profile_report ./my_cuda_program
# 输出: 计算吞吐、内存带宽、寄存器占用、bank conflict 次数PyTorch 中的 GPU 编程技巧
python
"""
PyTorch 中的 GPU 性能最佳实践
"""
import torch
# 1. 避免 CPU-GPU 数据传输
# ❌ 坏: 频繁 .item() 或 .cpu()
for i in range(1000):
loss = model(batch).item() # 1000 次 GPU→CPU 传输!
print(loss)
# ✅ 好: 批量收集后再传回
losses = []
for i in range(1000):
losses.append(model(batch).detach())
losses = torch.stack(losses).cpu() # 1 次传输
# 2. 使用 pin_memory 加速数据传输
dataloader = DataLoader(
dataset,
batch_size=64,
pin_memory=True, # 锁页内存, 加速 CPU→GPU
num_workers=4, # 多进程加载
)
# 3. 异步传输 (Non-blocking)
data = data.cuda(non_blocking=True) # CPU→GPU 异步, 不阻塞 CPU
# 4. CUDA Stream 并行
stream1 = torch.cuda.Stream()
stream2 = torch.cuda.Stream()
with torch.cuda.stream(stream1):
output1 = model(batch1) # Stream 1 推理
with torch.cuda.stream(stream2):
output2 = model(batch2) # Stream 2 并行推理
torch.cuda.synchronize() # 等待所有 Stream 完成GPU 显存管理
python
# 查看显存使用
print(f"已分配: {torch.cuda.memory_allocated() / 1e9:.2f} GB")
print(f"已缓存: {torch.cuda.memory_reserved() / 1e9:.2f} GB")
print(f"最大分配: {torch.cuda.max_memory_allocated() / 1e9:.2f} GB")
# 清空缓存(释放碎片,不释放正在使用的显存)
torch.cuda.empty_cache()
# 重置统计
torch.cuda.reset_peak_memory_stats()
# 训练后清理
del model
del optimizer
torch.cuda.empty_cache()常见性能陷阱
| 陷阱 | 表现 | 原因 | 解决 |
|---|---|---|---|
| 数据传输瓶颈 | CPU 利用率高但 GPU 空闲 | .item()/.cpu() 过于频繁 | 批量操作,减少 CPU-GPU 传输 |
| 小 batch 推理 | 吞吐量极低 | 每个 kernel 启动开销 > 计算量 | 动态批处理 (Continuous Batching) |
| Bank Conflict | 共享内存延迟异常 | 同一 warp 访问相同 bank | padding 或重排访问模式 |
| Occupancy 不足 | 大量 SM 空闲 | Block 太少或寄存器/共享内存超限 | 调整 Block 大小, 减少寄存器使用 |
| 分支发散 | warp 内线程走不同分支 | if/else 导致部分线程闲置 | 重构为无分支逻辑 |
登录后即可发表评论 👇