CPU 有 8 个核心,每个核心能跑 4GHz,单线程性能极强。GPU 有 5000 多个核心,每个核心只能跑 1.5GHz,单线程性能很弱——但 5000 个核心同时跑,吞吐量碾压 CPU。这不是”GPU 比 CPU 强”,而是两种完全不同的设计哲学:CPU 用延迟换吞吐量(尽快完成一个任务),GPU 用吞吐量换延迟(同时处理尽可能多的任务)。理解这个差异,才能知道什么时候该上 GPU,什么时候 CPU 反而更快。
一、GPU vs CPU:设计哲学
1.1 核心架构对比
| 维度 | CPU | GPU |
|---|---|---|
| 设计目标 | 最小化单线程延迟 | 最大化总吞吐量 |
| 核心数 | 4-8(桌面) | 数千(NVIDIA H100: 16896) |
| 时钟频率 | 3-5 GHz | 1-2 GHz |
| 缓存 | 大(MB 级) | 小(KB 级) |
| 分支预测 | 复杂、高准确率 | 无 |
| 乱序执行 | 支持 | 不支持 |
| 内存带宽 | ~50 GB/s | ~3000 GB/s |
| 单线程延迟 | ~1 ns | ~10 ns |
| 总吞吐量 | 低 | 极高 |
1.2 为什么 GPU 更适合并行计算
CPU 处理 1000 个数据: 1 个核心 × 1 ns/操作 × 1000 = 1000 ns 8 个核心 × 1 ns/操作 × 125 = 125 ns(并行)
GPU 处理 1000 个数据: 1000 个核心 × 10 ns/操作 × 1 = 10 ns(全并行)
GPU 的优势:当任务可以分解为大量独立操作时GPU 的劣势:当任务需要复杂分支或串行逻辑时二、SIMT 编程模型
2.1 SIMT vs SIMD
| 模型 | 含义 | 分支处理 |
|---|---|---|
| SIMD | Single Instruction Multiple Data | 所有数据必须执行相同操作 |
| SIMT | Single Instruction Multiple Threads | 线程可以分支,但会串行化执行 |
2.2 CUDA 执行层次
| 层次 | 说明 | 数量 |
|---|---|---|
| Grid | 一个 kernel 启动的所有线程 | 1 |
| Block | 线程组,共享内存,可同步 | 数百到数千 |
| Thread | 单个执行线程 | 数千到数百万 |
| Warp | 32 个线程同时执行(SIMT 单元) | Block 内多个 |
// CUDA kernel:指定 Grid 和 Block 大小__global__ void vectorAdd(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]; }}
// 启动 kernel:256 个线程/Block,n/256 个 Blockint blockSize = 256;int numBlocks = (n + blockSize - 1) / blockSize;vectorAdd<<<numBlocks, blockSize>>>(a, b, c, n);三、GPU 内存层次
3.1 内存类型
| 内存类型 | 延迟 | 容量 | 作用域 | 管理 |
|---|---|---|---|---|
| 寄存器 | ~1 ns | 256 KB/SM | 每线程 | 编译器 |
| 共享内存 | ~5 ns | 48-96 KB/SM | 每 Block | 程序员 |
| L1 缓存 | ~5 ns | 128 KB/SM | 每 SM | 硬件 |
| L2 缓存 | ~20 ns | 2-6 MB | 全局 | 略件 |
| 全局内存 | ~400 ns | 8-80 GB | 全局 | 程序员 |
| 常量内存 | ~5 ns(命中) | 64 KB | 全局 | 程序员 |
3.2 内存合并
// 非合并访问:线程访问分散的地址__global__ void badAccess(float* data) { int i = threadIdx.x * 256; // 步长 256 float val = data[i]; // 32 个线程访问 32 个分散地址 // 每个线程单独访问全局内存 → 32 次内存事务}
// 合并访问:线程访问连续地址__global__ void goodAccess(float* data) { int i = threadIdx.x; // 步长 1 float val = data[i]; // 32 个线程访问连续 128 字节 // 一次 128 字节内存事务 → 1 次访问}内存合并是 GPU 性能的关键。当 Warp 中的 32 个线程访问连续的 128 字节(32 × 4 字节 float),GPU 可以用一次内存事务完成所有读取。非合并访问需要 32 次独立事务,性能下降 10-30 倍。
四、Streaming Multiprocessor
4.1 SM 内部结构
一个 SM(Streaming Multiprocessor)包含:├── 64-128 个 CUDA 核心(FP32)├── 32-64 个 FP64 核心├── 4-8 个 Tensor 核心(矩阵加速)├── 1-2 个 Warp Scheduler├── 2-4 个 Dispatch Unit├── 64-128 KB 寄存器文件├── 48-96 KB 共享内存├── 128 KB L1 缓存└── 加载/存储单元└── 特殊功能单元(SFU)
NVIDIA H100 (Hopper): 132 个 SM × 128 核心/SM = 16896 个 FP32 核心 132 个 SM × 64 核心/SM = 8448 个 FP64 核心 132 个 SM × 4 Tensor 核心/SM = 528 个 Tensor 核心4.2 GPU 架构演进
| 架构 | 年份 | SM 数 | 核心/SM | 关键特性 |
|---|---|---|---|---|
| Fermi | 2010 | 16 | 32 | 首个完整 GPU 计算架构 |
| Kepler | 2012 | 15 | 192 | 动态并行、Hyper-Q |
| Maxwell | 2014 | 24 | 128 | 统一内存 |
| Pascal | 2016 | 60 | 64 | NVLink、FP16 |
| Volta | 2017 | 80 | 64 | Tensor Core、独立线程调度 |
| Ampere | 2020 | 108 | 128 | 第三代 Tensor Core、多实例 GPU |
| Hopper | 2022 | 132 | 128 | 第四代 Tensor Core、Transformer引擎 |
五、Warp 调度与 Occupancy
5.1 Warp 调度机制
SM 内部的 Warp Scheduler 是 GPU 实现高吞吐的关键。每个 SM 包含 2-4 个 Warp Scheduler,每个调度器每个周期从就绪的 Warp 中选一个发射指令。当某个 Warp 等待内存访问返回时,调度器立即切换到另一个就绪的 Warp——这种零开销切换正是 GPU 隐藏延迟的核心手段。
与 CPU 的线程切换不同,GPU 的 Warp 切换不需要保存和恢复寄存器——所有活跃 Warp 的寄存器都常驻在 SM 的寄存器文件中。调度器只需修改一个指针,下一周期就能执行新 Warp 的指令。
5.2 Occupancy:SM 利用率
Occupancy 衡量的是 SM 上实际活跃的 Warp 数量与最大支持数量的比值。高 Occupancy 意味着更多 Warp 可供调度,从而更好地隐藏内存延迟。
| 限制因素 | 影响 | 计算方式 |
|---|---|---|
| 寄存器数量 | 每个 Thread 使用的寄存器越多,SM 能容纳的 Block 越少 | 65536 ÷ (threads_per_block × regs_per_thread) |
| 共享内存 | 每个 Block 使用的共享内存越多,SM 能容纳的 Block 越少 | shared_mem_per_SM ÷ shared_mem_per_block |
| Block 大小 | 必须是 32 的倍数才有好的 Warp 利用率 | blockDim.x × blockDim.y × blockDim.z |
| 最大 Block 数 | 硬件限制每个 SM 最多 16-32 个 Block | 架构决定 |
// 使用 CUDA Occupancy API 查询最优配置int blockSize = 256;int numBlocks;cudaOccupancyMaxActiveBlocksPerMultiprocessor(&numBlocks, myKernel, blockSize, 0);float occupancy = (numBlocks * blockSize) / (float)maxThreadsPerSM;printf("Occupancy: %.1f%%\n", occupancy * 100);Occupancy 不是越高越好。当寄存器压力导致溢出(spill to local memory)时,反而会降低性能。实际调优中,50%-75% 的 Occupancy 往往就能达到最佳吞吐。
六、Tensor Core 与混合精度
6.1 Tensor Core 的工作原理
从 Volta 架构开始,NVIDIA 在每个 SM 中加入了 Tensor Core——专门执行矩阵乘加运算(MMA)的硬件单元。一条 Tensor Core 指令可以完成 D = A × B + C,其中 A、B 是 4×4 矩阵,一次计算 128 个乘加操作。
// 使用 WMMA (Warp Matrix Multiply-Accumulate) API#include <mma.h>using namespace nvcuda::wmma;
__global__ void tensorCoreMatmul(half* A, half* B, float* C, int N) { fragment<matrix_a, 16, 16, 16, half, row_major> a_frag; fragment<matrix_b, 16, 16, 16, half, col_major> b_frag; fragment<accumulator, 16, 16, 16, float> c_frag;
fill_fragment(c_frag, 0.0f);
for (int k = 0; k < N; k += 16) { load_matrix_sync(a_frag, A + ..., N); load_matrix_sync(b_frag, B + ..., N); mma_sync(c_frag, a_frag, b_frag, c_frag); } store_matrix_sync(C + ..., c_frag, N, mem_row_major);}6.2 精度格式对比
混合精度训练的核心思路:用低精度做前向和反向的计算(快),用高精度做梯度更新(准)。
| 格式 | 位宽 | 范围 | 精度 | 典型用途 |
|---|---|---|---|---|
| FP32 | 32 位 | ±3.4e38 | 7 位十进制 | 模型参数更新、损失计算 |
| FP16 | 16 位 | ±6.5e4 | 3 位十进制 | 前向/反向计算(需 loss scaling) |
| BF16 | 16 位 | ±3.4e38 | 2 位十进制 | 前向/反向计算(无需 loss scaling) |
| TF32 | 19 位 | ±3.4e38 | 3 位十进制 | Tensor Core 默认格式(Ampere+) |
| INT8 | 8 位 | -128~127 | 整数 | 推理量化 |
BF16 保持了 FP32 的动态范围但牺牲了精度,FP16 精度更高但范围小、容易溢出。TF32 是 Ampere 架构引入的折中——用 FP32 的寄存器和数据路径,但只保留 10 位尾数,在 Tensor Core 上获得接近 FP16 的吞吐。
七、GPU 编程模型深入
7.1 CUDA Streams 与异步执行
CUDA Stream 是 GPU 上的任务队列——同一个 Stream 内的操作串行执行,不同 Stream 之间的操作可以并行。合理使用多 Stream 可以实现数据传输与计算的 overlap。
// 多 Stream 实现 CPU-GPU 传输与计算的 overlapcudaStream_t stream0, stream1;cudaStreamCreate(&stream0);cudaStreamCreate(&stream1);
int chunkSize = N / 2;// Stream 0:处理前半部分数据cudaMemcpyAsync(d_A0, h_A0, chunkSize * sizeof(float), cudaMemcpyHostToDevice, stream0);kernel<<<grid, block, 0, stream0>>>(d_A0, d_B0, d_C0, chunkSize);cudaMemcpyAsync(d_C0, h_C0, chunkSize * sizeof(float), cudaMemcpyDeviceToHost, stream0);
// Stream 1:处理后半部分数据(与 Stream 0 并行)cudaMemcpyAsync(d_A1, h_A1, chunkSize * sizeof(float), cudaMemcpyHostToDevice, stream1);kernel<<<grid, block, 0, stream1>>>(d_A1, d_B1, d_C1, chunkSize);cudaMemcpyAsync(d_C1, h_C1, chunkSize * sizeof(float), cudaMemcpyDeviceToHost, stream1);
cudaDeviceSynchronize(); // 等待所有 Stream 完成7.2 Unified Memory
Unified Memory 让 CPU 和 GPU 共享同一块虚拟地址空间,驱动自动在 CPU 和 GPU 之间迁移数据:
// Unified Memory:无需手动 cudaMemcpyfloat* data;cudaMallocManaged(&data, N * sizeof(float));
// CPU 初始化for (int i = 0; i < N; i++) data[i] = i * 0.5f;
// GPU 计算(驱动自动将数据迁移到 GPU)kernel<<<grid, block>>>(data, N);cudaDeviceSynchronize();
// CPU 读取结果(驱动自动将数据迁移回 CPU)printf("result[0] = %f\n", data[0]);
cudaFree(data);Unified Memory 简化了编程,但首次访问时的页面迁移会产生额外延迟。生产环境中,通常用 cudaMemPrefetchAsync 主动预取数据到目标设备。
八、GPU 存储一致性
8.1 GPU 的弱序内存模型
GPU 采用弱序内存模型(Weakly Ordered Memory Model)——不同线程的内存操作对其他线程的可见顺序不保证与程序顺序一致。这与 CPU 的内存排序问题类似(详见第 8 章:内存排序),但 GPU 的模型更弱。
// GPU 上的可见性问题__device__ int data = 0;__device__ int flag = 0;
// Thread 0(生产者)data = 42; // 写入数据__threadfence(); // 确保所有先前写操作对全局可见flag = 1; // 设置标志
// Thread 1(消费者)while (flag != 1); // 等待标志// 没有 __threadfence(),可能看到 flag=1 但 data 仍是 0!int val = data; // 需要 fence 保证读到 428.2 同步原语
| 原语 | 作用域 | 功能 |
|---|---|---|
__syncthreads() | Block 内 | 所有线程到达屏障后才继续,并保证共享内存可见性 |
__threadfence() | 全局 | 确保此线程之前的所有内存写操作对全局可见 |
__threadfence_block() | Block 内 | 确保此线程之前的写操作对 Block 内其他线程可见 |
__threadfence_system() | 系统级 | 跨 GPU/CPU 的可见性保证(Pascal+) |
GPU 的弱序模型比 x86 TSO 更弱——x86 至少保证 Store-Store 顺序,而 GPU 连这个都不保证。在 GPU 上写并发代码时,必须显式使用 fence 和 barrier,不能依赖”直觉上的”顺序保证。
九、实战:矩阵乘法优化
9.1 基础版本
// 基础矩阵乘法:C = A × B__global__ void matmul_naive(float* A, float* B, float* C, int N) { int row = blockIdx.y * blockDim.y + threadIdx.y; int col = blockIdx.x * blockDim.x + threadIdx.x;
if (row < N && col < N) { float sum = 0.0f; for (int k = 0; k < N; k++) { sum += A[row * N + k] * B[k * N + col]; } C[row * N + col] = sum; }}// 性能:~1 GFLOPS(N=1024)// 问题:全局内存访问 N 次,每次 ~400 ns9.2 共享内存优化
// 使用共享内存的 tiled 矩阵乘法#define TILE_SIZE 16
__global__ void matmul_tiled(float* A, float* B, float* C, int N) { __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;
for (int t = 0; t < N / TILE_SIZE; t++) { // 加载 tile 到共享内存 As[threadIdx.y][threadIdx.x] = A[row * N + t * TILE_SIZE + threadIdx.x]; Bs[threadIdx.y][threadIdx.x] = B[(t * TILE_SIZE + threadIdx.y) * N + col];
__syncthreads(); // Block 内同步
// 在共享内存中计算 for (int k = 0; k < TILE_SIZE; k++) { sum += As[threadIdx.y][k] * Bs[k][threadIdx.x]; }
__syncthreads(); }
C[row * N + col] = sum;}// 性能:~50 GFLOPS(N=1024)// 优化:共享内存减少全局内存访问次数9.3 性能对比
| 版本 | GFLOPS | 优化手段 |
|---|---|---|
| Naive | ~1 | 无 |
| Tiled (共享内存) | ~50 | 共享内存 + 分块 |
| Tiled + 向量化 | ~100 | float4 加载 |
| Tensor Core | ~500 | WMMA API |
| cuBLAS | ~1000 | 官方优化库 |
十、GPU 性能分析
10.1 Nsight 工具链
GPU 程序的性能瓶颈不像 CPU 那样直观——你看到的”慢”可能是内存带宽受限、Occupancy 过低、Warp 分歧,或是指令吞吐不足。NVIDIA 提供了两款互补的分析工具:
| 工具 | 层级 | 用途 | 适合场景 |
|---|---|---|---|
| Nsight Systems | 系统级 | 时间线视图,看 Kernel/传输/Stream 的时序关系 | 发现传输瓶颈、Stream 未重叠 |
| Nsight Compute | Kernel 级 | 指令级剖析,看 SM 利用率、内存吞吐、Occupancy | 定位具体 Kernel 的瓶颈 |
# Nsight Systems:宏观时间线分析nsys profile --stats=true ./my_gpu_appnsys report report1.qdrep # 生成时间线报告
# Nsight Compute:微观 Kernel 剖析ncu --set full --target-processes all ./my_gpu_appncu --launch-skip 10 --launch-count 1 ./my_gpu_app # 跳过 warmup,只剖析第 11 次10.2 Roofline 模型
Roofline 模型将 GPU 性能分析简化为一个二维图:横轴是算术强度(FLOP/Byte),纵轴是可达性能(GFLOPS)。模型的”屋顶”由两个约束构成——内存带宽(左侧斜线)和计算吞吐(右侧平顶)。一个 Kernel 的实测点如果落在屋顶下方,说明还有优化空间。
| 瓶颈类型 | 算术强度 | 优化方向 |
|---|---|---|
| 内存受限 | < ~10 FLOP/Byte | 减少内存访问、提高合并率、使用共享内存 |
| 计算受限 | > ~30 FLOP/Byte | 使用 Tensor Core、提高指令级并行 |
| 延迟受限 | Occupancy 过低 | 增加 Block 数量、减少寄存器使用 |
GPU 程序最常见的错误是”凭直觉优化”——看到 Kernel 慢就加共享内存,实际上瓶颈可能在 PCIe 传输或 Kernel 启动开销。先用 Nsight Systems 定位瓶颈层级,再用 Nsight Compute 深入分析具体 Kernel。
十一、GPU 在后端系统中的应用
11.1 GPU 加速数据库
-- GPU 加速的数据库查询-- BlazingSQL / OmniDB 等项目
-- 传统:CPU 执行聚合SELECT user_id, SUM(amount)FROM ordersWHERE created_at >= '2026-01-01'GROUP BY user_id;-- 执行时间:~5 秒(1 亿行)
-- GPU 加速:将数据传输到 GPU 执行-- 1. 数据从磁盘加载到 CPU 内存-- 2. CPU 内存 → GPU 内存(PCIe,~12 GB/s)-- 3. GPU 执行聚合(数千核心并行)-- 4. 结果 GPU 内存 → CPU 内存-- 执行时间:~0.5 秒(10x 加速)11.2 GPU 在网络中的应用
GPU-Direct RDMA: 传统:网卡 → CPU 内存 → GPU 内存(2 次拷贝) GPU-Direct:网卡 → GPU 内存(0 次额外拷贝)
应用场景: - 高频交易:市场数据直接到 GPU 进行分析 - 深度学习推理:网络接收数据直接到 GPU 处理 - 视频处理:视频流直接到 GPU 编解码11.3 GPU 推理服务架构
现代 LLM 推理服务(如 vLLM、TensorRT-LLM)采用 CPU+GPU 协同架构:CPU 负责请求调度、KV Cache 管理和 Token 采样,GPU 负责模型前向计算。
| 组件 | 运行位置 | 瓶颈类型 |
|---|---|---|
| 请求调度与批处理 | CPU | CPU 密集(排序、合并) |
| Tokenize / Embedding | CPU / GPU | 内存带宽 |
| Transformer 前向计算 | GPU | 计算密集(Tensor Core) |
| KV Cache 管理 | GPU 显存 | 内存容量与带宽 |
| Token 采样 | CPU | 随机数生成 |
十二、动手实践
12.1 完整的 CUDA 向量加法程序
// vecadd.cu — 完整的 CUDA 程序#include <stdio.h>#include <stdlib.h>
__global__ void vecAdd(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() { int N = 1 << 20; // 1M 元素 size_t bytes = N * sizeof(float);
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; }
float *d_A, *d_B, *d_C; cudaMalloc(&d_A, bytes); cudaMalloc(&d_B, bytes); cudaMalloc(&d_C, bytes);
cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice); cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice);
int blockSize = 256; int numBlocks = (N + blockSize - 1) / blockSize; vecAdd<<<numBlocks, blockSize>>>(d_A, d_B, d_C, N);
cudaMemcpy(h_C, d_C, bytes, cudaMemcpyDeviceToHost); printf("h_C[0] = %f (expected 3.0)\n", h_C[0]);
cudaFree(d_A); cudaFree(d_B); cudaFree(d_C); free(h_A); free(h_B); free(h_C); return 0;}12.2 编译与运行
# 编译 CUDA 程序nvcc -O3 -o vecadd vecadd.cu
# 运行./vecadd# 输出:h_C[0] = 3.000000 (expected 3.0)
# 使用 Nsight Systems 分析时间线nsys profile --stats=true ./vecaddnsys report report1.qdrep
# 使用 Nsight Compute 剖析 Kernelncu --set basic ./vecadd# 关注指标:Memory Throughput、Compute Throughput、Occupancy12.3 性能剖析
# 使用 nvprof 快速查看 Kernel 执行时间(旧版工具)nvprof ./vecadd# 输出示例:# GPU activities: 99.3% vecAdd 12.3us# API calls: 85.1% cudaMalloc 320ms
# 对比不同 Block 大小的性能for bs in 128 256 512 1024; do echo "Block size = $bs" ncu --metrics gpu__time_duration.sum ./vecadddone十三、总结
上一章理解了无锁编程与原子操作。
| 维度 | CPU | GPU |
|---|---|---|
| 设计哲学 | 延迟优化 | 吞吐优化 |
| 核心数 | 少(4-8) | 多(数千) |
| 单线程性能 | 强 | 弱 |
| 并行吞吐 | 低 | 极高 |
| 内存带宽 | ~50 GB/s | ~3000 GB/s |
| 编程模型 | 串行 + 线程 | SIMT |
| 适用场景 | 通用计算、复杂逻辑 | 大规模并行、矩阵运算 |
GPU 不是 CPU 的替代品,而是互补品。CPU 处理复杂逻辑和串行任务,GPU 处理大规模并行任务。现代系统越来越采用 CPU+GPU 协同计算——CPU 负责调度和控制,GPU 负责计算密集型工作。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






