PyTorch-1-x-模型训练加速指南-一-

234 阅读30分钟

PyTorch 1.x 模型训练加速指南(一)

原文:zh.annas-archive.org/md5/787ca80dbbc0168b14234d14375188ba

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

你好!我是一名专注于高性能计算HPC)的系统分析师和学术教授。是的,你没看错!我不是数据科学家。那么,你可能会想知道我为什么决定写一本关于机器学习的书。别担心,我会解释的。

HPC 系统由强大的计算资源紧密集成,用于解决复杂问题。HPC 的主要目标是利用资源、技术和方法加速高强度计算任务的执行。传统上,HPC 环境已被用于执行来自生物学、物理学、化学等多个领域的科学应用程序。

但在过去几年中,情况发生了变化。如今,HPC 系统不仅仅运行科学应用程序的任务。事实上,在 HPC 环境中执行的最显著的非科学工作负载恰恰是本书的主题:复杂神经网络模型的构建过程。

作为数据科学家,您比任何人都知道训练复杂模型可能需要多长时间,以及需要多少次重新训练模型以评估不同场景。因此,使用 HPC 系统加速人工智能(AI)应用程序(不仅用于训练还用于推断)是一个需求增长的领域。

AI 与 HPC 之间的密切关系引发了我对深入研究机器学习和 AI 领域的兴趣。通过这样做,我能更好地理解 HPC 如何应用于加速这些应用程序。

所以,在这里我们是。我写这本书是为了分享我在这个主题上学到的东西。我的使命是通过使用单个或多个计算资源,为您提供训练模型更快的必要知识,并采用优化技术和方法。

通过加速训练过程,你可以专注于真正重要的事情:构建令人惊叹的模型!

本书适合谁

本书适合中级数据科学家、工程师和开发人员,他们希望了解如何使用 PyTorch 加速他们的机器学习模型的训练过程。尽管他们不是本材料的主要受众,负责管理和提供 AI 工作负载基础设施的系统分析师也会在本书中找到有价值的信息。

要充分利用本材料,需要具备机器学习、PyTorch 和 Python 的基础知识。然而,并不要求具备分布式计算、加速器或多核处理器的先前理解。

本书内容涵盖了什么

第一章分解训练过程,提供了训练过程在底层如何工作的概述,描述了训练算法并涵盖了该过程执行的阶段。本章还解释了超参数、操作和神经网络参数等因素如何影响训练过程的计算负担。

第二章加速训练模型,提供了加速训练过程可能的方法概述。本章讨论了如何修改软件堆栈的应用和环境层以减少训练时间。此外,它还解释了通过增加资源数量来提高性能的垂直和水平可伸缩性作为另一选项。

第三章编译模型,提供了 PyTorch 2.0 引入的新型编译 API 的概述。本章涵盖了急切模式和图模式之间的区别,并描述了如何使用编译 API 加速模型构建过程。此外,本章还解释了编译工作流程及涉及编译过程的各个组件。

第四章使用专用库,提供了 PyTorch 用于执行专门任务的库的概述。本章描述了如何安装和配置 OpenMP 来处理多线程和 IPEX 以优化在 Intel CPU 上的训练过程。

第五章构建高效数据管道,提供了如何构建高效数据管道以使 GPU 尽可能长时间工作的概述。除了解释数据管道上执行的步骤外,本章还描述了如何通过优化 GPU 数据传输并增加数据管道中的工作进程数来加速数据加载过程。

第六章简化模型,提供了如何通过减少神经网络参数的数量来简化模型而不牺牲模型质量的概述。本章描述了用于减少模型复杂性的技术,如模型修剪和压缩,并解释了如何使用 Microsoft NNI 工具包轻松简化模型。

第七章采用混合精度,提供了如何采用混合精度策略来加速模型训练过程而不影响模型准确性的概述。本章简要解释了计算机系统中的数值表示,并描述了如何使用 PyTorch 的自动混合精度方法。

第八章一瞥分布式训练,提供了分布式训练基本概念的概述。本章介绍了最常用的并行策略,并描述了在 PyTorch 上实施分布式训练的基本工作流程。

第九章多 CPU 训练,提供了如何在单台机器上使用通用方法和 Intel oneCCL 来编写和执行多 CPU 分布式训练的概述。

第十章使用多个 GPU 进行训练,提供了如何在单台机器的多 GPU 环境中编码和执行分布式训练的概述。本章介绍了多 GPU 环境的主要特征,并解释了如何使用 NCCL 在多个 GPU 上编码和启动分布式训练,NCCL 是 NVIDIA GPU 的默认通信后端。

第十一章使用多台机器进行训练,提供了如何在多个 GPU 和多台机器上进行分布式训练的概述。除了对计算集群的简介解释外,本章还展示了如何使用 Open MPI 作为启动器和 NCCL 作为通信后端,在多台机器之间编码和启动分布式训练。

要充分利用本书

您需要了解机器学习、PyTorch 和 Python 的基础知识。

书中涵盖的软件/硬件操作系统要求
PyTorch 2.XWindows、Linux 或 macOS

如果您使用本书的数字版本,建议您自己键入代码或者从本书的 GitHub 存储库中获取代码(下一节提供链接)。这样做将有助于避免与复制粘贴代码相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,网址为github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X。如果代码有更新,将在 GitHub 存储库中更新。

我们还提供其他来自我们丰富书籍和视频目录的代码包,请查阅github.com/PacktPublishing/

使用的约定

在本书中使用了许多文本约定。

文本中的代码:表示文本中的代码字词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。以下是一个例子:“ipex.optimize函数返回模型的优化版本。”

代码块设置如下:

config_list = [{    'op_types': ['Linear'],
    'exclude_op_names': ['layer4'],
    'sparse_ratio': 0.3
}]

当我们希望引起您对代码块特定部分的注意时,相关行或项目以粗体显示:

def forward(self, x):    out = self.layer1(x)
    out = self.layer2(out)
    out = out.reshape(out.size(0), -1)
    out = self.fc1(out)
    out = self.fc2(out)
    return out

任何命令行输入或输出如下所示:

maicon@packt:~$ nvidia-smi topo -p -i 0,1Device 0 is connected to device 1 by way of multiple PCIe

粗体:表示一个新术语、一个重要单词或者在屏幕上显示的单词。例如,菜单或对话框中的单词会以粗体显示。以下是一个例子:“OpenMP是一个库,用于通过使用多线程技术利用多核处理器的全部性能来并行化任务。”

提示或重要注释

像这样显示。

联系我们

我们随时欢迎读者的反馈。

总体反馈:如果您对本书的任何方面有疑问,请发送电子邮件至 customercare@packtpub.com,并在邮件主题中提及书名。

勘误:尽管我们已经尽最大努力确保内容的准确性,但错误不可避免。如果您在本书中发现错误,我们将不胜感激您向我们报告。请访问www.packtpub.com/support/err…并填写表格。

盗版:如果您在互联网上发现我们作品的任何形式的非法拷贝,请向我们提供位置地址或网站名称。请通过 copyright@packt.com 与我们联系,并提供链接至该材料的链接。

如果您有兴趣成为作者:如果您在某个专题上有专业知识,并且有意撰写或为一本书作贡献,请访问authors.packtpub.com

分享您的想法

一旦您阅读了Accelerate Model Training with PyTorch 2.X,我们很想听听您的想法!请点击此处直接访问亚马逊评论页面并分享您的反馈。

您的评论对我们和技术社区都很重要,将帮助我们确保我们提供的内容质量优秀。

下载本书的免费 PDF 副本

感谢您购买本书!

您喜欢随时随地阅读,但无法随身携带印刷书籍吗?

您的电子书购买是否与您选择的设备兼容?

别担心,现在每本 Packt 图书您都可以免费获取一个无 DRM 的 PDF 版本。

随时随地、任何地点、任何设备阅读。直接从您喜爱的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

这些好处并不止于此,您还可以独家获取折扣、新闻通讯和每天收到的优质免费内容

遵循以下简单步骤获取这些好处:

  1. 扫描下方的二维码或访问以下链接

packt.link/free-ebook/978-1-80512-010-0

  1. 提交您的购书证明

  2. 就是这样!我们将免费的 PDF 文件和其他好处直接发送到您的电子邮件中

第一部分:Paving the Way

在这一部分,你将学习关于性能优化的内容,在深入探讨本书中描述的技术、方法和策略之前。首先,你将了解训练过程中的各个方面,这些方面使其计算开销很大。之后,你将了解减少训练时间的可能方法。

这一部分包括以下章节:

  • 第一章, 解构训练过程

  • 第二章, 加快模型训练

第一章:拆解训练过程

我们已经知道训练神经网络模型需要很长时间才能完成。否则,我们不会在这里讨论如何更快地运行这个过程。但是,是什么特征使得这些模型的构建过程如此计算密集呢?为什么训练步骤如此耗时?要回答这些问题,我们需要理解训练阶段的计算负担。

在本章中,我们首先要记住训练阶段是如何在底层运行的。我们将理解什么使训练过程如此计算密集。

以下是您将在本章的学习中了解到的内容:

  • 记住训练过程

  • 理解训练阶段的计算负担

  • 理解影响训练时间的因素

技术要求

您可以在本章提到的示例的完整代码在书的 GitHub 仓库中找到,链接为github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main

您可以访问您喜爱的环境来执行这个笔记本,比如 Google Colab 或 Kaggle。

记住训练过程

在描述神经网络训练带来的计算负担之前,我们必须记住这个过程是如何工作的。

重要提示

本节对训练过程进行了非常简要的介绍。如果您对这个主题完全不熟悉,您应该花些时间理解这个主题,然后再转到后面的章节。学习这个主题的一个很好的资源是 Packt 出版的书籍《使用 PyTorch 和 Scikit-Learn 进行机器学习》,作者是 Sebastian Raschka、Yuxi (Hayden) Liu 和 Vahid Mirjalili。

基本上来说,神经网络学习示例,类似于一个孩子观察成年人。学习过程依赖于向神经网络提供输入和输出值对,以便网络捕捉输入和输出数据之间的内在关系。这样的关系可以解释为模型获得的知识。所以,在人看到一堆数据时,神经网络看到的是隐藏的知识。

这个学习过程取决于用于训练模型的数据集。

数据集

数据集包含一组与某个问题、情景、事件或现象相关的数据实例。每个实例都有特征和目标信息,对应输入和输出数据。数据集实例的概念类似于表或关系数据库中的记录。

数据集通常分为两部分:训练集和测试集。训练集用于训练网络,而测试部分则用于针对未见过的数据测试模型。偶尔,我们也可以在每次训练迭代后使用另一部分来验证模型。

让我们来看看 Fashion-MNIST,这是一个常用于测试和教授神经网络的著名数据集。该数据集包含 70,000 张标记的服装和配饰图像,如裙子、衬衫和凉鞋,属于 10 个不同的类别。数据集分为 60,000 个训练实例和 10,000 个测试实例。

正如图 1**.1所示,该数据集的单个实例包括一个 28 x 28 的灰度图像和一个标签,用来识别其类别。在 Fashion-MNIST 的情况下,我们有 70,000 个实例,通常称为数据集的长度。

图 1.1 – 数据集实例的概念

图 1.1 – 数据集实例的概念

除了数据集实例的概念外,我们还有数据集样本的概念。一个样本定义为一组实例,如图 1**.2所示。通常,训练过程执行的是样本而不仅仅是单个数据集实例。训练过程之所以采用样本而不是单个实例,与训练算法的工作方式有关。关于这个主题不用担心,我们将在接下来的章节中进行详细讨论:

图 1.2 – 数据集样本的概念

图 1.2 – 数据集样本的概念

样本中的实例数量称为批处理大小。例如,如果我们将 Fashion-MNIST 训练集分成批次大小为 32 的样本,则得到 1,875 个样本,因为该集合有 60,000 个实例。

批处理大小越大,训练集中样本的数量越少,如图 1**.3中所示:

图 1.3 – 批处理大小的概念

图 1.3 – 批处理大小的概念

在本例中,如果批处理大小为 8,则数据集被分为两个样本,每个样本包含八个数据集实例。另一方面,如果批处理大小较小(例如四个),则训练集将被分成更多的样本(四个样本)。

神经网络接收输入样本并输出一组结果,每个结果对应一个输入样本的实例。对于处理 Fashion-MNIST 分类图像问题的模型,神经网络接收一组图像并输出另一组标签,正如图 1**.4所示。每个标签表示输入图像对应的类别:

图 1.4 – 神经网络对输入样本的工作

图 1.4 – 神经网络对输入样本的工作

要提取数据集中的内在知识,我们需要将神经网络提交给训练算法,以便它可以学习数据中存在的模式。让我们跳转到下一节,了解这个算法是如何工作的。

训练算法

训练算法是一个迭代过程,它接受每个数据集样本,并根据正确结果与预测结果之间的误差调整神经网络参数。

单次训练迭代被称为训练步骤。因此,在学习过程中执行的训练步骤数量等于用于训练模型的样本数量。正如我们之前所述,批量大小定义了样本数量,也确定了训练步骤的数量。

执行所有训练步骤后,我们称训练算法完成了一个训练周期。开发者在开始模型构建过程之前必须定义训练周期的数量。通常,开发者通过变化并评估生成模型的准确性来确定训练周期的数量。

单个训练步骤按照图1**.5顺序执行四个阶段:

图 1.5 – 训练过程的四个阶段

图 1.5 – 训练过程的四个阶段

让我们逐步了解每一个步骤,理解它们在整个训练过程中的作用。

前向

在前向阶段,神经网络接收输入数据,执行计算,并输出结果。这个输出也称为神经网络预测的值。在 Fashion-MNIST 中,输入数据是灰度图像,预测的值是物品所属的类别。

考虑到训练步骤中执行的任务,前向阶段具有更高的计算成本。这是因为它执行神经网络中涉及的所有重计算。这些计算通常称为操作,将在下一节中解释。

有趣的是,前向阶段与推断过程完全相同。在实际使用模型时,我们持续执行前向阶段来推断一个值或结果。

损失计算

在前向阶段之后,神经网络将会输出一个预测值。然后,训练算法需要比较预测值与期望值,以查看模型所做预测的好坏程度。

如果预测值接近或等于真实值,则模型表现符合预期,训练过程朝着正确的方向进行。否则,训练步骤需要量化模型达到的错误,以调整参数与错误程度成比例。

重要提示

在神经网络术语中,这种误差通常称为损失成本。因此,在讨论此主题时,文献中常见到损失或成本函数等名称。

