首页 > 其他分享 >PyTorch-1-x-模型训练加速指南-全-

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

时间:2024-07-23 14:51:40浏览次数:8  
标签:指南 训练 模型 PyTorch GPU 我们 分布式

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.X Windows、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是一个库,用于通过使用多线程技术利用多核处理器的全部性能来并行化任务。”

提示或重要注释

像这样显示。

联系我们

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

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

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

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

如果您有兴趣成为作者:如果您在某个专题上有专业知识,并且有意撰写或为一本书作贡献,请访问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,264 1,443
第一轮执行时间 (s) 47 146
中位数轮次执行时间 (s) 45 26
准确率 (%) 74.26 74.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_nvfuser cudagraphs inductor
整体训练时间 (s) 2,474 2,290 1,407
第一个 epoch 执行时间 (s) 142 86 140
中位数 epoch 执行时间 (s) 46 44 25
准确率 (%) 74.68 77.57 79.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 外,您还学会了如何启用和配置多工作人员数据流水线。

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

第六章:简化模型

您听说过“简约原则”吗?简约原则在模型估计的背景下,意味着尽可能保持模型简单。这一原则来自这样的假设:复杂模型(参数数量较多的模型)会过度拟合训练数据,从而降低泛化能力和良好预测的能力。

另外,简化神经网络有两个主要好处:减少模型训练时间和使模型能够在资源受限的环境中运行。简化模型的一种方法是通过使用修剪和压缩技术来减少神经网络参数的数量。

在本章中,我们展示如何通过减少神经网络参数的数量来简化模型,而不牺牲其质量。

以下是本章节将学到的内容:

  • 简化模型的关键好处

  • 模型修剪和压缩的概念与技术

  • 如何使用 Microsoft NNI 工具包简化模型

技术要求

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

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

了解模型简化的过程

简单来说,简化模型涉及移除神经网络的连接、神经元或整个层,以获得一个更轻的模型,即具有减少参数数量的模型。当然,简化版本的效率必须非常接近原始模型的效果。否则,简化模型就没有任何意义。

要理解这个主题,我们必须回答以下问题:

  • 为什么要简化模型?(原因)

  • 如何简化模型?(过程)

  • 什么时候简化模型?(时机)

我们将在接下来的章节中逐一回答这些问题,以便全面理解模型简化。

注意

在继续本章之前,必须指出模型简化仍然是一个开放的研究领域。因此,本书中提到的一些概念和术语可能与其他资料或框架工具包中的使用稍有不同。

为什么要简化模型?(原因)

为了深入了解为什么需要简化模型,让我们利用一个简单而又生动的类比。

考虑这样一个假设情境,我们必须建造一座桥来连接河的两岸。为了安全起见,我们决定在桥每两米放置一个支柱,如图 6**.1所示:

图 6.1 – 桥梁类比

图 6.1 – 桥梁类比

这座桥似乎非常安全,由其 16 根柱子支撑。然而,有人可能会看到这个项目并说我们不需要所有 16 根柱子来维持桥梁。作为桥梁设计者,我们可以主张安全第一;因此,增加柱子以确保桥梁完整性是没有问题的。

即使如此,如果我们可以稍微减少柱子数量而不影响桥梁的结构呢?换句话说,也许用 16 根柱子支撑桥梁在安全性上有些过剩。正如我们在图6.2中看到的那样,也许只需要九根柱子在这种情况下就足够了:

图 6.2 - 移除一些柱子后桥梁仍然保持竖立状态

6.2 - 移除一些柱子后桥梁仍然保持竖立状态

如果我们在桥上减少柱子数量并保持其安全性,我们将减少预算和建造时间,同时简化未来的维护过程。没有合理的理由反驳这种方法。

这个天真的类比有助于加热我们关于简化模型原因的讨论。至于桥梁上的柱子,一个神经网络模型是否需要所有的参数来实现良好的准确性?答案并不简单,而是取决于问题类型和模型本身。然而,考虑到简化模型的表现与原始模型完全相同,为什么不尝试前者呢?

毕竟,简化模型具有明显的好处:

  • 加速训练过程:通常,由较少参数组成的神经网络训练速度更快。正如在第一章**,拆解训练过程中讨论的那样,参数数量直接影响神经网络的计算负担。

  • 在资源受限环境中运行推理:有些模型太大无法存储,执行起来太复杂,无法适应内存和计算能力有限的环境。在这种情况下,唯一的办法是尽可能简化模型来在这些环境中运行。

现在,简化模型的好处已经十分明显,让我们跳到下一节来学习如何执行这个过程。

如何简化模型?(过程)

我们通过应用包括两个步骤的工作流程来简化模型:修剪压缩

图 6.3 - 简化工作流程

图 6.3 - 简化工作流程

如图6.3所示,简化工作流程接收密集神经网络作为输入,其中所有神经元都与自身完全连接,输出原始模型的简化版本。换句话说,该工作流程将密集神经网络转换为稀疏神经网络。

术语密集稀疏来自数学,并用于描述矩阵。密集矩阵充满有用的数字,而稀疏矩阵则具有大量空值(零)。由于神经网络的参数可以用 n 维矩阵表示,非全连接神经网络也被称为稀疏神经网络,因为神经元之间的空连接数量很高。

让我们详细查看工作流程,以理解每个步骤的作用,从修剪阶段开始。

修剪阶段

修剪阶段负责接收原始模型并剪除连接(权重)、神经元(偏置)和滤波器(核值)中存在的参数,从而得到一个修剪过的模型:

图 6.4 – 修剪阶段

图 6.4 – 修剪阶段

正如图 6.4 所示,原始模型中的许多连接已被禁用(在修剪模型中表示为不透明的线条)。修剪阶段根据过程中应用的技术决定应移除哪些参数。

修剪技术具有三个维度:

  • 准则:定义要切断的参数

  • 范围:确定是否丢弃整个结构(神经元、层或滤波器)或孤立的参数

  • 方法:定义是一次性修剪网络还是迭代修剪模型,直到达到某个停止准则

注意

模型修剪是一个崭新的世界。因此,您可以轻松找到许多提出新方法和解决方案的科学论文。一篇有趣的论文名为深度神经网络修剪调查:分类、比较、分析和建议,概述了这一领域的最新进展,并简要介绍了诸如量化和知识蒸馏等其他简化技术。

实际上,修剪后的模型占用与原始模型相同的内存量,并且需要相同的计算能力。这是因为 null 参数虽然在前向和反向计算及结果上没有实际影响,但并未从网络中完全移除。

例如,假设由三个神经元组成的两个全连接层。连接的权重可以表示为一个矩阵,如图 6.5 所示:

图 6.5 – 权重表示为矩阵

图 6.5 – 权重表示为矩阵

在应用修剪过程后,神经网络有三个连接被禁用,正如我们在图 6.6 中所见:

图 6.6 – 修剪后的权重更改为 null

图 6.6 – 修剪后的权重更改为 null

注意,权重已更改为 null(0.00),这些权重所代表的连接在网络计算中已被移除。因此,这些连接在神经网络结果的意义上实际上并不存在。

然而,数据结构与原始模型完全相同。我们仍然有九个浮点数,所有这些数仍然乘以它们各自神经元的输出(虽然没有实际效果)。从内存消耗和计算负担的角度来看,到目前为止什么都没有改变。

如果简化模型的目的是减少参数数量,为什么我们继续使用与之前相同的结构呢?保持冷静,让我们继续执行简化工作流的第二阶段:压缩阶段。

压缩阶段

图 6**.7所示,压缩阶段接收修剪模型作为输入,并生成一个仅由未修剪参数组成的新脑模型,即修剪过程未触及的参数:

图 6.7 – 压缩阶段

图 6.7 – 压缩阶段

新网络的形状可以与原始模型完全不同,神经元和层次的布置也各异。总之,压缩过程可以自由生成一个新模型,因为它遵循修剪步骤保留的参数。

因此,压缩阶段有效地去除了修剪模型的参数,从而得到一个真正简化的模型。让我们以图 6**.8中的例子来理解模型压缩后发生了什么:

图 6.8 – 应用于修剪网络的模型压缩

图 6.8 – 应用于修剪网络的模型压缩

在这个例子中,一组禁用的参数——连接——已从模型中删除,将权重矩阵减少了三分之一。因此,权重矩阵现在占用更少的内存,并且在完成前向和后向阶段时需要更少的操作。

注意

我们可以把修剪和压缩阶段之间的关系想象成从磁盘中删除文件的过程。当我们要求操作系统删除文件时,它只是将分配给文件的块标记为自由。换句话说,文件内容仍然存在,只有当被新文件覆盖时才会被擦除。

我们何时简化模型?(时刻)

我们可以在训练神经网络之前或之后简化模型。换句话说,模型简化过程可以应用于未训练、预训练或已训练的模型,如下所述:

  • 未训练模型:我们的目标是加快训练过程。由于模型尚未训练,神经网络填充有随机参数,大多数修剪技术无法有效地工作。为了解决这个问题,通常在简化模型之前会运行一个预热阶段,即在简化模型之前对网络进行单个时期的训练。

  • 预训练模型:我们使用预训练网络来解决特定问题域的问题,因为这些网络在一般领域上具有通用的效率。在这种情况下,我们不需要执行预热阶段,因为模型已经训练好了。

  • 训练好的模型:通常简化训练好的模型是为了在资源受限的环境中部署训练好的网络。

现在我们已经回答了关于模型简化的问题,我们应该使用 PyTorch 及其工具包来简化模型吗?请跟随我到下一节来学习如何做到这一点!

使用 Microsoft NNI 简化模型

神经网络智能NNI)是微软创建的开源项目,旨在帮助深度学习从业者自动化任务,如超参数自动化和神经架构搜索。

NNI 还有一套工具集,用于更简单直接地处理模型简化。因此,我们可以通过在原始代码中添加几行代码来轻松简化模型。NNI 支持 PyTorch 和其他知名的深度学习框架。

注意

PyTorch 有自己的 API 来修剪模型,即torch.prune。不幸的是,在编写本书时,这个 API 不提供压缩模型的机制。因此,我们决定引入 NNI 作为完成此任务的解决方案。有关 NNI 的更多信息,请访问github.com/microsoft/nni

让我们从下一节开始对 NNI 进行概述。

NNI 概述

由于 NNI 不是 PyTorch 的本地组件,我们需要通过执行以下命令使用 pip 安装它:

pip install nni

NNI 有很多模块,但为了简化模型,我们只会使用其中的两个,即pruningspeedup

from nni.compression.pytorch import pruning, speedup

修剪模块

pruning模块提供一组修剪技术,也称为修剪器。每个修剪器应用特定的方法来修剪模型,并需要一组特定的参数。在修剪器所需的参数中,有两个是强制性的:模型和配置列表

配置列表是一个基于字典结构的结构,用于控制修剪器的行为。从配置列表中,我们可以指示修剪器必须处理哪些结构(层、操作或过滤器),以及哪些结构它应该保持不变。

例如,以下配置列表告诉修剪器处理所有实施Linear运算符的层(使用类torch.nn.Linear创建的层),除了名为layer4的层。此外,修剪器将尝试使 30%的参数归零,如sparse_ratio键中所示:

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

注意

您可以在nni.readthedocs.io/en/stable/compression/config_list.html找到配置列表接受的键值对的完整列表。

设置了配置列表后,我们就可以实例化修剪器,如下所示:

pruner = pruning.L1NormPruner(model, config_list)

由修剪器提供的最关键方法称为compress。尽管名称暗示的是另一回事,它通过应用相应的剪枝算法执行剪枝过程。

compress方法返回一个名为masks的数据结构,该结构表示剪枝算法丢弃的参数。进一步使用此信息有效地从网络中移除被修剪的参数。

注意

如前所述,简化过程仍在进行中。因此,我们可能会遇到一些技巧,比如术语的不一致使用。这就是为什么 NNI 将剪枝阶段称为compress,尽管压缩步骤是由另一种称为speedup的方法完成的。

请注意,到目前为止,原始模型确实没有任何变化;还没有。要有效地移除被修剪的参数,我们必须依赖于speedup模块。

speedup 模块

speedup模块提供了一个名为ModelSpeedup的类,用于创建速度器。速度器在修剪模型上执行压缩阶段,即有效地移除修剪器丢弃的参数。

在修剪器方面,我们还必须从ModelSpeedup类实例化一个对象。此类需要三个必填参数:修剪模型、一个输入样本和由修剪器生成的掩码:

speeder = speedup.ModelSpeedup(model, input_sample, masks)

之后,我们只需调用speedup_model方法,使速度器可以压缩模型并返回原始模型的简化版本:

speeder.speedup_model()

现在您已经概述了通过 NNI 简化模型的基本步骤,让我们跳到下一节,学习如何在实际示例中使用此工具包。

NNI 的实际应用!

为了看到 NNI 在实践中的工作,让我们简化我们众所周知的 CNN 模型。在此示例中,我们将通过使用 CIFAR-10 数据集来简化此模型。

注意

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

让我们从计算 CNN 模型的原始参数数目开始:

model = CNN()print(count_parameters(model))
2122186

CNN 模型在偏差、权重和神经网络的滤波器之间有2,122,186个参数。我们仅在 CPU 机器上使用 CIFAR-10 数据集进行了 10 个时期的训练,因为我们有兴趣比较不同修剪配置之间的训练时间和相应的准确性。因此,原始模型在 122 秒内训练,达到 47.10%的准确性。

好的,让我们移除桥的一些支柱,看看它是否仍然站立。我们将通过考虑以下策略简化 CNN 模型:

  • 操作类型:Conv2d

  • 每层稀疏度:0.50

  • 修剪器算法:L1 Norm

这个策略告诉简化过程只关注神经网络的卷积层,并且对于每一层,裁剪算法必须丢弃 50%的参数。由于我们正在简化一个全新的模型,我们需要执行预热阶段来为网络引入一些有价值的参数。

对于这个实验,我们选择了 L1 范数裁剪器,它根据 L1 范数测量的大小来移除参数。简单来说,裁剪器会丢弃对神经网络结果影响较小的参数。

注意

您可以在L1 Norm pruner找到更多关于 L1 范数裁剪器的信息。

下面的代码摘录显示了应用上述策略简化 CNN 模型所需的几行代码:

config_list = [{'op_types': ['Conv2d'],                'sparsity_per_layer': 0.50}]
pruner = pruning.L1NormPruner(model, config_list)
_, masks = pruner.compress()
pruner._unwrap_model()
input_sample, _ = next(iter(train_loader))
speeder = speedup.ModelSpeedup(model, input_sample, masks)
speeder.speedup_model()

在简化过程中,NNI 将输出一堆如下的行:

[2023-09-23 19:44:30] start to speedup the model[2023-09-23 19:44:30] infer module masks...
[2023-09-23 19:44:30] Update mask for layer1.0
[2023-09-23 19:44:30] Update mask for layer1.1
[2023-09-23 19:44:30] Update mask for layer1.2
[2023-09-23 19:44:30] Update mask for layer2.0
[2023-09-23 19:44:30] Update mask for layer2.1
[2023-09-23 19:44:30] Update mask for layer2.2
[2023-09-23 19:44:30] Update mask for .aten::size.8
[2023-09-23 19:44:30] Update mask for .aten::Int.9
[2023-09-23 19:44:30] Update mask for .aten::reshape.10

在完成这个过程后,我们可以验证原始神经网络的参数数量如预期般减少了约 50%:

print(count_parameters(model))1059306

好吧,模型变得更小更简单了。但是训练时间和效率呢?让我们看看!

我们使用相同的超参数通过相同数量的 epochs 对 CIFAR-10 进行了简化模型的训练。简化模型的训练过程只需 89 秒完成,表现提升了 37%! 虽然模型的效率略有下降(从 47.10%降至 42.71%),但仍接近原始版本。

有趣的是要注意训练时间、准确性和稀疏比之间的权衡。如表 6.1所示,当从网络中移除 80%的参数时,模型的效率降至 38.87%。另一方面,训练过程仅需 76 秒完成,比训练原始网络快 61%:

每层稀疏度 训练时间 准确性
10% 118 47.26%
20% 113 45.84%
30% 107 44.66%
40% 100 45.18%
50% 89 42.71%
60% 84 41.90%
70% 81 40.84%
80% 76 38.87%

表 6.1 – 模型准确性、训练时间和稀疏度水平之间的关系

俗话说,天下没有免费的午餐。因此,当简化模型时,准确性有望略微下降。这里的目标是在模型质量略微降低的情况下找到性能改进的平衡点。

在本节中,我们学习了如何使用 NNI 来简化我们的模型。通过在我们原始代码中改变几行代码,我们可以通过减少一定数量的连接来简化模型,从而减少训练时间,同时保持模型的质量。

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

测验时间!

让我们通过回答一些问题来回顾本章所学内容。首先,请尝试不查阅资料回答这些问题。

注意

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

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

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

  1. 在简化工作流程时需要执行哪两个步骤?

    1. 减少和压缩。

    2. 剪枝和减少。

    3. 剪枝和压缩。

    4. 减少和压缩。

  2. 剪枝技术通常具有以下维度:

    1. 标准、范围和方法。

    2. 算法、范围和大小。

    3. 标准、约束和目标。

    4. 算法、约束和目标。

  3. 关于压缩阶段,我们可以断言以下哪一个?

    1. 它接收一个压缩过的模型作为输入,并验证模型的完整性。

    2. 它接收一个压缩过的模型作为输入,并生成一个仅由未剪枝参数部分组成的模型。

    3. 它接收一个被剪枝的模型作为输入,并生成一个仅由未剪枝参数组成的新脑模型。

    4. 它接收一个被剪枝的模型作为输入,并评估应用于该模型的剪枝程度。

  4. 我们可以在以下哪些情况下执行模型简化过程?

    1. 仅预训练模型。

    2. 仅预训练和非训练模型。

    3. 仅非训练模型。

    4. 非训练、预训练和已训练模型。

  5. 简化训练模型的主要目标之一是什么?

    1. 加速训练过程。

    2. 将其部署在资源受限的环境中。

    3. 提高模型的准确性。

    4. 没有理由简化已训练的模型。

  6. 考虑以下配置列表传递给剪枝者:

    config_list = [{ 'op_types': ['Conv2d'],                 'exclude_op_names': ['layer2'],                 'sparse_ratio': 0.25 }]
    

    剪枝者会执行以下哪些操作?

    1. 剪枝者将尝试使所有网络参数的 75%无效化。

    2. 剪枝者将尝试使所有全连接层参数的 25%无效化。

    3. 剪枝者将尝试使卷积层参数的 25%无效化,除了标记为“layer2”的那个。

    4. 剪枝者将尝试使卷积层参数的 75%无效化,除了标记为“layer2”的那个。

  7. 在执行简化工作流程后,模型的准确性更可能发生什么变化?

    1. 模型的准确性倾向于增加。

    2. 模型的准确性肯定会增加。

    3. 模型的准确性倾向于降低。

    4. 模型的准确性保持不变。

  8. 在简化以下哪些内容之前,有必要执行热身阶段?

    1. 非训练模型。

    2. 已训练的模型。

    3. 预训练模型。

    4. 以上都不是。

