Press "Enter" to skip to content

在现代CPU上扩展BERT类模型的推断 – 第2部分

介绍:使用英特尔软件优化 CPU 上的 AI 效率

正如我们在之前的博客文章中详细介绍的那样,英特尔 Xeon CPU 提供了一组专为 AI 工作负载设计的特性,例如 AVX512 或 VNNI(矢量神经网络指令),用于使用整数量化神经网络进行高效推断,以及额外的系统工具,以确保以最高效的方式完成工作。在本博客文章中,我们将重点介绍软件优化,并让您了解英特尔的新一代 Ice Lake Xeon CPU 的性能。我们的目标是为您提供软件方面的全部信息,以充分利用您的英特尔硬件。与之前的博客文章一样,我们将展示基准测试结果和图表,以及新的工具,使所有这些参数和特性易于使用。

今年四月,英特尔推出了最新一代英特尔 Xeon 处理器,代号 Ice Lake,针对更高效和高性能的 AI 工作负载。具体而言,与之前的 Cascade Lake Xeon 处理器相比,Ice Lake Xeon CPU 在各种 NLP 任务上的推断速度可提高多达 75%。这是通过硬件和软件的综合改进实现的,例如新的 Sunny Cove 架构上提供的新指令和 PCIe 4.0,用于支持机器学习和深度学习工作负载。最后但并非最不重要的是,英特尔在各种框架上进行了专门的优化,这些框架现在具有英特尔的特色,例如英特尔扩展的 Scikit Learn,英特尔 TensorFlow 和英特尔 PyTorch 扩展。

所有这些特性在数据科学家和机器学习工程师日常工具集中是非常低级别的。在绝大多数情况下,更常见的做法是依赖于更高级别的框架和库来处理多维数组操作,例如 PyTorch 和 TensorFlow,并利用高度调优的数学运算符,如 BLAS(基本线性代数子程序)进行计算。

在这方面,英特尔通过提供 oneAPI 框架下的软件组件发挥了重要作用,通过 Intel oneMKL(数学内核库)提供高效的线性代数例程,通过 Intel OpenMP 或 Threading Building Blocks(oneTBB)提供更高级别的并行化框架。此外,oneAPI 还提供一些特定领域的库,例如 Intel oneDNN 用于深度神经网络基元(ReLU、全连接等),或者 oneCCL 用于集体通信,在使用分布式设置时特别有用,可实现多个主机上的高效全局归约操作。

其中一些库,特别是 MKL 或 oneDNN,已经原生地包含在 PyTorch 和 TensorFlow(自 2.5.0 起)等框架中,使最终用户可以轻松获得所有性能改进。当需要针对特定硬件特性时,英特尔提供了最常见软件的定制版本,专门为英特尔平台进行了优化和调优。例如,对于 TensorFlow,英特尔提供了定制的、高度调优和优化的框架版本,或者使用英特尔 PyTorch 扩展(IPEX)框架,可以将其视为 PyTorch 的功能实验室,然后再上游到 PyTorch。

深入探讨:利用先进的英特尔特性提高 AI 性能

性能调优参数

如上所述,我们将介绍一组新的可调参数,以改善我们的 AI 应用程序的性能。从高层次的角度来看,每个机器学习和深度学习框架都由相同的组成部分组成:

  1. 用于在内存中表示数据的结构化方式(向量、矩阵等)
  2. 数学运算符的实现
  3. 在目标硬件上进行高效并行计算

除了上述列出的几点之外,深度学习框架还提供了表示数据流和计算梯度所需的依赖关系。这超出了本博客文章的范围,并且利用与上述相同的组件!

图 1. 在 oneAPI 框架下的英特尔库概述

1. 内存分配和管理库

本博客文章将有意忽略关于数据表示的第一点,因为这是相当特定于框架的事情。作为参考,PyTorch 使用其自己的实现,称为 ATen,而 TensorFlow 则依赖于开源库 Eigen。

