在本文中,我们将介绍一些基本的底层细节,以了解为什么 GPU 擅长图形、神经网络和深度学习任务,而 CPU 擅长大量连续、复杂的通用计算任务。为了写这篇文章,我必须研究几个主题,并对其进行更细致的了解,其中一些我将顺便提及。这样做是故意只关注 CPU 和 GPU 处理的绝对基础知识。
早期的计算机是专用设备。硬件电路和逻辑门被编程来执行一组特定的任务。如果要完成一些新任务,就需要重新布线电路。“新任务”可能就像对两个不同的方程进行数学计算一样简单。二战期间,艾伦·图灵正在研究一种可编程机器来击败恩尼格玛密码机,后来发表了“图灵机”论文。大约在同一时间,约翰·冯·诺依曼和其他研究人员也在研究一个想法,这个想法从根本上提出了:
我们知道计算机中的所有东西都是二进制的。字符串、图像、视频、音频、操作系统、应用程序等都是用 1 和 0 表示的。CPU 架构(RISC、CISC 等)规范有指令集(x86、x86-64、ARM 等),CPU 制造商必须遵守这些指令集,并且这些指令集可供操作系统与硬件交互。
操作系统和应用程序(包括数据)被转换成指令集和二进制数据,以便在 CPU 中处理。在芯片级别,处理是在晶体管和逻辑门上完成的。如果您执行一个程序来将两个数字相加,则加法(“处理”)是在处理器中的逻辑门上完成的。
在按照冯·诺依曼架构设计的 CPU 中,当我们将两个数字相加时,电路中会运行一条加法指令。在那一毫秒的一小部分时间里,处理单元的(执行)核心中只执行了加法指令!这个细节总是让我着迷。
上图中的组件是不言而喻的。有关更多详细信息和详细解释,请参阅这篇出色的文章。在现代 CPU 中,单个物理核心可以包含多个整数 ALU、浮点 ALU 等,同样,这些单元是物理逻辑门。
为了更好地理解 GPU,我们需要了解 CPU 核心中的“硬件线程”。硬件线程是每个 CPU 时钟周期内 CPU 核心执行单元中可以完成的计算单位。它代表了核心中可以执行的最小工作单元。
上图说明了 CPU 指令周期/机器周期。它是 CPU 执行单个指令(例如:c=a+b)的一系列步骤。
提取:程序计数器(CPU 内核中的特殊寄存器)跟踪必须提取的指令。提取指令并将其存储在指令寄存器中。对于简单操作,还会提取相应的数据。
解码:指令被解码以查看操作符和操作数。
执行:根据指定的操作,选择并执行适当的处理单元。
内存访问:如果指令很复杂或需要额外的数据(有多种因素可能导致这种情况),则在执行之前会进行内存访问。(为简单起见,在上图中忽略了这一点)。对于复杂指令,初始数据将在计算单元的数据寄存器中可用,但要完全执行指令,需要从 L1 和 L2 缓存访问数据。这意味着在计算单元执行之前可能需要等待一小段时间,并且硬件线程在等待期间仍会保留计算单元。
写回:如果执行产生输出(例如:c=a+b),则输出将写回到寄存器/缓存/内存。(为简单起见,在上图或本文后面的任何地方均忽略此输出)
在上图中,只有在 t2 时刻才会进行计算。其余时间,核心处于空闲状态(没有进行任何工作)。
现代 CPU 具有硬件组件,基本上可以使(获取-解码-执行)步骤在每个时钟周期同时发生。
单个硬件线程现在可以在每个时钟周期进行计算。这称为指令流水线。
提取、解码、内存访问和写回由 CPU 中的其他组件完成。由于找不到更好的词,这些被称为“流水线线程”。当流水线线程处于指令周期的执行阶段时,它将成为硬件线程。
如您所见,我们从 t2 开始每个周期都会获得计算输出。以前,我们每 3 个周期获得一次计算输出。流水线提高了计算吞吐量。这是管理冯·诺依曼架构中处理瓶颈的技术之一。还有其他优化,如无序执行、分支预测、推测执行等,
这是我们结束讨论并讨论 GPU 之前我想讨论的最后一个 CPU 概念。随着时钟速度的提高,处理器也变得更快、更高效。随着应用程序(指令集)复杂性的增加,CPU 计算核心未得到充分利用,并且花费更多时间等待内存访问。
因此,我们看到了内存瓶颈。计算单元花费时间进行内存访问,而没有做任何有用的工作。内存比 CPU 慢几个数量级,而且这种差距不会很快缩小。我们的想法是增加单个 CPU 核心中某些单元的内存带宽,并在等待内存访问时让数据准备好利用计算单元。
超线程于 2002 年由英特尔在 Xeon 和 Pentium 4 处理器中推出。在超线程之前,每个核心只有一个硬件线程。使用超线程后,每个核心将有 2 个硬件线程。这是什么意思?某些寄存器、程序计数器、提取单元、解码单元等的处理电路重复。
上图仅显示了具有超线程的 CPU 核心中的新电路元件。这就是单个物理核心在操作系统中显示为 2 个核心的方式。如果您有一个 4 核处理器,并启用了超线程,则操作系统会将其视为 8 个核心。L1 - L3 缓存大小将增加以容纳额外的寄存器。请注意,执行单元是共享的。
假设我们有进程 P1 和 P2 执行 a=b+c、d=e+f,由于存在硬件线程 1 和 2,因此这些可以在单个时钟周期内同时执行。如前所述,如果只有一个硬件线程,这是不可能的。在这里,我们通过添加硬件线程来增加内核中的内存带宽,以便高效利用处理单元。这提高了计算并发性。
一些有趣的场景:
查看这篇文章并尝试Colab 笔记本。它展示了矩阵乘法如何成为可并行化的任务,以及并行计算核心如何加速计算。
随着计算能力的提高,对图形处理的需求也随之增加。UI 渲染和游戏等任务需要并行操作,从而推动了对电路级大量 ALU 和 FPU 的需求。为顺序任务设计的 CPU 无法有效处理这些并行工作负载。因此,GPU 的开发是为了满足图形任务对并行处理的需求,后来为它们在加速深度学习算法中的应用铺平了道路。
我强烈推荐:
CPU 和 GPU 的核心、硬件线程、时钟速度、内存带宽和片上内存存在很大差异。例如:
此数字用于与 GPU 进行比较,因为获得通用计算的峰值性能非常主观。此数字是理论上的最大限制,这意味着 FP64 电路得到了充分利用。
我们在 CPU 中看到的术语并不总是直接转化为 GPU。在这里,我们将看到 NVIDIA A100 GPU 的组件和核心。在为本文做研究时,令我感到惊讶的一件事是,CPU 供应商没有公布核心执行单元中有多少个 ALU、FPU 等。NVIDIA 对核心数量非常透明,CUDA 框架在电路级别提供了完全的灵活性和访问。
在上面的 GPU 图中,我们可以看到没有 L3 缓存、较小的 L2 缓存、较小但较多的控制单元和 L1 缓存以及大量的处理单元。
以下是上图中的 GPU 组件及其与 CPU 的对应关系,以供我们初步了解。我没有做过 CUDA 编程,因此将其与 CPU 等效物进行比较有助于初步理解。CUDA 程序员非常了解这一点。
图形和深度学习任务需要SIM(D/T) [单指令多数据/线程] 类型执行,即根据单个指令读取和处理大量数据。
我们讨论了 CPU 中的指令流水线和超线程,GPU 也具有这种功能。其实现和工作方式略有不同,但原理相同。
与 CPU 不同,GPU(通过 CUDA)提供对流水线线程的直接访问(从内存中获取数据并利用内存带宽)。GPU 调度程序首先尝试填充计算单元(包括用于存储计算操作数的相关共享 L1 缓存和寄存器),然后“流水线线程”将数据提取到寄存器和 HBM 中。再次强调,CPU 应用程序程序员不会考虑这一点,并且有关“流水线线程”和每个核心的计算单元数量的规格并未公布。Nvidia 不仅公布了这些,还为程序员提供了完全的控制权。
我将在关于 CUDA 编程模型和模型服务优化技术中的“批处理”的专门文章中更详细地介绍这一点,我们可以从中看到它有多么有益。
上图描绘了 CPU 和 GPU 核心中的硬件线程执行情况。请参阅我们之前在 CPU 流水线中讨论的“内存访问”部分。此图显示了这一点。CPU 复杂的内存管理使此等待时间足够短(几个时钟周期),以便将数据从 L1 缓存提取到寄存器。当需要从 L3 或主内存提取数据时,数据已在寄存器中的另一个线程(我们在超线程部分中看到过这一点)将获得对执行单元的控制权。
在 GPU 中,由于超额认购(大量流水线线程和寄存器)和简单的指令集,大量数据已在寄存器中等待执行。这些等待执行的流水线线程将成为硬件线程,并在每个时钟周期内执行,因为 GPU 中的流水线线程很轻量。
超额目标是什么?
这就是为什么在 CPU 和 GPU 中较小矩阵的矩阵乘法延迟大致相同的主要原因。尝试一下。
任务需要足够并行,数据需要足够大才能使计算 FLOP 和内存带宽饱和。如果单个任务不够大,则需要打包多个这样的任务来使内存和计算饱和,以充分利用硬件。
计算强度 = FLOPs / 带宽。即计算单元每秒可完成的工作量与内存每秒可提供的数据量之比。
在上图中,我们看到,随着延迟时间的增加和带宽内存的降低,计算强度会增加。我们希望这个数字尽可能小,以便充分利用计算。为此,我们需要在 L1/寄存器中保留尽可能多的数据,以便快速进行计算。如果我们从 HBM 获取单个数据,则只有少数操作需要对单个数据执行 100 次操作才能使其值得。如果我们不执行 100 次操作,计算单元就会闲置。这就是 GPU 中大量线程和寄存器发挥作用的地方。在 L1/寄存器中保留尽可能多的数据,以保持较低的计算强度并保持并行核心繁忙。
CUDA 和 Tensor 核心之间的计算强度存在 4X 差异,因为 CUDA 核心每个时钟周期只能完成一个 1x1 FP64 MMA 指令,而 Tensor 核心每个时钟周期可以执行 4x4 FP64 MMA 指令。
大量计算单元(CUDA 和 Tensor 核心)、大量线程和寄存器(超额认购)、精简指令集、无 L3 缓存、HBM(SRAM)、简单且高吞吐量的内存访问模式(与 CPU 相比 - 上下文切换、多层缓存、内存分页、TLB 等)是 GPU 在并行计算(图形渲染、深度学习等)方面优于 CPU 的原则
GPU 最初是为了处理图形处理任务而创建的。人工智能研究人员开始利用 CUDA 及其通过 CUDA 核心直接访问强大的并行处理。NVIDIA GPU 具有纹理处理、光线追踪、光栅、多态引擎等(可以说是图形专用的指令集)。随着人工智能的采用率不断提高,擅长 4x4 矩阵计算(MMA 指令)的 Tensor 核心正在被添加,专用于深度学习。
自 2017 年以来,NVIDIA 一直在增加每个架构中的 Tensor 核心数量。但是,这些 GPU 也擅长图形处理。尽管 GPU 中的指令集和复杂性要少得多,但它并非完全专用于深度学习(尤其是 Transformer 架构)。
FlashAttention 2是针对 Transformer 架构的软件层优化(对注意层的内存访问模式的机械同情),可为任务提供 2 倍的加速。
凭借我们对 CPU 和 GPU 的深入理解,我们可以理解对 Transformer Accelerators 的需求:专用芯片(仅用于 Transformer 操作的电路),具有大量用于并行计算的计算单元、精简指令集、无 L1/L2 缓存、大量 DRAM(寄存器)取代 HBM、针对 Transformer 架构的内存访问模式优化的内存单元。毕竟 LLM 是人类的新伙伴(继网络和移动之后),它们需要专用芯片来提高效率和性能。