摘要

在本章中,您了解到通过减少参数数量来简化模型可以加速网络训练过程,使模型能够在资源受限的平台上运行。

接着,我们看到简化过程包括两个阶段:剪枝和压缩。前者负责确定网络中必须删除的参数,而后者则有效地从模型中移除这些参数。

尽管 PyTorch 提供了一个 API 来剪枝模型,但它并不完全有助于简化模型。因此,介绍了 Microsoft NNI,一个强大的工具包,用于自动化与深度学习模型相关的任务。在 NNI 提供的功能中,这个工具提供了一个完整的工作流程来简化模型。所有这些都是通过向原始代码添加几行新代码来实现的。

在接下来的章节中,您将学习如何减少神经网络采用的数值精度,以加快训练过程并减少存储模型所需的内存量。

第七章:采用混合精度

科学计算是科学家用来推动已知界限的工具。生物学、物理学、化学和宇宙学是依赖科学计算来模拟和建模真实世界的领域的例子。在这些知识领域中,数值精度对于产生一致的结果至关重要。由于在这种情况下每个小数位都很重要,科学计算通常采用双精度数据类型来表示具有最高可能精度的数字。

然而,额外信息的需求是有代价的。数值精度越高,处理这些数字所需的计算能力就越高。此外,更高的精度还要求更高的内存空间,增加内存消耗。

面对这些缺点,我们必须问自己:我们是否需要如此高的精度来构建我们的模型?通常情况下,我们不需要!在这方面,我们可以针对一些操作降低数值精度,从而加快训练过程并节省一些内存空间。当然,这个过程不应影响模型产生良好预测的能力。

在本章中,我们将向您展示如何采用混合精度策略来加快模型训练过程,同时又不损害模型的准确性。除了总体上减少训练时间外,这种策略还可以利用特殊硬件资源,如 NVIDIA GPU 上的张量核心。

以下是本章的学习内容:

  • 计算机系统中数值表示的概念

  • 为什么降低精度可以减少训练过程的计算负担

  • 如何在 PyTorch 上启用自动混合精度

技术要求

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

你可以使用你喜欢的环境来执行这段代码,比如 Google Colab 或者 Kaggle。

记住数值精度

在深入探讨采用混合精度策略的好处之前,重要的是让您理解数字表示和常见数据类型的基础知识。让我们首先回顾一下计算机如何表示数字。

计算机如何表示数字?

计算机是一种机器,拥有有限的资源,旨在处理比特,即它能管理的最小信息单位。由于数字是无限的,计算机设计师不得不付出很大努力,找到一种方法来在实际机器中表示这一理论概念。

为了完成这项工作,计算机设计师需要处理与数值表示相关的三个关键因素:

  • 符号:数字是正数还是负数

  • 范围:表示数字的区间。

  • 精度:小数位数。

考虑到这些因素,计算机架构师成功地定义了数值数据类型,不仅可以表示整数和浮点数,还可以表示字符、特殊符号,甚至复数。

让我们通过一个例子来使事情更具体化。计算机架构和编程语言通常使用 32 位来通过所谓的 INT32 格式表示整数,如图 7**.1所示:

图 7.1 – 32 位整数的数字表示

图 7.1 – 32 位整数的数字表示

在这 32 位中,有 1 位用于表示数字的符号,其中 0 表示正数,1 表示负数。其余的 31 位用于表示数字本身。有了 31 位,我们可以得到 2,147,483,648 个不同的 0 和 1 的组合。因此,这种表示法的数值范围为-2,147,483,648 到+2,147,483,647。注意正数部分比负数部分少一个数,因为我们必须表示零。

这是有符号整数表示的一个示例,其中 1 位用于确定数值的正负。然而,如果在某些情况下只有正数是相关的,可以使用无符号表示法。无符号表示法使用所有 32 位来表示数字,因此其数值区间为 0 到 4,294,967,295。

在不需要更大区间的情况下,可以采用更便宜的格式 - 仅有 8 位 - 来表示整数:INT8 表示法,如图 7**.2所示。此表示法的无符号版本提供 0 到 255 之间的区间:

图 7.2 – INT8 格式中数字的示例

图 7.2 – INT8 格式中数字的示例

假设 1 字节等于 8 位(在某些计算机架构上这种关系可能不同),INT32 格式需要 4 字节来表示一个整数,而 INT8 只需 1 字节即可。因此,INT32 格式比 INT8 昂贵四倍,需要更多的内存空间和计算能力来处理这些数字。

整数表示方法非常简单。然而,要表示浮点数(小数),计算机架构师们必须设计更复杂的解决方案,这是我们将在下一节中学习的内容。

浮点数表示

现代计算机采用 IEEE 754 标准来表示浮点数。该标准定义了两种浮点数表示方式,即单精度和双精度。单精度,也称为 FP32 或 float32,使用 32 位,而双精度,也称为 FP64 或 float64,使用 64 位。

单精度和双精度在结构上相似,由三个元素组成:符号、指数和分数部分(有效数字),如图 7**.3所示。

图 7.3 – 浮点数表示的结构

图 7.3 – 浮点表示结构

符号位与整数表示的意义相同,即定义数字是正数还是负数。指数部分定义了数值范围,而分数部分则决定了数值的精度,即小数位数。

两种格式都使用 1 位来表示符号。关于其他部分,FP32 和 FP64 分别使用 8 和 11 位来表示指数,23 和 52 位来表示分数部分。粗略地说,FP64 的范围略高于 FP32,因为前者在指数部分比后者多使用了 3 位。另一方面,FP64 由于为分数部分保留了 52 位,因此提供了超过 FP32 两倍的双精度。

FP64 提供的高数值精度使其非常适合科学计算,其中每一位额外的小数点对于解决这一领域的问题至关重要。由于双精度需要 8 字节来表示一个数字,通常仅在需要如此高精度的任务中使用。在没有这种要求的情况下,使用单精度数据类型更为合适。这也是为什么训练过程通常采用 FP32 的原因。

新型数据类型

根据 IEEE 754 标准定义,单精度是表示浮点数的默认格式。然而,随着时间的推移,新问题的出现要求新的方法、方法和数据类型。

在新型数据类型中,我们可以突出显示三种特别适合机器学习模型的类型:FP16、BFP16 和 TF32。

FP16

FP16 或 float16,正如你可能猜到的那样,使用 16 位来表示浮点数,如 图 7**.4 所示。由于它只使用了单精度的一半 32 位,这种新数据类型被称为半精度

FP16 的结构与其兄弟 FP32 和 FP64 相同。区别在于用于表示指数和分数部分的位数。FP16 使用 5 位和 10 位分别表示指数和分数部分:

图 7.4 – FP16 格式结构

图 7.4 – FP16 格式结构

在需要的精度超出 float32 提供的情况下,FP16 是 FP32 的一种替代方案。在这些情况下,使用更简单的数据类型来节省内存空间和减少操作数据所需的计算能力要好得多。

BFP16

BFP16 或 bfloat16 是由谷歌大脑(Google Brain)——谷歌的人工智能研究团队创造的一种新型数据类型。BFP16 类似于 FP16,使用 16 位来表示浮点数。然而,与 FP16 不同,BFP16 的重点是保留与 FP32 相同的数值范围,同时显著降低精度。因此,BFP16 使用 8 位来表示指数部分(与 FP32 相同),但只使用 7 位来表示分数部分,如 图 7**.5 所示:

图 7.5 – BFP16 格式结构

图 7.5 – BFP16 格式的结构

谷歌创建了 BFP16 以满足机器学习和人工智能工作负载的需求,其中精度并不是很重要。截至撰写时,bfloat16 受到英特尔 Xeon 处理器(通过 AVX-512 BF16 指令集)、谷歌 TPU v2 和 v3、NVIDIA GPU A100 以及其他硬件平台的支持。

请注意,虽然这些硬件平台支持 TF32,但不能保证 bfloat16 会被所有软件支持和实现。例如,PyTorch 只支持在 CPU 上使用 bfloat16,尽管 NVIDIA GPU 也支持这种数据类型。

TF32

TF32代表 TensorFloat 32,尽管名字如此,但是这是由 NVIDIA 创建的 19 位格式。TF32 是 FP32 和 FP16 格式的混合体,因为它使用 8 位表示指数和 10 位表示小数部分,类似于 FP32 和 FP16 的格式。因此,TF32 结合了 FP16 提供的精度和 FP32 的数值范围。图 7.6以图形方式描述了 TF32 的格式:

图 7.6 – TF32 格式的结构

图 7.6 – TF32 格式的结构

与 bfloat16 类似,TF32 也是专门为处理人工智能工作负载而创建的,目前受到较新的 GPU 代数的支持,从安培架构(NVIDIA A100)开始。除了在范围和精度之间提供平衡的好处外,TF32 还受 Tensor Core 的支持,这是 NVIDIA GPU 上可用的特殊硬件组件。我们稍后将在本章更详细地讨论 Tensor Core。

总结一下!

是的,当然!那是大量信息需要消化。因此,表 7.1对此进行了总结:

格式 指数位数 小数位数 字节数 别名
FP32 8 23 4 Float32,单精度
FP64 11 52 8 Float64,双精度
FP16 5 10 2 Float16,半精度
BFP16 8 7 2 Bfloat16
TF32 8 10 4 TensorFloat32

表 7.1 – 数值格式摘要

注意

Grigory Sapunov 撰写了一个关于数据类型的好摘要。您可以在moocaholic.medium.com/fp64-fp32-fp16-bfloat16-tf32-and-other-members-of-the-zoo-a1ca7897d407找到它。

正如您可能已经注意到的那样,数值范围和精度越高,表示这些数字所需的字节数也越多。因此,数值格式会增加存储和处理这些数字所需的资源量。

如果我们不需要如此高的精度(和范围)来训练我们的模型,为什么不采用比通常的 FP32 更便宜的格式呢?这样做可以节省内存并加速整个训练过程。

我们有选择不必更改整个构建过程的数值精度,而是采用下面解释的混合精度方法的选项。

理解混合精度策略

使用低精度格式的好处是显而易见的。除了节省内存外,处理低精度数据所需的计算能力也比处理高精度数字所需的少。

加速机器学习模型训练过程的一种方法涉及采用混合精度策略。沿着第六章的思路,简化模型,我们将通过关于这种方法的一些简单问题(当然也会回答)来理解这一策略。

注意

当你搜索有关降低深度学习模型精度的信息时,你可能会遇到一个称为模型量化的术语。尽管这些是相关的术语,但混合精度的目标与模型量化有很大不同。前者旨在通过采用降低的数值精度格式加速训练过程。后者侧重于减少已训练模型的复杂性以在推理阶段使用。因此,务必不要混淆这两个术语。

让我们首先回答最基本的问题:这个策略到底是什么?

什么是混合精度?

正如你可能猜到的那样,混合精度方法将不同精度的数值格式混合使用。该方法旨在尽可能使用更便宜的格式 - 换句话说,它仅在必要时保留默认精度。

在训练过程的上下文中,我们希望将 FP32 - 在此任务中采用的默认数值格式 - 与 FP16 或 BFP16 等低精度表示混合。具体来说,我们在某些操作中使用 FP32,而在其他操作中使用低精度格式。通过这样做,我们在需要更高精度的操作上保持所需的精度,同时也享受半精度表示的优势。

正如图 7**.7所示,混合方法与传统策略相反,我们在训练过程中使用相同的数值精度:

图 7.7 – 传统与混合精度方法的差异

图 7.7 – 传统与混合精度方法的差异

鉴于使用低精度格式的优势,你可能会想知道为什么不在训练过程中涉及的所有操作中使用纯低精度方法 - 比如说纯低精度方法而不是混合精度策略。这是一个合理的问题,我们将在接下来的部分中回答它。

为什么使用混合精度?

这里提出的问题不是关于使用混合精度的优势,而是为什么我们不应该采用绝对的低精度方法。

好吧,我们不能使用纯低精度方法,因为有两个主要原因:

  • 梯度信息的丢失

  • 缺乏低精度操作

让我们更详细地看一下每一种。

梯度信息的丢失

降低精度可能导致梯度问题,从而影响模型的准确性。随着优化过程的进行,由降低精度导致的梯度信息丢失可以阻碍整个优化过程,使模型无法收敛。因此,训练后的模型可能表现出较低的准确性。

我们应该澄清这个问题吗?假设我们处于一个假设情境,正在使用两种不同的精度格式 A 和 B 来训练模型。格式 A 支持五位小数精度,而格式 B 只支持三位小数精度。

假设我们已经对模型进行了五个训练步骤的训练。在每个训练步骤中,优化器计算了梯度来指导整体优化过程。然而,正如图 7**.8所示,在第三个训练步骤后,格式 B 上的梯度变为零。此后,优化过程将是盲目的,因为梯度信息已丢失:

图 7.8 – 梯度信息丢失

图 7.8 – 梯度信息丢失

这是一个关于梯度信息丢失的朴素而形象的例子。然而,总体而言,这是我们在选择使用低精度格式时可能面临的问题。

因此,我们必须将一些操作保持在默认的 FP32 格式上运行,以避免这些问题。然而,当使用较低精度表示时,我们仍然需要注意梯度的处理,正如我们将在本章后面理解的那样。

缺乏低精度操作

关于第二个原因,我们可以说许多操作没有更低精度的实现。除了技术限制外,实施某些操作的低精度版本的成本效益比可能非常低,因此不值得这样做。

因此,PyTorch 维护了一个合格操作列表,以在较低精度下运行,以查看当前支持给定精度和设备的操作。例如,conv2d操作可以在 CUDA 设备上以 FP16 运行,在 CPU 上以 BFP16 运行。另一方面,softmax操作既不在 GPU 上也不在 CPU 上支持低精度运行。一般来说,在撰写本文时,PyTorch 仅在 CUDA 设备上支持 FP16,仅在 CPU 上支持 BFP16。

注意

您可以在 PyTorch 的pytorch.org/docs/stable/amp.html找到可以在低精度下运行的所有合格操作的完整列表。

也就是说,我们必须始终评估我们的模型是否至少执行了其中一个可以在低精度下运行的合格操作,然后再全力采用混合精度方法。否则,我们将徒劳地尝试从这种策略中获益。

即使可以减少训练过程的数值精度,我们也不应该期望在任何情景下都会有巨大的性能增益。毕竟,目前只有训练过程中执行的操作的子集支持较低精度的数据类型。另一方面,任何从无需费力的过程中获得的性能改进总是受欢迎的。

如何使用混合精度

通常,我们依赖于自动解决方案来应用混合精度策略到训练过程中。这种解决方案称为自动混合精度,简称 AMP

图 7**.9 所示,AMP 自动评估在训练过程中执行的操作,以决定哪些操作可以以低精度格式运行:

图 7.9 – AMP 过程

图 7.9 – AMP 过程

一旦 AMP 发现符合要求以低精度执行的操作,它就会接管并用低精度版本替换以默认精度运行的操作。这是一个优雅而无缝的过程,旨在避免难以检测、调查和修复的错误。

尽管强烈建议使用自动解决方案来应用混合精度方法,但也可以手动进行。然而,我们必须意识到一些事情。一般来说,当自动解决方案提供的结果不好或不合理时,或者简单地不存在时,我们寻求手动实施过程。由于自动解决方案已经可用,并且无法保证手动操作能获得显著的性能改进,我们应该把手动方法仅作为采用混合精度的最后选择。

注意

您始终可以尝试并手动实施混合精度。深入了解这个主题可能是个好主意。您可以从 NVIDIA GTC 2018 上的材料开始,该材料可在 on-demand.gputechconf.com/gtc-taiwan/2018/pdf/5-1_Internal%20Speaker_Michael%20Carilli_PDF%20For%20Sharing.pdf 上获取。

张量核心如何?

张量核心是一种处理单元,能够加速矩阵乘法执行,这是在人工智能和高性能计算工作负载中经常执行的基本操作。为了使用这一硬件资源,软件(库或框架)必须能够处理张量核心支持的数值格式。如 表 7.2 所示,张量核心支持的数值格式根据 GPU 架构而异:

表 7.2 – 张量核心支持的数据类型(来自 NVIDIA 官方网站)

表 7.2 – 张量核心支持的数据类型(来自 NVIDIA 官方网站)

新型 GPU 模型的张量核心,如 Hopper 和 Ampere(分别为系列 H 和 A),支持 TF32、FP16 和 bfloat16 等低精度格式,以及双精度格式(FP64),这对于处理传统 HPC 工作负载特别重要。

注意

Hopper 架构开始支持 FP8,一种全新的数字表示,只使用 1 字节来表示浮点数。NVIDIA 创建了这种格式,以加速 Transformer 神经网络的训练过程。使用张量核心来运行 FP8 操作依赖于 Transformer 引擎库,超出了本书的范围。

另一方面,现有的架构都不配备支持 FP32 的张量核心,默认精度格式。因此,为了利用这种硬件能力的计算能力,我们必须调整我们的代码,以便它可以使用更低精度格式。

此外,激活张量核心取决于其他因素,超出了采用较低精度表示的范围。在其他事项中,我们必须注意给定架构、库版本和数字表示的矩阵维度所需的内存对齐。例如,在 A100(安培架构)的情况下,当使用 FP16 和 CuDNN 版本在 7.6.3 之前时,矩阵的维度必须是 8 字节的倍数。

因此,采用较低精度是使用张量核心的第一个条件,但这并不是启用此资源的唯一要求。

注意

您可以在docs.nvidia.com/deeplearning/performance/dl-performance-matrix-multiplication/index.html#requirements-tc找到有关使用张量核心要求的更多详细信息。

现在我们了解了混合精度的基础知识,我们可以学习如何在 PyTorch 中使用这种方法。

启用 AMP

幸运的是,PyTorch 提供了方法和工具,通过在我们的原始代码中进行少量更改即可执行 AMP。

在 PyTorch 中,AMP 依赖于启用一对标志,用torch.autocast对象包装训练过程,并使用梯度缩放器。更复杂的情况是在 GPU 上实施 AMP,涉及这三个部分,而最简单的场景(基于 CPU 的训练)只需要使用torch.autocast

让我们从涵盖更复杂的情况开始。因此,跟随我进入下一节,学习如何在我们基于 GPU 的代码中激活这种方法。

在 GPU 上激活 AMP。

要在 GPU 上激活 AMP,我们需要对我们的代码进行三处修改:

  1. 启用 CUDA 和 CuDNN 后端标志。

  2. torch.autocast对象包装训练循环。

  3. 使用梯度缩放器。

让我们仔细看看。

启用后端标志

正如我们在第四章中学到的,使用专业库,PyTorch 依赖第三方库(在 PyTorch 术语中也称为后端)来帮助执行专业任务。在 AMP 的上下文中,我们必须启用与 CUDA 和 CuDNN 后端相关的四个标志。所有这些标志默认情况下都是禁用的,应在代码开头打开。