不同类型的损失函数,每种适合处理特定类型的问题。交叉熵CE)损失函数用于多类图像分类问题,其中我们需要将图像分类到一组类别中。例如,这种损失函数可以在 Fashion-MNIST 问题中使用。假设我们只有两个类别或分类。在这种情况下,面对二元类问题,建议使用二元交叉熵BCE)函数而不是原始的交叉熵损失函数。

对于回归问题,损失函数与分类问题中使用的不同。我们可以使用诸如均方误差MSE)的函数,该函数衡量神经网络预测值与原始值之间的平方差异。

优化

在获取损失之后,训练算法计算相对于网络当前参数的损失函数的偏导数。这个操作产生所谓的梯度,训练过程使用它来调整网络参数。

略去数学基础,我们可以将梯度视为需要应用于网络参数以最小化错误或损失的变化。

重要提示

您可以通过阅读 Packt 出版的《深度学习数学实战》一书(作者 Jay Dawani 编著)来了解深度学习中使用的数学更多信息。

与损失函数类似,优化器也有不同的实现方式。随机梯度下降SGD)和 Adam 最常用。

反向传播

为完成训练过程,算法根据优化阶段获得的梯度更新网络参数。

重要提示

本节提供了训练算法的理论解释。因此,请注意,根据机器学习框架的不同,训练过程可能具有与前述列表不同的一组阶段。

本质上,这些阶段构成了训练过程的计算负担。请跟随我到下一节,以了解这种计算负担如何受不同因素影响。

理解模型训练阶段的计算负担

现在我们已经复习了训练过程的工作原理,让我们了解训练模型所需的计算成本。通过使用计算成本或负担这些术语,我们指的是执行训练过程所需的计算能力。计算成本越高,训练模型所需的时间就越长。同样,计算负担越高,执行训练模型所需的计算资源就越多。

本质上,我们可以说训练模型的计算负担由三个因素定义,如图 1.6所示。

图 1.6 – 影响训练计算负担的因素

图 1.6 – 影响训练计算负担的因素

这些因素中的每一个(在某种程度上)都对训练过程施加了计算复杂性。让我们分别讨论每一个。

超参数

超参数定义了神经网络的两个方面:神经网络配置和训练算法的工作方式。

关于神经网络配置,超参数确定了每个层的数量和类型以及每个层中的神经元数量。简单的网络有少量的层和神经元,而复杂的网络有成千上万个神经元分布在数百个层中。层和神经元的数量决定了网络的参数数量,直接影响计算负担。由于参数数量在训练步骤的计算成本中有显著影响,我们将在本章稍后讨论这个话题作为一个独立的性能因素。

关于训练算法如何执行训练过程,超参数控制着周期数和步数的数量,并确定了训练阶段使用的优化器和损失函数,等等。其中一些超参数对训练过程的计算成本影响微乎其微。例如,如果我们将优化器从 SGD 改为 Adam,对训练过程的计算成本不会产生任何相关影响。

然而,其他超参数确实会显著增加训练阶段的时间。其中最典型的例子之一是批大小。批大小越大,训练模型所需的训练步骤就越少。因此,通过少量的训练步骤,我们可以加快建模过程,因为训练阶段每个周期执行的步骤会减少。另一方面,如果批大小很大,我们可能需要更多时间执行单个训练步骤。这是因为在每个训练步骤上执行的前向阶段必须处理更高维度的输入数据。换句话说,这里存在一个权衡。

例如,考虑批大小等于32的 Fashion-MNIST 数据集的情况。在这种情况下,输入数据的维度为32 x 1 x 28 x 28,其中 32、1 和 28 分别表示批大小、通道数(颜色,在这种情况下)和图像大小。因此,对于这种情况,输入数据包括 25,088 个数字,这是前向阶段应该计算的数字数量。然而,如果我们将批大小增加到128,输入数据就会变为 100,352 个数字,这可能导致单个前向阶段迭代执行时间较长。

另外,更大的输入样本需要更多的内存来执行每个训练步骤。根据硬件配置的不同,执行训练步骤所需的内存量可能会显著降低整个训练过程的性能,甚至使其无法在该硬件上执行。相反,我们可以通过使用具有大内存资源的硬件加速训练过程。这就是为什么我们需要了解我们使用的硬件资源的细节以及影响训练过程计算复杂度的因素。

我们将在整本书中深入探讨所有这些问题。

操作

我们已经知道每个训练步骤执行四个训练阶段:前向、损失计算、优化和反向。在前向阶段,神经网络接收输入数据并根据神经网络的架构进行处理。除其他事项外,架构定义了网络层,每个层在前向阶段执行一个或多个操作。

例如,一个全连接神经网络FCNN)通常执行通用的矩阵乘法运算,而卷积神经网络CNNs)执行特殊的计算机视觉操作,如卷积、填充和池化。结果表明,一个操作的计算复杂度与另一个不同。因此,根据网络架构和操作,我们可以得到不同的性能行为。

没有什么比一个例子更好了,对吧?让我们定义一个类来实例化一个传统的 CNN 模型,该模型能够处理 Fashion-MNIST 数据集。

重要提示

本节中显示的完整代码可在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter01/cnn-fashion_mnist.ipynb找到。

该模型接收大小为64 x 1 x 28 x 28的输入样本。这意味着模型接收到 64 张灰度图像(一个通道),高度和宽度均为 28 像素。因此,模型输出一个维度为64 x 10的张量,表示图像属于 Fashion-MNIST 数据集的 10 个类别的概率。

该模型有两个卷积层和两个全连接层。每个卷积层包括一个二维卷积、修正线性单元ReLU)激活函数和池化。第一个全连接层有 3,136 个神经元连接到第二个全连接层的 512 个神经元。然后,第二层连接到输出层的 10 个神经元。

重要提示

如果您对 CNN 模型不熟悉,观看 Packt YouTube 频道上的视频*什么是卷积神经网络(CNN)*会很有用,链接在youtu.be/K_BHmztRTpA

通过将这个模型导出为 ONNX 格式,我们得到了图 1.7中所示的图表:

图 1.7 – CNN 模型的操作

图 1.7 – CNN 模型的操作

重要提示

开放神经网络交换ONNX)是机器学习互操作性的开放标准。除其他外,ONNX 提供了一种标准格式,用于从许多不同的框架和工具中导出神经网络模型。我们可以使用 ONNX 文件来检查模型细节,将其导入到另一个框架中,或执行推断过程。

通过评估图 1.7,我们可以看到五个不同的操作:

  • Conv: 二维卷积

  • MaxPool: 最大池化

  • Relu: 激活函数(ReLU)

  • Reshape: 张量维度转换

  • Gemm: 通用矩阵乘法

所以,在底层,神经网络在前向阶段执行这些操作。从计算的角度来看,这是机器在每个训练步骤中运行的真实操作集合。因此,我们可以从操作的角度重新思考这个模型的训练过程,并将其写成一个更简单的算法:

for each epoch    for each training step
        result = conv(input)
        result = maxpool(result)
        result = relu(result)
        result = conv(result)
        result = maxpool(result)
        result = relu(result)
        result = reshape(result)
        result = gemm(result)
        result = gemm(result)
    loss = calculate_loss(result)
    gradient = optimization(loss)
    backwards(gradient)

如您所见,训练过程只是一系列按顺序执行的操作。尽管用于定义模型的函数或类,机器实际上是在运行这一系列操作。

结果表明,每个操作都具有特定的计算复杂性,因此需要不同级别的计算能力和资源来满足执行的要求。因此,我们可能会面对每个操作的不同性能增益和瓶颈。同样,一些操作可能更适合在特定的硬件架构中执行,这一点我们将在本书中看到。

要理解这个主题的实际意义,我们可以检查这些操作在训练阶段花费的时间百分比。因此,让我们使用PyTorch Profiler来获取每个操作的 CPU 使用百分比。以下列表总结了在 Fashion-MNIST 数据集的一个输入样本上运行 CNN 模型的前向阶段时的 CPU 使用情况:

aten::mkldnn_convolution: 44.01%aten::max_pool2d_with_indices: 30.01%
aten::addmm: 13.68%
aten::clamp_min: 6.96%
aten::convolution: 1.18%
aten::copy_: 0.70%
aten::relu: 0.59%
aten::_convolution: 0.49%
aten::empty: 0.35%
aten::_reshape_alias: 0.31%
aten::t: 0.31%
aten::conv2d: 0.24%
aten::as_strided_: 0.24%
aten::reshape: 0.21%
aten::linear: 0.21%
aten::max_pool2d: 0.17%
aten::expand: 0.14%
aten::transpose: 0.10%
aten::as_strided: 0.07%
aten::resolve_conj: 0.00%

重要提示

ATen 是 PyTorch 用于执行基本操作的 C++库。您可以在pytorch.org/cppdocs/#aten找到有关该库的更多信息。

结果显示,Conv 操作(此处标记为aten::mkldnn_convolution)占用了更高的 CPU 使用率(44%),其次是 MaxPool 操作(aten::max_pool2d_with_indices),占用 30%的 CPU 时间。另一方面,ReLU(aten::relu)和 Reshape(aten::reshape)操作消耗的 CPU 时间不到总 CPU 使用率的 1%。最后,Gemm 操作(aten::addmm)占用了约 14%的 CPU 时间。

通过这个简单的分析测试,我们可以确定前向阶段涉及的操作;因此,在训练过程中,存在不同级别的计算复杂性。我们可以看到,在执行 Conv 操作时,训练过程消耗了更多的 CPU 周期,而不是在执行 Gemm 操作时。请注意,我们的 CNN 模型具有两层,包含这两种操作。因此,在这个例子中,这两种操作被执行了相同的次数。

基于对神经网络操作的不同计算负担的了解,我们可以选择最佳的硬件架构或软件堆栈,以减少给定神经网络的主要操作的执行时间。例如,假设我们需要训练一个由数十个卷积层组成的 CNN 模型。在这种情况下,我们将寻找具有特殊能力的硬件资源,以更有效地执行 Conv 操作。尽管模型具有一些全连接层,但我们已经知道,与 Conv 相比,Gemm 操作可能计算负担较小。这就证明了,优先考虑能够加速卷积操作的硬件资源是合理的。

参数

除了超参数和操作外,神经网络参数是影响训练过程计算成本的另一个因素。正如我们之前讨论的那样,神经网络配置中层的数量和类型定义了网络上的总参数数量。

显然,参数数量越多,训练过程的计算负担越重。这些参数包括用于卷积操作的核值、偏置以及神经元之间连接的权重。

我们的 CNN 模型只有 4 层,却有 1,630,090 个参数。我们可以使用 PyTorch 的这个函数轻松地计算出网络中的总参数数量。

def count_parameters(model):    parameters = list(model.parameters())
    total_parms = sum(
        [np.prod(p.size()) for p in parameters if p.requires_grad])
    return total_parms

如果我们在我们的 CNN 模型中添加一个额外的具有 256 个神经元的全连接层,并重新运行这个函数,我们将得到总共 1,758,858 个参数,增加了近 8%。

在训练和测试这个新的 CNN 模型之后,我们得到了与之前相同的准确性。因此,注意网络复杂度与模型准确性之间的权衡是至关重要的。在许多情况下,增加层和神经元的数量不一定会导致更好的效率,但可能会增加训练过程的时间。

参数的另一个方面是用于表示模型中这些数字的数值精度。我们将在第七章采用混合精度中深入讨论这个话题,但现在请记住,用于表示参数的字节数对训练模型所需的时间有重要的贡献。因此,参数数量不仅影响训练时间,而且所选择的数值精度也会影响训练时间。

下一节将提出一些问题,帮助您巩固本章学到的内容。

测验时间!

让我们通过回答八个问题来复习本章学到的内容。首先,尝试在不查阅资料的情况下回答这些问题。

重要提示

所有这些问题的答案都可以在 github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/quiz/chapter01-answers.md 找到。

在开始测验之前,请记住,这根本不是一次测试!本节旨在通过复习和巩固本章节所涵盖的内容来补充您的学习过程。

为以下问题选择正确选项:

  1. 训练过程包括哪些阶段?

    1. 前向,处理,优化和反向。

    2. 处理,预处理和后处理。

    3. 前向,损失计算,优化和反向。

    4. 处理,损失计算,优化和后处理。

  2. 哪些因素影响训练过程的计算负担?

    1. 损失函数,优化器和参数。

    2. 超参数,参数和操作。

    3. 超参数,损失函数和操作。

    4. 参数,操作和损失函数。

  3. 在执行训练算法处理所有数据集样本后,训练过程完成了一个训练什么?

    1. 进化。

    2. epoch。

    3. 步骤。

    4. 生成。

  4. 数据集样本包含一组什么?

    1. 数据集集合。

    2. 数据集步骤。

    3. 数据集的 epochs。

    4. 数据集实例。

  5. 哪个超参数更有可能增加训练过程的计算负担?

    1. 批次大小。

    2. 优化器。

    3. epoch 数。

    4. 学习率。

  6. 一个训练集有 2,500 个实例。通过定义批次大小分别为 1 和 50,训练过程执行的步骤数分别是以下哪一个?

    1. 500 和 5。

    2. 2,500 和 1。

    3. 2,500 和 50。

    4. 500 和 50。

  7. 训练过程的分析显示,最耗时的操作是 aten::mkldnn_convolution。在这种情况下,训练过程中哪个计算阶段更重?

    1. 反向。

    2. 前向。

    3. 损失计算。

    4. 优化。

  8. 一个模型有两个卷积层和两个全连接层。如果我们向模型添加两个额外的卷积层,将增加什么数量?

    1. 超参数。

    2. 训练步骤。

    3. 参数。

    4. 训练样本。

让我们总结一下这一章节我们学到的内容。

总结

我们已经完成了训练加速旅程的第一步。您从回顾训练过程如何工作开始了本章。除了复习数据集和样本等概念外,您还记得训练算法的四个阶段。

接下来,您了解到超参数,操作和参数是影响训练过程计算负担的三个因素。

现在你已经记住了训练过程,并理解了导致其计算复杂性的因素,是时候转向下一个主题了。

让我们迈出第一步,学习如何加速这一繁重的计算过程!

第二章:更快地训练模型

在上一章中,我们了解了增加训练过程的计算负担的因素。这些因素直接影响训练阶段的复杂性,从而影响执行时间。

现在是时候学习如何加快这一过程了。一般来说,我们可以通过改变软件堆栈中的某些内容或增加计算资源的数量来提高性能。

在本章中,我们将开始理解这两个选项。接下来,我们将学习可以在应用程序和环境层面进行修改的内容。

