Contents

显卡相关

原文which gpu for deep learning by Tim Dettmers - 2023/01/30

对此摘抄了 显卡工作原理 与 显卡关键因素


算力产品 AutoDL

许多人说GPU快是因为 matrix multiplication 矩阵乘法 convolution 卷积 高效,Tim Dettmers认为是memory bandwidth 内存带宽.


CPU是延迟优化的,而GPU是带宽优化的.类比CPU是法拉利,GPU是大卡车, 在从 随机 A点 运货到 随机 B点 过程中,

CPU可以快速获取 RAM Random Access Memory 中的一些memory,而GPU则更慢(latency 延迟 要高得多), 但是CPU需要多次来回才能完成其工作,而GPU可以一次获取更多memory 内存.

CPU擅长快速获取少量内存(5 3 7), 而GPU擅长获取大量内存(矩阵乘法:(A*B)*C).

最好的CPU有大约 50GB/s,而最好的GPU有 750GB/s 的内存带宽.

如果计算操作需要的内存越多,GPU相对于CPU的优势就越明显.但是仍然存在可能会损害GPU性能的延迟.

一辆大卡车每次旅行可能都能拿起很多包裹,但问题是你要等很长时间,直到下一组包裹到达. 如果不解决这个问题,即使对于大量数据,GPU 也会非常慢.那么这个问题是如何解决的呢?


如果你要求一辆大卡车进行多次旅行来取包裹,一旦卡车离开进行下一次旅行,你总是会等待很长时间才能收到下一批包裹——卡车只是很慢.

但是,如果您现在使用由法拉利和大型卡车组成的车队(线程并行),并且你有很多包裹(大块内存如 matrix 矩阵)的工作,那么你将等待第一辆卡车,

但在那之后你将根本没有等待时间 — 卸载包裹需要花费大量时间,以至于所有卡车都将在卸载位置B排队,以便你始终可以直接访问你的包裹(memory). 这有效地隐藏了延迟,以便GPU提供高带宽,同时在线程并行性下隐藏其延迟 — 因此对于大块内存,GPU提供最佳的内存带宽, 同时几乎没有由于线程并行延迟而导致的缺点.

这是GPU在深度学习方面比CPU更快的第二个原因.作为旁注,你还将了解为什么更多线程对CPU没有意义:法拉利车队在任何情况下都没有真正的好处.


但GPU的优势并不止于此. 这是将内存从main memory (RAM)获取到芯片上的local memory (L1 cache 和 registers 寄存器)的第一步.

第二步对性能不太重要,但仍增加了GPU的领先优势. 所有执行的计算都发生在直接连接到执行单元(CPU内核,GPU stream processor 流处理器)的registers中.

通常,fast L1和register memory非常靠近执行引擎,并且你希望保持这些内存较小,以便快速访问. 与执行引擎的距离增加会大大降低内存访问速度,因此访问它的距离越大,它的速度就越慢.

如果你让你的内存越来越大,那么,反过来, 它访问它的内存会变慢(平均而言,在小商店里找到你想买的东西比在大商店里找到你想买的东西要快,即使你知道那件东西在哪里).

因此,register files的大小是有限的 - 我们在这里只是处于物理学的极限,每一纳米都很重要,我们希望保持它们很小.


GPU 的优势在于它有 a small pack of registers 可以提供给每个处理单元(流处理器或 SM Streaming Multiprocessor),且很多.

因此,我们总共可以拥有大量的register memory,特点是非常小,因此非常快. 这导致聚合GPU registers 大小比CPU大 30倍 以上,并且速度仍然是其两倍,这意味着高达 14MB 的 registers memory 以惊人的 80TB/s 的速度运行.

相比之下,CPU L1 cache 仅以大约 5TB/s 的速度运行,这非常慢,大小约为 1MB;CPU registers的大小通常约为 64-128KB,运行速度为 10-20TB/s. 当然,这种数字比较有点缺陷,因为registers的操作与 GPU registers略有不同(有点像苹果和橙子),但这里的大小差异比速度的差异更重要,而且确实有所不同.


作为旁注,GPU中的完全register利用率起初似乎很难实现,因为它是最小的计算单元,需要手动微调以获得良好的性能。

但是,NVIDIA 开发了有用的编译器工具,可以指示每个流处理器何时使用过多或过少的registers。

