第三章:GPU硬件架构详解——SM、CUDA Core、Warp调度与内存层次

各位同学,今天我们来聊聊GPU的“骨架”。

说实话,我刚开始接触CUDA时,最头疼的就是搞不清硬件到底长什么样。你写个kernel,它怎么就跑起来了?为什么有些代码快得飞起,有些却慢得像蜗牛?

嗯,答案全在硬件架构里。这一章,我们就把GPU的“五脏六腑”拆开看看。

3.1 流多处理器(SM)——GPU的“计算单元”

先问个问题:GPU和CPU最大的区别在哪?

CPU是“全能选手”,什么活都能干,但一次只能干几件。GPU呢?它是“人海战术”的典范——成千上万个简单计算单元同时干活。

这个“简单计算单元”的集合,就是SM(Streaming Multiprocessor,流多处理器)

核心概念:SM是GPU内部最基本的独立计算单元。一个GPU芯片里,通常有几十到上百个SM。每个SM都有自己的控制单元、执行单元、缓存和寄存器文件。

我习惯把SM比作一个“小工厂”。每个工厂里有很多工人(CUDA Core),有仓库(共享内存),有调度员(Warp Scheduler)。你写的CUDA程序,最终就是被分配到这些工厂里执行的。

举个例子,NVIDIA的A100 GPU有108个SM。每个SM内部有64个FP32 CUDA Core。算一下:108 × 64 = 6912个核心。这就是为什么GPU能同时处理那么多线程。

个人经验:我在做大规模并行计算时,经常需要估算SM数量。比如你要处理100万个数据点,如果SM太少,线程块分配就会很拥挤。我一般会先查一下目标GPU的SM数量,再决定线程块的大小。

3.2 CUDA Core——真正的“打工人”

SM里的CUDA Core,才是真正干活的。

每个CUDA Core本质上是一个浮点运算单元,能执行整数和浮点数的加减乘除。但注意,它不像CPU核心那样有复杂的乱序执行、分支预测功能。说白了,它就是个“傻大个”,只会按指令干活。

一个典型的CUDA Core包含:

  • 整数运算单元(INT ALU):处理整数加减、位运算等
  • 单精度浮点单元(FP32):处理32位浮点数运算
  • 双精度浮点单元(FP64):处理64位浮点数(数量通常只有FP32的1/2或1/4)
  • 张量核心(Tensor Core):专门为AI矩阵运算设计的加速单元
核心类型 用途 性能特点
INT32 整数运算、地址计算 快,但精度有限
FP32 通用浮点计算 最常用,平衡精度和速度
FP64 科学计算、高精度需求 慢,数量少
Tensor Core 矩阵乘法、深度学习 极快,但需要特定数据格式

避坑指南:我曾经有个项目,用FP64做物理模拟,结果性能惨不忍睹。后来才发现,我的GPU(RTX 3080)FP64核心只有FP32的1/64。换成FP32后,速度提升了30倍。所以,不是所有核心都生而平等

3.3 Warp调度——GPU的“指挥艺术”

现在问题来了:SM里有那么多CUDA Core,怎么让它们高效工作?

答案是Warp

Warp是GPU调度的基本单位。一个Warp包含32个线程。这32个线程在同一个SM上执行相同的指令,但处理不同的数据。这就是所谓的SIMT(单指令多线程)模型。

每个SM里有多个Warp Scheduler(调度器)。它的工作就是:

  1. 从就绪的Warp中选一个
  2. 发射一条指令给CUDA Core
  3. 切换到下一个Warp
  4. 重复以上步骤

为什么要这样?因为内存访问很慢。当一个Warp在等数据时,调度器可以立刻切换到另一个Warp,让CUDA Core不闲着。这叫延迟隐藏

关键点:Warp内的32个线程必须执行同一条指令。如果遇到分支(if-else),部分线程会“休眠”,等另一部分执行完再唤醒。这就是Warp Divergence(分支发散),会严重降低性能。

我记得有一次优化一个图像处理算法,发现性能始终上不去。后来用nvprof一查,Warp Divergence高达40%。原来是我在循环里用了if (threadIdx.x % 2 == 0)这样的分支。改成按Warp对齐后,性能直接翻倍。

建议:写CUDA代码时,尽量让Warp内的32个线程走相同的路径。如果必须分支,让分支条件基于warpId(线程ID除以32),而不是基于单个线程ID。

3.4 内存层次结构——GPU的“存储迷宫”

GPU的内存系统,是性能优化的重中之重。我见过太多人,代码写得漂亮,但内存访问一塌糊涂,性能惨不忍睹。