以下是本章的学习内容:

  • 理解加速训练过程的方法

  • 知道用于训练模型的软件堆栈的层次

  • 学习垂直和水平扩展的区别

  • 理解可以在应用程序层面修改以加速训练过程的内容。

  • 理解可以在环境层面进行修改以提高训练阶段性能的内容

技术要求

你可以在本书的 GitHub 仓库中找到本章提到的示例的完整代码,网址为 github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main

您可以访问您喜爱的环境来执行此笔记本,例如 Google Colab 或 Kaggle。

我们有哪些选择?

一旦我们决定加速模型训练过程,我们可以采取两个方向,如图 2.1所示:

Figure 2.1 – 加速训练阶段的方法

图 2.1 – 加速训练阶段的方法

在第一个选项(修改软件堆栈)中,我们将遍历用于训练模型的每一层软件堆栈,寻找改进训练过程的机会。简而言之,我们可以更改应用程序代码,安装和使用专门的库,或者在操作系统或容器环境中启用特殊功能。

这第一种方法依赖于对性能调整技术的深刻了解。此外,它要求具有高度的调查意识,以识别瓶颈并应用最合适的解决方案来克服它们。因此,这种方法是通过提取计算系统的最大性能来利用最多的硬件和软件资源。

然而,请注意,根据我们运行训练过程的环境,我们可能没有必要权限来更改软件堆栈的较低层。例如,假设我们在像KaggleGoogle Colab这样的第三方环境提供的笔记本中运行训练过程。在这种情况下,我们无法更改操作系统参数或修改容器镜像,因为这个环境是受控制和限制的。我们仍然可以更改应用程序代码,但这可能不足以加速训练过程。

当在软件堆栈中无法改变事物或未提供预期性能增益时,我们可以转向第二个选项(增加计算资源)来训练模型。因此,我们可以增加处理器数量和主内存量,使用加速器设备,或将训练过程分布在多台机器上。自然地,我们可能需要花费金钱来实现这个选项。

请注意,在云端比在本地基础设施中采用这种方法更容易。使用云时,我们可以轻松地合同一台配备加速器设备的机器或在我们的设置中添加更多机器。我们可以通过几次点击准备好这些资源以供使用。另一方面,在本地基础设施中添加新的计算资源时可能会遇到一些约束,例如物理空间和能源容量限制。尽管如此,这并非不可能,只是可能更具挑战性。

此外,我们还有另一种情况,即我们的基础设施,无论是云端还是本地,已经拥有这些计算资源。在这种情况下,我们只需开始使用它们来加速训练过程。

因此,如果我们有资金购买或合同这些资源,或者如果它们已经在我们的环境中可用,问题解决了,对吗?并非一定如此。不幸的是,使用额外的资源在训练过程中不能自动提高性能的保证。正如本书将讨论的那样,性能瓶颈并非总是通过增加计算资源而得以克服,而需要重新思考整个流程、调整代码等。

这最后的断言给了我们一个宝贵的教训:我们必须将这两种方法视为一个循环,而不是两个孤立的选择。这意味着我们必须根据需要反复使用这两种方法,以达到所需的改进,如图 2**.2所示。

图 2.2 – 持续改进循环

图 2.2 – 持续改进循环

让我们在接下来的几节中详细了解这两种方法的更多细节。

修改软件堆栈

用于训练模型的软件堆栈可以根据我们用于执行此过程的环境而异。为简单起见,在本书中,我们将从数据科学家的角度考虑软件堆栈,即作为计算服务或环境的用户。

总的来说,软件堆栈看起来像是图 2*.3* 所示的层次结构:

图 2.3 – 用于训练模型的软件堆栈

图 2.3 – 用于训练模型的软件堆栈

从顶部到底部,我们有以下层次:

  1. 应用程序:该层包含建模程序。该程序可以用任何能够构建神经网络的编程语言编写,如 R 和 Julia,但 Python 是主要用于此目的的语言。

  2. 环境:用于构建应用程序的机器学习框架,支持此框架的库和工具位于此层。一些机器学习框架的例子包括PyTorchTensorFlowKerasMxNet。关于库的集合,我们可以提到Nvidia Collective Communication LibraryNCCL),用于 GPU 之间的高效通信,以及jemalloc,用于优化内存分配。

  3. 执行:此层负责支持环境和应用层的执行。因此,容器解决方案或裸金属操作系统属于此层。虽然上层组件可以直接在操作系统上执行,但如今通常使用容器来封装整个应用程序及其环境。尽管 Docker 是最著名的容器解决方案,但更适合运行机器学习工作负载的选择是采用ApptainerEnroot

在软件堆栈的底部,有一个框代表执行上层软件所需的所有硬件资源。

为了实际理解这种软件堆栈表示,让我们看几个示例,如图 2*.4* 所示:

图 2.4 – 软件堆栈示例

图 2.4 – 软件堆栈示例

2*.4* 中描述的所有场景都使用 Python 编写的应用程序。如前所述,应用程序可以是用 C++编写的程序或用 R 编写的脚本。这并不重要。需要牢记的重要提示是应用层代表我们代码的位置。在示例 ABD 中,我们有使用 PyTorch 作为机器学习框架的场景。情况 AB 还依赖于额外的库,即OpenMPIntel One API。这意味着 PyTorch 依赖于这些库来增强任务和操作。

最后,场景 BC 的执行层使用容器解决方案来执行上层,而示例 AD 的上层直接在操作系统上运行。此外,请注意,场景 BC 的硬件资源配备了 GPU 加速器,而其他情况只有 CPU。

重要提示

请注意,我们正在抽象化用于运行软件堆栈的基础设施类型,因为在当前讨论阶段这是无关紧要的。因此,您可以考虑将软件堆栈托管在云端或本地基础设施中。

除非我们使用自己资源提供的环境,否则我们可能没有权限修改或添加执行层中的配置。在大多数情况下,我们使用公司配置的计算环境。因此,我们没有特权修改容器或操作系统层中的任何内容。通常,我们会将这些修改提交给 IT 基础设施团队。

因此,我们将专注于应用和环境层面,在这些层面上我们有能力进行修改和执行额外的配置。

本书的第二部分专注于教授如何改变软件堆栈,以便我们可以利用现有资源加速训练过程。

重要说明

在执行层面,有一些令人兴奋的配置可以提升性能。然而,它们超出了本书的范围。鉴于数据科学家是本材料的主要受众,我们将重点放在这些专业人士有权访问并自行修改和定制的层面上。

修改软件堆栈以加速训练过程有其局限性。无论我们采用多么深入和先进的技术,性能改进都会受到限制。当我们达到这个限制时,加速训练阶段的唯一方法是使用额外的计算资源,如下一节所述。

增加计算资源

在现有环境中增加计算资源有两种方法:垂直扩展和水平扩展。在垂直扩展中,我们增加单台机器的计算资源,而在水平扩展中,我们将更多机器添加到用于训练模型的设备池中。

在实际操作中,垂直扩展允许为机器配备加速器设备,增加主存储器,添加更多处理器核心等,正如图 2**.5所示的例子。进行这种扩展后,我们获得了一台资源更强大的机器:

图 2.5 – 垂直扩展示例

图 2.5 – 垂直扩展示例

水平扩展与我们的应用使用的机器数量增加有关。如果我们最初使用一台机器来执行训练过程,我们可以应用水平扩展,使用两台机器共同训练模型,正如在图 2**.6的示例中所示:

图 2.6 – 水平扩展示例

图 2.6 – 水平扩展示例

无论扩展的类型如何,我们都需要知道如何利用这些额外的资源来提高性能。根据我们设置的资源类型,我们需要在许多不同的部分调整代码。在其他情况下,机器学习框架可以自动处理资源增加,而无需任何额外的修改。

正如我们在本节学到的,加速训练过程的第一步依赖于修改应用层。跟我来到下一节,了解如何操作。

修改应用层

应用层是性能提升旅程的起点。因为我们完全控制应用代码,所以可以独立进行修改,不依赖于任何其他人。因此,开始性能优化过程的最佳方式就是独立工作。

我们可以在应用层中做出哪些改变?

你可能会想知道我们如何修改代码以改善性能。好吧,我们可以减少模型复杂性,增加批量大小以优化内存使用,编译模型以融合操作,并禁用分析函数以消除训练过程中的额外开销。

无论应用层所作的变更如何,我们不能以牺牲模型准确性为代价来改善性能,因为这毫无意义。由于神经网络的主要目标是解决问题,加速无用模型的构建过程就显得毫无意义。因此,在修改代码以减少训练阶段时间时,我们必须注意模型质量。

图 2*.7* 中,我们可以看到我们可以在应用层中进行的改变类型:

图 2.7 – 修改应用层以加快训练过程

图 2.7 – 修改应用层以加快训练过程

让我们看看每个变化:

  • 修改模型定义:修改神经网络架构以减少每层的层数、权重和执行的操作

  • 调整超参数:更改超参数,如批量大小、迭代次数和优化器

  • 利用框架的能力:充分利用像内核融合、自动混合精度和模型编译等框架能力

  • 禁用不必要的功能:摆脱不必要的负担,如在验证阶段计算梯度

重要提示

一些框架能力依赖于在环境层进行的变更,比如安装额外的工具或库,甚至升级框架版本。

当然,这些类别并不涵盖应用层性能改进的所有可能性;它们的目的是为您提供一个明确的心理模型,了解我们可以有效地对代码做些什么以加速训练阶段。

实际操作

让我们通过仅更改应用代码来看一个性能改进的实际示例。我们的实验对象是前一章介绍的 CNN 模型,用于对 Fashion-MNIST 数据集中的图像进行分类。

重要提示

此实验中使用的计算环境的详细信息在此时无关紧要。真正重要的是这些修改所实现的加速效果,在相同环境和条件下考虑。

此模型有两个卷积层和两个全连接层,共1,630,090个权重。当训练阶段的批量大小为 64,训练时长为 148 秒。训练后的模型对来自测试数据集的 10,000 张图像的准确率达到了 83.99%,如您所见:

Epoch [1/10], Loss: 0.9136Epoch [2/10], Loss: 0.6925
Epoch [3/10], Loss: 0.7313
Epoch [4/10], Loss: 0.6681
Epoch [5/10], Loss: 0.3191
Epoch [6/10], Loss: 0.5790
Epoch [7/10], Loss: 0.4824
Epoch [8/10], Loss: 0.6229
Epoch [9/10], Loss: 0.7279
Epoch [10/10], Loss: 0.3292
Training time: 148 seconds
Accuracy of the network on the 10000 test images: 83.99 %

重要提示

本节中展示的完整代码可在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter02/baseline.ipynb找到。

通过仅对代码进行一个简单的修改,我们可以将该模型的训练时间减少 15%,同时保持与基线代码相同的准确性。改进后的代码完成时间为 125 秒,训练后的模型达到了 83.76%的准确率:

Epoch [1/10], Loss: 1.0960Epoch [2/10], Loss: 0.6656
Epoch [3/10], Loss: 0.6444
Epoch [4/10], Loss: 0.6463
Epoch [5/10], Loss: 0.4772
Epoch [6/10], Loss: 0.5548
Epoch [7/10], Loss: 0.4800
Epoch [8/10], Loss: 0.4190
Epoch [9/10], Loss: 0.4885
Epoch [10/10], Loss: 0.4708
Training time: 125 seconds
Accuracy of the network on the 10000 test images: 83.76 %

重要提示

本节中展示的完整代码可在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter02/application_layer-bias.ipynb找到。

我们通过禁用两个卷积层和两个全连接层的偏差参数来提高性能。下面的代码片段展示了如何使用bias参数来禁用函数的Conv2dLinear层上的偏差权重:

def __init__(self, num_classes=10):    super(CNN, self).__init__()
    self.layer1 = nn.Sequential(
        nn.Conv2d
            (1, 32, kernel_size=3, stride=1,padding=1, bias=False),
        nn.ReLU(),
        nn.MaxPool2d(kernel_size = 2, stride = 2))
    self.layer2 = nn.Sequential(
        nn.Conv2d
            (32, 64, kernel_size=3,stride=1,padding=1, bias=False),
        nn.ReLU(),
        nn.MaxPool2d(kernel_size = 2, stride = 2))
    self.fc1 = nn.Linear(64*7*7, 512, bias=False)
    self.fc2 = nn.Linear(512, num_classes, bias=False)

这一修改将神经网络权重数量从 1,630,090 减少到 1,629,472,仅降低了总体权重的 0.04%。正如我们所看到的,权重数量的变化并未影响模型的准确性,因为它的效率几乎与之前相同。因此,我们以几乎没有额外工作的情况下,将模型的训练速度提高了 15%。

如果我们改变批量大小会怎样?

如果我们将批量大小从 64 增加到 128,则性能提升比禁用偏差要好得多:

Epoch [1/10], Loss: 1.1859Epoch [2/10], Loss: 0.7575
Epoch [3/10], Loss: 0.6956
Epoch [4/10], Loss: 0.6296
Epoch [5/10], Loss: 0.6997
Epoch [6/10], Loss: 0.5369
Epoch [7/10], Loss: 0.5247
Epoch [8/10], Loss: 0.5866
Epoch [9/10], Loss: 0.4931
Epoch [10/10], Loss: 0.4058
Training time: 96 seconds
Accuracy of the network on the 10000 test images: 82.14 %

重要提示

本节中展示的完整代码可在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter02/application_layer-batchsize.ipynb找到。

我们通过将批量大小加倍来使模型训练速度提高了 54%。正如我们在第一章中学到的,解构训练过程,批量大小决定了训练阶段的步骤数。因为我们将批量大小从 64 增加到 128,每个 epoch 的步骤数减少了,即从 938 减少到 469。因此,学习算法执行了完成一个 epoch 所需阶段的一半。

然而,这样的修改是有代价的:准确率从 83.99% 降低到了 82.14%。这是因为学习算法在每个训练步骤中执行优化阶段。由于步骤数量减少而 epoch 数量保持不变,学习算法执行的优化阶段数量减少了,因此降低了减少训练成本的机会。

只是出于好奇,让我们看看将批量大小更改为 256 时会发生什么:

Epoch [1/10], Loss: 1.5919Epoch [2/10], Loss: 0.9232
Epoch [3/10], Loss: 0.8151
Epoch [4/10], Loss: 0.6488
Epoch [5/10], Loss: 0.7208
Epoch [6/10], Loss: 0.5085
Epoch [7/10], Loss: 0.5984
Epoch [8/10], Loss: 0.5603
Epoch [9/10], Loss: 0.6575
Epoch [10/10], Loss: 0.4694
Training time: 76 seconds
Accuracy of the network on the 10000 test images: 80.01 %

