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]。我们可以做一个操作把乘法和加法融合在一起。

这种情况下内存的录用率非常小。内存搬运占了绝大部分时间。许多编译器会自动优化展开为循环。
现代 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 单元:做
double、float、AVX/FMA 运算- Load 单元:从 L1/L2 Cache 读取数据
- Store 单元:把结果写回 Cache
- Branch 单元:处理跳转和分支
- 还有多个调度、解码、寄存器等部件
概念上CPU要进行
- 计算 x[i] 的地址
- load x[i]
- 计算 y[i] 的地址
- load y[i]
- alpha * x[i]
- 加上 y[i]
- 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的二三十倍

Thread Required:你的 kernel / workload 需要多少线程。
Thread Available:当前 GPU 硬件在对应层级上 最多能提供或同时容纳多少线程。
GPU所有资源被设计为增加线程而不是减少延迟。
| SIMD | SIMT | |
|---|---|---|
| 编程视角 | 一个线程操作一个向量 | 很多个独立线程 |
| 硬件执行 | 多个 vector lane | 多个 thread 通常组成 warp/wavefront |
| 每个元素/线程状态 | lane 状态较弱 | 每个 thread 有独立寄存器和执行状态 |
| 分支 | 通常需要 mask | 支持 thread 分支,但可能发生 divergence |
| 常见平台 | CPU AVX/NEON | GPU CUDA / HIP |
因为我们要尽可能减少内存的时延和搬运,因此GPU会有自己的HBM memory,也就是显存。GPU也有自己的寄存器。L1缓存,L2缓存。
GPU 里的 SM 指的是 Streaming Multiprocessor(流式多处理器)。你可以把它理解成 NVIDIA GPU 中真正执行计算任务的“核心计算单元”。
一个 GPU 不是简单地由几千个独立的 CUDA Core 拼起来,而是由多个 SM 组成,每个 SM 里面又包含很多计算资源
如果我们要把内存的数据搬运到GPU中速度就太慢了。 因为数据要通过PCIE传输。


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

GPU线程是超配的,超过大部分程序的使用需求。
AI训练
对于卷积操作来说,卷积操作可以转换为举证相乘,对kernel和input进行重新排列。将输入数据按照卷积窗进行展开,并存储在卷积的列中,对哦股份输入通道的对应的窗展开之后将拼接成最终输出matrix的一列。因此AI运算的本质就是矩阵乘法。
在AI计算中,不是所有计算都是线程独立的。

卷积中元素之间是有交换的。快速傅里叶变化就涉及到All to all。
GPU的线程分层执行,可以理解为,不把成千上万个线程当做完全独立的个体来管理,而是把线程按照多个层级组织起来,再以较小的线程组为单位实际执行。
以英伟达的cuda为例
Grid(网格) → Block(线程块) → Warp(线程束) → Thread(线程)
一般来说小于256的矩阵大小可以直接给CPU算。256-512属于交接区,看情况而定。512以上GPU开始出现优势。
GPU更适合AI计算,不只是因为算力,还有算力和带宽的匹配度。算的再快,内存来不及搬运也是没意义的。

- 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的并发执行。更好的提升了运算的吞吐量。

这里的橙色的就是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是线程结构的第一层次。

我们可以理解为俩套层级一套是编程执行层级,一套是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的算力和计算核心个数,核心破率,核心单时钟周期能力三个因素相关。三个数相乘就是峰值浮点算力。
