GPU工作原理

原理

GPU(Graphics processing units)原是设计用于处理图像视频等数据。

GPU设计目标是最大化吞吐量,比单人舞执行快慢,更关心并行度(parallelism),也就是同时可以执行多少任务,CPU则更关心延迟和并发。

实际上CPU是会有prefetch来进行优化的。

void demo(double alpha, double *x, double *y)
{
    int n = 2000;
    for (int i = 0; i < n; ++i)
    {
        y[i] = alpha * x[i] + y[i];
    }
}

这里每次操作需要在内存中读取俩个数据,一个是x[i]和y[i]。我们可以做一个操作把乘法和加法融合在一起。

image.png

这种情况下内存的录用率非常小。内存搬运占了绝大部分时间。许多编译器会自动优化展开为循环。

现代 CPU 的 hardware prefetcher 通常会识别这种模式,然后提前把后面的 cache line,例如 x[i+8]y[i+8] 所在的数据,从更低层 cache,甚至内存,搬到更高层 cache。

另外还有一种是指令预取。CPU 前端确实也会提前把后面的机器指令从内存/L2/L1I 搬到 instruction cache,并且现代 CPU 还会进行 branch prediction、instruction fetch、decode、甚至 speculative execution。

例如一个现代核心内部可能有这些单元:

  • 整数 ALU:做整数加减、位运算、地址计算
  • 浮点/SIMD 单元:做 doublefloat、AVX/FMA 运算
  • Load 单元:从 L1/L2 Cache 读取数据
  • Store 单元:把结果写回 Cache
  • Branch 单元:处理跳转和分支
  • 还有多个调度、解码、寄存器等部件

概念上CPU要进行

  1. 计算 x[i] 的地址
  2. load x[i]
  3. 计算 y[i] 的地址
  4. load y[i]
  5. alpha * x[i]
  6. 加上 y[i]
  7. store 回 y[i]

实际上核心内部有多个执行单元,可以同时做

Load unit 1: 读取 x[i]
Load unit 2: 读取 y[i]

Address unit: 计算 x[i+1] 的地址

FP unit: 计算前一个元素的 alpha*x[i-1]

Store unit: 写回更早的 y[i-2]‘

也就是说在同一个核心在同一个时刻推荐多个不同的机器指令。、这就是所谓的超标量superscalar。

void fun_axy(int n, double alpha, double *x, double *y)
{
    for (int i = 0; i < n; i += 8)
    {
        y[i+0] = alpha * x[i+0] + y[i+0];
        y[i+1] = alpha * x[i+1] + y[i+1];
        y[i+2] = alpha * x[i+2] + y[i+2];
        y[i+3] = alpha * x[i+3] + y[i+3];
        y[i+4] = alpha * x[i+4] + y[i+4];
        y[i+5] = alpha * x[i+5] + y[i+5];
        y[i+6] = alpha * x[i+6] + y[i+6];
        y[i+7] = alpha * x[i+7] + y[i+7];
    }
}

Z=AX+Y通过并发进行循环展开。

编译器真实过程中很少对循环展开100次以上。一个线程一次执行上千条指令,一个线程很难直接处理。一个线程很难处理700个计算负载。

void fun_axy(int n, double alpha, double *x, double *y)
{
    Parallel for(int i = 0; i < n; i++)
    {
        y[i] = alpha * x[i] + y[i];
    }
}

我们让每个线程去计算AX+Y这么一个操作。这个时候程序收到线程数和内存请求bound。

GPU的时延会比CPU高很多。但是线程数是CPU的二三十倍

image.png

Thread Required:你的 kernel / workload 需要多少线程

Thread Available:当前 GPU 硬件在对应层级上 最多能提供或同时容纳多少线程

GPU所有资源被设计为增加线程而不是减少延迟。

SIMDSIMT
编程视角一个线程操作一个向量很多个独立线程
硬件执行多个 vector lane多个 thread 通常组成 warp/wavefront
每个元素/线程状态lane 状态较弱每个 thread 有独立寄存器和执行状态
分支通常需要 mask支持 thread 分支,但可能发生 divergence
常见平台CPU AVX/NEONGPU CUDA / HIP

因为我们要尽可能减少内存的时延和搬运,因此GPU会有自己的HBM memory,也就是显存。GPU也有自己的寄存器。L1缓存,L2缓存。

GPU 里的 SM 指的是 Streaming Multiprocessor(流式多处理器)。你可以把它理解成 NVIDIA GPU 中真正执行计算任务的“核心计算单元”。

一个 GPU 不是简单地由几千个独立的 CUDA Core 拼起来,而是由多个 SM 组成,每个 SM 里面又包含很多计算资源

如果我们要把内存的数据搬运到GPU中速度就太慢了。 因为数据要通过PCIE传输。

image.png

image.png

每一个SM有64个warp。每个SM有2048个线程。整个A100有20多万个线程。很多时候程序是用不完线程的。

image.png

GPU线程是超配的,超过大部分程序的使用需求。

AI训练

对于卷积操作来说,卷积操作可以转换为举证相乘,对kernel和input进行重新排列。将输入数据按照卷积窗进行展开,并存储在卷积的列中,对哦股份输入通道的对应的窗展开之后将拼接成最终输出matrix的一列。因此AI运算的本质就是矩阵乘法。

在AI计算中,不是所有计算都是线程独立的。

image.png