尽管与从 64 改为 128 相比,训练时间缩短得更多,但效果并不明显。另一方面,模型效率降至 80%。与之前的测试相比,我们还可以观察到每个 epoch 的损失增加。

简言之,在调整批量大小时,我们必须在训练加速和模型效率之间找到平衡。理想的批量大小取决于模型架构、数据集特征以及用于训练模型的硬件资源。因此,在真正开始训练过程之前,通过一些实验来定义最佳批量大小是最好的方法。

这些简单的例子表明,通过直接修改代码可以加速训练过程。在接下来的部分中,我们将看看在环境层可以进行哪些更改以加速模型训练。

修改环境层

环境层包括机器学习框架及其执行所需的所有软件,例如库、编译器和辅助工具。

我们可以在环境层做哪些改变呢?

正如前面讨论过的,我们可能没有必要修改环境层的权限。这种限制取决于我们用于训练模型的环境类型。在第三方环境中,例如笔记本的在线服务中,我们没有灵活性进行高级配置,如下载、编译和安装专门的库。我们可以升级包或安装新的库,但不能做更多的事情。

为了克服这种限制,我们通常使用容器。容器允许我们配置运行应用程序所需的任何内容,而无需得到所有其他人的支持或权限。显然,我们讨论的是环境层,而不是执行层。正如我们之前讨论过的,修改执行层需要管理员权限,这在我们通常使用的大多数环境中是超出我们能力范围的。

重要提示

本节中展示的完整代码可在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter02/environment_layer.ipynb找到。

对于环境层,我们可以修改这些内容:

  • 安装和使用专用库:机器学习框架提供了训练模型所需的一切。但是,我们可以通过使用专门用于内存分配、数学运算和集体通信等任务的库来加快训练过程。

  • 通过环境变量控制库的行为:库的默认行为可能不适合特定情景或特定设置。在这种情况下,我们可以通过应用程序代码直接修改它们的环境变量。

  • 升级框架和库到新版本:这听起来可能很傻,但将机器学习框架和库升级到新版本可以比我们想象中提升训练过程的性能。

我们将在本书的进程中学习到许多这类事情。现在,让我们跳到下一节,看看实际中的性能改进。

实战

就像在上一节中所做的那样,我们将在这里使用基准代码来评估从修改环境层中获得的性能增益。请记住,我们的基准代码的训练过程花费了 148 秒。用于执行的环境层由 PyTorch 2.0(2.0.0+cpu)作为机器学习框架。

在对环境层进行两次修改后,我们获得了接近 40%的性能改进,同时模型的准确性几乎与之前相同,正如您所见:

Epoch [1/10], Loss: 0.6036Epoch [2/10], Loss: 0.3941
Epoch [3/10], Loss: 0.4808
Epoch [4/10], Loss: 0.5834
Epoch [5/10], Loss: 0.6347
Epoch [6/10], Loss: 0.3218
Epoch [7/10], Loss: 0.4646
Epoch [8/10], Loss: 0.4960
Epoch [9/10], Loss: 0.3683
Epoch [10/10], Loss: 0.6173
Training time: 106 seconds
Accuracy of the network on the 10000 test images: 83.25 %

我们只进行了一个更改,将基准模型的训练过程加速了将近 40%:安装和配置了 Intel OpenMP 2023.1.0 版本。我们通过设置三个环境变量配置了该库的行为:

import osos.environ['OMP_NUM_THREADS'] = "16"
os.environ['KMP_AFFINITY'] = "granularity=fine,compact,1,0"
os.environ['KMP_BLOCKTIME'] = "0"

简而言之,这些参数控制了 Intel Open 启动和编排线程的方式,并确定了库创建的线程数量。我们应该根据训练负担的特性和硬件资源来配置这些参数。请注意,在代码中设置这些参数属于修改环境层而不是应用程序层。即使我们在更改代码,这些修改也与环境控制相关,而不是模型定义。

重要提示

不必担心如何安装和启用 Intel OpenMP 库,以及本次测试中使用的每个环境变量的含义。我们将在第四章使用专用库,中详细介绍这个主题。

尽管通过 PIP 安装的 PyTorch 包默认包含 GNU OpenMP 库,但在装有 Intel CPU 的机器上,Intel 版本往往会提供更好的结果。由于本次测试所用的硬件机器配备了 Intel CPU,建议使用 Intel 版本的 OpenMP 而不是 GNU 项目提供的实现。

我们可以看到,在环境层中进行少量更改可以在不消耗大量时间或精力的情况下获得显著的性能提升。

下一节提供了一些问题,帮助你巩固本章学到的内容。

测验时间!

让我们通过回答一些问题来回顾我们在本章学到的内容。起初,试着在不查阅材料的情况下回答这些问题。

重要说明

所有这些问题的答案都可以在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/quiz/chapter02-answers.md找到。

在开始测验之前,请记住这根本不是一次测试!本节旨在通过复习和巩固本章涵盖的内容来补充你的学习过程。

为以下问题选择正确答案:

  1. 在单台机器上使用两个 GPU 运行训练过程后,我们决定添加两个额外的 GPU 来加速训练过程。在这种情况下,我们试图通过以下哪种方式来提高训练过程的性能?

    1. 水平扩展。

    2. 纵向扩展。

    3. 横向扩展。

    4. 分布式扩展。

  2. 简单模型的训练过程花费了很长时间才能完成。调整批量大小并删减一个卷积层后,我们可以在保持相同精度的情况下更快地训练模型。在这种情况下,我们通过更改以下软件栈的哪个层来改善训练过程的性能?

    1. 应用层。

    2. 硬件层。

    3. 环境层。

    4. 执行层。

  3. 以下哪种变化应用于环境层?

    1. 修改超参数。

    2. 采用另一种网络架构。

    3. 更新框架的版本。

    4. 在操作系统中设置参数。

  4. 以下哪个组件位于执行层?

    1. OpenMP。

    2. PyTorch。

    3. Apptainer。

    4. NCCL。

  5. 作为特定环境的用户,我们通常不修改执行层的任何内容。这是什么原因呢?

    1. 我们通常没有管理权限来更改执行层的任何内容。

    2. 在执行层没有任何变化可以加快训练过程。

    3. 执行层和应用层几乎是相同的。因此,在更改其中一个层和另一个层之间没有区别。

    4. 因为我们通常在容器上执行训练过程,所以在执行层没有任何变化可以改善训练过程。

  6. 通过使用两台额外的机器和应用机器学习框架提供的特定能力,我们加速了给定模型的训练过程。在这种情况下,我们采取了哪些措施来改进训练过程?

    1. 我们进行了水平和垂直扩展。

    2. 我们已经进行了水平扩展并增加了资源数量。

    3. 我们已经进行了水平扩展并应用了对环境层的变更。

    4. 我们已经进行了水平扩展并应用了对执行层的变更。

  7. 通过环境变量控制库的行为是应用在以下哪个层次上的变更?

    1. 应用层。

    2. 环境层。

    3. 执行层。

    4. 硬件层。

  8. 增加批量大小可以提高训练过程的性能。但它也可能导致以下哪些副作用?

    1. 减少样本数量。

    2. 减少操作数量。

    3. 减少训练步骤的数量。

    4. 降低模型精度。

让我们总结一下本章中涵盖的内容。

总结

我们已经完成了书的介绍部分。我们从学习如何减少训练时间的方法开始了本章。接下来,我们了解了可以在应用和环境层中进行的修改,以加速训练过程。

我们在实践中经历了如何在代码或环境中进行少量更改,从而实现令人印象深刻的性能改进。

您已经准备好在性能之旅中继续前进!在下一章中,您将学习如何应用 PyTorch 2.0 提供的最令人兴奋的功能之一:模型编译。

第二部分:加速过程

在这一部分中,您将学习如何在 PyTorch 中使用主要的技术和方法来加速深度学习模型的训练过程。首先,您将学习如何通过使用编译 API 来编译模型。之后,您将学习如何使用和配置专门的库来优化在 CPU 上的训练过程。然后,您将学习如何构建高效的数据管道,以确保 GPU 始终处于繁忙状态。此外,您将学习如何通过应用剪枝和压缩技术来简化模型。最后,您将学习如何采用自动混合精度以减少计算时间和内存消耗。

这一部分包括以下章节:

  • 第三章, 编译模型

  • 第四章, 使用专门的库

  • 第五章, 构建高效的数据管道

  • 第六章, 简化模型

  • 第七章, 采用混合精度

第三章:编译模型

引用一位著名的演讲者的话:“现在是时候了!”在完成我们朝性能改进迈出的初步步骤后,是时候学习 PyTorch 2.0 的新功能,以加速深度学习模型的训练和推断。

我们正在讨论在 PyTorch 2.0 中作为这个新版本最激动人心的功能之一呈现的 Compile API。在本章中,我们将学习如何使用这个 API 构建更快的模型,以优化其训练阶段的执行。

以下是本章的学习内容:

  • 图模式比热切模式的好处

  • 如何使用 API 编译模型

  • API 使用的组件、工作流程和后端

技术要求

你可以在本书的 GitHub 代码库中找到本章提到的所有示例的完整代码,链接为 github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main

您可以访问您喜爱的环境来执行这个笔记本,比如 Google Collab 或者 Kaggle。

您所说的编译是什么意思?

作为程序员,您会立即将“编译”这个术语分配给从源代码构建程序或应用的过程。尽管完整的构建过程包括生成汇编代码并将其链接到库和其他对象等额外阶段,但这种思考方式是合理的。然而,乍一看,在这本书的上下文中考虑编译过程可能会有点令人困惑,因为我们讨论的是 Python。毕竟,Python 不是一种编译语言;它是一种解释语言,因此不涉及编译。

注意

需要澄清的是,Python 函数为了性能目的而使用编译过的函数,尽管它主要是一种解释语言。

那么,编译模型的含义是什么?在回答这个问题之前,我们必须理解机器学习框架的两种执行模式。接下来请跟我来到下一节。

执行模式

本质上,机器学习框架有两种不同的执行模式。在热切模式下,每个操作都按照代码中出现的顺序执行,这正是我们期望在解释语言中看到的。解释器 – 在这种情况下是 Python – 一旦操作出现,就立即执行操作。因此,在执行操作时没有评估接下来会发生什么:

图 3.1 – 热切执行模式

图 3.1 – 热切执行模式

图 3**.1所示,解释器在 t1、t2 和 t3 的瞬间依次执行这三个操作。术语“热切”表示在进行下一步之前立即执行事务而不停顿评估整个情景。

除了急切模式外,还有一种名为图模式的方法,类似于传统的编译过程。图模式评估完整的操作集以寻找优化机会。为了执行此过程,程序必须整体评估任务,如图 3**.2所示:

图 3.2 - 图执行模式

图 3.2 - 图执行模式

图 3*.2*显示程序使用 t1 和 t2 执行编译过程,而不是像之前那样急切地运行操作。一组操作只在 t3 时执行编译后的代码。

术语“图”指的是由此执行模式创建的有向图,用于表示任务的操作和操作数。由于此图表示任务的处理流程,执行模式评估此表示以查找融合、压缩和优化操作的方法。

例如,考虑图 3**.3中的案例,该图表示由三个操作组成的任务。Op1 和 Op2 分别接收操作数 I1 和 I2。这些计算的结果作为 Op3 的输入,与操作数 I3 一起输出结果 O1:

图 3.3 - 表示在有向图中的操作示例

图 3.3 - 表示在有向图中的操作示例

在评估了这个图表之后,程序可以决定将所有三个操作融合到一个编译后的代码中。如图 3**.4所示,这段代码接收三个操作数并输出一个值 O1:

图 3.4 - 编译操作示例

图 3.4 - 编译操作示例

除了融合和减少操作外,编译模型 - 图模式的结果 - 可以专门针对某些硬件架构进行创建,以利用设备提供的所有资源和功能。这也是图模式比急切模式表现更好的原因之一。

和生活中的一切一样,每种模式都有其优点和缺点。简而言之,急切模式更容易理解和调试,并且在开始运行操作时没有任何延迟。另一方面,图模式执行速度更快,尽管更复杂,需要额外的初始时间来创建编译后的代码。

模型编译

现在您已经了解了急切模式和图模式,我们可以回到本节开头提出的问题:编译模型的含义是什么?

编译模型意味着将前向和后向阶段的执行模式从急切模式改变为图模式。在执行此操作时,机器学习框架提前评估所有涉及这些阶段的操作和操作数,以将它们编译成单一的代码片段。因此,请注意,当我们使用术语“编译模型”时,我们指的是编译在前向和后向阶段执行的处理流程。

但为什么我们要这样做呢?我们编译模型是为了加速其训练时间,因为编译后的代码往往比在急切模式下执行的代码运行速度更快。正如我们将在接下来的几节中看到的,性能提升取决于各种因素,如用于训练模型的 GPU 的计算能力。

然而需要注意的是,并非所有硬件平台和模型都能保证性能提升。在许多情况下,由于需要额外的编译时间,图模式的性能可能与急切模式相同甚至更差。尽管如此,我们应始终考虑编译模型以验证最终的性能提升,尤其是在使用新型 GPU 设备时。

此时,你可能会想知道 PyTorch 支持哪种执行模式。PyTorch 的默认执行模式是急切模式,因为它“更易于使用并且更适合机器学习研究人员”,正如 PyTorch 网站上所述。然而,PyTorch 也支持图模式!在 2.0 版本之后,PyTorch 通过编译 API本地支持图执行模式。

在这个新版本之前,我们需要使用第三方工具和库来启用 PyTorch 上的图模式。然而,随着编译 API 的推出,现在我们可以轻松地编译模型。让我们学习如何使用这个 API 来加速我们模型的训练阶段。

使用 Compile API

我们将从将 Compile API 应用于我们广为人知的 CNN 模型和 Fashion-MNIST 数据集的基本用法开始学习。之后,我们将加速一个更重的用于分类 CIFAR-10 数据集中图像的模型。

基本用法

不是描述 API 的组件并解释一堆可选参数,我们来看一个简单的例子,展示这个能力的基本用法。以下代码片段使用 Compile API 来编译在前几章中介绍的 CNN 模型:

model = CNN()graph_model = torch.compile(model)

注意

此部分展示的完整代码可在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter03/cnn-graph_mode.ipynb处获取。

要编译一个模型,我们需要调用一个名为compile的函数,并将模型作为参数传递进去。对于这个 API 的基本用法,没有其他必要的内容了。compile函数返回一个对象,在第一次调用时将被编译。其余代码保持和以前完全一样。

我们可以设置以下环境变量来查看编译过程是否发生:

import osos.environ['TORCH_COMPILE_DEBUG'] = "1"

如果是这样,我们将会看到很多消息,如下所示:

[INFO] Step 1: torchdynamo start tracing forward[DEBUG] TRACE LOAD_FAST self []
[DEBUG] TRACE LOAD_ATTR layer1 [NNModuleVariable()]
[DEBUG] TRACE LOAD_FAST x [NNModuleVariable()]
[DEBUG] TRACE CALL_FUNCTION 1 [NNModuleVariable(), TensorVariable()]

另一种验证我们成功编译模型的方法是使用 PyTorch Profiler API 来分析前向阶段:

from torch.profiler import profile, ProfilerActivityactivities = [ProfilerActivity.CPU]
prof = profile(activities=activities)
input_sample, _ = next(iter(train_loader))
prof.start()
model(input_sample)
prof.stop()
print(prof.key_averages().table(sort_by="self_cpu_time_total", 
                                row_limit=10))

如果模型成功编译,分析结果将显示一个标记为CompiledFunction的任务,如以下输出的第一行所示:

CompiledFunction: 55.50%aten::mkldnn_convolution: 30.36%
aten::addmm: 8.25%
aten::convolution: 1.06%
aten::as_strided: 0.63%
aten::empty_strided: 0.59%
aten::empty: 0.43%
aten::expand: 0.27%
aten::resolve_conj: 0.20%
detach: 0.20%
aten::detach: 0.16%

前述输出显示,CompiledFunctionaten::mkldnn_convolution几乎占据了执行前向阶段所需时间的 86%。如果我们在急切模式下分析模型,可以轻松识别哪些操作已被融合并转换为CompiledFunction

aten::mkldnn_convolution: 38.87%aten::max_pool2d_with_indices: 27.31%
aten::addmm: 17.89%
aten::clamp_min: 6.63%
aten::convolution: 1.88%
aten::relu: 0.87%
aten::conv2d: 0.83%
aten::reshape: 0.57%
aten::empty: 0.52%
aten::max_pool2d: 0.52%
aten::linear: 0.44%
aten::t: 0.44%
aten::transpose: 0.31%
aten::expand: 0.26%
aten::as_strided: 0.13%
aten::resolve_conj: 0.00%

通过评估急切和图模式的分析输出,我们可以看到编译过程将九个操作融合到CompiledFunction操作中,如图 3**.5所示。正如本例所示,编译过程无法编译所有涉及前向阶段的操作。这是由于诸如数据依赖等多种原因造成的:

图 3.5 – 编译函数中包含的一组操作

图 3.5 – 编译函数中包含的一组操作

您可能会对性能改进感到好奇。毕竟,这就是我们来的目的!您还记得我们在本章开头讨论的并非在所有情况下都能实现性能改进的内容吗?嗯,这就是其中之一。

图 3*.6*显示了在急切模式和图模式下运行的 CNN 模型每个训练时期的执行时间。正如我们所见,所有时期的执行时间在图模式下都比急切模式高。此外,图模式的第一个时期明显比其他时期慢,因为在那一刻执行了编译过程:

图 3.6 – 急切模式和图模式下 CNN 模型每个训练时期的执行时间

图 3.6 – 急切模式和图模式下 CNN 模型每个训练时期的执行时间

急切和图模式下模型训练的总时间分别为 118 和 140 秒。因此,编译模型比默认执行模式慢了 18%。

令人沮丧,对吧?是的,确实如此。然而,请记住我们的 CNN 只是一个玩具模型,因此真正改善性能的空间并不大。此外,这些实验是在非 GPU 环境中执行的,尽管编译过程往往在 GPU 设备上能够产生更好的结果。

话虽如此,让我们进入下一节,看看通过编译 API 实现显著的性能改进。

给我一个真正的挑战——训练一个更重的模型!

为了看到这种能力的全部效果,我们将其应用于一个更复杂的案例。我们这次的实验对象是torchvision模块。

注意

本节中显示的完整代码可在 github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter03/densenet121_cifar10.ipynb 获取。

CIFAR-10 是一个经典的图像分类数据集,包含 60,000 张大小为 32x32 的彩色图像。这些图像属于 10 个不同的类别,这也解释了数据集名称中的后缀“10”。

尽管每个数据集图像的尺寸为 32x32,但将它们调整大小以在模型训练中取得更好的结果是一种好方法。因此,我们将每个图像调整为 224x224,但保留原始的三个通道以表示 RGB 颜色编码。

我们使用以下超参数在 DenseNet121 模型上进行了这个实验:

  • Batch size:64

  • Epochs:50

  • Learning rate:0.0001

  • Weight decay:0.005

  • Criterion:交叉熵

  • Optimizer:Adam

与以前在 CNN 模型上进行的实验不同,这次测试是在具有新型 Nvidia A100 GPU 的环境中执行的。该 GPU 的计算能力等于 8.0,满足了 PyTorch 要求的利用编译 API 获得更好结果的条件。

注意

计算能力是 NVIDIA 分配给其 GPU 的评分。计算能力越高,GPU 提供的计算能力就越高。PyTorch 的官方文档表示,编译 API 在具有等于或高于 8.0 的计算能力的 GPU 上产生更好的结果。

以下代码片段显示了如何加载和启用 DenseNet121 模型进行训练:

from torchvision import modelsdevice = "cuda"
weights = models.DenseNet121_Weights.DEFAULT
net = models.densenet121(weights=weights)
net.to(device)
net.train()

在这种情况下,使用编译 API 的用法几乎与之前一样,只有编译行中有一个轻微的变化:

model = torch.compile(net, mode="reduce-overhead")

正如您所见,我们调用的是与前一个例子中使用的不同的编译模式。我们在CNNxFashion-MNIST案例中没有使用“mode”参数,因此编译函数采用了默认的编译模式。编译模式改变了整个工作流的行为,使我们能够调整生成的代码,以使其适应特定的情景或需求。

图 3*.7*显示了三种可能的编译模式:

图 3.7 - 编译模式

图 3.7 - 编译模式

这是一个详细解释:

  • default:在编译时间和模型性能之间取得平衡。顾名思义,这是该函数的默认编译模式。在许多情况下,此选项可能提供良好的结果。

  • reduce-overhead:这适用于小批量 - 这是我们目前的情况。此模式减少了将批量样本加载到内存并在计算设备上执行前向和后向阶段的开销。

  • max-autotune:可能的最优化代码。编译器需要尽可能多的时间来生成在目标机器或设备上运行的最佳优化代码。因此,与其他模式相比,编译模型所需的时间较长,这可能在许多实际情况下使此选项不可行。即便如此,该模式仍然对实验目的很有趣,因为我们可以评估并理解使该模型比使用默认和 reduce-overhead 模式生成的其他模型更好的特征。

在运行热切和已编译模型的训练阶段后,我们得到了列在表 3.1中的结果:

Eager 模型已编译模型
总体训练时间(s)2,2641,443
第一轮执行时间 (s)47146
中位数轮次执行时间 (s)4526
准确率 (%)74.2674.38

表 3.1 – 急切和编译模型训练结果

结果显示,我们训练编译模型比其急切版本快了 57%。预期地,第一轮执行编译版本花费的时间要比急切模式多得多,因为编译过程是在那时进行的。另一方面,剩余轮次的执行时间中位数从 45 降至 26,大约快了 1.73 倍。请注意,我们在不牺牲模型质量的情况下获得了这种性能改进,因为两个模型都达到了相同的准确率。

编译 API 将 DenseNet121xCIFAR-10 案例的训练阶段加速了近 60%。但为什么这种能力不能同样适用于 CNNxFashion-MNIST 示例呢?实质上,答案在于两个问题:计算负担和计算资源。让我们逐一来看:

  • 计算负担: DenseNet121 模型有 7,978,856 个参数。与我们的 CNN 模型的 1,630,090 个权重相比,前者几乎是后者的四倍。此外,CIFAR-10 数据集的一个调整大小样本的维度为 244x244x3,远高于 Fashion-MNIST 样本的维度。正如在 第一章 中讨论的那样,拆解训练过程中的模型复杂性直接与训练阶段的计算负担有关。有了如此高的计算负担,我们有更多加速训练阶段的机会。否则,这就像从光亮表面上除去一粒灰尘一样;没有什么可做的。

  • 计算资源: 我们在 CPU 环境中执行了之前的实验。然而,正如 PyTorch 官方文档所述,当在 GPU 设备上执行时,Compile API 倾向于在具有高于 8.0 的计算能力的 GPU 设备上提供更好的结果。这正是我们在 DenseNet121xCIFAR-10 案例中所做的,即训练过程在 GPU Nvidia A100 上执行。

简而言之,当使用 A100 训练 DenseNet121xCIFAR-10 案例时,计算负担与计算资源完美匹配。这种良好的匹配是通过编译 API 改善性能的关键。

现在,您已经确信将这一资源纳入性能加速工具包是一个好主意,让我们来了解编译 API 在幕后是如何工作的。

编译 API 在幕后是如何工作的?

编译 API 恰如其名:它是访问 PyTorch 提供的一组功能的入口,用于从急切执行模式转换为图执行模式。除了中间组件和流程之外,我们还有编译器,它是负责完成最终工作的实体。有半打编译器可用,每个都专门用于为特定架构或设备生成优化代码。

以下部分描述了编译过程中涉及的步骤以及使所有这些成为可能的组件。

编译工作流程和组件

到此为止,我们可以想象,编译过程比在我们的代码中调用一条单行要复杂得多。为了将急切模型转换为编译模型,编译 API 执行三个步骤,即图获取、图降低和图编译,如图 3.8所示:

图 3.8 – 编译工作流程

图 3.8 – 编译工作流程

让我们讨论每个步骤:

  1. 图获取:编译工作流程的第一步,图获取负责捕获模型定义,并将其转换为在前向和反向阶段执行的原始操作的代表性图形。

  2. 图降低:拥有图形表示后,现在是时候通过融合、组合和减少操作来简化和优化过程。图形越简单,执行时间就越短。

  3. 图编译:最后一步是为给定的目标设备生成代码,例如不同供应商和架构的 CPU 和 GPU,甚至是另一种设备,如张量处理单元(TPU)

PyTorch 依赖于两个主要组件来执行这些步骤。TorchDynamo执行图获取,而后端编译器执行图降低和编译,如图 3.9所示:

图 3.9 – 编译工作流程的组件

图 3.9 – 编译工作流程的组件

TorchDynamo 使用在 CPython 中实现的新功能执行图获取。这种功能称为帧评估 API,并在PEP 523中定义。简而言之,TorchDynamo 在 Python 字节码执行前捕获它,以创建一个表示由该函数或模型执行的操作的图形表示。

注意

PEP代表Python Enhancement Proposal。这份文档向 Python 社区介绍了新功能、相关变更以及编写 Python 代码的一般指导。

之后,TorchDynamo 调用编译器后端,它负责将图形有效地转换为可以在硬件平台上运行的代码片段。编译器后端执行编译工作流程的图降低和图编译步骤。我们将在下一小节更详细地介绍这个组件。

后端

Compile API 支持使用半打后端编译器。 PyTorch 的默认后端编译器是TorchInductor,它通过 OpenMP 框架和 Triton 编译器分别为 CPU 和 GPU 生成优化代码。

注意

本节中显示的完整代码可在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter03/backends.ipynb找到。

要指定编译器后端,我们必须在torch.compile函数中设置参数 backend。如果该参数被省略,Compile API 将使用 TorchInductor。以下一行选择cudagraphs作为编译器后端:

model = torch.compile(net, backend="cudagraphs")

通过运行以下命令,我们可以轻松发现给定环境支持的后端:

torch._dynamo.list_backends()# available backends
['aot_ts_nvfuser',
 'cudagraphs',
 'inductor',
 'ipex',
 'nvprims_nvfuser',
 'onnxrt',
 'tvm']

此列表显示在我们实验中使用的环境中有七个可用的后端。请注意,通过list_backends()返回的后端,尽管受当前 PyTorch 安装支持,但不一定准备好使用。这是因为一些后端可能需要额外的模块、包和库来执行。

在我们的环境中可用的七个后端中,只有三个能够及时运行。表 3.2显示了我们测试 DenseNet121xCIFAR-10 案例并使用aot_ts_nvfusercudagraphsinductor进行编译时取得的结果:

aot_ts_nvfusercudagraphsinductor
整体训练时间 (s)2,4742,2901,407
第一个 epoch 执行时间 (s)14286140
中位数 epoch 执行时间 (s)464425
准确率 (%)74.6877.5779.90

表 3.2 - 不同后端编译器的结果

结果显示,TorchInductor 比其他后端效果更好,因为它使训练阶段快了 63%。尽管 TorchInductor 在这种情况和场景下呈现出最佳结果,但测试所有环境中可用的后端始终是有意义的。此外,一些后端,如onnxrttvm,专门用于生成适合推理的模型。

下一节提供了一些问题,帮助您巩固本章学到的内容。

测验时间开始!

让我们通过回答一些问题来回顾本章学到的知识。最初,请尝试回答这些问题而不参考资料。

注意

所有这些问题的答案都可以在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/quiz/chapter03-answers.md找到。

在开始本次测验之前,请记住这不是一个测试!本节旨在通过复习和巩固本章内容来补充您的学习过程。

选择以下问题的正确选项:

  1. PyTorch 的两种执行模式是什么?

    1. 水平和垂直模式。

    2. 急切和图模式。

    3. 急切和分布模式。

    4. 急切和自动模式。

  2. PyTorch 在哪种执行模式下会立即执行代码中出现的操作?

    1. 图模式。

    2. 急切模式。

    3. 分布模式。

    4. 自动模式。

  3. PyTorch 在哪种执行模式下评估完整的操作集,寻找优化机会?

    1. 图模式。

    2. 急切模式。

    3. 分布模式。

    4. 自动模式。

  4. 使用 PyTorch 编译模型意味着在训练过程的哪个阶段从急切模式切换到图模式执行?

    1. 前向和优化。

    2. 前向和损失计算。

    3. 前向和后向。

    4. 前向和训练。

  5. 关于在急切模式和图模式下执行第一个训练时期的时间,我们可以做出什么断言?

    1. 在急切模式和图模式下执行第一个训练时期的时间总是相同的。

    2. 在图模式下执行第一个训练时期的时间总是小于在急切模式下执行。

    3. 在图模式下执行第一个训练时期的时间可能高于在急切模式下执行。

    4. 在急切模式下执行第一个训练时期的时间可能高于在图模式下执行。

  6. 编译 API 执行的编译工作流程包括哪些阶段?

    1. 图前向、图后向和图编译。

    2. 图获取、图后向和图编译。

    3. 图获取、图降低和图优化。

    4. 图获取、图降低和图编译。

  7. TorchDynamo 是 Compile API 的一个组件,执行哪个阶段?

    1. 图后向。

    2. 图获取。

    3. 图降低。

    4. 图优化。

  8. TorchInductor 是 PyTorch Compile API 的默认编译器后端。其他编译器后端是哪些?

    1. OpenMP 和 NCCL。

    2. OpenMP 和 Triton。

    3. Cudagraphs 和 IPEX。

    4. TorchDynamo 和 Cudagraphs。