尽管对不同的对象结构和布局应用通用优化非常复杂,但有一个领域可以产生影响:内存分配。简要回顾一下,这里的内存分配是指以编程的方式向操作系统动态(事先未知)地请求系统上的一个区域,我们将能够将项目存储到其中,例如在 C 中的 malloc 和派生函数,或者在 C++ 中的 new 运算符。内存的效率,无论是在速度上还是在碎片化上,都是一个广阔的科学和工程课题,具有多种解决方案,取决于任务和底层硬件。在过去的几年中,我们看到了在这一领域的更多工作,特别是:

  • jemalloc(Facebook – 2005)
  • mimalloc(Microsoft – 2019)
  • tcmalloc(Google – 2020)

每个项目都在不同方面推进了内存分配和管理的不同方法,适用于各种软件。

2. 高效并行计算

现在我们有了一种高效表示数据的方式,我们需要一种方法来充分利用可用的计算硬件。有趣的是,当涉及到推理时,CPU在某种程度上比GPU具有潜在优势,因为它们无处不在,并且不需要特定的应用组件和管理人员来操作。

现代CPU配备了许多核心和复杂的机制,以提高软件的总体性能。然而,正如我们在第一篇博客文章中强调的那样,它们还具有可以根据您的目标工作负载(CPU绑定还是I/O绑定)进行调整的功能,以进一步提高应用程序的性能。

然而,实现并行算法可能并不像增加更多的核心来完成工作那样简单。许多因素,如使用的数据结构,并发数据访问,CPU缓存失效等,都可能阻止您的算法实际上变得更快。如果您对此主题更感兴趣,我们建议您观看Scott Meyers的演讲:《CPU缓存和为什么你关心它们》。

幸运的是,有一些库可以使开发这种并行算法的过程更加简单和少出错。在最常见的并行库中,我们可以提到OpenMP和TBB(Threading Building Blocks),它们在各个层次上工作,从C/C++编程API到环境变量调整和动态调度。在英特尔硬件上,建议使用OpenMP规范的英特尔实现,通常称为“IOMP”,作为英特尔oneAPI工具包的一部分。

图2. 通过OpenMP显示并行计算的代码片段

3. 优化的数学运算符

现在,我们已经涵盖了设计高效数据结构和并行算法所需的必要构建模块,剩下的最后一块是运行计算的部分,即实现各种数学运算符和神经网络层来进行我们最喜欢的工作,设计神经网络!😊

在每个程序员的工具包中,都有多个级别可以提供数学运算支持,然后可以根据各种因素进行不同的优化,例如使用的数据存储布局(连续内存,分块,紧凑等),表示每个标量元素的数据格式(Float32,整数,长整型,Bfloat16等)以及处理器支持的各种指令。

如今,几乎所有处理器都支持对标量项(一次一个项)或矢量化模式(意味着它们在同一CPU指令中对多个项进行操作,称为SIMD“单指令多数据”)进行基本数学运算。著名的SIMD指令集有SSE2,AVX,AVX2以及最新一代英特尔CPU上的AVX-512,能够在单个CPU时钟内操作16个字节的内容。

大多数时候,一个人不必太担心执行两个向量之间的简单逐元素加法所生成的实际汇编代码,但如果您确实担心,还有一些库可以让您比编写调用CPU特定的内部函数的代码更高一级,以实现高效的数学核心。这正是英特尔的MKL“数学核心库”所提供的,以及著名的BLAS“基本线性代数子程序”接口,用于实现线性代数的所有基本操作。

最后,除此之外,还可以找到一些特定领域的库,例如英特尔的oneDNN,它提供了实现神经网络层所需的所有常见和基本构建模块。 Intel MKL和oneDNN已经与PyTorch框架进行了本地集成,可以为某些操作(如线性+ ReLU或卷积)提供性能加速。在TensorFlow方面,可以通过设置环境变量TF_ENABLE_ONEDNN_OPTS=1(TensorFlow >= 2.5.0)来启用oneDNN,以在内部实现类似的机制。