卷积中元素之间是有交换的。快速傅里叶变化就涉及到All to all。

GPU的线程分层执行,可以理解为,不把成千上万个线程当做完全独立的个体来管理,而是把线程按照多个层级组织起来,再以较小的线程组为单位实际执行。

以英伟达的cuda为例

Grid(网格) → Block(线程块) → Warp(线程束) → Thread(线程)

一般来说小于256的矩阵大小可以直接给CPU算。256-512属于交接区,看情况而定。512以上GPU开始出现优势。

GPU更适合AI计算,不只是因为算力,还有算力和带宽的匹配度。算的再快,内存来不及搬运也是没意义的。

image.png

  • GPC graphics processing cluster:图形处理簇,一个GPU中有多个GPC,最初是面向图形渲染设计的,不过做CUDA,AI计算,这些SM物理上依然在GPC上面。
  • TPC texture processing cluster:纹理处理簇,一个GPC有多个TCP,TPC 这个名字同样来自图形渲染时代,因为它和纹理处理、纹理采样等工作密切相关。现在是多个SM和相关纹理单元组织起来的一组硬件。
  • SM stream multi cluster:流式多处理器,一个 SM 更像一个拥有大量计算单元的小型并行处理器。SM里面有许多core
  • Core:通常是CUDA core,CUDA core不等于cpu核心。因为CPU核心中包含复杂控制逻辑,分支预测,乱序执行,大缓存。 CUDA core是高度并行的算数执行单元。

SM中包含了许多内容

SM
├── CUDA Cores
│ └── 主要做 FP32 / INT 等普通算术

├── Tensor Cores
│ └── 专门做矩阵乘加,AI / 深度学习特别重要

├── Warp Schedulers
│ └── 决定哪些 warp 发射执行

├── Load / Store Units
│ └── 负责内存读写

├── Special Function Units
│ └── sin、cos、sqrt 等特殊函数

├── Registers
│ └── 每个线程使用的高速寄存器

├── Shared Memory / L1 Cache
│ └── SM 内部高速存储

└── 其他控制和调度逻辑

SM早期的处理单元被叫做SP:stream processor,流处理单元。fermi架构后,SP被改称为CUDA core,通过CUDA来控制具体的执行指令。目前流处理器的数量就是CUDA core的数量。每个CUDA core都有浮点运算单元和整数运算单元。

到了 volta架构,CUDA core和fermi架构时期发生了变化。这里就没有以前的CUDA core了。变成了单独的FP32 FPU和INT32.现在每个SM支持FP32和INT32的并发执行。更好的提升了运算的吞吐量。

image.png

这里的橙色的就是warp线程束。逻辑上所有Thread并行,但是硬件角度上来说,并不是所有的Thread能够在同一时刻执行,这里就需要Warp的引入。

SIMT single instruction,multiple threads。单指令多线程。也就是一组线程同时执行一条指令,但每个线程处理自己的数据。

SIMD 是“一条指令直接操作一组数据”;SIMT 是“很多线程各自像独立线程一样写程序,但硬件把它们成组一起执行”。

Warp是SM基本执行单元,一个Warp包含32个并行Thread,这32个Thread执行属于SIMT模式,也就是说所有Thread以锁同步的的方式执行同一相同指令。

但每个Thread会使用各自的Data执行指令的分支。如果在Warp中没有32个Thread需要工作,但么Warp虽然还是最为一个整体运行,但是这部分Thread是出于非激活状态的。

CUDA

2006年11月,英伟达退出了CUDA,通用并行计算平台和编程模型,用于图形处理单元GPU上的通用计算。基于LLVM构建了CUDA编译器,方便开发者使用C进行开发。提供了对其他编程语言的支持。支持OpenCL和DirectCompute等应用程序接口。

CUDA执行流程中最重要的一个过程是调用CUDA的核函数来执行并行计算,kernel是CUDA中的一个重要概念。

在CUDA程序架构中,HOST代码部分在CPU上执行,是普通C代码;当遇到数据并行处理的部分,CUDA就会将程序编译成GPU能执行的程序,并传送到GPU,这个程序在CUDA里称作Kernel。

kernel在device上执行时,实际上是启动很多线程,一个kernel锁启动的所有线程成为一个网格。同一个网格上的线程共享相同的全局内存空间。grid是线程结构的第一层次。

image.png

我们可以理解为俩套层级一套是编程执行层级,一套是GPU物理硬件层级。

CUDA 执行层级大致对应的硬件关键点
Grid 网格整块 GPU一个 Grid 的 Blocks 会被分发到很多 SM
Block 线程块SM一个 Block 在某一时刻只驻留在一个 SM 上
Warp 线程束SM 内的 warp scheduler + 执行单元一个 Warp 通常是 32 个 Thread
Thread 线程CUDA Core / FP/INT/SFU 等执行通路不是 Thread = CUDA Core
TPC一组 SM,具体数量因架构而异
GPC更大的硬件分区,包含若干 TPC/SM 等

Block线程 块值在一个SM上通过Wrap进行调度。一旦在SM上调起了Block线程快,就会一直保留到执行完kernel。SM可以同时保存多个Block线程块,块间并行的执行。

GPU的算力和计算核心个数,核心破率,核心单时钟周期能力三个因素相关。三个数相乘就是峰值浮点算力。

image.png

Last modification:September 9, 2026
如果觉得我的文章对你有用,请随意赞赏