现在,让我们总结本章的要点。

总结。

在本章中,您了解了 Compile API,这是在 PyTorch 2.0 中推出的一项新功能,用于编译模型 - 即从急切模式切换到图模式的操作模式。在某些硬件平台上,执行图模式的模型往往训练速度更快。要使用 Compile API,我们只需在原始代码中添加一行代码。因此,这是加速我们模型训练过程的简单而强大的技术。

在下一章节中,您将学习如何安装和配置特定库,如 OpenMP 和 IPEX,以加快我们模型的训练过程。

第四章:使用专门的库

没有人需要自己做所有的事情。PyTorch 也不例外!我们已经知道 PyTorch 是构建深度学习模型最强大的框架之一。然而,在模型构建过程中涉及许多其他任务时,PyTorch 依赖于专门的库和工具来完成工作。

在本章中,我们将学习如何安装、使用和配置库来优化基于 CPU 的训练和多线程。

比学习本章中呈现的技术细节更重要的是捕捉它所带来的信息:通过使用和配置 PyTorch 依赖的专门库,我们可以改善性能。在这方面,我们可以寻找比本书中描述的选项更多的选择。

作为本章的一部分,您将学到以下内容:

  • 理解使用 OpenMP 进行多线程处理的概念

  • 学习如何使用和配置 OpenMP

  • 理解 IPEX – 一种用于优化在 Intel 处理器上使用 PyTorch 的 API

  • 理解如何安装和使用 IPEX

技术要求

您可以在本书的 GitHub 存储库中找到本章提到的所有示例代码,网址为github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main

您可以访问您喜欢的环境来执行此笔记本,例如 Google Colab 或 Kaggle。

使用 OpenMP 进行多线程处理

OpenMP是一个库,通过使用多线程技术,利用多核处理器的全部性能来并行化任务。在 PyTorch 的上下文中,OpenMP 被用于并行化在训练阶段执行的操作,以及加速与数据增强、归一化等相关的预处理任务。

多线程是这里的一个关键概念,要了解 OpenMP 是如何工作的,请跟我进入下一节来理解这项技术。

什么是多线程?

多线程是在多核系统中并行化任务的一种技术,这种系统配备有多核处理器。如今,任何计算系统都配备有多核处理器;智能手机、笔记本电脑甚至电视都配备了具有多个处理核心的 CPU。

举个例子,让我们看看我现在用来写这本书的笔记本。我的笔记本配备了一颗 Intel i5-8265U 处理器,具有八个核心,如图 4**.1所示:

图 4.1 – 物理核心和逻辑核心

图 4.1 – 物理核心和逻辑核心

现代处理器具有物理核心和逻辑核心。物理核心是完整的独立处理单元,能够执行任何计算。逻辑核心是从物理核心的空闲资源实例化出来的处理实体。因此,物理核心比逻辑核心提供更好的性能。因此,我们应该始终优先使用物理单元而不是逻辑单元。

尽管如此,从操作系统的角度来看,物理核心和逻辑核心没有区别(即,操作系统看到的核心总数,无论它们是物理还是逻辑的)。

重要提示

提供逻辑核心的技术称为同时多线程。每个厂商对于这项技术都有自己的商业名称。例如,Intel 称其为超线程。关于这个主题的详细信息超出了本书的范围。

我们可以使用 Linux 的lscpu命令来检查处理器的详细信息:

[root@laptop] lscpuArchitecture: x86_64
  CPU op-mode(s): 32-bit, 64-bit
  Address sizes: 39 bits physical, 48 bits virtual
  Byte Order: Little Endian
CPU(s): 8
  On-line CPU(s) list: 0-7
Vendor ID: GenuineIntel
  BIOS Vendor ID: Intel(R) Corporation
  Model name: Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz
    BIOS CPU family: 205
    CPU family: 6
    Model: 142
    Thread(s) per core: 2
    Core(s) per socket: 4
    Socket(s): 1
    Stepping: 12
    CPU(s) scaling MHz:  79%
    CPU max MHz: 3900,0000
    CPU min MHz: 400,0000
    BogoMIPS: 3600.00

输出显示有关处理器的大量信息,例如核心数、插槽数、频率、架构、厂商名称等等。让我们检查与我们案例最相关的字段:

  • CPU(s):系统上可用的物理和逻辑核心总数。“CPU”在这里被用作“核心”的同义词。

  • 在线 CPU(s)列表:系统上可用核心的识别。

  • 1

  • 1,系统只有物理核心。否则,系统既有物理核心又有逻辑核心。

  • 每个插槽核心数:每个多核处理器上可用的物理核心数量。

重要提示

我们可以使用lscpu命令获取运行硬件上可用的物理核心和逻辑核心数量。正如您将在接下来的部分中看到的那样,这些信息对于优化 OpenMP 的使用至关重要。

现代服务器拥有数百个核心。面对如此强大的计算能力,我们必须找到一种合理利用的方法。这就是多线程发挥作用的地方!

多线程技术涉及创建和控制一组线程来协作并完成给定任务。这些线程分布在处理器核心上,使得运行程序可以使用不同的核心来处理计算任务的不同部分。因此,多个核心同时处理同一任务以加速其完成。

线程是由进程创建的操作系统实体。由给定进程创建的一组线程共享相同的内存地址空间。因此,线程之间的通信比进程容易得多;它们只需读取或写入某个内存地址的内容。另一方面,进程必须依靠更复杂的方法,如消息交换、信号、队列等等。这就是为什么我们更倾向于使用线程来并行化任务而不是进程的原因:

图 4.2 – 线程和进程

然而,使用线程的好处是有代价的:我们必须小心我们的线程。由于线程通过共享内存进行通信,当多个线程试图在同一内存区域上写入时,它们可能会遇到竞争条件。此外,程序员必须保持线程同步,以防止一个线程无限期地等待另一个线程的某个结果或操作。

重要提示

如果线程和进程的概念对您来说很新,请先观看 YouTube 上的以下视频,然后再继续下一节:youtu.be/Dhf-DYO1K78。如果您需要更深入的资料,可以阅读 Roderick Bauer 撰写的文章,链接在medium.com/@rodbauer/understanding-programs-processes-and-threads-fd9fdede4d88

简而言之,手动编写线程(即自行编写)是一项艰苦的工作。然而,幸运的是,有 OpenMP 在这里帮忙。因此,让我们学习如何与 PyTorch 一起使用它,加速我们的机器学习模型训练阶段。

使用和配置 OpenMP

OpenMP 是一个能够封装和抽象许多与编写多线程程序相关的缺点的框架。通过这个框架,我们可以通过一组函数和原语并行化我们的顺序代码。在谈论多线程时,OpenMP 是事实上的标准。这也解释了为什么 PyTorch 将 OpenMP 作为默认的后端来并行化任务。

严格来说,我们不需要更改 PyTorch 的代码就可以使用 OpenMP。尽管如此,有一些配置技巧可以提高训练过程的性能。让我们在实践中看看!

重要提示

此部分中显示的完整代码可在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter04/baseline-cnn_cifar10.ipynbgithub.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter04/gomp-cnn_cifar10.ipynb处查看。

首先,我们将运行与《第二章》Chapter 2中相同的代码,更快地训练模型,以使用 CIFAR-10 数据集训练 CNN 模型。环境配置有 GNU OpenMP 4.5,并且拥有一个总共 32 个核心的 Intel 处理器,一半是物理核心,一半是逻辑核心。

要检查当前环境中使用的 OpenMP 版本和线程数,我们可以执行torch.__config__.parallel_info()函数:

ATen/Parallel:    at::get_num_threads() : 16
    at::get_num_interop_threads() : 16
OpenMP 201511 (a.k.a. OpenMP 4.5)
    omp_get_max_threads() : 16
Intel(R) oneAPI Math Kernel Library Version 2022.2
    mkl_get_max_threads() : 16
Intel(R) MKL-DNN v2.7.3
std::thread::hardware_concurrency() : 32
Environment variables:
    OMP_NUM_THREADS : [not set]
    MKL_NUM_THREADS : [not set]
ATen parallel backend: OpenMP

输出的最后一行确认了 OpenMP 是配置在环境中的并行后端。我们还可以看到它是 OpenMP 版本 4.5,以及设置的线程数和为两个环境变量配置的值。hardware_concurrency()字段显示了一个值为32,表明环境能够运行多达 32 个线程,因为系统最多有 32 个核心。

此外,输出提供了关于get_num_threads()字段的信息,这是 OpenMP 使用的线程数。OpenMP 的默认行为是使用与物理核心数量相等的线程数。因此,在这种情况下,默认线程数为 16。

训练阶段花费 178 秒来运行 10 个 epochs。在训练过程中,我们可以使用htop命令验证 OpenMP 如何将线程绑定到核心。在我们的实验中,PyTorch/OpenMP 进行了一种配置,如图 4**.3所描述的。

图 4.3 – 默认的 OpenMP 线程分配

图 4.3 – 默认的 OpenMP 线程分配

OpenMP 将这组 16 个线程分配给了 8 个物理核心和 8 个逻辑核心。如前一节所述,物理核心比逻辑核心提供了更好的性能。即使有物理核心可用,OpenMP 也使用了逻辑核心来执行 PyTorch 线程的一半。

乍一看,即使有物理核心可用,决定使用逻辑核心也可能听起来很愚蠢。然而,我们应该记住,处理器是整个计算系统使用的 – 也就是说,它们用于除我们的训练过程之外的其他任务。因此,操作系统与 OpenMP 应该尽量对所有的需求任务公平 – 也就是说,它们也应该提供使用物理核心的机会。

尽管 OpenMP 的默认行为如此,我们可以设置一对环境变量来改变 OpenMP 分配、控制和管理线程的方式。下面的代码片段附加到我们的 CNN/CIFAR-10 代码的开头,修改了 OpenMP 操作以提高性能:

os.environ['OMP_NUM_THREADS'] = "16"os.environ['OMP_PROC_BIND'] = "TRUE"
os.environ['OMP_SCHEDULE'] = "STATIC"
os.environ['GOMP_CPU_AFFINITY'] = "0-15"

这些行直接从 Python 代码设置了四个环境变量。在解释这些变量的含义之前,让我们先看看它们所提供的性能改进。

使用 CIFAR-10 数据集训练 CNN 模型的时间从 178 秒减少到 114 秒,显示出 56%的性能提升!在代码中没有其他更改!在这个执行过程中,OpenMP 创建了一个线程分配,如图 4**.4所描述的。

图 4.4 – 优化的 OpenMP 线程分配

图 4.4 – 优化的 OpenMP 线程分配

正如您在图 4**.4中所见,OpenMP 使用了所有 16 个物理核心,但未使用逻辑核心。我们可以说,将线程绑定到物理核心是性能增加的主要原因。

让我们详细分析在这个实验中配置的环境变量集合,以了解它们如何有助于改进我们训练过程的性能:

  • OMP_NUM_THREADS:这定义了 OpenMP 使用的线程数。我们将线程数设置为 16,这与 OpenMP 默认设置的值完全相同。虽然此配置未在我们的场景中带来任何变化,但了解这个选项以控制 OpenMP 使用的线程数是至关重要的。特别是在同一服务器上同时运行多个训练过程时。

  • OMP_PROC_BIND:这确定了线程亲和性策略。当设置为TRUE时,这个配置告诉 OpenMP 在整个执行过程中保持线程在同一个核心上运行。这种配置防止线程从核心中移动,从而最小化性能问题,比如缓存未命中。

  • OMP_SCHEDULE:这定义了调度策略。因为我们希望静态地将线程绑定到核心,所以应将此变量设置为静态策略。

  • GOMP_CPU_AFFINITY:这指示 OpenMP 用于执行线程的核心或处理器。为了只使用物理核心,我们应指定与系统中物理核心对应的处理器标识。

这些变量的组合极大加速了我们 CNN 模型的训练过程。简而言之,我们强制 OpenMP 仅使用物理核心,并保持线程在最初分配的同一个核心上运行。因此,我们利用了所有物理核心的计算能力,同时最小化由频繁上下文切换引起的性能问题。

重要说明

本质上,当操作系统决定中断一个进程的执行以给另一个进程使用 CPU 的机会时,上下文切换就会发生。

OpenMP 除了本章介绍的变量外,还有几个变量来控制其行为。为了检查当前的 OpenMP 配置,我们可以在运行 PyTorch 代码时将 OMP_DISPLAY_ENV 环境变量设置为 TRUE

OPENMP DISPLAY ENVIRONMENT BEGIN  _OPENMP = '201511'
  OMP_DYNAMIC = 'FALSE'
  OMP_NESTED = 'FALSE'
  OMP_NUM_THREADS = '16'
  OMP_SCHEDULE = 'STATIC'
  OMP_PROC_BIND = 'TRUE'
  OMP_PLACES = '{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12}
                ,{13},{14},{15}'
  OMP_STACKSIZE = '36668818'
  OMP_WAIT_POLICY = 'PASSIVE'
  OMP_THREAD_LIMIT = '4294967295'
  OMP_MAX_ACTIVE_LEVELS = '2147483647'
  OMP_CANCELLATION = 'FALSE'
  OMP_DEFAULT_DEVICE = '0'
  OMP_MAX_TASK_PRIORITY = '0'
OPENMP DISPLAY ENVIRONMENT END

学习每个环境变量如何改变 OpenMP 的操作是非常有趣的;因此,我们可以针对特定场景进行微调。这个输出也有助于验证环境变量的更改是否确实生效。

本节描述的实验使用了 GNU OpenMP,因为它是 PyTorch 采用的默认并行后端。然而,由于 OpenMP 实际上是一个框架,除了 GNU 提供的实现外,我们还有其他的 OpenMP 实现。其中一个实现是 Intel OpenMP,适用于 Intel 处理器环境。

然而,Intel OpenMP 是否带来了显著的改进?是否值得用它来取代 GNU 实现?请在下一节中自行查看!

使用和配置 Intel OpenMP