在最新的英特尔Ice Lake CPU上进行更高效的AI处理

为了报告Ice Lake产品系列的性能,我们将密切遵循本系列第一篇博客文章中使用的方法。作为提醒,我们将采用完全相同的模式来基准测试我们将在本文中介绍的各种设置。更具体地说,以下部分中呈现的结果基于:

  • PyTorch: 1.9.0
  • TensorFlow: 2.5.0
  • 批量大小: 1, 4, 8, 16, 32, 128
  • 序列长度: 8, 16, 32, 64, 128, 384, 512

我们将通过领域内接受的度量标准来呈现结果,以确定所提出优化的性能:

  • 延迟: 执行单个推理请求(即“前向调用”)通过模型所需的时间,以毫秒为单位。
  • 吞吐量: 系统在定义的时间内可以维持的推理请求(即“前向调用”)数量,以每秒调用数表示。

我们还将提供一个初始基准,显示原始结果,并在第一篇博客中突出的所有不同优化措施上应用第二个基准。所有工作都在由英特尔提供的云实例上运行,该实例采用运行在Ubuntu 20.04.2 LTS上的Ice Lake Xeon Platinum 8380 CPU。

您可以在各个云服务提供商上找到相同的处理器:

  • AWS m6i / c6i 实例
  • Azure Ev5 / Dv5 系列

图3. Intel Ice Lake Xeon 8380 规格

建立基准

如前所述,基准将由两个不同的设置组成: – 原始设置: 我们按原样运行工作负载,没有任何调整 – 优化设置: 我们应用第一篇博客中提到的各种调整

另外,根据我们之前博客文章的评论,我们想改变在结果基准中呈现框架的方式。因此,在本文的其余部分,我们将根据以下内容将框架基准结果进行分组:

  • 使用“eager”模式进行计算的框架(PyTorch,TensorFlow)
  • 使用“graph”模式进行计算的框架(TorchScript,TensorFlow Graph,Intel TensorFlow)

基准: Eager 框架延迟

在 eager 模式下运行的框架通常在执行过程中发现实际图形。更准确地说,实际计算图在执行之前是未知的,您逐步(渴望地)执行一个运算符,该运算符将成为下一个运算符的输入,依此类推,直到达到叶节点(输出)。

这些框架通常在您实现的算法中提供更灵活性,但会增加运行时开销,并略微增加内存使用量以跟踪所有需要的反向传播元素。

最后但并非最不重要的是,在这些框架中通常很难启用运算符融合等图形优化。例如,许多深度学习库(如oneDNN)对于卷积+ReLU都有优化的核心,但您实际上需要在执行图形之前知道此模式将出现在操作序列中,而在 eager 框架中,这是不可能的设计。

图4. PyTorch 延迟与涉及的核心数目相关

图5. 谷歌的 TensorFlow 延迟与涉及的核心数目相关

图6. 谷歌的 TensorFlow 在启用 oneDNN 的情况下的延迟与涉及的核心数目相关

图7. 英特尔的 TensorFlow 延迟与涉及的核心数目相关

整体趋势凸显了核心数对观察到的延迟的积极影响。在大多数情况下,增加核心数会减少不同工作负载大小的计算时间。然而,将更多的核心分配给任务并不会导致单调的延迟降低,工作负载的大小和分配给执行作业的资源数量之间总是有一个权衡。

如上图所示,从使用系统上可用的所有核心中得出一个非常常见的模式趋向于出现。插槽间通信引入了显著的延迟开销,并且对整体延迟的提高几乎没有什么改进。

此外,这种插槽间通信开销在工作负载变大时越来越不明显,这意味着使用所有计算资源从使用所有可用核心中受益。在这个领域中,似乎 PyTorch(图1.)和英特尔 TensorFlow(图4.)似乎具有稍微更好的并行支持,如在序列长度384和512上所示,使用所有核心仍然减少了观察到的延迟。

基准:图框架延迟

