GPU的相比CPU有几个特点
-
运算资源非常丰富
-
控制部件占的面积比较小
-
内存带宽大,目前独显都采用 GDDR5 显存,位宽也高,主流独显内存带宽是CPU的十倍(200GB/s 对比 20GB)
-
内存延迟高,对比 CPU 使用多级缓存掩盖延迟,GPU 采用多线程掩盖延迟
-
寄存器资源极为丰富,32bit 寄存器有 64k 个,单线程可用 255 个
所以,GPU 只适合处理分支少,数据量大,延迟不敏感的任务。
先看一个GTX 1080 (Compute capability 6.1) 的 SM(stream multiprocessor) 结构
可以看到,一个 SM 中包含4个 Warp,每个 Warp 含有 32 个 CUDA Core【1】。那么,是不是一个Warp 就相当于 CPU 的 32 核呢?
一、 GPU 不适合处理大量分支
我们上面说了,GPU 控制部件面积比较小,为了节约控制器,32 个 CUDA Core 必须时刻执行同样的指令。也就是说,一个 Warp 内部的所有 CUDA Core 的 PC(程序计数器)一直是同步的【2】,但是访存地址是可以不同的,每个核心还可以有自己独立的寄存器组,这种执行方式叫做 SIMT(Single Instruction Multi Trhead)。
这是,你可能会问,如果这一个 Warp 中永远都在执行相同的指令,如果分支了怎么处理呢?
问的好,其实 Warp 中的 CUDA Core 并不是真的永远都执行相同的指令,它还可以不执行啊
这样会导致 Warp Divergence(见上图)。如果极端情况下,每一个Core的指令流都不一样,那么甚至还可能导致一个 Warp 中仅有一个 Core 在工作,效率降低为 1/32.
二、GPU 需要数据高度对齐
别看 GPU 一个 Warp 核心这么多,带宽看起来这么大,但是实际上一个一个 Warp 的内存访问是成组的,一次只能读取连续的且对齐的 128byte。【3】(这正好是WarpSize 32 * 4 byte)
上图这种操作的效率是最高的。如果访问完全分散,那么效率可能会又变成1/32.如下图。
而且 NVIDIA GPU 的缓存策略和 CPU 也不同,没有时间局部性
DIFFERENCE BETWEEN CPU L1 CACHE AND GPU L1 CACHE
The CPU L1 cache is optimized for both spatial and temporal locality. The GPU L1 cache is designed for spatial but not temporal locality. Frequent access to a cached L1 memory location does not increase the probability that the data will stay in cache.
-- 《Professional CUDA Programming》
你可能又会问,CPU 的 Cache line 不也有 64bytes嘛,也就比 GPU 少一半啊,这有什么差别吗?当然有,CPU 是一个核心一个 L1,GPU 是两个 Warp 一个 L1 Cache【4】。整个Warp 有一个核心数据没准备好都执行不了。
当然,这么苛刻的访存条件,如果真的做 C = A+ B 还是没什么问题的,现实中访存不会真的这么对齐,所以NVIDIA也下了很多功夫,准备了 Cache 和 Shared Memory, Constant Cache 等部件,力求让程序员能高效访问内存。
三、GPU 访存延迟大
说起来访存延迟和上一节的对齐还是有不少关系,这里分开讲。
你可能还注意到,一个 SM(CC6.1) 最多可同时启动 1024 个线程,但是一个 SM 中仅有 4个 Warp 共计 4 * 32 = 128 个 CUDA Core。显然一个SM可以启动的线程数比 CUDA Core 的数量大好多。这是为什么呢。
我们看下典型的 GPU 访存延迟(《Professional CUDA Programming》数据可能有点老)
10-20 cycles for arithmetic operations
400-800 cycles for global memory accesses
访存一次能做40个运算啦!但是GPU的显存带宽实际上是非常高的。怎么能让CudaCore 尽量满载呢?这时 SIMT 就上场了。
没关系,这个 Warp (这里指32个线程,之前文中混淆了调度单位和硬件单位)在等数据准备好,我们可以执行另外一组32个线程嘛,这样虽然延迟还是很大,但是 CUDA Core 和 Memory 都能够充分利用。
GPU 的线程切换不同于 CPU,在 CPU 上切换线程需要保存现场,将所有寄存器都存到主存中,而我们最开始说了,一个 SM 中有高达 64k 个 (注意不是64kbytes,有些中文书写错了)4 bytes 寄存器。而每个 Thread 最高使用的寄存器数量为255。少年你发现什么了吗?
256 * 4 * 32 = 32k。也就是说我每个线程把寄存器用到爆,也才用了一半的寄存器,那多出来的这些寄存器是干啥的?
其实,GPU 的线程切换只是切换了寄存器组,延迟超级低,几乎没有成本。考虑到通常线程并不会使用高达255个寄存器,实际上一个 CUDA Core 可以随时在八个线程之间反复横跳,那个线程数据准备好了就执行哪个【5】。这是 GPU 优于 CPU 的地方,也是为了掩盖延迟没办法的事情。
总而言之,GPU 访存还是需要对齐,而且延迟还是很大,但是最大吞吐量(在场景合适的情况下,一个比较长的单位时间,处理的数据量)是远高于 CPU 的。
【注1】LD/SD 是存取部件,用来访问显存,SFU 为超越函数单元
【注2】 Volta 架构重大更新,目前允许每个线程有单独PC
【注3】经过 L1 Cache 的数据读取是以 128 byte 为单元,还可以配置为不经过缓存,单元大小为32byte,写入操作单元大小可以为为 32,64,128 bytes,本条说的都是 Global Memory access。
【注4】NVIDIA GPU 的 Cache 最近几代架构变化明显,具体架构请具体分析
【注5】实际上线程切换是以 Warp 为单位。