Intel 有自己的 OpenMP 实现,在 Intel 基础环境中承诺提供更好的性能。由于 PyTorch 默认使用 GNU 实现,我们需要采取三个步骤来使用 Intel OpenMP 替代 GNU 版本:

  1. 安装 Intel OpenMP。

  2. 加载 Intel OpenMP 库。

  3. 设置特定于 Intel OpenMP 的环境变量。

重要提示

此部分展示的完整代码可在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter04/iomp-cnn_cifar10.ipynb找到。

第一步是最简单的一步。考虑到基于 Anaconda 或支持 PIP 的 Python 环境时,我们只需执行以下其中一个命令来安装 Intel OpenMP:

pip install intel-openmpconda install intel-openmp

安装完成后,我们应优先加载 Intel OpenMP 库,而不是使用 GNU。否则,即使在系统上安装了 Intel OpenMP,PyTorch 仍将继续使用默认 OpenMP 安装的库。

重要提示

如果我们不使用 PIP 或基于 Anaconda 的环境,我们可以自行安装它。这个过程需要编译 Intel OpenMP,然后在环境中进一步安装它。

我们通过在运行代码之前设置LD_PRELOAD环境变量来执行此配置:

export LD_PRELOAD=/opt/conda/lib/libiomp5.so:$LD_PRELOAD

在这些实验所使用的环境中,Intel OpenMP 库位于/opt/conda/lib/libiomp5.soLD_PRELOAD环境变量允许在默认加载配置之前强制操作系统加载库。

最后,我们需要设置一些与 Intel OpenMP 相关的环境变量:

import osos.environ['OMP_NUM_THREADS'] = "16"
os.environ['KMP_AFFINITY'] = "granularity=fine,compact,1,0"
os.environ['KMP_BLOCKTIME'] = "0"

OMP_NUM_THREADS与 GNU 版本具有相同的含义,而KMP_AFFINITYKMP_BLOCKTIME则是 Intel OpenMP 的独有功能:

  • KMP_AFFINITY: 这定义了线程的分配策略。当设置为granularity=fine,compact,1,0时,Intel OpenMP 会将线程绑定到物理核心,尽力在整个执行过程中保持这种方式。因此,在使用 Intel OpenMP 时,我们不需要像 GNU 实现那样传递一个物理核心列表来强制使用物理处理器。

  • KMP_BLOCKTIME: 这确定线程在完成任务后应等待休眠的时间。当设置为零时,线程在完成工作后立即进入休眠状态,从而最小化因等待另一个任务而浪费处理器周期。

类似于 GNU 版本,当OMP_DISPLAY_ENV变量设置为TRUE时,Intel OpenMP 也会输出当前配置(简化的输出示例):

OPENMP DISPLAY ENVIRONMENT BEGIN   _OPENMP='201611'
  [host] OMP_AFFINITY_FORMAT='OMP: pid %P tid %i thread %n bound to OS 
                              proc set {%A}'
  [host] OMP_ALLOCATOR='omp_default_mem_alloc'
  [host] OMP_CANCELLATION='FALSE'
  [host] OMP_DEBUG='disabled'
  [host] OMP_DEFAULT_DEVICE='0'
  [host] OMP_DISPLAY_AFFINITY='FALSE'
  [host] OMP_DISPLAY_ENV='TRUE'
  [host] OMP_DYNAMIC='FALSE'
  [host] OMP_MAX_ACTIVE_LEVELS='1'
  [host] OMP_MAX_TASK_PRIORITY='0'
  [host] OMP_NESTED: deprecated; max-active-levels-var=1
  [host] OMP_NUM_TEAMS='0'
  [host] OMP_NUM_THREADS='16'
OPENMP DISPLAY ENVIRONMENT END

为了比较 Intel OpenMP 带来的性能,我们以 GNU 实现提供的结果作为基准。使用 CIFAR-10 数据集训练 CNN 模型的时间从 114 秒减少到 102 秒,性能提升约为 11%。尽管这不如第一次实验那样令人印象深刻,但性能的提升仍然很有趣。此外,请注意,我们可以通过使用其他模型、数据集和计算环境获得更好的结果。

总结一下,使用本节中展示的配置,我们的训练过程速度快了近 1.7 倍。为了实现这种改进,并不需要修改代码;只需在环境级别直接进行配置即可。

在接下来的部分中,我们将学习如何安装和使用 Intel 提供的 API,以加速 PyTorch 在其处理器上的执行。

优化 Intel CPU 使用 IPEX

IPEX 代表 Intel extension for PyTorch,是由 Intel 提供的一组库和工具,用于加速机器学习模型的训练和推理。

IPEX 是 Intel 强调 PyTorch 在机器学习框架中重要性的明显标志。毕竟,Intel 在设计和维护专门为 PyTorch 创建的 API 上投入了大量精力和资源。

有趣的是,IPEX 强烈依赖于 Intel oneAPI 工具集提供的库。oneAPI 包含特定于机器学习应用的库和工具,如 oneDNN,以及加速应用程序(如 oneTBB)的其他工具。

重要提示

本节展示的完整代码可在 github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter04/baseline-densenet121_cifar10.ipynbgithub.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter04/ipex-densenet121_cifar10.ipynb 处找到。

让我们学习如何在我们的 PyTorch 代码中安装和使用 IPEX。

使用 IPEX

IPEX 不会默认与 PyTorch 一起安装;我们需要单独安装它。安装 IPEX 的最简单方法是使用 PIP,与我们在上一节中使用 OpenMP 的方式类似。因此,在 PIP 环境中安装 IPEX,只需执行以下命令:

pip install intel_extension_for_pytorch

安装完 IPEX 后,我们可以继续进行 PyTorch 的默认安装。一旦 IPEX 可用,我们就可以将其整合到我们的 PyTorch 代码中。第一步是导入 IPEX 模块:

import intel_extension_for_pytorch as ipex

使用 IPEX 非常简单。我们只需将我们的模型和优化器用 ipex.optimize 函数包装起来,让 IPEX 完成其余工作。ipex.optimize 函数返回一个经过优化的模型和优化器(如 SGD、Adam 等),用于训练模型。

为了看到 IPEX 提供的性能改进,让我们使用 DenseNet121 模型和 CIFAR-10 数据集进行测试(我们在前几章介绍过它们)。

我们的基准执行涉及使用 CIFAR-10 数据集在 10 个 epochs 上训练 DenseNet121。为了公平起见,我们使用了 Intel OpenMP,因为我们使用的是基于 Intel 的环境。但在这种情况下,我们没有改变 KMP_BLOCKTIME 参数。

import osos.environ['OMP_NUM_THREADS'] = "16"
os.environ['KMP_AFFINITY'] = "granularity=fine,compact,1,0"

基准执行完成 10 个 epochs 花费了 1,318 秒,并且结果模型的准确率约为 70%。

正如之前所述,使用 IPEX 非常简单;我们只需在基准代码中添加一行代码:

model, optimizer = ipex.optimize(model, optimizer=optimizer)

虽然 ipex.optimize 可以接受其他参数,但通常以这种方式调用已经足够满足我们的需求。

我们的 IPEX 代码花了 946 秒来执行 DenseNet121 模型的训练过程,性能提升了近 40%。除了在代码开头配置的环境变量和使用的那一行之外,原始代码没有进行任何其他更改。因此,IPEX 只通过一个简单的修改加速了训练过程。

乍一看,IPEX 看起来类似于我们在第三章中学习的 Compile API,编译模型。两者都需要添加一行代码并使用代码包装的概念。然而,相似之处止步于此!与 Compile API 不同,IPEX 不会编译模型;它会用自己的实现替换一些默认的 PyTorch 操作。

跟我来到下一节,了解 IPEX 的内部工作原理。

IPEX 是如何在内部工作的?

要理解 IPEX 的内部工作原理,让我们分析基准代码,检查训练过程使用了哪些操作。以下输出显示训练过程执行的前 10 个消耗最大的操作:

aten::convolution_backward: 27.01%aten::mkldnn_convolution: 12.44%
aten::native_batch_norm_backward: 8.85%
aten::native_batch_norm: 7.06%
Optimizer.step#Adam.step: 6.80%
aten::add_: 3.75%
aten::threshold_backward: 3.21%
aten::add: 2.75%
aten::mul_: 2.45%
aten::div: 2.19%

我们 DenseNet121 和 CIFAR-10 的基准代码执行了那些常用于卷积神经网络的操作,例如 convolution_backward。这一点毫不意外。

看看 IPEX 代码的分析输出,验证 IPEX 对我们基准代码做出了哪些改变:

torch_ipex::convolution_backward: 32.76%torch_ipex::convolution_forward_impl: 13.22%
aten::native_batch_norm: 7.98%
aten::native_batch_norm_backward: 7.08%
aten::threshold_backward: 4.40%
aten::add_: 3.92%
torch_ipex::adam_fused_step: 3.14%
torch_ipex::cat_out_cpu: 2.39%
aten::empty: 2.18%
aten::clamp_min_: 1.48%

首先要注意的是某些操作上的新前缀。除了表示默认 PyTorch 操作库的 aten 外,我们还有 torch_ipex 前缀。torch_ipex 前缀指示了 IPEX 提供的操作。例如,基准代码使用了 aten 提供的 convolution_backward 操作,而优化后的代码则使用了 IPEX 提供的操作。

正如您所见,IPEX 并没有替换每一个操作,因为它没有所有 aten 操作的优化版本。这种行为是预期的,因为有些操作已经是最优化的形式。在这种情况下,试图优化已经优化的部分是没有意义的。

图 4*.5* 总结了默认 PyTorch 代码与 IPEX 和 Compile API 优化版本之间的差异:

图 4.5 – IPEX 和 Compile API 生成的默认和优化代码之间的差异

图 4.5 – IPEX 和 Compile API 生成的默认和优化代码之间的差异

不像编译 API,IPEX 不会创建一个庞大的编译代码块。因此,通过ipex.optimize执行的优化过程要快得多。另一方面,编译后的代码往往会提供更好的性能,正如我们在第三章中详细讨论的那样,编译 模型

重要提示

有趣的是,我们可以将 IPEX 用作编译 API 的编译后端。通过这样做,torch.compile函数将依赖于 IPEX 来编译模型。

正如 IPEX 展示了 Intel 在 PyTorch 上所做的重大赌注,它在不断发展并接收频繁更新。因此,使用这个工具的最新版本以获得最新的改进非常重要。

下一节提供了一些问题,帮助您记住本章节学到的内容。

测验时间!

让我们通过回答几个问题来回顾本章节学到的内容。首先,尝试回答这些问题,而不查阅资料。

重要提示

所有这些问题的答案都可以在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/quiz/chapter04-answers.md找到。

在开始测验之前,请记住这根本不是一次测试!本节旨在通过复习和巩固本章节涵盖的内容来补充您的学习过程。

选择以下问题的正确选项。

  1. 多核系统可以具有以下两种类型的计算核心:

    1. 物理和活动。

    2. 物理和数字的。

    3. 物理和逻辑的。

    4. 物理和向量的。

  2. 由同一进程创建的一组线程...

    1. 可能共享相同的内存地址空间。

    2. 不共享相同的内存地址空间。

    3. 在现代系统中是不可能的。

    4. 共享相同的内存地址空间。

  3. 以下哪个环境变量可用于设置 OpenMP 使用的线程数?

    1. OMP_NUM_PROCS

    2. OMP_NUM_THREADS

    3. OMP_NUMBER_OF_THREADS

    4. OMP_N_THREADS

  4. 在多核系统中,使用 OpenMP 能够提升训练过程的性能,因为它可以...

    1. 将进程分配到主内存。

    2. 将线程绑定到逻辑核心。

    3. 将线程绑定到物理核心。

    4. 避免使用缓存内存。

  5. 关于通过 Intel 和 GNU 实现 OpenMP,我们可以断言...

    1. 两个版本的性能无差异。

    2. 当在 Intel 平台上运行时,Intel 版本可以优于 GNU 的实现。

    3. 当在 Intel 平台上运行时,Intel 版本从不优于 GNU 的实现。

    4. 无论硬件平台如何,GNU 版本始终比 Intel OpenMP 更快。

  6. IPEX 代表 PyTorch 的 Intel 扩展,并被定义为...

    1. 一组低级硬件指令。

    2. 一组代码示例。

    3. 一组库和工具。

    4. 一组文档。

  7. IPEX 采用什么策略来加速训练过程?

    1. IPEX 能够使用特殊的硬件指令。

    2. IPEX 用优化版本替换了训练过程的所有操作。

    3. IPEX 将训练过程的所有操作融合成一个单片代码。

    4. IPEX 用自己优化的实现替换了训练过程中的一些默认 PyTorch 操作。

  8. 在我们原始的 PyTorch 代码中,为使用 IPEX 需要做哪些改变?

    1. 一点儿也不需要。

    2. 我们只需导入 IPEX 模块。

    3. 我们需要导入 IPEX 模块,并使用 ipex.optimize() 方法包装模型。

    4. 我们只需使用最新的 PyTorch 版本。

让我们总结一下到目前为止我们讨论过的内容。

总结

您了解到 PyTorch 依赖于第三方库来加速训练过程。除了理解多线程的概念外,您还学会了如何安装、配置和使用 OpenMP。此外,您还学会了如何安装和使用 IPEX,这是由英特尔开发的一组库,用于优化在基于英特尔平台上执行的 PyTorch 代码的训练过程。

OpenMP 可通过使用多线程来并行执行 PyTorch 代码以加速训练过程,而 IPEX 则有助于通过优化专为英特尔硬件编写的操作来替换默认 PyTorch 库提供的操作。

在下一章中,您将学习如何创建一个高效的数据管道,以保持 GPU 在整个训练过程中处于最佳状态。

第五章:构建高效的数据管道

机器学习基于数据。简而言之,训练过程向神经网络提供大量数据,如图像、视频、声音和文本。因此,除了训练算法本身,数据加载是整个模型构建过程中的重要部分。

深度学习模型处理大量数据,如成千上万的图像和数百兆字节的文本序列。因此,与数据加载、准备和增强相关的任务可能会严重延迟整个训练过程。因此,为了克服模型构建过程中的潜在瓶颈,我们必须确保数据集样本顺畅地流入训练过程。

在本章中,我们将解释如何构建一个高效的数据管道,以确保训练过程的顺利运行。主要思路是防止训练过程因与数据相关的任务而停滞不前。

以下是本章的学习内容:

  • 理解为何拥有高效的数据管道是必要的

  • 学习如何通过内存固定来增加数据管道中的工作人员数量

  • 理解如何加速数据传输过程

技术要求