调整 GPU 代码以利用适量的registers和 L1 cache 以实现快速性能很容易。

这使得GPU比其他架构(如Xeon Phis)更具优势, 在Xeon Phis中,这种利用很难实现并且调试起来很痛苦,最终使得很难在Xeon Phi上最大化性能.


最终的意义是,你可以将大量数据存储在GPU的L1 caches 和 register files中,以便重复使用卷积和矩阵乘法的tiles.

例如,最优的矩阵乘法算法使用 64x32 到 96x64 大小的2个矩阵tiles存储在L1 cache中,

以及一个 16x16 到 32x32 大小的数字 register tile 来存储每个thread 线程 block的输出总和

(1个thread block = 最多1024个threads;每个流处理器有8个thread blocks,在整个GPU中总共有60个流处理器)。

如果你有一个100MB 的矩阵,你可以将它分割成的较小矩阵去适应你的 cache 和 registers, 然后以每秒10-80TB 的速度进行三个矩阵tiles的矩阵乘法运算,速度非常快! 这是GPU比CPU快得多的第三个原因,也是它们非常适用于深度学习的原因。


请记住,较慢的内存始终是性能瓶颈的主要因素。 如果 95% 的内存移动发生在registers 中 (80TB/s),且5% 发生在main memory 中 (0.75TB/s), 那么你仍然将大部分时间花在main memory的内存访问上(大约是六倍).


因此,按重要性排序:

  • 高带宽main memory
  • 在线程并行性下隐藏内存访问延迟
  • 大而快速的register和易于编程的L1 memory是使GPU非常适合深度学习的组件。

Tensor Cores是执行非常高效矩阵乘法的微小核心. 由于任何深度神经网络中最昂贵的部分是矩阵乘法,因此Tensor Cores非常有用. 总之,它们是如此强大,以至于我不推荐任何没有Tensor Cores的 GPU.


重要性解释

举例: A*B=C 矩阵乘法

设定: 所有矩阵的大小为 32×32


要完全理解这个例子,你必须理解周期的概念。

如果处理器以 1GHz 运行,它每秒可以执行 10^9 个周期。

每个周期都代表一个计算的机会。但是,大多数情况下,操作需要的时间超过一个周期。

因此,我们本质上有一个队列,下一个操作需要等待下一个操作完成。这也称为操作的延迟。


下面是操作的一些重要延迟周期计时。这些时间可能会因每代GPU而异。这些数字适用于缓存相对较慢的Ampere GPU。

  • Global memory access (up to 80GB): ~380 cycles 周期
  • L2 cache: ~200 cycles
  • L1 cache or Shared memory access (up to 128 kb per Streaming Multiprocessor): ~34 cycles
  • Fused multiplication and addition, a*b+c (FFMA): 4 cycles
  • Tensor Core matrix multiply: 1 cycle

每个操作总是由a pack of 32 threads 执行.这个 pack 被称为 a warp of threads. Warps通常以同步模式操作,即warp内的线程必须等待彼此。

GPU上的所有内存操作都经过warps优化。

例如,从global memory加载数据以32*4bytes的粒度进行,恰好是32 floats, 即每个warp中的每个线程恰好一个float。 在一个流处理器(SM)中,可以拥有最多32 warps,即1024个线程,这相当于与一个CPU核心等价的GPU部件。

一个SM的资源被分配给所有活动的warps。这意味着有时我们希望运行较少的warps, 以便每个warp拥有更多的 registers/shared memory/Tensor Core.


对于以下两个示例,我们假设具有相同的计算资源。 对于这个 32×32 矩阵乘法的小例子,我们使用 8 个 SM(大约是 RTX 3090 型号显卡 的 10%)和每个 SM 8 warps。

为了了解周期延迟如何与每个 SM 的线程数和每个 SM 的共享内存等资源一起发挥作用,我们现在看一下矩阵乘法的示例。

虽然以下示例大致遵循了有和没有Tensor Core的矩阵乘法计算步骤的顺序,但请注意,这些是非常简化的示例。

矩阵乘法的真实案例涉及更大的共享内存tiles和略有不同的计算模式。