这次我们比较在“图”模式下使用框架时的性能,其中图在事先完全已知,并且可以进行所有的分配和优化,如图修剪和操作融合。

图8. 相对于涉及的核心数量的TorchScript延迟

图9. 相对于涉及的核心数量的Google TensorFlow延迟

图10. 相对于涉及的核心数量的启用了oneDNN的Google TensorFlow延迟

图11. 相对于涉及的核心数量的Intel TensorFlow延迟

这通常被称为“追踪”图,正如您在这里所看到的,结果与TorchScript(来自PyTorch的图执行模式)与TensorFlow之间的结果并没有太大差异。当并行化受限(在操作内计算中涉及的核心数量较少)时,所有TensorFlow实现似乎表现优于TorchScript,但是随着计算资源的增加,它们似乎无法高效扩展,而TorchScript似乎能够更好地利用现代CPU的计算能力。

尽管如此,在大多数情况下,所有这些框架之间的差距非常有限。

调整内存分配器:这会影响观察到的延迟吗?

每个动态分配内存的程序都依赖于内存分配器。如果您熟悉C/C++编程,这个组件为malloc/free或new/delete提供了低级别的支持。大多数情况下,您不必太担心它,而默认的分配器(例如大多数Linux发行版上的glibc)将在开箱即用时提供出色的性能。然而,在某些情况下,它可能无法提供最高效的性能,因为这些默认分配器大多数情况下都设计为大多数时间都表现良好,而不是针对特定工作负载或并行性进行微调。

那么,有哪些替代方案,它们什么时候比默认方案更合适呢?嗯,再次说,这取决于您软件周围的上下文。

可能的情况是大量分配/释放导致随着时间的推移发生碎片化,您正在执行软件的特定硬件和/或架构,以及应用程序的并行度级别。

您看出这是什么意思了吗?深度学习以及所有进行重型计算的应用程序都是高度多线程的,这也适用于诸如PyTorch、TensorFlow和其他针对机器学习工作负载的软件库。

默认的内存分配器策略通常依赖于全局内存池,这需要使用同步原语来操作,增加了系统的整体压力,降低了应用程序的性能。一些由Google、Facebook和Microsoft等公司最近进行的工作提供了在自定义内存分配器库中实现的替代内存分配策略,可以轻松地直接集成到软件组件中,或者使用动态共享库预加载来交换正在使用的库以实现分配/释放。

在这些库中,我们可以列举出一些,例如tcmalloc、jemalloc和mimalloc。

图12. 在不同任务上对各种内存分配器进行基准测试

在本博客文章中,我们只专注于对tcmalloc和jemalloc进行基准测试,作为潜在的内存分配器候选项。为了完全透明起见,对于下面的结果范围,我们使用了Ubuntu发行版版本2.9上提供的gperftools软件包中的tcmalloc和jemalloc 5.1.0-1。

内存分配器基准测试

同样,我们首先将性能与以渴望方式执行的框架进行比较。这可能是分配器可能发挥最大作用的用例:由于在其执行上述节点的实际执行时,每个框架必须管理每个操作所需的内存,因此无法提前进行计划。在这种情况下,由于所有的系统调用来分配和回收内存,分配器是一个重要的组成部分。

图13. PyTorch内存分配器和核心扩展的延迟

图14. 相对于涉及的核心数量的Google TensorFlow内存分配器和核心扩展的延迟

图15. 相对于涉及的核心数量的启用了oneDNN的Google TensorFlow内存分配器和核心扩展的延迟

图16. 相对于涉及的核心数量的Intel TensorFlow内存分配器和核心扩展的延迟

从上面的图表可以看出,标准库分配器(glibc)在性能方面通常落后,但提供合理的性能。Jemalloc分配器有时是最快的,但只在非常特定的情况下,即并发性不是很高的情况下,这可以通过jemalloc内部使用的底层结构来解释,这超出了本博客的范围,但您可以阅读Facebook Engineering博客以了解更多信息。

