跳到主要内容

GPU 计算基础

GPU(图形处理器)是现代 AI 计算的核心硬件。理解 GPU 的工作原理和编程模型,对于优化 AI 应用的性能至关重要。

GPU 与 CPU 的区别

GPU 和 CPU 在设计理念上有根本性的不同:

特性CPUGPU
核心数量少(几十个)多(数千个)
单核性能
内存带宽较低极高
适用场景串行任务、复杂逻辑并行任务、简单计算
设计目标低延迟高吞吐量

CPU 适合处理复杂的逻辑控制和串行任务,而 GPU 则擅长处理大规模并行计算。这种差异源于它们的设计目标:CPU 追求单线程的低延迟,GPU 追求整体的高吞吐量。

一个形象的比喻

如果把计算任务比作运送货物:

  • CPU 就像一辆法拉利,速度快但载货量小,适合运送少量急需的货物
  • GPU 就像一支由小型卡车组成的车队,每辆车速度不快但数量庞大,适合运送大量货物

GPU 架构详解

流式多处理器(SM)

GPU 的核心计算单元是流式多处理器(Streaming Multiprocessor,SM)。每个 SM 包含:

  • CUDA 核心:执行浮点和整数运算
  • 张量核心:专门用于矩阵运算,对深度学习至关重要
  • 共享内存:SM 内部的高速缓存
  • 寄存器文件:存储线程的局部变量
  • 调度单元:管理线程的执行

以 NVIDIA H100 为例,单个 GPU 包含 132 个 SM,每个 SM 有 128 个 CUDA 核心,总计 16896 个核心。

内存层次结构

GPU 的内存层次结构对性能有重大影响:

┌─────────────────────────────────────────┐
│ HBM(高带宽内存) │
│ 容量:80GB,带宽:3.35TB/s │
├─────────────────────────────────────────┤
│ L2 缓存 │
│ 容量:50MB,延迟:低 │
├─────────────────────────────────────────┤
│ 共享内存 / L1 缓存(每个 SM) │
│ 容量:228KB,延迟:极低 │
├─────────────────────────────────────────┤
│ 寄存器文件 │
│ 容量:256KB,延迟:最低 │
└─────────────────────────────────────────┘

关键原则:数据越靠近计算单元,访问速度越快。优化 GPU 程序的核心就是最大化数据在高速存储中的复用。

张量核心

张量核心(Tensor Core)是 NVIDIA GPU 中专门用于矩阵乘法加速的硬件单元。它可以在一个时钟周期内完成一个 4×4 矩阵乘法运算。

对于深度学习中最常见的矩阵乘法 C = A × B

  • 传统 CUDA 核心:需要 64 次乘法和 48 次加法
  • 张量核心:单次操作完成

H100 的张量核心在 FP16 精度下可提供近 2000 TFLOPS 的算力。

CUDA 编程模型

CUDA(Compute Unified Device Architecture)是 NVIDIA 推出的并行计算平台和编程模型。

线程层次结构

CUDA 的线程组织分为三个层次:

Grid(网格)
└── Block(线程块)
└── Thread(线程)
  • Thread(线程):最小的执行单元,执行 kernel 中的代码
  • Block(线程块):一组线程,可以协作执行,共享内存
  • Grid(网格):一组线程块,组成完整的 kernel 执行

线程索引

每个线程都有唯一的索引,用于确定它应该处理的数据:

// 一维索引
int idx = blockIdx.x * blockDim.x + threadIdx.x;

// 二维索引
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;

Kernel 函数

Kernel 是在 GPU 上执行的函数,使用 __global__ 关键字声明:

// 向量加法 kernel
__global__ void vectorAdd(float *a, float *b, float *c, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
c[idx] = a[idx] + b[idx];
}
}

// 主机端调用
int blockSize = 256;
int numBlocks = (n + blockSize - 1) / blockSize;
vectorAdd<<<numBlocks, blockSize>>>(d_a, d_b, d_c, n);

内存管理

CUDA 程序需要显式管理主机(CPU)和设备(GPU)之间的数据传输:

// 分配设备内存
float *d_a;
cudaMalloc(&d_a, size);

// 数据传输:主机到设备
cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice);

// 数据传输:设备到主机
cudaMemcpy(h_c, d_c, size, cudaMemcpyDeviceToHost);

// 释放内存
cudaFree(d_a);

性能优化原则

最大化并行度

GPU 的性能来自于大规模并行。要充分利用 GPU,需要:

  • 足够的线程数量:通常需要数千到数万个线程来隐藏内存延迟
  • 合理的线程块大小:通常是 128、256 或 512,需要是 32 的倍数

优化内存访问

内存带宽通常是 GPU 程序的瓶颈:

合并访问:相邻线程访问相邻的内存地址,可以合并为一次内存事务

// 好的访问模式(合并访问)
int idx = blockIdx.x * blockDim.x + threadIdx.x;
float val = data[idx];

// 差的访问模式(跨步访问)
float val = data[threadIdx.x * stride];

使用共享内存:共享内存比全局内存快很多,适合存储需要重复访问的数据

__shared__ float sharedData[BLOCK_SIZE];
sharedData[threadIdx.x] = globalData[idx];
__syncthreads(); // 同步线程块内所有线程

减少数据传输

主机和设备之间的数据传输是性能瓶颈:

  • 尽量减少传输次数
  • 使用 pinned memory 加速传输
  • 考虑使用统一内存(Unified Memory)

利用张量核心

对于矩阵乘法等操作,使用张量核心可以大幅提升性能:

// 使用 cublas 库调用张量核心
cublasGemmEx(handle,
CUBLAS_OP_N, CUBLAS_OP_N,
m, n, k,
&alpha, A, CUDA_R_16F, lda,
B, CUDA_R_16F, ldb,
&beta, C, CUDA_R_16F, ldc,
CUDA_R_32F, CUBLAS_GEMM_DEFAULT_TENSOR_OP);

常用工具

NVIDIA Nsight

Nsight 是 NVIDIA 提供的性能分析工具套件:

  • Nsight Systems:系统级性能分析,查看整体执行时间线
  • Nsight Compute:kernel 级性能分析,分析内存访问效率等

CUDA-MEMCHECK

检测内存错误:

cuda-memcheck ./my_program

nvprof

命令行性能分析工具:

nvprof ./my_program

多 GPU 编程

数据并行

每个 GPU 持有完整模型,处理不同数据:

int deviceCount;
cudaGetDeviceCount(&deviceCount);

for (int i = 0; i < deviceCount; i++) {
cudaSetDevice(i);
// 在设备 i 上执行 kernel
myKernel<<<blocks, threads>>>(...);
}

点对点通信

GPU 之间直接通信:

// 启用 P2P 访问
cudaDeviceEnablePeerAccess(peerDevice, 0);

// 直接从另一个 GPU 复制数据
cudaMemcpyPeer(dst, dstDevice, src, srcDevice, size);

实践建议

  1. 先保证正确性,再优化性能:使用 CUDA-MEMCHECK 检查内存错误
  2. 使用性能分析工具:找到真正的瓶颈再优化
  3. 参考最佳实践:NVIDIA 提供了许多优化示例
  4. 利用现有库:cuBLAS、cuDNN 等库已经高度优化

小结

GPU 是 AI 计算的核心硬件,理解其架构和编程模型对于构建高效的 AI 系统至关重要。CUDA 提供了灵活的编程接口,但也需要开发者深入理解硬件特性才能充分发挥性能。在实际开发中,优先使用高度优化的库,如 cuBLAS 和 cuDNN,可以避免重复造轮子。

参考资料