如果我们想做一个 A*B=C 矩阵乘法,其中每个矩阵的大小为 32×32, 那么我们希望将我们重复访问的内存加载到共享内存中,因为它的延迟大约低五倍(200 个周期 vs 34 个周期)。 共享内存中的memory block 通常称为 a memory tile 或者 a tile。

通过使用 2*32 warps,可以将两个 32×32 floats 加载到共享内存tile 中并行发生。 我们有 8 个 SM,每个 SM 有 8 warps,因此由于并行化,我们只需要从全局内存到共享内存进行一次顺序加载,这需要 200 个周期。


要进行矩阵乘法,我们现在需要从共享内存 A 和共享内存 B 加载 32 个数字的vector,并执行a fused multiply-and-accumulate (FFMA).

然后将输出存储在 registers C中。我们划分工作,以便每个SM执行8倍点积(32×32)来计算8个C输出。 为什么这正好是 8(旧算法中的 4)是非常技术性的。 我推荐Scott Gray关于矩阵乘法的博客文章来理解这一点。 这意味着我们有 8 次共享内存访问,每次访问 34 个周期和 8 个 FFMA 操作(32 个并行),每个操作花费 4 个周期。 因此,我们总共有以下成本:

200 cycles (global memory) + 834 cycles (shared memory) + 84 cycles (FFMA) = 504 cycles

使用Tensor Core,我们可以在一个周期内执行 4×4 矩阵乘法。 为此,我们首先需要将内存放入Tensor Core。与上述类似,我们需要从全局内存(200 个周期)中读取并存储在共享内存中。 要进行 32×32 矩阵乘法,我们需要执行 8×8=64 Tensor Core运算。单个 SM 有 8 个Tensor Core。 因此,有了 8 SMs,我们就有了 64 个Tensor Core——正是我们需要的数量! 我们可以通过 1 次内存传输(34 个周期)将数据从共享内存传输到Tensor Core,然后执行这 64 个并行Tensor Core操作(1 个周期)。 这意味着Tensor Core矩阵乘法的总成本,在这种情况下,为:

200 cycles (global memory) + 34 cycles (shared memory) + 1 cycle (Tensor Core) = 235 cycles.

因此,我们通过Tensor Core将矩阵乘法成本从 504 个周期显着降低到 235 个周期。在这种简化的情况下,Tensor Core降低了共享内存访问和 FFMA 操作的成本。

此示例经过简化,例如,通常每个线程都需要计算在将数据从全局内存传输到共享内存时要读取和写入的内存。 通过新的Hooper (H100)架构,我们还拥有Tensor内存加速器(TMA)在硬件中计算这些索引,从而帮助每个线程专注于更多的计算而不是计算索引。

RTX 30 Ampere 和 RTX 40 Ada 系列 GPU 还支持在全局内存和共享内存之间执行异步传输。 H100 Hopper GPU 通过引入Tensor Memory Accelerator(TMA) 单元进一步扩展了这一点。 TMA 单元结合了异步副本和索引计算,可同时进行读取和写入,因此每个线程不再需要计算下一个要读取的元素,每个线程可以专注于执行更多的矩阵乘法计算。 这看起来如下。

TMA 单元将内存从全局内存获取到共享内存(200 个周期) 。数据到达后,TMA 单元从全局内存异步获取下一个数据块。发生这种情况时,线程从共享内存加载数据,并通过Tensor Core执行矩阵乘法。 线程完成后,它们等待 TMA 单元完成下一次数据传输,然后重复序列。

因此,由于异步性质,TMA 单元读取的第二个全局内存已经在线程处理当前共享内存tile时进行。 这意味着,第二次读取只需要 200 – 34 – 1 = 165 个周期。

由于我们执行许多读取,因此只有第一次内存访问会很慢,所有其他内存访问将与 TMA 单元部分重叠。 因此,平均而言,我们将时间减少了 35 个周期。

165 个周期(等待异步复制完成)+ 34 个周期(共享内存)+ 1 个周期(Tensor Core)= 200 个周期。

这使矩阵乘法又加速了 15%。

从这些例子中,可以清楚地看出为什么下一个属性,内存带宽,对于配备Tensor Core的GPU如此重要。 由于全局内存是迄今为止使用Tensor Core进行矩阵乘法的最大循环成本,因此如果可以减少全局内存延迟,我们甚至会拥有更快的 GPU。 我们可以通过增加存储器的时钟频率(每秒更多的周期,但也有更多的热量和更高的能量需求) 或通过增加可以在任何时间传输的元素数量(总线宽度)来做到这一点。