GPU的内存从快到慢、从近到远,分为这几个层次:

3.4.1 寄存器(Register)

最快,每个线程私有。容量极小,每个线程最多255个32位寄存器。如果你用了太多局部变量,寄存器不够用,数据会被“溢出”到本地内存(其实在全局内存里),速度骤降。

3.4.2 共享内存(Shared Memory)

在SM内部,所有线程块内的线程可以共享。速度很快,但容量有限(通常几十KB到上百KB)。

共享内存是手动管理的缓存。你可以在__shared__关键字声明它。我经常用它来做数据复用,比如矩阵分块计算。

// 共享内存示例:矩阵分块
__global__ void matmul_shared(float *A, float *B, float *C, int N) {
    __shared__ float As[BLOCK_SIZE][BLOCK_SIZE];
    __shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE];
    
    int bx = blockIdx.x, by = blockIdx.y;
    int tx = threadIdx.x, ty = threadIdx.y;
    
    float sum = 0.0f;
    for (int k = 0; k < N / BLOCK_SIZE; ++k) {
        // 加载数据到共享内存
        As[ty][tx] = A[by * BLOCK_SIZE + ty][k * BLOCK_SIZE + tx];
        Bs[ty][tx] = B[k * BLOCK_SIZE + ty][bx * BLOCK_SIZE + tx];
        __syncthreads();  // 同步,确保所有线程加载完成
        
        // 计算
        for (int i = 0; i < BLOCK_SIZE; ++i)
            sum += As[ty][i] * Bs[i][tx];
        __syncthreads();
    }
    C[by * BLOCK_SIZE + ty][bx * BLOCK_SIZE + tx] = sum;
}

3.4.3 L1/L2缓存

L1缓存和共享内存共用同一块物理存储(通常可配置)。L2缓存是所有SM共享的,容量更大(几MB到几十MB)。

缓存是自动管理的,但你可以通过__ldg()等指令提示编译器使用只读缓存。

3.4.4 全局内存(Global Memory)

最大的存储空间(几GB到几十GB),但最慢。延迟通常400-800个时钟周期。

访问全局内存时,要特别注意合并访问(Coalesced Access)。说白了,就是让同一个Warp的32个线程访问连续的内存地址。这样GPU可以把多次访问合并成一次大的内存事务。

合并访问示例:

好的访问模式:data[threadIdx.x](连续)

坏的访问模式:data[threadIdx.x * 32](跨步)

坏的访问模式:data[blockIdx.x * blockDim.x + threadIdx.x](如果blockDim.x不是32的倍数)

3.4.5 常量内存和纹理内存

常量内存:只读,有缓存,适合存储系数、查找表等。所有线程访问同一地址时很快。

纹理内存:专门为图像处理设计,支持二维/三维空间局部性访问。

内存类型 位置 访问速度 作用域 生命周期
寄存器 SM内部 1个周期 单个线程 线程内
共享内存 SM内部 ~5个周期 线程块内 线程块内
L1缓存 SM内部 ~10个周期 SM内 自动管理
L2缓存 芯片共享 ~100个周期 全局 自动管理
全局内存 显存 400-800个周期 全局 程序运行期间
常量内存 显存(有缓存) ~100个周期(缓存命中) 全局 程序运行期间

我曾经踩过的坑:有次做粒子模拟,每个粒子需要访问一个全局数组。我图省事,直接让每个线程独立访问。结果因为访问模式不连续,内存带宽利用率只有5%。后来改成共享内存做缓存,先让线程块内的线程协作加载连续数据,再各自计算。性能提升了15倍。

记住:全局内存访问,能合并就合并,能缓存就缓存。

3.5 总结与思考

好了,这一章的内容就到这里。我们来捋一捋:

  • SM是GPU的基本计算单元,里面有CUDA Core、调度器、缓存等
  • CUDA Core是执行算术运算的“工人”,不同类型核心性能差异巨大
  • Warp是调度单位,32个线程一起执行,要避免分支发散
  • 内存层次从快到慢:寄存器 → 共享内存 → L1/L2缓存 → 全局内存

我个人觉得,理解这些硬件细节,是写出高性能CUDA代码的必经之路。你不需要记住每个GPU的精确参数,但一定要明白:你的代码最终是在这些硬件上运行的,它的性能受限于硬件的特性。

下一章,我们会深入CUDA编程模型,看看如何用代码控制这些硬件资源。到时候,我会分享一些我实际项目中用过的优化技巧。

嗯,今天就到这里。有问题随时交流。