注意

CuDNN 是一个 NVIDIA 库,提供了在深度学习神经网络上常执行的优化操作。

第一个标志是torch.backend.cudnn.benchmark,它激活了 CuDNN 的基准模式。在此模式下,CuDNN 执行一组简短的测试,以确定在给定平台上执行哪些操作是最佳的。尽管此标志与混合精度不直接相关,但它在过程中发挥着重要作用,增强了 AMP 的正面效果。

CuDNN 在第一次被 PyTorch 调用时执行此评估。一般来说,这一时刻发生在第一个训练时期。因此,如果第一个时期执行比训练过程的其余时期需要更多的时间,请不要感到惊讶。

另外两个标志称为cuda.matmul.allow_fp16_reduced_precision_reductioncuda.matmul.allow_bf16_reduced_precision_reduction。它们告诉 CUDA 在执行 FP16 和 BFP16 表示时使用减少精度的matmul操作。matmul操作与矩阵乘法相关,这是通常可以在神经网络上执行的最基本的计算任务之一。

最后一个标志是torch.backends.cudnn.allow_tf32,它允许 CuDNN 使用 TF32 格式,从而启用 NVIDIA Tensor Cores 支持的格式之一。

在启用这些标志之后,我们可以继续更改训练循环部分。

使用torch.autocast包装训练循环

torch.autocast类负责在 PyTorch 上实现 AMP。我们可以将torch.autocast用作上下文管理器或装饰器。其使用取决于我们如何实现我们的代码。无论使用哪种方法,AMP 的工作方式都是相同的。

具体来说,在上下文管理器的情况下,我们必须包装在训练循环中执行的前向和损失计算阶段。所有其他在训练循环中执行的任务必须排除在torch.autocast的上下文之外。

torch.autocast 接受四个参数:

  • device_type:这定义了自动转换将在哪种设备上执行 AMP。接受的值包括cudacpuxpuhpu – 即,我们可以分配给torch.device对象的相同值。自然地,这个参数最常见的值是cudacpu

  • dtype:在 AMP 策略中使用的数据类型。此参数接受与我们想要在自动转换中使用的数据类型相关联的数据类型对象 – 从torch.dtype类实例化。

  • enabled:一个标志,用于启用或禁用 AMP 过程。默认情况下是启用的,但我们可以将其切换为false来调试我们的代码。

  • cache_enabled:在 AMP 过程中,torch.autocast是否应启用权重缓存。此参数默认启用。

使用torch.autocast必须传入device_typedtype参数。其他参数是可选的,仅用于微调和调试。

下面的摘录展示了在训练循环中作为上下文管理器使用torch.autocast(为了简单起见,此示例仅显示了训练循环的核心部分):

with torch.autocast(device_type=device, dtype=torch.float16):    output = model(images).to(device)
    loss = criterion(output, labels)

训练循环中执行的其他任务没有被torch.autocast封装,因为我们只对前向和损失计算阶段应用 AMP 感兴趣。此外,训练过程的剩余任务都被梯度缩放器包装,如下所述。

梯度缩放器

正如我们在本章开头所学到的,使用低精度表示法可能导致梯度信息的丢失。为了解决这个问题,我们需要使用torch.cuda.amp.GradScaler来包装优化和反向传播阶段:

scaler = torch.cuda.amp.GradScaler()with torch.autocast(device_type=device, dtype=torch.float16):
    output = model(images).to(device)
    loss = criterion(output, labels)
optimizer.zero_grad()
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()

首先,我们实例化了一个来自torch.cuda.amp.GradScaler的对象。接下来,我们将optimizer.step()loss.backward()调用包装到梯度缩放器中,以便它控制这些任务。最后,训练循环要求缩放器最终更新网络参数。

我们将这些乐高积木拼接成一个独特的建筑块,看看 AMP 在下一节中能做些什么!

AMP,展示你的能力!

为了评估使用 AMP 的好处,我们将使用 EfficientNet 神经网络架构进行训练,该架构位于torch.vision.models包中,并使用 CIFAR-10 数据集。

注意

此部分显示的完整代码可在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter07/amp-efficientnet_cifar10.ipynb中找到。

在这个实验中,我们将在 GPU NVIDIA A100 上使用 FP16 和 BFP16 评估 AMP 的使用情况,运行 50 个 epochs。我们的基准执行涉及使用 EfficientNet 的默认精度(FP32),但启用了 CuDNN 基准标志。通过这样做,我们将使事情变得公平,因为尽管 CuDNN 基准标志对 AMP 过程很重要,但并不直接相关。

基准执行花了 811 秒完成,达到了 51%的准确率。这里我们不关心准确率本身;我们感兴趣的是评估 AMP 是否会影响模型的质量。

通过采用 BFP16 精度格式,训练过程耗时 754 秒完成,这表示性能提升了 8%,这是一个较为微小和令人失望的改进。这是因为仅实现了在 CPU 上执行的 BFP16-适用操作,尽管我们正在使用 GPU 训练模型,但仍然有一些操作在 CPU 上执行。因此,这种微小的性能改进来自于继续在 CPU 上执行的一小部分代码片段。

注意

我们正在 GPU 上使用 BFP16 运行这个实验,以展示处理 AMP 的细微差别。尽管 PyTorch 并未提供 BFP16-适用的操作在 GPU 上执行的功能,我们却没有收到任何关于此操作的警告。这是一个重要示例,说明了了解我们在代码中使用的过程细节是多么重要。

好的,但是 FP16 呢?嗯,在 Float16 下运行的 AMP 训练过程完成了 50 个 epochs,耗时 486 秒,性能提升了 67%。由于梯度缩放器的工作,模型的准确性没有受到使用低精度格式的影响。事实上,这种场景下训练的模型与基线代码达到了相同的 51% 准确率。

我们必须牢记,这种性能提升只是 AMP 加速训练过程的一个示例。根据模型、库和设备在训练过程中的使用情况,我们可以获得更令人印象深刻的结果。

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

测验时间!

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

注意

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

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

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

  1. 以下哪种数值格式仅使用 8 位表示整数?

    1. FP8。

    2. INT32。

    3. INT8。

    4. INTFB8。

  2. FP16 是一种使用 16 位表示浮点数的数值表示。这种数值格式也被称为什么?

    1. 半精度浮点表示。

    2. 单精度浮点表示。

    3. 双精度浮点表示。

    4. 四分之一精度浮点表示。

  3. 以下哪种数值表示是由 Google 创建,用于处理机器学习和人工智能工作负载的?

    1. GP16。

    2. GFP16。

    3. FP16。

    4. BFP16。

  4. NVIDIA 创建了 TF32 数据表示。以下哪种位数用于表示浮点数?

    1. 32 位。

    2. 19 位。

    3. 16 位。

    4. 20 位。

  5. PyTorch 在执行训练过程的操作时使用的默认数值表示是什么?

    1. FP32。

    2. FP8。

    3. FP64。

    4. INT32。

  6. 混合精度方法的目标是什么?

    1. 混合精度试图在训练过程执行期间采用较低精度的格式。

    2. 混合精度试图在训练过程执行期间采用较高精度的格式。

    3. 混合精度在训练过程执行期间避免了使用较低精度的格式。

    4. 混合精度在训练过程执行期间避免了使用更高精度的格式。

  7. 使用 AMP 方法而不是手动实现的主要优势是什么?

    1. 简单使用和性能提升的减少。

    2. 简单使用和减少功耗。

    3. 复杂使用和避免涉及数值表示的错误。

    4. 简单使用和避免涉及数值表示的错误。

  8. 除了缺少低精度操作外,以下哪个选项是不使用纯低精度方法进行训练过程的另一个原因?

    1. 低性能提升。

    2. 高能耗。

    3. 梯度信息的丢失。

    4. 高使用主存储器。

总结

在本章中,您学习到采用混合精度方法可以加速我们模型的训练过程。

尽管可以手动实现混合精度策略,但最好依赖于 PyTorch 提供的 AMP 解决方案,因为这是一个优雅且无缝的过程,旨在避免涉及数值表示的错误发生。当出现此类错误时,它们非常难以识别和解决。

在 PyTorch 上实施 AMP 需要在原始代码中添加几行额外的代码。基本上,我们必须将训练循环包装在 AMP 引擎中,启用与后端库相关的四个标志,并实例化梯度放大器。

根据 GPU 架构、库版本和模型本身,我们可以显著改善训练过程的性能。

本章结束了本书的第二部分。接下来,在第三部分中,我们将学习如何将训练过程分布在多个 GPU 和机器上。

第三部分:进入分布式

在这部分中,您将学习如何将训练过程分布到多个设备和机器上。首先,您将了解与分布式训练过程相关的基本概念。然后,您将学习如何在单台机器上的多个 CPU 上分布训练过程。之后,您将学习如何通过多个 GPU 在单台机器上训练模型。最后,您将学习如何在多台机器上的多个设备之间分布训练过程。

本部分包括以下章节:

  • 第八章, 一览分布式训练

  • 第九章, 使用多个 CPU 进行训练

  • 第十章, 使用多个 GPU 进行训练

  • 第十一章, 使用多台机器进行训练

第八章:一览分布式训练

当我们在现实生活中面对复杂问题时,通常会尝试通过将大问题分解为易于处理的小部分来解决。因此,通过结合从原始问题的小部分获得的部分解决方案,我们达到最终解决方案。这种称为分而治之的策略经常用于解决计算任务。我们可以说这种方法是并行和分布式计算领域的基础。

原来,将一个大问题分解成小块的想法在加速复杂模型的训练过程中非常有用。在单个资源无法在合理时间内训练模型的情况下,唯一的出路是分解训练过程并将其分散到多个资源中。换句话说,我们需要分布训练过程

您将在本章中学到以下内容:

  • 分布式训练的基本概念

  • 用于分散训练过程的并行策略

  • 在 PyTorch 中实现分布训练的基本工作流程

技术要求

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

您可以访问您喜爱的环境来执行提供的代码,例如 Google Colab 或 Kaggle。

分布式训练的首次介绍

我们将从讨论将训练过程分布到多个资源中的原因开始本章。然后,我们将了解通常用于执行此过程的资源。

我们何时需要分布训练过程?

分布训练过程最常见的原因涉及加速构建过程。假设训练过程花费很长时间并且我们手头有多个资源,那么我们应该考虑在这些各种资源之间分布训练过程以减少训练时间。

将大型模型加载到单个资源中存在内存泄漏的第二个动机与分布式训练相关。在这种情况下,我们依靠分布式训练将大型模型的不同部分分配到不同的设备或资源中,以便可以将模型加载到系统中。

然而,分布式训练并非解决所有问题的灵丹妙药。在许多情况下,分布式训练可以达到与传统执行相同的性能,或者在某些情况下甚至更差。这是因为准备初始设置和在多个资源之间进行通信所带来的额外开销可能会抵消并行运行训练过程的好处。此外,我们可以首先尝试简化模型的复杂性,如第六章中描述的那样,简化模型,而不是立即转向分布式方法。如果成功,简化过程的结果模型现在可能适合设备上运行。

因此,分布式训练并不总是减少训练时间或将模型适配到给定资源的正确答案。因此,建议冷静下来,并仔细分析分布式训练是否有望解决问题。简而言之,我们可以使用图8.1 中描述的流程图来决定何时采用传统或分布式方法:

图 8.1 – 用于决定何时使用传统或分布式方法的流程图

图 8.1 – 用于决定何时使用传统或分布式方法的流程图

面对内存泄漏或长时间训练,我们应该在考虑采用分布式方法之前,应用所有可能的性能改进技术。通过这样做,我们可以避免诸如浪费未有效使用的分配资源之类的问题。

除了决定何时分发训练过程外,我们还应评估在分布式方法中使用的资源量。一个常见的错误是获取所有可用资源来执行分布式训练,假设资源量越多,训练模型的时间就越短。然而,并没有保证增加资源量将会带来更好的性能。结果甚至可能更糟,正如我们之前讨论过的那样。

总之,分布式训练在训练过程需要较长时间完成或模型无法适应给定资源的情况下非常有用。由于这两种情况都可以通过应用性能改进技术来解决,因此我们应首先尝试这些方法,然后再考虑采用分布式策略。否则,我们可能会面临资源浪费等副作用。

在接下来的部分,我们将对用于执行此过程的计算资源提供更高层次的解释。

我们在哪里执行分布式训练?

更一般地说,我们可以说分布式训练涉及将训练过程划分为多个部分,每个部分管理整个训练过程的一部分。每个部分都分配在单独的计算资源上运行。

在分布式训练的背景下,我们可以在 CPU 或加速器设备上运行部分训练过程。尽管 GPU 是常用于此目的的加速器设备,但还存在其他不太流行的选项,如 FPGA、XPU 和 TPU。

这些计算资源可以在单台机器上或分布在多台服务器上。此外,一台单机可以拥有一个或多个这些资源。

换句话说,我们可以在 一台具有多个计算资源的机器 上分发训练过程,也可以跨 具有单个或多个资源的多台机器 进行分布。为了更容易理解这一点,Figure 8**.2 描述了在分布式训练过程中可以使用的可能计算资源安排:

Figure 8.2 – 计算资源的可能安排

Figure 8.2 – 计算资源的可能安排

安排 A,即将多个设备放置在单个服务器中,是运行分布式训练过程最简单最快的配置。正如我们将在 第十一章 中了解的那样,使用多台机器进行训练,在多台机器上运行训练过程取决于用于互连节点的网络提供的性能。

尽管网络的性能表现不错,但使用此附加组件可能会单独降低性能。因此,尽可能采用安排 A,以避免使用网络互连。

关于安排 BC,最好使用后者,因为它具有更高的每台机器设备比率。因此,我们可以将分布式训练过程集中在较少数量的机器上,从而避免使用网络。

然而,即使没有安排 AC,使用安排 B 仍然是一个好主意。即使受到网络施加的瓶颈限制,分布式训练过程很可能会胜过传统方法。

通常情况下,GPU 不会在多个训练实例之间共享 – 即分布式训练过程会为每个 GPU 分配一个训练实例。在 CPU 的情况下,情况有所不同:一个 CPU 可以执行多个训练实例。这是因为 CPU 是一个多核设备,因此可以分配不同的计算核心来运行不同的训练实例。

例如,我们可以在具有 32 个计算核心的 CPU 中运行两个训练实例,其中每个训练实例使用可用核心的一半,如 Figure 8**.3 所示:

Figure 8.3 – 使用不同计算核心运行不同训练实例

Figure 8.3 – 使用不同计算核心运行不同训练实例

尽管可以以这种方式运行分布式训练,但通常情况下在单台(或多台)多个 GPU 或多台多个机器上运行更为常见。这种配置在许多情况下可能是唯一的选择,因此了解如何操作非常重要。我们将在第十章使用 多个 CPU,中详细了解更多。

在了解了分布式训练世界之后,现在是时候跳到下一节,您将在这一节中学习这种方法的基本概念。

学习并行策略的基础知识

在前一节中,我们了解到分布式训练方法将整个训练过程分解为小部分。因此,整个训练过程可以并行解决,因为这些小部分中的每一个在不同的计算资源中同时执行。

并行策略定义如何将训练过程分解为小部分。主要有两种并行策略:模型并行和数据并行。接下来的章节将详细解释这两种策略。

模型并行

模型并行将训练过程中执行的操作集合划分为较小的计算任务子集。通过这样做,分布式过程可以在不同的计算资源上并行运行这些较小的操作子集,从而加快整个训练过程。

结果表明,在前向和后向阶段执行的操作彼此并非独立。换句话说,一个操作的执行通常依赖于另一个操作生成的输出。由于这种约束,模型并行并不容易实现。

尽管如此,卓越的人类头脑发明了三种技术来解决这个问题:层间操作内操作间范式。让我们深入了解。

层间范式

层间范式中,每个模型层在不同的计算资源上并行执行,如图 8.4所示:

图 8.4 – 层间模型并行范式

图 8.4 – 层间模型并行范式

然而,由于给定层的计算通常依赖于另一层的结果,层间范式需要依赖特定策略来实现这些条件下的分布式训练。

在采用这种范式时,分布式训练过程建立了一个连续的训练流程,使神经网络在同一时间处理多个训练步骤 – 也就是说,同时处理多个样本。随着事情的发展,一个层在给定的训练步骤中所需的输入已经在训练流程中被处理,并且现在可用作该层的输入。

因此,在特定时刻,分布式训练过程可以并行执行不同层。这个过程在前向和反向阶段都执行,从而进一步提高可以同时计算的任务的并行性水平。

在某种程度上,这种范式与现代处理器中实现的指令流水线技术非常相似,即多个硬件指令并行执行。由于这种相似性,内部层范式也被称为流水线并行主义,其中各阶段类似于训练步骤。

跨操作范式

跨操作范式依赖于将在每个层上执行的操作集合分成更小的可并行计算任务的,如图8**.5所示。每个这些计算任务块在不同的计算资源上执行,因此并行化层的执行。在计算所有块之后,分布式训练过程将来自每个块的部分结果组合以得出层输出:

图 8.5 – 跨操作模型并行主义范式

图 8.5 – 跨操作模型并行主义范式

由于在层内执行的操作之间存在依赖关系,跨操作范式无法将依赖操作放入不同的块中。这种约束对将操作分割为并行计算任务块施加了额外的压力。

例如,考虑图8**.6中所示的图形,它表示在层中执行的计算。该图由两个输入数据块(矩形)和四个操作(圆形)组成,箭头表示操作之间的数据流动:

图 8.6 – 跨操作范式中操作分区的示例

图 8.6 – 跨操作范式中操作分区的示例

易于看出,操作 1 和 2 仅依赖于输入数据,而操作 3 需要操作 1 的输出来执行其计算。操作 4 在图中的依赖最强,因为它依赖于操作 2 和 3 的结果才能执行。

因此,如图8**.6所示,这个图的独特分区为此图创建了两个并行操作块,以同时运行操作 1 和 2。由于操作 3 和 4 依赖于先前的结果,它们在其他任务完成之前无法执行。因此,根据层内操作之间的依赖程度,跨操作范式无法实现更高水平的并行性。

内部操作范式

内部操作范式将操作的执行分成较小的计算任务,其中每个计算任务在不同的输入数据块中应用操作。通常,跨操作方法需要结合部分结果来完成操作。

虽然间操作在不同计算资源上运行不同操作,内操作则将同一操作的部分分布到不同计算资源上,如图8**.7所示:

图 8.7 – 内操作模型并行化范式

图 8.7 – 内操作模型并行化范式

例如,考虑一种情况,即图8**.8中所示,一个层执行矩阵到矩阵乘法。通过采用内操作范式,这种乘法可以分成两部分,其中每部分将在矩阵 A 和 B 的不同数据块上执行乘法。由于这些部分乘法彼此独立,因此可以同时在不同设备上运行这两个任务:

图 8.8 – 内操作范式中数据分区示例

图 8.8 – 内操作范式中数据分区示例

在执行这两个计算任务之后,内操作方法需要将部分结果合并以生成最终的矩阵。

