GPU 架构和核算入门
大多数程序员对 CPU 和次序编程都有深入的了解,因为他们是在为 CPU 编写代码的过程中长大的,但许多程序员不太熟悉 GPU 的内部作业原理以及它们如此特别的原因。在曩昔的十年中,GPU 因为在深度学习中的广泛运用而变得异常重要。如今,每个软件工程师都有必要对其作业方法有根本的了解。我写这篇文章的目的是为您供给一些基础知识。
本文的大部分内容依据 Hwu 等人所著的《大规模并行处理器编程》第四版。因为本书涵盖了 Nvidia GPU,因而我还将评论 Nvidia GPU 并运用 Nvidia 特定术语。但是,GPU 编程的根本概念和办法也适用于其他供应商。*
托马斯福斯特 (Thomas Foster) 在 Unsplash 上拍摄的照片
CPU 和 GPU 的比照
咱们将首先对 CPU 和 GPU 进行比较,这将使咱们更好地了解 GPU 领域。但是,这是一个独自的主题,咱们不行能将一切内容都压缩在一个章节中。因而,咱们将重视几个关键点。
CPU 和 GPU 之间的首要区别在于它们的规划方针。 CPU 规划用于履行次序指令 【注释1】。为了进步次序履行功用,多年来 CPU 规划中引入了许多功用。关键是削减指令履行推迟,以便 CPU 能够赶快履行指令序列。这包括指令流水线、乱序履行、估测履行和多级缓存等功用(仅列出一些)。
另一方面,GPU 专为大规模并行性和高吞吐量而规划,但代价是 较高的指令推迟。这一规划方向受到了它们在视频游戏、图形、数值核算和现在深度学习中的运用的影响。一切这些应用程序都需求以非常快的速度履行很多线性代数和数值核算,因而人们对进步这些设备的吞吐量投入了很多重视度。
让咱们考虑一个具体的比如。因为指令推迟较低,CPU 能够比 GPU 更快地核算两个数字加法。他们将能够以比 GPU 更快的速度 连续履行多项此类核算。但是,当进行数百万或数十亿次此类核算时,GPU 因为其巨大的并行性而比 CPU 更快地完结这些核算。
咱们来谈谈数值核算。数值核算 硬件的功用 是依据每秒能够履行多少次浮点运算 (FLOPS) 来衡量的。 Nvidia Ampere A100 在 32 位精度下供给 19.5 T FLOPS 的吞吐量。相比之下,英特尔 24 核处理器的 32 位精度吞吐量为 0.66 T FLOPS(这些数字来自 2021 年)。而且,GPU 和 CPU 之间的吞吐量功用距离逐年扩展。
下图比较了CPU和GPU的架构。
图 1:CPU 和 GPU 芯片规划的比较。图来自 Nvidia CUDA C++ 编程攻略
正如您所看到的,CPU 将很多芯片面积专门用于可削减指令推迟的功用,例如大缓存(蓝色部分)、更少的 ALU (绿色部分)和更多的操控单元(黄色部分)。相比之下,GPU 运用很多 ALU(绿色部分) 来最大化其核算才干和吞吐量。它们运用非常少数的芯片区域作为缓存和操控单元,然后削减 CPU 的推迟。
推迟忍受和高吞吐量
您或许想知道,GPU 怎么忍受高推迟并供给高功用。 GPU 具有很多线程和强壮的核算才干,使这一点成为或许。即使单个指令具有很高的推迟,GPU 也会有用地调度线程履行,以便它们在每个时刻点都运用核算才干。例如,当某些线程正在等候指令成果时,GPU 将切换到履行其他非等候线程。这可保证 GPU 上的核算单元在一切时刻点都以其最大容量运转,然后供给高吞吐量。稍后当咱们评论内核怎么在 GPU 上履行时,咱们将对此有更明晰的了解。
GPU架构
因而,咱们知道 GPU 偏向于高吞吐量,但它们的架构是什么样的才干完结这一方针,让咱们在本节中评论。
GPU核算架构
GPU 由一系列 流式多处理器 (streaming multiprocessors (SM) ) 组成。每个 SM 又由多个流处理器或内核或线程组成。例如,Nvidia H100 GPU 有 132 个 SM,每个 SM 有 64 个中心,总共有 8448 个中心。
每个 SM 都有 有限的片上存储器,一般称为同享存储器或暂存器,在一切内核之间同享。同样,SM 上的操控单元资源由一切核同享。此外,每个 SM 都装备了依据硬件的线程调度程序来履行线程。
除此之外,每个 SM 还具有多个功用单元或其他加速核算单元,例如张量中心或光线追踪单元,以满足 GPU 所满足的作业负载的特定核算需求。
图 2:GPU 核算架构
接下来,咱们来分化一下 GPU 内存,看看里面的情况。
GPU内存架构
GPU 有多层不同类型的存储器,每层都有其特定的用例。下图显示了 GPU 中一个 SM 的内存层次结构。
图 3:来自康奈尔大学了解 GPU 虚拟研讨会的 GPU 内存架构
让咱们来分化一下。
-
Registers: 寄存器:我们将从寄存器开端。 GPU中的每个SM都有很多的寄存器。例如,Nvidia A100 和 H100 型号的每个 SM 有 65,536 个寄存器。这些寄存器在内核之间同享,并依据线程的要求动态分配给它们。在履行期间,分配给线程的寄存器是该线程私有的,即其他线程无法读取/写入这些寄存器。
-
Constant Caches: 常量缓存:接下来,咱们在芯片上有常量缓存。它们用于缓存 SM 上履行的代码所运用的常量数据。为了运用这些缓存,程序员有必要在代码中显式地将对象声明为常量,以便 GPU 能够缓存并将它们保存在常量缓存中。
-
Shared Memory: 同享内存:每个 SM 还具有同享内存或暂存器,它是少数快速且低推迟的片上可编程 SRAM 内存。它被规划为由运转在 SM 上的线程块同享。同享内存背后的主意是,假如多个线程需求处理同一块数据,则只要其间一个线程应该从大局内存加载它,而其他线程则同享它。谨慎运用同享内存能够削减大局内存的冗余加载操作,进步内核履行功用。同享内存的另一个用处是作为块内履行的线程之间的同步机制。
-
L1 Cache:
L1 Cache:每个 SM 还具有一个 L1 缓存,能够缓存 L2 缓存中常常拜访的数据。 -
L2 Cache:
L2 Cache:有一个L2 Cache,由一切SM 同享。它缓存大局内存中常常拜访的数据以削减推迟。请注意,L1 和 L2 缓存对 SM 都是透明的,即 SM 不知道它正在从 L1 或 L2 获取数据。对于SM来说,它是从大局内存中获取数据。这相似于 CPU 中 L1/L2/L3 缓存的作业方法。 -
Global Memory: 大局内存:GPU还有一个片外大局内存,它是一种高容量、高带宽的DRAM。例如,Nvidia H100 具有 80 GB 高带宽内存 (HBM),带宽为 3000 GB/秒。因为距离SM较远,大局内存的推迟适当高。但是,片上存储器的几个附加层和很多核算单元有助于躲藏这种推迟。
现在咱们现已了解了 GPU 硬件的关键组件,让咱们更深入地了解这些组件在履行代码时怎么发挥作用。
了解GPU的履行模型
要了解 GPU 怎么履行Kernel 函数,咱们首先需求了解什么是 kernel 以及怎么装备它们。让咱们从这儿开端。
CUDA Kernels 和线程块(Thread Blocks)
CUDA 是 Nvidia 供给的编程接口,用于为其 GPU 编写程序。在 CUDA 中,您以相似于 C/C++ 函数的方法表达要在 GPU 上运转的核算,该函数称为 kernel。kernel对数字向量进行并行操作,这些向量作为函数参数供给给它。一个简单的比如是履行向量加法的kernel,即,一个kernel将两个数字向量作为输入,将它们按元素相加并将成果写入第三个向量。
为了在 GPU 上履行Kernel,咱们需求发动许多线程,这些线程统称为网格(grid)。但网格还有更多结构。网格由一个或多个线程块(有时简称为块)组成,每个块由一个或多个线程组成。
块和线程的数量取决于 数据的巨细和咱们想要的并行度。例如,在咱们的向量加法示例中,假如咱们核算 维度为 256 的向量加法,那么咱们或许会决议装备一个包括 256 个线程的线程块,以便每个线程都对向量的一个元素进行操作。对于更大的问题,咱们或许在 GPU 上没有满足的可用线程,而且咱们或许期望每个线程处理多个数据点。
图 4:线程块网格(图来自 Nvidia CUDA C++ 编程攻略)
就完结而言,编写kernel 函数需求两个部分。第一部分是在 CPU 上履行的主机代码(host code)。这是咱们加载数据、在 GPU 上分配内存以及运用装备的线程网格加载发动 kernel 的当地。第二部分是编写在 GPU 上履行的设备 (GPU) 代码。
对于咱们的向量加法示例,下图显示了host代码。
图 5:用于核算矢量加法的 CUDA 内核的host代码
下面是设备代码(device code),它界说了实践的kernel函数。
图 6:包括矢量加法kernel界说的设备代码(device code,)
因为本文的关键不是教授 CUDA,因而咱们不会更深入地评论此代码。现在,让咱们看看 GPU 上履行kernel的具体步骤。
GPU 上履行Kernel的步骤
1. 将数据从主机(Host) 仿制到设备 (Device)
在调度kernel履行之前,有必要将其所需的一切数据从主机(CPU)的内存仿制到 GPU(设备)的大局内存。虽然如此,在最新的 GPU 硬件中,咱们还能够运用 统一虚拟内存 直接从主机内存中读取数据(请参阅论文第 2.2 节:“EMOGI:GPU 中内存外图遍历的高效内存拜访”)。
2. SM上线程块的调度
当 GPU 的内存中具有一切必要的数据后,它会将线程块分配给 SM。块内 的一切线程一同由同一个 SM 处理。为了完结这一点,GPU 有必要在 SM 上为这些线程预留资源,然后才干开端履行它们。实践中,多个线程块 能够分配给同一个SM一同履行。
图 7:将 线程块 分配给 SM
因为 SM 的数量有限,而且大的kernel或许具有 很多块,因而并非一切块都能够立即分配履行。 GPU 保护一个等候分配和履行的块列表。当任何块完结履行时,GPU 会分配等候列表中的块之一来履行。
3.单指令多线程(SIMT)和 线程束(Wraps)
咱们知道一个block的一切线程都被分配到同一个SM。但在此之后还有另一个层次的 线程 区分。这些 线程进一步分为 32 个巨细的线程束 warp [见注释2]),并一同分配以在称为 处理块 的一组中心上履行。
SM 经过获取并向一切线程发出相同的指令来一同履行 warp 中的一切线程。然后,这些线程一同履行该指令,但针对数据的不同部分。在咱们的向量加法示例中,warp 中的一切线程或许都在履行加法指令,但它们将在向量的 不同索引进步行操作。
这种 warp 的履行模型也称为单指令多线程 (SIMT),因为多个线程正在履行同一指令。它相似于 CPU 中的单指令多数据 (SIMD) 指令。
从 Volta 开端,新一代 GPU 供给了一种替代指令调度机制,称为独立线程调度。它允许线程之间完全并发,而不管warp怎么。它能够用来更好地运用履行资源,或许作为线程之间的同步机制。咱们不会在这儿评论独立线程调度,但您能够在 CUDA 编程攻略中阅读相关内容。
4. Warp调度和推迟忍受
关于Warp的作业原理,有一些有趣的细节值得评论。
即使 SM 中的一切处理块(中心组)都在处理Warp,但在任何给定时刻,只要少数块正在主动履行指令。发生这种情况是因为 SM 中可用的履行单元数量有限。
但有些指令需求更长的时刻才干完结,导致Warp等候成果发生。在这种情况下,SM 会将等候的 warp 置于睡眠状况,并开端履行另一个不需求等候任何操作的 warp。这使得 GPU 能够最大极限地运用一切可用的核算并供给高吞吐量。
零开支调度:因为每个线程束中的每个线程都有自己的一组寄存器,因而 SM 从履行一个线程束切换到履行另一个线程束时没有开支。 这与 CPU 进步程之间的上下文切换方法形成比照。假如一个进程正在等候长时刻运转的操作,CPU 会一同在该中心上调度另一个进程。但是,CPU 中的上下文切换是贵重的,因为 CPU 需求将寄存器保存到主存中,并康复其他进程的状况。
5. 将成果数据从设备仿制到主机内存
最后,当kernel 的一切线程都履行结束后,最后一步是将成果仿制回主机内存。
虽然咱们涵盖了有关典型kernel 履行的一切内容,但还有一件事需求独自的部分:动态资源分区。
资源区分和占用概念 (Resource Partitioning and the Concept of Occupancy )
咱们经过称为“占用(occupancy)”的指标来衡量 GPU 资源的运用率,该指标表示分配给 SM 的 warp 数量与其可支持的最大数量的比率。为了完结最大吞吐量,咱们期望具有 100% 的占用率。但是,在实践中,因为各种约束,这并不总是可行。
那么,为什么咱们总是不能达到100%的occupancy呢? SM具有一组固定的履行资源,包括寄存器、同享内存、线程块槽和线程槽。这些资源依据线程的要求和 GPU 的约束在线程之间动态分配。例如,在 Nvidia H100 上,每个 SM 能够处理 32 个块、64 个warp(即 2048 个线程)以及每个块 1024 个线程。假如咱们发动块巨细为 1024 个线程的网格,GPU 会将 2048 个可用线程槽分成 2 个块。
动态分区与固定分区:动态分区能够更有用地运用 GPU 中的核算资源。假如咱们将其与固定分区方案进行比较,其间每个线程块接纳固定数量的履行资源,那么它或许并不总是最有用的。在某些情况下,或许会为线程分配比其需求更多的资源,然后导致资源糟蹋和吞吐量降低。
现在,咱们经过一个比如来看看资源分配怎么影响SM的占用。假如咱们运用 32 个线程的块巨细而且总共需求 2048 个线程,那么咱们将有 64 个这样的块。但是,每个 SM 一次只能处理 32 个区块。因而,即使 SM 能够运转 2048 个线程,但它一次只能运转 1024 个线程,然后导致 50% 的占用率。
同样,每个SM有65536个寄存器。要一同履行 2048 个线程,每个线程最多能够有 32 个寄存器 (65536/2048 = 32)。假如内核每个线程需求 64 个寄存器,那么每个 SM 只能运转 1024 个线程,同样会导致 50% 的占用率。
次优occupancy的挑战在于,它或许无法供给必要的推迟忍受度或达到硬件峰值功用所需的核算吞吐量。
高效创建 GPU kernel是一项复杂的任务。咱们有必要明智地分配资源,以坚持高占用率,一同最大极限地削减推迟。例如,具有许多寄存器能够使代码运转得更快,但或许会削减占用率,因而细心的代码优化很重要。
总结
我知道了解这么多新术语和概念是令人畏惧的。让咱们总结一下关键以便快速回顾。
-
GPU 由多个流式多处理器 (streaming multiprocessors (SM)) 组成,其间每个 SM 具有多个处理中心。
-
有一个片外大局存储器,它是 HBM 或 DRAM。距离芯片上的SM较远,推迟较高。
-
有一个片外 L2 缓存和一个片内 L1 缓存。这些 L1 和 L2 高速缓存的运转方法与 CPU 中 L1/L2 高速缓存的运转方法相似。
-
每个 SM 上都有少数可装备的同享内存。这是中心之间同享的。一般,线程块(thread block)内 的 线程将一段数据加载到这个同享内存中,然后重用它,而不是从大局内存中再次加载它。
-
每个 SM 都有很多寄存器,这些寄存器依据线程的要求在线程之间进行分区。 Nvidia H100 每个 SM 有 65,536 个寄存器。
-
为了在 GPU 上履行kernel ,咱们发动了线程网格(grid)。网格由一个或多个线程块组成(thread blocks),每个线程块又由一个或多个线程组成。
-
GPU 依据资源可用性分配一个或多个块在 SM 上履行。一个块的一切线程都在同一个SM上分配和履行。这是为了运用数据局部性和线程之间的同步。
-
分配给 SM 的线程 进一步分为 32 个巨细的warp。 warp 内的一切线程一同履行相同的指令,但在数据的不同部分 (SIMT)。 (虽然新一代 GPU 也支持独立线程调度。)
-
GPU依据每个线程的需求和SM的约束在线程之间履行动态资源区分。程序员需求细心优化代码,以保证履行期间SM占用率达到最高水平。
GPU 如今已得到普遍运用,但其架构和履行模型从根本上与 CPU 有很大不同。在本文中,咱们介绍了 GPU 的各个方面,包括其架构和履行模型。假如您对 GPU 如此受追捧的原因以及它们的作业原理感到好奇,我期望本文能够供给一些有价值的见地。
参考资料
假如您想更深入地了解 GPU,能够参考以下一些资源:
-
Programming Massively Parallel Processors: 4th edition is the most up-to-date reference, but earlier editions are fine, too.
【注释】
是的,得益于超线程和多核,CPU 也能够并行履行任务。但是,长期以来,人们在进步次序履行的功用方面投入了很多精力。
在当前一代 Nvidia GPU 上,warp巨细为 32。但在未来的硬件迭代中或许会发生变化。