最后,根据这里进行基准测试的所有工作负载来看,tcmalloc似乎是提供了最佳性能的选择。再次强调,tcmalloc与Jemalloc在分配资源的方式上有所不同,特别是tcmalloc为每个线程本地维护了一个内存段池,这减少了全局、独占、关键路径的需求。

再次,如需了解更多细节,请阅读Google Abseil团队的完整博客。

现在,回到图形模式,我们对具有全面计算图的基准框架进行测试。

图17. TorchScript内存分配器和核心扩展延迟

图18. Google的TensorFlow内存分配器和核心扩展延迟

图19. 启用oneDNN的Google TensorFlow内存分配器和核心扩展延迟

图20. Intel TensorFlow内存分配器和核心扩展延迟

这一次,通过了解操作符流和涉及的矩阵形状的底层结构,框架可以预先规划和保留所需的资源。在这个上下文中,正如上面的图表所显示的那样,各个框架之间的差异非常小,并且在jemalloc和tcmalloc之间没有明确的赢家。当然,作为通用内存分配器,glibc仍然稍稍落后,但较急切的设置中的差距不那么显著。总之,在优化过程的最后一毫秒改善时,调整内存分配器可以提供一个有趣的因素,特别是如果您已经在使用跟踪计算图。

OpenMP

在前面的部分中,我们讨论了机器学习软件中的内存管理,这些软件主要涉及CPU密集型的工作负载。这些软件通常依赖于中间框架,如PyTorch或TensorFlow进行深度学习,这些框架通常会将所有底层高度并行化的操作器实现进行抽象。

编写这样高度并行和优化的算法是一个真正的工程挑战,它需要对CPU操作的所有实际元素有非常低级别的了解(同步、内存缓存、缓存有效性等)。在这种情况下,能够利用原语来实现这样的强大算法非常重要,与从头开始实现相比,可以大大减少交付时间和计算时间。

有许多可用的库提供这样的高级功能来加速算法的开发。在最常见的库中,可以看到OpenMP、Thread Building Blocks以及直接使用C++(针对最新版本的标准)。在本博客文章的后续部分,我们将限制在OpenMP上,特别是比较GNU开源和社区版实现与Intel OpenMP的差异。后者特别针对Intel CPU进行了优化,以在使用GNU OpenMP的情况下提供最佳性能。

OpenMP公开了许多环境变量,用于自动配置将参与计算的底层资源,例如用于分派计算的线程数(内部操作线程)、系统调度程序应如何将每个线程与CPU资源(线程、核心、插槽)绑定以及一些其他变量,这些变量为用户提供了进一步的控制。Intel OpenMP公开更多这些环境变量,以为用户提供更大的灵活性来调整软件的性能。

图21. 运行PyTorch的OpenMP与Intel OpenMP延迟

图22. 运行PyTorch的OpenMP与Intel OpenMP延迟

如上所述,调整OpenMP是在尝试了所有其他与系统相关的调整旋钮后可以开始调整的内容。它可以通过设置单个环境变量为模型带来最终的加速。

此外,值得注意的是,调整OpenMP库仅适用于使用OpenMP API的软件。更具体地说,现在只有PyTorch和TorchScript真正使用OpenMP,并从OpenMP后端调整中受益。

自动性能调优:使用Intel SigOpt进行贝叶斯优化

如上所述,可以调整许多旋钮以改善Intel CPU上的延迟和吞吐量,但由于存在许多旋钮,调整所有这些旋钮以获得最佳性能可能会很麻烦。例如,在我们的实验中,对以下旋钮进行了调整:

  • 核心数量:尽管使用尽可能多的核心通常是一个好主意,但它并不总是提供最佳性能,因为这也意味着不同线程之间的通信更多。除此之外,使用较少核心获得更好性能非常有用,因为它可以同时运行多个实例,从而提高延迟和吞吐量。
  • 内存分配器:默认的malloc、Google的tcmalloc和Facebook的jemalloc中的哪个内存分配器提供最佳性能?
  • 并行库:GNU OpenMP和Intel OpenMP中的哪个并行库提供最佳性能?
  • 透明大页面:在系统上启用透明大页面(THP)是否提供更好的性能?
  • KMP块时间参数:在完成并行区域的执行后,线程应等待的时间(以毫秒为单位),然后再休眠。