根据操作类型和输入数据的大小,内操作可以实现合理的并行性水平,因为可以创建更多数据块并提交给额外的计算资源。

然而,如果数据太少或操作太简单而无法进行计算,将计算分布到不同设备可能会增加额外的开销,超过并行执行操作的潜在性能改进。这种情况适用于内操作和间操作方法。

摘要

总结我们在本节学到的内容,表 8.1涵盖了每种模型并行化范式的主要特征:

范式 策略
间层 并行处理层
内操作 并行计算不同操作
间操作 并行计算同一操作的部分

表 8.1 – 模型并行化范式总结

虽然模型并行化可以加速训练过程,但它也有显著的缺点,比如扩展性差和资源使用不均衡,除此之外,还高度依赖网络架构。这些问题解释了为什么这种并行策略在数据科学家中并不那么流行,并且通常不是分布式训练过程的首选。

即便如此,模型并行化可能是在模型不适合计算资源的情况下的独特解决方案——也就是说,当设备内存不足以分配整个模型时。这种情况适用于大型语言模型LLMs),这些模型通常有数千个参数,在内存中加载时占用大量字节。

另一种策略,称为数据并行化,更加健壮、可扩展且实现简单,我们将在下一节中学习。

数据并行化

数据并行策略的思想非常容易理解。与将网络执行的计算任务集合进行分割不同,数据并行策略将训练数据集分成更小的数据块,并使用这些数据块来训练原始模型的不同副本,如图8**.9所示。由于每个模型副本彼此独立,它们可以并行训练:

图 8.9 – 数据并行策略

图 8.9 – 数据并行策略

在每个训练步骤结束时,分布式训练过程启动一个同步阶段,以更新所有模型副本的权重。此同步阶段负责收集并分享所有在不同计算资源中运行的模型之间的平均梯度。收到平均梯度后,每个副本根据此共享信息调整其权重。

同步阶段是数据并行策略的核心机制。简单来说,它确保了模型副本在执行单个训练步骤后获得的知识被与其他副本共享,反之亦然。因此,在完成分布式训练过程时,生成的模型具有与传统训练相同的知识:

图 8.10 – 数据并行中的同步阶段

图 8.10 – 数据并行中的同步阶段

有半打方法执行此同步阶段,包括参数服务器全 reduce。前者在可扩展性方面表现不佳,因为使用唯一服务器聚合每个模型副本获得的梯度,计算平均梯度,并将其发送到各处。随着训练过程数量的增加,参数服务器成为分布式训练过程的主要瓶颈。

另一方面,全 reduce 技术具有更高的可扩展性,因为所有训练实例均匀参与更新过程。因此,此技术已被所有框架和通信库广泛采用,以同步分布式训练过程的参数。

我们将在下一节详细了解它。

全 reduce 同步

全 reduce 是一种集体通信技术,用于简化由多个进程执行的计算。由于全 reduce 源自 reduce 操作,让我们在描述全 reduce 通信原语之前了解这种技术。

在分布式和并行计算的背景下,reduce 操作在多个进程中执行一个函数,并将该函数的结果发送给一个根进程。Reduce 操作可以执行任何函数,尽管通常应用于诸如求和、乘法、平均值、最大值和最小值等简单的函数。

图 8**.11 展示了将减少操作应用于四个进程持有的向量的示例。在这个示例中,减少原语执行四个向量的和,并将结果发送到进程 0,这是此场景中的根进程。

图 8.11 – Reduce 操作

图 8.11 – Reduce 操作

All-Reduce 操作是减少原语的一个特例,其中所有进程接收函数的结果,如 图 8**.12 所示。因此,与仅将结果发送给根进程不同,All-Reduce 将结果与参与计算的所有进程共享。

图 8.12 – All-Reduce 操作

图 8.12 – All-Reduce 操作

有不同的方式可以实施 All-Reduce 操作。在分布式训练环境中,最有效的解决方案之一是环形 All-Reduce。在这种实现中,进程使用逻辑环形拓扑(如 图 8**.13 所示)在它们之间交换信息。

图 8.13 – 环形 All-Reduce 实现

图 8.13 – 环形 All-Reduce 实现

信息通过环路流动,直到所有进程最终拥有相同的数据。有一些库提供了优化版本的环形 All-Reduce 实现,比如 NVIDIA 的 NCCL 和 Intel 的 oneCCL。

总结

数据并行性易于理解和实施,而且灵活且可扩展。然而,正如万事皆有不完美之处一样,这种策略也有其缺点。

尽管与模型并行主义方法相比,它提供了更高层次的并行性,但它可能面临一些限制因素,这些因素可能阻碍其实现高度的可扩展性。由于每个训练步骤后梯度在所有副本之间共享,这些副本之间的通信延迟可能会减慢整个训练过程。

此外,数据并行策略并未解决训练大模型的问题,因为模型完全如原样加载到设备上。同一个大模型将以不同的计算资源加载,这反过来将无法支持它们。对于无法放入设备的模型,问题依旧存在。

即便如此,如今,数据并行策略是分布式训练过程的直接途径。这种策略的简单性和灵活性使其能够训练广泛的模型类型和架构,因此成为了默认的分布式训练选择。从现在开始,我们将使用术语分布式训练来指代基于数据并行策略的分布式训练。

构建机器学习模型的最常用框架都内置了分布式训练的实现,PyTorch 也不例外!在接下来的部分,我们将首次探讨如何实施这个过程。

PyTorch 上的分布式训练

本节介绍了在 PyTorch 上实现分布式训练的基本工作流程,同时介绍了此过程中使用的组件。

基本工作流程

总体来说,实现 PyTorch 分布式训练的基本工作流程包括图 8**.14中展示的步骤:

图 8.14 – 在 PyTorch 中实现分布式训练的基本工作流程

图 8.14 – 在 PyTorch 中实现分布式训练的基本工作流程

让我们更详细地看看每一步。

注意

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

初始化和销毁通信组

通信组是 PyTorch 用来定义和控制分布式环境的逻辑实体。因此,编写分布式训练的第一步涉及初始化通信组。通过实例化torch.distributed类的对象并调用init_process_group方法来执行此步骤,如下所示:

import torch.distributed as distdist.init_process_group()

严格来说,初始化方法不需要任何参数。但是,有两个重要的参数,虽然不是必须的。这些参数允许我们选择通信后端和初始化方法。我们将在第九章中学习这些参数,使用多 CPU 进行训练

在创建通信组时,PyTorch 识别将参与分布式训练的进程,并为每个进程分配一个唯一标识符。这个标识符称为get_rank方法:

my_rank = dist.get_rank()

由于所有进程执行相同的代码,我们可以使用 rank 来区分给定进程的执行流程,从而将特定任务的执行分配给特定进程。例如,我们可以使用 rank 来分配执行最终模型评估的责任:

if my_rank == 0:    test(ddp_model, test_loader, device)

在分布式训练中执行的最后一步涉及销毁通信组,这在代码开头创建。这个过程通过调用destroy_process_group()方法来执行:

dist.destroy_process_group()

终止通信组是重要的,因为它告诉所有进程分布式训练已经结束。

实例化分布式数据加载器

由于我们正在实现数据并行策略,将训练数据集划分为小数据块以供每个模型副本使用是必需的。换句话说,我们需要实例化一个数据加载器,它了解分布式训练过程。

在 PyTorch 中,我们依赖于DistributedSampler组件来简化这个任务。DistributedSampler组件将程序员不需要的所有细节抽象化,并且非常易于使用:

from torch.utils.data.distributed import DistributedSamplerdist_loader = DistributedSampler(train_dataset)
train_loader = torch.utils.data.DataLoader(train_dataset,
                                           batch_size=batch_size,
                                           shuffle=False,
                                           sampler=dist_loader)

唯一的变化是在原始的DataLoader创建行中添加了一个额外的参数,称为samplersampler参数必须填写一个从DistributedSampler组件实例化的对象,该对象仅需要原始数据集对象作为输入参数。

最终的数据加载器已准备好处理分布式训练过程。

实例化分布式模型

现在已经有了通信组和准备好的分布式数据加载器,是时候实例化原始模型的分布式版本了。

PyTorch 提供了本地的DistributedDataParallel组件(简称 DDP),用于封装原始模型并准备以分布式方式进行训练。DDP 返回一个新的模型对象,然后用于执行分布式训练过程:

from torch.nn.parallel import DistributedDataParallel as DDPmodel = CNN()
ddp_model = DDP(model)

在实例化分布式模型之后,所有进一步的步骤都在分布式模型的版本上执行。例如,优化器接收分布式模型作为参数,而不是原始模型:

optimizer = optimizer(ddp_model.parameters(), lr,                       weight_decay=weight_decay)

此时,我们已经具备运行分布式训练过程所需的一切。

运行分布式训练过程

令人惊讶的是,在分布式方式下执行训练循环几乎与执行传统训练相同。唯一的区别在于将 DDP 模型作为参数传递,而不是原始模型:

train(ddp_model, train_loader, num_epochs, criterion, optimizer,       device)

除此之外不需要任何其他内容,因为到目前为止使用的组件具有执行分布式训练过程的内在功能。

PyTorch 持续运行分布式训练过程,直到达到定义的 epoch 数。在完成每个训练步骤后,PyTorch 会自动在模型副本之间同步权重。程序员无需进行任何干预。

检查点和保存训练状态

由于分布式训练过程可能需要很多小时才能完成,并涉及不同的计算资源和设备,因此更容易受到故障的影响。

因此,建议定期检查点保存当前训练状态,以便在出现故障时恢复训练过程。我们将在第十章使用多个 GPU 进行训练中详细讨论此主题。

总结

我们可能需要实例化其他模块和对象来实现分布式训练的特殊功能,但这个工作流程通常足以编写一个基本的——虽然功能齐全的——分布式训练实现。

通信后端和程序启动器

在 PyTorch 上实现分布式训练涉及定义一个通信后端,并使用程序启动器在多个计算资源上执行进程。

下面的小节简要解释了每个组件。

通信后端

正如我们之前所学的,在分布式训练过程中,模型副本彼此交换梯度信息。从另一个角度来看,运行在不同计算资源上的进程必须彼此通信,以传播这些数据。

同样地,PyTorch 依赖于后端软件来执行模型编译和多线程操作。它还依赖于通信后端来提供模型副本之间优化的通信渠道。

有些通信后端专注于与高性能网络配合工作,而其他一些适合处理单台机器内多个设备之间的通信。

PyTorch 支持的最常见的通信后端包括 Gloo、MPI、NCCL 和 oneCCL。每个这些后端在特定场景下的使用都非常有趣,我们将在接下来的几章中了解到。

程序启动器

运行分布式训练并不同于执行传统的训练过程。任何分布式和并行程序的执行都与运行任何传统和顺序程序有显著的区别。

在 PyTorch 的分布式训练环境中,我们使用程序启动器来启动分布式进程。这个工具负责设置环境并在操作系统中创建进程,无论是本地还是远程。

用于此目的的最常见启动器包括mp.spawn,该启动器由torch.multiprocessing包提供。

将所有内容整合起来

图 8.15 所示的分布式训练过程概念图展示了 PyTorch 提供的组件和资源:

图 8.15 – PyTorch 分布式训练概念图

图 8.15 – PyTorch 分布式训练概念图

正如我们所学的,PyTorch 依赖于通信后端来控制多个计算资源之间的通信,并使用程序启动器将分布式训练提交到本地或远程操作系统。

有多种方法可以完成同样的事情。例如,我们可以使用某种程序启动器基于两种不同的通信后端执行分布式训练。反之亦然 – 也就是说,有些通信后端支持多个启动器的情况。

因此,定义元组通信后端 x 程序启动器将取决于分布式训练过程中使用的环境和资源。在接下来的几章中,我们将更多地了解这一点。

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

测验时间!

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

注意

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

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

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

  1. 分布训练的两个主要原因是什么?

    1. 可靠性和性能改进。

    2. 内存泄漏和功耗。

    3. 功耗和性能改进。

    4. 内存泄漏和性能改进。

  2. 分布训练过程的两个主要并行策略是哪些?

    1. 模型和数据并行。

    2. 模型和硬件并行。

    3. 硬件和数据并行。

    4. 软件和硬件并行。

  3. 模型并行主义方法使用哪种范式?

    1. 模型间。

    2. 间数据。

    3. 内操作。

    4. 参数间。

  4. 什么是内操作范式并行处理?

    1. 不同操作。

    2. 相同操作的部分。

    3. 模型的层。

    4. 数据集样本。

  5. 除参数服务器外,数据并行策略还使用了哪种同步方法?

    1. 所有操作。

    2. 全部聚集。

    3. 全部减少。

    4. 全部分散。

  6. 在 PyTorch 中执行分布式训练的第一步是什么?

    1. 初始化通信组。

    2. 初始化模型副本。

    3. 初始化数据加载器。

    4. 初始化容器环境。

  7. 在 PyTorch 中的分布式训练背景下,使用哪个组件来启动分布式过程?

    1. 执行库。

    2. 通信后端。

    3. 程序启动器。

    4. 编译器后端。

  8. PyTorch 支持哪些作为通信后端?

    1. NDL。

    2. MPI。

    3. AMP。

    4. NNI。

总结

在本章中,您学到了分布式训练有助于加速训练过程以及训练不适合设备内存的模型。虽然分布式可能是这两种情况的出路,但在采用分布式之前,我们必须考虑应用性能改进技术。

我们可以通过采用模型并行策略或数据并行策略来进行分布式训练。前者采用不同的范式将模型计算分配到多个计算资源中,而后者创建模型副本,以便在训练数据集的各个部分上进行训练。

我们还了解到,PyTorch 依赖于第三方组件,如通信后端和程序启动器来执行分布式训练过程。

在下一章中,我们将学习如何分散分布式训练过程,使其可以在单台机器上的多个 CPU 上运行。

第九章:使用多个 CPU 进行训练

当加速模型构建过程时,我们立即想到配备 GPU 设备的机器。如果我告诉您,在仅配备多核处理器的机器上运行分布式训练是可能且有利的,您会怎么想?

尽管从 GPU 获得的性能提升是无法比拟的,但我们不应轻视现代 CPU 提供的计算能力。处理器供应商不断增加 CPU 上的计算核心数量,此外还创建了复杂的机制来处理共享资源的访问争用。

使用 CPU 来运行分布式训练对于我们无法轻松访问 GPU 设备的情况尤其有趣。因此,学习这个主题对丰富我们关于分布式训练的知识至关重要。

在本章中,我们展示了如何通过采用通用方法并使用 Intel oneCCL 后端,在单台机器上的多个 CPU 上执行分布式训练过程。

以下是本章的学习内容:

  • 在多个 CPU 上分布训练的优势

  • 如何在多个 CPU 之间分布训练过程

  • 如何通过使用 Intel oneCCL 来突破分布式训练

技术要求

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

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

为什么要在多个 CPU 上分布训练?

乍一看,想象将训练过程分布在单台机器上的多个 CPU 之间听起来有点令人困惑。毕竟,我们可以增加训练过程中使用的线程数以分配所有可用的 CPU(计算核心)。

然而,正如巴西著名诗人卡洛斯·德·安德拉德所说,“在路中央有一块石头。在路中央有一块石头。” 让我们看看在具有多个核心的机器上仅增加线程数量时训练过程会发生什么。

为什么不增加线程数?

第四章使用专用库中,我们了解到 PyTorch 依赖于 OpenMP 通过采用多线程技术来加速训练过程。OpenMP 将线程分配给物理核心,旨在改善训练过程的性能。

因此,如果我们有一定数量的可用计算核心,为什么不增加训练过程中使用的线程数,而不是考虑分布式呢?答案实际上很简单。

当使用多线程运行训练过程时,PyTorch 对并行性的限制意味着在超过某个线程数后,性能不会有所提升。简单来说,达到一定阈值后,训练时间将保持不变,无论我们使用多少额外的核心来训练模型。

这种行为不仅限于 PyTorch 执行的训练过程。在许多种类的并行应用中,这种情况都很普遍。根据问题和并行策略的设计,增加线程数可能会导致并行任务变得太小和简单,以至于并行化问题的好处会被控制每个并行任务的开销所抑制。

看一个行为的实际例子。表 9.1 展示了使用一台装备有 16 个物理核心的机器,在 CIFAR-10 数据集上训练 CNN 模型五个周期的执行时间:

线程 执行时间
1 311
2 189
4 119
8 93
12 73
16 73

表 9.1 – 训练过程的执行时间

表 9.1 所示,无论是使用 12 还是 16 个核心来训练模型,执行时间都没有差异。由于并行级别的限制,尽管核心数量增加了超过 30%,PyTorch 在相同的执行时间上被限制住了。而且,即使训练过程使用了比之前多 50% 的线程(8 到 12),性能改善也不到 27%。

这些结果表明,在这种情况下,使用超过八个线程执行训练过程将不会显著减少执行时间。因此,我们会因为 PyTorch 分配了一定数量的核心而产生资源浪费,这些核心并没有加速训练过程。实际上,线程数越多,可能会增加通信和控制任务的开销,从而减慢训练过程。

要解决这种相反的效果,我们应考虑通过在同一台机器上运行不同的训练实例来分布训练过程。与其看代码,让我们直接看结果,这样您就能看到这种策略的好处!

救援上的分布式训练

我们使用与前一个实验相同的模型、参数和数据集进行了以下测试。当然,我们也使用了同一台机器。

在第一个测试中,我们创建了两个分布式训练过程的实例,每个实例使用八个核心,如 图 9.1 所示:

图 9.1 – 分布式训练实例的分配

图 9.1 – 分布式训练实例的分配

分布式训练过程花费了 58 秒完成,代表了执行模型构建过程所需时间的26%改善。我们通过采用并行数据策略技术将执行时间减少了超过 25%。除此之外,硬件能力和软件堆栈都没有改变。此外,性能改善对于具有更多计算核心的机器来说可能会更高。

然而,正如本书中一直所言,一切通常都有代价。在这种情况下,成本与模型准确性有关。传统训练过程构建的模型准确性为 45.34%,而分布式训练创建的模型达到了 44.01%的准确性。尽管差异很小(约为 1.33%),但我们不应忽视它,因为模型准确性与分布式训练实例的数量之间存在关系。

表格 9.2展示了涉及不同训练实例组合和每个训练实例使用的线程数的测试结果。由于测试是在一个具有 16 个物理核心的机器上执行的,并考虑到 2 的幂次方,我们有三种可能的训练实例和线程组合:

训练实例 线程数 执行时间 准确性
2 8 58 44.01%
4 4 45 40.11%
8 2 37 38.63%

表格 9.2 – 分布式训练过程的执行时间

表格 9.2所示,训练实例数量越多,模型准确性越低。这种行为是预期的,因为模型副本根据平均梯度更新其参数,这导致了关于优化过程的信息损失。

相反,训练实例数量增加时,执行时间减少。当使用每个 8 个训练实例 2 个线程时,分布式训练过程仅需 37 秒即可完成,几乎比使用 16 个线程的传统训练快两倍。然而,准确性从 45%下降到 39%。