您可以在这本书的 GitHub 仓库中找到本章提到的所有代码示例,网址为github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main

您可以访问您喜欢的环境来执行这个笔记本,比如 Google Colab 或 Kaggle。

为什么我们需要一个高效的数据管道?

我们将从使您意识到拥有高效的数据管道的重要性开始本章。在接下来的几个小节中,您将了解数据管道的定义以及它如何影响训练过程的性能。

什么是数据管道?

正如您在第一章中学到的,解构训练过程,训练过程由四个阶段组成:前向、损失计算、优化和反向。训练算法在数据集样本上进行迭代,直到完成一个完整的周期。然而,我们在那个解释中排除了一个额外的阶段:数据加载

前向阶段调用数据加载以获取数据集样本来执行训练过程。更具体地说,前向阶段在每次迭代中调用数据加载过程,以获取执行当前训练步骤所需的数据,如图 5.1所示:

图 5.1 – 数据加载过程

图 5.1 – 数据加载过程

简而言之,数据加载执行三项主要任务:

  1. 加载:此步骤涉及从磁盘读取数据并将其加载到内存中。我们可以将数据加载到主内存(DRAM)或直接加载到 GPU 内存(GRAM)。

  2. 准备: 通常,我们在将数据用于训练过程之前需要对其进行准备,例如执行标准化和调整大小等操作。

  3. 增广: 当数据集很小时,我们必须通过从原始样本派生新样本来增广它。否则,神经网络将无法捕捉数据中呈现的内在知识。增广任务包括旋转、镜像和翻转图像。

通常情况下,数据加载按需执行这些任务。因此,在前向阶段调用时,它开始执行所有任务,以将数据集样本传递给训练过程。然后,我们可以将整个过程看作是一个 数据流水线,在这个流水线中,在用于训练神经网络之前对数据进行处理。

数据流水线(在 图 5.2 中以图形描述)类似于工业生产线。原始数据集样本被顺序处理和转换,直到准备好供训练过程使用:

图 5.2 – 数据流水线

图 5.2 – 数据流水线

在许多情况下,模型质量取决于对数据集进行的转换。对于小数据集来说尤其如此——几乎是必需的增广——以及由质量低劣的图像组成的数据集。

在其他情况下,我们不需要对样本进行任何修改就能达到高度精确的模型,也许只需要改变数据格式或类似的内容。在这种情况下,数据流水线仅限于从内存或磁盘加载数据集样本并将其传递给前向阶段。

无论与数据转换、准备和转换相关的任务如何,我们都需要构建一个数据流水线来供给前向阶段。在 PyTorch 中,我们可以使用 torch.utils.data API 提供的组件来创建数据流水线,如我们将在下一节中看到的那样。

如何构建数据流水线

torch.utils.data API 提供了两个组件来构建数据流水线:DatasetDataLoader(如 图 5.3 所示)。前者用于指示数据集的来源(本地文件、从互联网下载等)并定义要应用于数据集的转换集合,而后者用作从数据集获取样本的接口:

图 5.3 – DataLoader 和 Dataset 组件

图 5.3 – DataLoader 和 Dataset 组件

在实际操作中,训练过程直接与 DataLoader 对话以消耗数据集样本。因此,前向阶段在每个训练步骤中向 DataLoader 请求数据集样本。

以下代码片段展示了 DataLoader 的基本用法:

transform = transforms.Compose(transforms.Resize(255))dataset = datasets.CIFAR10(root=data_dir,
                           train=True,
                           download=True,
                           transform=transform)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=128)

以下代码片段创建了一个 DataLoader 实例,即 dataloader,以批量大小为 128 提供样本。

注意

注意,在这种情况下没有直接使用 Dataset,因为 CIFAR-10 封装了数据集创建。

在 PyTorch 中建立数据管道的其他策略也有,但DatasetDataLoader通常适用于大多数情况。

接下来,我们将学习一个低效的数据管道如何拖慢整个训练过程。

数据管道瓶颈

根据数据管道中任务的复杂性以及数据集样本的大小,数据加载可能需要一定的时间来完成。因此,我们可以控制整个构建过程的节奏。

通常情况下,数据加载在 CPU 上执行,而训练则在 GPU 上进行。由于 CPU 比 GPU 慢得多,GPU 可能会空闲,等待下一个样本以继续训练过程。数据喂养任务的复杂性越高,对训练阶段的影响越大。

图 5**.4所示,数据加载使用 CPU 处理数据集样本。当样本准备好时,训练阶段使用它们来训练网络。这个过程持续执行,直到所有训练步骤完成:

图 5.4 – 由低效数据管道引起的瓶颈

图 5.4 – 由低效数据管道引起的瓶颈

尽管这个过程乍看起来还不错,但我们浪费了 GPU 的计算能力,因为它在训练步骤之间空闲。期望的行为更接近于图 5**.5所示:

图 5.5 – 高效数据管道

图 5.5 – 高效数据管道

与前一场景不同,训练步骤之间的交错时间几乎被减少到最低,因为样本提前加载,准备好喂养在 GPU 上执行的训练过程。因此,我们在模型构建过程中总体体验到了加速。

在下一节中,我们将学习如何通过对代码进行简单的更改来加速数据加载过程。

加速数据加载

加速数据加载对于获得高效的数据管道至关重要。一般来说,以下两个改变足以完成这项工作:

  • 优化 CPU 和 GPU 之间的数据传输

  • 增加数据管道中的工作线程数量

换句话说,这些改变可能听起来比实际要难以实现。实际上,做这些改变非常简单 – 我们只需要在创建DataLoader实例时添加几个参数。我们将在以下子节中介绍这些内容。

优化 GPU 的数据传输

要将数据从主存储器传输到 GPU,反之亦然,设备驱动程序必须请求操作系统锁定一部分内存。在获得对该锁定内存的访问权限后,设备驱动程序开始将数据从原始内存位置复制到 GPU,但使用锁定内存作为缓冲区

图 5.6 – 主存储器与 GPU 之间的数据传输

图 5.6 – 主存储器与 GPU 之间的数据传输

在此过程中使用固定内存是强制性的,因为设备驱动程序无法直接从可分页内存复制数据到 GPU。 这涉及到该过程中的架构问题,解释了这种行为。 无论如何,我们可以断言,这种双重复制过程可能会对数据管道的性能产生负面影响。

注意

您可以在此处找到有关固定内存传输的更多信息:developer.nvidia.com/blog/how-optimize-data-transfers-cuda-cc/。

要解决这个问题,我们可以告诉设备驱动程序立即分配一部分固定内存,而不是像通常那样请求可分页的内存区域。 通过这样做,我们可以消除可分页和固定内存之间不必要的复制,从而大大减少 GPU 数据传输中涉及的开销,如图5**.7所示:

图 5.7 – 使用固定内存的数据传输

图 5.7 – 使用固定内存的数据传输

要在数据管道上启用此选项,我们需要在创建DataLoader时打开pin_memory标志:

dataloader = torch.utils.data.DataLoader(dataset,                                         batch_size=128,
                                         pin_memory=True)

没有其他必要的事情。 但是如果实现起来如此简单且收益颇丰,那么为什么 PyTorch 不默认启用此功能呢? 这有两个原因:

  • 请求固定内存可能失败:如 Nvidia 开发者博客所述,“固定内存分配可能失败,因此您应始终检查错误。” 因此,无法保证成功分配固定内存。

  • 内存使用增加:现代操作系统通常采用分页机制来管理内存资源。 通过使用这种策略,操作系统可以将未使用的内存页面移到磁盘以释放主存储器上的空间。 但是,固定内存分配使操作系统无法移动该区域的页面,从而破坏内存管理过程并增加实际内存使用量。

除了优化 GPU 数据传输外,我们还可以配置工作者以加速数据管道任务,如下一节所述。

配置数据管道工作者

DataLoader的默认操作模式是等待样本的DataLoader保持空闲,浪费宝贵的计算资源。 这种有害行为在重型数据管道中变得更加严重:

图 5.8 – 单工作器数据管道

图 5.8 – 单工作器数据管道

幸运的是,我们可以增加操作数据管道的进程数 - 也就是说,我们可以增加数据管道工作者的数量。 当设置为多个工作者时,PyTorch 将创建额外的进程以同时处理多个数据集样本:

图 5.9 – 多工作器数据管道

图 5.9 – 多工作器数据管道

图 5**.9所示,DataLoader 在请求新样本时会立即接收Sample 2,这是因为Worker 2已开始异步并同时处理该样本,即使没有收到请求也是如此。

要增加工作者数量,我们只需在创建DataLoader时设置num_workers参数:

torch.utils.data.DataLoader(train_dataset,                            batch_size=128,
                            num_workers=2)

下一节我们将看一个实际的性能提升案例。

收获成果

注意

本节显示的完整代码可以在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter05/complex_pipeline.ipynb找到。

要看到这些更改提供的相关性能改进,我们需要将它们应用于一个复杂的数据管道——也就是说,一个值得的数据管道!否则,性能提升的空间就不存在了。因此,我们将采用由七个任务组成的数据管道作为我们的基线,如下所示:

transform = transforms.Compose(            [transforms.Resize(255),
             transforms.CenterCrop(size=224),
             transforms.RandomHorizontalFlip(p=0.5),
             transforms.RandomRotation(20),
             transforms.GaussianBlur(kernel_size=3),
             transforms.ToTensor(),
             transforms.Normalize([0.485, 0.456, 0.406],
                                  [0.229, 0.224, 0.225])
             ])

对于每个样本,数据加载过程应用五种转换,即调整大小、裁剪、翻转、旋转和高斯模糊。在应用这些转换后,数据加载将结果图像转换为张量数据类型。最后,数据根据一组参数进行标准化。

为了评估性能改进,我们使用此管道在CIFAR-10数据集上训练ResNet121模型。这个训练过程包括 10 个 epochs,共计 1,892 秒完成,即使在配备 NVIDIA A100 GPU 的环境下也是如此:

Epoch [1/10], Loss: 1.1507, time: 187 secondsEpoch [2/10], Loss: 0.7243, time: 199 seconds
Epoch [3/10], Loss: 0.4129, time: 186 seconds
Epoch [4/10], Loss: 0.3267, time: 186 seconds
Epoch [5/10], Loss: 0.2949, time: 188 seconds
Epoch [6/10], Loss: 0.1711, time: 186 seconds
Epoch [7/10], Loss: 0.1423, time: 197 seconds
Epoch [8/10], Loss: 0.1835, time: 186 seconds
Epoch [9/10], Loss: 0.1127, time: 186 seconds
Epoch [10/10], Loss: 0.0946, time: 186 seconds
Training time: 1892 seconds
Accuracy of the network on the 10000 test images: 92.5 %

注意,这个数据管道比本书中到目前为止采用的那些要复杂得多,这正是我们想要的!

要使用固定内存并启用多工作进程能力,我们必须在原始代码中设置这两个参数:

torch.utils.data.DataLoader(train_dataset,                            batch_size=128,
                            pin_memory=True,
                            num_workers=8)

在我们的代码中应用这些更改后,我们将得到以下结果:

Epoch [1/10], Loss: 1.3163, time: 86 secondsEpoch [2/10], Loss: 0.5258, time: 84 seconds
Epoch [3/10], Loss: 0.3629, time: 84 seconds
Epoch [4/10], Loss: 0.3328, time: 84 seconds
Epoch [5/10], Loss: 0.2507, time: 84 seconds
Epoch [6/10], Loss: 0.2655, time: 84 seconds
Epoch [7/10], Loss: 0.2022, time: 84 seconds
Epoch [8/10], Loss: 0.1434, time: 84 seconds
Epoch [9/10], Loss: 0.1462, time: 84 seconds
Epoch [10/10], Loss: 0.1897, time: 84 seconds
Training time: 846 seconds
Accuracy of the network on the 10000 test images: 92.34 %

我们已将训练时间从 1,892 秒缩短至 846 秒,性能提升达到 123%,令人印象深刻!

下一节提供了几个问题,帮助您巩固本章学习的内容。

测验时间!

让我们通过回答一些问题来回顾本章学到的内容。初始时,请尝试不查阅材料回答这些问题。

注意

所有这些问题的答案都可以在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/quiz/chapter05-answers.md找到。

在开始本测验之前,请记住这不是一次测试!本节旨在通过复习和巩固本章内容来补充您的学习过程。

为以下问题选择正确的选项:

  1. 数据加载过程执行的三个主要任务是什么?

    1. 加载、缩放和调整大小。

    2. 缩放、调整大小和加载。

    3. 调整大小、加载和过滤。

    4. 加载、准备和增强。

  2. 数据加载是训练过程的哪个阶段?

    1. 向前。

    2. 向后。

    3. 优化。

    4. 损失计算。

  3. torch.utils.data API 提供的哪些组件可用于实现数据流水线?

    1. 数据管道数据加载器

    2. 数据集数据加载

    3. 数据集数据加载器

    4. 数据管道数据加载

  4. 除了增加数据流水线中的工作人员数量,我们还能做什么来改善数据加载过程的性能?

    1. 减少数据集的大小。

    2. 不使用 GPU。

    3. 避免使用高维图像。

    4. 优化 CPU 和 GPU 之间的数据传输。

  5. 我们如何加快 CPU 和 GPU 之间的数据传输?

    1. 使用更小的数据集。

    2. 使用最快的 GPU。

    3. 分配和使用固定内存而不是可分页内存。

    4. 增加主存储器的容量。

  6. 我们应该做什么来启用在DataLoader上使用固定内存?

    1. 没有。它已经默认启用。

    2. pin_memory参数设置为True

    3. experimental_copy参数设置为True

    4. 更新 PyTorch 到 2.0 版本。

  7. 为什么在 PyTorch 上使用多个工作人员可以加速数据加载?

    1. PyTorch 减少了分配的内存量。

    2. PyTorch 启用了使用特殊硬件功能的能力。

    3. PyTorch 使用最快的链接与 GPU 通信。

    4. PyTorch 同时处理多个数据集样本。

  8. 在请求分配固定内存时,以下哪项是正确的?

    1. 它总是满足的。

    2. 它可能失败。

    3. 它总是失败。

    4. 不能通过 PyTorch 完成。

现在,让我们总结一下本章涵盖的内容。

摘要

在本章中,您了解到数据流水线是模型构建过程中的重要组成部分。因此,一个高效的数据流水线对于保持训练过程的连续运行至关重要。除了通过内存固定优化数据传输到 GPU 外,您还学会了如何启用和配置多工作人员数据流水线。

在下一章中,您将学习如何减少模型复杂性以加快训练过程,而不会影响模型质量。