从上一节中,我们已经看到Tensor Core非常快。 事实上,速度如此之快,以至于它们在等待全局内存中的内存到达时大部分时间都处于空闲状态。 例如,在 GPT-3 大小的训练期间,它使用巨大的矩阵——越大,对Tensor Core越好——我们的Tensor Core TFLOPS 利用率约为 45-65%, 这意味着即使对于大型神经网络大约 50% 的时间,Tensor Core也是空闲的。

这意味着,当将两个 GPU 与Tensor Core进行比较时,每个 GPU 性能的最佳指标之一是它们的内存带宽。 例如,A100 GPU 的内存带宽为 1,555 GB/s,而 V100 的内存带宽为 900 GB/s。 因此,A100 与 V100 加速的基本估计值为 1555/900 = 1.73 倍。

由于内存传输到Tensor Core是性能的限制因素,因此我们正在寻找其他 GPU 属性,以便更快地将内存传输到Tensor Core。 L2 Cache、Shared Memory、L1 Cache和使用的Registers数量都是相关的。 要了解内存层次结构如何实现更快的内存传输,它有助于了解如何在 GPU 上执行矩阵乘法。

为了执行矩阵乘法,我们利用 GPU 的内存层次结构,从慢速全局内存到更快的 L2 Cache, 再到快速的本地Shared Memory,再到闪电般的Registers。 但是,内存越快,它就越小。

虽然从逻辑上讲,L2 和 L1 内存是相同的,但 L2 Cache更大,因此检索缓存行需要遍历的平均物理距离更大。 你可以将 L1 和 L2 Cache视为要在其中检索物料的有组织的仓库。 你知道物品在哪里,但对于较大的仓库来说,去那里平均需要更长的时间。 这是 L1 和 L2 Cache之间的本质区别。大=慢,小=快。

对于矩阵乘法,我们可以将这种分层分离为越来越小的内存块,从而越来越快地执行非常快的矩阵乘法。 为此,我们需要将大矩阵乘法分成较小的子矩阵乘法。这些块称为memory tiles,或通常简称为tiles。

我们在本地Shared Memory中对这些较小的tiles执行矩阵乘法,该内存速度快且接近流式多处理器 (SM) — 相当于 CPU 内核。 使用Tensor Core,我们更进一步:我们获取每个tile并将这些tiles的一部分加载到Tensor Core中, 该Tensor Core由registers直接寻址。 L2 Cache中的矩阵内存tile比global GPU memory (GPU RAM) 快 3-5 倍, Shared Memory比global GPU memory快 ~7-10 倍,而Tensor Core的registers比global GPU memory快 ~200 倍。

拥有更大的tiles意味着我们可以重用更多的内存。 我在我的TPU vs GPU博客文章中详细写过这一点。事实上,你可以看到TPU的每个Tensor Core都有非常非常大的tiles。 因此,TPU 可以在每次从全局内存传输时重用更多的内存,这使得它们在矩阵乘法方面的效率比 GPU 高一些。

每个tile大小取决于每个流式处理多处理器(SM)的内存量以及所有 SM 中的 L2 cache量。我们在以下体系结构上具有以下共享内存大小:

  • Volta (Titan V): 128kb shared memory / 6 MB L2
  • Turing (RTX 20s series): 96 kb shared memory / 5.5 MB L2
  • Ampere (RTX 30s series): 128 kb shared memory / 6 MB L2
  • Ada (RTX 40s series): 128 kb shared memory / 72 MB L2

我们看到 Ada 具有更大的二级缓存,允许更大的tile尺寸,从而减少了全局内存访问。 例如,在BERT大型训练期间 ,任何矩阵乘法的输入和权重矩阵都整齐地适合 Ada 的 L2 缓存(but not other Us). 因此,数据只需要从全局内存加载一次,然后数据就可以通过 L2 缓存获得,这使得 Ada 的这种架构的矩阵乘法速度提高了大约 1.5 – 2.0 倍。 对于较大的模型,训练期间的加速较低,但存在某些sweetspots,这可能会使某些模型更快。批量大小大于 8 的inference 也可以从较大的 L2 caches中受益匪浅。