无可否认,将训练过程分布在多个处理核心之间在加速训练过程方面是有利的。我们只需关注模型的准确性。

在下一节中,我们将学习如何在多个 CPU 上编码和运行分布式训练。

在多个 CPU 上实施分布式训练

本节展示了如何使用Gloo,一种简单而强大的通信后端,在多个 CPU 上实施和运行分布式训练。

Gloo 通信后端

第八章一瞥分布式训练,我们学习到 PyTorch 依赖于后端来控制涉及到的设备和机器之间的通信。

PyTorch 支持的最基本通信后端称为 Gloo。这个后端默认随 PyTorch 提供,不需要特别的配置。Gloo 后端是由 Facebook 创建的集体通信库,现在是一个由 BSD 许可证管理的开源项目。

注意

你可以在 github.com/facebookincubator/gloo 找到 Gloo 的源代码。

由于 Gloo 使用简单且在 PyTorch 中默认可用,因此看起来是在只包含 CPU 和通过常规网络连接的机器的环境中运行分布式训练的第一选择。让我们在接下来的几节中实际看一下这个后端的运作。

编写在多个 CPU 上运行分布式训练的代码

本节展示了在 单机多核心 上运行分布式训练过程的代码。这段代码基本与 第八章 中展示的一致,只是与当前情境相关的一些细节不同。

注意

本节展示的完整代码可以在 github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter09/gloo_distributed-cnn_cifar10.py 找到。

因此,本节描述了调整基本工作流程以在多核心上运行分布式训练所需的主要更改。基本上,我们需要进行两个修改,如下两个部分所述。

初始化通信组

第一个修改涉及通信组的初始化。不再像以前那样调用 dist.init_process_group 而是要传入两个参数,正如我们在 第八章 中提到的 一览分布式训练

dist.init_process_group(backend="gloo", init_method="env://")

backend 参数告诉 PyTorch 使用哪个通信后端来控制多个训练实例之间的通信。在这个主要的例子中,我们将使用 Gloo 作为通信后端。所以,我们只需要向参数传递后端名称的小写字符串。

注意

要检查后端是否可用,我们可以执行 torch.distributed.is_<backend>_available() 命令。例如,要验证当前 PyTorch 环境中是否有 Gloo 可用,我们只需要调用 torch.distributed.is_gloo_available()。该方法在可用时返回 True,否则返回 False

第二个名为 init_method 的参数,定义了 PyTorch 用于创建通信组的初始化方法。该方法告诉 PyTorch 如何获取其初始化分布式环境所需的信息。

现在,有三种可能的方法来通知初始化通信组所需的配置:

  • TCP:使用指定的 IP 地址和 TCP 端口

  • 共享文件系统:使用一个对所有参与通信组的进程都可访问的文件系统

  • 环境变量:使用操作系统范围内定义的环境变量

正如你可能猜到的那样,这个例子中使用的 env:// 值,指的是初始化通信组的第三种方法,即环境变量选项。在下一节中,我们将了解用于设置通信组的环境变量。现在,重要的是记住 PyTorch 如何获取所需信息以建立通信组。

CPU 分配映射

第二次修改 指的是定义每个训练实例中线程分配到不同核心的操作。通过这样做,我们确保所有线程使用独占的计算资源,不会竞争给定的处理核心。

为了解释这意味着什么,让我们举一个实际例子。假设我们希望在一个具有 16 个物理核心的机器上运行分布式训练。我们决定运行两个训练实例,每个实例使用八个线程。如果我们不注意这些线程的分配,两个训练实例可能会竞争同一个计算核心,导致性能瓶颈。这恰恰是我们所不希望的。

要避免这个问题,我们必须在代码开始时为所有线程定义分配映射。以下代码片段显示了如何做到这一点:

import osnum_threads = 8
index = int(os.environ['RANK']) * num_threads
cpu_affinity = "{}-{}".format(index, (index + num_threads) - 1)
os.environ['OMP_NUM_THREADS'] = "{}".format(num_threads)
os.environ['KMP_AFFINITY'] = \
    "granularity=fine,explicit,proclist=[{}]".format(cpu_affinity)

注意

需要记住,所有通信组进程执行相同的代码。如果我们需要为进程定义不同的执行流程,必须使用等级。

让我们逐行理解这段代码在做什么。

我们首先定义每个参与分布式训练的进程使用的线程数:

num_threads = 8

接下来,我们计算进程的 index,考虑其等级和线程数。等级从称为 RANK 的环境变量中获得,该变量由程序启动器正确定义:

index = int(os.environ['RANK']) * num_threads

此索引用于标识分配给该进程的第一个处理核心。例如,考虑到 8 个线程和两个进程的情况,等级为 0 和 1 的进程的索引分别为 0 和 8。

从该索引开始,每个进程将为其线程分配后续的核心。因此,以前述场景为例,等级为 0 的进程将把其线程分配给计算核心 0、1、2、3、4、5、6 和 7。同样,等级为 1 的进程将使用计算核心 8、9、10、11、12、13、14 和 15。

由于 OpenMP 接受间隔列表格式作为设置 CPU 亲和性的输入,我们可以通过指示第一个和最后一个核心来定义分配映射。第一个核心是索引,最后一个核心通过将索引与线程数相加并从 1 中减去来获得:

cpu_affinity = "{}-{}".format(index, (index + num_threads) - 1)

在考虑我们的例子时,等级为 0 和 1 的进程将使用变量 cpu_affinity 分别设置为“0-7”和“8-15”。

我们代码片段的最后两行根据之前获得的值定义了 OMP_NUM_THREADSKMP_AFFINITY 环境变量:

os.environ['OMP_NUM_THREADS'] = "{}".format(num_threads)os.environ['KMP_AFFINITY'] = \
    "granularity=fine,explicit,proclist=[{}]".format(cpu_affinity)

正如您应该记得的那样,这些变量用于控制 OpenMP 的行为。OMP_NUM_THREADS 变量告诉 OpenMP 在多线程中使用的线程数,KMP_AFFINITY 定义了这些线程的 CPU 亲和性。

这两个修改足以调整第八章中介绍的基本工作流程,以在多个 CPU 上执行分布式训练。

当代码准备好执行时,接下来的步骤涉及定义程序启动器和配置参数以启动分布式训练。

在多个 CPU 上启动分布式训练

正如我们在第八章中学到的,一览分布式训练,PyTorch 依赖程序启动器来设置分布环境并创建运行分布式训练所需的进程。

对于这个场景,我们将使用 torchrun,它是一个本地的 PyTorch 启动器。除了使用简单外,torchrun 已经包含在默认的 PyTorch 安装中。让我们看看关于这个工具的更多细节。

torchrun

粗略地说,torchrun 执行两个主要任务:定义与分布式环境相关的环境变量在操作系统上实例化进程

torchrun 定义了一组环境变量,用于通知 PyTorch 关于初始化通信组所需的参数。设置适当的环境变量后,torchrun 将创建参与分布式训练的进程。

注意

除了这两个主要任务外,torchrun 还提供了更多高级功能,如恢复失败的训练进程或动态调整训练阶段使用的资源。

要在单台机器上运行分布式训练,torchrun 需要一些参数:

  • nnodes: 分布式训练中使用的节点数

  • nproc-per-node: 每台机器上运行的进程数

  • master-addr:用于运行分布式训练的机器的 IP 地址

执行我们示例的 torchrun 命令如下:

maicon@packt:~$ torchrun --nnodes 1 --nproc-per-node 2 --master-addr localhost pytorch_ddp.py

由于分布式训练将在单台机器上运行,我们将 nnodes 参数设置为 1,并将 master-addr 参数设置为 localhost,这是本地机器的别名。在此示例中,我们希望运行两个训练实例;因此,nproc-per-node 参数设置为 2

从这些参数中,torchrun 将设置适当的环境变量,并在本地操作系统上实例化两个进程来运行程序 pytorch_ddp.py,如图 9**.2所示。

图 9**.2 – torchrun 执行方案

图 9**.2 – torchrun 执行方案

图 9**.2所示,每个进程都有其排名,并通过 Gloo 相互通信。此外,每个进程将创建八个线程,每个线程将在不同的物理核心上运行,如 CPU 分配图中定义的那样。尽管在同一台机器上执行并在相同的 CPU die 上运行,但这些进程将作为分布式训练过程的不同实例。

为了简化操作,我们可以创建一个 bash 脚本来简化 torchrun 在不同情况下的使用。让我们在下一节学习如何做到这一点。

启动脚本

我们可以创建一个 bash 脚本来简化分布式训练过程的启动,并在具有多个计算核心的单台机器上运行它。

此启动脚本的示例如下:

TRAINING_SCRIPT=$1NPROC_PER_NODE=$2
NNODES= "1"
MASTER_ADDR= "localhost"
TORCHRUN_COMMAND="torchrun --nnodes $NNODES --nproc-per-node $NPROC_PER_NODE --master-addr $MASTER_ADDR $TRAINING_SCRIPT"
$TORCHRUN_COMMAND

重要提示

此部分显示的完整代码在 github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/scripts/chapter09/launch_multiple_cpu.sh 上可用。

此脚本设置不变的参数,如 nnodesmaster-addr,并留下可自定义的参数,例如程序的名称和 nproc-per-node,以便在执行行中定义。因此,要运行我们之前的示例,只需执行以下命令:

maicon@packt:~$ ./launch_multiple_cpu.sh pytorch_ddp.py 2

脚本 launch_multiple_cpu.sh 将使用适当的参数调用 torchrun。正如你所想象的那样,更改此脚本的参数以与其他训练程序一起使用,或者运行不同数量的训练实例,都非常简单。

此外,我们可以修改此脚本,以与 Apptainer 和 Docker 等解决方案提供的容器镜像一起使用。因此,不直接在命令行上调用 torchrun,而是在容器镜像中执行 torchrun 的脚本可以被修改为:

TRAINING_SCRIPT=$1NPROC_PER_NODE=$2
SIF_IMAGE=$3
NNODES= "1"
MASTER_ADDR= "localhost"
TORCHRUN_COMMAND="torchrun --nnodes $NNODES --nproc-per-node $NPROC_PER_NODE --master-addr $MASTER_ADDR $TRAINING_SCRIPT"
apptainer exec $SIF_IMAGE $TORCHRUN_COMMAND

考虑到一个名为 pytorch.sif 的容器镜像,这个新版本 local_launch 的命令行将如下所示:

maicon@packt:~$ ./launch_multiple_cpu_container.sh pytorch_ddp.py 2 pytorch.sif

在下一节中,我们将学习如何运行相同的分布式训练过程,但使用 Intel oneCCL 作为通信后端。

使用 Intel oneCCL 加速

Table 9.2 中显示的结果证明,Gloo 在 PyTorch 的分布式训练过程中很好地完成了通信后端的角色。

尽管如此,还有另一种选择用于通信后端,可以在 Intel 平台上更快地运行:Intel oneCCL 集体通信库。在本节中,我们将了解这个库是什么,以及如何将其用作 PyTorch 的通信后端。

Intel oneCCL 是什么?

Intel oneCCL 是由英特尔创建和维护的集体通信库。与 Gloo 类似,oneCCL 还提供诸如所谓的“全局归约”之类的集体通信原语。

Intel oneCCL 自然地优化为在 Intel 平台环境下运行,尽管这并不一定意味着它在其他平台上无法工作。我们可以使用此库在同一台机器上执行的进程之间(进程内通信)或在多节点上运行的进程之间提供集体通信。

尽管其主要用途在于为深度学习框架和应用程序提供集体通信,但任何用 C++ 或 Python 编写的分布式程序都可以使用 oneCCL。

与 Intel OpenMP 一样,Intel oneCCL 不会默认随常规 PyTorch 安装一起提供;我们需要自行安装它。在考虑基于 pip 的环境时,我们可以通过执行以下命令轻松安装 oneCCL:

pip install oneccl_bind_pt==2.1.0 --extra-index-url https://pytorch-extension.intel.com/release-whl/stable/cpu/us/

安装完 oneCCL 后,我们准备将其整合到我们的代码中,并启动分布式训练。让我们看看如何在接下来的章节中实现这一点。

注意

您可以在 oneapi-src.github.io/oneCCL/ 找到关于 Intel oneCCL 的更多信息。

代码实现和启动

要将 Intel oneCCL 作为通信后端使用,我们必须更改前一节中呈现的代码的几个部分。

注意

本节展示的完整代码可在 github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter09/oneccl_distributed-cnn_cifar10.py 找到。

第一个修改涉及导入一个 artifact 和设置三个环境变量:

import oneccl_bindings_for_pytorchos.environ['CCL_PROCESS_LAUNCHER'] = "torch"
os.environ['CCL_ATL_SHM'] = "1"
os.environ['CCL_ATL_TRANSPORT'] = "ofi"

这些环境变量配置了 oneCCL 的行为。CCL_PROCESS_LAUNCHER 参数用于与 oneCCL 通信,并启动它。在我们的情况下,必须将此环境变量设置为 torch,因为 PyTorch 在调用 oneCCL。环境变量 CCL_ATL_SHMCCL_ATL_TRANSPORT 分别设置为 1ofi,以将共享内存配置为 oneCCL 用于进程间通信的手段。

共享内存是一种进程间通信技术。

注意

您可以通过访问此网站深入了解 Intel oneCCL 的环境变量:oneapi-src.github.io/oneCCL/env-variables.html

第二次修改涉及更改初始化通信组中的后端设置:

dist.init_process_group(backend="ccl", init_method="env://")

代码的其余部分和启动方法与 Gloo 的代码相同。我们可以将 CCL_LOG_LEVEL 设置为 debugtrace 环境变量,以验证是否正在使用 oneCCL。

在进行这些修改之后,您可能会想知道 oneCCL 是否值得。让我们在下一节中找出答案。

oneCCL 真的更好吗?

表 9.3 所示,与 Gloo 的实现相比,oneCCL 将我们的训练过程加速了约 10%。如果与传统的 16 线程执行进行比较,使用 oneCCL 的性能改进几乎达到了 40%:

oneCCL Gloo
训练 实例 线程数 执行时间 准确率
2 8 53 43.12%
4 4 42 41.03%
8 2 35 37.99%

表 9.3 – 在 Intel oneCCL 和 Gloo 下运行的分布式训练过程的执行时间

关于模型的准确性,使用 oneCCL 和 Gloo 进行的分布式训练在所有场景中实际上取得了相同的结果。

因此,让我们心中产生的问题是,何时使用一种后端而不是另一种后端?如果我们使用的是基于 Intel 的环境,那么 oneCCL 更可取。毕竟,使用 Intel oneCCL 进行的训练过程比使用 Gloo 快了 10%。

另一方面,Gloo 默认与 PyTorch 一起使用,非常简单,并实现了合理的性能改进。因此,如果我们不在 Intel 平台上训练,也不追求最大可能的性能,那么 Gloo 是一个不错的选择。

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

测验时间!

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

注意

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

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

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

  1. 在多核系统中,通过增加 PyTorch 使用的线程数,可以改善训练过程的性能。关于这个话题,我们可以确认以下哪一个?

    1. 在超过一定数量的线程后,性能改进可能会恶化或保持不变。

    2. 性能改善始终在增加,无论线程数量如何。

    3. 增加线程数时没有性能改善。

    4. 仅使用 16 个线程时才能实现性能改善。

  2. PyTorch 支持的最基本的通信后端是哪一个?

    1. NNI。

    2. Gloo。

    3. MPI。

    4. TorchInductor。

  3. PyTorch 提供的默认程序启动器是哪一个?

    1. PyTorchrun。

    2. Gloorun。

    3. MPIRun。

    4. Torchrun。

  4. 在 PyTorch 的上下文中,Intel oneCCL 是什么?

    1. 通信后端。

    2. 程序启动器。

    3. 检查点自动化工具。

    4. 性能分析工具。

  5. 在考虑非 Intel 环境时,通信后端的最合理选择是什么?

    1. Gloorun。

    2. Torchrun。

    3. oneCCL。

    4. Gloo。

  6. 在使用 Gloo 或 oneCCL 作为通信后端时,关于训练过程的性能,我们可以说以下哪些内容?

    1. 完全没有任何区别。

    2. Gloo 比 oneCCL 总是更好。

    3. oneCCL 在 Intel 平台上可以超越 Gloo。

    4. oneCCL 总是比 Gloo 更好。

  7. 在将训练过程分布在多个 CPU 和核心之间时,我们需要定义线程的分配以完成以下哪些任务?

    1. 保证所有线程都独占计算资源的使用。

    2. 保证安全执行。

    3. 保证受保护的执行。

    4. 保证数据在所有线程之间共享。

  8. torchrun 的两个主要任务是什么?

    1. 创建共享内存池并在操作系统中实例化进程。

    2. 定义与分布式环境相关的环境变量,并在操作系统上实例化进程。

    3. 定义与分布式环境相关的环境变量,并创建共享内存池。

    4. 确定在 PyTorch 中运行的最佳线程数。

概要。

在本章中,我们了解到,将训练过程分布在多个计算核心上比传统训练中增加线程数量更有优势。这是因为 PyTorch 在常规训练过程中可能会面临并行级别的限制。

要在单台机器上的多个计算核心之间分布训练,我们可以使用 Gloo,这是 PyTorch 默认提供的简单通信后端。结果显示,使用 Gloo 进行的分布式训练在保持相同模型精度的同时,实现了 25%的性能改善。

我们还了解到,Intel 的一个集体通信库 oneCCL 在 Intel 平台上执行时可以进一步加快训练过程。使用 Intel oneCCL 作为通信后端,我们将训练时间缩短了 40%以上。如果我们愿意稍微降低模型精度,可以加快两倍的训练速度。

在下一章中,我们将学习如何将分布式训练过程扩展到单台机器上的多个 GPU 上运行。

第十章:使用多个 GPU 进行训练

无疑,GPU 提供的计算能力是推动深度学习领域发展的因素之一。如果单个 GPU 设备可以显著加速训练过程,那么想象一下在多 GPU 环境下我们可以做什么。

在本章中,我们将展示如何利用多个 GPU 加速训练过程。在描述代码和启动过程之前,我们将深入探讨多 GPU 环境的特性和细微差别。

以下是您将在本章学到的内容:

  • 多 GPU 环境的基础知识

  • 如何将训练过程分布到多个 GPU 中

  • NCCL,NVIDIA GPU 上分布式训练的默认后端

技术要求

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

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

解密多 GPU 环境

多 GPU 环境是一个具有多个 GPU 设备的计算系统。虽然只有一个 GPU 的多个互连机器可以被认为是多 GPU 环境,但我们通常使用此术语来描述每台机器具有两个或更多 GPU 的环境。

要了解此环境在幕后的工作原理,我们需要了解设备的连接性以及采用的技术,以实现跨多个 GPU 的有效通信。

