“Two strong oxen or 1024 chickens?”
之前CPU系列的内容:
- CPU简介
- Cache
- SIMD
- Profiling
大家应该能感觉到,近些年来,CPU的发展速度远远跟不上GPU的发展速度,这里有很多因素,比如AMD的疲软,Intel主观上缺少动力,比如GPU更适合大计算量的应用,因此CPU没有太多必要提升计算能力。总之,一切都是由市场这个看不见的手来操纵。如下图,相比CPU,GPU计算能力更强,价格也更便宜。
GPU架构
我们和CPU做一个简单的类比:
- SM(shading multiprocessors)->CPU Cores
- Warps->hyperthreading
- 每个warp包含32个threads,相当于SIMD
- 每个warp内的线程执行相同的指令
- 每个SM中有多个register,可以在warps间共享
- Sharedmem->L1 Cache
- Global memory->内存
和CPU之间不同的是,GPU的内存是可编程的,而CPU的缓存是不可编程的;GPU的线程管理是不可编程的,而CPU的多线程管理(SIMD)是不可编程的。
这里有两点强调,第一,CPU通过Cache来解决latency,而GPU则通过并行来解决这个问题,比如原本有5s延迟,因为缓存优化,只有1s延迟,而GPU并没有缓存,所以还是有5s延迟,但因为有1000个线程同时并行,所以整体的latency就降低了。第二,就是GPU的thread scheduler,如下图,解释了为什么GPU中尽可能减少逻辑判断
GPGPU编程
目前,我所了解的主要有三种,Compute Shader,CUDA和OpenCL,这个是个人的优先级。在编程角度,思想上都大同小异。
首先是Global Size和LocalSize的理解,首先,我们需要计算的数据有可能是一维,二维甚至N维,对应global和local的维度。下图是一个二维数据(比如图片)对应的概念:
这样,我们便可以获取每一个thread对应的相对位置和全局位置,实现业务需求,如下,global(800,400),local(32,32),column line和getlocal id则是对应的索引。
其次,作为运算的参数和结果,我们尽可能减少内存和显存之间的转换,比如我们计算创建一张纹理(GPU),getBits(RAM),然后OpenGL渲染(GPU),在这种场景下,如果在GPGPU中的纹理能够直接对应OpenGL的Texture,则可以直接渲染,省去了FromDevice和ToDevice的操作,性能会有很大的提高。OpenCL和CUDA都支持绑定Texture对象,而Compute Shader自动支持。
整体来说,OpenCL需要自己做一个简单封装,方便调用,ComputeShader需要我们对OpenGL有不错的理解,CUDA可以通过VS自动创建,更为易用。下图是我使用三种框架做的一个Voronoi noise,github: https://github.com/pasu/opengllearner
GPGPU的应用
首先,大规模的计算,比如CNN神经网络或者挖矿,这类应用最适合GPU,没有太多技术难点,就是怕GPU闲着,堪称GPU的996。
其次,很多CPU时代的算法并不支持并行,比如排序,如何能够实现GPU版本的算法(Bitonic sort),需要我们设计新的轮子了。
比如下面这个Prefix sum的并行版本,原本存在的loop dependency O(N),在并行版本下为O(logN)的loop,而每一个loop内部则是完全的并行计算(蓝色箭头示意部分)。
最后,还有一些计算量大,逻辑也很复杂的应用。比如GPU Ray Tracing就是一个很大,也很诱人的领域,can you feel it?
回到开头问题,当我们锄地时,你会选择两头牛呢,还是1024只鸡,希望这篇文章能帮助你找到自己的答案~