当然,粗力法,即尝试所有可能性的方法,将提供最佳的旋钮值,以获得最佳性能,但是,由于搜索空间的大小为N x 3 x 2 x 2 x 2 = 24N,这可能需要很长时间:在具有80个物理核心的机器上,这意味着最多尝试24 x 80 = 1920种不同的设置! 😱

幸运的是,英特尔的 SigOpt 通过贝叶斯优化,使我们能够更快速、更方便地分析这些调整实验,同时提供与粗力法类似的性能。

当我们分析绝对最佳延迟与 SigOpt 提供的结果之间的相对差异时,我们观察到,尽管它通常不如粗力法(除了特定情况下的序列长度 = 512),但它提供了非常接近的性能,其中最大差距为8.6%

图23. SigOpt 自动调整找到的绝对最佳延迟 vs 粗力法 图24. SigOpt 自动调整找到的相对最佳延迟 vs 粗力法

SigOpt 还非常有用于分析:它提供了许多图表和有价值的信息。首先,它提供了它能找到的最佳值、相应的旋钮以及随着试验的进行而改进的历史记录,例如,当序列长度为20时:

图25. SigOpt 最佳值报告 图26. SigOpt 最佳值报告

在这个特定的设置中,16个核心以及其他旋钮能够给出最佳结果,这非常重要,因为正如前面提到的,这意味着可以同时运行多个模型实例,同时每个实例仍然具有最佳的延迟。

它还显示在大约20次试验时收敛,这意味着也许只需要25次试验,而不是40次就足够了。还有许多其他有价值的信息可供使用,例如参数重要性:

正如预期的那样,核心数量远远是最重要的参数,但其他参数也起到一定作用,并且这非常依赖于实验。例如,对于序列长度 = 512 的实验,这是参数重要性:

图27. 批大小 = 1,序列长度 = 20 的 SigOpt 最佳值 图28. 批大小 = 1,序列长度 = 512 的 SigOpt 最佳值

在这里,使用 OpenMP vs Intel OpenMP 的影响比分配器的影响更大,每个旋钮的相对重要性比序列长度为20的实验更平衡。SigOpt 还提供许多其他图表,通常是交互式的,例如:

  • 2D 实验历史记录,允许比较旋钮 vs 旋钮或旋钮 vs 目标
  • 3D 实验历史记录,允许与2D实验历史记录相同的操作,但多了一个旋钮/目标。

结论 – 加速生产中的 Transformer

在本文中,我们展示了新的英特尔 Ice Lake Xeon CPU 适用于大规模运行 AI 工作负载以及可以交换和调整的软件元素,以充分发挥硬件的潜力。在设置了前面博客中详细介绍的各种低级旋钮后,所有这些项目都应予以考虑,以最大限度地利用所有核心和资源的使用。

在 Hugging Face,我们的使命是使最先进的机器学习 democra化,我们工作的一个关键部分是使这些最先进的模型尽可能高效,以便在规模上使用更少的能源和内存,并且对所有规模的公司来说更加实惠。

我们与英特尔的合作通过 🤗 硬件合作伙伴计划使我们能够将先进的效率和优化技术轻松提供给社区,通过我们的新的 🤗 Optimum 开源库专门用于生产性能。

对于希望加速 Transformer 模型推断的企业,我们的新产品 🤗 Infinity 提供了一种即插即用的容器化解决方案,在 GPU 上的延迟可达到1毫秒,在英特尔 Xeon Ice Lake CPU 上可达到2毫秒。

如果您觉得这篇文章对您的工作有趣或有用,请考虑给Optimum打个星星。如果这篇文章让您如听音乐般愉悦,请考虑加入我们的机器学习优化团队!

Leave a Reply

Your email address will not be published. Required fields are marked *