然而,在我们深入讨论这些话题之前,我们将回答一个让您担忧的问题:我们是否能够访问像那样昂贵的环境?是的,我们可以。但首先,让我们简要讨论多 GPU 环境的日益流行。

多 GPU 环境的流行度

十年前,想象一台拥有多个 GPU 的机器是不可思议的事情。除了设备成本高昂外,GPU 的适用性仅限于解决科学计算问题,这是仅由大学和研究机构利用的一个小众领域。然而,随着人工智能AI)工作负载的蓬勃发展,GPU 的使用在各种公司中得到了极大的普及。

此外,在过去几年中,随着云计算的大规模采用,我们开始看到云服务提供商以竞争性价格提供多 GPU 实例。例如,在亚马逊网络服务AWS)中,有多种实例配备了多个 GPU,例如p5.48xlargep4d.24xlargep3dn.24xlarge,分别为 H100、A100 和 V100 型号提供了 8 个 NVIDIA GPU。

微软 Azure 和谷歌云平台GCP)也提供多 GPU 实例。前者提供配备 4 个 NVIDIA A100 的NC96ads,而后者提供配备 8 个 NVIDIA H100 的a3-highgpu-8g 实例。即使是次要的云服务提供商,如 IBM、阿里巴巴和Oracle 云基础设施OCI),也有多 GPU 实例。

在本地环境方面,我们有重要的供应商,如 Supermicro、惠普和戴尔,在其产品组合中提供多 GPU 平台。例如,NVIDIA 提供了专门设计用于运行 AI 工作负载的完全集成服务器,称为 DGX 系统。例如,DGX 版本 1 配备了 8 个 Volta 或 Pascal 架构的 GPU,而 DGX 版本 2 的 GPU 数量是其前身的两倍。

考虑到这些环境越来越受欢迎,可以合理地说,数据科学家和机器学习工程师迟早会接触到这些平台。需要注意的是,许多专业人士已经拥有这些环境,尽管他们不知道如何利用它们。

注意

虽然多 GPU 环境可以显著提高训练过程的性能,但也存在一些缺点,比如获取和维护这些环境的高成本,以及控制这些设备温度所需的大量能源。

为了有效地使用这个资源,我们必须学习这个环境的基本特征。所以,让我们朝着这个方向迈出第一步,了解 GPU 如何连接到这个平台。

理解多 GPU 互连

多 GPU 环境可以被视为资源池,不同的用户可以单独分配设备来执行其训练过程。然而,在分布式训练的背景下,我们有兴趣同时使用多个设备——也就是说,我们将每个 GPU 用于运行分布式训练过程的模型副本。

由于每个模型副本得到的梯度必须在所有其他副本之间共享,多 GPU 环境中的 GPU 必须连接,以便数据可以流过系统上的多个设备。GPU 连接技术有三种类型:PCI Express、NVLink 和 NVSwitch。

注意

您可以在由 Ang Li 等人撰写的论文评估现代 GPU 互连:PCIe、NVLink、NV-SLI、NVSwitch 和 GPUDirect中找到这些技术的比较。您可以通过 ieeexplore.ieee.org/document/8763922 访问此论文。

下面的章节描述了每一个部分。

PCI Express

PCI Express,也称为 PCIe,是连接各种设备(如网络卡、硬盘和 GPU)到计算机系统的默认总线,如图 10**.1所示。因此,PCIe 并不是一种特定的连接 GPU 的技术。相反,PCIe 是一种通用且与厂商无关的扩展总线,连接外围设备到系统中:

图 10.1 – PCIe 互连技术

图 10.1 – PCIe 互连技术

PCIe 通过两个主要组件互连外围设备:PCIe 根复杂PCIe 交换机。前者将整个 PCIe 子系统连接到 CPU,而后者用于将端点设备(外围设备)连接到子系统。

注意

PCIe 根复杂也称为 PCIe 主机桥或 PHB。在现代处理器中,PCIe 主机桥位于 CPU 内部。

图 10**.2所示,PCIe 使用交换机以分层方式组织子系统,连接到同一交换机的设备属于同一层次结构级别。同一层次结构级别的外围设备之间的通信成本低于层次结构不同级别中的外围设备之间的通信成本:

图 10.2 – PCIe 子系统

图 10.2 – PCIe 子系统

例如,GPU #0NIC #0之间的通信比GPU #1NIC #0之间的通信要快。这是因为第一组连接到同一个交换机(switch #2),而最后一组设备连接到不同的交换机。

类似地,GPU #3Disk #1之间的通信比GPU #3Disk #0之间的通信要便宜。在后一种情况下,GPU #3需要穿过三个交换机和根复杂来到达Disk #0,而Disk #1距离GPU #3只有两个交换机的距离。

PCI Express 不提供直接连接一个 GPU 到另一个 GPU 或连接所有 GPU 的方法。为了解决这个问题,NVIDIA 发明了一种新的互连技术称为 NVLink,如下一节所述。

NVLink是 NVIDIA 的专有互连技术,允许我们直接连接成对的 GPU。NVLink 提供比 PCIe 更高的数据传输速率。单个 NVLink 可以提供每秒 25 GB 的数据传输速率,而 PCIe 允许的最大数据传输速率为每秒 1 GB。

现代 GPU 架构支持多个 NVLink 连接。每个连接可以用于连接 GPU 到不同的 GPU(如图 10**.3 (a)所示)或者将连接绑定在一起以增加两个或多个 GPU 之间的带宽(如图 10**.3 (b)所示)。例如,P100 和 V100 GPU 分别支持四个和六个 NVLink 连接:

图 10.3 – NVLink 连接

图 10.3 – NVLink 连接

如今,NVLink 是连接 NVIDIA GPU 的最佳选择。与 PCIe 相比,使用 NVLink 的好处非常明显。通过 NVLink,我们可以直接连接 GPU,减少延迟并提高带宽。

尽管如此,PCIe 在一个方面胜过 NVLink:可扩展性。由于 GPU 中存在的连接数量有限,如果每个 GPU 仅支持四个 NVLink 连接,则 NVLink 将无法连接某些数量的设备。例如,如果每个 GPU 仅支持四个 NVLink 连接,那么是无法将八个 GPU 全部连接在一起的。另一方面,PCIe 可以通过 PCIe 交换机连接任意数量的设备。

要解决这个可扩展性问题,NVIDIA 开发了一种名为NVSwitch的 NVLink 辅助技术。我们将在下一节详细了解它。

NVSwitch

NVSwitch 通过使用 NVLink 交换机扩展了 GPU 的连接度。粗略来说,NVSwitch 的思想与 PCIe 技术上使用交换机的方式相似 - 也就是说,两者的互连都依赖于像聚集器或集线器一样的组件。这些组件用于连接和聚合设备:

图 10.4 – NVSwitch 互连拓扑

图 10.4 – NVSwitch 互连拓扑

正如图 10**.4所示,我们可以使用 NVSwitch 连接八个 GPU,而不受每个 GPU 支持的 NVLink 数量的限制。其他配置包括 NVLink 和 NVSwitch,如图 10**.5所示:

图 10.5 – 使用 NVLink 和 NVSwitch 的拓扑示例

图 10.5 – 使用 NVLink 和 NVSwitch 的拓扑示例

图 10**.5中所示的示例中,所有 GPU 都通过 NVSwitch 连接到自身。然而,一些 GPU 对通过两个 NVLink 连接,因此可以使这些 GPU 对之间的数据传输速率加倍。此外,还可以使用多个 NVSwitch 来提供 GPU 的完全连接性,改善设备对或元组之间的连接。

总之,在多 GPU 环境中,可以通过不同的通信技术连接 GPU,提供不同的数据传输速率和不同的设备连接方式。因此,我们可以有多条路径来连接两个或多个设备。

系统中设备连接的方式称为互连拓扑,在训练过程的性能优化中扮演着至关重要的角色。让我们跳到下一节,了解为什么拓扑是值得关注的。

互连拓扑如何影响性能?

为了理解互联拓扑对训练性能的影响,让我们考虑一个类比。想象一个城市,有多条道路,如高速公路、快速路和普通街道,每种类型的道路都有与速限、拥堵等相关的特征。由于城市有许多道路,我们有不同的方式到达同一目的地。因此,我们需要决定哪条路径是使我们的路线尽可能快的最佳路径。

我们可以把互联拓扑看作是我们类比中描述的城市。在城市中,设备之间的通信可以采用不同的路径,一些路径快速,如高速公路,而其他路径较慢,如普通街道。如同在城市类比中所述,我们应始终选择训练过程中使用的设备之间最快的连接。

要了解设备互联拓扑选择无意识可能影响的潜在影响,考虑图 10.6中的块图,该图代表一个用于运行高度密集计算工作负载作为训练过程的环境:

图 10.6 - 系统互联拓扑示意图示例

图 10.6 - 系统互联拓扑示意图示例

注意

图 10.6中显示的图表是真实互联拓扑的简化版本。因此,我们应将其视为真实拓扑结构方案的教学表现。

图 10.6中展示的环境可以被归类为多设备平台,因为它拥有多个 GPU、CPU 和其他重要组件,如超快速磁盘和网络卡。除了多个设备外,此类平台还使用多种互联技术,正如我们在前一节中学到的那样。

假设我们打算在图 10.6所描述的系统上使用两个 GPU 来执行分布式训练过程,我们应该选择哪些 GPU?

如果我们选择GPU #0GPU #1,通信速度会很快,因为这些设备通过 NVLink 连接。另一方面,如果我们选择GPU #0GPU #3,通信将穿越整个 PCIe 子系统。在这种情况下,与 NVLink 相比,通过 PCIe 进行通信具有较低的带宽,通信会穿过各种 PCIe 交换机、两个 PCIe 根复杂以及两个 CPU。

自然而然地,我们必须选择提供最佳通信性能的选项,这可以通过使用数据传输速率更高的链接和使用最近的设备来实现。换句话说,我们需要使用具有最高亲和力的 GPU

您可能想知道如何发现您环境中的互联拓扑。我们将在下一节中学习如何做到这一点。

发现互联拓扑

要发现 NVIDIA GPU 的互联拓扑结构,我们只需执行带有两个参数的nvidia-smi命令:

maicon@packt:~$ nvidia-smi topo –m

topo参数代表拓扑,并提供获取系统中采用的互连拓扑的更多信息的选项。-m选项告诉nvidia-smi以矩阵格式打印 GPU 的亲和性。

nvidia-smi打印的矩阵显示了系统中每对可用 GPU 之间的亲和性。由于同一设备之间的亲和性是不合逻辑的,矩阵对角线标有 X。在其余坐标中,矩阵展示了标签,以表示该设备对的最佳连接类型。矩阵可能的标签如下(从nvidia-smi手册调整):

  • SYS:连接通过 PCIe 以及 NUMA 节点之间的互联(例如 QPI/UPI 互联)

  • NODE:连接通过 PCIe 以及 NUMA 节点内 PCIe 根复杂的连接

  • PHB:连接通过 PCIe 以及 PCIe 根复杂(PCIe 主机桥)

  • PXB:连接通过多个 PCIe 桥(而不通过任何 PCIe 根复杂)

  • PIX:连接最多通过单个 PCIe 桥

  • NV#:连接通过一组#个 NVLink 的绑定

让我们评估一个由nvidia-smi生成的亲和矩阵的示例。在一个由 8 个 GPU 组成的环境中生成的表 10.1 所示的矩阵:

GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7
GPU0 X NV1 NV1 NV2 NV2 系统 系统 系统
GPU1 NV1 X NV2 NV1 系统 NV2 系统 系统
GPU2 NV1 NV2 X NV2 系统 系统 NV1 系统
GPU3 NV2 NV1 NV2 X 系统 系统 系统 NV1
GPU4 NV2 系统 系统 系统 X NV1 NV1 NV2
GPU5 系统 NV2 系统 系统 NV1 X NV2 NV1
GPU6 系统 系统 NV1 系统 NV1 NV2 X NV2
GPU7 系统 系统 系统 NV1 NV2 NV1 NV2 X

表 10.1 - 由 nvidia-smi 生成的亲和矩阵示例

表 10.1中描述的亲和矩阵告诉我们,一些 GPU 通过两个 NVLink 连接(标记为NV2),而其他一些只通过一个 NVLink 连接(标记为NV1)。此外,许多其他 GPU 没有共享 NVLink 连接,仅通过系统中的最大路径连接(标记为SYS)。

因此,在分布式训练过程中,如果我们需要选择两个 GPU 一起工作,建议使用例如 GPU #0#3,GPU #0#4,以及 GPU #1#2,因为这些设备对通过两个绑定 NVLink 连接。相反,较差的选择将是使用 GPU #0#5或者#2#4,因为这些设备之间的连接跨越整个系统。

如果我们有兴趣了解两个特定设备的亲和性,可以执行带有-i参数的nvidia-smi,然后跟上 GPU 的 ID:

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

在这个例子中,GPU #0#1 通过多个 PCIe 开关连接,虽然它们不经过任何 PCIe 根复杂。

注意

另一种映射 NVIDIA GPU 拓扑的方法是使用 NVIDIA 拓扑感知 GPU 选择NVTAGS)。NVTAGS 是 NVIDIA 创建的工具集,用于自动确定 GPU 之间最快的通信通道。有关 NVTAGS 的更多信息,您可以访问此链接:developer.nvidia.com/nvidia-nvtags

设置 GPU 亲和性

设置 GPU 亲和性最简单的方法是使用 CUDA_VISIBLE_DEVICES 环境变量。此变量允许我们指示哪些 GPU 将对基于 CUDA 的程序可见。要做到这一点,我们只需指定 GPU 的编号,用逗号分隔即可。

例如,考虑一个配备 8 个 GPU 的环境,我们必须将 CUDA_VISIBLE_DEVICES 设置为 2,3,以便可以使用 GPU #2#3

CUDA_VISIBLE_DEVICES = "2,3"

注意 CUDA_VISIBLE_DEVICES 定义了 CUDA 程序将使用哪些 GPU,而不是设备的数量。因此,如果变量设置为 5,例如,CUDA 程序将只看到系统中可用的八个设备中的 GPU 设备 #5

有三种方法可以设置 CUDA_VISIBLE_DEVICES 以选择我们想在训练过程中使用的 GPU:

  1. 在启动训练程序之前 导出 变量:

    maicon@packt:~$ export CUDA_VISIBLE_DEVICES="4,6"maicon@packt:~$ python training_program.py
    
  2. 在训练程序内部 设置 变量:

    os.environ['CUDA_VISIBLE_DEVICES'] ="4,6"
    
  3. 在训练程序的同一命令行中 定义 变量:

    maicon@packt:~$ CUDA_VISIBLE_DEVICES="4,6" python training_program.py
    

在下一节中,我们将学习如何在多个 GPU 上编写和启动分布式训练。

在多个 GPU 上实施分布式训练

在本节中,我们将向您展示如何使用 NCCL 在多个 GPU 上实施和运行分布式训练,NCCL 是 NVIDIA GPU 的 事实上 通信后端。我们将首先简要概述 NCCL,之后我们将学习如何在多 GPU 环境中编写和启动分布式训练。

NCCL 通信后端

NCCL 代表 NVIDIA 集体通信库。顾名思义,NCCL 是为 NVIDIA GPU 提供优化集体操作的库。因此,我们可以使用 NCCL 来执行诸如广播、归约和所谓的全归约操作等集体例程。粗略地说,NCCL 在 Intel CPU 上的作用类似于 oneCCL。

PyTorch 原生支持 NCCL,这意味着默认安装的 PyTorch 针对 NVIDIA GPU 已经内置了 NCCL 版本。NCCL 可在单台或多台机器上工作,并支持高性能网络的使用,如 InfiniBand。

与 oneCCL 和 OpenMP 类似,NCCL 的行为也可以通过环境变量进行控制。例如,我们可以通过 NCCL_DEBUG 环境变量来控制 NCCL 的日志级别,接受 traceinfowarn 等值。此外,还可以通过设置 NCCL_DEBUG_SUBSYS 变量来根据子系统过滤日志。

注意

可以在 docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html 找到完整的 NCCL 环境变量集合。

在下一节中,我们将学习如何使用 NCCL 作为分布式训练过程中的通信后端,实现多 GPU 环境下的分布式训练。

编写和启动多 GPU 的分布式训练

将训练过程分布在多个 GPU 上的代码和启动脚本与《第九章》中介绍的几乎相同,即多 CPU 训练。在这里,我们将学习如何将它们调整为多 GPU 环境下的分布式训练。

编写多 GPU 的分布式训练

我们只需要对多 CPU 代码进行两处修改。

注意

本节展示的完整代码可以在 github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter10/nccl_distributed-efficientnet_cifar10.py 找到。

第一个修改涉及将 nccl 作为 init_process_group 方法(第 77 行)中 backend 参数的输入传递:

dist.init_process_group(backend="nccl", init_method="env://")

第二个修改尽管最重要。由于我们正在多 GPU 环境中运行训练进程,因此需要确保每个进程专属地分配系统上可用的一个 GPU。

通过这样做,我们可以利用进程排名来定义将分配给进程的设备。例如,考虑一个由四个 GPU 组成的多 GPU 环境,进程排名 0 将使用 GPU #0,进程排名 1 将使用 GPU #1,依此类推。

尽管这个变更对于正确执行分布式训练至关重要,但实现起来非常简单。我们只需将存储在 my_rank 变量中的进程排名分配给 device 变量即可。

device = my_rank

关于 GPU 的关联性,您可能会想知道,如果每个进程分配对应于其排名的 GPU,那我们该如何选择要使用的 GPU 呢? 这个问题是合理的,通常会导致很多混淆。幸运的是,答案很简单。

结果表明,CUDA_VISIBLE_DEVICES变量从训练程序中抽象出真实的 GPU 标识。因此,如果我们将该变量设置为6,7,训练程序将只看到两个设备 - 即标识为 0 和 1 的设备。因此,等级为 0 和 1 的进程将分配 GPU 号码 0 和 1,这些号码实际上是 6 和 7 的真实 ID。

总结一下,这两个修改就足以使代码在多 GPU 环境中准备好执行。那么,让我们继续下一步:启动分布式训练过程。

启动在多 GPU 上的分布式训练过程

在多 GPU 上执行分布式训练的脚本与我们用于在多 CPU 上运行分布式训练的脚本逻辑相同:

TRAINING_SCRIPT=$1NGPU=$2
TORCHRUN_COMMAND="torchrun --nnodes 1 --nproc-per-node $NGPU --master-addr localhost $TRAINING_SCRIPT"
$TORCHRUN_COMMAND

在 GPU 版本中,我们将 GPU 数量作为输入参数传递,而不是进程数量。因为我们通常将一个完整的 GPU 分配给一个单独的进程,在分布式训练中,进程数量等于我们打算使用的 GPU 数量。

关于执行脚本的命令行,CPU 版本和 GPU 版本之间没有区别。我们只需调用脚本的名称并通知训练脚本,然后是 GPU 的数量:

maicon@packt:~$ ./launch_multiple_gpu.sh nccl_distributed-efficientnet_cifar10.py 8

