英伟达架构师Stephen Jones关于GPU工作原理的演讲:
1.设计gpu的时候,我花了很多时间思考,编程语言的需求是什么以及我们如何支持这些编程语言。
2.硬件对编程方式的限制:物理定律和硬件本质在很大程度上决定了我们对这些机器的编程方式。
3.why gpu computing works 为什么gpu计算是可行的。如果你理解发生了什么,你才能更好地使用它。
4.最后,我才意识到,实际上标题应该是 where's my data 我的数据在哪里。
5.因为最终我们会发现这才是最重要的,当你对计算有更多的了解,你会看到它真正的问题是“我的数据在哪里”
6.nobody cares about flops(每秒钟浮点运算次数)
flops跟机器的算力有关,很多人买机器的时候会关心这个问题,我要告诉你们真的不应该关心,因为这不是最重要的问题。
7.cpu大约每秒能进行2万亿次的双精度(FP64)运算,内存每秒传输200g字节,也就是每秒25g的fp64数值,因为每个fp64是8个字节。
内存可以提供25亿个fp64值,挺多的,但是cpu每秒能够处理2万亿个fp64数据。这两者的比值就是设备的计算强度。
So for every number I load from memory ,I need to do 80 operations on it to break even
每秒内存需要同时传输80次数据给cpu,才可以让cpu满载。
8.
9.需要等待的加载时间,抵消了flops的能力
10.
流水线 pipeline
现在的问题是与计算延迟相比,内存延迟非常大。
11.物理实际上是相当复杂的,但根据经验,一个时钟周期内,电流的移动只有20毫米。
想象一下芯片DIE的尺寸,只是一个时钟周期内,让电流从一端到达另一端,什么都不做,仅仅是直线上以最快的速度移动,处理器就有5--7个时钟周期的延迟。
这是令人震惊的,电的速度正在与电脑的计算速度赛跑。
因为物理上的原因,当我必须从内存中提取数据时,内存需要用到5--10个时钟周期才能返回。
电路的工作方式就是把信号从一组晶体管传递到另一组晶体管。
当你在设备里进行逻辑运算时,设备会调整它的时钟频率,电流的速度必须要和时钟频率一样快才能弥补速度上的差距。
但这并不是最大的因素,晶体管流水线的深度才是最大的因素。
所以我花的很多时间等待数据
这意味着什么,我们可以计算一下看看花费了多少代价。
我花了很多钱买cpu,我的cpu有很强的浮点运算能力,我希望我的内存一直全速运行。
所以即使我有一个很高带宽的内存,但是对于计算强度几乎没有任何作用,而我在cpu和高性能内存上都花了很多钱。
12.让我们来看看其余这些处理器的延迟和内存效率
现在你知道我为什么一点都不关心flops了吧?因为我都不能让带宽忙起来,更别提flops了。
有趣的是,gpu的表现远比其他的要差,下面我们就来介绍gpu
13.
它可以在指令流水线中缓存指令,但是它必须追踪每一个请求。
注意,虽然我仍然在用一个线程,但是循环展开之后,及时我有729个迭代请求也没问题了。
我的处理器可以在一个线程内处理729个负载,所以loop unrolling 完成了729个迭代的计算问题。
很好,它让我有了更多的流水线操作,但它也显然受到了机器架构的其他各个部分的限制。
14.
并行性是指在硬件的限制下,每个线程同时执行一个操作,但是硬件可以处理许多线程。
所以在现实中,我可以通过loop unrolling 多线程操作来提高硬件的使用效率。
15.让我们来看看,在硬件的限制下,我们可以运行多少线程
但这就是它真正的不同之处,gpu有更高的延迟和更多的线程。
gpu的可用线程数是其他处理器的100倍以上。
所以gpu实际上做得更好,gpu线程比为5.6倍。
而cpu的线程比为1.2左右,这是gpu最重要的设计要点。
gpu拥有比你需要的多得多的线程,因为它是为大量任务而设计的。
它是为多线程设计的。
gpu的设计师将所有的资源投入到添加更多线程中,而不是减少延迟。
16.cpu是一台延迟机,cpu的期望是一个线程基本完成所有的工作。
将这些线程从一个切换到另一个是非常昂贵的,就像上下文切换一样。
gpu是一台吞吐机。
所以你只需要足够的线程就可以解决延迟问题,而cpu设计者把所有的资源都投入到减少延迟上了。不是增加线程,而是使用完全相反的方法来解决延迟问题。
这就是gpu和cpu运作方式的根本区别。
17.所有的程序都是和内存相关的,内存带宽、内存延迟以及数据在内存中的位置。
18.但gpu采用了一种不同的方法,这就是我要告诉你们的,gpu是如何让你们的程序工作的。
gpu在每个线程中使用大量的寄存器,寄存器能够以很低的延迟来保存活动数据,因为不同类型的缓存延迟差距很大。
寄存器需要更靠近内存,并且需要足够大的内存空间来完成你的flops请求。
当你需要加载操作时,你会用到指针。而硬件需要一个地方来放置指针。所以当我从内存中加载数据并放到寄存器中,我就可以计算它了。
我能做的内存操作与寄存器的数量直接相关。
A100 GPU原则上可以维持27兆的高速数据,这是A100中总寄存器的数量,如果为我们想使用双精度计算,则它可以存放330万个双精度数据。
对于更低精度的数据,它能放的更多,这是非常不同于cpu的,gpu使用寄存器缓存数据来解决高延迟问题。
以及通过靠近数据来减少延迟。
大量的数据也是gpu运行的基础。
19.让我们来看看带宽和延迟时间
我们的主内存是具有高带宽的HBM内存,将主内存作为带宽的基本单位,1倍,看看速度会有多快。
L2缓存的带宽是它的3倍。
同时L1缓存也是物理上离计算单元最近的存储。
如果它的延迟是1倍,那么L2缓存是5倍。
20.将带宽和延迟进行比较,你将理解为什么需要将数据放在gpu上,因为通过PCle总线移动数据,是迄今为止最大的瓶颈。
HBM 计算强度为100,L2缓存的计算强度则要好得多,每个数据只需要39个操作。而L1缓存,只需要8个操作,8个是很容易实现的。
这就是为什么L1缓存、L2缓存和寄存器为什么如此有用。
我可以把数据放在L1缓存,然后对数据做8个操作,使我的Flops饱和。
所以如果可以的话,我真的很想把缓存用完,我真的不想用PCIe。
PCIe带宽太糟糕了,延迟也太可怕了。
我需要做难以想象数量的操作,NVLink我也放这里了,在这里它没有作用是因为它是gpu和gpu之间的链接。
但我认为NVLink比PCIe更接近主内存领域。
21.相当多的线程解决了延迟问题,这是一个有趣的事情,你需要大量的线程。
延迟降低了,但带宽在增加,你需要为主内存准备几乎相同数量的线程。
我们希望能够让整个内存系统一直处于繁忙状态。
因为计算强度很高,所以如果这个内存系统中有一个比其他需要更多的线程,我就会发现那是瓶颈。
我必须添加更多线程去满足那部分,然后我的内存系统的其他部分便会拥有更多线程,这是一种精心设计的平衡。
22.
SM是一个基础处理单元,它里面有很多东西,但实际上,要记住的是warp,它由32个线程组成一组,warp就是gpu的基本调度单位。
在一个时钟周期内,我可以运行多个warp
一个SM包含64个warps,4个warps可以并行运行。
gpu的开发伴随着这些概念和组件,这是gpu发展战略的一部分。
gpu设计者通过增加线程来解决延迟问题,而不是减少延迟。
23.
102个线程我一次就跑完了,这就是我说的gpu是超量配额的原因。
因此,当一些线程因为等待延迟关闭时,其他线程大概已经收到了它们的响应,准备运行了,这就是gpu工作的全部秘密,它可以在不同的warp之间切换,并且在一个时钟周期内完成,所以根本没有上下文开销。
gpu可以连续运行线程。
为什么不希望固定线程,因为gpu是一个吞吐机。
24.让我们来谈谈吞吐量和延迟
开车需要它的速度越快越好,虽然它并不能快速有效地帮助其他人,它只能载着少数人从一个地方到另一个地方。
另外一个方面,火车可以载很多人,会停很多地方,所以这条线路上的人都得到了帮助,而且沿途可以有很多火车。
关于延迟系统的另一件事是,当它们过载了就很可怕,一切就只剩下希望。
开车时,如果路上有太多车,那就哪里也去不了,而火车坐满了,你可以等下一列。
虽然你迟到了,但你不会迟到三个小时,因为总会有另一列火车来。
所以吞吐量系统会像火车系统一样让你在站台上等,因为如果火车到站了,而站台上没有人,火车就坐不满,白白浪费了资源。
gpu就像火车一样,需要保持忙碌,而cpu是一个延迟机,切换线程开销很大,所以希望一个线程尽可能快地运行。
25.
左边是总工作量,它通过网格分解成提供gpu所需的超量分配的块,然后我得到由少量本地线程组成的块,所有这些线程都在一个任务中一起工作。
每个区块都是独立运行的,最终,我的整个图像完成了。
但所有块都是在超量分配模式下独立调度的,如此才能两全其美。
我需要通过吞吐量让机器保持忙碌,但它也允许一定数量的线程相互交互,这就是gpu编程的本质。
就是把你的问题分解在这些块中,合作的线程将一起工作,但每一个区块都非常独立。
26.
所有这些线程都是通过超量分配:程序 网络模型 线程 所有的都在我的块中运行。这从根本上解决了延迟问题,现在回到带宽限制。
27.现在我有很多线程,根据前面那个表,我有5倍的线程,远远超过我需要的线程,问题是这么多的线程,我该怎么用呢?这归结到算法的复杂性了。
也就是说,如果我增加问题的规模,我可以增加很多的线程,但我还需要做什么操作呢?
例如,对于element-wise,每次我添加一个线程,我都加载一个新的数据元素,但我只做了一个操作,我添加了一个线程,加载数据的一部分,再做一个计算,我添加的线程实际上并不会带来什么变化,我所请求的flops增加了,但我的算法以及算法的强度是平滑的。
而像卷积,2d,3d,我现在的数据,当我增加方块时,它是可扩展的,这里并不存在算法复杂度。
再强调一次,在卷积中,即使再多的数据也无法与我的计算强度相抗衡。
每当我的线程数增加一倍时,我所需要的计算量就会大大增加,因为所有线程都开始交互了,不过我的算法的计算强度是可扩展的
27.
它实际上表明了计算强度。
大多数处理器都有自己的指令集,尤其是对于FMA运算
为什么它很重要,因为它是许多数学运算中最基本的操作
是一种数学算法,矩阵乘法是一个大而复杂的东西,但它是由大量的FMA堆积起来的。
这里有一个重用我加载内容的例子
重复了很多很多次,所以这个矩阵,每个绿点装载25次,我只处理了这一行并用它做了25次计算,这是严格的计算强度需求
如果我的矩阵是10×10,我会以100次操作的速度重复使用它,这是我想要的计算机强度,因此,随着矩阵的增长,我极大地提高了我的flops能力。
28.
29.总结
flops并不重要,因为计算强度的原因
然后我们知道带宽并不像延迟那么重要,因为延迟很长,为了修复延迟,我需要更多线程。
gpu的架构就是这样建立起来的,通过大量线程和超量分配来解决延迟问题。
所以gpu运行时,线程是一个层次结构,一大堆工作被分解成以吞吐量模式运行的块,块中的线程可以一起工作,并合并一些操作。
所以在解决了延迟之后,最后开始平衡计算和带宽
而在小型 计算密集型工作上获得更高效率的方法,实际上就是需要有层次的缓存。
我可以用线程消除延迟,也可以用局部性解决低带宽问题,然后我就能获取所有的flops能力,甚至从tensor core上。
30.因为我能够最大限度地提高系统 线程 内存 和计算机中所有组件的效率,这取决于数据从哪里开始。
计算强度可以通过增加线程来解决延迟问题。
感谢up主,感谢演讲者,侵权删。