GPU架构基础:CUDA核心概念、SM与Warp调度、内存层次结构

各位同学,今天我们来聊聊GPU架构。说实话,我刚开始接触CUDA时,也被那一堆术语搞得头晕——SM是什么?Warp又是什么?Global Memory和Shared Memory到底差在哪?

别急,我们一个一个来拆解。搞懂了这些,你写算子的时候就能知道瓶颈在哪,优化方向在哪。

一、CUDA核心概念:从线程到网格

CUDA的编程模型,说白了就是「分层」的。我习惯把它想象成一个军队的编制:

  • Thread(线程):最基层的士兵,每个线程执行一条指令
  • Block(线程块):一个班,里面的线程可以互相通信(通过Shared Memory)
  • Grid(网格):一个连队,由多个Block组成

你写一个kernel函数,实际上是在描述「每个线程该干什么」。GPU会帮你把这些线程分配到硬件上执行。

关键点:CUDA的编程模型是逻辑上的,而硬件执行是物理上的。你写的Grid/Block只是告诉编译器「我有这么多工作要做」,至于怎么分给SM,那是硬件的事。

举个例子,你要做1000个元素的向量加法:

__global__ void vec_add(float *a, float *b, float *c, int n) {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    if (idx < n) {
        c[idx] = a[idx] + b[idx];
    }
}

// 启动配置:256个线程一个Block,一共 ceil(1000/256) = 4个Block
vec_add<<<4, 256>>>(a, b, c, 1000);

这里每个线程处理一个元素。嗯,简单粗暴,但很有效。

二、SM与Warp调度:GPU的「心脏」

SM(Streaming Multiprocessor)是GPU真正的计算单元。你可以把SM想象成一个「小型的多核CPU」,但它有自己的调度逻辑。

Warp是什么?

Warp是SM调度的基本单位,通常包含32个线程。为什么是32?这是NVIDIA的设计选择,跟指令宽度、寄存器文件有关。

我个人习惯把Warp理解成「一捆线程」——这32个线程执行同一条指令,只是操作不同的数据。这就是SIMT(单指令多线程)的精髓。

避坑指南:我曾经在写一个reduce算子时,没注意Warp内线程的分支发散。结果32个线程里有一半走if,一半走else,性能直接腰斩。记住:同一个Warp里的线程,尽量走同一个分支。

SM怎么调度Warp?

SM里有一个Warp Scheduler,它维护着一堆Warp的状态。每个时钟周期,Scheduler会选一个「就绪」的Warp发射指令。

为什么会这样?因为内存访问有延迟。当一个Warp在等数据时,Scheduler就切换到另一个Warp去执行。这叫「零开销上下文切换」——切换Warp不需要保存恢复寄存器,因为每个Warp有自己的寄存器文件。

你想想看,这跟CPU的线程切换完全不一样。CPU切换线程要保存一堆上下文,GPU不用。所以GPU可以同时维护很多Warp来隐藏延迟。

下面我用一张图来展示SM的内部结构:

SM (Streaming Multiprocessor) Warp Scheduler 每个时钟选一个就绪Warp 指令缓存 存放待发射指令 Warp Pool(就绪队列) Warp 0 Warp 1 Warp 2 Warp 3 Warp 4 Warp 5 ... 执行单元(CUDA Core / Tensor Core / SFU) INT32 FP32 FP64 Tensor SFU

这张图展示了SM内部的核心组件。Warp Scheduler从Warp Pool里选一个,然后从指令缓存取指令,发给执行单元去算。

三、内存层次结构:速度与容量的博弈

GPU的内存跟CPU一样,也有层次结构。但GPU的层次更「极端」——离计算单元越近越快,但容量越小。

内存类型 位置 大小 延迟 作用域
Register SM内部 每个SM约64K~256K个 ~1 cycle 单个线程
Shared Memory SM内部 每个SM 48KB~164KB ~5 cycles 单个Block
Global Memory 显存(DRAM) 几GB~几十GB ~400 cycles 所有线程

看到这个延迟差距了吗?Register只要1个时钟周期,Global Memory要400个。差了400倍!

Register(寄存器)

寄存器是每个线程私有的。你在kernel里定义的局部变量,只要不溢出,都会放在寄存器里。

注意:寄存器是有限的!每个SM的寄存器总数是固定的。如果你一个Block用了太多寄存器,SM能同时容纳的Block数就会减少,导致Occupancy下降。

我遇到过一个问题:一个kernel用了很多局部变量,结果编译器报了「spill to local memory」的警告。这意味着寄存器不够用,变量被挤到Global Memory里去了,性能直接崩了。

Shared Memory(共享内存)

Shared Memory是同一个Block内所有线程共享的。它的速度比Global Memory快得多,但需要手动管理。

说白了,Shared Memory就是「程序员可控的缓存」。你手动把数据从Global Memory搬到Shared Memory,然后让Block内的线程一起用。

举个例子,矩阵乘法中常用的tiling技术:

__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++) {
        // 协作加载数据到Shared Memory
        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();  // 同步:确保所有线程都加载完了
        
        // 从Shared Memory计算
        for (int k = 0; k < TILE_SIZE; k++) {
            sum += As[threadIdx.y][k] * Bs[k][threadIdx.x];
        }
        __syncthreads();
    }
    C[row * N + col] = sum;
}

这里的关键是__syncthreads()——它保证所有线程都完成加载后,才开始计算。不然有的线程还在等数据,有的已经开始算了,结果就错了。

避坑指南:我曾经在Shared Memory里写了一个bank conflict的代码,性能比不用Shared Memory还差。Shared Memory被分成32个bank,如果多个线程同时访问同一个bank的不同地址,就会冲突。解决办法是给数组加padding,或者调整访问模式。

Global Memory(全局内存)

Global Memory就是显存,容量大但延迟高。访问Global Memory时,要尽量做到「合并访问」——相邻的线程访问相邻的地址。

为什么?因为Global Memory的访问是以128字节为单位的。如果32个线程访问连续的128字节,一次就能搞定。如果访问分散的地址,就要多次访问,带宽就浪费了。

我刚开始写CUDA时,没注意这个,写了个随机访问的代码,结果带宽利用率只有10%。后来改成连续访问,性能直接提升了8倍。

四、总结:一张图看懂GPU架构

最后,我用一张图总结整个GPU架构的层次关系:

GPU架构层次结构 Grid(网格) Block 0 Block 1 Block 2 SM 0 SM 1 SM 2 每个SM包含:Warp Scheduler + CUDA Core + Shared Memory + Register File Warp(32个线程) Warp 0 Warp 1 Thread(线程) Thread 0 Thread 1 Thread 2 ...

从Grid到Thread,每一层都有对应的硬件映射。搞懂了这张图,你就知道你的代码在GPU上是怎么跑的。

嗯,今天就先聊到这。记住:写算子时,多想想数据在哪、怎么访问、线程怎么协作。这些基础打牢了,后面的优化技巧才能用得上。

核心要点回顾:

  • CUDA编程模型:Grid → Block → Thread,逻辑分层
  • SM是物理执行单元,Warp是调度单位(32线程一组)
  • 内存速度:Register > Shared Memory > Global Memory
  • 优化方向:减少Global访问、合并访问、避免Warp发散、控制寄存器使用

想要更多资料,联系可可