我们也可以调整脚本,使其像 CPU 实现一样使用容器:

TRAINING_SCRIPT=$1NGPU=$2
SIF_IMAGE=$3
TORCHRUN_COMMAND="torchrun --nnodes 1 --nproc-per-node $NGPU --master-addr localhost $TRAINING_SCRIPT"
apptainer exec --nv $SIF_IMAGE $TORCHRUN_COMMAND

GPU 实现的独特差异涉及 Apptainer 命令行。当使用 NVIDIA GPU 时,我们需要使用--nv参数调用 Apptainer 来启用容器内这些设备的支持。

注意

本节展示的完整代码可在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/scripts/chapter10/launch_multiple_gpu.sh获取。

现在,让我们看看使用多个 GPU 进行快速分布式训练的速度能有多快。

实验评估

为了评估在多个 GPU 上的分布式训练,我们使用了一台配备 8 个 NVIDIA A100 GPU 的单机对 CIFAR-10 数据集训练 EfficientNet 模型进行了 25 个 epoch。作为基准,我们将使用仅使用 1 个 GPU 训练此模型的执行时间,即 707 秒。

使用 8 个 GPU 训练模型的执行时间为 109 秒,相比仅使用 1 个 GPU 训练模型的执行时间,性能显著提升了 548%。换句话说,使用 8 个 GPU 进行的分布式训练比单个训练方法快了近 6.5 倍。

然而,与使用多个 CPU 进行的分布式训练过程一样,使用多 GPU 进行的训练也会导致模型准确率下降。使用 1 个 GPU 时,训练的模型达到了 78.76%的准确率,但使用 8 个 GPU 时,准确率降至 68.82%。

这种模型准确性的差异是相关的;因此,在将训练过程分配给多个 GPU 时,我们不应该将其置之不理。相反,我们应该考虑在分布式训练过程中考虑这一点。例如,如果我们不能容忍模型准确性差异达到 10%,我们应该尝试减少 GPU 的数量。

为了让您了解性能增益与相应模型准确性之间的关系,我们进行了额外的测试。结果显示在表 10.2中:

GPU 的数量 执行时间 准确性
1 707 78.76%
2 393 74.82%
3 276 72.70%
4 208 70.72%
5 172 68.34%
6 142 69.44%
7 122 69.00%
8 109 68.82%

表 10.2 - 使用多个 GPU 进行分布式训练的结果

表 10.2所示,随着 GPU 数量的增加,准确性往往会下降。然而,如果我们仔细观察,我们会发现 4 个 GPU 在保持准确性超过 70%的同时,实现了非常好的性能提升(240%)。

有趣的是,当我们在训练过程中使用 2 个 GPU 时,模型准确性下降了 4%。这个结果显示,即使使用最少数量的 GPU,分布式训练也会影响准确性。

另一方面,从 5 个设备开始,模型准确性几乎保持稳定在约 68%,尽管性能改进不断上升。

简而言之,在增加分布式训练过程中 GPU 数量时,注意模型准确性至关重要。否则,对性能提升的盲目追求可能会导致训练过程中不良结果的产生。

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

测验时间!

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

注意

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

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

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

  1. GPU 互联的三种主要类型是哪三种?

    1. PCI Express、NCCL 和 GPU-Link。

    2. PCI Express、NVLink 和 NVSwitch。

    3. PCI Express、NCCL 和 GPU-Switch。

    4. PCI Express、NVML 和 NVLink。

  2. NVLink 是一种专有的互联技术,允许您做哪些事情?

    1. 将 GPU 连接到 CPU。

    2. 将 GPU 连接到主存储器。

    3. 将 GPU 对直接连接到一起。

    4. 将 GPU 连接到网络适配器。

  3. 用于定义 GPU 亲和性的环境变量是哪一个?

    1. CUDA_VISIBLE_DEVICES

    2. GPU_VISIBLE_DEVICES

    3. GPU_ACTIVE_DEVICES

    4. CUDA_AFFINITY_DEVICES

  4. 什么是 NCCL?

    1. NCCL 是用于连接 NVIDIA GPU 的互连技术。

    2. NCCL 是用于分析在 NVIDIA GPU 上运行的程序的库。

    3. NCCL 是用于为 NVIDIA GPU 生成优化代码的编译工具包。

    4. NCCL 是为 NVIDIA GPU 提供优化集体操作的库。

  5. 哪个程序启动器可用于在多个 GPU 上运行分布式训练?

    1. GPUrun。

    2. Torchrun。

    3. NCCLrun。

    4. oneCCL。

  6. 如果我们将 CUDA_VISIBLE_DEVICES 环境变量设置为“2,3”,那么训练脚本将传递哪些设备号?

    1. 2 和 3。

    2. 3 和 2。

    3. 0 和 1。

    4. 0 和 7。

  7. 如何获取有关在特定多 GPU 环境中采用的互连拓扑的更多信息?

    1. 运行带有 -``interconnection 选项的 nvidia-topo-ls 命令。

    2. 运行带有 -``gpus 选项的 nvidia-topo-ls 命令。

    3. 运行带有 -``interconnect 选项的 nvidia-smi 命令。

    4. 运行带有 -``topo 选项的 nvidia-smi 命令。

  8. PCI Express 技术用于在计算系统中连接 PCI Express 设备的哪个组件?

    1. PCIe 交换机。

    2. PCIe nvswitch。

    3. PCIe 连接。

    4. PCIe 网络。

摘要

在本章中,我们学习了如何通过使用 NCCL,在多个 GPU 上分发训练过程,这是优化的 NVIDIA 集体通信库。

我们从理解多 GPU 环境如何利用不同技术来互连设备开始本章。根据技术和互连拓扑,设备之间的通信可能会减慢整个分布式训练过程。

在介绍多 GPU 环境后,我们学习了如何使用 NCCL 作为通信后端和 torchrun 作为启动提供者,在多个 GPU 上编码和启动分布式训练。

我们的多 GPU 实现的实验评估显示,使用 8 个 GPU 进行分布式训练比单个 GPU 运行快 6.5 倍;这是显著的性能改进。我们还了解到,在多 GPU 上进行分布式训练可能会影响模型的准确性,因此在增加用于分布式训练过程中的设备数量时必须考虑这一点。

结束我们加速 PyTorch 训练过程的旅程,下一章中,我们将学习如何在多台机器之间分发训练过程。

第十一章:使用多台机器进行训练

我们终于到达了性能提升之旅的最后一英里。在这最后阶段,我们将开阔视野,学习如何在多台机器或服务器间分布训练过程。所以,我们可以利用几十甚至上百个计算资源来训练我们的模型,而不仅仅是四台或八台设备。

一个由多个连接的服务器组成的环境通常被称为计算集群或简称为集群。这些环境被多个用户共享,并具有高带宽和低延迟网络等技术特性。

在本章中,我们将描述与分布式训练过程更相关的计算集群特性。接下来,我们将学习如何使用 Open MPI 作为启动器,以及 NCCL 作为通信后端,将训练过程分布到多台机器上。

以下是本章将学到的内容:

  • 计算集群最相关的方面

  • 如何在多个服务器间分布训练过程

  • 如何使用 Open MPI 作为启动器和 NCCL 作为通信后端

技术要求

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

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

什么是计算集群?

计算集群是由高性能网络互连的强大服务器系统组成的环境,如图 11**.1所示。此环境可以在本地部署或云中进行配置:

图 11.1 – 一个计算集群

图 11.1 – 一个计算集群

这些机器提供的计算能力结合起来,用于解决复杂问题或执行高强度计算任务。计算集群也被称为高性能计算HPC)系统。

每台服务器都拥有强大的计算资源,例如多个 CPU 和 GPU、快速内存设备、超快速磁盘和特殊的网络适配器。此外,计算集群通常还配备并行文件系统,提供高传输 I/O 速率。

虽然未正式定义,但我们通常使用术语“集群”来指代由至少四台机器组成的环境。一些计算集群有六台机器,而其他的则拥有超过两三百台服务器。

提交到集群的每个任务称为作业。当提交作业时,用户请求一定数量和类型的资源,并指示在环境中执行哪个程序。因此,运行在集群中的任何计算任务都被视为作业。

作业和操作系统进程有许多共同之处。作业像进程一样,在系统中由唯一编号标识,具有有限状态生命周期,并属于系统用户。

图 11**.2形象地描述的那样,大部分服务器被用作计算节点 – 换句话说,这些机器专门用于运行作业。一些机器,称为管理节点,用于执行监控和安装等管理任务,或提供辅助和补充服务,例如用户访问入口,通常称为登录节点

图 11.2 – 管理和计算节点

图 11.2 – 管理和计算节点

托管在管理节点上的另一个重要服务是集群管理系统或工作负载管理器。由于集群被多个用户共享,有一个工作负载管理器是必不可少的,以确保资源的公平高效使用。让我们在下一节学习它。

工作负载管理器

工作负载管理器负责通过提供资源的公平高效使用来保持集群环境的顺畅运行。如图 11**.3所示,工作负载管理器位于用户和资源之间,接收来自用户的请求,处理这些请求,并授予或拒绝访问所需的资源:

图 11.3 – 工作负载管理器

图 11.3 – 工作负载管理器

在该系统执行的任务中,有两个任务脱颖而出:资源管理和作业调度。以下各节简要描述了它们。

资源管理

大致来说,集群可以看作是一组共享资源的池子,这些资源被一组用户消耗。资源管理的主要目标是确保这些资源的公平使用。

公平使用意味着避免不平衡的情况,例如贪婪用户消耗所有可用资源,防止不频繁的用户无法访问环境。

资源管理依赖于资源分配策略来决定何时以及如何处理用户的请求。此策略可用于定义优先级别、最大使用时间、最大运行作业数、资源类型以及许多其他条件。

基于这些策略,集群管理员可以根据由负责集群的组织或部门定义的标准,为用户分配不同的策略。

例如,集群管理员可以定义两种资源分配策略,以限制问题,如最大运行作业数、资源类型以及运行作业的最大允许时间。如图 11**.4所示,更严格的策略,命名为A,可以应用于用户组X,而更宽松的策略,命名为B,可以分配给用户Y

图 11.4 – 资源分配策略示例

图 11.4 – 资源分配策略示例

通过这样做,集群管理员可以确定集群的不同使用配置文件。

作业调度

工作负载管理器还负责资源的高效使用。为了达到这个目标,工作负载管理器必须在计算节点上执行一个最优(或次优)的作业分配。这个过程被称为作业调度,并定义为决定在哪里运行新作业的任务。

图 11**.5 所示,工作负载管理器必须选择新作业将在其中执行的计算节点。为了做出这一决定,工作负载管理器评估了请求资源的数量和类型以及所有计算节点中可用资源的数量和类型。通过这样做,作业调度得到了一组适合执行作业的潜在节点列表 – 换句话说,具有足够资源满足作业需求的节点。

图 11.5 – 作业调度

图 11.5 – 作业调度

从潜在节点列表中,作业调度需要决定选择哪个节点来执行作业。此决策根据调度策略做出,该策略可能优先满足所有节点,然后再使用另一个节点,或者尽可能将作业分配到计算节点中,以避免因共享资源访问冲突而引起的干扰。

这些部分提供了工作负载管理器如何工作的一般解释。在实际操作中,真实的工作负载管理器具有特定的实现方式和资源管理和作业调度过程。

有一些工作负载管理器可供选择。有些是专有的和供应商特定的,而另一些是免费和开源的,比如 SLURM,它是目前最广泛使用的工作负载管理器。让我们在下一节介绍这个系统。

满足 SLURM 工作负载管理器

SLURM 的网站将其描述为一个“开源、容错和高度可扩展的大型和小型 Linux 集群管理和作业调度系统” – 这是正确的。

注意

您可以在此链接找到关于 SLURM 的更多信息:slurm.schedmd.com/

SLURM 强大、健壮、灵活且易于使用和管理。除了任何工作负载管理器中的基本功能外,SLURM 还提供特殊功能,如QOS(服务质量)、计费、数据库存储以及允许您获取环境信息的API(应用程序编程接口)。

这个工作负载管理器使用分区的概念来组织计算节点,并在可用资源上定义资源分配策略,如 图 11**.6 所示:

图 11.6 – SLURM 分区示例

图 11.6 – SLURM 分区示例

在图 11**.6 中所示的示例中,我们有三个分区,每个分区有八个计算节点,但具有不同的资源分配策略。例如,short_jobs_cpu 分区允许您运行最长四小时的作业,而 long_jobs_cpu 分区的最长执行时间为八小时。此外,只有 long_jobs_gpu 分区拥有可以运行 GPU 作业的计算节点。

注意

SLURM 使用术语 partition 来表示其他工作负载管理器称为 queue 的内容。尽管如此,分区在本质上像一个队列,接收作业请求并根据资源分配和作业调度策略组织其执行。

因此,分区是 SLURM 架构的一个核心方面。有了分区,集群管理员可以使用不同的资源分配策略,并且可以将节点分开以运行特定应用程序或留待某个部门或用户组专属使用。

当用户提交作业时,他们必须指定作业将运行的分区。否则,SLURM 将作业提交到由集群管理员定义的默认分区。

当使用计算集群时,我们可能会遇到其他工作负载管理器,如 OpenPBS、Torque、LSF 和 HT Condor。然而,由于在高性能计算行业中的广泛采用,更有可能在您访问的集群上遇到 SLURM 作为工作负载管理器。因此,我们建议您投入一些时间来深入了解 SLURM。

除了工作负载管理外,计算集群还有另一个在这些环境中尤为重要的组件:高性能网络。下一节简要解释了这个组件。

理解高性能网络

在单台机器上运行分布式训练与使用计算集群之间的一个重要区别是用于连接服务器的网络。网络对参与分布式训练的进程间通信施加了额外的瓶颈。幸运的是,计算集群通常配备高性能网络,以连接环境中的所有服务器。

这种高性能网络与普通网络的区别在于其高带宽和非常低的延迟。例如,以太网 10 Gbps 的最大理论带宽约为 1.25 GB/s,而 NVIDIA InfiniBand 100 Gbps EDR,作为最广泛采用的高性能网络之一,提供的带宽接近 12.08 GB/s。换句话说,高性能网络可以提供比普通网络高出 10 倍的带宽。

注意

您可以在这个链接找到关于 NVIDIA InfiniBand 的更多信息:www.nvidia.com/en-us/networking/products/infiniband/

尽管 InfiniBand 提供的高带宽令人惊叹,但高性能网络之所以如此特别,是因为其非常低的延迟。与以太网 10 Gbps 相比,InfiniBand 100 Gbps EDR 的延迟几乎可以降低四倍。低延迟对于执行分布式应用程序至关重要。由于这些应用程序在计算过程中交换大量消息,消息中的任何延迟都可能使整个应用程序陷入停滞。

除了具有高带宽和低延迟之外,高性能网络(包括 InfiniBand)还具备另一种特殊功能,称为远程直接内存访问RDMA。让我们在下一节中学习它。

RDMA

RDMA 是高性能网络提供的一种功能,旨在减少设备之间的通信延迟。在了解使用 RDMA 的优势之前,我们首先应该记住常规通信是如何工作的。

常规数据传输涉及两个 GPU 的程序如图 11.7所示:

图 11.7 – 两个 GPU 之间的常规数据传输

图 11.7 – 两个 GPU 之间的常规数据传输

首先,GPU A请求 CPU 将数据发送到位于Machine B上的GPU B。CPU 接收请求并在Machine A的主内存上创建缓冲区,用于存储要传输的数据。接下来,GPU A将数据发送到主内存,并通知 CPU 数据已经在主内存上准备就绪。因此,Machine A上的 CPU 将数据从主内存复制到网络适配器的缓冲区。然后,Machine A上的网络适配器与Machine B建立通信通道并发送数据。最后,Machine B上的网络适配器接收数据,并且Machine B执行与Machine A之前相同的步骤,将接收到的数据传递给GPU B

请注意,此过程涉及许多主内存中数据的中介复制;换句话说,数据从 GPU 内存复制到主内存,然后从主内存复制到网络适配器的缓冲区,这两个方向都是如此。因此,很容易看出,此过程对位于远程机器上的 GPU 之间的通信施加了很高的开销。

为了克服这个问题,应用程序可以使用 RDMA 在设备之间传输数据。如图 11.8所示,RDMA 可以通过高性能网络直接从一个 GPU 传输数据到另一个 GPU。在完成初始设置后,网络适配器和 GPU 能够在不涉及 CPU 和主内存的情况下传输数据。因此,RDMA 消除了传输数据的大量中介复制,从而大幅降低通信延迟。这也是 RDMA 被称为零拷贝传输的原因。

图 11.8 – 两个 GPU 之间的 RDMA

图 11.8 – 两个 GPU 之间的 RDMA

要使用 RDMA,高性能网络、设备和操作系统必须支持此功能。因此,如果我们打算使用这个资源,我们应该首先与集群管理员确认在环境中是否可用以及如何使用它。

在学习了计算集群环境的主要特征后,我们可以继续学习如何在多台机器上实施分布式训练。

在多台机器上实施分布式训练

本节展示如何使用 Open MPI 作为启动提供程序和 NCCL 作为通信后端在多台机器上实施和运行分布式训练。让我们从介绍 Open MPI 开始。

介绍 Open MPI

MPI代表消息传递接口,是一个标准,规定了一套用于实现基于分布式内存的应用程序的通信例程、数据类型、事件和操作。MPI 对高性能计算行业非常重要,因此由全球知名的科学家、研究人员和专业人士组成的论坛管理和维护。

注意

您可以在此链接找到有关 MPI 的更多信息:www.mpi-forum.org/

因此,严格来说,MPI 不是软件;它是一种标准规范,可用于实现软件、工具或库。与非专有编程语言(如 C 和 Python)类似,MPI 也有许多实现。其中一些是供应商特定的,例如 Intel MPI,而另一些是免费和开源的,例如 MPICH。

在所有实现中,Open MPI作为最知名和采用最广泛的 MPI 实现之一脱颖而出。Open MPI 是免费的、开源的,并由包括 AMD、AWS、IBM、Intel 和 NVIDIA 在内的多家主要技术公司组成的贡献者联盟维护。该联盟还依靠著名大学和研究机构,如洛斯阿拉莫斯国家实验室和法国国家计算机科学与控制研究所(Inria)。

注意

您可以在此链接找到有关 Open MPI 的更多信息:www.open-mpi.org/

Open MPI 不仅仅是一个用于应用程序上实现 MPI 例程的库,它还是一个提供编译器、调试器和完整运行时机制等其他组件的工具集。

下一节介绍如何执行 Open MPI 程序。这些知识对于学习如何启动分布式训练过程非常重要。

执行 Open MPI 程序

要执行 Open MPI 程序,我们应该调用mpirun命令并传递 MPI 程序和进程数量作为参数:

maicon@packt:~$ mpirun --np 2 my_mpi_program

