最近为了更加深入地理解gpu训练,开始学习CMU的15-418课程,并对pytorch的tensor batching计算机制有了一些新的理解,现记录在下,希望可以抛砖引玉。参考了经典教材《并行程序设计》和CMU的slides
Flynn的计算机分类方法
Flynn(1996)创造了一种通过计算机能同时处理的程序数和数据份数来分类各种计算机的方法。分别为:
-
单指令流单数据流计算机(single instruction stream- single data stream,
SISD):即单计算单元计算机,在一个时间点中只有一条指令在执行并处理同一份数据,暂且称为同一批(batch)数据 -
多指令流多数据流计算机(multiple instruction stream- multiple data stream, MIMD)
:多计算单元计算机,每个计算单元会各自拥有一份程序,并会对不同批的数据进行操作。 -
单指令流多数据流计算机(single instruction stream- multiple data stream, SIMD) :每个计算单元共享同一份程序,源于这份程序的指令被广播(broadcast)到多个计算单元。每个计算单元执行相同的指令,但使用不同批的数据。这种架构适用于处理重复的计算操作+超大规模数据的场景,一个例子就是我们当前关注的机器学习/深度学习场景,被广泛应用于现代GPU上,属于数据并行(Data Parallel)的并行设计结构。
-
多指令流单数据流计算机(multiple instruction stream- single data stream, MISD): 实际上并不存在,但是有些特殊的体系可以被看作这种架构,在此不赘述。
基于SIMD的GPU核心(core)的简单模型
基于SIMD的GPU核心的基本单元如下图,其中橙色为取指/译码器,黄色的ALU为算术逻辑单元,蓝灰色为执行程序必要的数据信息。
在执行时,取指器会将同一个指令广播到所有ALU上,并行地处理同一批数据中的不同部分。在这种情况下,均衡地划分数据十分重要。假设你划分了99%的数据给其中一个ALU,其余的1%平分给剩余的ALU,最终几乎相当于仅仅使用了一个ALU进行计算,并行计算的优势完全消失了。那么,该如何划分数据呢?这个至少在pytorch上不需要程序员担心。如果你是使用ISPC的C++程序员,那么你可以自己划分每一个ALU需要计算的数据量,也可以使用foreach语法交给编译器决定如何划分数据。
为什么要使用batching机制?
下图是一个简单的GPU框架与CPU框架的对比。请记住:GPU的设计初衷是为了重复的计算操作+超大数据量的场景。比如最常见的反向传播神经网络的矩阵计算。
看出来了吗?GPU的缓存区(cache)大小相比CPU来说非常小,而连接内存(memory)的总线带宽相较CPU来说更大,即GPU从内存中取数据的速度比CPU快很多,在图中大概快7倍左右。
需要补充的一点是,缓存机制是处理器加速数据访问的一个核心机制,将处理器经常使用的数据放入访问速度更快的缓存,就可以减少处理器从访问速度更慢的内存中取数据的次数,由此提高了计算速度。
但是,由于提高高速缓存的容量十分昂贵,所以一般来说,缓存器容量在数据量极大的情况下根本不够看,缓存缺失(cache miss)会非常频繁地发生,对内存的访问会十分地频繁,可以认为缓存的加速不怎么起作用。在这种情况下,计算速度的瓶颈就在于总线的带宽上。所以GPU舍弃缓存区大小,堆叠总线带宽的设计也非常合理了。
总结
现在我们就可以理解为什么在机器学习/深度学习中batching是一种非常常见的操作,因为只有单份的计算代码同时处理多份数据,基于SIMD的GPU的算术逻辑单元(ALU)才能被尽可能多地利用起来,才符合GPU的设计初衷——简单指令、超大数据量场景的并行化计算。同时,由于GPU的设计动机就是为了超大量的数据,故其基本假设就是:不管是使用再精妙的多级缓存结构还是更大的缓存单元,GPU核心也会频繁地发生cache miss,即需要频繁地从内存中更耗时地取数据,故其GPU上的缓存相对CPU较小。这就意味着如果我们单次计算使用的数据量不够大,由于cache miss的频繁发生,GPU的计算效率可能反倒不如相同配置的CPU。