--np参数告诉 Open MPI 它必须创建的进程数。如果没有其他信息传递给 Open MPI,它将在本地创建这些进程 – 换句话说,在调用mpirun命令的机器上。要在远程机器上实例化进程,我们必须使用--host参数,后跟逗号分隔的远程机器列表:

maicon@packt:~$ mpirun --np 2 --host r1:1,r2:1 my_mpi_program

在前面的示例中,mpirun 将执行两个进程,一个在r1远程机器上,另一个在r2远程机器上。名称后面的值表示该机器愿意接受的槽位(或进程)数。例如,如果我们要执行六个进程,其中四个在r1远程机器上,两个在r2远程机器上,我们应该调用以下mpirun命令:

maicon@packt:~$ mpirun --np 6 --host r1:4,r2:2 my_mpi_program

Open MPI 将一些环境变量设置为由mpirun命令创建的每个进程的作用域。这些环境变量提供了关于分布式环境的重要信息,例如进程的排名。其中三个对我们的情况特别有趣:

  • OMPI_COMM_WORLD_SIZE:参与分布式执行的进程总数

  • OMPI_COMM_WORLD_RANK:进程的全局排名

  • OMPI_COMM_WORLD_LOCAL_RANK:进程的局部排名

要理解全局排名与局部排名的区别,让我们拿六个进程的前面示例,并在表 11.1中列出每个进程的全局和局部排名值:

进程 远程机器 全局排名 局部排名
0 r1 0 0
1 r1 1 1
2 r1 2 2
3 r1 3 3
4 r2 4 0
5 r2 5 1

表 11.1 – 全局排名与局部排名

正如表 11.1所示,全局排名是进程的全局标识 – 换句话说,无论进程在哪台机器上运行,它都有一个全局标识。

r2机器,那么进程 5 和 6 的局部排名分别等于 0 和 1。

局部排名的概念可能看起来违反直觉且无用,但事实并非如此。局部排名在分布式程序中非常有用,特别适合我们的分布式训练过程。等着瞧吧!

为什么要使用 Open MPI 和 NCCL?

你可能会想为什么我们使用 Open MPI 作为启动器和 NCCL 作为通信后端。事实上,也许你在问自己以下问题:

  1. 是否可以同时使用 Open MPI 作为启动器和通信后端?

  2. 是否可以使用 NCCL 作为通信后端和torchrun作为启动器?

对于这些问题的简短答案是:“是的,这是可能的。”然而,采用这些方法也存在一些缺点。让我们逐个讨论。

因为我们正在使用多 GPU 运行分布式训练,所以对于这种情况,最好的通信后端肯定是 NCCL。尽管可以使用 Open MPI 作为此场景的通信后端,但由 NCCL 提供的集体操作是针对 NVIDIA GPU 进行了最优化的。

因此,现在我们知道为什么应选择 NCCL 而不是 Open MPI 作为通信后端。但为什么不像到目前为止那样使用 torchrun 作为启动提供程序呢?

嗯,torchrun 是在本地运行分布式训练的一个很好的选择。但是,要在多台机器上运行分布式训练,我们需要在每台参与分布式环境的远程机器上手动执行 torchrun 实例。

torchrun 不同,Open MPI 在本地支持更轻松、更优雅地在远程机器上执行。通过使用其运行时机制,Open MPI 可以顺利在远程机器上创建进程,使我们的生活更加轻松。

简而言之,我们决定同时使用 NCCL 和 Open MPI,以获取两者结合的最佳效果。

为多台机器编写并启动分布式训练

分发训练进程的代码几乎与第十章中呈现的代码相同,使用多个 GPU 进行训练。毕竟,我们将执行多 GPU 训练,但是使用多台机器。因此,我们将调整多 GPU 实现,以使用 Open MPI 作为启动提供程序在多台机器上执行。

因为我们将使用 Open MPI 作为启动器,所以用于启动分布式训练的脚本将不会执行 torchrun 命令,就像我们在最后两章中所做的那样。因此,我们需要从头开始创建一个脚本来采用 Open MPI 作为启动方法。

让我们继续学习如何调整多 GPU 实现并创建在计算集群环境中进行分布式训练的启动脚本的以下部分。

为多台机器编写分布式训练代码

第十章中呈现的多 GPU 实现相比,在计算集群中运行分布式训练的代码有以下三个修改:

os.environ['RANK'] = os.environ['OMPI_COMM_WORLD_RANK']os.environ['WORLD_SIZE'] = os.environ['OMPI_COMM_WORLD_SIZE']
device = int(os.environ['OMPI_COMM_WORLD_LOCAL_RANK'])

此部分展示的完整代码可在 github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/code/chapter11/nccl_mpi_distributed-efficientnet_cifar10.py 查看。

前两个修改涉及设置环境变量 RANKWORLD_SIZE,这些变量被 init_process_group 方法所需,用于创建通信组。由于 Open MPI 使用其他变量名来存储这些信息,我们需要在代码中明确定义这些变量。

第三处修改与定义每个进程分配的设备(GPU,在本例中)有关。正如我们在前一节中学到的,本地排名是一个索引,用于标识每台机器上运行的进程。因此,我们可以利用这些信息作为选择每个进程使用的 GPU 的索引。因此,代码必须将OMPI_COMM_WORLD_LOCAL_RANK环境变量的内容分配给device变量。

例如,考虑使用每台装备有四个 GPU 的两台机器执行包含八个进程的分布式训练的情况。前四个进程的全局和本地排名分别为 0、1、2 和 3。因此,全局排名为 0 的进程,其本地排名为 0,将使用第一台机器上的#0 GPU,其他进程依此类推。

关于第二台机器上的四个进程,全局排名为 4 的进程,也就是第二台机器上的第一个进程,其本地排名为 0。因此,全局排名为 4 的进程将访问第二台机器上的#0 GPU。

只需对多 GPU 代码进行这三处修改即可使其在多台机器上运行。在下一节中,让我们看看如何通过使用 Open MPI 启动分布式训练。

在多台机器上启动分布式训练

要将 Open MPI 作为启动器使用,我们需要在计算集群环境中安装它。由于我们将 Open MPI 作为外部组件而不是内置在 PyTorch 中使用,因此这个安装应该由集群管理员提供。集群管理员应该按照 Open MPI 网站上描述的安装说明进行操作。

一旦我们在环境中安装了 Open MPI,我们有两种方法可以在多台机器上启动分布式训练。我们可以手动执行,也可以提交作业到工作负载管理器。让我们首先学习如何手动执行。

手动执行

要手动执行分布式训练,我们可以使用类似以下的启动脚本:

TRAINING_SCRIPT=$1NPROCS= "16"
HOSTS="machine1:8,machine2:8"
COMMAND="python $TRAINING_SCRIPT"
export MASTER_ADDR="machine1"
export MASTER_PORT= "12345"
mpirun -x MASTER_ADDR -x MASTER_PORT --np $NPROCS --host $HOSTS $COMMAND

注意

本节展示的完整代码可在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/scripts/chapter11/launch_multiple_machines.sh找到。

正如我们之前提到的,这个脚本不同于基于torchrun的脚本。脚本不再调用torchrun命令,而是执行mpirun,正如我们在前几节中学到的。此脚本中的mpirun命令使用五个参数执行。我们逐一来看它们。

前两个参数使用mpirun-x参数将MASTER_ADDRMASTER_PORT环境变量导出到训练程序中。

通过这样做,init_process_group方法可以正确地创建通信组。MASTER_ADDR环境变量指示启动脚本将在其中执行的机器。在我们的案例中,它在machine1上执行。MASTER_PORT环境变量定义通信组用于与参与分布式环境中所有进程建立通信的 TCP 端口号。我们可以选择一个较高的数字,以避免与任何绑定的 TCP 端口冲突。

--np参数确定进程数,--host参数用于指示mpirun将在其中创建进程的机器列表。在此示例中,我们考虑两台名为machine1machine2的机器。由于每台机器都有八个 GPU,其名称后跟数字八,以指示每台服务器可以执行的最大进程数。

最后一个参数是基于 MPI 的程序。在我们的情况下,我们将传递 Python 解释器的名称,然后是训练脚本的名称。

要执行此脚本运行名为distributed-training.py的程序,我们只需运行以下命令:

maicon@packt:~$ ./launch_multiple_machines.sh distributed-training.py

注意

本节中显示的完整代码可在github.com/PacktPublishing/Accelerate-Model-Training-with-PyTorch-2.X/blob/main/scripts/chapter11/launch_multiple_machines_container.sh找到。

当然,此脚本可以定制以接受其他参数,如进程数量、主机列表等。然而,我们在这里的目的是展示手动使用 Open MPI 执行分布式训练的基本(尽管重要)方法。

作业提交

考虑到工作负载管理器是 SLURM,我们必须执行以下步骤将作业提交到计算集群:

  1. 创建一个批处理脚本来提交作业给 SLURM。

  2. 使用sbatch命令提交作业。

在 SLURM 上提交分布式训练的批处理脚本如下所示:

#!/bin/bash#SBATCH -n 16
#SBATCH --partition=long_job_gpu
#SBATCH --nodes=2
#SBATCH --gpus-per-node=8
export MASTER_ADDR=$(hostname)
export MASTER_PORT= "12345"
mpirun -x MASTER_ADDR -x MASTER_PORT --np 16 python /share/distributed-training.py

这个批处理脚本将提交一个作业请求两个节点,每个节点八个 GPU,在long_job_gpu分区上执行。就像在启动脚本中一样,我们还需要导出MASTER_ADDRMASTER_PORT变量,以便init_process_group方法可以创建通信组。

创建脚本后,我们只需执行以下命令提交作业:

maicon@packt:~$ sbatch distributed-training.sbatch

注意

之前提供的批处理脚本只是展示如何在 SLURM 上提交分布式训练作业的示例。由于每个计算集群环境可能有特殊性,最佳方法始终是遵循集群管理员关于使用 Open MPI 的指导方针。无论如何,您可以参考官方 SLURM 文档了解在slurm.schedmd.com/mpi_guide.html上运行 Open MPI 作业的详细信息。

在下一节中,我们将看一下在两台机器上运行分布式训练的结果。

实验评估

要评估在多台机器上进行的分布式训练效果,我们使用了两台每台装有 8 块 NVIDIA A100 GPU 的机器,对 EfficientNet 模型在 CIFAR-10 数据集上进行了 25 个 epochs 的训练作为实验基准,我们将使用在单台装有 8 块 GPU 的机器上训练此模型所需的执行时间作为基准,其时间为 109 秒。

使用 16 块 GPU 训练模型的执行时间为 64 秒,与在单台装有八块 GPU 的机器上训练模型所需的时间相比,性能提升了 70%。

乍一看,这个结果可能会有点令人失望,因为我们使用了双倍的计算资源,但只获得了 70%的性能提升。由于我们使用了两倍的资源,我们应该达到 100%的提升。

但是,我们应该记住,这个系统还有一个额外的组成部分:机器之间的互联。尽管它是一个高性能网络,但预计额外的元素会对性能产生一定影响。即便如此,这个结果还是相当不错的,因为我们接近了我们可以实现的最大性能改进——换句话说,接近了 100%。

正如预期的那样,模型的准确性从 68.82%降至 63.73%,证实了分布式训练中模型副本数量与精度之间关系的断言。

总结这些结果,我们可以强调两个有趣的见解如下:

  • 当寻求性能改进时,我们必须始终关注模型的质量。正如我们在这里和前两章中看到的那样,在增加模型副本数量时,模型精度可能会出现潜在的降低。

  • 当决定在多台机器之间分发训练时,我们应考虑互连网络可能带来的影响。根据具体情况,保持在单台多 GPU 机器内进行训练可能比使用多台服务器更有优势。

简而言之,盲目追求性能提升通常不是一个好主意,因为我们可能会因微小的性能提升或模型质量的悄然降低而导致资源浪费。因此,我们应始终注意性能改进、准确性和资源使用之间的权衡。

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

测验时间!

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

注意

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

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

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

  1. 提交给计算集群的任务称为什么?

    1. 线程。

    2. 进程。

    3. 作业。

    4. 工作。

  2. 工作负载管理器执行的主要任务是什么?

    1. 资源管理和作业调度。

    2. 内存分配和线程调度。

    3. GPU 管理和节点调度。

    4. 资源管理和节点调度。

  3. 以下哪个是大型和小型 Linux 集群的开源、容错和高度可扩展的工作负载管理器?

    1. MPI。

    2. SLURM。

    3. NCCL。

    4. Gloo。

  4. 计算集群通常配备高性能网络,如 NVIDIA InfiniBand。除了提供高带宽外,高性能互连还提供以下哪些功能?

    1. 高延迟。

    2. 高数量的连接。

    3. 低连接数量。

    4. 非常低的延迟。

  5. RDMA 显著降低了两个远程 GPU 之间的通信延迟,因为它使以下哪个操作成为可能?

    1. 在 GPU 上分配更高的内存空间。

    2. GPU 上的特殊硬件能力。

    3. 在不涉及 CPU 和主内存的情况下进行数据传输。

    4. 在不涉及网络适配器和交换机的情况下进行数据传输。

  6. Open MPI 的最佳定义是以下哪个?

    1. Open MPI 是用来创建分布式应用的编译器。

    2. Open MPI 是一个工具集,包括编译器、调试器和完整的运行时机制,用于创建、调试和运行分布式应用。

    3. Open MPI 是一个标准,指定了一组用于实现分布式应用的通信例程、数据类型、事件和操作。

    4. Open MPI 是专门用于在 PyTorch 下运行分布式训练的通信后端。

  7. 考虑这样一个情景,一个分布式训练在两台机器上运行四个进程(每台机器执行两个进程)。在这种情况下,Open MPI 为第二台机器上执行的两个进程分配了什么等级?

    1. 0 和 1。

    2. 0 和 2。

    3. 2 和 3。

    4. 0 和 3。

  8. 关于将训练过程分布在多台机器上还是保留在单个主机的决定,合理考虑以下哪个选项?

    1. 使用网络适配器的功耗。

    2. 网络适配器上可用内存空间的泄漏。

    3. 没有;通常建议使用多台机器来运行分布式训练。

    4. 互连网络可能对参与分布式训练的进程之间的通信产生何种影响。

总结。

在本章中,我们学习了如何将训练过程分布到多台机器上的多个 GPU 上。我们使用 Open MPI 作为启动提供程序和 NCCL 作为通信后端。

我们决定使用 Open MPI 作为启动器,因为它提供了一种简单而优雅的方式在远程机器上创建分布式进程。虽然 Open MPI 也可以作为通信后端使用,但更推荐采用 NCCL,因为它在 NVIDIA GPU 上具有最优化的集合操作实现。

结果显示,使用两台机器上的 16 个 GPU 进行分布式训练比单机上的 8 个 GPU 运行速度快 70%。模型准确率从 68.82%降到了 63.73%,这是预料之中的,因为我们在分布式训练过程中复制了模型的数量。

本章结束了我们关于如何通过 PyTorch 加速训练过程的旅程。除了了解如何应用技术和方法来加快模型训练速度之外,我们希望您已经领悟到本书的核心信息:性能改进并不总是与新的计算资源或新型硬件有关;通过更有效地利用手头上的资源,我们可以加速训练过程。

标签:指南,训练,模型,PyTorch,GPU,我们,分布式
From: https://www.cnblogs.com/apachecn/p/18318410

相关文章

  • 代码改进,代跑通,预测模型,模型优化
    代码改进,代跑通,预测模型,模型优化,增加模块,python代做,预测,微调,融合,强化学习,深度学习,机器学习程序代写,环境调试,代码调通,模型优化,模型修改,时间序列,机器学习数据处理等开发工程项目主攻:Pytorch,Tensorflow,Yolo,Unet,DNN,CNN,GAN,Transformer,matlab训练模型,优化,price代跑增......
  • 广义线性模型(2)线性回归
    线性回归算法应该是大多数人机器学习之路上的第一站,因为线性回归算法原理简单清晰,但却囊括了拟合、优化等等经典的机器学习思想。说到线性回归,我们得先说说回归与分类、线性与非线性这些概念的区别。一分类与回归的区别机器学习中的分类和回归是两种主要的预测性监督学......
  • 大模型实战—你的个人AI数字大脑Khoj
    Khoj是你的开源个人AI伴侣,提供即时答案。Khoj轻松地深入知识,简化复杂信息,整合你的个人背景,并根据你的独特需求量身定制响应。在线问题:如果你有一个问题需要从互联网获取最新的信息,Khoj可以进行在线搜索,找到相关答案。例如,查询当前的天气情况或某个新闻事件的最新动态。......
  • 自动 Wi-Fi 设置网页:实施指南
    我想创建一个网页,我有自己的WiFi,当我给用户一个二维码时,直接转到用户填写手机号码和OTP验证的网页,验证后Wifi自动连接30分钟解决问题自动连接wifi结果就是这样解决了很多人的问题自动Wi-Fi设置网页:实施指南创建自动连接Wi-Fi的网页需要结合前端网页设计和后端逻......
  • 多模态大模型主流架构模式的演化历程
    多模态大模型主流架构模式的演化历程一、引言近年来,随着深度学习技术的飞速发展,多模态学习逐渐成为人工智能领域的研究热点。与单一模态不同,多模态学习旨在利用不同模态数据(如文本、图像、音频等)之间的互补信息,构建更加全面、准确的智能模型。多模态融合是实现多模......
  • AI大模型技术的四大核心架构演进之路
    随着人工智能技术的飞速发展,大模型技术已经成为AI领域的重要分支。本文将深入探讨四种关键的大模型技术架构:纯粹Prompt提示词法、Agent+FunctionCalling机制、RAG(检索增强生成)以及Fine-tuning微调技术,揭示它们的特性和应用场景。一、纯粹Prompt提示词法:构建直观交互模......
  • 多模态大模型:基础架构
    多模态大型语言模型(MLLM)是人工智能领域的前沿创新,它结合了语言和视觉模型的功能,可以处理复杂的任务,如视觉问答和图像字幕。这些模型利用大规模预训练,集成了多种数据模态,以显著提高其在各种应用程序中的性能。架构概览较为常见的MLLM框架可以分为三个主要模块:接收且有效......
  • PyTorch的模型定义方法
    文章目录1、简介2、导包3、设置属性4、构建数据集5、训练函数5.1、初始准备5.2、训练过程5.3、绘制图像6、运行效果7、完整代码......
  • 如何在 PyTorch 中使用类权重和焦点损失来处理多类分类的不平衡数据集
    我正在研究语言任务的多类分类(4类),并使用BERT模型进行分类任务。我正在关注这篇博文NLP的迁移学习:微调BERT用于文本分类我的BERT微调模型返回nn.LogSoftmax(dim=1)我的数据非常不平衡,所以我使用了|||计算类别的权重并使用损失中的权重。......
  • Python 协议和 Django 模型
    假设我有一个简单的协议A和一个未能实现该协议的类B:fromtypingimportProtocolclassA(Protocol):deffoo(self)->str:...classB:pass当下面的代码进行类型检查时,Mypy将正确地抱怨x:A=B()mypy.error:Incompatibletypes......