目录

 序言一

 序言二

 第2版前言

 第1版前言

 第1章 概述

 1.2 智能计算系统

 2.2 神经网络

 2.3 神经网络的训练方法

 2.4 神经网络的设计基础

 2.5 过拟合与正则化

 2.6 交叉验证

 第3章 深度学习应用

 3.2 适合文本/语音处理的循环神经网络

 3.3 大模型

 3.4 神经网络的优化

 3.5 神经网络量化

 3.6 驱动范例

 第4章 编程框架使用

 4.2 PyTorch概述

 4.3 PyTorch编程模型及基本用法

 4.4 基于PyTorch的模型推理实现

 4.5 基于PyTorch的模型训练实现

 4.6 驱动范例

 第5章 编程框架原理

 5.2 计算图构建

 5.3 计算图执行

 *5.4 深度学习编译

 *5.5 分布式训练

 第6章 面向深度学习的处理器原理

 6.2 向量处理器

 6.3 深度学习处理器

 6.4 大规模深度学习处理器

 6.5 本章小结

 习题

 第7章 深度学习处理器架构

 7.2 存储

 7.3 通信

 *7.4 设计优化

 第8章 智能编程语言

 8.2 智能计算系统抽象架构

 8.3 智能编程模型

 8.4 智能编程语言基础

 8.5 智能应用编程接口

 8.6 智能应用功能调试

 8.7 智能应用性能调优

 8.8 智能编程语言的应用

 习题

 第9章 大模型计算系统

 9.2 大模型驱动范例:BLOOM

 9.3 大模型系统软件

 9.4 大模型基础硬件

 习题

 后记


 陈云霁

 916个笔记

 序言一

  • 我国的人工智能应用和算法研究走在世界前列。但是,我国人工智能基础层、技术层和应用层的人才数量占比分别为3.3%、34.9%和61.8%,基础层人才比例严重偏低。这种现状是我国计算机领域长期不重视系统教育造成的。
  • 同样一个程序,一个普通的程序员来写和一个懂体系结构的程序员来写,性能可能差几万倍。
  • 。他的“智能计算系统”课程受到老师和学生的普遍欢迎。
  • 目前阻碍人工智能在各个行业落地的因素之一是人员成本太高,加速培养研究生只是解决困难的路径之一,但不能从根本上解决问题。

 序言二

  • 以一个典型的深度学习应用(图像风格迁移)作为驱动范例,将上层的算法、中间的编程、底层的芯片串联起来,帮助读者对人工智能的完整软硬件技术栈形成系统的理解。

 第2版前言

  • 例如GPT-3的模型参数量达1750亿,其训练使用了1万颗英伟达V100 GPU组成的高性能智能计算系统,单次训练用时14.8天,单次训练成本约为1000万美元。GPT-4有1.76万亿个参数,其训练更是使用2.5万颗A100 GPU运行了近100天,对智能计算系统算力的需求达到了GPT-3的67倍。未来如果还要训练出人脑规模的大模型(100万亿个参数),我们对智能计算系统算力的需求还将进一步提升。因此,大模型的发展使我们必须重新审视智能计算系统课程的知识体系。

 第1版前言

  • 仅谷歌一个公司就发表了2019年国际机器学习会议(ICML)近20%的论文,和整个中国相当。然而,当我们真正认真审视谷歌时就会发现,谷歌并不只是一个算法公司,它更是一个系统公司。谷歌的董事长J. Hennessy是国际最知名的计算机系统结构研究者,图灵奖得主;谷歌人工智能研究的总领导者J. Dean(每次谷歌I/O大会都是他代表谷歌介绍全公司的智能研究进展)是计算机系统研究者,著名的MapReduce分布式计算系统就出自他之手。谷歌在人工智能领域最令人瞩目的三个贡献——机器学习编程框架TensorFlow,战胜人类围棋世界冠军李世石的AlphaGo,以及谷歌自研的智能芯片TPU——也和系统有关,而非单纯的算法。
  • 到那时,如果一个学生只会算法调参,而对整个系统的耗时、耗电毫无感觉,不具备在实际系统上部署算法的能力,那他找到好工作的难度会较大。
  • “会用TensorFlow每年挣20万元人民币,会写TensorFlow每年挣20万美元。”
  • 缺乏系统思维的学生很容易陷入精度的牛角尖中,把科学研究当成体育比赛来搞(别人做了97%的精度,我就要做98%;别人做了98%,我就要做99%),最后研究道路越走越窄。事实上,从系统角度看,评价智能的标准远不止精度一个维度。速度、能效、成本等都是很重要的维度,无论在哪一个维度上做出突破,都是非常有价值的研究。
  • 详细的课程大纲、讲义、录像等参见智能计算系统课程主页http://novel.ict.ac.cn/aics/。

 第1章 概述

  • 同样在20世纪50年代,R. Bellman发表了论文“A Markovian Decision Process”(一种马尔可夫决策过程)[插图],奠定了强化学习的理论基础。
  • A. Pnueli提出了时态逻辑(Temporal Logic, TL),即在一阶逻辑上加入时间,并因此获得了1996年的图灵奖。但是TL还不能方便地表述对不确定的未来的判断,因此E. Clarke等人进一步提出了计算树逻辑(Computation Tree Logic, CTL),即把时间建模成一个树状结构,而树的每条路径都是历史发展的一种可能性。Clarke等人也因此获得了2007年图灵奖。
  • [插图] 图1.2 人工智能发展历史 在第一次热潮的初期,人工智能研究者对未来非常乐观。1957年H. Simon就提出:“现在世界上已经有机器可以思考、可以学习、可以创造。它们的能力将迅速提高,处理的问题范围在可见的未来就能延伸到人类思维应用的范围。”
  • 国际上还出现了一批基于领域知识和符号规则进行推理的系统,并有了一些较为成功的案例,包括医学领域的MYCIN和CADUCEUS。有的专家系统甚至在商业中发挥了实际作用。

 1.2 智能计算系统

  • 而人工智能算法或代码本身并不能构成一个完整的智能体,必须要在一个具体的物质载体上运行起来才能展现出智能。因此,智能计算系统就是智能的物质载体。
  • 常用的深度学习编程框架包括TensorFlow和PyTorch等,深度学习编程语言包括CUDA语言和BCL语言等。
  • 中科院计算所从2008年开始做人工智能和芯片设计的交叉研究,2013年和法国国家信息与自动化研究所(Inria)共同设计了国际上首个深度学习处理器架构——DianNao。随后,中科院计算所又研制了国际上首个深度学习处理器芯片“寒武纪1号”。
  • 手机更是因其要用深度学习处理大量图像识别、语音识别、自动翻译等任务,被广泛看作一种典型的小型智能计算系统。仅集成寒武纪深度学习处理器的手机就已有近亿台。
  • 因此,本书主要介绍第二代智能计算系统。 表1.1 代表性深度学习处理器/计算机 [插图] 1.2.3.3 第三代智能计算系统展望
  • 第一代和第二代智能计算系统均是面向弱人工智能的定制化设计的智能计算系统,目标是让智能算法跑得更快更省电。它们之间的区别仅在于,第一代智能计算系统面向符号主义智能(Prolog和LISP),而第二代面向连接主义智能(深度学习)。一个非常有意思的问题是,未来的第三代智能计算系统会是什么样子?

 2.2 神经网络

  • 深度学习的工作原理是,通过对信息的多层抽取和加工来完成复杂的功能。图2.8展示了深度学习在不同层上抽取出的特征[插图]。在第一层,深度学习通过卷积提取出局部比较简单的特征,如对角线;在第二层,可以提取到一些稍大范围稍复杂的特征,如条纹状的结构;在第三层,可以提取到更大范围更复杂的特征,如蜂窝网格的结构;最后,通过逐层细化的抽取和加工,可以完成很多复杂的功能。
  • 1998年,Y. LeCun[插图]提出了用于手写数字识别的卷积神经网络LeNet,其定义的卷积神经网络的基本框架和基本组件(卷积、激活、池化、全连接)沿用至今,可谓是深度学习的序曲。
  • 卷积神经网络主要被应用于图像处理领域,如VGG[插图]、GoogLeNet[插图]、ResNet[插图]等,对卷积神经网络的介绍详见3.1节。循环神经网络则被广泛应用于自然语言处理和语音识别等领域,如LSTM[插图]、GRU[插图]等,对循环神经网络的介绍详见3.2节。2017年后,以Transformer[插图]为基础的大模型不断发展,并向着可以处理多种任务、更加通用的方向发展,在多种不同任务上展现出更通用的智能,例如GPT-4[插图]在自然语言处理、图像处理、编写代码等多种任务上展现出非常接近人类的水平。

 2.3 神经网络的训练方法

  • 反向传播的基本原理是,首先根据正向传播结果和真实值计算出损失函数L(W),然后采用梯度下降法,通过链式法则计算出损失函数对每个权重和偏置的偏导,即权重或偏置对损失的影响,最后更新权重和偏置。

 2.4 神经网络的设计基础

  • 为了提高神经网络的训练准确率,常用方法包括调整网络的拓扑结构、选择合适的激活函数、选择合适的损失函数。
  • 神经网络中的隐层是用来提取输入特征中的隐藏规律的,因此隐层的节点数非常关键。如果隐层的节点数太少,神经网络从样本中提取信息的能力很差,则反映不出数据的规律;如果隐层的节点数太多,网络的拟合能力过强,则可能会把数据中的噪声部分拟合出来,导致模型泛化能力变差。泛化是指,机器学习不仅要求模型在训练集上的误差较小,在测试集上也要表现良好,因为模型最终要部署到没有见过训练数据的真实场景中。
  • 方法就会更稳定;如果激活函数的输出范围是无限的,例如一个激活函数的输出域是[0, +∞)​,神经网络的训练速度可能会很快,但必须选择合适的学习率(learning rate)。
  • 如果设计的神经网络达不到预期目标,可以尝试不同的激活函数。常见的激活函数包括sigmoid函数、tanh函数、ReLU函数、PReLU/Leaky ReLU函数、ELU函数等。
  • sigmoid函数也有一些缺点:(1)输出的均值不是0。sigmoid的均值不是0,会导致下一层的输入的均值产生偏移,可能会影响神经网络的收敛性。(2)计算复杂度高。sigmoid函数中有指数运算,通用CPU需要用数百条加减乘除指令才能支持e-x运算,计算效率很低。(3)饱和性问题。sigmoid函数的左右两边是趋近平缓的。当输入值x是比较大的正数或者比较小的负数时,sigmoid函数提供的梯度会接近0,导致参数更新变得非常缓慢,这一现象被称为sigmoid的饱和性问题。此外,sigmoid函数的导数的取值范围是(0, 0.25]​。当深度学习网络层数较多时,通过链式法则计算偏导,相当于很多小于0.25的值相乘,由于初始化的权重的绝对值通常小于1,就会导致梯度趋于0,进而导致梯度消失现象
  • sigmoid函数把输入变换到(0, 1)范围内,而tanh函数把输入变换到(-1, 1)的对称范围内,所以该函数是零均值的。因此tanh解决了sigmoid函数的非零均值问题。但是当输入很大或很小时,tanh函数的输出是非常平滑的,梯度很小,不利于权重更新,因此tanh函数仍然没有解决梯度消失的问题。
  • ReLU函数的计算特别简单,没有tanh函数和sigmoid函数中的指数运算,只需要对0和x取最大值,可以用一条计算机指令实现。而且,当x>0时,ReLU函数可以保持梯度不衰减,如图2.12所示,从而缓解梯度消失问题。因此,现在深度学习里,尤其是ResNet等上百层的神经网络里,常用类似于ReLU的激活函数。
  • 但是ReLU函数也存在一些问题:(1)ReLU函数的输出不是零均值的。(2)对于有些样本,会出现ReLU“死掉”的现象。在反向传播过程中,如果学习率比较大,一个很大的梯度值经过ReLU函数,可能会导致ReLU函数更新后的偏置和权重是很小的负数,进而导致下一轮正向传播过程中ReLU函数的输入是负数,输
  • 出为0。由于ReLU函数的输出为0,在后续迭代的反向传播过程中,该处的梯度一直为0,相关参数的值不再变化,从而导致ReLU函数的输入始终是负数,输出始终为0,即ReLU“死掉”​。
  • (3)ReLU函数的输出范围是无限的。这可能导致神经网络的输出的幅值随着网络层数的增加不断变大。
  • PReLU函数的定义与Leaky ReLU类似,唯一的区别是α是可调参数。每个通道有一个参数α,该参数通过反向传播训练得到。
  • ELU的输出均值接近0,可以加快收敛速度。当x > 0时,ELU取值为y=x,从而避免梯度消失。当x⩽0时,ELU为左软饱和,如图2.14所示,可以避免神经元死掉。ELU的缺点是涉及指数运算,计算复杂度比较高。
  • 高斯误差线性单元[插图](Gaussian Error Linear Units, GELU)是一种在BERT和GPT-2等模型中广泛使用的激活函数。
  • 如图2.15所示,GELU函数在输入接近零时保持近似线性,在远离零时则表现出非线性的饱和特性。GELU函数的优点之一是它的光滑性质,在保持非线性特性的同时,允许模型输出的概率分布更加平滑。这种光滑性与ReLU等激活函数相比,在处理语言模型中预测下一个词的概率时非常有用。因此,虽然GELU函数相比ReLU函数的计算复杂度高,但它可以提供更好的梯度传播和模型收敛性能,因而被广泛应用在基于Transformer的大语言模型中。还有很多其他的激活函数,本节不再一一介绍。
  • 基于梯度下降法的神经网络反向传播过程首先需要定义损失函数(loss function),然后计算损失函数对梯度的偏导,最后沿梯度下降方向更新权重及偏置参数。因此,损失函数的设定对梯度计算有重要的影响。
  • 由于均方差损失函数和sigmoid函数的组合会出现梯度消失,因此可以用别的损失函数(例如交叉熵损失函数)与sigmoid激活函数组合以避免这一现象。
  • 从式(2.51)和式(2.53)可以看出,使用sigmoid激活函数的交叉熵的损失函数对w
  • 和b的梯度中没有sigmoid的导数σ'(z),可以缓解梯度消失。
  • 损失函数是权重参数w和偏置参数b的函数,是一个标量,可以用来评价网络模型的好坏,损失函数的值越小说明模型和参数越符合训练样本(x, y)。对于同一个算法,损失函数不是固定唯一的。除了交叉熵损失函数,还有很多其他的损失函数。特别需要说明的是,必须选择对参数(w, b)可微的损失函数,否则无法应用链式法则。

 2.5 过拟合与正则化

  • 关于过拟合,冯·诺依曼有一个形象的说法,​“给我4个参数,我能拟合出一头大象;给我5个参数,我能让大象的鼻子动起来”​。当网络层数很多时,神经网络可能会学到一些并不重要甚至错误的特征。例如,训练时用一个拿着黑板擦的人的照片作为人的样本,过拟合时可能会认为人一定是拿黑板擦的,但这不是人的真正特征。过拟合时,神经网络的泛化能力比较差。深层神经网络具有很强的表示能力,但经常遭遇过拟合。为了提高神经网络的泛化能力,可以使用许多不同形式的正则化方法,包括参数范数惩罚、稀疏化、Bagging集成、Dropout、提前终止等。
  • 过拟合指模型过度逼近训练数据,影响了模型的泛化能力。具体表现为在训练数据集上的误差很小,但在测试数据集上的误差很大。尤其是神经网络层数多、参数多时,很容易出现过拟合的情况。除了过拟合,还有欠拟合。欠拟合主要是训练的特征少,拟合函数无法有效逼近训练集,导致误差较大。欠拟合一般可以通
  • 过增加训练样本或增加模型复杂度等方法来解决。图2.17a至图2.17c分别是合适的拟合、欠拟合、过拟合的示例。对于这个比较复杂的分类问题,合适的拟合可能是一条弧线,虽然会有一点误差;欠拟合会学出一条很简单的直线,误差比较大;而过拟合会学出奇怪的形状。当深度学习中训练的特征维度很多时,比如有上亿个参数,过拟合的函数可以非常接近数据集,函数形状很奇怪,但泛化能力差,对新数据的预测能力不足。
  • 再看看图2.18中的例子。如果只有三个变量,可以用二次曲线y=w0+w1x+w2x2把样本点拟合出来。如果用四次曲线去拟合,可能会拟合出一个奇怪的形状,该曲线在训练集上的误差可能会比二次曲线小一些,但在真实场景中,将其应用到没有见过的测试集上,效果是不会好的。为了减小三次项、四次项对模型的影响,可以采用正则化方法。
  • θ为正则化参数。对神经网络来说,模型参数包括权重w和偏置b,正则化过程一般仅对权重w进行惩罚,因此正则化项可记为θΩ(w)。正则化后的目标函数记为
  • 因此正则化的效果是使wi更接近0,即神经网络中的权重接近0,从而减少过拟合。
  • 稀疏化是在训练时让神经网络中的很多权重或神经元为0。有些稀疏化技术甚至可以让神经网络中90%的权重或神经元为0。稀疏化的好处是,在使用该神经网络时,如果神经网络的权重或神经元为0,则可以跳过不做计算,从而降低神经网络正向传播中90%的计算量。稀疏化很多时候是通过加惩罚项来实现的。
  • 具体识别的时候,可以取三个模型的均值作为输出,也可以再训练一个分类器去选择什么情况下该用三个模型中的哪一个。通过Bagging集成学习可以减少神经网络的识别误差。
  • Dropout正则化也可以避免过拟合,因为过拟合通常是由于神经网络模型太复杂了。Dropout丢掉一些隐层节点可能会带来意想不到的效果,降低神经网络模型的复杂度,还能避免过拟合。

 2.6 交叉验证

  • 所以训练集对应的是平时的作业题,会提供正确答案,而测试集对应的是期末用来判定学生水平的考试题。
  • 因此常见的方法是将数据集划分为三个部分:训练集、验证集和测试集。利用训练集训练神经网络后,在验证集上评估网络的准确率,并用验证集选择合适的神经网络超参数。当数据集规模较大、数据划分后三部分的数据分布较为接近时,同一个神经网络在验证集和测试集上的评估结
  • 果较为相近,使用验证集可以在选择合适的神经网络超参数的同时避免在测试集上出现过拟合的问题。
  • 如果数据集的规模较大,训练集和测试集基本可以满足样本间独立同分布,划分方式不会对神经网络的准确率造成较大的影响。但是当数据集规模较小时,划分方式不同可能会导致测出来的神经网络模型准确率波动很大。
  • 由于k-折交叉验证方法只需要训练k个模型,相比留一法交叉验证,其计算量小,耗费时间短。
  • 在目前的神经网络应用中,在图像分类、目标检测、机器翻译等大部分应用领域通常可以收集到较大规模的数据集,因此无须使用k-折交叉验证。但在某些领域,如医学图像处理、遥感图像处理等,数据收集较为困难,依然需要使用k-折交叉验证来评估模型的泛化能力,避免神经网络过拟合。

 第3章 深度学习应用

  • 图像处理主要用卷积神经网络,而语音、文字、视频等序列信息主要用循环神经网络及其改进版本——长短期记忆模型
  • 训练有30万个突触权重的神经网络层,很容易出现过拟合。如2.5.1节所介绍的,过拟合会把图像中对于分类不重要的信息当成重要的信息,抓不住问题的本质,导致模型在训练数据上表现好,在更广泛的测试数据上表现差,即模型的泛化能力差。实际应用中神经网络的权重远多于30万个。深度学习的神经网络中,通常有很多个隐层,每一层的神经元的数量可能远不止100个。如果采用简单粗暴的全连接的方式,当输入是224×224大小的RGB图像、第一个隐层有1000个神经元时,仅输入和隐层之间的权重就有1.5亿个。有如此多参数的神经网络是很难训练的,即使训练出来也往往会过拟合。
  • 卷积神经网络进行了以下两点设计:(1)局部连接。视觉理解的关键在于建立联系。相邻的数个像素点很可能属于同一个物体,它们之间具有紧密的联系;距离较远的两个点很可能不属于同一物体,它们的联系也相对松散。因此,视觉联系具有很强的局部性,卷积神经网络也放弃使用全局全连接,而使用更高效的局部稠密连接。(2)权重共享。卷积神经网络使用卷积核(也称为滤波器或卷积模板)做卷积处理,一张图片中不同的位置可以用同样的卷积系数(即突触权重)​。例如一张图片的左上角和右下角,神经网络的突触权重可以是同一组值。其原理是,每一组权重抽取图像的一种特征。例如,抽取形状特征时,在图像的不同位置都可以用同一组权重。基于局部连接和权重共享两种技术,卷积神经网络可以大幅减少处理图像时所需的权重的数量,从而避免过拟合。
  • 征图代表64个不同的卷积核抽取出的图像中的64种不同的特征,同时特征图尺寸保持不变;卷积层后面是池化层,通过池化把特征图的尺寸从224×224变为112×112;然后交替地出现卷积层和池化层,特征图的尺寸随之不断变小,从112×112变为56×56,最后变成7×7。与此同时,抽取出来的特征数量在不断增多,从第1组卷积层抽取出64种特征,从第2组卷积层抽取出128种特征,从最后一组卷积层抽取出512种特征。然后用全连接层,将输入的神经元和输出的神经元全部一一连接起来。当然,在每一个卷积层和每一个全连接层内部,除了向量内积,还需要有激活函数。最后,还会用到softmax函数进行分类概率的凸显、抑制以及归一化。
  • 由于卷积层的局部连接和权重共享的特点,对一张图片做卷积时,不相邻的区域不会放在一起计算,如图3.3所示。
  • 浅层神经网络采用全连接方式,计算一个输出需要用到所有输入。而卷积神经网络计算卷积层的一个输出只需要用到kw×kh个输入,其中kw×kh是卷积核的大小,kw和kh可以是1、3、5、7、9、11等。此外,浅层神经网络中所有神经元之间的连接都采用不同的权重,因此具有Ni个输入、No个输出的全连接的权重为Ni×No个。而卷积神经网络中卷积层的一对输入特征图和输出特征图共用同一组权重,权重仅为kw×kh个,大幅减少了权重的数量。
  • 元素对应相乘再求和的计算过程恰好与向量内积的计算过程相同,因此可以将每个窗口滑动时对应的元素拉成行向量,同时将卷积核拉成列向量,通过向量内积计算卷积当前位置的结果。这个将特征图拉成向量的过程称
  • 为im2col(image to column)。
  • 原始卷积运算过程中,窗口滑动后多个窗口对应的行向量排列在一起,就形成矩阵。每个卷积核分别拉成一个列向量,多个卷积核对应的列向量排列在一起,也形成矩阵。通过这个过程,卷积运算就可以转化成矩阵运算。矩阵运算结束后,输出矩阵的每个列向量对应一个输出特征图的结果,可以再将列向量恢复成特征图,这个过程称为col2im(column to image)。
  • 本质上这是一种用空间换时间的方法,因为在im2col的过程中,两个窗口重叠的数据被冗余存储,占用了更多的存储空间。另一方面,大部分编程框架中都可以调用高效的矩阵乘法库,如BLAS、MKL等。将卷积运算转换成矩阵运算形式后,可以通过这些库进行加速。
  • 以3×3大小的卷积核为例,用[1, 0, -1; 1, 0, -1; 1, 0, -1]做卷积核与输入进行卷积,可以得到图3.8a右侧的输出。在该输出中,0在两侧,30在中间,即输入图片中两侧没有明显变化的区域变成了0,相当于找到了输入图片中最中间的一条竖线把左右两边区分开来,从而把垂直边缘特征准确地提取出来。
  • 对角线边缘特征,可以用图3.8b中的卷积核[1, 1,
  • 0; 1, 0, -1; 0, -1, -1]​。该卷积核对角线上的系数是0,右下的3个系数是-1,左上的3个系数是1。用该卷积核和输入做卷积,可以得到右侧的输出。在该输出中,对角线上的值为30,右下角和左上角的值为0,因为输入图片中左上角和右下角没有变化。
  • 做卷积运算时,如果不做边界扩充(padding),卷积之后的输出尺寸会被动地略微变小。假设输入图片或特征图的大小为Wi×Hi,卷积核的大小为kw×kh,则卷积输出的特征图的大小为(Wi-kw+1)×(Hi-kh+1)。这是因为计算每个点的输出时,卷积核需要完全在输入特征图内。例如,输入是32×32大小的图像,用一个4×4大小的卷积核进行卷积,如果不做边界扩充,输出特征图的大小为29×29;如果用同样大小的卷积核再做一层卷积,输出特征图的大小就变成26×26;如果用同样大小的卷积核再做一层卷积,输出特征图的大小就变成23×23;经过几层卷积之后,就没有输出了。对于一个上百层的神经网络,如果不做边界扩充,将计算不出最后的特征图,因此一定要做边界扩充。
  • 因为扩充的点都是0,在卷积中会发挥出比较强的特征提取的作用,而且图像的边缘通常会有比较重要的特征。
  • 采用大于1的卷积步长对特征图进行降采样,可以利用局部特征,获得平移不变性等。
  • 如果要将输出特征图的长宽都减半,可以做边界扩充,同时将卷积步长设为2。
  • 长方形的卷积核可以减少参数的数量,例如将在3.1.2.3节介绍的Inception-v3,用一层1×n的卷积和一层n×1的卷积代替n×n的卷积,参数量减少了n×n-1×n-n×1。卷积计算之后可能还要加一个偏置来得到输出。
  • 如果卷积步长不是1,则输出特征图的大小会变成输入特征图的1/(sw×sh),其中sh和sw分别是高度方向和宽度方向的卷积步长。不失一般性,假设边界扩充时在输入特征图的上下以及左右边界分别加pt、pb行0,以及pl、pr列0,则输出特征图的宽度和高度分别是
  • 卷积层和池化层构成特征提取器,而全连接层(fully-connected layer,简记为FC)是分类器。全连接层将特征提取得到的高维特征图映射成一维特征向量,该特征向量包含所有特征信息,可以转化为最终分类为各个类别的概率。例如,一个224×224大小的输入图片经过多层卷积和池化,可能变成4096个1×1大小的特征图,根据这4096个特征可以做一个全连接层,来判定最后是猪、狗、猫、牛、羊中的哪一个。在具体计算时,全连接层等价于先将输入的高维特征图展平成一个向量,然后通过矩阵相乘对向量做线性变换,映射成另一个向量。
  • 通过归一化计算,可以凸显较大的值并抑制较小的值,从而显著地抑制次要特征,决定分类概率。
  • 当图3.11中N=3, M=1, K=2时,其网络结构为:输入→卷积层(ReLU)→卷积层(ReLU)→卷积层(ReLU)→池化层→全连接层(ReLU)→全连接层(ReLU)→全连接层→输出。在GoogLeNet等网络中,还有更多种组合方式,例如池化层和卷积层可以包含分支。譬如一支卷积核输出一组76×76×37的特征,另一支卷积核输出一组38×38×99的特征,做完分支之后,最后将分支合起来做分类。关于分支将在3.1.2.3节详细介绍
  • 卷积神经网络为何选择深而不广的神经网络结构?2.2.2节介绍过,两层的神经网络中只有一个隐层,理论上只要有足够多的神经元,两层的神经网络足以拟合出任意的函数[插图]。但在实际中,一个复杂特征往往是由多个简单特征组成的,采用深层的网络结构,可以很好地完成对图像从局部到整体的理解。例如人脸识别时,可能先看到一个局部的简单特征,可能是一团黑色的圆圈;再到更大的范围看,这个圆圈可能是眼睛;再到更大的范围看,眼睛上面还有眉毛;再到更大的范围看,可能左边有一个眼睛和眉毛,右边有一个眼睛和眉毛;再到更大的范围看,可能是一张脸,从而识别出一个人。这种层次化的结构非常适合从局部到整体地理解图像。
  • 图3.11 常见的卷积神经网络结构
  • 如果卷积核的大小是3×3,输入是3个特征图,输出是2个特征图,最终的所有参数个数仅为3×3×3×2=54。由于深度神经网络权重的数量较少,它过拟合的风险也会变小,在面对海量训练数据时,它的训练速度也能被业界所接受。
  • 基于卷积神经网络的图像分类算法的起源非常早,最早可追溯到日本学者福岛邦彦在1980年提出的Neocognitron(神经认知机)神经网络模型[插图]。该模型借鉴了生物的视觉神经系统。但该模型提出之后,在国际上一直不温不火,一直到2012年AlexNet在ImageNet大规模视觉识别比赛(ImageNet Large Scale VisualRecognition Competition, ILSVRC)中大获全胜之后,卷积神经网络的潜力才被广泛认识到,并真正成为业界关注的焦点。
  • 2010年和2011年ILSVRC冠军主要采用传统视觉算法,Top-5错误率分别是28.2%和25.8%。2012年提出的AlexNet[插图]是一个8层的卷积神经网络,将Top-5错误率从25.8%降到了16.4%。在此之后,深度学习的发展非常迅速,网络深度也在不断地快速增长,从8层的AlexNet[插图]到19层的VGG[插图],再到22层的GoogLeNet[插图],以及152层的ResNet[插图],甚至252层的SENet[插图]。对于ImageNet上1000种物体的分类,ResNet的Top-5错误率仅为3.57%,这是非常振奋人心的进展。
  • AlexNet的网络配置如表3.2所示。
  • (1)Dropout(随机失活)​。在训练的过程中随机舍弃部分隐层节点,可以避免过拟合。(2)LRN(局部响应归一化)​。LRN可以提升较大响应,抑制较小响应。当然最近几年业界发现LRN层作用不大,所以现在使用LRN的研究者很少。(3)max pooling(最大池化)​。最大池化可以避免特征被平均池化模糊,提高特征的鲁棒性。在AlexNet之前,很多研究用平均池化。从AlexNet开始,业界公认最大池化的效果比较好。(4)ReLU激活函数。在AlexNet之前,常用的激活函数是sigmoid和tanh。而ReLU函数很简单,输入小于0时输出0,输入大于0时输出等于输入。以前业界认为ReLU函数太简单了,但事实上非常简单的ReLU函数带来了非常好的效果。AlexNet在卷积层和全连接层的输出均使用ReLU激活函数,能有效提高训练时的收敛速度。
  • LRN对同一层的多个输入特征图在每个位置上做局部归一化,以提升高响应特征和抑制低响应特征。LRN的输入是卷积层的输出特征图经过ReLU激活函数后的输出。假设LRN的输入是C个特征图,LRN要对输入的c个相邻特征图上相同位置的点进行归一化处理,得到第m个输出特征图上位置(i, j)处的值[插图]
  • 但实际上,一个位置上的点可能既参与到三角形中,又参与到正方形和长方形中,还参与到菱形中,强行抑制一个点参与的低响应特征并不一定合理。因此,LRN在实际中并没有产生明显效果,现在已很少有人使用。
  • 使用Dropout进行模型训练的过程为:(1)以一定概率(如0.5)随机地舍弃部分隐层神经元,即将这些神经元的输出置为0。(2)一小批训练样本经过正向传播后,在反向传播更新权重时,不更新与被舍弃神经元相连的权重。(3)恢复被删除神经元,输入另一小批训练样本。(4)重复步骤(1)~(3),直到处理完所有训练样本。在Dropout训练过程中,并不是真的丢掉部分隐层神经元,只是暂时不更新与其相连的权重。对于这一批样本,可能不用某些隐层神经元,但对于下一批样本,可能又会用到这些隐层神经元,并且需要更新与其相连的权重。训练完成后使用神经网络进行预测时,所有神经元都是要用到的。AlexNet网络的前两个全连接层使用了Dropout。Dropout可以防止训练数据中复杂的共同适应(co-adaptation),即一个特征检测器需要依赖其他几个特定特征检测器,从而缓解过拟合。
  • 神经网络层数增多之后会遇到很多问题,包括梯度爆炸、梯度消失等。距离输出层(计算损失函数)很近的一两层,可能很快就可以训练好;但是距离输出层10层、100层时,损失函数的导数可能会非常小,神经网络可能就无法继续训练了。
  • K. Simonyan和A. Zisserman设计了一系列不同配置的VGG网络结构[插图],在此基础上提出了预训练策略,利用较浅神经网络训练出来的权重参数来初始化更深神经网络的部分层,从而达到逐步加深神经网络层数的效果。
  • VGG通过逐渐增加层数,训练出了多个不同版本的神经网络。首先增加LRN层,发现LRN层没用。然后发现,随着层数的增多,Top-5错误率基本上是在下降的。但是到了16层的VGG-D和19层的VGG-E, Top-5错误率基本都在8%左右,不再有大的变化。
  • 7×7的卷积与3层连续的3×3的卷积的感受野相同,11×11的卷积与5层连续的3×3的卷积的感受野相同。而11×11的卷积,有121个参数,5层连续的3×3的卷积只有45个参数。因此,VGG通过使用连续的小卷积核,用更少的参数,就能完成AlexNet中大卷积核的任务,训练起来难度也会更小。
  • 图3.15 一个5×5卷积层和连续2个3×3卷积层的感受野大小相同
  • VGG的成功主要得益于以下几点:(1)使用规则的多层小卷积替代大卷积。在相同视野下,有效减少了权重参数的数量,提高了训练速度。(2)使用更深的卷积神经网络。在神经网络中使用更多的卷积层及非线性激活函数,提高了图像分类的准确率。(3)通过预训练,对部分网络层参数进行初始化,提高了训练收敛速度
  • 在VGG之后,Inception进一步考虑了能否用更小的卷积核、更多的神经网络层来降低错误率。在Inception系列工作中,最著名的是Inception-v1,即GoogLeNet,它获得了2014年ImageNet比赛的冠军。在GoogLeNet之后,又出现了BN-Inception、Inception-v3、Inception-v4等(见表3.4)​。
  • 1×1卷积。1×1卷积实际上是跨通道的聚合,将多个输入特征图上相同位置的点做全连接处理,计算出输出特征图上对应位置的一个点,因此相当于是输入和输出之间的全连接。同时,每个1×1卷积层都使用ReLU激活函数来提取非线性特征。
  • 增加1×1卷积层后,5×5卷积层的输入维度从256降为32,参数数量减少为原来的1/7.2,计算量也减少为原来的1/7.2。因此,1×1卷积已广泛应用于多种神经网络架构中,包括ResNet。
  • 利用该辅助分类网络,可以在训练过程中观察到第l层的训练结果,如果训练效果不好,可以提前从第l层做反向传播,调整权重。这种方式有助于训练一个很多层的神经网络,防止梯度消失。而传统的训练方式从神经网络的最终输出进行反向传播,如果中间某个地方出错了,也得从最终输出反向传播过来,采用梯度下降法寻找最小误差,在该过程中梯度很可能就会消失或爆炸。值得注意的是,softmax辅助分类网络仅用于训练阶段,不用于推理阶段。
  • 相对于VGG, GoogLeNet网络层数更深、参数更少、分类准确率更高,这主要得益于三个方面:一是增加了softmax辅助分类网络(也称为观察网络)​,可以观察训练的中间结果,提前反向传播;二是增加了很多1×1卷积,可以降低特征图维度,减少参数数量以及计算量;三是引入了非常灵活的Inception模块,能让每一层网络适应不同尺度的图像的特性。
  • 以下是BN背后的原理。随着神经网络层数的增多,在训练过程中,各层的参数都在变化,因此每一层的输入的分布都在变化,其分布会逐渐偏移,即内部协方差偏移(internal covariate shift)。输入分布通常会向非线性激活函数的两端偏移,靠近饱和区域,因此会导致反向传播时梯度消失。此外,为了不断适应新的分布,训练时需要较低的学习率和合适的初始化参数,因此训练速度很慢。在批训练时,如果对多个样本做归一化,把激活函数的输入归一化到标准正态分布(均值为0,方差为1)​,激活函数的取值就会靠近中间区域,输入很小的变化就能显著地体现到损失函数中,就不容易出现梯度消失。
  • 如果激活函数的输入都简单归一化为标准正态分布,可能会将激活函数的输入限定到线性区域,此时激活函数不能提供非线性特征,神经网络的表达能力会下降。因此,BN需要对归一化后的值进行缩放和偏移。缩放因子γ和偏移变量β是和模型参数一起训练得到的,相当于对标准正态分布做一个水平偏移并变宽或变窄,等价于将非线性函数的值从中间的线性区域向非线性区域偏移,从而可以保持网络的表达能力。
  • 图3.18 GoogLeNet网络结构(左图)及配置(右图)[插图]
  • 其中,γk和βk为每一个维度的缩放和偏移参数。在整个训练集上做上述BN变换是难以实现的;此外随机梯度训练时通常使用小批量数据进行训练。因此实际中,使用随机梯度训练中的小批量数据来估计均值和方差,做BN变换。
  • BN很多时候比LRN、Dropout或L2正则化的效果好,而且可以大幅提高神经网络的训练速度。在Inception训练中加上BN,可以把学习率调得非常大,从而加速训练。把学习率调大5倍,BN训练速度比原始Inception快14倍[插图],更惊人的是,准确率还可以略有提升(0.8%),可谓是“多快好省”​。现在,BN已经不仅用于Inception系列网络,而是已经成为各种当代深度学习神经网络必备的训练技术之一。
  • Inception-v1(GoogLeNet)将部分7×7卷积和5×5卷积拆分为多个连续的3×3卷积。延续这个思想,Inception-v3[插图]将卷积核进一步变小,将对称的n×n卷积拆
  • 分为两个非对称的卷积,1×n卷积和n×1卷积。例如,将3×3卷积拆分为1×3卷积和3×1卷积,从而把参数的数量从9个减为6个。这种非对称的拆分方式,可以增加特征的多样性。因此,形成了图3.20中的3种新的Inception模块结构。将图3.20中的3种Inception模块组合起来,把GoogLeNet中第一层7×7卷积拆分为3层3×3卷积,对所有卷积层和辅助分类网络的全连接层做BN,就形成了Inception-v3的网络结构,如表3.5所示。该网络进一步提高了分类的准确率。
  • 何恺明等人认为,深层神经网络准确率降低是因为层数增加导致的神经网络的退化[插图]。也就是说,神经网络层数增加越多,训练时越容易收敛到一些局部最优的极值点上,而不是全局最优点,导致误差比较大。
  • 常规的卷积神经网络,对输入做卷积运算得到输出,等同于用多项式拟合输出并使输出与图像识别的结果一致。但ResNet的基本单元如图3.22b所示,增加了从输入到输出的直连(shortcut connection),其卷积拟合的是输出与输入的差(即残差)​。由于输入和输出都做了BN,都符合正态分布,因此输入和输出可以做减法,如图3.22b中F(x):=H(x)-x。而且从卷积和BN后得到的每层的输出响应的均方差来看,残差网络的响应小于常规网络[插图]。残差网络的优点是对数据波动更灵敏,更容易求得最优解,因此能够改善深层网络的训练。
  • 如图3.23所示,ResNet网络结构是基于VGG19的网络结构发展而来的。首先,把VGG中对应的3×3卷积层拆出来,变成对应的残差模块,这些残差模块由2个3×3卷积层组成。其次,特征图的缩小用步长为2的卷积层来完成。如果特征图的尺寸减半,则特征图的数量翻倍;如果特征图的尺寸不变,则特征图的数量也不变。再次,增加了跳转层。跳转层中有实线和虚线,实线表示特征图的尺寸和数量不变,虚线表示特征图的尺寸减半而数量翻倍。当特征图数量翻倍时(虚线连接)​,对于输入到输出的直连有两种处理方式:一种是做恒定映射,增加的维度的特征填充0;另一种是用1×1卷积进行特征图数量翻倍,该方式会引入额外的卷积参数。这两种方式处理时的步长都为2,以满足特征图尺寸减半。此外,每层卷积之后、激活函数之前做BN,一方面使得残差模块的输入和输出在同一值域,另一方面缓解梯度消失。
  • 在ResNet被提出后,研究人员在其基础上进一步设计了规模更大、层数更深、分类准确率更高的卷积神经网络。例如,DenseNet[插图]将残差连接设计改进为稠密连接,进一步提高分类准确率;ResNeXt[插图]通过结合ResNet和Inception的思想设计了分组卷积(group convolution),可以在与ResNet同等参数量的情况下取得更高的分类准确率;MobileNet系列[插图][插图][插图]通过设计深度可分离卷积(depthwise separable convolution)实现卷积神经网络的轻量化设计;ShuffleNet系列[插图][插图]通过设计通道重排(channel shuffle)增强深度可分离卷积的通道间信息交互,提升轻量级卷积神经网络的性能;EfficientNet[插图]通过同时增加网络的深度、宽度(通道数)和输入网络的分辨率来提升卷积神
  • 经网络的性能。在Transformer被提出后,基于Transformer的神经网络(如ViT和Swin Transformer,详见3.3.5节)在图像分类领域也取得了令人瞩目的效果,甚至一度超过了卷积神经网络。之后,ConvNeXt[插图]从ResNet出发,借鉴SwinTransformer的设计思路,通过一系列宏观设计、模块调整、卷积核、微观设计等多种改进提升卷积神经网络的分类准确率,取得超越Swin Transformer的结果。
  • 目前,基于卷积神经网络的图像目标检测算法主要分为两大类:两阶段和一阶段。两阶段(two-stage)算法基于候选区域方法,首先产生边界框把所有物体框出来;然后用基于卷积神经网络的图像分类算法对每个候选区域进行分类。两阶段算法的代表是R-CNN系列算法。一阶段(one-stage)算法对输入图像直接处理,同时输出物体定位及其类别,即在框出来物体的同时对物体进行分类,主要包括YOLO系列以及SSD算法。
  • 常用的目标检测数据集包括PASCAL VOC[插图]和MSCOCO[插图]。PASCAL VOC[插图]是PASCAL视觉目标分类挑战赛所使用的数据集,有VOC2007和VOC2012两个版本,包含20个类别的标注。其中VOC2007中包含9963张标注过的图片,共标注出24 640个物体,VOC2012有11 540张标注图片共27 450个物体。PASCALVOC是目标检测领域经典的基准数据集,但随着目标检测网络的规模不断扩大,能力不断增强,PASCAL VOC的规模和类别数量制约了评测更强目标检测网络的能力。因此微软建立了规模更大的目标检测数据集MSCOCO[插图]。目前目标检测领域常用的版本是COCO2017,包含80个类别,训练集和验证集共包含约12万张图像。平均每张图片中包含7个物体,一张图片中最多可能包含63个物体,且图片中物体的尺寸跨度较大,对目标检测带来极大的挑战。
  • 目标检测领域常用的评价指标是mAP,需要通过计算输出结果的边界框与实际框的交并比(IoU),绘制查准率-召回率曲线计算。下面介绍具体的评价指标计算方式。
  • 果定位准确,方形框A和B完全重叠,则IoU=1。如果完全定位不到,方形框A和B完全没有重叠,则IoU=0。如果定位到一部分,方形框A和B有一定重叠,则IoU∈(0, 1)。通常如果IoU⩾0.5,则认为定位比较准确。具体标准也可以根据具体场景进行分析。
  • 召回率/查全率(Recall):选出的N个样本中,选对的k个正样本占总的M个正样本的比例,即[插图]查准率/精度(Precision):选出的N个样本中,选对的k个正样本的比例,即[插图]上面的例子中,物体A的召回率是RecallA=50/100=0.5,查准率为PrecisionA=50/1000=0.05。显然通过增加框,比如增加100万个框,可以提高召回率,但会降低查准率。
  • 假设算法在100张测试图像中共检测出20个分类为A的候选框,各候选框的置信度(confidence score)及其标签如表3.7中左表所示。其中置信度用IoU来度量,如果框的标签为0则表示框内没有物体,标签为1则表示框内有物体。平均查准率AP的计算过程如下:
  • 根据PASCAL Visual Object Classes Challenge 2012(PASCAL视觉目标分类挑战赛,简称VOC2012)[插图]中平均召回率AP的计算方法,对于每个召回率r,计算任意召回率r~⩾r时的最大的查准率,作为召回率r对应的查准率,如表3.7中右表的最右列所示。然后,计算更新后的查准率-召回率曲线的面积作为平均查准率AP。该例子中类别A检测的平均查准率为[插图]最后,图像测试集中C种类别的检测的平均查准率均值[插图]。
  • -CNN算法是R-CNN系列的基础,其处理流程比较复杂。如图3.26所示,R-CNN主要包括四个步骤:表3.8 R-CNN系列[插图]注:表中数据来源于论文[插图][插图]在VOC2012测试集上的实验结果。(1)候选区域提取:通过选择性搜索(selective search)从原始图像中提取约2000个 候选区域。(2)特征提取:首先将所有候选区域裁切缩放为固定大小,再对每个候选区域用AlexNet(其中的5个卷积层和2个全连接层)提取出4096维的图像特征,也可以用ResNet、VGG等网络。(3)线性分类:用特定类别的SVM(Supported Vector Machine,支持向量机)对每个候选区域做分类。(4)边界框回归:用线性回归来修正边界框的位置与大小,其中每个类别单独训练一个边界框回归器(bbox regressor)。通过上述方式,可以把图3.26中的物体用候选框提取出来,包括一个人、一匹马、一面墙等。
  • -CNN算法中只有第三步与神经网络有关。第一步用选择性搜索方法提取约2000个候选区域,第二步用图像缩放算法,第四步用SVM分类做图像识别、线性回归微调边框,这些都是传统机器学习和计算机视觉的方法。候选区域的选取。候选区域提取通常是采用经典的目标检测算法,使用滑动窗口依次判断所有可能的区域。R-CNN对候选区域选取做了优化,采用选择性搜索[插图]预先提取一系列比较有可能是物体的候选区域,之后仅在这些候选区域上提取特征,从而可以大大减少计算量。基于选择性搜索[插图]的候选区域提取算法主要使用层次化分组算法生成不同图像条件下的目标位置。层次化分组算法,首先用基于图的图像分割方法创建初始区域,并将其加入候选区域列表中;再计算所有相邻区域间的相似度;随后,每次合并相似度最高的两个相邻图像区域,计算合并后的区域与其相邻区域的相似度,将合并后的图像区域加到候选区域列表中,重复该过程直至所有图像区域合并为一张完整的图像;然后,提取候选区域的目标位置框,并按层级排序(覆盖整个图像的区域的层级为1)​。为了找到不同图像条件下的候选区域,要在不同图像分割阈值、不同色彩空间、不同相似度(综合考虑颜色、纹理、大小、重叠度)下,调用层次化分组算法,然后对所有合并策略下得到的位置框按照优先级排序,去掉冗余框。其中,为了避免按照区域大小排序,优先级采用层级乘以随机数的方式。最后,R-CNN取约2000个候选区域作为后续卷积神经网络的输入。分类与回归。分类与回归的处理过程如图3.27所示。首先,对候选区域进行分类,每个类别有一个SVM分类器,将2000个候选区域中的物体(包括背景)都通过21个分类器进行分类处理,判断每个候选区域最可能的分类,例如人、车、马等。然后,做NMS(Non-Maximum Suppression,非极大值抑制)去掉一些冗余框。例如同一个物体可能有不同的框,需要去掉一些冗余框,仅保留一个框。最后做边界框回归,通过线性回归进行候选框的微调校准,以比较准地框出物体,最终提高物体检测的mAP。[插图]图3.27 R-CNN算法中分类与回归上述过程中最重要的环节之一是非极大值抑制。在目标检测过程中,会形成2000个左右的候选框,同一物体位置(比如图3.27中的车)可能会有多个候选框,这些候选框之间会有重叠,就需要利用NMS找到较优的目标边界框,去除冗余的边
  • 界框。每个类别都要做一次NMS,以得到最终候选框的输出列表[1]。对单个类别的NMS的处理步骤包括:(1)根据检测得分对候选框进行排序作为候选框列表。(2)将分数最高的候选框bm加到最终输出列表中,并将其从候选框列表中删除。(3)计算bm与其他候选框bi的IoU,如果IoU大于阈值,则从候选框列表中删除bi。(4)重复上述步骤,直至候选框列表为空。
  • 根据映射关系,从卷积特征图上提取出不同尺寸的候选区域对应的特征图,并池化为维度相同的特征图(因为全连接层要求输入尺寸固定)​。由于Fast R-CNN只做一次卷积神经网络处理,大幅减少了计算量,提高了处理速度。然后,将维度相同的特征图送到全连接层,转化为RoI特征向量(RoI featurevector)。最后经过全连接层,用softmax分类器进行识别,用边界框回归器修正边界框的位置和大小,再对每个类别做NMS,去除冗余候选框。Fast R-CNN最本质的变化是,将需要运行2000次的卷积神经网络,变成运行一个大的卷积神经网络。感兴趣区域(Region of Interest, RoI)对应提取出来的候选区域。RoI pooling可以将不同尺寸的RoI对应的卷积特征图转换为固定大小的特征图(如7×7)​,一方面可以复用卷积层提取的特征图以提高图像处理速度,另一方面可以向全连接层提供固定尺寸的特征图。对于每个特征图通道,RoI pooling根据输出尺寸Wo×Ho将输入特征图Wi×Hi均分为多块,每个块大小约为Wi/Wo×Hi/Ho,然后取每块的最大值作为输出值。
  • Fast R-CNN的主要改进包括:(1)直接对整个图像做卷积,不再对每个候选区域分别做卷积,减少了大量重复计算。(2)用RoI pooling对不同候选区域的特征图进行尺寸归一化,使不同尺寸的候选区域对应到固定尺寸的特征图。(3)将边界框回归器和网络一起训练,每个类别对应一个回归器。(4)用softmax层代替SVM分类器,从而将R-CNN中很多小的神经网络变成一个大的神经网络。
  • 为了解决Fast R-CNN中候选区域提取的瓶颈,Faster R-CNN[插图]设计了更高效的候选区域提取方法——区域候选网络(Region Proposal Network, RPN),把候选区域提取也用神经网络来实现,从而进一步提升了图像目标检测的速度。Faster R-CNN将RPN和Fast R-CNN结合起来,如图3.29a所示,其主要处理过程大致如下。(1)输入图片经过多层卷积神经网络(如ZF[插图]和VGG-16的卷积层)​,提取出卷积特征图,供RPN和Fast R-CNN中的RoI pooling使用。RPN和Fast R-CNN共享特征提取网络可大大减少计算时间。(2)RPN对特征图进行处理,生成候选框,用softmax判断候选框是前景还是背景(对应图3.29a中的cls层)​,从中选取前景候选框并利用bbox回归器调整候选框的位置(对应图3.29a中的reg层)​,得到候选区域。(3)RoI pooling层,与Fast R-CNN一样,将不同尺寸的候选框在特征图上的对应区域池化为维度相同的特征图。(4)与Fast R-CNN一样,用softmax分类器判断图像类别,同时用边界框回归器修正边界框的位置和大小。
  • 图3.29 Faster R-CNN结构,图中省略了RPN中softmax前后的转换(分别将二维特征图转为一维向量、将一维向量转为二维特征图)Faster R-CNN的核心是RPN。RPN的输入是特征图,输出是候选区域集合,包括
  • 各候选区域属于前景或背景的概率以及位置坐标,并且不限定候选区域的个数。RPN中采用一种anchor机制,能够从特征图上直接选出候选区域的特征,相对于选择性搜索,大大减少了计算量,且整个过程融合在一个神经网络里面,方便训练和测试。RPN的具体计算过程大致如下。(1)先经过一个3×3卷积,使每个卷积窗口输出一个256维(ZF模型)或512维(VGG16)特征向量。(2)然后分两路处理:一路经过1×1卷积之后做softmax处理,输出候选框为前景或背景的概率;另一路做边界框回归来确定候选框的位置及大小。(3)两路计算结束后,计算得到前景候选框(因为物体在前景中)​,再用NMS去除冗余候选框,最后输出候选区域。Faster R-CNN没有限定候选框的个数(如2000个)​,而是提出了anchor box(锚框)​,如图3.30所示。特征图的每个位置可以有k=9个可能的候选框,包括3种面积和3种长宽比。3种面积可以是128×128、256×256、512×512,每种面积又分成3种长宽比,分别为2:1、1:2、1:1,总计9个不同的候选框,这些候选框也被称为anchor。在RPN中,特征图的每个位置会输出2k个得分,分别表示该位置的k个anchor为前景/背景的概率,同时每个位置会输出4k个坐标值,分别表示该位置的k个框的中心坐标(x, y)及其宽度w和高度h,这些值都是用神经网络计算
  • Fast R-CNN用softmax层取代了R-CNN中的SVM分类器,Faster R-CNN用RPN取代了选择性搜索。从R-CNN到Fast R-CNN,再到Faster R-CNN,目标检测的四个基本步骤(候选区域提取、特征提取、分类、边界框回归)中的很多传统的计算机视觉的技术逐渐被统一到深度学习框架中,大大提高了运行速度。R-CNN系列工作是两阶段目标检测算法的代表性工作,后续的两阶段目标检测算法大多是基于Faster R-CNN的改进。例如,Light-Head R-CNN[插图]通过设计轻量化的特征提取网络提高检测效率;FPN[插图]通过在RPN中添加特征金字塔(feature pyramid)实现多尺度特征融合,从而提升算法对不同尺寸目标检测的mAP; Mask R-CNN[插图]通过添加额外的目标掩码(object mask)分支,并将RoIpooling改进为更加精确的感兴趣区域对齐(RoI align)操作,进一步提升目标检测的mAP; Soft NMS[插图]通过改进NMS提升候选框的准确度。以R-CNN系列工作为代表的两阶段目标检测算法通常检测mAP较高,但是检测速度较慢,不利于其在视频监控、自动驾驶等实际场景的使用。相比之下,一阶段的目标检测算法虽然检测mAP低于两阶段算法,但检测速度较快,更加容易满足实际应用中的实时性 要求。
  • YOLO的主要思想是,把目标检测问题转换为直接从图像中提取边界框和类别概率的单回归问题,一次就可检测出目标的类别和位置[插图]。因此,YOLO模型的运行速度非常快,在一些GPU上可以达到45帧/秒的运算速度,可以满足实时性应用要求。
  • confidence综合考虑当前边界框中存在目标的可能性Pr(Object)以及预测框和真实框的交并比[插图],定义为[插图][插图]如果一个框内没有物体,则confidence=0,否则confidence等于交并比。在训练时,可以计算出每一个框的confidence。
  • (1)检测速度非常快。YOLO将目标检测重建为单一回归问题,对输入图像直接处理,同时输出边界框坐标和分类概率,而且每幅图像只预测98个边界框。因此YOLO的检测速度非常快,在Titan X GPU上能达到45帧/秒,Fast YOLO的检测速度可以达到155帧/秒[插图]。(2)背景误判少。以往基于滑动窗口或候选区域提取的目标检测算法,只能看到图像的局部信息,会把图像背景误认为目标。而YOLO在训练和测试时每个格子都可以看到全局信息,因此不容易把图像背景预测为目标。(3)泛化性更好。YOLO能够学习到目标的泛化表示,能够迁移到其他领域。例如,当YOLO在自然图像上做训练,在艺术品上做测试时,其性能远优于DPM、R-CNN等。
  • YOLO目标检测速度很快,但mAP不是很高,主要是因为以下方面:(1)每个格子只能预测两个边界框和一种目标的分类。YOLO将一幅图像均分为49个格子,如果多个物体的中心在同一单元格内,一个单元格内只能预测出一个类别的物体,就会丢掉其他的物体。(2)损失函数的设计过于简单。边界框的坐标和分类表征的内容不同,但YOLO都用其均方误差作为损失函数。(3)YOLO直接预测边界框的坐标位置,模型不易训练。针对YOLO中存在的问题,出现了很多改进版。YOLOv2[插图]借鉴了Faster R-CNN中锚框的思想,同时改进网络结构,形成了Darknet-19网络,此外还用卷积层替换了YOLO中的全连接层,大幅减少了参数量,提高了目标检测的mAP及速度。YOLOv3[插图]采用了多尺度预测,同时借鉴ResNet的思想形成了一个53层的Darknet-53网络,并使用多标签分类器代替softmax等技术,进一步提高了目标检测的mAP。YOLOv4[插图]在特征融合阶段使用了多尺度结构,同时引入了深度学习领域多种技巧提升效果,包括加权的残差连接、跨阶段的部分连接、跨批量标准化、自对抗训练、Mish激活函数等。YOLOv5[插图]通过增加高质量正样本anchor加快收敛,同时整合了大量深度学习领域的技巧,有效提升YOLO系列工作的灵活度与速度,便于使用和部署。此后,YOLO系列又推出了Scaled-YOLOv4[插图]、YOLOR[插图]、YOLOX[插图]、YOLOv6[插图], YOLOv7[插图], YOLOv8[插图]等工作
  • SSD(Single Shot Detector,单次检测器)[插图]基于YOLO直接回归边界框和分类概率的一阶段检测算法,借鉴了Faster R-CNN中的锚框思想,使用了多尺度特征图检测,用一个深度神经网络就可以完成目标检测,在满足检测速度要求的同时,大幅提高了检测的mAP。
  • SSD的主要思想是,在不同大小的特征图上都提取默认框(default box,类似于anchor box)做检测,以找到最合适的默认框的位置和尺寸。在比较大的特征图上检测比较小的目标,在比较小的特征图上检测比较大的目标,如图3.35所示。8×8特征图上的框用来检测比较小的目标——猫,而下一层4×4特征图上的框用来检测比较大的目标——狗。
  • 图3.36是在不同层的特征图上的默认框的示例[插图]。在低层特征图上,默认框可能框到一个物体的局部,交并比很小;在高层特征图上,默认框可能框到一个物体,但框太大了,交并比也很小;在中间层特征图上,框的大小和形状最合适,交并比也是最高的。通过这些优化技术,SSD的目标检测mAP相对于YOLO有一定的提升,也更容易训练。
  • RCNN、Fast R-CNN、Faster R-CNN等。Faster R-CNN之后还有很多优化算法,包括更好的特征网络、更好的RPN、更完善的RoI分类、样本后处理等,形成了现在非常有名的FPN、Mask R-CNN等算法。一阶段算法中,YOLO系列工作影响力较大且不断发展,目前已经发展到YOLOv8;此外一阶段算法中的SSD也很有影响力,SSD之后又有像RSSD[插图]、DSSD[插图]、DSOD[插图]、FSSD[插图]等工作
  • 目前主流的卷积神经网络聚焦于对图像的语义判别,即给定图像,识别图像中的语义信息,如图像中的物体类别、物体位置等,这些都是判别式任务。与判别式任务相对应的是生成式任务,即给定一些语义信息,要求模型生成相应的图像或内容。由于图像中存在大量的细节,生成生动而真实的图像是一件非常具有挑战性的任务。目前最常见的应用于图像生成的神经网络包括生成对抗网络(Generative Adversarial Net, GAN)[插图]和扩散模型(Diffusion Model)[插图]。这两类模型通过学习图像的分布实现图像生成。
  • 生成对抗网络由两个网络组成,即生成网络(也可称为生成器)和判别网络(也可称为判别器)​。生成网络相当于伪装者,会找出观测数据内部的统计规律,尽可能生成能够以假乱真的样本。而判别网络相当于警察,会判断一个样本是来自真实样本集还是生成样本集。生成对抗网络的核心思想是让生成网络和判别网络互相对抗,互相提高,从而使生成网络能生成以假乱真的样本,而判别网络能准确地判断哪些是真样本,哪些是生成的样本。生成器和判别器之间的关系类似于辩论双方之间的关系:通过辩论,提升彼此的能力。[插图]
  • 判别网络采用常规的训练方法。训练数据包括小批量的真实样本x和小批量的噪声z,真实样本的标签为1,生成网络输入噪声后生成的假样本的标签为0。利用这些数据训练出判别网络之后,再将判别网络用到生成网络训练过程中。生成网络训练时,它根据输入训练数据,输出假样本,并将其作为判别网络的输入。判别网络可能会被欺骗,如果被欺骗,则输出1,否则输出0。接着做反向传播更新生成网络的参数,判别网络的参数不变。然后再继续交替训练判别网络和生成网络,经过多次迭代,生成网络就可以生成出非常逼真的假样本。例如,用在图像风格迁移里,就可以生成出一幅以假乱真的梵高风格的画。
  • 判别网络训练的目标是,输入真样本时,网络输出接近于1;输入假样本时,网络输出接近于0。因此,判别网络D的代价函数为[插图]其中,pdata(x)表示真实样本x的分布,pz(z)表示输入噪声z的分布,D(x)表示x来自真实样本集的概率,G(z)表示生成网络输入为z时生成的样本,L(D)是交叉熵损失函数。
  • 生成网络训练的目标是,尽可能生成假样本G(z),使得判别器的输出接近于1。因此,生成网络G的代价函数为[插图]通过最小化L(G)来使判别器尽可能将生成的假样本当成真样本。显然,L(G)和L(D)是紧密相关的。生成对抗网络的总的代价函数可以记为[插图]生成对抗网络的训练,是极小极大博弈(minimax game)问题。这是一个零和博弈,其优化过程包括代价函数V内层的最大化和外层的最小化:[插图]I. Goodfellow等[插图]证明了,当生成器固定时,最优的判别器为[插图]其中,pg(x)表示生成器生成的数据x的分布。当pg(x)=pdata(x)时,生成器是最优的。
  • 在生成对抗网络的极小极大博弈中,当判别器以高置信度成功判断出生成器生成的样本为假样本时,生成器的梯度就会消失。这种情况很容易出现在训练学习的早期,由于生成器很弱,生成的伪样本和真实样本差别很大,判别器能够以高置信度识别出来,因此log(1-D(G(z)))会饱和。为了解决学习早期梯度消失的问题,文献[插图]用下面的代价函数来训练生成器:[插图]通过最小化该代价函数,生成器能够最大化判别器被欺骗的概率。该方法在训练早期能够提供更强的梯度。但上述方法又可能导致模式崩溃[插图]。模式崩溃是指,生成器只生成几种模式的样本,甚至只生成一种特定模式的样本,生成样本缺乏多样性。M. Arjovsky等[插图]证明了,当使用式(3.19)作生成器的代价函数时,生成器优化就变成最小化生成分布与真实分布之间的KL散度(Kullback-Leibler divergence),同时最大化其JS散度(Jensen-Shannon divergence)的问题。二者相互矛盾,会导致梯度不稳定,此外由于KL散度是非对称的,生成假样本的代价远高于模式减小的代价。因此生成器就会收敛到只生成几种模式的样本上,这几种样本都能以高置信度欺骗判别器。与此同时,由于生成网络只会生成几种特定模式的样本,判别网络的能力也
  • 会有局限性。为解决模式崩溃的问题,M. Arjovsky等[插图]提出了Wasserstein GAN(WGAN)。WGAN的思想是用基于Wasserstein距离的代价函数来取代基于KL散度和JS散度的代价函数:[插图]其中,D为1-Lipschitz(利普希茨)连续函数的集合,判别器D(在该论文中称为critic)相对于传统GAN,可以提供更加可靠的梯度信息。WGAN的代价函数近似模拟真实分布与生成分布之间的Wasserstein距离。在最优化判别器基础上优化生成器可以缩小Wassertein距离,即拉近生成分布与真实分布。而且,相对于KL散度和JS散度,Wasserstein距离几乎处处可微。因此,WGAN有效解决了GAN模式崩溃和训练不稳定的问题。
  • GAN的提出宣告着描述分布差异的函数,从人工设计正式步入自拟合阶段。而GAN因其简洁的设计与优雅的风格,得到了业界的广泛关注,目前已经有上千篇相关论文[插图]。最初GAN的设计中,判别器和生成器均使用全连接神经网络。随着卷积神经网络在图像级的分类与回归任务中的迅速发展,出现了基于卷积神经网络的GAN的结构,在降低了计算量的同时,有了更加良好的视觉表现。代表性工作包括DCGAN[插图]、ResGAN[插图]、SRGAN[插图]、StyleGAN[插图]、BigGAN[插图]等。后来,Transformer被广泛应用于视觉领域,并在大数据集上取得了更为突出的效果(相关介绍见3.3.5节)​。为进一步提升GAN的潜能,TransGAN[插图]首次提出了完全基于Transformer实现的图像生成对抗网络,ViTGAN[插图]通过调整注意力的相似性度量,提高了Transformer判别器的稳定性。在数据输入的形式方面,一个标准的单输入GAN网络[插图]包含一个生成器和一个判别器,其中生成器接受一个随机向量作为输入,输出一个生成图像;判别器接受一个图像输入,处理后输出该图像是真实或者生成的二分类概率。但是,单随机向量输入无法显式地控制生成画面的内容。为此,条件GAN的生成器和判别器中增加了类别条件作为额外输入,从而提供更好的多模态数据生成的表示,代表性工作包括CGAN(Conditional GAN,条件GAN)[插图]、InfoGAN[插图]等。至此,GAN已经能生成不错的256×256分辨率的图像。为进一步提高生成图像的分辨率,层级结构的GAN被提出,通过逐层次、分阶段的生成策略,一步步提升图像的分辨率。代表性工作包括ProgressiveGAN[插图]、StyleGAN[插图]和BigGAN[插图]。但是,有的时候我们希望输入的条件不是类别而是一个图像,进而生成一个内容一致但风格不同的图像。此时输入/输出是一一对应的图像对或者相互映射的图像域。据此,循环GAN通过使用两个生成器和两个判别器学习两个域之间数据的互
  • 相映射,从而实现更加灵活多样的图像生成,代表性工作包括CycleGAN[插图]、DiscoGAN[插图]等。GAN在计算机视觉领域的主要应用包括图像的内容生成、超分辨率、风格迁移和域适应等任务。• 内容生成旨在生成包含输入要求内容的图像。BigGAN[插图]在先前工作的基础上,扩大了批量(Batch)的大小,并增加了对输入随机向量的截断,允许对样本多样性和保真度进行精细控制。StyleGAN[插图]通过分别修改每一层级的输入,在不影响其他层级的情况下,来控制该层级所表示的视觉特征,通过实现无监督地分离高级属性控制图像内容生成。DragGAN[插图]在特征图上进行运动监督和精确点跟踪,迭代地更新输入向量来实现同一目标的姿态编辑。• 超分辨率旨在不改变图像语义的基础上,将输入的低分辨率图像重建到高分辨率。SR-GAN[插图]首次将GAN应用到超分辨率任务,通过约束高清图像与重建图像的中间层像素级特征,以及在最高层的图像级语义上开展对抗来训练生成网络。• 风格迁移旨在不同图像风格的转换,以输入图像作为控制条件,输出相应的目标图像,如从手绘图生成自然图像、从游戏图像生成真实图像、相似物体图像转换(如马变成斑马,苹果变成橘子)等。代表性工作有Pix2Pix[插图]、CycleGAN、DiscoGAN和StarGAN[插图]等。• 域适应旨在将一个在有良好标注的数据集上训练的模型,迁移到另一个未标注的数据集上。DANN[插图]将特征提取网络视为生成器,将提取的特征作为域判别器的输入,利用判别器的约束对齐两个域的数据分布,从而实现知识迁移。限于篇幅,下文仅简单介绍DCGAN和条件GAN。DCGANDCGAN(Deep Convolutional Generative Adversarial Network,深度卷积生成对抗网络)用卷积神经网络取代了GAN中的全连接网络。DCGAN的判别网络和生成网络分别使用步长卷积和小数步长卷积取代池化层,来做空间下采样和上采样,以支持高维的图像空间与低维的潜在空间之间的映射。生成网络和判别网络中都使用批归一化,以支持深度神经网络训练,同时可以防止生成网络出现模式崩溃。生成网络使用tanh函数作为输出的激活函数,使用ReLU作为其他层的激活函数,可以加速学习。判别网络使用LeakyReLU作为激活函数,效果很好,尤其是处理高分辨率图像时。图3.38是LSUN卧室数据集上训练得到的生成网络结构。
  • CGAN在生成网络的输入z的基础上加上辅助信息y,如类别标签或其他模态数据;在判别网络的输入x的基础上也加上辅助信息y。CGAN训练的目标函数就变为[插图][插图]其结构如图3.39所示。判别器不仅要分辨出输入样本的真伪,还要判断输入样本与辅助信息是否一致。在CGAN中y是有标签的,相当于是有监督学习。InfoGAN和CGAN类似,都可以完成图像风格迁移的任务。InfoGAN将输入的噪声向量分成两部分:不可压缩噪声z和隐编码(latent code)c。通过最大化隐编码c和生成器输出G(z, c)之间的互信息,可以无监督方式学习出隐编码。隐编码用于表示数据分布中显著的结构语义特征,包括位置、光照等。InfoGAN的目标函数变为:[插图]其中,I(c, G(z, c))为c和G(z, c)的互信息。InfoGAN的结构如图3.40所示,辅助分布网络Q共用判别网络的卷积层,仅额外增加了一个全连接层,增加的开销很小。InfoGAN不仅要求生成图像和真实图像难以区分,而且能够从生成图像中学习出隐编码的条件概率分布Q(c|x)
  • 2020年,UC Berkeley提出了DDPM(Denoising Diffusion Probabilistic Model,去噪扩散概率模型)[插图],从数学上推导了优化上界,从而简化了训练目标,最终实现从随机噪声生成高质量图像。自此扩散模型(diffusion model)广泛应用于图像生成,其生成效果甚至超过了GAN。
  • 扩散模型包括正向过程(也称为扩散过程或加噪过程)和反向过程(也称为逆扩散过程或去噪过程)​,如图3.41所示。正向过程不断向输入图像中添加高斯噪声,反向过程逐步去噪来恢复原始输入图像。以DDPM[插图]为例,接下来简单介绍扩散模型的基本原理。
  • 1,…,xt,…,xT,
  • 基于正向过程,反向过程被定义为由噪声预测网络拟合参数θ的可学习高斯变换。该过程也是一个马尔可夫链,根据当前时间步和图像数据,对噪声进行采样,进而得到前一时间步的图像数据。具体地,可分为T时刻的初始采样:[插图]与其他时刻的迭代采样:[插图]其中,µθ(xt, t)与σt分别为xt-1估计的均值与标准差,θ为噪声预测网络的参数。最终,整个反向过程的联合分布表示为[插图]扩散模型训练时,首先通过正向过程获得每步的加噪后数据,随后据此训练反向过程每一步的去噪,从而学习如何恢复数据分布。扩散模型推理时,采样随机噪声作为输入,仅执行反向过程。
  • 训练扩散模型的关键在于构建一个噪声预测网络,能在反向过程的每一步预测合理的噪声参数,从而使采样数据的分布与训练数据分布保持一致。噪声预测网络的结构如图3.42所示,通常使用U-Net架构,根据当前时间t以及图像特征xt预测要去掉的噪声ϵθ(xt, t)。
  • 从本质上来说,U-Net架构是通过编码器去除原图中的冗余信息并压缩图像,然后通过解码器还原到输入尺寸。DDPM的噪声预测网络主要对原始的U-Net进行了两点改动:第一,DDPM将每一个尺度的卷积计算修改为带有跳跃连接的残差块(residual block),并在每个残差块的末尾增加了自注意力层(self-attention layer),从而增强了关系感知的能力。第二,DDPM使用正弦位置编码,将当前时间t编码为时间向量,作为每个残差块的第二个输入。这样一来,网络能为不同的时间预测不同的噪声,并且由于权重不含时间,没有增加额外的参数量。
  • 在正向过程中,我们不断向数据x0中添加高斯噪声,最终在第T步得到数据xT。那么,我们期望在反向传播的时候,噪声预测网络能够从xT开始,准确预测每一步去掉的噪声,最后还原输入数据x0。因此,对于输入x0,扩散模型的训练目标为最大化边缘分布[插图]。这等价于要求噪声预测网络的参数能满足[插图]pθ(x0)]​,即最大化对数似然(最小化负对数似然)​。
  • 图3.43 原始U-Net架构[插图],其中左侧为编码器,右侧为解码器据此,DDPM通过变分上(下)界给出了优化目标的上界:[插图]通过引入KL散度,该变分上界可被表示为:
  • 为降低KL散度项求解难度,DDPM将反向过程中每一步的方差设为定值,并重写后验概率的均值项,使网络从预测均值转为预测噪声,最终得到简化后的损失函数:[插图]其中U(1, T)表示[1, T]中均匀分布的整数,ϵθ表示噪声预测网络,其输入为xt与t,期望输出逼近ϵ~N(0, I)。
  • 网络训练完成后,在实际推理阶段,将从时间步T开始,按算法3.2逐步向前采样生成图片。具体地,结合公式(3.27),开始从标准正态分布中生成噪声xT~N(0,I);然后对于每个时间步t,将获取的图像xt与t输入到训练好的噪声预测网络模型中预测噪声ϵθ(xt, t);结合公式(3.28),计算时间步t对应的xt-1:[插图]不断重复上述过程,即可得到生成图像x0。
  • • 图像到图像的翻译,需要将输入图像按多模态输入进行修改,代表性工作分别有对输入图像进行超分辨率的SR3[插图]、进行重着色的Palette[插图]、输出语义分割图的DDPM-Segmentation[插图]、对图像指定区域进行编辑的Sdedit[插图]以及风格迁移的InST[插图]等。从本质上而言,任何形式的图像翻译都是将输入图像作为采样的条件。那么如果能针对不同的输入条件,学习少量额外的参数来实现定制化的生成内容,就能充分利用扩散模型的泛化性。据此,LoRA[插图]在冻结原模型参数的基础上,额外引入旁路参数来模拟模型参数的更新,仅用十几个图像就能学习图像的内容和风格。ControlNet采取相似的思路冻结原模型参数,但制作了一份U-Net中编码器的可学习拷贝作为旁路,允许自由度更高的条件控制。
  • DALL·E 2。DALL·E 2是OpenAI提出的以文本作为图像生成条件的分层扩散模型。如图3.44所示,模型主要划分为三个部分:CLIP[插图]、先验模块(prior)和解码器。CLIP是描述图像-文本映射的跨模态大模型[插图],其通过一个文本编码器和一个图像编码器将自然语言描述与图像嵌入到共同的空间,并对齐文本嵌入和图像嵌入,来学习文本描述与图像的对应关系。先验模块是一个扩散模型,用于将输入的文本转换为图像嵌入。先验模块接收由CLIP的文本编码器生成的文本嵌入,生成与CLIP图像编码器数据分布一致的图像嵌入,用于后续的图像生成。解码器是一个带条件的扩散模型,用于将先验模块生成的图像嵌入还原为符合输入文本描述的图像。解码器将图像嵌入作为条件,学习CLIP图像编码器的逆过程,还原出与原始输入具有相同语义,而又不完全一致的图像,从而保证了生成图像的多样性。Stable Diffusion。Stable Diffusion是基于潜在扩散模型[插图]的图像生成模型,由初创公司Stability AI于2022年发布。其主要用于文本生成图像,也可以用于内(外)补绘和图像翻译等任务。Stable Diffusion模型的整体框架如图3.45所示,首先需要训练一个自编码模型,包括能将图像压缩到隐空间的图像嵌入的编码器,以及一个能将图像嵌入恢复到像素空间图像的解码器。自编码模型分析并提取图像中的高维特征,将其压缩到低维空间,因此该过程也被称为感知压缩。其优势在于,低维特征的泛化性更强,要求的计算量也更低,更容易进行跨模态的迁移。随后,在隐空间上进行扩散模型的训练和推理。特别地,Stable Diffusion在传统扩散模型上进行了两点改动。第一,设计了领域专用编码器来预处理多模态条件,从而方便引入各种模态的条件(如文本、参考图像、分割图等)来控制图片生成。第二,向U-Net网络中加入了交叉注意力层,通过注意力映射将控制信息融入到U-Net的中间层,从而使多模态条件参与扩散过程。交叉注意力层的实现如下:
  • ControlNet[插图]的核心思想是,对大型扩散模型的权重进行克隆并划分为“可训练副本”(trainable copy)和“锁定副本”(locked copy)。其中,锁定副本中的权重在后续微调时保持不变,这样继承了模型从大规模数据集中预训练的能力;而可训练副本中的权重在特定任务的数据集上进行微调,以实现对特定任务的拟合。可训练副本网络和锁定副本网络与一种特殊的卷积层——零卷积(zeroconvolution)连接。零卷积是1×1卷积,其权重和偏置都初始化为0,并在训练过程中逐渐增长到优化参数。对于一个新的条件输入,条件输入通过零卷积后,与U-Net的输入向量相加,再输入到可训练副本中,随后可训练副本的输出经过零卷积处理后,加入到锁定副本的相应层中。这样的设计带来两点优势。第一,由于零卷积不会给深度特征添加噪声,模型从训练初期就完整地保留了预训练得到的全部能力,因此ControlNet的训练速度远远超过从头开始训练新的扩散模型。第二,由于显式保护了预训练的权重,与普通的微调方法相比,ControlNet仅需要少量数据就能取得较好的效果,缓解了模型在小数据集上的退化与过拟合问题。总的来说,ControlNet降低了微调扩散模型的成本,使个人用户定制自己的扩散模型成为可能,也为创作者们带来了极大的便利。
  • [1]在NMS处理过程中,可能会有一些比较复杂的场景。例如两个前后站立的人,这两个不同的物体重叠度很高。最初的R-CNN所采用的传统NMS可能会丢掉站在后面的人的候选框,但soft NMS算法[插图]可以把两个人的候选框都保留下来。

 3.2 适合文本/语音处理的循环神经网络

  • 序列数据中相邻数据之间有相关性,这就要求神经网络有存储信息的能力,才能有效处理序列数据。而本章前面介绍过的做图像识别的卷积神经网络不需要有存储信息的能力,它只需要固定的权重参数,不需要根据已处理的前一张图片的情况来改变内部状态。为此,研究者提出了循环神经网络(Recurrent NeuralNetwork, RNN)[插图]。它可以有效保存序列数据的历史信息,因此比较适合处理序列数据。本节将对循环神经网络进行介绍,并在此基础上介绍循环神经网络的两种改进算法:长短期记忆网络(Long Short-Term Memory, LSTM)[插图]和门限循环单元(Gated Recurrent Unit, GRU)[插图]。
  • 其中,b和c为偏置,U、V、W分别为输入-隐层、隐层-输出、隐层-隐层连接的 权重矩阵。在不同时刻,权重矩阵U、V、W的值是相同的。f(x)为非线性激活函数,通常是tanh或ReLU函数,下文以f(x)=tanh(x)为例。这个例子比较简单,只有一个隐层,也可以有多个隐层。
  • 图3.47 RNN结构(左图)​,按时间展开的RNN结构(右图)
  • 环神经网络有记忆(memory),隐层h(t)中捕捉了时刻t之前的所有信息。理论上h(2)中可能蕴藏了部分x(2)的信息和x(1)的信息,而h(t)中可能蕴藏了x(t), x(t-1),…,x(1)中的信息。因此,理论上h(t)可以蕴藏t时刻之前的所有信息,其记忆的内容可以无限长,但实际训练时由于梯度爆炸等原因导致能够获得的记忆是非常有限的。
  • 神经图灵机[插图]是谷歌DeepMind做过的一个很
  • 意思的工作,它的改进版本[插图]发表在Nature上。
  • 从某种意义说,图灵机中的读写头对应计算机和程序(即硬件和软件)​,纸带对应内存。神经图灵机用神经网络代替图灵机中的读写头。图灵机是通用的机器,如果读写头设计好了,可以完成任意的功能,比如排序、串拷贝等。理论上如果我们把一个神经网络训练得能完成读写头的功能,就可以让神经网络完成任意的计算机功能,包括不限于排序和串拷贝。这是通往通用人工智能的非常重要的工作。读写头要考虑历史信息,比如往左或往右与纸带上其他的信息有关。因此,可以用RNN来做读写头。但是DeepMind的研究者发现,用很长时间训练出来的做串拷贝的神经网络读写头,如果处理拷贝长度在20个以内的字符串,基本上没有问题,但是对于更长的字符串(比如100个或200个)​,神经网络读写头就做不对了。这就是因为RNN(及其改进版本LSTM)的记忆是有限的。
  • 图3.48d和图3.48e是多对多的RNN结构,输出和输入都是序列,例如机器翻译(machinetranslation)将英文翻译成中文或者中文翻译成英文,视频描述(videocaptioning)对一个很长的连续序列如足球比赛写出一个新闻报道,如“第5分钟张三传给李四、李四传给王五、王五射门”等。多对多的结构,还支持同步序列转化,例如视频分类(video classification)对视频的每一帧标注信息。RNN的应用很灵活,只需要提供训练样本,神经网络可以根据需要训练出一对多、多对一或多对多的结构。
  • RNN的训练一般采用一种变种的反向传播方法,学名为基于时间的反向传播(Back-Propagation Through Time, BPTT)[插图]。如图3.49所示,它的核心思想是将RNN按时间展开后做反向传播。BPTT完成正向传播后一般用交叉熵作为损失函数,然后做梯度下降。
  • 由于RNN很容易出现梯度消失或梯度爆炸,RNN只能学到很短期的依赖关系,比如邻近几个时刻内的依赖。RNN中显著的梯度消失或梯度爆炸现象主要是循环结构引起的。一般的神经网络有很多层,每一层的权重矩阵不同,但RNN中每一层的权重矩阵都是相同的。这就导致梯度的绝对值急剧单调增或者单调减。下面是一个由于梯度消失导致循环神经网络无法处理长期依赖关系的示例。考虑一个语言模型,试图根据之前的单词预测下一个单词。如果要预测“Thebirds are flying in the_____”中最后一个单词,不需要很多的上下文就可以知道下一个单词是“sky”​。相关信息(​“birds”和“flying”​)与预测位置的间隔比较小。这种情况下RNN处理起来问题不大,可以学会使用之前的信息预测出“sky”​。但如果要预测“I grew up in Italy… I speak fluent_____”中最后一个 单词,就需要用到包含“Italy”的前文。相关信息(“Italy”)与预测位置的间隔可能很大。随着这种间隔的拉长,RNN就会由于梯度消失,找不到前后的依赖关系,从而做出错误判断。为了解决梯度爆炸,Pascanu等提出了梯度截断的方法[插图],当梯度[插图]大于预定义的阈值threshold时进行截断,得到[插图]。为了解决梯度消失,可以用现在流行的长短期记忆模型(Long Short-Term Memory, LSTM)或门限循环单元(Gated Recurrent Unit, GRU)。
  • LSTM的核心思想是,很长时刻之前的信息可能很重要,需要保留,但神经网络的记忆是有限的(就像杯子倒满了水就会溢出)​,要想记住过去的重要的信息,就要丢掉新学到的不重要的信息。因此,LSTM会去判定一个新信息是否重要。如果重要,就应当进入长期记忆,持久地保留;否则,属于短期记忆,很快就要丢掉。为了达到这个目的,LSTM循环网络设计了LSTM单元来替代RNN中的隐层单元
  • 相对于RNN,每个LSTM单元的输入和输出不变,但增加了状态和多个门限单元来控制信息的传输。其中,最重要的单元状态c(t),由前一状态和当前输入组合而成,并通过遗忘门单元和输入门单元分别控制前一状态和当前输入的信息传输。极端情况下,如果所有的遗忘门为0,则忽略前一状态;如果所有的输入门为0,则忽略当前输入计算出的状态。
  • 图3.50 按时间展开的LSTM循环网络单元
  • GRU(Gated Recurrent Unit,门限循环单元)[插图]是2014年提出来的,在某种意义上它也是LSTM的变体。GRU在LSTM的基础上,把单元状态与隐藏状态合并,把输入门与遗忘门合并成为更新门(update gate),去掉输出门,增加重置门(reset gate),如图3.53所示。
  • 现在有很多LSTM变体,其核心思想是增加各种各样的门来选择是否保留过去的知识,是否把新的信息更新到已有知识中。如果将当前的输入更新到已有知识中,必然会冲淡已有的知识;如果希望记住过去的知识,比如说1000个单词之前的信息,就要将其一直保持在隐藏状态中。
  • GRU是LSTM的一种变体。LSTM有隐藏状态和单元状态,还有遗忘门、输出门和输入门,表征能力更强,但参数更多,训练起来难一点。而GRU更简单一些,只有隐藏状态、更新门和重置门,参数更少,训练速度更快。LSTM和GRU哪种模型更好,没有定论,实际中可以根据应用情况来选择。除了GRU外,LSTM还有很多其他的变体(可以参见文献[插图])​,都是在LSTM上面增加一些门或者减少一些门,其核心思想也都还是用门来选择是否保留过去的知识,是否把新的信息更新到知识中。

 3.3 大模型

  • 2014年谷歌的研究人员在循环神经网络的基础上提出了Seq2Seq(Sequence-to-Sequence,序列到序列)结构[插图]。当输入序列很长时,Seq2Seq存在一定的局限性,研究人员进而设计了注意力(attention)机制[插图],通过聚焦到输入序列中相关性更强、更重要的信息解决长序列输入的问题。当时,Seq2Seq结构和注意力机制被广泛应用于循环神经网络中。但由于循环神经网络是一个串行结构,每个时间步的输入依赖于上一时间步的输出,所以在训练时的并行效率较低。为了能够让序列模型实现高度并行训练,2017年谷歌开创性地提出了Transformer网络[插图]。该网络摒弃了循环结构,利用注意力机制实现Seq2Seq结构,解决了如何高效地处理长序列输入/输出的问题,同时可以支持高度并行的训练。由于可以高效地利用大规模序列数据,Transformer网络开启了大语言模型(Large Language Model, LLM)时代。在Transformer的基础上,研究人员提出了BERT[插图]和GPT系列工作[插图][插图][插图],通过不断扩大训练语料的规模和模型规模,有效提高了大语言模型的效果。其中2022年提出的ChatGPT[插图]不仅可以流利回答用户提出的各种问题,还可以实现写邮件、写诗、写代码、做数学题等功能。
  • Transformer也被应用于图像处理领域,并与文本处理结合,形成多模态大模型(Large Multimodal Model, LMM)。
  • 一般的Seq2Seq模型会在编码阶段将输入编码成固定长度的语义编码。这样会带来两个问题:(1)模型性能受限于语义编码长度,即语义编码的长度通常是固定的,难以存储较长的输入序列的所有信息,进而影响模型的性能;(2)计算每个输入元素的权重相同,这导致Seq2Seq模型无法区分输入序列中不同元素的重要程度。为了解决这两个问题,研究人员提出了注意力机制。
  • 意力机制的输入通常包括查询(query)、键(key)和值(value)三个部分。人眼的注意力会根据自身的需求决定人眼聚焦于哪个视觉区域。例如,当饥饿时,人眼会将注意力聚焦于食物上。注意力机制中的查询就代表了需求。而键和值是成对出现的,键是值的代表。注意力机制的目的是寻找与查询相关的值,为了加快寻找速度,注意力机制会计算查询和键的相关度,那些相关度高的键对应的值被认为是重要的信息而被提取出来加以利用。
  • 注意力机制的计算过程大概分为两个步骤:(1)计算查询与所有键的相似度,利用相似度计算每个键值对的注意力权重;(2)利用注意力权重计算所有值的注意力汇聚结果,例如使用加权求和的方式聚合所有的值,得到注意力机制的输出结果。
  • 原始的Seq2Seq模型如图3.54所示。加入注意力机制后,在解码器第t'时间步中,以解码器上一个时间步的隐藏状态[插图]作为当前时间步的查询,以编码器所有时间步的隐藏状态h作为键值对,注意这里为了简化运算,第t步对应的键值对的键和值都为h(t),共有T对键值对。第一步,计算注意力权重α,将查询[插图]与所有键h分别计算内积作为查询与每个键的相似度,所有相似度利用softmax函数归一化后作为注意力权重:[插图]第二步,计算注意力汇聚,利用每个键的注意力权重与相应的值计算加权求和,获得注意力汇聚的结果[插图]:[插图]注意力汇聚的结果[插图]将作为上下文信息,也作为解码器计算第t'时间步的输入,此时解码器的输入包含上一时间步的输出[插图]、隐藏状态[插图]和注意力汇聚的结果[插图],解码器的计算公式(3.55)变为:
  • 注意力机制的计算量主要在于计算查询和键的相似度,而查询和不同的键计算相似度的过程是可以并行的。相比之下,循环神经网络的计算过程是串行的,每个时间步的计算都依赖于上一时间步计算的隐藏状态。为了提高训练过程的并行度,Transformer在实现Seq2Seq结构时摒弃了循环神经网络,而使用了大量可以并行的注意力机制,切实有效地提高了并行度。在介绍Transformer的具体结构前,我们先介绍Transformer中的注意力机制。
  • Transformer中核心的注意力机制被称为缩放点积注意力,如图3.56所示。缩放点积注意力的输入包括查询、键、值三个部分,其中查询和键都是维度为dk的向量,值是维度为dv的向量。第一步,计算查询和所有键的内积,利用内积计算查询和键的相似度,然后除以[插图],经过softmax操作后获得注意力权重;第二步,注意力权重与所有值加权求和,获得注意力汇聚的结果。在实际计算时,可以将多个查询合并为一个查询矩
  • 阵[插图]同时计算,多组键值对合并为键矩阵[插图]和值矩阵[插图],此时向量内积将转变成矩阵相乘操作,缩放点积注意力的计算可以表示为[插图]注意在计算查询和键的内积后,会有一个除以[插图]的缩放操作,这是因为Q和K都是输入的特征,它们的数值都是接近均值为0、方差为1的正态分布。Q和K内积后的方差为dk,当dk较大时,softmax的梯度会比较小,难以优化。因此通过除以[插图],使内积的方差变为1,比较利于获得较大的softmax梯度进行权重优化
  • 自注意力的查询和键值对的数据来源是相同的,自注意力机制捕捉的是输入序列内部的相关关系。当查询、键和值来源于同一组隐藏状态,此时的注意力机制被称为自注意(self-attention),也被称为内部注意力(intra-attention)。给定一组维度为dm的输入序列x1, x2,…,xn。为简化计算过程,将这组输入序列合并为矩阵[插图]。在计算自注意力时,首先将输入矩阵I经过不同的投影变换[插图]得到查询、键和值的矩阵,然后再计算缩放点积注意力。自注意力的计算公式为:
  • 如图3.57所示,对于输入查询[插图]、键K∈[插图]和值[插图],多头注意力机制使用h组不同的线性变换矩阵[插图][插图],i=1,…,h,得到h组不同的查询、键和值的矩阵,分别计算缩放点积注意力后,将结果拼接在一起,并通过一个矩阵[插图]计算线性映射进行融合。多头注意力的计算公式为:[插图]其中fconcat(·)函数将多个缩放点积注意力的结果拼接在一起。多头注意力机制的 设计思路可以类比卷积中的多通道卷积,多通道卷积通过不同的卷积核捕捉不同方面的特征,多头注意力机制则通过多个头的不同线性变换捕捉序列不同方面的相关关系。此外,当多头注意力机制的查询、键和值来源于同一组隐藏状态组成的输入时,即为多头自注意力机制
  • Transformer完全利用注意力机制和全连接层实现编码器和解码器,没有使用任何卷积层或循环层。编码器的输入是输入序列中词元(token)[插图]的嵌入(embedding)表示,在训练阶段即为训练数据集的输入序列中词元的嵌入表示。编码器由6个相同的层(layer)组成(即图3.58中N=6)​,而每个层又由两个子层(sub-layer)组成,分别是多头自注意力机制(Multi-Head Self-Attention Mechanism, MHA)和全连接前馈网络。编码器中的多头自注意力机制可以捕捉输入序列内部的相关关系。全连接前馈网络由两层全连接层实现,两个全连接层中间使用了ReLU激活函数。多头自注意力和全连接前馈网络都使用了残差连接(residual connection)和层归一化(LayerNormalization, LN)[插图]。残差连接借鉴了残差网络的设计思路,由于Transformer整体的层数较多,使用残差连接后可以更好地优化Transformer。层归一化可以使输入特征分布更加稳定,加快Transformer的收敛速度。与卷积神经网络中常用的批归一化不同,层归一化针对每个样本的不同特征做归一化操作,与批量大小和序列长度无关,因此非常适合应用于输入长度不定的序列模型中。
  • 解码器的输入是当前时间步前面的输出序列的词元嵌入表示和编码器计算得到的语义编码。解码器也由6个相同的层组成,每个层由三个子层组成,包括一个掩码(mask)多头自注意力机制,一个交叉多头注意力机制和一个全连接前馈网络。相比编码器的层,解码器的层中增加了一个交叉多头注意力机制。在解码器的掩码多头自注意力机制中,查询、键和值都来源于解码器上一层的输出,用于捕捉输出序列。解码器使用了自回归(auto-regressive)结构,即利用前面的序列预测下一个时间步的输出,因此解码器只使用当前时间步前的序列进行计算。为了确保解码器遵循自回归结构,掩码多头自注意力机制通过掩码操作确保注意力的计算仅利用输出序列中当前时间步前面位置的特征。解码器中的交叉多头注意力机制的查询来源于前一子层掩码多头自注意力机制的输出,键和值来源于编码器输出的语义编码,因此交叉多头注意力机制可以捕捉输入序列和输出序列之间的相关关系。与编码器类似,解码器中的全连接前馈网络同样由两层全连接层实现,并且解码器中的三个子层也都使用了残差连接和层归一化。在推理阶段,解码器的输入使用了上一时间步的解码器输出,因此需要对逐个位置进行解码,无法并行。但在训练阶段,为了提高并行度,解码器的输入使用了训练数据集中真实的输出序列(整体向右偏移一位)​,不依赖于上一时间步的输出,因此可以并行,极大地提高了Transformer的训练效率。
  • 在处理词元序列时,循环神经网络通过逐个处理序列中的词元引入不同词元的位
  • 置信息。注意力机制为了实现并行计算,放弃了顺序操作,丢失了词元在序列中的位置信息。为了解决这个问题,Transformer在输入嵌入和输出嵌入中添加了位置编码(positional en-coding),以引入当前词元嵌入在序列中的相对或绝对位置信息。Transformer使用了波长从2π到10000×2π的正弦和余弦函数作为位置编码,正弦和余弦函数的输入为当前词元嵌入结果在序列中的位置i以及嵌入结果中每个元素所在的维度。当元素所在的维度为偶数2j时,使用正弦函数计算位置编码,当元素所在的维度为奇数2j+1时,使用余弦函数计算位置编码,计算公式为:
  • 在Transformer提出一年后,2018年6月,OpenAI的研究人员提出了GPT(Generative PretrainedTransformer,生成式预训练Transformer)​,是一种基于Transformer的解码器设计预训练-微调的统一框架,在大规模语料库上预训练GPT模型后,通过微调将GPT模型快速应用于不同的下游任务。GPT的参数量约1.17亿,使用约5 GB语料进行预训练,在多种自然语言处理的下游任务中取得了当时最好的结果。为了对标GPT, 2018年10月,谷歌提出了BERT(Bidirectional EncoderRepresentation from Transformers,基于Transformer的双向编码器表示)​,同样实现预训练-微调架构。与GPT不同的是,BERT使用了Transformer的编码器结构,通过对序列上下文的双向编码提升预训练模型的能力。BERT在预训练中使用了3.3 B词元的语料,其中参数量与GPT相当的BERT-base模型的效果优于GPT,参数量为GPT两倍的BERT-large模型在多种下游任务上取得了更好的效
  • 图3.59 大模型的发展历程
  • ChatGPT在很多专业领域表现出超过普通人的能力,例如可以编写多种不同语言的代码,通过了美国司法考试和医疗执照考试。由于其强大的能力,ChatGPT引起了各行各业的广泛关注,推出仅仅5天时间用户就超过百万,目前很多公司已经使用ChatGPT代替员工完成一些任务。在ChatGPT的基础上,OpenAI于2023年3月发布了GPT-4。不同于ChatGPT仅能进行文本操作,GPT-4是多模态模型,可以同时接收图像输入和文本输入,在无须微调的情况下,在多种多模态任务上的效果超过了针对性训练后的模型。微软的研究人员表示,GPT-4或许是强人工智能的雏形。
  • 将会根据任务特性对输入进行特定的转换,同时网络结构仅需微小的更改,如添加一个全连接层。对所有参数进行微调后(额外的输出层需要从头训练)​,即可将GPT应用于不同的下游任务。因此,GPT可以通过这种预训练-微调的方式,快速灵活地应用于各种自然语言处理任务,无须大量与特定任务相关的结构更改,并在多种自然语言处理任务上获得了当时最优的结果。
  • GPT利用Transformer解码器的自回归结构实现生成式语言模型。解码器中的多头自注意力机制中包含掩码操作,可以确保注意力的计算仅利用解码器的输入序列中当前时间步前面位置的特征,从而实现自回归结构。由于没有编码器,GPT去掉了原始Transformer解码器中的交叉多头注意力,仅保留了带掩码的多头自注意力和前馈神经网络,如图3.60所示。GPT中包含12个Transformer层,每层中的多头注意力机制包含12个头,隐藏状态的维度为768,总参数量约1.17亿。GPT的训练包括预训练和微调两个阶段,其中在预训练中使用了约5GB的图书馆语料。在预训练阶段,GPT使用了标准的语言模型目标函数。语言模型是指给定序列前面的i-1个词元,去预测第i个词元。给定无标注语料中的文本序列U={u1,…,un},语言模型的目标函数是极大化似然:
  • 常见的四种自然语言处理下游任务时的改变方式。(1)分类任务:分类任务需要预测输入序列的类别。在微调时,对输入序列添加开始(Start)词元和抽取(Extract)词元后,输入到GPT的Transformer解码器中,添加一个线性变换层(即全连接层)获取最终的分类预测结果。[插图]图3.61 GPT四种不同的下游任务[插图](2)蕴含任务:蕴含任务输入一段前提文本和一段假设文本,判断二者是否为蕴含关系。在微调时,将前提文本和假设文本用分隔符(Delim)进行连接,再添加开始词元和抽取词元,然后输入到GPT的Transformer解码器中,同样添加一个全连接层获取最终的蕴含关系预测结果。(3)相似任务:相似任务需要判断两段输入文本是否相似。两段文本出现的先后顺序应当不影响相似关系的判断,但在文本连接后会出现顺序。因此微调时,将两段文本分别使用两种顺序连接,再添加开始词元和抽取词元,然后都输入GPT的Transformer解码器中,将两个输出结果相加后输入到添加的全连接层中,获得相似关系的预测结果。(4)多选任务:多选任务中,给定一个问题,需要从多个答案中选取最佳答案。在微调时,对于n个可能的答案,分别构造n个文本序列,每个序列的前半部分都是问题文本,后半部分分别是每个答案的文本,中间用分隔符连接,并添加开始词元和抽取词元。然后将这n个文本序列分别输入GPT的Transformer解码器中,将得到的n个输出结果输入到添加的全连接层中。最后将所有n个线性层的输出结果使用一个softmax计算置信度最大的作为最终答案。
  • BERT的输入嵌入(embedding)表示为词元嵌入(token embedding)、分段嵌入(segment embedding)和位置嵌入(position embedding)的加和,如图3.62所示。其中,词元嵌入是添加特殊标记后的BERT输入序列中每个词的词元嵌入;分段嵌入用于区分输入的前后句子;位置嵌入含义与Transformer的位置编码相同,但Transformer中使用不同频率的三角函数直接计算位置编码,而BERT输入嵌入中的位置嵌入是通过训练学习得到的。
  • 掩码语言模型(masked language model)可以训练双向的表示。一般的条件语言模型只能从左到右或者从右到左进行训练,而BERT使用了双向的结构可以同时利用左边和右边的序列进行预测。掩码语言模型是一种类似“完形填空”的学习方式,随机遮盖序列中15%的词,利用其左边和右边的序列预测被遮盖的词
  • 预训练完成后,BERT通过微调应用于不同的下游任务。在微调阶段,BERT仅需要根据下游任务的特性添加一个特定的输出层,如图3.63所示。微调时,只需将特定任务数据的输入/输出送入BERT,端到端地更新所有参数,其中额外添加的输出层参数从头训练,其他参数均在预训练获得的参数基础上微调。与完全从头训练整个模型相比,微调的代价较小,可以快速灵活地获得不同下游任务的模型。
  • 为了实现通用任务的语言模型,需要告诉模型当前的任务是什么,因此OpenAI将任务相关的信息也作为语言模型的输入,即p(output|input, task)。同时,也可以用语言来灵活地表示任务,从而将任务描述、输入文本、输出文本全部转变为无监督训练的样本序列。例如,翻译任务的训练样本可以转变成(翻译为英文,智能计算系统,AI Computing System)​,阅读理解任务的训练样本可以转变成(回答问题,文档,问题,答案)​。利用这种方式,GPT-2实现了多任务的无监督学习。
  • 为了使GPT-3也具有类似的灵活性和通用性,GPT-3通过上下文学习实现了仅根据自然语言指令或任务的一些演示完成不同的任务。具体而言,GPT-3提供了三种上下文学习的设置,如图3.64左图所示。
  • 图3.64 英语翻译为法语的4种方法。左侧为GPT-3中使用的零样本(zero-shot)、单样本(one-shot)和小样本(few-shot)方法;右侧为传统微调方法[插图](1)小样本学习(few-shot learning):在输入文本中,向模型提供描述任务的自然语言提示,以及一些任务相关的示例演示。示例演示的数量设置在10~100之间的范围内。小样本学习方式可以大大减少对特定任务数据的需求,并减少过拟合的可能性。(2)单样本学习(one-shot learning):在输入文本中,除了向模型提供描述任务的自然语言提示,仅提供一个任务相关的示例演示。区分单样本、小样本和零样本的原因是单样本学习与人类传达任务的方式最接近。在实际生活中,当要求人类完成某个任务时,通常会给人类提供一个该任务的示例演示,如果没有给出示例,有时很难传达该任务的内容或格式。(3)零样本学习(zero-shot learning):在输入文本中,只向模型提供描述任务的自然语言提示,不提供任务相关的示例演示。这种方式使用最为便捷,但也最具挑战性。在某些情况下,如果没有给出示例,即使是人类也很难理解任务的具体要求,尤其当任务描述模棱两可时。但在大多数情况下,零样本是最接近人类执行任务的方式,例如在翻译等明确的任务中,人类仅凭文本指令就知道应该做什么。上下文学习与微调的最大区别在于,微调需要利用大量的下游任务数据对模型进行训练,更新预训练模型的参数,如图3.64右图所示;而上下文学习不需要大量下游任务数据,也不需要进行梯度更新。
  • 为了降低复杂度,GPT-3中使用了一种稀疏Transformer(SparseTransformer)[插图]的方法,在注意力机制中引入了稀疏性。稀疏Transformer使用了新的行列注意力核,通过跨步和固定注意力连接矩阵来进一步优化计算,如图3.65所示。跨步注意力是指模型可以在注意力计算过程中跨步跳过一些位置,固定注意力通过限制哪些位置可以相互交互来降低注意力计算的复杂度。
  • 了训练Codex, OpenAI从5000万个公开的GitHub库里面收集了179GB的代码文件,然后进行过滤,去除掉那些自动生成的或者过大的文件,得到了159GB的文件作为训练数据集。在该数据集上,使用一个120亿参数规模的GPT-3网络进行训练,获得Codex的模型参数。由于代码数据中包含许多代码片段和相应的注释,Codex通过语言模型框架获得了根据注释预测相应代码的能力,即可以实现根据自然语言提示编写代码。最后,通过单元测试验证Codex生成代码的正确性。实验表明,当生成一个答案时,Codex可以解决28.8%的问题。如果产生10
  • 个答案,要求其中至少有一个答案正确,此时Codex可以解决77.5%的问题。此后,根据自然语言提示编写代码的能力成为评测大语言模型的性能指标之一。
  • 送到第一步获得的有监督微调模型SFT中,产生多个模型输出的回答。标注员将这些模型输出的不同回答按照好坏程度进行排序。将大量经过人工排序的数据整理为一个数据集,用于训练一个奖励模型。奖励模型的输入是问题和回答的文本,优化目标是让奖励模型判断的分数排序后要接近人工排序的顺序。这样就获得了一个评价回答是否符合人类习惯的奖励模型。步骤3,使用奖励模型,通过强化学习训练模型。利用强化学习中流行的近端策略优化(Proximal Policy Optimization, PPO)算法[插图]对第一步获得的有监督微调模型进行训练。用第二步生成的奖励模型对有监督微调模型(即图3.66步骤3中的“策略”​)的输出结果进行评估,通过强化学习训练后,使模型的输出结果的奖励尽量高。由于奖励模型是符合人类习惯的,通过这种强化学习的方式训练获得的InstructGPT会产生更加贴合人类偏好的回答。其中步骤2和步骤3可以重复迭代多次。在步骤2中,利用当前最优模型产生更多的不同回答数据,标注员排序后,这些回答数据用于训练新的奖励模型,然后步骤3利用新的奖励模型通过强化学习训练获得更优的模型。
  • Transformer的结构也受到了广泛的认可和推广。研究人员开始尝试把Transformer结构应用于图像处理领域,并在此基础上设计图像处理和多模态大模型。2020年,研究人员提出了ViT和DETR,分别将Transformer首次成功应用于图像分类和目标检测。ViT[插图]通过将2D图像
  • 转换成图像块序列,简单快捷地实现利用Transformer学习图像的特征表示,为以后的图像处理和多模态大模型设计奠定了基础。DETR[插图]则基于Transformer实现端到端的目标检测,去掉了anchor机制、非极大值抑制等步骤,大大简化了目标检测的流程。2021年提出的Swin Transformer[插图],在ViT的基础上添加层次化结构,更好地学习图像不同尺度的特征表示。Swin Transformer作为骨干网络在图像分类、目标检测、图像分割等不同图像处理任务上都取得了当时最好的结果。在此基础上,研究人员将应用Transformer进行图像处理和文本处理的思路结合,形成多模态大模型,例如CLIP[插图]通过学习文本和图像之间的关联极大地提高了模型的视觉表征能力,BLIP[插图]和BLIP-2[插图]首先提取图像感知特征,然后使用自然语言处理大模型处理感知内容的语义信息,为图像的复杂语义推理开拓了新的路线。
  • 给定输入图像为x∈RH×W×C,其中H、W、C分别表示图像x的高、宽和通道数。首先将图像x切分为N个大小为P×P的图像块[插图],i=1,…,N,N=HW/P2。这样图像x就转变为一个长度为N的序列,序列中每个元素[插图]是维度为P×P×C的图像块。假设原始Transformer编码器可以接收的输入序列中每个词元嵌入的原始维度为D,这时需要将每个图像块通过一个线性映射E变换为维度为D的向量,称为图像块嵌入(patch embedding)。与BERT中在输入序列开头添加分类标记[CLS]的做法类似,ViT在图像块序列的开头也添加分类标记[class]的词元嵌入,记为xclass。xclass与图像块嵌入的序列拼接后作为ViT的输入序列。此外,图像块序列是有顺序的,可以通过图像块的顺序将原始图像的部分空间信息包含在位置嵌入中。ViT中对每个图像块嵌入加上位置嵌入(positional embedding)Epos后,再输入到ViT。ViT的网络结构遵循了原始Transformer的编码器结构,如图3.67所示。ViT中包含L层,每层包含一个多头自注意力机制fMSA和一个多层感知机fMLP(即3.3.3节中
  • 介绍的全连接前馈神经网络)​,同时还使用了残差连接和层归一化fLN。ViT中的层归一化位置与3.3.3节中介绍的原始Transformer中层归一化的位置不同。原始Transformer中层归一化在子层后,但Transformer在后续的发展使用中,将层归一化放在子层之前的残差连接内,并在最后一层添加一个额外的层归一化来处理最终输出的大小。
  • 在DETR出现之前,目标检测算法主要包括以YOLO系列为代表的一阶段算法和以Faster R-CNN为代表的两阶段算法。这些方法使用区域生成网络、锚框生成和非极大值抑制等处理方法,在提高检测mAP的同时也增大了模型的复杂度,使得模型不易调参和部署。为解决上述问题,DETR提出了一种端到端的目标检测框架,利用Transformer结构的全局建模能力和新的目标函数,直接生成一组唯一的预测框,简化了目标检测的流程。
  • DETR的网络结构如图3.68所示,主要由三部分组成:CNN主干网络、Transformer编解码器和预测头。CNN主干网络负责提取图像特征,经过CNN得到的特征图再进行降维和扁平化以适配后续Transformer编码器输入的通道数。随后Transformer编解码器解析CNN提取的图像特征序列,输出一系列的查询特征。最后,查询特征输入到预测头,输出预测的目标类别与位置。
  • 通过预测头中的前馈网络将它们独立解码为框坐标和类标签,从而产生P个最终预测。P是一个超参数,它的值远大于图片中目标的数量。预测头(prediction head)是一个三层全连接前馈网络,激活函数为ReLU。每个目标查询通过预测头得到目标的类别和检测框,其中检测框由目标的中心点坐标以及宽和高组成。DETR总共预测P个检测框,这P个检测框利用匈牙利算法与真实边界框进行二分图匹配,匹配后能够对每个待检测物体保留一个唯一的检测框。然后计算唯一的检测框与真实边界框的分类和检测框损失,通过回传损失的梯度更新模型的参数。DETR首次将Transformer模型应用在目标检测中,利用Transformer中注意力机制能够有效建模图像中的长程关系(long range dependency)。DETR将目标检测问题转化为集合预测的问题,直接通过二分图匹配对每个待检测物体保留一个唯一的检测框,避免了传统目标检测方法中复杂的锚框生成和非极大值抑制等步骤。这种端到端的设计使得DETR模型在速度和准确性方面取得了显著的改进,开辟了目标检测的新范式。此外,DETR具有很好的可扩展性,例如仅在解码器输出后加入新的检测头就可以扩展到全景分割等任务。
  • 但DETR相比传统目标检测算法需要更长的训练时间达到收敛,同时对于小目标的检测性能较差。针对这些问题,后续涌现了大量基于DETR的工作,如Deformable DETR[插图]、Efficient DETR[插图]、PnP DETR[插图]、Sparse DETR[插图]等,这些方法优化了模型的训练时间,也进一步提升了对不同尺度规模目标检测的mAP。
  • 图像中物体的大小 是变化的,这种图像块固定大小的设计无法有效捕捉图像的多尺度信息,而多尺度信息对于目标检测、图像分割等下游任务是非常重要的。此外,当图像的分辨率增大时,输入序列的长度会快速增长,将整个图像的图像块序列输入Transformer会带来较大的计算复杂度。为了解决这两个问题,SwinTransformer设计了分层Transformer结构,如图3.70a所示。Swin Transformer将图像划分为不同的窗口(图3.70a中的红色框)​,每个窗口中包含相同数量的图像块(图3.70a中的灰色框)​,在窗口内的序列上计算自注意力。由于窗口中包含的图像块个数固定,自注意力的计算复杂度也是固定的,整张图像的计算复杂度会与图像大小呈线性关系。这种在窗口内计算自注意力的方式能够缓解图像分辨率变大后图像块序列变长,Transformer计算复杂度变大的问题。此外,SwinTransformer还设计了图像块合并操作,高层的Transformer层将相邻的较小的图像块合并为较大的图像快,使不同层使用不同大小的图像块嵌入进行计算,从而学习图像不同尺度的特征
  • 与ViT中Transformer层的结构十分类似,SwinTransformer块中包含一个基于移动窗口的多头自注意力子层和一个MLP子层,并且每个子层前都添加了层归一化操作,并且使用了残差连接。两个连续的SwinTransformer块的计算可以表示为:
  • 需要注意的是,采用移动窗口划分,会改变窗口的数量和大小,如图3.72所示,原来的4个窗口变成了9个窗口,且每个窗口的大小可能不同,这时无法将这些窗口作为一个批量(batch)进行计算。针对这个问题,Swin Transformer设计了一种循环移位的窗口划分方式,如图3.73所示。通过将图像边界划分出的大小不同的窗口进行循环移位后拼接,可以将特征图仍然划分为4个窗口大小,进而合并成批量进行计算。这种循环移位的方式破坏了原特征图中相邻像素点之间的相关性,将原来不在同一窗口的特征点划分在了同一个窗口中。为了防止后续的自注意力计算出现问题,Swin Transformer使用了掩码多头自注意力机制,通过添加掩码,去掉原来不在同一窗口的特征点的自注意力计算结果。在计算完多头注意力之后,再将计算结果反向循环移位还原回去,保持划分窗口的原始位置不变,从而实现划分窗口后的自注意力快速高效的计算。
  • 根据Transformer层的性质,Swin Transformer块的输入/输出尺寸是相同的。为了在Swin Transformer中引入层次化结构,Swin Transformer在第二、三、四阶段的开始都使用了块合并(patch merging)模块,如图3.71a所示。块合并的设计思路与卷积神经网络中的池化层非常类似,通过合并相邻的图像块,将输入图像块的数量缩减为原来的四分之一,每个图像块嵌入的维度加倍,非常类似于在卷
  • 积神经网络中经过池化层后特征图的长宽减半、通道数加倍的情况。例如,第二个阶段中,输入共[插图]个图像块嵌入,每个输入图像块嵌入的维度是C。块合并模块将相邻2×2的图像块嵌入拼接,拼接后的嵌入维度为4C。然后经过线性变换,将嵌入维度降为2C,此时图像块嵌入的数量减少为[插图],每个图像块对应原始图像中8×8的区域。依次类推,第三阶段的块合并模块将图像块数量减少为[插图],每个图像块嵌入的维度为4C,对应原始图像中16×16的区域;第四阶段的块合并模块将图像块数量减少为[插图],每个图像块嵌入的维度为8C,对应原始图像中32×32的区域。通过块合并模块,不同阶段的图像块嵌入对应的原始图像区域越来越大,类似卷积神经网络中卷积的感受野越来越大,从而可以实现捕捉多尺度的特征。这些不同尺度的特征输入到目标检测或图像分割常用的多尺度融合模块中,进一步获得目标检测或图像分割的结果。Swin Transformer通过层次化结构的设计,将Transformer的结构设计与图像的多尺度特点相结合,目前已经成为计算机视觉领域普遍使用的骨干网络,被应用于图像分类、目标检测、图像分割等多种任务。
  • 研究人员还将Swin Transformer拓展到视频处理领域,提出VideoSwin Transformer[插图],在视频识别相关任务上达到当时最好的结果。2022年,Swin Transformer的研究团队提出了Swin Transformer V2[插图],通过扩大模型的 参数量和输入图像分辨率,进一步提高了Swin Transformer在执行图像分类、目标检测、图像分割、视频动作识别等任务时的效果。此后,研究人员还将SwinTransformer与自监督学习相结合[插图],进一步挖掘Swin Transformer从大量无标注数据中的学习能力。
  • 2021年,OpenAI提出了CLIP(Contrastive Language-Image Pre-training)[插图],利用从网上获取的大规模数据集,通过学习文本和图像之间的关联,获得了当时最优的视觉表征模型。CLIP具有极强的泛化能力,在图像分类、多种细粒度物体识别、文字识别、动作识别、文本图像检索等多种下游任务上都获得了超过监督训练模型的效果。同时,CLIP能够在未训练的数据集上表现出较好的识别结果,实现零样本学习。CLIP的出现推动了视觉和语言模型的融合,促进了多模态大模型的发展。
  • 在预训练阶段,CLIP使用自监督学习的方式,学习一个图像-文本共享的多模态表征空间,如图3.74所示。首先,对于一个批量(batch)中的N个图像-文本对,CLIP利用一个图像编码器和一个文本编码器分别提取图像表征I1,…,IN和文本表征T1,…,TN。其中图像编码器可以使用ResNet或ViT,文本编码器使用了Transformer。然后利用对比学习计算损失函数,匹配的N个图像-文本对(Ii, Ti)记作正样本,其余不匹配的N2-N个图像-文本对(Ii, Tj)记作负样本,其中,i/=j。CLIP的优化目标是使正样本在表征空间中距离更接近,而负样本距离更远。CLIP利用余弦相似度计算图像和文本表征的距离,在训练中最大化正样本的余弦相似度,同时最小化负样本的余弦相似度。通过优化由正负样本组成的目标函数,CLIP将图像和文本映射到一个共享的多模态表征空间,实现图像-文本的匹配。经过预训练后,CLIP能够将图像和文本映射到同一个表征空间,具有极强的泛化性能,并且学习到的图像和文本表征可以直接应用到下游视觉任务中。例如,进行图像分类时,如图3.75所示,所有可能的类别通过提示模板(prompt template)“A photo of a{object}.”转换成一段文本,即用类别名代替提示模板中的{object},然后输入到文本编码器中得到一组文本特征向量,同时待分类的图片经过图像编码器后得到图像特征向量。计算图像特征向量与该组文本特征向量间的余弦相似度,选取相似度最大的文本作为图像的分类结果。
  • CLIP的提出为后续的研究打下了坚实的基础,此后基于CLIP的工作层出不穷,例如,VideoCLIP[插图]将CLIP应用在视频领域实现零样本的视频理解,HairCLIP[插图]将CLIP应用在图像编辑上实现定制化修改发型,StyleCLIP[插图]将CLIP与StyleGAN[插图]结合实现文本引导的图像风格迁移。CLIP在跨模态学习和图像文本理解领域展示了巨大的潜力,开辟了一系列新的研究方向。
  • 型。作为一个同时具有理解和生成能力的统一的预训练多模态模型,BLIP采用了一种多模态混合编码器-解码器结构,如图3.76所示。BLIP具有三种运行结构:单模态编码器、基于图像的文本编码器和基于图像的文本解码器。
  • 单模态编码器:分别对图像和文本进行编码。BLIP使用ViT作为单模态图像编码器,将输入图像分割成多个图像块(patch),并将其编码为嵌入序列作为图像的特征。BLIP的单模态文本编码器使用与BERT一致的双向Transformer结构,接收的输入序列是[CLS]词元与文本的连接,并将输入序列编码为文本的特征信息。类似于CLIP,单模态编码器计算图像和文本的对比(Image-Text Contrastive, ITC)损失,使配对的图像与文本的表征相似度越来越高,不配对的图像与文本的表征相似度越来越低,从而实现图像表征和文本表征的对齐。• 基于图像的文本编码器:在文本编码器的每个Transformer块的自注意力层和前馈网络之间插入一个额外的交叉注意力层,从而向文本的嵌入序列中注入视觉信息。输入文本中添加了与任务相关的[Encode]词元,输出的序列中[Encode]词元的嵌入表示将作为图像-文本对的多模态特征表示。在训练阶段,该多模态特征用于计算图像-文本匹配(Image-Text Matching, ITM)损失。ITM是一个二分类任务,基于图像的文本编码器输出的多模态特征经过一个全连接层(被称为ITM头)​,判断输入的文本-图像对是否匹配。通过ITM损失,基于图像的文本编码器可以实现视觉和文本之间的细粒度对齐。• 基于图像的文本解码器:将基于图像的文本编码器中的双向自注意力层替换为因果自注意力层,从而实现对未来字符的预测。具体而言,在输入文本的开头添加[Decode]词元表示序列的开始,解码器的输出序列中的[EOS]词元表示输出序列的结束。基于图像的文本解码器为输入图像生成预测的文本描述,并通过语言建模(Language Modeling, LM)损失来训练该解码器,通过自回归方式将视觉信息转换为连贯的文本描述。
  • BLIP提出了描述生成过滤(Captioning and Filtering, CapFilt)方法,如图3.77所示。通过使用自动清洗噪声文本的过滤器(filter),以及产生新文本的描述生成器(captioner),来提高文本描述的质量。具体来说,过滤器是BLIP中的基于图像的文本编码器,它判断输入图像和输入文本是否匹配,并过滤不匹配的噪声文本。描述生成器是基于图像的文本解码器,它根据输入图像生成图像的新的文本描述,并替换掉原始的噪声文本,构成新的图像-伪文本对。将过滤后的图像-文本对、描述生成器产生的图像-伪文本对和少量的人工标注的图像-文本对合并,形成一个新的数据集(称为自举数据集)​,并使用该数据集训练新的模型。BLIP有效建立了视觉和文本特征的交叉融合,从而在多模态理解和生成任务上取得了更好的结果。然而,由于模型的参数量较大,数据集的规模较大,端到端的从头训练多模态大模型需要花费大量的计算资源。为了减少训练多模态大模型的计算资源,BLIP-2[插图]通过对现有的预训练好的视觉和语言单模态大模型进行自举,实现通用且计算高效的多模态大模型预训练。
  • 为了借助冻结的预训练图像编码器和LLM自举视觉-语言预训练,BLIP-2提出一个轻量级的Q-Former(Querying Transformer,查询Transformer)来弥补模态之间的差距。Q-Former训练分为两个阶段:第一阶段,用冻结的图像编码器自举视觉-语言表达学习能力,使Q-Former学习到与文本最相关的视觉表征。第二阶段,用冻结的LLM自举视觉到语言的生成学习能力,通过一个全连接层将Q-Former输出的视觉表征映射到与LLM的文本嵌入相同的表征空间,然后利用LLM的文本生成能力和复杂推理能力实现多模态理解和生成任务。BLIP-2的框架如图3.78所示。
  • 基于类似的思路,还出现了许多多模态大模型,如Flamingo[插图]、MiniGPT-4[插图]、LLaVA[插图]等。
  • 规划模块主要包括子任务分解(task decomposition)和反思(reflection)两大部分。
  • 在每个步骤中生成多个思考,每一步生成的多种策略拓宽了思维链的横向维度,形成了如图3.81b所示的树状结构。为从思维树中得到一条从输入到输出的完整路径,可以通过广度优先搜索或深度优先搜索系统地搜索思维树,利用分类器或投票评估每个状态,寻找全局最优的思维链。此外,还可以使用经典的规划器来进行长期规划[插图]。
  • 代表性的反思模块实现方法包括ReAct[插图]、Reflexion[插图]和Chain of Hindsight[插图]等。
  • 自我反思模块结合环境外部反馈和验证模块产生的内部反馈进一步生成文本摘要形式的“口头反思”​,来帮助LLM改进下一轮的推理和动作的生成。
  • CoH(Chain of Hindsight,后见链)利用一系列过去的输出和对应的人类反馈来改进模型的输出。CoH将各种类型的反馈都转换成文本,用于微调模型。具体来说,模型接收过去的输入和人类反馈,并基于预测对应输出的损失来微调模型参数。经过微调的模型可以在测试时选择性地接受人类指令以产生更好的输出
  • 在基于大模型的智能体系统中,上下文学习的输入可以类比为短期记忆,存储着执行当前复杂任务所需的信息[插图]
  • 在基于大模型的智能体系统中,长期记忆指的是外部向量存储。智能体通过访问外部向量存储,通过查询和检索利用其中的知识解决复杂问题
  • 以ChatGPT为例,OpenAI公司为其设计了插件和API函数调用功能。工具API的集合可以由其他开发者提供插件或自定义函数组成。智能体系统使用工具的代表性工作还有MRKL和HuggingGPT。
  • MRKL通过微调LLM,使其能够准确地将数学计算与计算器模块对应,完成相应的调用。因此,当外部工具足够丰富的时候,如何让智能体系统选择合适的工具是至关重要的。针对这一需求,微调后的LLM能够很好地扮演调度者的角色。类似地,TALM[插图]和Toolformer[插图]都通过微调LLM来学习使用外部工具API。HuggingGPT[插图]则将ChatGPT和HuggingFace平台相结合。HuggingFace平台扮演工具箱的角色,ChatGPT扮演控制器的角色。面对输入的需求,ChatGPT依次完成任务规划、模型选择、任务执行和响应生成,如图3.83所示。
  • 如,在科学发现方面,ChemCrow[插图]是一个基于LLM设计的化学智能体系统,通过整合17种专家设计的工具,有效地提升了LLM在有机合成、药物发现、材料设计等方面的性能。在社会模拟方面,生成式智能体系统利用LLM产生多个虚拟角色,借助记忆、规划和反思机制,使智能体能够根据过去的经验做出反应,并与其他智能体进行交互。
  • 谷歌提出了T5[插图]之后于2022年4月发布了PaLM[插图],于2023年5月发布了PaLM2[插图]; Meta于2023年2月发布了LLaMA[插图],于2023年7月发布了Llama2[插图],之后于2024年4月发布了Llama3[插图];华为于2023年4月发布了PanGu-Σ[插图],参数量达到了千万亿规模;清华大学提出了GLM[插图]
  • CLIP[插图]利用对比学习的方式实现图像和文本数据的表征对齐,在此基础上,BLIP[插图]将对齐后的图像表征与文本表征进行有机的融合,BLIP-2[插图]则将图像特征映射到LLM的输入文本嵌入相同的表征空间,从而将仅仅可以处理文本的大语言模型拓展为可以处理多模态数据的多模态大模型,类似的工作还有Flamingo[插图]、MiniGPT-4[插图]、LLaVA[插图]等多模态大模型。
  • 目前常用的方式包括通过提示工程(prompt engineering)将大模型中的知识引导出来并应用于各种各样的具体任务,以及对大模型进行领域微调使其能够快速应用于不同的下游任务。在此基础上,以大模型作为大脑或中央控制器,添加规划、记忆、使用工具等模块,形成智能体系统,可以更加像人一样思考和解决问题,处理复杂的决策问题,推动科学、社会学等更加广泛的领域的发展。更多的介绍可以阅读相关的文献[插图][插图]进一步了解。

 3.4 神经网络的优化

  • 对神经网络进行优化前,首先需要对神经网络的权重和偏置进行初始化。初始化方法对深度神经网络的训练非常重要,不合适的初始化方法可能导致神经网络的训练无法收敛甚至崩溃。常用的初始化方法有Xavier初始化[插图]及Kaiming初始化[插图]方法
  • Xavier初始化是由Xavier Glorot和Yoshua Bengio提出的一种初始化方法[插图]。在这一工作中,他们提出,为了保证神经网络模型的稳定性和有效性,避免梯度消失或梯度爆炸,对模型的初始化需满足以下两个条件:(1)[插图],即前向传播时每一层激活值的方差保持一致。(2)[插图],即反向传播时每一层对状态的梯度方差保持一致。
  • 其中,图3.84a和图3.84b的上图表示采用标准初始化方法的结果,下图表示采用Xavier初始化方法得到的结果。从中可以观察到,采用Xavier方法的网络,各层的激活值和梯度都较为一致,满足Glorot条件。Xavier方法适用于关于0对称、在原点处具有单位导数(如f'(0)=1)的激活函数,如tanh,但对于ReLU、PReLU这种非对称的激活函数的效果并不好。对于ReLU函数,当其输入小于0时输出为0,这种性质影响了输出的分布模式,使得前向和反向各层的方差无法保持一致。为此,Kaiming初始化对其进行了改进。接下来,我们对此进行介绍。
  • Kaiming初始化在满足Glorot条件的基础上,额外考虑了使用ReLU激活函数情况下的权重参数初始化问题[插图]。
  • 考虑连续可微实值函数f:R→R,由泰勒展开可以得到[插图]设步长η>0,然后令ϵ=-ηf'(x)。将其代入式(3.75)可得[插图]令η足够小使高阶项变得可以忽略,于是[插图]如果f(x)的导数f'(x)不为0,那么ηf′2(x)>0,因此
  • [插图]式(3.78)表明,按照x:=x-ηf'(x)的方式来更新x,能够降低f(x)的值,即减小损失函数的值,达到使模型预测值更接近真实值的目的。
  • 公式(3.81)中的步长η也称为学习率,是梯度下降算法中的一个超参数,用于控制每次参数更新的步长或幅度。学习率决定了优化过程中参数更新的速度和方向,学习率的选择对于模型的训练和性能至关重要。学习率设置过低(如图3.85a所示)可能导致参数更新缓慢,无法充分利用梯度信息,需要更多次的迭代才能达到收敛,增加训练时间和计算开销。学习率设置过高(如图3.85b所示)可能导致参数在更新过程中发生剧烈波动,跳过最优解,甚至无法收敛。学习率的选择是一个关键的超参数调整过程,依赖于数据集、模型架构和优化算法等因素。在实践应用中,对于不同的任务和情况,需要进行实验和调整以选择最佳的学习率。
  • 尽管梯度下降在深度学习中起到了基础作用,但直接使用传统的梯度下降算法并不常见,这是由于其在计算复杂度、内存要求、非凸优化问题和收敛速度等方面存在一定的问题。
  • 深度学习中使用了一些改进的优化算法,如动量梯度下降和Adam等,这些算法有助于跳出局部最小值并更快地收敛到更好的解。
  • 随机梯度下降法和传统的梯度下降法是两个极端,每次迭代更新参数时,前者只用一个样本,后者用所有数据。从训练时间来看,随机梯度下降法由于每次仅采样一个样本来进行迭代,因此训练时间较短;而传统梯度下降法的训练时间随样本数量的增加而增加。从准确率来看,随机梯度下降法仅用一个样本决定梯度方向,很有可能得不到最优解;而传统的梯度下降法在每次参数更新时使用所有样本的梯度,通常能够更准确地收敛到全局最优解。从收敛速度来看,随机梯度下降法每次只使用一个样本的梯度,具有较快的收敛速度,但参数更新存在不稳定性,因此可能会在局部最优解附近震荡或停留;而传统梯度下降法每次迭代时使用所有样本的梯度,通常收敛速度较快,但在大规模数据集上,计算所有样本的梯度非常耗时,导致收敛速度变慢。综上,传统梯度下降通常更适合在较小的数据集上使用,追求更高的准确率和稳定性。而随机梯度下降则更适用于大规模数据集和在线学习场景,追求更快的训练速度和适应性。此外,还有一种折中的方法,即小批量梯度下降,在训练时间和准确度上取得了平衡。
  • 假设将包含N个样本的训练数据集分成多个大小相等的小批量B。每个小批量包含样本的数量通常为2的幂次方,如32、64、128等,这样可以更好地适应计算硬件的向量化和并行操作,从而提高训练速度和计算效率。梯度g的值为一个小批量中所有样本梯度的平均值,即[插图]设学习率为η,参数的更新过程为:[插图]如何选择合适的小批量B是实践中需要关注的问题。较小的批量大小可以加快训练速度,但可能会导致梯度估计的噪声较大;较大的批量大小可以减少梯度估计的方差,但会增加计算负担。通常需要通过尝试不同的批量大小,并结合实际问题和计算资源进行调整。
  • 相对于随机梯度下降,小批量梯度下降在每次更新参数时使用了更多的样本信息,有助于减少参数更新的方差;而相对于传统梯度下降,小批量梯度下降可以更快地进行参数更新,且对于大规模数据集更具可行性。小批量梯度实现了计算效率和模型稳定性之间的平衡,是目前使用最广泛的一种梯度下降优化方法。
  • 为了解决这些问题,动量法(momentum)引入了物理学中动量的概念,认为梯度在参数空间中具有“惯性”​。在物理学中,动量体现了物体在运动方向上保持运动的趋势。对应在深度学习中,动量法使模型参数的更新总是保持在之前梯度的方向上。当某个参数在最近一段时间内的梯度方向不一致时,参数更新幅度变小;梯度方向一致时,更新幅度变大,起到加速作用。动量法用之前积累的动量来代替真正的梯度,这样,每个参数实际更新差值取决于最近一段时间内梯度的加权平均值。设v表示动量,初始化为零向量或较小的值。令g表示梯度,学习率为η,设β为动量参数,那么t时刻的动量表示为[插图]参数的更新过程为[插图]动量法可以与其他优化算法(如随机梯度下降、小批量梯度下降等)结合使用,以进一步优化模型的训练过程。图3.86体现了动量法对随机梯度下降算法的影响,引入动量后,参数更新的过程更加稳定,而且能够更快地收敛到最优解。[插图]图3.86 动量法对随机梯度下降(SGD)的影响
  • 梯度下降等一阶优化方法只利用梯度信息,而二阶优化方法使用二阶导数
  • (Hessian矩阵)信息,可以更准确地估计优化目标函数的形状,从而提供更快的收敛速度和更好的收敛性能。
  • 对于具有正定Hessian矩阵的局部二次函数,通过H-1重新调整参数,可以使f(θ)直接跳到极小值。如果f(θ)是凸的但是有高阶项,需要迭代应用牛顿法以达到最优解。每次迭代过程分为两步,首先更新或计算Hessian矩阵H的逆,然后根据式(3.89)更新参数。牛顿法采用二阶梯度,因此收敛速度较快。同时,牛顿法对凸优化问题具有全局收敛性,在凸优化问题中,可以找到全局最优解。但是,牛顿法需要计算和存储Hessian矩阵,特别是对于大规模问题来说,计算和存储Hessian矩阵的代价非常高。
  • 算开销。但BFGS算法仍然要存储整个近似矩阵M,需要O(n2)的存储空间。在BFGS算法基础上,L-BFGS(Limited-memory Broyden-Fletcher-Goldfarb-Shanno)算法做了进一步优化,不需要存储完整的Hessian矩阵的逆H-1的近似矩阵M,只需要存储一些历史信息,包括每次迭代的参数差值和梯度差值。存储的历史信息可以用于重新构建矩阵M,存储开销为O(n)。L-BFGS算法通常需要选择一个适当的历史信息数量,以平衡存储开销和近似Hessian矩阵的准确性。
  • AdaGrad(Adaptive Gradient,自适应梯度)算法[插图]根据参数的历史梯度的平方和自适应地调整学习率,对于频繁出现的参数梯度会降低学习率,对于不频繁出现的参数梯度会增加学习率。从训练集中采样m个样本{x1, x2,…,xm}的小批量,θ为模型参数,第i个样本的标签为yi, L表示损失函数,那么梯度表示为[插图]令累积变量r表示历史梯度的平方和,r的初始值为0。每次r会更新为[插图]其中,⊙代表Hadamard乘积,表示矩阵对应位置元素相乘。每次参数更新为[插图]其中,η为全局学习率,常数δ为较小的值,例如δ=10-7。相对于全局固定的学习率η,AdaGrad算法在学习率前引入了一个动态因子[插图],当历史梯度平方和r越大,动态因子[插图]就越小,学习率降低;相应地,当r越小,动
  • 态因子就越大,学习率提高。对于数据分布稀疏的场景,AdaGrad算法能更好地利用稀疏梯度的信息,比标准的随机梯度下降算法提高收敛性能。但AdaGrad算法的主要问题是通过累积参数的历史梯度平方和来自适应地调整学习率,随着时间的增加,学习率快速降低,可能导致训练后期学习率过低,无法有效更新参数。
  • RMSProp(Root Mean Square Propagation,均方根传播)[插图]改进了AdaGrad算法中学习率递减过快的缺点,将历史梯度平方和替换为指数加权移动平均。RMSProp算法引入衰减速率超参数ρ,来控制历史梯度信息保留多少。累积变量r的更新为[插图]参数θ更新为[插图]RMSProp利用衰减速率ρ丢弃遥远过去的历史梯度,减轻了学习率递减过快的问题。同时,RMSProp能够自动调整学习率,适应不同参数的更新,进一步优化了参数更新中摆动幅度过大的问题,加快了模型的收敛速度。
  • Adam(Adaptive Moment Estimation,自适应矩估计)[插图]结合了动量方法和RM-SProp算法的思想,计算参数梯度的一阶矩估计(平均值)和二阶矩估计(方差)​,并使用偏差修正来纠正矩估计的偏差,自适应地调整学习率。Adam算法的整体流程如下。在初始化时,初始学习率用η表示,通常设置为一个较小的值。一阶矩估计的指数衰减率用β1表示,通常取值为0.9。二阶矩估计的指数衰减率用β2表示,通常取值为0.999。常数δ用于数值稳定,取值通常为一个很小的数,如10-8。累积梯度的一阶矩估计的初始值m0设置为0,累积梯度的二阶矩估计的初始值v0设置为0。迭代次数表示为t。在每次迭代中,首先计算当前步骤的梯度gt,然后更新计数器,将迭代次数t增加
  • Adam算法结合了动量方法和自适应学习率的优点,具有较好的性能和收敛速度,适用于大规模数据集和高维参数空间。在实践中,Adam算法被广泛应用于深度学习中的优化问题。

 3.5 神经网络量化

  • 训练一个BERT-base网络,使用64块V100 GPU需要79小时,耗电1507千瓦时[插图],是全球平均每年人均家庭用电量(731千瓦时)[插图]的两倍;训练1750亿参数的GPT-3模型,使用1024张A100 GPU需要34天[插图],耗电约1287兆瓦时[插图]。这种巨大的时间、能耗和费用开销已经超出了普通研究者和用户承受范围,限制了深度学习技术的发展和实际应用。因此,如何有效减少深度神经网络的计算开销,提高计算效率,对深度学习领域的发展具有重要意义。
  • 图3.87 里程碑式神经网络模型的训练计算量(FLOPs)
  • 在深度神经网络的计算中,线性计算层(例如卷积层、全连接层)的计算开销占整个神经网络计算开销的绝大部分。这是由于线性计算层由大量乘累加(MultiplyAccumulate, MAC)计算组成,其中包含延时和能耗都很高的单精度浮点数(FP32)乘法。FP32乘法的延时大约是8比特整数(INT8)乘法延时的3倍,所需存储空间是 INT8乘法存储空间的4倍,能耗大约是INT8乘法能耗的17倍[插图]。
  • 为了减少深度神经网络的计算开销,研究人员提出了神经网络量化方法,将高位宽数据(如FP32)离散化到低位宽数据(如INT8)​。在深度神经网络的存储、前向传播、反向传播的过程中,用低位宽数据表示激活值、权重、梯度等数据。相对于原始高位宽数据,使用量化技术可能会降低深度神经网络的准确率,但不会对准确率造成很大的影响,同时可以有效减少深度神经网络的计算量和存储空间,提升计算速度、降低计算能耗,同时也能减小运行深度神经网络的乘法器面积。量化后的定点数位宽越低,带来的准确率损失越大,同时减少的计算量也越多。
  • 深度神经网络中的权重(见图3.88a)​、激活值(见图3.88b)​、梯度(见图3.88c)的数据分布不同,不同层间的数据分布也不同。这些数据通常服从尖峰长尾分布,但不同类型或不同层的数据的取值范围有较大差异,例如AlexNet不同层的梯度的最大绝对值有两个数量级的差异(0.00035~0.0312)。此外,神经网络中有些数据是以0为中心的对称分布,有些数据是非0中心的对称分布,有些数据则是非对称分布,如图3.89所示。
  • 针对神经网络数据的上述特点,需要选择合适的数据量化方式对深度神经网络的数据进行低位宽表示,在降低数据表示位宽的基础上减少数据的量化误差,从而降低因数据量化导致的深度神经网络准确率损失。根据量化的对称性,可以将数据量化方式分为对称量化和非对称量化;根据量化的均匀性,可以分为均匀量化和非均匀量化;根据量化粒度,可以分为分层量化和分通道量化。下面将分别介绍不同的量化方式。
  • 为方便下文描述,假设F表示需要量化的数据集合,Z表示F中绝对值的最大值,R∈F为需要量化的实数,Q为量化后的低位宽定点数,A为量化后数据Q可以表示的最大绝对值,n为量化后定点数的位宽(例如,INT8的位宽为8, INT16的位宽为16)​,p表示量化参数,2p表示量化步长(也称为缩放系数)​。
  • 称量化中最关键的是找到合适的量化步长2p。下面介绍如何得到量化参数p。假设将F中实数数据量化为n位定点数Q。n位定点数可以表示的最大值为2n-1-1,例如INT8可以表示的最大值为127。量化后数据Q可以表示的最大绝对值A为[插图]对称量化后,量化数据能表示的数据范围为[-A, A]​。为了能够完整地表示实数集合F中数据,A需要大于等于F中绝对值最大值Z,同时Z要大于A/2,即[插图]求解可得p为[插图]其中⌈ ⌉表示向上取整操作。对深度神经网络进行数据对称量化时,首先统计待量化数据中的绝对值最大值Z,根据设置的定点数位宽n,利用公式(3.105)计算量化参数p,然后利用量化参数p
  • 按公式(3.101)计算量化后的定点数,进行后续乘累加等计算。计算完成后,将结果利用公式(3.102)计算反量化后的数据,得到最终的结果。
  • 深度神经网络的层数多,参数量大,每层的权重和激活值(也称为特征图)的数据分布范围不同
  • 例如,Q-BERT[插图]对BERT模型的注意力层进行分组量化,模型准确率优于分层量化,接近分通道量化,计算和存储开销介于分层量化和分通道量化之间。
  • 图3.93 神经网络量化流程(以INT8量化为例)
  • 在实际应用中,量化可以有效加速深度神经网络的推理或训练过程。其中,推理加速有训练后量化(Post-Training Quantization, PTQ)和量化感知训练(Quantization Aware Training, QAT)两种方式。
  • 除了在推理阶段使用量化,量化训练方法可以在深度神经网络训练阶段利用量化降低训练的计算复杂度,提高训练效率,如图3.94c所示。与应用于推理阶段的量化方式不同,量化训练在深度神经网络从初始化参数进行从头训练的整个过程中使用量化。此外,量化训练对深度神经网络的激活值、权重和梯度都进行量化,即对前向传播和反向传播都进行量化,来降低前向传播和反向传播过程的计算
  • 量,从而达到加速训练的目的。由于是从头训练就使用量化,量化训练方法不使用预训练模型,以降低训练成本。该方法的优势是可以同时加速训练和推理过程,并且可以在训练完成后直接得到低位宽表示的模型,不需要再进行推理阶段的量化。但由于每次前向传播和反向传播都进行量化,因此不能使用过于复杂、开销过大的数据量化方式,否则会引入过多的额外计算量。
  • 混合精度量化(Mixed Precision Quantization)[插图]是指在神经网络量化推理或训练中对网络各层的激活值、权重或梯度合理分配不同的比特精度的量化方法。需要注意的是,混合精度量化的概念不同于常见的混合精度训练(Mixed PrecisionTraining)[插图]。后者专指在网络训练过程中,使用半精度浮点数(简称为FP16)或谷歌bfloat16(简称为BF16)[插图]格式(如图3.95所示)的数据代替部分单精度浮点数FP32数据进行计算和存储,同时在FP32权重上进行更新的方法。
  • 混合精度量化的重点在于如何合理地分配比特精度。好的分配策略既要尽可能地保证网络模型的准确率不受影响,也要尽量提高计算和存储效率。数据格式分配一般来说可以分为三个维度:不同层的精度、不同训练迭代(iteration)的精度,以及每层内不同数据的精度。最简单的分配方式只考虑第一个维度,即不同层的精度不一致但层内的所有数据精度一致,且不随着迭代次数变化。例如,人为指定某些层为高精度数据格式、其他层为低精度数据格式,或者根据设定的指标为每层分配不同精度。这种固定分配每层精度的方式实现简单,但是不能很好地适应变化的数据,常用于训练后量化。在量化感知训练和量化训练中往往也要考虑第二个维度,即根据设计好的规则在不同训练迭代使用不同比特位宽的数据格式。例如,在规定的迭代次数调整比特位宽,或者自适应地在每一次迭代根据当前数据得到比特位宽。此外,为了更好地拟合不同类型的数据,同一层中激活值、权重和梯度也可以使用不同的数据格式来表示。例如,使用INT8表示激活值,INT4表示权重,FP16表示梯度。这类方法往往需要设计特殊的运算器来进行不同数据格式间的乘法。
  • 大模型广泛采用混合精度训练(即用FP16或BF16替代部分FP32进行数据存储和计算)进行预训练和微调,得到的FP16(或BF16)模型在推理过程中继续使用FP16(或BF16)数据进行计算,以降低训练和推理的计算和存储成本。然而,将数据精度降低到FP16(或BF16)对大模型而言是远远不够的。训练和部署百亿甚至千亿量级参数的大模型需要的存储和算力仍是巨大的,例如训练多语种大语言模型BLOOM-176B需要384块80GB的A100 GPU跑约3.5个月[插图],推理该模型需要8块这样的GPU[插图],而微调该模型需要72块这样的GPU[插图]。
  • 当前大模型量化主要用于训练后量化,很多开源大语言模型都会支持基础的INT8和INT4训练后量化,不过量化后的模型效果会有不同程度的下降。此外,研究人员也针对大模型提出了一些新的量化方法,如LLM.int8()[插图]、GPTQ[插图]和SmoothQuant[插图]等。其中有的方法主要关注减少显存占用但没有加速效果,而有的方法在减少显存占用的同时加速了推理过程。这些方法的出现为实现准确率无损的大模型低位宽量化推理提供了有力支持。
  • 量化技术可以仅用于神经网络的前向传播阶段来加速推理过程,或同时用于前向传播和反向传播阶段来加速训练过程。随着大模型的发展,模型的参数量和训练数据量不断增加,算力需求不断提升,量化技术已经成为大模型推理中不可或缺的技术。目前将模型量化到INT16或INT8已经几乎不损失准确率。研究人员还在不断尝试使用更低位宽,如使用INT4进行大模型的微调和推理,但目前使用INT4等更低位宽时还会引起模
  • 型的准确率下降,因此如何使用INT4等更低位宽对大模型进行量化并减少准确率损失依然是需要探索和解决的问题。

 3.6 驱动范例

  • 图像风格迁移的目标是将一张图片(称为内容图像)的语义内容与目标风格(称为风格图像)融合起来,产生语义内容与内容图像相同但具备风格图像的风格的图片。传统的图像处理领域可以通过一些纹理迁移的方式达到图像风格迁移的目的,但对风格的迁移程度有限。2015年,Gatys等人[插图]提出了一种用卷积神经网络来实现图像风格迁移的方法,并由此开辟了“神经风格迁移”的新领域。最初的神经风格迁移采用基于图像优化的非实时风格迁移算法[插图],通过对风格迁移图像进行迭代更新,使其风格接近风格图像,同时内容接近内容图像。这种方法是在线训练的、非实时的,每生成一张风格迁移图像都需要重复训练过程,速度较慢。为了提高神经风格迁移的速度,研究人员提出了基于模型优化的实时风格迁移算法,训练卷积神经网络模型代表目标风格,当模型训练好后,给定任意的内容图像,仅需一次前向计算就可以获得风格迁移图像。这类方法是离线的、实时
  • 的,速度较快。在基于模型优化的实时风格迁移算法中,研究人员首先提出了单模型单风格算法[插图],即训练一个模型仅表示一种风格。之后为了增加模型能够表示的风格数量,研究人员又设计了单模型多风格算法[插图][插图],通过发掘不同风格网络之间的共享部分,然后对新的风格只改变其有差别的部分,并保持共享部分不变,实现单个模型表示多种风格。在此基础上,研究人员又设计了单模型任意风格算法[插图],通过在大规模风格和内容图上进行训练学习任意风格的模型表示。
  • 基于GAN的一些工作也可以实现图像风格迁移,例如CycleGAN[插图]、StarGAN[插图]、MUNIT[插图]等。此后,研究人员又尝试将扩散模型和多模态大模型结合实现更加逼真、灵活的图像风格迁移,例如StyleCLIP[插图]、CLIPstyler[插图]、CLIPDraw[插图]等。
  • 文献[插图]使用预训练的VGG19中的16个卷积层和5个池化层来做图像风格迁移。图像风格迁移需要用到2个损失函数:内容损失函数和风格损失函数。内容损失函数是随机噪声图像与内容图像在内容特征上的欧氏距离[插图]
  • 图3.96 图像风格迁移
  • 此用VGG19中的conv4_2的特征来计算内容损失。通过最小化内容损失函数,可以缩小生成图像与内容图像在内容上的差距
  • 总的损失函数定义为内容损失函数和风格损失函数的加权和:
  • 通过求损失函数对输入像素的偏导[插图]可以得到梯度,然后进行反向传播更新输入像素值,经过多轮迭代可以得到合成图像,如图3.97所示。例如,开始输入白噪声,经过正向传播计算得到损失,然后做反向传播调整输入像素值,调整完图片像素值再做正向传播并计算损失,重复多轮之后,损失将趋近于0,就认为生成图像同时具有内容和风格。
  • 本书的实验采用了文献[插图]中提出的一种实时、快速的图像风格迁移方法。该方法将图像转换分为如图3.98所示的两个步骤:训练过程和实时转换过程。训练过程的目的是训练出一个图像转换网络。一旦训练好了图像转换网络,对每个输入图像只需要做一次图像转换网络的正向计算即可输出风格迁移后的图像,而不需要像文献[插图]一样对每个输入图像做繁重的神经网络训练。
  • 图3.98 实时图像风格迁移过程
  • 实时图像风格迁移的具体流程如图3.99所示。这里面包括图像转换网络和损失网络两个网络。在训练图像转换网络的过程中,输入图像(即内容图像)x送到图像转换网络进行处理,输出生成图像yˆ;再将生成图像、风格图像ys、内容图像yc=x分别送到损失网络中提取特征,并计算损失。图像转换网络fW是一个深度残差网络,以便于训练。具体来说,这个深度残差网络参考了DCGAN[插图]的设计思想,用步长卷积和小数步长卷积取代池化;除了输出层,所有非残差卷积层后面
  • 都加了批归一化和ReLU,输出层使用tanh函数将输出像素值限定在[0, 255]范围内;第一层和最后一层卷积使用9×9卷积核,其他卷积层都使用3×3卷积核。
  • 损失网络采用在ImageNet数据集上预训练出来的VGG16网络。损失网络中定义的视觉损失函数由特征重建损失Lf和风格重建损失Ls组成[插图]:
  • Cj、Hj、Wj分别表示第j层卷积输出特征图的通道数、高度和宽度,ϕ(y)是损失网络中第j层卷积输出的特征图,实际中选择第7层卷积的特征计算特征重建损失。而第j层卷积后的风格重建损失为输出图像和目标图像的格拉姆矩阵的差的F-范数[插图]:[插图]其中,格拉姆矩阵Gj(x)为Cj×Cj大小的矩阵,矩阵元素为[插图]
  • 图3.99 实时图像风格迁移算法流程[插图]

 第4章 编程框架使用

  • 编程框架在整个智能计算系统中起到了承上启下的作用,在某种意义上就像信息产业里的操作系统。操作系统是软硬件之间的界面,用以管理计算机硬件与软件资源,程序或用户都通过操作系统来使用硬件。在智能计算系统中,编程框架为程序员提供了使用硬件和系统的界面,是智能计算系统中非常关键的核心枢纽。PyTorch是当前最流行的深度学习编程框架之一,用户使用时不需要考虑底层的CPU、GPU或者深度学习处理器如何工作。
  • 一旦写错了,想要调试深度学习算法的代码非常困难,因为其结果仅仅是一个在大量测试样本上平均出来的准确率。这些都给程序员自己写代码实现深度学习算法增加了难度。
  • 014年,伯克利视觉和学习中心(Berkeley Visionand Learn-ing Center, BVLC)发布了编程框架Caffe[插图],因为其易用、稳健、高效等优点,发布后即被广泛用于深度学习算法的训练和预测。2015年年底,谷歌发布并开源了编程框架TensorFlow[插图][插图],该框架基于计算图进行数值计算,支持自动求导,不需要在反向传播过程中手动求解梯度,且具有灵活的可移植性,能够把训练好的模型方便地部署到多种硬件、操作系统平台上,因此一经发布就 受到了广泛关注,迅速成为当时使用人数最多、应用范围最广的编程框架。2017年,Facebook(现已改名为Meta)发布了开源编程框架PyTorch[插图],该框架基于动态图的运行机制,在构建动态图的同时即执行图的运算,给模型的开发、调试带来了更多的便利,在学术界、工业界,有越来越多的用户开始使用PyTorch开发深度学习算法。此外,还有MXNet[插图]、PaddlePaddle[插图]等深度学习框架,这些编程框架为程序员开发深度学习算法提供了便利,也积极推动了深度学习算法的发展。
  • MATLAB直到2020年才发布了专门用于处理神经网络算法的深度学习工具箱(deep learning toolbox)。Torch是面向机器学习应用的编程框架,但其采用相对小众的LuaJIT脚本语言作为编程接口,增加了学习成本,且Lua自身的第三方库较少,在易用性上存在不足。这些问题都限制了程序员开发神经网络的效率。
  • 2008年,Yoshua Bengio领导团队开发并发布了Theano[插图], Theano是一个基于Python的开源科学计算框架。使用Theano能动态生成C代码,可被用作深度学习开发的基础框架。但由于其偏向底层,因此存在编译时间长、调试不便等问题。
  • 在该阶段,大量的编程框架被提出,包括TensorFlow(2015年)​、MXNet(2015年)​、Keras[插图](2015年)​、CNTK[插图](2016年)​、ONNX[插图](2017年)​、PyTorch(2017年)​、JAX[插图](2018年)等。这些编程框架大多基于计算图机制,支持CPU和GPU,具有较高性能,且支持多种编程语言(如C++、Python、Go、R等)​,易用性较好。其中,TensorFlow和PyTorch是最具代表性的两个编程框架,经过近几年的发展,目前绝大多数的深度学习模型均是基于这两种编程框架开发实现的。
  • 高性能方面的代表框架是TensorFlow。它提出细粒度的张量、算子和计算图抽象,分别表征神经网络中的数据、运算和网络结构。网络的前向、反向计算过程均使用计算图来实现,计算图中的节点代表具体的运算操作,边代表在节点之间流动的数据(即张量)​。这种方式易于神经网络的并行计算和优化。TensorFlow这个名字就表达了张量在计算图各个节点之间流动的过程。TensorFlow 1.x是基于静态图的机制,计算图构建完成后并不会立即执行,需要发送到会话(session)环境中,才能实现输入数据的赋值、网络的计算。这种基于静态图的处理机制可以进行全局的性能优化,但是由于不能在编程时立即执行并
  • 获得执行结果,影响了使用和调试效率。TensorFlow能够支持各种不同的硬件平台,包括CPU、GPU、DLP和各种移动端设备,对于一种新的设备类型,只需要按照特定格式完成设备的定义并将其注册到TensorFlow中,用户就可方便地利用该新增设备完成模型的计算。同时,TensorFlow提供了大量的工具和服务,如TensorFlow Serving、TensorFlow Lite,使得从研究原型到各种不同类型平台上的生产部署过程更加顺畅。PyTorch设计的主要目标是易用性,次要目标是高性能,这对于不断产生新算法结构且需要立即验证结果的学术研究非常友好。PyTorch简单易上手,因此自提出以来,就开始逐渐抢占TensorFlow的份额,已成为目前学术界使用最广泛的深度学习编程框架。与TensorFlow使用静态图不同,PyTorch使用的是动态计算图,代码编写的过程中同步实现计算图的构建及执行,可以灵活搭建动态的神经网络,更便于调试。2023年3月,PyTorch 2.0发布,新版本的PyTorch框架引入了深度学习编译机制,在100%前向兼容的情况下显著提升了神经网络模型的实现性能。

 4.2 PyTorch概述

  • PyTorch的名字来源于Python以及Torch。这里的Torch是由瑞士亚普研究所(IDIAP)在2002年发布的一款机器学习框架[插图],该框架采用Lua语言作为编程接口,内核采用C/C++实现。Torch中提供了许多代表性的算法,提高了用户的编程效率,同时在实现复杂的神经网络拓扑结构方面具有较大的灵活性,在GPU上具有较高的性能。可以将PyTorch编程框架看成Torch的Python版本,是一个基于Torch的Python开源机器学习库,并在原有Torch内核的基础上进行了大量的扩展。借助Python蓬勃发展的开源生态,PyTorch可以直接使用已有的Python库,实现对深度学习算法的高效开发。此外,目前的PyTorch中还并入了Caffe2[插图]的代码,通过吸收Caffe2中的模块化设计,进一步增强了PyTorch框架的可扩展性及在生产环节的开发效率。
  • 据统计,2019年人工智能方向的国际顶级会议中,基于PyTorch的工作占比已经超过了TensorFlow[插图]。此外,另一项主要针对人工智能学术界、工业界用户的统计也表明,2021年PyTorch是人工智能应用开发中最常用到的编程框架,用户数超过TensorFlow的用户数量[插图]。

 4.3 PyTorch编程模型及基本用法

  • numpy.eye()可以通过多种方式生成数组。可以用numpy.eye(N, M, k)生成一个N×M的二维数组,对角线元素为1,其余元素均为0。其中,参数M表示数组的列数,是可选参数,没有定义就默认为N;参数k也为可选参数,其值为0表示主对角线值全为1,为负表示主对角线左移|k|位的对角线值全为1,为正表示主对角线右移k位的对角线值全为1。还可以用numpy.eye(num_class)[label_array]对标签数组label_array进行独热编码,返回编码后得到的数组,参数num_class表示标签类别数量。使用numpy.eye()生成数组的示例程序见图4.5,其创建的数组如图4.6所示。
  • 2025/06/18 发表想法

    第7行有问题,应该是my_data3 = np.arrange(6)

     

    原文:图4.8 修改形状属性方法示例

  • 原生的NumPy只支持CPU计算。为了能够在GPU上高效运行Python计算库,出现了如CuPy[插图]、Numba[插图]、PyCUDA[插图]、PyTorch等编程库或编程框架。
  • 一张RGB图片可以表示为三阶张量,而多张RGB图像构成的数据集可以表示为四阶张量。
  • 张量和NumPy数组之间具有较高的相似性,二者可以相互转换,且转换的开销较小。例如,使用OpenCV方法读取的结果为NumPy数组,当需要对其进行一些张量相关的操作时,需要将其转换为张量;相比torch. Tensor, NumPy中支持的操作种类更为丰富,当需要对张量执行一些torch. Tensor不支持的操作时,可以先将张量转换为NumPy数组,然后利用NumPy提供的函数进行计算,再转换回张量。
  • bfloat16(BF16)型是一种格式上介于FP16和FP32之间的数据类型[插图],如图3.95所示,由1位符号位、8位指数位和7位尾数位组成(FP16型的指数位为5位、尾数位为10位;FP32型的指数位为8位、尾数位为23位)​,该数据类型通过降低精度来获得更大的数值空间,目前在深度学习中被大量使用。
  • 当需要对张量的形状属性进行转换时,可以使用tensor.reshape()方法,如图4.16所示。其中,第7行代码中,目标形状属性表示为(-1, 2),单个形状维度上的值为-1,表示该形状维度上的值需要根据其他维度的形状属性来推算。因此,my_data4对应的形状属性推算后应为(3, 2)。
  • GPU上不能直接支持NumPy数组,如果希望将GPU上的张量转换成NumPy数
  • 组,需要先将张量转换到CPU上,再转换成NumPy,具体过程如图4.17所示
  • 图中第3~5行代码的执行结果为my_data1=[0, 7, 8,3
  • 当需要对张量进行切片时,可以采用[start:end:step]的形式,其含义为:从start开始,以step为步长开始读取张量,到end终止(不包含end)​。其中,start、end、step均可以缺省,start缺省为0,可以为负数,end缺省为该维度最后一个元素(包含)​,而step缺省为1,不能为负数。
  • 2025/06/18 发表想法

    主流标准格式:Tensor[图片序号, 通道序号, 高, 宽] 即 (Batch, Channels, Height, Width) PyTorch 默认格式:NCHW 格式(Number-Channels-Height-Width)

     

    原文:图4.20 张量的切片索引

  • 如果要对张量进行维度压缩、扩展,可以分别使用torch.squeeze()和torch.unsqueeze()函数。前者能够将张量中所有为1的维度移除,或者将张量中指定的、维度为1的维度移除;后者能够在指定维度插入值为1的维度,使用实例如图4.22所示。
  • 图4.22 张量的维度压缩、扩展
  • 张量数据可以有多种数据格式,代表了多维数组以何种线性存储方式在存储空间中存储[插图]。比如,PyTorch和GPU中通常采用NCHW的数据格式,而TensorFlow中通常采用NHWC的数据格式
  • 下面以图4.23所示的数据(N=2, C=3, H=2, W=2)为例,说明其在计算设备中的存储形式,其中左右两图分别对应该批量中的第一张图及第二张图。在计算设备中,所有的数据按照一维来存储,不同的数据格式对应了该数据在计算设备中的存储顺序[插图]。对于图4.23中所示的数据,当其按照NCHW的数据格式来组织时,其在计算设备中按照W→H→C→N的顺序来存储数值,如图4.24a所示;而当其按照NHWC的数据格式来组织时,其在计算设备中是按照C→W→H→N的顺序来存储数值,如图4.24b所示。
  • 图4.24 不同数据格式的在计算设备中的存储顺序
  • 对于一个计算操作来说,如果所有输入中有一个输入需要求导,则输出就需要求导;如果所有输入都不需要求导,则输出也不需要求导。
  • 图4.26的第12行代码使用了tensor.operation_()方法来定义计算操作,其与第9行代码相比,区别仅为在操作后面增加了“_”标识。该标识代表操作为原位(in-place)操作,即在存储原张量的内存上直接计算并更新张量值,而不是先复制张量再计算更新。Python语言中也有类似的原位操作,如+=、*=等。
  • 但原位操作也有一定的局限性[插图]。首先,原位操作会覆盖原张量,如果在模型训练时使用原位操作来更新张量的梯度,则每次迭代计算所得的梯度值都将被覆盖,从而破坏模型的训练过程;其次,对于多个张量同时引用一个张量的情况,对该张量进行原位操作会影响其他张量的操作。
  • PyTorch中,某个操作能够进行广播的条件有两个:(1)参与计算的每个张量都有至少1个维度。(2)对于维度较少的张量,需要从其末尾的维度开始对齐扩展,扩展出的新维度对应的维度尺寸为1。对于扩展后的张量,当满足下列两种情况中的任意一种即可进行广播操作:a)扩展后的形状属性与另一张量相同;b)扩展后的形状属性与另一张量不同,但不同的那个维度对应的维度尺寸为1。
  • 在图4.30中,my_data1的形状为(2, 3, 3),my_data2的形状为(3, 1),将两个张量从末尾的维度对齐,相比my_data1, my_data2缺少第一个维度,且最后一个维度的尺寸为1,可以先扩展出第一个维度,再将第一个维度与最后一个维度的尺寸均从1扩展到3,最后再进行加法操作,也属于可以广播的情况。
  • 计算图的本质是节点和边的关系。其中,节点可以表示数据的输入起点、输出终点、模型参数等,如图4.35中的X、W和Y;也可以表示各类处理,包括数学运算、变量读写、数据填充等,如图4.35中的矩阵相乘(记作“matmul”​)​。边则表示节点之间的输入/输出关系。边有两种类型,一类是传递具体数据的边,传递的数据即为张量。还有一类是表示节点之间控制依赖关系的边,这一类边不传递数据,只表示节点执行的顺序,前序节点完成计算,后序节点才能开始计算。
  • 图4.36 静态图机制
  • 图4.37 动态图机制

 4.4 基于PyTorch的模型推理实现

  • 在PyTorch中,一般使用torchvision包中自带的transforms模块来完成PIL图像与张量数据的格式转换。torchvision.transforms模块中包含了图像转换相关的函数,这些函数可以作用于PIL对象和Tensor对象,实现二者的互相转换。表4.10中列出了transforms模块中的常用函数操作及其含义。
  • 表4.10 transforms模块中的常用函数操作及其含义
  • 图4.44 使用torchvision.io包读入图
  • 第11行代码调用loader方法,将读入的PIL图像转换为张量,并将数据格式扩展为(1, C,H, W)
  • 当需要对自定义模型进行训练时,PyTorch能够根据forward方法中描述的计算过程,自动实现反向的梯度计算,因此在forward方法中无须定义反向计算过程。图4.47给出了一个简单的构建自定义模型的示例。在第6行代码中,super()函数的作用是获取当前类(即myModule)的父类,也就是nn. Module;第11行代码定义了模型的推理过程,即对输入先进行卷积计算Conv2d,再进行非线性激活操作softmax。由于这两个操作步骤中,仅有卷积操作涉及模型参数更新,因此,在__init__方法中仅对卷积操作进行了初始化。
  • 图4.47 自定义模型示例
  • 在PyTorch中保存模型时,保存的是包含在状态字典中的参数,即parameter参数和persistent buffer参数[插图]。parameter和buffer参数均可以在模块中注册,对使用register_parameter()和register_buffer()方法注册过的parameter和buffer参数,在执行module.to(device)操作时,可以自动进行设备转换。
  • 表4.12 模块的常用属性或方法[插图]有两种创建parameter参数的方法。第一种是在定义模块类时将成员变量(如self.weight)通过nn. Parameter()创建,得到的参数会自动注册为parameter参数,如图4.48所示。在第6行代码中,通过nn. Parameter()创建了模型的权重参数,该参数会自动注册为parameter。使用该方法创建的参数可以通过model.parameters()方法返回,并自动保存到OrderDict中。第二种创建parameter的方法是直接通过nn. Parameter()创建张量,得到的parameter对象再通过register_parameter()方法进行注册,图4.49
  • 给出了程序实例。第4行代码创建了普通的parameter参数对象weight,此时的parameter参数不作为模型的成员变量,第5行代码将parameter对象weight通过register_parameter()方法进行注册,此时得到的参数可以通过model.parameters()方法返回,注册后的参数也会自动保存到OrderDict中。
  • 对于神经网络中的不可学习的参数,即buffer参数,其创建方法为:先创建张量,得到的张量对象再通过register_buffer()注册,如图4.50所示。第4行代码创建了形状为(2, 2)的张量my_buffer,第5行代码通过register_buffer()注册该张量。使用此方法得到的参数可以通过model.buffers()返回,注册完成后参数也会自动保存到OrderDict中。
  • 从定义上来看,torch.nn.xx是一个类,通常在模块的__init__方法中通过对其进行实例化来定义模块的成员变量,并在forward方法中进行模型中可学习参数的更新;torch.nn.functional.xx是一个函数,通常用于forward方法,函数中传递的参数需要在__init__方法中创建并初始化。下面以卷积计算Conv2d的实现为例说明二者的区别,如图4.51和图4.52所示。图4.51中定义的torch.nn. Conv2d是一个类,其参数包括了(卷积计算过程中)输入和输出数据的通道数、卷积核尺寸、卷积步幅等;而图4.52中定义的torch.nn.functional.conv2d是一个函数,其参数为参与卷积计算的输入数据、权重数据、偏置数据等。
  • 在用法上二者也是不同的。torch.nn类的用法示例如图4.53所示。该类通常在模块的__init__方法中进行实例化,并在forward方法中进行模型中
  • 可学习参数的更新。
  • torch.nn.functional函数的用法示例如图4.54所示。该函数通常用于forward方法中,函数中传递的参数需要在__init__方法中创建并初始化。
  • 图4.55 torch.nn. Sequential用法
  • 图4.56 利用torch.nn. Sequential构建计算模块
  • 在torchvision.models.vgg的源代码中[插图],VGG网络包含了3个顺序执
  • 对模型参数进行初始化的方法有三种:使用torch.nn.init模块、使用torch.nn. Module.apply函数以及使用self.modules方法。1.使用torch.nn.init模块进行初始化神经网络模块中的parameter和buffer参数默认为CPU上执行的32位浮点数,在定义神经网络模块时可以将其转换为任意的数据类型及设备类型。在模块类的构建函数__init__中,可以使用torch.nn.init模块来对parameter和buffer参数进行初始化。图4.59给出一种程序示例,第8行代码将线性层中所有的参数初始化为全0。
  • 2.使用torch.nn. Module.apply函数进行初始化torch.nn. Module.apply(fn)函数能够对模块中的每个子模块(包括模块自身)递归地应用fn方法完成初始化。使用时,为神经网络中的每一个/每一种子模块分别定义初始化方法fn,再应用apply(fn)函数实现对每个子模块的初始化。示例程序如图4.60所示,第1~10行定义了一个多层的神经网络,包含了卷积、BN、ReLU三个子模块,其中卷积、批归一化操作中都涉及需要训练的参数。第13~17行代码根据计算类型的不同定义了不同的初始化方法weights_init,对于卷积层的参数使用Kaiming初始化方法,而对于批归一化层的参数则初始化为全1。第23代码使用apply(weights_init)函数,将自定义的初始化方法应用到神经网络中的每一个子模块中。
  • 3.使用self.modules()方法进行初始化第三种初始化方式是使用self.modules()完成初始化。self.modules()继承了所定义的模块类拥有的方法,并按顺序返回此前定义的所有层。在__init__方法中,使用self.modules()方法循环地完成所有子模块的初始化,图4.61给出了使用self.modules()完成初始化的程序示例。第9~13行代码中,对self.modules()中的所有子模块,按照操作类型的不同进行了初始化。使用self.modules()进行初始化的过程遵循深度优先遍历,如遇到Sequential则会继续深入,直到到达最底层子模块。
  • PyTorch使用了动态图机制,调试起来较为灵活方便。针对神经网络模型的调试主要有三种方法:(1)使用Python调试工具;(2)打印模型结构、参数等信息来辅助调试;(3)使用TensorBoard实现数据可视化。其中,针对第一种方法,主要使用Python的交互式调试库pdb。可以针对PyTorch程序代码,实现断点设置,单步执行,代码、变量、参数查看等调试功能,具体使用方法可参考文献[插图]。针对第二种方法,可以直接在代码中使用print语句打印模型结构、参数等信息。还可以使用第三方torchsummary库[插图],逐层打印模型的形状、参数量等信息。根据打印出来的神经网络结构、形状、参数等信息,来检查程序中所定义的神经网络结构是否正确。图4.63给出使用该方法的程序示例,第5行代码使用print语句打印神经网络模型,第7行代码利用torchsummary库中的summary语句来打印神经网络模型。
  • 图4.63 使用torchsummary库示例
  • 第三种方法是使用TensorBoard实现数据可视化。TensorBoard原本是另一个编程框架TensorFlow中的可视化工具,现在也可以以独立第三方库的形式加载到PyTorch程序中。TensorBoard提供了对神经网络模型结构、参数等的可视化功能,可用于查看、分析神经网络模型的结构、权重、损失值、准确率等[插图],从而对构建的神经网络模型进行调试。PyTorch中提供了torch.utils.tensorboard模块,用于加载TensorBoard工具,实现对神经网络模型及张量的可视化。使用时,主要通过其中的SummaryWriter类来实现数据的可视化。SummaryWriter类定义了多种在TensorBoard中添加数据显示的方法,如表4.17所示。
  • 图4.64 图4.63的输出结果
  • 模型优化的思路主要是减少模型的计算量和参数量。一方面,可以通过对模型中的神经元、突触进行剪枝来减少模型的计算量,从而减少在硬件平台上运行模型所需要的计算单元数量,提升模型在硬件平台上的运行性
  • 能。另一方面,还可以通过量化技术将模型参数由高位宽的浮点数据转换为低位宽的定点数据,这样在硬件平台上运行时,可以使用低位宽定点计算单元代替高位宽浮点计算单元,提升计算单元的计算速度,同时,应用量化技术能够大幅减少模型参数占用的存储空间,进而使得硬件平台上的访存数据量减少,能够进一步提升访存的速度。针对上述两种优化方法,PyTorch均提供了相应的支持:使用torch.nn.utils.prune模块对神经网络模型进行剪枝操作,使用torch.quantization.quantize_dynamic()函数对神经网络模型进行动态量化。
  • 按照剪枝规则的不同,常用的剪枝方法可以分为非结构化剪枝和结构化剪枝两种。非结构化剪枝是按一定规则对单个参数进行裁剪;结构化剪枝是按一定结构规则对一组参数进行裁剪,如裁减掉卷积核的某一行或某一列,或裁减掉一个卷积核,或裁减掉一个通道的所有卷积核,分别如图4.66a、图4.66b和图4.66c所示。
  • 图4.67 全局剪枝示例
  • 神经网络模型量化,将权重和(或)激活值从高位宽的浮点数转换为低位宽的定点数,详见3.5节。量化参数(即缩放系数)与待量化的张量的数值范围有关。模型中的权重参数,在进行量化时,其数值范围已经是固定的,因此量化时对应的缩放系数也是固定的。动态量化是指每一层激活值的数值范围随计算过程变化,需要动态确定缩放系数,并根据缩放系数动态地完成数据格式转换。
  • PyTorch中提供了torch.quantization.quantize_dynamic()函数用于模型的动态量化[插图]。使用该函数量化后的数据,可以进行定点类型的乘法、卷积计算,其速度要快于浮点类型的计算。图4.68给出了使用torch.quantization.quantize_dynamic()函数进行动态量化的程序示例。第10~14行代码定义了动态量化的模型对象、操作对象以及量化后的数据格式。

 4.5 基于PyTorch的模型训练实现

  • 在torchvision.datasets包中提供了一些常用数据集,以及面向数据集的常用操作[插图],可以在训练时直接加载使用。表4.19列出了其中包含的典型数据集,所有的内建数据集均继承了torch.utils.data. Dataset基类,且均已定义好了__getitem__和__len__方法,可以直接用来构建数据集。
  • 数据集加载的形式为:torch.util.data.DataLoader(dataset, batch_size=1, shuffle=None,
  • num_workers=0)。其中,batch_size表示加载数据时一次加载多少样本,shuffle参数为true表示每个epoch数据都需要重新乱序排列一次,num_workers表示加载数据时使用多少个子进程。根据训练时对数据集加载方式的不同需求,对该DataLoader函数中的参数进行设置。
  • 使用PyTorch进行模型训练时,首先需要定义损失函数的计算方法,然后构建优化器,迭代的计算损失函数对于模型参数的梯度,并选择合适的优化算法实现对模型参数的更新。在模型训练的过程中,可以利用PyTorch内建的性能分析工具、梯度检查函数等,来验证梯度计算过程的正确性和有效性。
  • 表4.20 内建损失函数
  • PyTorch提供torch.optim包来实现多种梯度优化算法,在torch.optim包中集成了目前常用的优化方法,可以根据算法特点来选择使用。
  • 反向传播时需要使用计算图,此处计算图的主要作用为在前向计算过程中保存所有中间节点的计算结果,以便于反向传播时构建反向传播路径,并利用链式法则完成计算图中各个节点的自动求导。如4.3.4节所述,PyTorch中构建的是动态图,其生命周期为训练过程中的一次迭代(iteration)。计算图在一次反向传播后会被立即销毁,同时释放存储空间,下次调用时需要再次创建计算图。因此,只有在训练时计算图是必需的,因为需要利用计算图先前向计算得到中间计算结果,再反向计算。而如果只是单纯的推理,可以选择禁用计算图,从而节省存储空间的占用和资源的消耗。
  • 如果需要禁用计算图,可以使用torch.no_grad上下文管理器,在其作用域内定义的所有计算,仍然可以前向传播得到计算输出,但不会反向传播计算梯度,也不会创建计算图,如图4.71所示。第1行代码定义了张量my_data1,其requires_grad属性为True,因此会构建计算图;第2行代码使用了torch.no_grad上下文管理器,其作用域为第3行代码,在该作用域下定义的张量my_data2,其requires_grad属性为False,即不需要计算梯度,且该节点不会被添加到计算图中。
  • 对于requires_grad属性为True的张量,如图4.71中的my_data1,其前向计算过程中的后继张量,除了使用no_grad管理的,其他张量均默认requires_grad=True,即需要计算梯度,且需要创建计算图。对于需要计算梯度的张量,可以使用tensor.backward()函数计算该张量相对于计算图中所有叶节点的梯度。每调用一次tensor.backward()函数之后,计算图就会被销毁,对应资源会被释放掉。
  • 在反向传播时,仅有requires_grad=True的节点才会计算梯度,而其中仅有叶节点张量的.grad属性(即其梯度值)会被保存在内存中。非叶节点张量如果想保留.grad属性,需要设置其retain_grad属性为True。
  • 如果想直接知道一个张量是不是叶节点张量,可以通过tensor.is_leaf函数来查看,如果函数返回True则表示该张量为叶节点张量。
  • 图4.73给出了使用detach()方法修改计算图的程序示例。其中,my_data2是my_data1的后续节点,其requires_grad属性与my_data1相同,也为True,因此my_data2会默认加入计算图;而my_data3由于定义在torch.no_grad的作用域内,因此其不被添加到计算图中,requires_grad属性为False; my_data4与my_data2共享内存,但其不在计算图中,且其requires_grad属性为False。
  • 在使用loss.backward()计算梯度时,如果前向传播过程中使用到的计算操作是PyTorch中的内建计算函数,则PyTorch能自动调用各函数对应的梯度计算函数,完成自动求导。torch.autograd包提供了用于自动求导的类
  • 和函数。
  • 在图4.74的示例程序中,使用了@staticmethod方法来标记前向和反向计算方法。@staticmethod是Python中的一种修饰符,用于标记静态方法。当一个方法被标记为静态方法,就可以在不进行实例化的情况下,直接调用该方法,如可以使用“my_sin.forward”来直接计算自定义操作的前向计算结果,从而提高代码的灵活性和可重用性。第4~14行代码描述了自定义操作类my_sin的计算方法,my_sin继承自torch.autograd.Function,而torch.autograd. Function是PyTorch中自定义自动求导操作的基类。my_sin的前向和反向计算方法均通过@staticmethod修饰符标记为静态方法。在前向计算方法forward()中,首先给出my_sin的前向计算方法,然后使用save_for_backward函数来保存在反向计算时需要用到的所有张量,save_for_backward函数仅能在forward()方法内部调用,且至多调用一次。而在反向计算方法backward()中,首先使用saved_tensors函数来获取前向计算阶段保存的张量,再完成梯度计算。
  • 完成梯度计算方法定义后,使用PyTorch提供的优化器torch.optim包来更新模型参数。使用时,首先须构建优化目标,其次计算预测值,根据预测值和优化目标计算损失函数,然后对参数梯度清零,计算损失函数关于所有参数的梯度,并优化梯度、更新模型参数。PyTorch中所有优化器的基类是torch.opim. Optimizer(params,defaults)模块,其中,params为需要优化的模型参数列表,defaults为包含了如learning rate等优化选项的字典。所有的梯度优化方法均继承该基类,常用梯度优化算法包括Adadelta、Adagrad、Adam、RMSprop、SGD、LBFGS等。优化器支持的常用操作如表4.21所示。
  • 而在优化器的状态字典,即torch.optim.state_dict中,记录了优化器的状态、待优化参数、优化器中使用的超参数等。图4.79给出了优化器的状态字典使用示例,该程序的执行结果如图4.80所示。
  • 保存模型可以使用torch.save()函数。PyTorch官方推荐的使用方式是仅保存模型的状态字典,保存为扩展名为pt或pth的文件,然后再使用model.load_state_dict()恢复模型。图4.81给出保存模型状态字典的程序示例。
  • 模型的状态字典会随着模型的训练过程的进行而更新,因此,如果想保存某个指定的状态字典,需要使用图4.82所示方法。第1行代码对当前状态字典及其子对象进行深度拷贝,第4行代码对拷贝的状态字典进行保存。
  • 当需要恢复模型时,首先完成模型的实例化,再使用model.load_state_dict()装载模型参数,如图4.83所示。
  • 当然,用户也可以自定义保存的内容,如将模型的状态字典、优化器的状态字典、epoch、损失值等一起保存为检查点,检查点文件的保存形式通常是扩展名为rar的文件,图4.84给出了保存模型的检查点文件的程序实例。
  • 当需要使用这些保存信息时,可以使用torch.load()函数来恢复检查点文件中的参数,恢复后的参数可用于推理或继续训练,用法如图4.85所示。第5行代码将保存的检查点文件恢复并保存到checkpoint中,第6~9行分别读取检查点文件中保存的epoch、model_state_dict、optimizer_state_dict和loss值。

 4.6 驱动范例

  • 首先使用PIL方法读入图像(第11行)​,然后将该图像转换为(C, H, W)格式的张量,再增加第0维,将张量图像的数据格式转换为(1, C, H, W)(第12行
  • 在非实时风格迁移算法中,会创建一个与内容图像尺寸相同的随机图像,该图像和内容图像共同提取内容特征,计算内容损失,同时和风格图像共同提取风格特征,计算风格损失,内容损失和风格损失加权后得到总的损
  • 失函数。计算总损失函数对输入图像每个像素的梯度,并运用梯度下降算法来迭代地优化输入图像。最终训练完成的输入图像,即为风格迁移后的图像。
  • 如4.6.3节所述,分别对内容图像和输入图像计算内容损失,对风格图像和输入图像计算风格损失,然后对内容损失和风格损失进行加权计算,得到总损失函数。由于需要将内容图像、风格图像和输入图像分别作为VGG19网络的输入来提取内容特征和风格特征,而计算时仅会实例化一个VGG19网络,因此在损失函数定义时,需要进行特别的设计。图4.90给出了计算内容损失的程序示例,由内容图像、输入图像分别通过conv_4层得到的特征图计算均方误差。因此,内容损失计算函数ContentLoss会作为一个新的模块,添加到conv_4层后面。第4行代码中,输入图像经过conv_4层后会得到提取出的内容特征,该输出应为一个状态值,而不应是一个变量,否则在forward方法中计算损失时会报错,因此,第4行代码中,使用detach()函数将self.target从当前计算图中剥离,第7行代码计算内容图像和输入图像分别提取了内容特征后的均方误差损失
  • 第37~40行代码将非实时风格迁移网络model中,内容损失计算函数层ContentLoss、最后一个风格损失计算函数层StyleLoss之后的层全部去掉。

 第5章 编程框架原理

  • 深度学习编译技术,结合图层级和算子层级的编译优化方法,最大限度地发挥硬件计算能力,提高应用程序的性能

 5.2 计算图构建

  • 具体来说,编程框架通常用计算图来建模计算流程,这个流程描述了输入数据在模型中的流动和计算过程。计算图由两个基本元素构成:张量(tensor)和张量操作(operation)。计算图一般以有向无环图(DirectedAcyclic Graph, DAG)的形式表示,有向边指明了张量的流动方向,如果一个操作的输出张量是下一个操作的输入张量,则在这两个操作之间建立一条有向边;如果一个操作的输入来自外部,或输出指向外部,则可显式标记为计算图整体的输入或输出张量。通过计算图,我们可以建模神经网络计算中的具体行为,并通过拓扑排序等方式,将图转化为线性的执行序列,使其在硬件平台上执行。
  • 正向计算图的建立方向是从输入张量到最终的输出张量,而反向计算图的构建则是从输出张量开始回溯,建立梯度节点并指向正向的算子节点。
  • 正向计算图的两种构建形式:动态图和静态图。动态图在执行函数时,按照函数顺序通过解释执行的方式,逐条语句地生成节点并立即计算返回结果;而静态图则在执行计算之前,通过编译构建好所有图上的节点,在图运行时再计算整个计算图并返回最终结果。在此前的学习中,我们介绍了这两种计算图的取舍及其带来的优缺点:静态图性能较好,但使用难度较大;动态图易于使用,但性能优化方面受限。下面介绍两种计算图构建方式的机理和产生如上差异的原因。
  • 首先,在动态图模式下,用户可以在图执行过程中使用断点或打印张量进行调试,而静态图不行。此外,相较动态图的语义,使用静态图模式提供的控制流节点来实现发展较快的动态神经网络[1]会更为复杂。
  • TensorFlow使用了一套领域特定语言(DSL)内嵌在Python中为用户提供定义计算图的接口。在定义和编译后,用户调用tf. Session()来执行计算图。这是TensorFlow 1.x中的默认执行过程,又被称作图(graph)模式。
  • TensorFlow使用若干基本控制流算子的不同组合来实现各种复杂控制流场景[插图]。为了方便不同语言使用这种控制流作为后端,控制流算子应具备灵活和表达能力强等特点。同时,控制流算子应能很好地实现TensorFlow当前的计算图、分布式执行和自动求导等功能。TensorFlow的控制流设计原理参考了Arvind和D. Culler的数据流图机制[插图],提供了5个基本控制流算子,如图5.4所示,主要包括Switch、Merge、Enter、
  • 控制流算子应能很好地实现TensorFlow当前的计算图、分布式执行和自动求导等功能。TensorFlow的控制流设计原理参考了Arvind和D. Culler的数据流图机制[插图],提供了5个基本控制流算子,如图5.4所示,主要包括Switch、Merge、Enter、
  • Exit和NextIteration。其中Switch和Merge组合可以实现条件分支功能,而5个算子一起使用可以实现条件操作及循环操作。
  • 为此,PyTorch 2.0的开发者引入即时编译(Just In Time, JIT)的思想,考虑在运行时进行编译,通过图捕获技术将用户编写的动态图转化为静态图。当用户调用一个正向函数时,这一函数不会被立即执行,而是被PyTorch的图捕获模块TorchDynamo获取,其对该函数的内容进行分析,将PyTorch相关的代码片段交由编译器编译,然后使用编译后的函数替换函数调用,再进行执行。当面临包含复杂控制流的动态图时,需要对图进行切分,通过Python的动态语义处理控制流部分,静态部分通过即时编译转化成静态图。同时,编译后生成的静态图结构将被缓存。在未来的代码执行上,如果静态部分相同,则可利用此前缓存的计算图;如果静态图结构发生了变化,则会重新进行即时编译。这一复杂的过程可以被理解为,不是整个逻辑上的计算图都被编译成了静态图,而是通过控制流将计算图进行了子图划分,子图内部是静态图,而子图与子图之间是动态图。
  • 深度学习中通常使用梯度下降法,通过反向传播来更新模型参数。
  • 当前主流的框架往往以正向计算图为输入,通过自动求导的方法完成反向计算图的构建。
  • 在PyTorch 1.x中,动态计算图技术是其核心特性之一。这种技术允许在正向传播过程中动态且即时地构建计算图。在训练模式下进行正向传播时,每当一个算子被调用执行,AutoGrad模块会同时创建对应的反向算子。然后,创建的反向算子作为反向节点,被加入到反向计算图中。当正向传播完成后,整个反向计算图也随之构建完成。反向计算图中的叶子节
  • 和根节点本质上是正向传播的输入张量和输出张量,如图5.5所示。当用户调用backward函数时,反向计算图便开始执行,从而进行梯度的反向传播和计算。在推理模式下,为了优化性能并减少内存占用,AutoGrad模块并不会在正向传播中构建这些反向节点。
  • backward函数的主要功能是根据输入配置反向传播的计算图并执行反向传播,总体分为准备输入参数、初始化输出边列表和实际的反向计算图执行三个步骤。首先,PyArg_ParseTupleAndKeywords函数将传入的Python参数转换为C++对象。
  • 第58行代码调用engine的execute方法,执行反向计算图获得最终的导数outputs,完成反向传播计算。

 5.3 计算图执行

  • 为了支持设备管理,PyTorch在pytorch/c10/core/impl/DeviceGuardImplInterface.h中定义了抽象的设备管理类DeviceGuardImplInterface,如图5.7所示。在DeviceGuardImplInterface类中,提供了统一的设备管理的抽象接口,包括设备操 作、执行流管理和事件管理的函数接口。例如,调用getDevice()接口可以获取当前设备的标识符,通过synchronizeStream()接口可以完成执行流同步,通过destroyEvent()接口可以完成事件的销毁等。在实现了基本的抽象设备管理类后,常见的计算设备(包括CPU、GPU和DLP)都可以通过对该设备抽象类进行继承实现设备子类,并将该设备提供的设备管理方法添加到设备抽象类成员函数的具体实现之中,从而完成该设备的注册和实现。在计算设备完成注册和实现后,框架即可在运行时正常调用该设
  • 图5.7 PyTorch中的抽象设备管理类DeviceGuardImplInterface的定义
  • 张量可以被看作N维数组,可以灵活表示标量、向量、矩阵等不同维数的数据。除原始数据外,张量有一些基本属性,包括形状、布局、步长、偏移量、数据类型和设备等。这些基本属性可以统称为张量数据结构的逻辑视图。张量数据结构的逻辑视图是编程框架使用者在软件层面上能够直接控制和表达的一些基本属性。对于编程框架开发者来说,还需要维护张量数据结构的另一种视图——物理视图。张量数据结构的物理视图主要包括在设备上的物理地址空间大小、指针、数据类型等属性。物理视图是编程框架底层需要维护的基本属性,对编程框架使用者是不可见的。表5.2对比了张量数据结构的逻辑视图和物理视图的基本属性。
  • 值得注意的是,这样的切片操作并没有隐式地创建一个新的张量并拷贝,而是提供了原本物理视图下的一个新的逻辑视图,它的物理视图仍是物理地址空间中从0x10位置开始连续存储的一块数据。但是,由于步长等于2,在进行物理地址空间寻址时,每访问一个元素都要跳跃2个元素(也就是每间隔一个元素取下一个元素)​,从而对应0x10和0x18两个位置的数据。
  • 此时,需要在逻辑视图中额外引入偏移量的属性,记录这个新的逻辑视图对应到物理视图上数据实际开始的位置。
  • 具体来说,张量数据结构的物理视图确定了张量在设备上实际的数据大小、物理地址指针;而逻辑视图提供了对同一份物理地址空间中数据的不同解释方式。在逻辑视图中,通过两个关键变量(偏移量和步长)来确定张量对应的物理视图中物理地址空间的寻址方法。由于张量的物理视图和逻辑视图之间存在一对多的关系,在编程框架
  • 张量数据结构的实现中,有必要对张量的逻辑视图和物理视图进行解耦。
  • PyTorch通过张量(Tensor)抽象类和存储(Storage)抽象类来分别表示张量数据结构中的逻辑视图和物理视图。这两个抽象类的具体实现在pytorch/c10/core/TensorImpl.h和pytorch/c10/core/StorageImpl.h中,如图5.9和图5.10所示。TensorImpl类提供了对张量抽象的实现,包含维度、步长、数据类型、设备、布局等逻辑视角的张量属性。StorageImpl类则提供了对张量的存储实现,包含内存指针、数据总数等物理视角的张量信息。值得注意的是,在TensorImpl类中,第11行明确声明了storage_作为私有成员变量,不同的TensorImpl类可以引用同样的StorageImpl对象,这也进一步说明了Tensor类和Storage类之间的多对一映射关系。
  • 图5.9 PyTorch中的张量数据结构逻辑视角类TensorImpl的定义
  • 从逻辑视图到物理视图的转换需要完成对张量的内存分配,即对张量进行内存管理。在物理视图表示中,StorageImpl调用结构体Allocator(内存分配器)
  • 进行张量数据空间的分配,见图5.10第14行。内存分配器的代码实现与后端设备相关,通过继承来支持不同设备的张量内存管理方法。张量内存管理主要分为即时分配和内存池分配。
  • 图5.10 PyTorch中的张量数据结构物理视角类StorageImpl的定义
  • 在领域专用处理器(如GPU和DLP)上,需要使用处理器的运行时接口来管理设备端内存,这使得张量内存管理变得更加复杂。在这种情况下,通常会通过内存池的方式手动管理设备端的张量内存,如图5.12所示,该段代码实现了在CUDA平台上手动管理设备端上的张量内存的功能。相比调用系统API直接分配内存,这种做法不仅减少了系统调用的开销,还具备两个重要优势:节约设备内存使用并减少设备内存碎片化问题。
  • Tensor类由TensorBase类继承而来。TensorBase中包含唯一的成员变量impl_,相当于一个指向TensorImpl的指针,并表达了前述张量数据结构中的逻辑视图,如张量的形状、布局、步长、偏移量、数据类型和设备等。TensorImpl还包含了指向Storage数据结构的成员变量,并通过StorageImpl表达前述张量数据结构中的物理视图。在StorageImpl中,包含了指向Allocator数据结构的成员变量,用于进行张量内存管理。
  • 在CPU上创建一个新的张量时,会调用empty_cpu函数进行张量的初始化。首先根据固定内存标志(pinned_memory)选择相应的分配器(allocator),在这里使用的是CPU内存分配器(即时分配)​。然后设置DispatchKey为CPU,并调用empty_generic函数进行具体实现。empty_generic实现了通用的张量创建逻辑:首先分配存储空间,其次创建张量并设置形状,最后调整内存布局。在其他硬件后端(如GPU或DLP)进行张量初始化,只需要选择相应的分配器并且设置对应的参数,然后调用empty_generic函数进行实现即可。
  • 计算图的执行过程可以被分解为每个算子单独执行的过程。首先,通过计算图生成一个执行序列,该序列确定算子的执行顺序,以确保正确的数据流和依赖关系。然后,针对每个算子进行算子实现,包含算子注册前端实现、后端实现三个步骤。最后,进行分派执行,包括查找适合给定输入的算子实现,并调用相应的实现来执行具体的计算任务。
  • 注意拓扑排序算法可以有多个不同的结果,因为在具有相同入度的节点中可以任意选择下一个执行的节点。因此,同一个计算图可能存在多个合法的执行序列。
  • 图5.15以算式f(x1, x2)=cos(x1)+x1·x2-exp(x2)为例,介绍其构建的计算图对应的正向传播算子执行序列(反向传播的算子执行序列与正向传播完全相反)​。图中上侧是由算式构建得到的计算图,下侧是通过拓扑排序算法获得的某条合法的执行序列。值得注意的是,在拓扑排序算法中,如果同时存在多个入度为0的节点,这些节点代表的算子可以在计算资源满足的条件下并发执行。
  • 深度学习编程框架中算子实现过程可以总结为以下三个步骤:(1)算子注册:开发者在编程框架中注册算子信息,包含算子的输入、输出以及相关的前端接口定义,并声明该算子的多种后端核函数[插图]定义。(2)前端实现:在前端实现阶段,开发者使用Python编程语言,编写算子的前端实现代码,作为编程框架的前端用户接口。(3)后端实现:在后端实现阶段,开发者使用C++或其他高级编程语言,编写算子的底层实现代码,完成算子的计算逻辑部分实现。
  • PyTorch提供了一种管理整个算子实现模块的高效模式,称为native_functions模式。在使用该模式进行算子实现时,首先需要修改配置文件native_functions.yaml以添加算子配置信息,进行算子注册,如图5.16所示。其中,func字段定义了算子名称和输入、输出的参数类型,variants字段表示需要自动生成的高级方法,dispatch字段表示该算子所支持的后端类型和对应的实现函数。PyTorch基于这种设计模式,再
  • 辅以一个名为gen.py的自动生成工具[插图],生成不同模块之间的胶水代码,就可以完成算子实现。
  • PyTorch使用算子分派机制来管理前后端对应关系,分派器(dispatcher)在分派机制中扮演着调度和控制的角色,确保在不同的后端环境中选择正确的实现方法。分派器管理的分派机制中,主要有三个对象参与:(1)算子:分派器的调度对象。算子可以是各种操作,如加法、乘法、卷积等,描述了具体的计算任务。(2)分派键(dispatch key):分派键可以简单地理解为与硬件平台相关联的标识符,比如CPU、GPU和DLP等。通过使用分派键,PyTorch能够根据张量的特性选择对应的核函数进行计算,以提高性能和效率。(3)核函数:特定硬件平台上实现算子功能的具体代码。在每次算子调用时,根据指定的分派键,分派器会将控制流转到相应的核函数上执行实际的计算。
  • 分派器使用分派表维护三者之间的关系,分派表的表项记录着算子到具体的后端实现的对应关系。分派表可以视为一个二维网格,纵轴表示PyTorch所支持的算子,横轴表示支持的分派键(与后端相关的标识符),该表初始为空。当添加算子到某个后端实现的对应关系时,需要编写TORCH_LIBRARY_IMPL函数实现注册。如图5.17所示,TORCH_LIBRARY_IMPL函数实现了在单个分派键(CPU)上注册算子(prelu)对应的后端实现(cpu_prelu)。
  • 如果算子的正向传播函数是通过PyTorch原生算子(如ATen)组装出来的,用户不需要关心反向传播函数的实现,而是通过编程框架的自动微分机制保证。如果用户想针对特定后端专门优化自定义算子实现从而获得最佳性能,则需要在tools/autograd/derivatives.yaml文件中手动维护正向传播算子和反向传播算子之间的关系。
  • 1.算子注册图5.18展示了PyTorch中PReLU的算子注册配置文件(即native-functions文件)。图5.18的3-5行定义了PReLU算子。该算子接受两个张量作为输入,self表示输入张量,weight表示该算子的可学习参数。该算子属于构建神经网络时的激活函数,根据variants字段,该算子会自动生成到torch.nn.functional模块中。第7~19行注册了prelu正向传播函数、反向传播函数,以及它们在不同后端平台上的核函数名称。
  • 然后,需要在配置文件derivatives.yaml中添加算子正向传播函数和反向传播函数的对应关系,如图5.19所示。代码段表明正向传播函数_prelu_kernel对应的反向传播函数是_prelu_kernel_backward。
  • [插图] 图5.19 PReLU算子正向传播函数对应的反向传播函数 2.前端实现 PReLU算子的前端实现代码如图5.20所示,这段代码实现了PyTorch中PReLU类的定义,包括PReLU类的相关描述、初始化函数以及正向传播函数。 至此,就得到了PReLU算子的前端使用接口torch.nn. PReLU,并定义了算子的实现函数对应关系。调用PReLU算子时,会跳转到torch.nn.functional.prelu函数(即F.prelu函数),该函数会调用torch.prelu。torch.prelu的实现和算子实现中后端实现部分相关。在编译构建PyTorch源码时,gen.py会自动生成与PReLU相关的文件,并将其添加到其他文件的引用中,以确保在编译和执行过程中能够正确识别和调用该算子。
  • 2.前端实现PReLU算子的前端实现代码如图5.20所示,这段代码实现了PyTorch中PReLU类的定义,包括PReLU类的相关描述、初始化函数以及正向传播函数。至此,就得到了PReLU算子的前端使用接口torch.nn. PReLU,并定义了算子的实现函数对应关系。调用PReLU算子时,会跳转到torch.nn.functional.prelu函数(即F.prelu函数),该函数会调用torch.prelu。torch.prelu的实现和算子实现中后端实现部分相关。在编译构建PyTorch源码时,gen.py会自动生成与PReLU相关的文件,并将其添加到其他文件的引用中,以确保在编译和执行过程中能够正确识别和调用该算子。
  • 图5.21是PReLU算子的表层实现代码。通过DEFINE_DISPATCH(*_stub)定义了stub函数,并实现了_prelu_kernel()和_prelu_kernel_backward()函数。这两个函数的代码结构相似,首先创建一个空的返回对象,然后通过配置TensorIteratorConfig来创建一个TensorIterator(即iter),用于迭代张量操作。iter提供了统一的计算抽象,其封装了正向计算的输入input、权重weight,以及反向计算的梯度grad。核函数(即上文的_kernel函数)只需要接受并计算这些输入,极大地简化了函数实现。接下来,调用stub函数(prelu_stub或prelu_backward_stub)进行实现,完成PReLU算子的运算。最后,返回计算结果。需要注意的是,上述代码中定义的实现只是一个封装,没有完成真正的实现,还需要根据后端硬件编写对应的底层实现。
  • 图5.22是PReLU算子的底层实现代码,这里是以CPU为硬件后端的算子实现代码。其中,prelu_kernel()函数实现了PReLU算子的正向传播,prelu_backward_kernel()函数实现了PReLU算子的反向传播。这两个函数都利用了SIMD指令实现向量优化,优化了在CPU上的执行效率。完整的代码文件在aten/src/ATen/native/cpu/文件夹下。最后通过REGISTER_DISPATCH为正向传播stub函数和反向传播stub函数注册具体的核函数。这种表层实现和底层实现相解耦的设计,使得不同后端注册核函数时可以复用相关的接口。以GPU为硬件后端的PReLU算子实现代码需要以CUDA编程语言来编写,来充分利用GPU的硬件资源,感兴趣的读者详见aten/src/ATen/native/cuda/Activation-PreluKernel.cu。
  • 分派器首先根据输入张量和其他信息找到对应的分派键,然后由该分派键找到并调用相应的核函数。从源码角度出发,每个算子对应一个定义在Dispatcher.h中的Operator-Handle实例,即一个OperatorHandle实例负责处理一个算子。每个OperatorHandle又对应多个KernelFunction,每个KernelFunction代表一个特定硬件后端的核函数。当分派表中已经存在需要分派的算子指针时,就可以直接进行分派执行;如果不存在,就需要根据分派键将对应的算子实现注册到分派表中(见算子实现中的算子注册)。
  • 分派执行过程需要使用分派器中两个重要的方法:查找方法(即findOp函数)和调用方法(即call函数)。1.查找方法图5.23展示了分派器的查找方法。以OperatorName作为键访问operator LookupTable_,查找并返回与算子相对应的OperatorHandle表项。operatorLookupTable_是分派器中一个重要的成员变量,保存了OperatorName到OperatorHandle的映射关系。
  • 2.调用方法图5.24展示了分派器的调用方法。根据查找到的算子获取对应的dispatchKeySet(64比特的数组,每一个比特都代表了一个分派键),再根据dispatchKeySet从op.opera-torDef中查找得到对应的核函数,并选择其中优先级最高的分派键对应的核函数。
  • 算子执行的流程可以总结为:首先由计算图获得算子执行序列,然后使用native_functions模式定义算子并注册到分派表中,最后使用分派器中的查找方法和调用方法找到并调用对应的算子实现,完成算子执行流程。

 *5.4 深度学习编译

  • 常见深度学习编程框架中采用的编译技术和深度学习编译器,包括TVM、Tensor Comprehensions、XLA、MLIR、TorchDynamo和TorchInductor。
  • 开发者希望在获得便利的同时,能够更有效地利用深度学习硬件,加快训练/推理速度。因此,近年来深度学习编译技术被逐渐引入编程框架。这有以下两点好处:• 减少人工开发工作量:深度学习编译技术可以针对不同硬件平台进行代码生成,减少人工为不同的硬件平台后端开发并调优算子的烦琐工作量。这使得神经网络模型能够快速在包括CPU、GPU和DLP在内的各种硬件上迁移,提升了编程框架的跨平台支持能力。• 便于性能优化:在计算图层级,针对静态图,通过提前编译(Ahead Of Time, AOT)方法,可以对完整的计算图进行静态分析和全局优化,能够显著提升计算图的执行效率。针对动态图,通过即时编译(Just In Time, JIT)方法,在运行时进行动态编译优化,相较直接执行的动态图而言,也有了更多的性能优化的机会。在算子层级,通过自动调优技术,可对神经网络当前使用的算子类型和形状进行针对性调优,充分利用硬件的计算和存储资源,最大限度提升硬件利用率,从而提升神经网络整体的性能。
  • 广义的“编译”是将某一输入格式的信息转为另一格式。在计算机技术中,传统编译通常是指高级语言到机器码的变换。在智能计算系统中,深度学习编译未必涉及机器码生成,但其性质同传统编译技术是一致的:将高度抽象且未经优化的代码表示,也就是前文中介绍的计算图,转为对应硬件后端且性能更好的代码,如GPU上的CUDA代码和DLP上的智能编程语言BCL代码等。具体来说,深度学习编译器接收以计算图形式表示的深度学习任务,并在指定硬件平台上生成高性能代码。
  • 图层级中间表示用于描述和目标平台无关的运算过程,图的节点是计算任务(例如卷积等)​,边则表示张量的流动。编译器会基于图层级中间表示进行目标平台无关的优化,例如子图替换、常量折叠、公共子表达式消除、布局优化和算子融合等。接下来,编译器将优化后的计算图的节点进行算子层级的自动调优,并在算子层级中间表示(如LLVM中间表示)上进行优化,最终生成目标平台的高性能可执行代码。
  • 常见的深度学习编译器包括TVM[插图]、Tensor Comprehensions[插图]、XLA[插图]、MLIR[插图]等。其中,TVM继承了Halide[插图]中计算与调度相分离的思想,并引入自动调优技术,从而可以实现对计算密集型任务及其融合子图的调度;Tensor Comprehensions基于多面体编译技术实现CPU和
  • GPU上的自动调度;XLA源自TensorFlow,主要针对计算图中的非计算密集型算子进行融合和代码生成;MLIR是LLVM原作者Chris开发的编译器框架,其目标是提供一套可复用的工具,从而解决编译器的“碎片化问题”​,降低领域特定编译器构建的代价;PyTorch 2.0中引入了以TorchDynamo和TorchInductor为核心的深度学习编译技术,使得用户无须修改原有代码,通过一行torch.compile()就能获得性能提升。
  • 深度学习编译器将深度学习框架传递的计算图转换为自己的内部中间表示,经过图层级优化和算子层级优化后,自动生成在目标硬件平台上的高性能算子,显著降低了人工算子开发的工作量和框架的维护成本。值得注意的是,深度学习编译器的引入是渐进式的,并没有完全取代传统流程。在目前的实践之中,典型算子仍需要厂商提供的计算库或者手写算子来达到极致的性能,深度学习编译器则针对长尾算子(指神经网络中大多数的算子类别,其出现频次不高,很难被计算库或手写算子覆盖)在灵活性和高效性上进行了折中。
  • 编译器遍历图的中间表示,通过各种优化途径,在不改变图的运算语义的前提下提升计算图的执行性能。通常而言,编译器所能获取的图信息越全面,优化空间越大。TensorFlow使用静态图定义,可以对整个计算图做高效的全局优化。PyTorch先前的版本采用动态图机制,在计算图上的编译优化能力比较弱。自Py-Torch 2.0开始,其通过在运行时将动态图转化为静态图,也能对子图进行充分的图层级编译优化。
  • 图层级的中间表示可以分为两类:基于有向无环图的表示和基于let绑定的表示。
  • 由于基于有向无环图的表示并未明确告知编译器何时求值,编译器采取立即(eager)策略在使用语句声明时就求值,或是采用惰性(lazy)策略在之后访问该变量时才求值,都是可行的。基于let绑定的方式可以有效避免这种语义混淆。当使用let关键词定义一个表达式时,编译器会产生一个let节点,该节点同时指向表达式的操作符(即算子)和变量(即张量)​。因此,在使用let关键词定义表达式时,求值的顺序也就唯一确定了。编译器生成的代码将总是在let节点处对表达式进行求值。
  • 通常而言,基于有向无环图的表示被用在输入层级的计算图描述上,而基于let绑定的表示被用在需要明确计算图中变量求值时机的场景中。二者的开发需要采用不同风格的模式匹配,因此开发者需要考虑其应用场景的中间表示是否需要明确变量求值时机,进而确定最终选取何种表示。
  • 对于一个给定的计算图,可行的子图替换策略很多,但并不是所有的策略都能获得性能提升,因此这些框架或编译器通常采用一些人工设计的替换规则进行子图替换。例如,在TensorFlow中,有约150条人工设定的替换规则,其中部分规则如表5.3所示。
  • 一些编译器(如TASO[插图]、PET[插图]、EINNET[插图]等)探索自动生成图替换的方式来取代人工设计。这类方法随机生成大量图替换,然后进行等价性验证筛选出等价的图替换,最后在等价的图替换中选取带来更高性能的替换,从而实现性能的优化。
  • 表5.3 TensorFlow中子图替换规则示例 [插图] 2.常量折叠与公共子表达式消除 常量折叠与公共子表达式消除都是传统编译器中常见的编译优化方法,也应用到了深度学习编译器的图层级优化中。 常量折叠是指,在分析静态计算图的过程中,检测到存在可以被提前计算的常数节点,就用其计算结果生成新的节点来代替原来的常数节点,从而减少运行时的计算量。图5.28中的张量A和张量B都是常数张量,因此A+B可以在编译时计算出来,直接用一个编译时计算出的常数张量TMP替换A+B,从而有效缩短运行时间。
  • 表5.3 TensorFlow中子图替换规则示例
  • 神经网络算子的输入布局影响执行性能。典型的布局形式包括Conv2D运算的NCHW格式与NHWC格式。在DLP的相同输入数据下,采用NHWC格式的性能普遍优于NCHW格式,因此可以通过将NCHW格式的数据转换成NHWC格式进行计算,结束后再转回NCHW格式。
  • 如图5.30所示,可以通过在计算节点前后加上NCHW2NHWC和NHWC2NCHW实现转换。此外,相邻的两个操作进行格式转换后,可以通过布局优化抵消冗余的格式转换运算,从而降低开销。在CPU上也广泛采用布局优化,例如Conv2D运算的OhwI64o4i、OhwI48o4i以及OhwI32o4i都是常见的高性能布局格式,这些布局格式对于CPU的缓存更加友好
  • 将两个算子进行融合,可以避免第一个算子的计算结果写出和第二个算子的输入数据读入的访存开销,在算力大而带宽不足的硬件平台上能带来很大的性能提升。其次,在一些异构计算平台上,小算子的内核启动(kernel launch)开销不可忽略不计,将多个小算子融合成一个大算子,只进行一次内核启动,可显著降低这部分开销。
  • 在计算密集型算子与访存密集型算子的融合中,比较有代表性的形式是卷积运算和激活运算的融合。例如计算密集型算子Conv和访存密集型算子BN、ReLU可以纵向融合成一个称为CBR的算子。这类融合需要由特定的硬件算子库支持。
  • 这种融合方式利用共享存储等片上存储资源,将前一个算子的部分输出存储起来,以供后续计算使用,从而避免了一次写出和读入的访存开销。
  • 在如BERT和Transformer的一些网络中,访存密集型算子计算时间占比可以达到50%以上,因此优化这些访存密集型算子是有意义的。比较有代表性的编译技术工作是AStitch[插图],其以计算密集型算子为边界,将其中的多个访存密集型算子通过共享存储全部融合成一个大的访存密集型算子,从而降低了GPU上内核启动和访存的开销。
  • 对于图层级的优化,通常由编译器使用Pass(遍)机制实现。Pass是指在编译过程中对源码进行遍历,并在遍历过程中对图进行变换,使其在不更改图的运算定义的情况下提升图的运算性能。优化Pass会采用数据流分析等方法对计算图进行分析,并实现上一节中提到的优化方法,对存在的节点进行变换。例如,一个算子融合的Pass如图5.34所示。
  • 算子调度能够针对目标硬件后端上的计算特性和访存特性进行针对性的优化,在不改变算子运算行为的前提下提升算子性能。自动调优技术能够在算子调度的海量程序空间中自动确定最优的调度配置,降低算子调优的人力成本。
  • 常用的算子层级中间表示包括基于多面体模型的中间表示和基于Halide的中间表示。基于多面体模型的中间表示会采用结构化的方法表示循环代码的结构和语义,分析循环依赖信息并通过仿射变换进行循环优化。基于Halide的中间表示采用计算与调度相分离的表示思想。其计算表示涵盖了算子的计算定义信息,但不包括具体的实现方法。调度的表示确定了算子的具体实现方法,并最终下降到一个循环嵌套程序上。我们这里以TVM中的矩阵乘法算子为例,具体介绍基于Halide的中间表示中的计算和调度表示。
  • 在嵌套循环程序上可以进行多种循环变换以实现性能优化,这些变换会对程序表示进行改写。例如,我们可以在该示例的最外层循环增加“parallel”标签。编译时,编译器会识别该标签并为该层循环加上并行化的标注,然后再对该层循环实施并行化。此外,从图5.36可以看出,嵌套循环程序表示并不是简单地将计算表示展开成循环的形式,在此过程中也可能会有一些平台无关的编译优化,例如对于下标的访问进行了公共子表达式消除优化。同一个表示可以用于生成不同的目标语言,例如图5.36既可以生成CPU的代码也可以生成GPU的代码。利用这种嵌套循环程序表示,编译器可以有效地进行目标语言无关的变换。
  • 编译器对算子进行循环变换的过程被称为算子调度,一个完整的调度是由多个调度原语构成的。表5.4列出了一些常用的调度原语。调度原语通常由三部分构成:计算阶段、循环迭代变量以及调度的参数。
  • 我们主要从以下两个方面对矩阵乘法进行性能优化:• 提升缓存命中率。通过循环变换提升矩阵乘法的空间局部性和时间局部性,提升缓存命中率,降低访存开销,从而提升性能。• 使用向量化加速。基于SIMD的向量化,使用LLVM作为后端最终实现性能优化。
  • 首先考虑循环分块(tiling)优化。循环分块优化将整个矩阵乘法的运算分解成更小的子矩阵乘法运算,该方法可以有效提高运算中访存的局部性,提升缓存命中率。如图5.38所示,循环分块是通过对循环进行多次拆分完成的,迭代变量i、j和k分别被拆分了一次,例如i被拆分为i.outer和i.inner。使用k.inner和i.inner实现对子矩阵A的访问,使用k.inner和j.inner实现对子矩阵B的访问,使用i.inner和j.inner实现对子矩阵C的访问。对子矩阵的访问提高了程序的局部性,该程序的运行时间降低为0.136秒。
  • 接着使用循环向量化(vectorize)。循环向量化通过SIMD指令加速无数据依赖的循环操作。如图5.39所示,我们对嵌套循环的最内层循环进行向量化。首先对迭代变量j.inner进行向量化,使用ramp(n)表示向量[1, 2, ..., n];其次改写矩阵C和B的索引;然后通过广播的形式实现标量和向量的计算。经过向量化优化后,该程序的运行时间降低为0.105秒。
  • 还可以通过并行化、循环展开以及写缓存等手段对其进行进一步的优化。
  • 自动调优的典型流程由三部分构成,包括空间探索、代价模型以及性能测量,如图5.40所示。首先,编译器对算子的计算表示进行静态分析,为算子生成调度搜索空间,搜索空间中的每一个点代表一种调度配置。接着,通过搜索算法获得一些调度配置后,再使用代价模型预测这些配置下给定程序的性能。为了节省硬件测试资源,仅选择部分具有较高预测性能的调度配置进行硬件性能测量。然后,选取部分程序在目标硬件上进行性能测量,用测量得到的性能数据来更新代价模型。最终,编译过程达到停止条件时(例如编译时间达到预设值),编译器输出性能最优的程序作为优化程序。相比于硬件厂家提供的手写算子库,自动调优方式可以有效降低新硬件平台软件支持的时间和人力成本。
  • 图5.40 自动调优流程图 自动调优的核心是搜索,搜索研究主要包含搜索空间和搜索算法的设计,这二者是密切相关的。图5.41展示了三种常见的搜索方式。 [插图] 图5.41 典型搜索空间构建方式[插图] 基于手工模板的搜索依赖于一个给定的调度模板。该模板一般是手工设计的原语序列,该序列通常只有调度参数没有确定。由于参数空间较为简单,该方式对于搜索算法没有太多限制,可以使用随机搜索、网格搜索、遗传算法、模拟退火以及强化学习等优化算法进行参数搜索。
  • 图5.41 典型搜索空间构建方式
  • 基于手工模板的搜索依赖于一个给定的调度模板。该模板一般是手工设计的原语序列,该序列通常只有调度参数没有确定。由于参数空间较为简单,该方式对于搜索算法没有太多限制,可以使用随机搜索、网格搜索、遗传算法、模拟退火以及强化学习等优化算法进行参数搜索。这类方式的优势是设计简单、对于新的优化任务灵活性较高。然而这类方式缺点也很明显,需要领域专家设计手工模板,耗时耗力,对于普通用户要求过高。基于序列构建的搜索方式,逐条循环语句地构建优化程序。对于给定的循环语句,编译器需要选择合适的调度原语以及调度参数,并使用代价模型对不完整程序进行性能评估。受限于序列构建的特性,采用的搜索算法存在一定的限制,可以采用的搜索算法包括随机搜索、集束搜索(beam search)、蒙特卡罗树搜索(Monte Carlo Tree Search, MCTS)以及强化学习等。该方式属于一类自动调度方式,无须手工指定调度的模板,且有更大的搜索空间。然而该方式的搜索空间中有许多情况是低效的,这类方式缺少程序优化的先验知识,因而无法对空间进行有效剪枝,加之对于不完整程序的性能估计误差很大,在有限时间内很难有较好的优化效果。层次化构建的搜索方式,按照从粗到细的粒度构建优化程序。在较粗的粒度上,编译器负责决定程序所要采用的循环结构,例如考虑计算节点融合时的融合位置,以及是否使用并行归约。在较细的粒度上,编译器负责决定具体的调度参数,比如循环是否向量化、是否并行化以及循环拆分的长度等。该方式对搜索算法存在一定的限制,通常采用的搜索算法包括随机搜索和遗传算法。该方式也属于一类自动调度方式,相比于手工模板方式有更大的搜索空间,相比于序列构建方式可以将优化的先验知识加入粗粒度的结构选择策略中。然而这类方式对于搜索算法的限制较大,且需要领域专家根据不同的硬件平台单独设计粗粒度的循环结构搜索空间。
  • TVM使用TensorIR作为张量程序的中间表示语言,其变换逻辑实现存放在src/tir/schedule/primitive目录下。其中,Reorder是一种关键的调度原语,它在之前进行矩阵乘法优化时,用于调整因循环分块而产生的迭代变量的顺序。如图5.42所示,调度原语的实施过程分为几个步骤:首先,收集所有引用目标循环的语句,并在调度完成后,更新这些引用指向新的调度循环;其次,搜集目标循环内部的所有引用,并核查数据依赖关系等可能影响调度合法性的因素,以防止调度后出现语义不一致的问题;最后,在确认通过所有必要的合法性检查后,执行目标循环的调度,更新所有相关的引用和变量,确保在整体编译流程中局部调度的正确性得以维持。
  • 图5.42 TVM中的Reorder实现
  • 在实现调度原语后,实现自动调优还需要为构建搜索空间及填入标注(annotation),读者可参考自动调优工作Ansor[插图]中的具体实现。图5.43展示了自动调优的实现逻辑。这套实现逻辑可以实现图5.40所示的自动调优流程,以此驱动编译器中的自动调优。该图将总体实现分为以下几个步骤:(1)模型输入:抽取模型中需要调优的计算,将调度该算子的任务分发到搜索空间生成器。(2)搜索空间生成:接收需要调度的算子,对算子进行静态分析,并通过预设的调度规则为算子产生调度序列,及该调度序列所需的参数取值范围。(3)搜索空间探索:使用代价模型预测生成的算子调度运行性能,通过特定搜索策略选取高性能的调度配置,编译产生程序。(4)性能测量:对编译产生的程序进行性能测试,在指定硬件运行测试并收集运行耗时等性能信息,使用性能信息更新代价模型。若搜索已满足条件,则输出性能最优程序,否则,回到搜索空间中继续选取下一个要被测量评估的程序。
  • 2018年,陈天奇等人提出了端到端的深度学习编译器[插图]TVM(Tensor Virtual Machine,张量虚拟机),能够为CPU、GPU和其他专用的硬件架构提供面向常见深度学习算法的编译优化
  • 在图层级上,TVM能够通过图优化、算子融合等手段,对数据布局和计算图进行优化。在算子层级上,TVM借鉴了针对图像处理领域的调度语言Halide[插图]中计算描述和算法调度相分离的思想,通过张量描述语言来对算子进行计算描述,同时通过一系列调度原语对算子进行调度。其中,张量化(tensorize)是TVM针对GPU的Tensor Core等具备张量运算能力的硬件架构专门设计的调度原语,能够将一组循环嵌套表示的标量计算替换成一条具备张量运算语义的内置函数,然后在代码生成阶段将内置函数直接翻译成目标硬件的张量指令。
  • 同样在2018年,Nicolas Vasilache等人提出了端到端的深度学习编译器Tensor Com-prehensions。它是第一个能够自动生成高性能算子代码的深度学习编译器,而同时代的TVM仍需要手工编写计算和调度。它提供了一种与深度学习任务中所用计算相近的描述语言,以及一个基于多面体模型(polyhedral model)的即时编译器,将描述语言编译生成为具有内存管理和同步的CUDA核函数。
  • 基于上述概念,多面体模型可以使用结构化的方式来捕获和表示循环代码的结构和语义,并可以在这个表示的基础上应用各种优化和变换手段,在保持代码语义不变的基础上提高性能。感兴趣的读者可以阅读相关论文[插图],进一步了解多面体技术是如何被应用于深度学习编译优化的。
  • XLA(Accelerated Linear Algebra,加速线性代数)是谷歌提出的一款为加速线性代数运算而设计的深度学习编译器,并作为TensorFlow的一部分被提供。XLA主要在计算图层级上进行编译优化,从而显著提高神经网络整体的执行速度和效率。它的核心是XLA HLO(High Level Operations,高级运算)IR,是一种图层级中间表示。XLA HLO本质上提供了一系列细粒度的算子抽象,能够用来组合成任意的算子,其上会运行多种与硬件架构无关的分析和优化过程,包括公共子表达式消除、算子融合等。例如,在其他中间表示中作为基本单位的BatchNorm算子,在XLA HLO上可以通过一系列的Broadcast、Reduce和Element-wise操作组成。XLA一般使用LLVM IR作为算子层级中间表示,并在此基础上进行算子层级的优化以生成后端代码。
  • 谷歌提出了OpenXLA项目。图5.45是OpenXLA项目结构图。OpenXLA能接收来自不同编程框架的输入并转化为StableHLO(从XLA HLO发展而来,是一个支持动态、量化和稀疏等不同特性的XLA HLO的操作集合),然后再经过一系列的硬件无关优化和硬件相关优化,最终进行目标硬件代码生成,从而高效运行在不同的后端上。
  • MLIR(Multi-Level Intermediate Representation,多级中间表示)是由LLVM[插图]的核心作者Chris Lattner在总结过去编译器的开发经验后,提出的一种用来构建可重用和可扩展编译器基础设施的方法。MLIR使用一种混合的中间表示,解决当下深度学习编译器领域的软件碎片化问题,从而显著减少构建深度学习编译器的成本。
  • MLIR的最大创新是方言(dialect)机制。方言机制为编译器的开发人员提供了设计中间表示的统一规范,也为各种基于方言的中间表示之间的转换统一了格式。MLIR中方言的结构如图5.46所示,主要由自定义的接口、属性、类型以及操作构成。
  • 在深度学习编译领域,MLIR官方已经构建了一些比较成熟的方言,主要包括:• Func:函数定义方言,用于表示函数级别的抽象,包括函数的定义、调用以及参数传递等。• Tensor:张量数据结构方言,可表示高维张量。• MemRef:内存引用数据结构方言,作为实际访存的方言使用,指向张量数据结构对应的内存区域。• Linalg:线性代数操作方言,表示对张量的操作,如矩阵乘等,本质是完美嵌套循环的表示。• Affine:仿射方言,常被用于多面体模型编译分析,通过仿射关系表示数学运算。虽然Linalg方言可以下降到Affine方言,但是Affine方言具有比Linalg方言更强的表达能力,两者不具备包含关系。• Arith:算术操作方言,表示在标量、向量、张量上基本的整数和浮点数学运算。• Vector:向量数据结构方言,作为承接Linalg和衔接硬件指令的方言使用,用于将高维表示转换为硬件上的低维向量指令。• LLVM:低层级中间表示方言,与LLVM IR一一对应,从而便于生成后端机器代码。
  • 谷歌基于MLIR开发了端到端的编译器IREE[插图](Intermediate Representation Execution Environment,中间表示执行环境),是目前MLIR社区中最活跃和成熟的项目之一。IREE为用户提供了一套友好的接口,可将传入的使用特定方言(如Linalg)表示的深度学习模型,编译为CPU或GPU的高性能可执行程序。IREE可以满足多种操作系统(例如Android、Linux等)、多种开发环境(例如Vulkan、CUDA、WebGPU等)下的深度学习模型推理任务的需求。
  • PyTorch 2.0中引入了多项编译相关的新技术,用户只需使用一行代码torch.compile()即可获得性能提升,无需编写任何额外代码。这些编译技术帮助PyTorch维持了自身的高易用性,同时进一步提高了框架的竞争力。PyTorch 2.0的核心编译器包括TorchDynamo和TorchInductor,其完整的组织结构如图5.47所示。
  • 表5.5 常见深度学习编译器的对比 [插图]

 *5.5 分布式训练

  • 解释分布式训练的基础概念,包括分布式架构(如参数服务器和集合通信)以及分布式同步策略(如同步通信和异步通信);然后介绍常见的分布式训练方法,包括数据并行、模型并行和混合并行;最后介绍一个简易分布式训练框架的实现方法。
  • 当前用于实现分布式训练的主流分布式架构有两种:参数服务器(Parameter Server, PS)架构和集合通信(Collective Communication, CC)架构。
  • 参数服务器会将所有节点分成中心节点(server)和计算节点(worker)两类。中心节点可以有一个或多个,用于存储参数和更新梯度。计算节点一般有多个,用于完成中心节点下发的实际计算任务。在分布式训练过程中,每个计算节点在执行训练任务前都需要向对应的中心节点请求最新的模型参数,这个过程称为拉取(pull);然后计算节点会基于这些参数使用输入数据完成计算,得到梯度更新值,这个梯度更新值会被推送(push)到对应的中心节点;最后中心节点更新模型参数完成本轮训练。
  • 参数服务器架构的优缺点都十分显著。优点包括:(1)参数服务器架构的计算和存储分离,通过改变中心节点数量就可以调整系统的并行性和处理能力,可以灵活地适应不同的负载和数据规模;(2)参数服务器架构允许多个计算节点同时读取和更新来自中心节点的模型参数,使得训练过程中能实现高效的参数共享,避免了每个节点都要复制一份完整模型的开销。参数服务器架构存在三个主要缺点:(1)参数服务器架构的“中心化”特性会导致单点故障,一旦某个中心节点发生故障,该中心节点存储的参数将无法被系统使用,整个系统的性能和可用性都会受到影响;(2)多个计算节点可能同时读取和更新模型参数,会导致数据一致性的问题,需要采取适当的同步机制来确保参数的正确性和一致性;(3)随着计算节点数量的增加,中心节点与计算节点的网络通信开销也随之显著增加。当连接的计算节点达到一定数量时,受通信带宽的限制,中心节点将成为系统的瓶颈,并限制了分布式训练系统的加速效果。
  • 集合通信中最基础的操作有发送(send)、接收(receive)、复制(copy)、组内进程障碍同步(barrier)以及节点间进程同步(signal+wait),这几个基础操作经过组合可以得到在分布式训练中常用的通信原语。
  • 当前主流的并行计算架构标准(如Message Passing Interface, MPI)和用于智能计算系统的集合通信库(如Cambricon Neuware Communication Library, CNCL[插图])都实现并高度优化了这些通信原语。图5.50展示了部分通信原语在DLP集群(以四个DLP节点组成的集群为例)间的作用效果。• 一对多广播(Broadcast):将一个进程的数据广播到所有进程,常用于分享模型参数。• 一对多散射(Scatter):将一个进程中的数据按索引散射到多个进程,常用于更新权重。• 多对一收集(Gather):从多个进程收集数据到一个进程,常用于收集梯度。• 多对一归约(Reduce):从多个进程收集数据,并按某种运算(如求和运算)归约到一个进程,常用于梯度累加。• 多对多收集(All-Gather):从多个进程收集数据,并广播到所有进程,常用于数据同步。
  • 多对多归约(All-Reduce):从多个进程收集数据,并按某种运算(如求和运算)归约,再广播到所有进程,常用于数据同步和梯度累加。• 多对多交换(All-to-All):将每个进程中的数据按索引发射到对应进程,每个进程接收数据后以发送进程号为索引存储到对应的数据块中,常用于数据同步和信息传递。• 多对多归约散射(Reduce-Scatter):从多个进程收集数据,并按某种运算(如求和运算)归约到一个进程,将该进程中的数据按索引散射到对应进程上,常用于更新权重。其中,多对多归约是最关键的通信操作,在分布式训练中会被频繁用到。多对多归约操作是归约操作的变种,归约操作可以实现加和、乘积、最大值、最小值、平均值等运算。一般而言,使用多对多归约计算节点集群的平均梯度时,首先选择一个主设备,然后让主设备收集所有设备的梯度并计算平均梯度,最后再将平均梯度广播到全部的设备。
  • [插图]
  • 分布式训练是应对“算力墙”和“存储墙”的一种有效策略。
  • 在多设备并行训练场景下,为了确保模型参数的一致性,每个设备计算出的梯度需要经过集合通信操作,如多对多归约,以得到各设备梯度的平均值。各设备再利用得到的平均梯度来分别更新其模型参数,从而完成一轮的训练迭代。
  • PyTorch中用于实现数据并行的常见库有DP库、DDP(Distributed Data Parallel,分布式数据并行)库和FSDP(Fully Sharded Data Parallel,完全分片数据并行)库。使用DP库只需要一行代码model=nn. DataParallel(model)。但是使用DP库实现并行时会存在冗余数据副本、负载不均衡、不支持模型并行等缺点,因此当前官方推荐使用DDP库来实现PyTorch的分布式训练。
  • 不同于传统数据并行需要维护每个GPU中的模型参数、梯度和优化器状态,FSDP可以将这些状态进行分片,并且可以选择卸载到CPU上,更适合大模型训练[插图]。
  • 在正向传播阶段,输入数据首先会被广播至两个设备上。随后,每个设备基于其所存储的算子1模型参数分区独立完成对应的计算任务,再将计算结果合并后输入到下游的算子2进行后续的计算。在反向传播阶段,算子2的梯度输出会被广播到设备1和设备2。接着,两个设备利用其各自存储的模型参数分区独立执行局部的反向计算。计算得到的梯度片段会在两设备之间合并,从而得到用于更新模型参数的完整梯度信息。
  • 算子内并行可以采用不同的模型参数分区方式,例如按行切分和按列切分。以矩阵乘法Y=XA为例,其中X是维度为M×K的输入矩阵,A是维度为K×N的参数矩阵,Y是维度为M×N的输出矩阵。图5.56为对矩阵乘法Y=X A的参数矩阵A进行按行切分和按列切分的示意图。按行切分时会将参数矩阵A切分为[插图],分别放置在两个设备上;同时将输入矩阵X按列切分为[X1, X2],分别输入两个设备,每个设备分别计算Y1=X1A1和Y2=X2A2;最后使用归约(此处是加和操作)通信原语将两个设备的计算结果求和,得到的结果等于原始矩阵乘法Y=XA的结果。按列切分时会将参数矩阵A切分为[A1, A2],分别放置在两个设备上;每个设备的输入都是完整的输入矩阵X,每个设备分别计算Y1=X A1和Y2=X A2;最后使用收集通信原语将Y1和Y2拼接,得到的结果等于原始矩阵乘法Y=XA的结果。
  • Trans-former网络结构中的全连接前馈网络由两层全连接层实现,因此包含两个连续的矩阵乘法操作,我们对这两个矩阵乘法操作分别采用不同的切分方式并存放到不同的计算设备上:对第一个矩阵乘法的权重矩阵A按列切分,对第二个矩阵乘法的权重矩阵B按行切分。这样,第一个矩阵乘法完成后,其中间结果是按列切分的格式,刚好能够满足第二个矩阵乘法按行切分的输入要求,从而省去了中间结果的多对多收集通信操作,这使得整个Trans-former算子内并行的实现更加简洁高效。
  • 从本质上讲,算子间并行是通过对神经网络的模型参数进行“垂直”切分(即切分为不同的网络层)来实现的,并将这些分割后的参数放置到不同的计算设备上进行分布式训练。
  • 在上述的算子间并行过程中,下游设备需要等待上游设备完成计算才能执行当前计算,因此下游设备容易较长时间处于空闲状态,该现象被称为模型并行空泡(model parallelism bubble)。为了缓解模型空泡现象,流水线并行(pipeline parallelism)被提出。
  • 在流水线并行的训练过程中,首先将输入数据的批量(batch)细分为多个微批量(micro-batch),然后每个微批量依次进入训练系统,完成正向传播和反向传播,计算得到当前微批量的梯度。在全部微批量的梯度完成计算并得到平均梯度后,再统一更新模型参数。
  • 图5.59为GPipe实现流水线并行的示例图。首先基于数据并行的思想,对输入数据的批量进行拆分,使设备处理的单位从原本的批量(F0)变为更细化的微批量(F00、F01…)。当设备0完成其第一个微批量(F00)的正向传播计算后,会将中间结果发送给设备1;设备1接收到数据后,就会开始其第一个微批量(F10)的正向传播计算,同时设备0会开始其第二个微批量(F01)的正向传播计算。当设备3完成其最后一个微批量(F33)的正向传播计算后,系统开始反向传播。当设备3完成第一个微批量(B33)的反向传播计算后,梯度结果会被缓存并发送到设备2,设备2接收到数据后,就会开始其第一个微批量(B23)的反向传播计算。当设备3完成其全部微批量的反向传播计算后,会用本地缓存的梯度计算得到平均梯度,最后更新模型参数。使用这种流水线并行的方法,空泡减小到之前的1/4(4即每个批量划分出的微批量数)。
  • 混合并行策略会带来额外的通信开销,对于数据并行而言,通信发生在反向传播过程之中,共享参数的模型副本之间需要交换权重梯度数据,通信类型是多对多归约;对于张量并行而言,通信发生在每层的正向与反向传播过程中,通信类型是多对多归约或者多对多收集;对于流水线并行而言,通信发生在流水线划分点的前后两层之间,传输的是网络层的激活值和激活值梯度,正向与反向传播过程均需要通信,通信类型是点对点通信。
  • DeepSpeed[插图]是微软团队开发的开源深度学习分布式训练优化库,能够自适应分布式训练规模,提升分布式训练速度,极大地提升了大模型训练能力。图5.61描述了Deep-Speed中的混合并行分布式训练策略:32层的神经网络被纵向分成了4个流水级进行算子间的模型并行(即流水线并行,见图中流水线阶段0~3);然后每个流水阶段中又将所属的8层神经网络进行横向划分,进行以4为单位的算子内模型并行(即张量并行,见图中MP-0到MP-3);最后整个流水线并行被复制到2个数据并行实例中。最终一共使用了32(=4×4×2)个计算设备完成混合并行分布式训练。
  • 在分布式训练框架实现中,划分模块和通信模块是最重要的两个模块,如图5.62所示。划分模块负责将训练任务划分(即划分数据或模型)到不同的设备上,得到划分后的计算子图,后续再实际分发到各个设备执行计算。与此同时,在每个计算节点上可能使用不同批量的数据进行训练,需要将各节点上的模型参数进行聚合和同步,确保模型的全局同步更新,因此需要高效可靠的通信机制,并且需要通信模块管理节点之间的通信,支撑节点之间频繁的数据交换以及同步。
  • 以PyTorch的DDP库为例,其数据并行划分主要是依靠Distributed Sampler类对样本(即训练数据)进行采样。DistributedSampler类的部分源码如图5.63所示,实现了在分布式环境中对数据集进行采样。其初始化函数__init__的输入参数包括:• dataset:待采样数据集。• num_replicas:可选参数,表示进程[插图]总数,即参与分布式训练的进程数。• rank:可选参数,代表当前进程的标识符,即进程在进程组中的排名。• shuffle:布尔值,指示是否在每轮开始时对数据集进行洗牌。• seed:整数值,用于生成洗牌的随机种子。• drop_last:布尔值,指示是否丢弃最后一批数据(可能小于batch size)。初始化函数会根据drop_last参数和数据集大小来计算单次采样数num_samples和总样本数total_size。DistributedSampler类中最主要的方法是__iter__方法,该方法会返回一个采样迭代器,当shuffle=True时,使用给定的种子和当前轮次生成样本的随机索引顺序。DistributedSampler类将确保每个进程在每个轮次中都能获得数量相同且不重复的样本,从而实现数据在多个进程间的均匀分布。
  • 在该模型的正向计算函数中,需要将设备0计算得到的第一层输出x移动到设备1上,并通过第二层进行线性变换得到最终输出。
  • 在分布式训练中,系统初始化时需要将模型参数分发到各个设备,计算时需要对各个设备进行参数梯度平均,这些操作都需要通信模块的支持。当前已经有许多成熟的通信库,比如CNCL和NCCL,开发者在开发编程框架时,可以直接使用这些通信库作为通信模块的基础。
  • 1.模型数据发送在DDP库初始化时,会调用函数_sync_module_states()将参数和缓冲区信息从主节点发送到其他节点,以确保所有进程上的模型状态保持一致。如图5.65所示,该函数首先会收集模块中的参数和缓冲区信息,并将它们加入列表module_states之中。接下来,该函数会调用_sync_params_and_buffers()函数,使用broadcast通信原语将参数和缓冲区信息广播到其他进程。所有的进程都能获得相同的模型参数和缓冲区信息,以便进行同步更新。
  • 2.参数梯度平均参数梯度平均中最重要的两点是:参数选择和通信时机。为了更好地利用计算和通信资源,DDP库中引入了桶(bucket)机制,将参数进行分组管理,每一组称为一个桶。桶是参数的集合,这些参数一般是相同类型,但位于不同的进程中。图5.66完成了简易的参数分桶,将同类型的参数分在了一个桶中。当桶内全部参数的梯度计算完成时,可以先进行通信操作,而不影响进程中其他梯度的计算。因此,在参数选择上,会以桶为单位进行选择。在梯度平均的通信过程中,仅当某个桶完成桶内全部参数的梯度计算时,才会使用all-reduce通信原语进行平均,使得每个设备获得相同的平均梯度。
  • 从代码流程来看,_ddp_init_helper()函数首先会调用dist.compute_bucket_assignment_by_size()函数,对收集到的参数分组。该函数可以生成不同大小的桶,并尽可能地使同类型/设备的张量进入同一个桶,减少通信开销,感兴趣的读者可以阅读torch/csrc/distribut-ed/c10d/reducer.cpp进一步了解。最后使用dist. Reducer来初始化通信管理器。
  • 当某个桶中所有参数都被标记为完成计算后,就可以利用all-reduce原语进行桶内梯度平均,而不用等待同一进程全部参数的梯度计算完成。
  • run_comm_hook()函数用于设置通信钩子,默认多对多归约进行梯度平均,即调用run_allreduce_hook()函数。run_allreduce_hook()函数会创建一个_AllReduceBySumCommHook对象,并调用其中的runHook函数执行多对多归约操作。

 第6章 面向深度学习的处理器原理

  • 再将一个处理器核心放大观察,即图6.2c,我们将其中占据面积较为显著的功能单元标记出来[插图]。可以看到,分支预测器、指令译码逻辑、调度单元占据了核心上方绝大部分面积,它们负责将需要执行的指令找到、取来、翻译成控制信号,以便高效地控制运算逻辑单元;数据访问逻辑和一级、二级缓存占据了核心右下方绝大部分面积,它们负责将需要处理的数据找到、取来、暂存备用,以便及时地供给运算逻辑单元;然而真正负责计算的运算逻辑单元,只占据处理器核心约2.4%的面积[插图]。如果放眼整块处理器芯片,8个处理器核心的运算逻辑单元总共占比不到1%。
  • 现在,通用处理器的性能提升速度已经放缓到每年约6%。如此缓慢的提升速度无法满足深度学习大模型时代每年十倍增长的性能需求,因此我们必须另寻出路,对处理器架构进行革新。
  • 在分别为哈佛马克一号、ENIAC编程后,冯·诺依曼受邀作为下一代计算机“EDVAC”项目的顾问,撰写了《EDVAC报告初稿》[插图]。在报告中,他总结了一台计算机的六个必要组成部分:运算器、控制器、存储器、输入模块、输出模块,加上慢速的外部存储器;数据应当采用二进制表示;程序指令应当被视作一种数据,共同存储于同一个存储器(称为主存储器)。这些原则后来被统称为冯·诺依曼结构。冯·诺依曼结构是通用处理器结构最基本的出发点,如图6.3所示。由于指令被视为数据存储在主存储器中,需要一个取指模块负责将指令从主存储器中取出来,按照指令控制系统送给控制器;控制器控制了运算器何时进行什么运算、从主存储器的什么位置取数据;运算器在控制器的指挥下完成运算。这样就构成了一个最基础的通用处理器原型,能够根据程序指令,对主存储器中的数据进行运算。其中,取指模块、控制器和运算器位于处理器芯片内部,而主存储器位于处理器芯片外部。
  • 在过去,通用处理器的运算速度一直随摩尔定律发展,但主存的读写速度发展非常缓慢,二者的发展速度形成了明显的剪刀差。为了弥补计算速度和片外访存速度逐渐扩大的差距,后来的研究者在冯·诺依曼所定的外存、主存两级存储层次之上,又在通用处理器芯片内部增设了高速缓存这一新层次(见图6.4)。因为与处理器本身同处一块芯片之内,又常常采用昂贵而高性能的SRAM技术实现,缓存的读取速度比位于芯片之外的主存储器快很多,与处理器的运算速度比较接近。当今,缓存的存储容量可以达到MB量级,且常见设有两到三个层次的缓存,以便提供更大容量和更高速度的缓存效果。
  • 在缓存的硬件中设计了数据的分配和替换机制。处理器每次访问主存储器时,都会首先经过缓存。如果缓存内包含该数据(称为“命中”),就不再需要真正访问主存储器,而是直接由速度更快的缓存提供数据。如果在缓存中没有找到所需的数据(称为“未命中”),那么从主存储器中读取数据的同时,该数据会留存在缓存上,以备加速不久之后对同一数据的下一次访问。如果缓存中暂存的数据已满,缓存会根据某种替换策略将已存储的数据中最不太可能再次被用到的数据替换出去,腾出空间来存储新的数据。这样一来,缓存被设计为一种“透明”的存储层次结构,在处理器中添加缓存可以自动加速数据的访问,不必对程序进行任何修改就能发挥作用。缓存有三种分配方式:直接相联、全相联、组相联,如图6.5所示。假设内存地址空间是0~7,缓存地址空间是0~3,现在需要将内存中第4行的数据存放到缓存中。直接相联方式将内存中的数据按地址分配到缓存中的固定位置,在该例子中,直接相联方式将内存地址对4取模得到0,然后将数据存放到取模结果的位置,即缓存的第0行,如图6.5所示。直接相联的硬件实现最为简单,但分配的灵活度差,常出现缓存未满却因分配冲突发生不必要的数据替换的情况。全相联方式允许将内存中的数据分配到缓存中的任何位置,但实现时硬件也最为复杂,导致很难实现足够的缓存容量。组相联介于直接相联和全相联缓存之间,是更为折中的方案,因此在实际设计中更常被采用。以两路组相联缓存为例,每个数据分配了两个位置用于存放,例如内存中地址为4的数据可以放到缓存中0、1两个位置。缓存中每一个位置上存放的数据来源于内存上哪个地址,需要用缓存标签(tag)来记录,以便在访问时进行精确的查找。
  • 缓存中的数据替换策略有随机替换、最长时间未使用(Least Recently Used, LRU)、先进先出(First In First Out, FIFO)等。
  • FIFO会把最先进来的数据替换出去,所以称为先进先出;LRU会将最近最少使用的数据替换出去,因为最久没用到的数据相比最早读取进来的数据,更像是不再使用的数据。LRU的实际效率通常较好,因此被广泛采用。
  • 冯·诺依曼结构将程序与数据存储在一起,也有它的弊端。在计算机中,主存储器只有一个。如果有缓存,各级缓存通常也只有一个(否则会面临复杂的缓存一致性问题)。所以,主存储器(或缓存)在同一时间只能服务一个访问请求。处理器在运行时,既需要取指令,又需要取数据。如果两种需求发生在同一时刻,主存储器(或缓存)就只能先后进行服务。这意味着取指模块和运算器二者当中,同一时刻必然有一个是不能工作的,这导致处理器必须频繁地暂停执行,效率很低。
  • 人们用缓存代替了纸带,在最靠近处理器核心的高速缓存层次上,将存储指令的缓存和存储数据的缓存分离成两块不同的缓存。如图6.6所示,缓存分离后对它们的同时访问互不干扰,在绝大多数时间内都可以同时进行取指令和取数据的操作。只有在非常罕见的情况下,指令缓存和数据缓存同时未命中,对二级缓存的访问发生冲突,才需要暂停执行。这对实现通用处理器的高效运行至关重要。经过第一级缓存的分离后,从二级缓存开始,指令和数据又可以重新合并在一起,共享二级缓存的存储容量,这样对缓存容量的利用更加充分。
  • 为了保证运算器能够以最快的速度访问最常用的数据,人们在缓存之上又追加了寄存器:一种每个时钟周期内都能提供访问但容量非常小的存储结构。
  • 涉及访问主存的时候,每个操作数都又可以分为多种寻址模式,例如立即数寻址、寄存器寻址、偏移量寻址等。为了将所有这些模式组合列举出来,指令集开始变得臃肿不堪。这一时期的设计后来被统称为复杂指令集结构计算机(Complex Instruction Set Computer, CISC),如图6.7a所示。在CISC设计中,运算器、寄存器、主存储器之间的访问关系是混乱而无约束的,这导致这一时期硬件中的控制器、软件中的编译器都变得十分复杂。
  • 人们引入了精简指令集结构计算机(Reduced Instruction Set Computer, RISC)的概念。RISC最主要的一条设计原则是:必须通过专门的装载(load)、存储(store)指令来访问主存储器,将数据交换到处理器芯片内部的寄存器中,然后才允许进行计算。其他所有的计算指令都必须发生在寄存器上,不允许访存。图6.7展示了采用RISC原则后的设计。在这种设计中,运算器不再直接和主存储器(通过缓存)交互,所有的运算数据都是从寄存器中来,运算结果也只会存到寄存器中去。这样,处理器结构的设计就大幅简化了;各类型指令的职责划分清晰之后,通过软硬件手段挖掘性能也变得更加容易,最终导致了更简洁的处理器结构设计和更优秀的实际性能。
  • 而RISC一词是后来由美国人大卫·帕特森(David Patterson)创造的,他在1980年发起的“伯克利RISC”项目使这一概念广为人知;同期,另一个知名的类似项目是斯坦福大学的MIPS(1981年)。
  • 处理器所采用的指令集不一定是服从RISC原则的。例如在个人计算机和数据中心中广为流行的x86指令集将其1970年代的复杂指令设计传承至今,以保持其软件生态的延续性。在这种情况下,需要通过一个指令译码单元将复杂指令首先翻译成符合RISC原则的指令微码,再通过微码进行控制(见图6.7)。这样既可以获得RISC的优势,又尊重了历史上遗留的设计惯例。
  • 如图6.8所示,要从结构上支持分支跳转,通用处理器需要单独设立一个名为程序计数器(Program Counter, PC)的寄存器来追踪下一条指令的地址。在执行分支指令的条件判断时,运算器根据条件情况,计算出下一条指令的地址,然后将计算得到的地址存入PC中。取指模块不再是顺序取指令,而是按照PC指明的地址来取指令。在运算器完成分支指令的条件判断之前,处理器无法确切得知下一条指令的地址,也就无法提前进行取指和译码操作,必须暂停等待结果才能继续执行。频繁启动和暂停对处理器的运行速度有着不利影响,特别是在流水线较深的处理器结构中。因此人们至今仍然一直在研究如何降低条件分支跳转指令导致处理器暂停带来的影响。
  • 因为后一条指令使用了前一条指令的结果,因此存在依赖。存在依赖的指令无法同时执行,因此必须增设发射单元,发射单元专门负责挑选出不存在依赖、适合同时执行的指令。发射单元在每一个时钟周期内,从微码队列读取即将执行的四条指令微码,判断其中互相不依赖的指令,将它们同时送至各个运算器处(称为“发射”),以备同时执行。因为每一个时钟周期内可以发射多条指令,这项技术称为“多发射”;而从计算的角度看,因为多个运算器能够同时执行多个标量计算操作,这项技术又被称为“超标量”。
  • 发射后的指令互相不存在依赖关系,可以分别相对独立地进行控制。这样,控制器也被分解为四组,各自控制对应的运算器完成运算。这些分解开的控制器通常称为调度单元。调度单元一方面负责提供运算器、寄存器等结构的控制信号;另一方面要协调处理器中的资源(例如寄存器、缓存等),在出现争用时适当推迟运算器的执行。经过多发射的结构改造,处理器可以同时利用多个运算器执行互不相关的指令,在相同主频下能够挖掘更高的处理器性能。
  • 1964年,美国人西蒙·克雷(Seymour Cray)设计的CDC 6600计算机首创了多发射技术。这台计算机获得了巨大的成功,其性能远远甩开了同时期的其他竞争对手。人们将它归为“超级计算机”这一新品类,而西蒙·克雷则被尊称为“超算之父”。
  • 实际上,寻址和数据计算之间是相对独立的。可以增加一组专门用于计算地址的运算器,称为地址生成单元(Address Generation Unit, AGU),如图6.10所示;与之相对,原本用于数据计算的运算器称为运算逻辑单元(Arithmetic Logic Unit, ALU)。
  • 为保证随时有未被使用的寄存器可用,物理寄存器通常需要做得比逻辑编号更多一些,常常达上百个;这些物理寄存器在电路中聚集在一起,形成一个小容量、访问极快的存储器,称为物理寄存器堆。
  • 寄存器重命名技术作为乱序执行的基础,两项技术共同诞生在名为IBM System/360 Model 91的计算机上,由美国人罗伯特·托马苏罗(R. Tomasulo)在1967年发明。
  • 处理器设计开始普遍采用乱序执行技术。乱序执行是指允许处理器在保证最终执行行为一致的前提下,不按照程序中所写的指令先后顺序来执行指令的技术。在靠前的指令因依赖未准备好或访存未命中等原因陷入停滞时,可以允许处理器绕过该指令,提前执行后面的无依赖指令。Tomasulo算法是一种常用的乱序执行架构设计方案,有兴趣的读者可以查阅相关资料了解。
  • 为了能够对访存指令执行撤销操作,需要再添加一个写入/写出队列,如图6.13所示。在执行存储指令时,不要直接操作缓存和主存储器,而是暂时放入写入/写出队列当中备用;等到存储指令确认提交时,再从写入/写出队列中真正发起对缓存和主存储器的操作。如果存储指令需要撤销,只要清空写入/写出队列即可实现对数据的拦截。在执行装载(load)指令时,根据指令之间的顺序,首先需要检查写入/写出队列中是否有相对应的存储的数据,因为直接访问缓存得到的数据不能体现乱序执行、尚未提交的存储指令的执行结果,可能不是最新的数据。
  • 数据前递技术通过对连续使用的数据进行数据通路的路由,省去了反复从寄存器堆中写入和读出的过程,提高了处理器的效率。
  • 例如,在一些程序中包含循环次数较多、代码较少的小循环,分支指令的延迟使得大部分的运行时间都花费在等待分支结果进行取指,而真正运算花费的时间占比很低。实际上,在反复执行的小循环中,我们都可以猜想分支指令的结果几乎一定是向上跳转的,除了退出循环的最后一次。分支预测技术允许处理器根据猜测,在分支指令的结果真正产生之前,就按照猜测的方向继续投机执行,省去等待时间。为了实现分支预测,需要增加一个分支预测器,如图6.15所示。最简单的预测方法是永远预测向上跳转;一种常用的方法是根据历史执行行为记录进行预测;今天,芯片内晶体管的密度已经非常充裕,一些处理器开始设计一个小型的硬件神经网络去进行预测[插图]。
  • 如果运算逻辑单元给出的跳转方向与预测方向一致,那么以上操作就成功省去了等待时间,提高了处理器效率。如果运算逻辑单元给出的跳转方向与预测方向不一致,那么利用已经在乱序执行技术中设计好的撤销机制,撤销掉沿着错误预测方向已经投机执行的指令,对整个程序的执行不会造成不利影响。现在,优秀的分支预测器常常能将预测准确率提升至97%以上,但分支预测器所占用的硬件资源也增长到了整个处理器核心的约1/5。
  • 一方面,通用处理器需要以标量作为运算的基本粒度,运算量越大、控制越复杂的程序就需要通过越多的指令来执行。然而,要达到通用性意味着任意两条指令之间都可能存在依赖关系,准确识别和处理这些依赖关系是通用处理器所面临的一项根本性的难题。为此,通用处理器架构当中设计了精巧复杂的指令流水线,花费了大量的硬件资源来处理依赖,以便让这些标量指令以尽可能快的速度发射执行。处理器能够发射执行指令的速度从约0.12条/时钟周期(英特尔4004, 1971年)提升至了今天最高10条/时钟周期。但最终,每条指令执行的不过是一次标量运算而已,解决这10条指令的依赖关系远比执行10个标量运算更难。另一方面,随着集成电路工艺的发展,处理器的速度与主存储器的速度之间的差距逐渐拉大,形成存储墙(memory wall)现象。为了及时地给指令流水线供应数据,通用处理器必须搭配容量越来越大、层次越来越多的高速缓存。这些缓存必然逐渐占据芯片中的主要面积。理解了通用处理器的这两项难处,也就理解了为何运算逻辑单元逐渐发展成在通用处理器芯片中只占不到1%面积的小模块了。
  • 这样一个复杂的处理器结构执行智能任务的效率却并不高,其原因有以下两点,其一,智能任务的控制与寻址需要非常多的指令来完成,使得实际用于计算的指令成为少数;其二,通过高速缓存进行智能任务的数据访问,很容易触发特殊情况,导致访存效率骤减。
  • 如图6.16所示,将卷积运算编译到真实指令集上并分别标注其中用于控制、寻址和计算的指令。从图中可以很明显地发现,绝大部分指令都用于控制,占比其次的是用于寻址的指令,它们的数目都远远超过实际用于计算的指令数目。所以,即使经过了大量的架构优化,在通用处理器上执行一个卷积运算,其控制和寻址的开销占比仍然很高,远超计算开销。
  • 因为免去了循环的分支指令和循环计数,也就免去了循环带来的控制开销。
  • 现代优化编译器会尝试评估循环展开的预期效果,然后再决定是否施加循环展开。
  • 如果用户判断这一评估是错误的,倾向于展开循环,可以通过程序杂注(pragma)等方式对编译器进行指导,要求其展开。
  • 缓存是编程透明的,自动作用于访存过程中提供加速,减轻了程序员的负担。
  • 发生容量失效时,该级缓存将完全失去作用。
  • 现在读者可以看出,深度学习程序有规律的访存行为有时反而会让通用处理器暴露弱点。一般认为,数据的规模越大,缓存容量就相对越不足,缓存命中率也应当会越低。但是在产生伪共享现象时,实际情况却反常起来:规模大一点,命中率高;规模小一点,反而命中率严重下降。产生临界步幅现象时情况则更加诡异,只当矩阵为某个特定规模时,访存效率突然严重下降,将矩阵增大一点或减小一点都将恢复正常。可以看出,缓存给通用处理器的访存行为带来很多复杂的现象,而这些现象是需要编程人员对通用处理器的底层体系结构原理有着非常深入的了解才能察觉的。
  • 可以对数据进行分块(tiling)处理,将运算数据分解至较小尺寸的区块再分别处理。如果能进行递归的分块处理,那么程序总会在某一个递归层次上使得所有需要用到的数据都落在缓存之内,避免掉前述各种问题。这样的算法被称为缓存超越算法(cache-oblivious algorithm)[插图],因为它总能自然适应各种不同的缓存层次结构,避免缓存容量带来的种种问题。
  • 对于矩阵乘来说,可以用递归法(分治法)求解,也可以使用迭代法(三重循环)求解。二者的效率孰高孰低没有定论:递归法的优势是实现了缓存超越算法,可以避免潜在的缓存失效问题,以及更容易改用Strassen快速矩阵乘算法[插图]等;而迭代法的优势在于程序简单,控制和寻址的开销相对较低,节省了递归函数调用带来的开销。

 6.2 向量处理器

  • 我们首先尝试一种直接的改进思路,允许一条指令并行操作多个数据。每进行一次循环、计算一次地址,可以从此地址起,连续取出一段数据来一同参与并行运算。预期达到的效果是将控制、寻址、访存的开销分摊到多个数据的计算上,提升计算的效率。这样进行改造后,我们将得到向量处理器。
  • 最广为人知的向量处理器结构即是图形处理器(GPU)。
  • [插图] 图6.22 处理器结构的费林分类法
  • 在采用SIMT编程模型时,程序员可以更为简单直观地编写分支和循环。虽然多个线程其实在硬件上共享了一组指令流水线,但每一条线程都能够独立选择自己的执行控制流,因而可以视作独立的线程。SIMT深刻揭示了线程的本质:线程与处理器核心、发射单元、指令流等都没有必然联系,线程的本质其实是独立的控制流状态。
  • [插图] 图6.24 向量处理器结构 控制方面,向量指令也需要接入乱序执行流水线当中。其寄存器重命名记录等状态信息也需要接入提交队列,按顺序进行提交。这样,才能对向量指令进行撤销,而能够撤销又是进行投机执行的前提,分支预测等功能才能在向量指令中发挥作用。但从另一方面考虑,因为向量运算的能耗较高,随意进行撤销会影响处理器的能效表现,所以不允许向量指令进行投机执行,乃至不允许向量指令出错、撤销的设计,也都是合理的。
  • [插图] 图6.25 渲染管线中的主要步骤与早期GPU硬件单元 随着计算机图形学的发展,渲染管线变得越来越灵活和复杂。例如显示物体光泽、动物毛发等都需要额外的步骤,倘若GPU为渲染管线中的每个步骤都设计不同的硬件单元,那么GPU的结构将会十分复杂且冗余。
  • [插图] 图6.26 Tesla架构图[插图] 在TPC内,流式多处理器控制器(Streaming Multiprocessor Controller, SMC)负责将各种任务以线程束(warp)为单元进行打包,交给流式多处理器(Streaming Multi-processor, SM)进行运算处理,其中每个线程束包含32条线程。SM是执行计算任务的主要单元,其作用类似于一个相对独立的小型处理器核心。在SM中,指令缓存负责保存要执行的指令;常量缓存负责保存常量;指令发射单元负责发射指令,控制所有的流处理器(Streaming Processor, SP)
  • 此外,SM当中还有一种特殊的SP——特殊功能单元(Special Function Unit, SFU)承担图形任务中常用的更为复杂的运算,比如超越函数(指数、对数、三角函数等)、属性插值和透视矫正等。除了SM以外,TPC中还有一些单元用于执行渲染管线中特定的步骤,如纹理单元负责处理纹理滤波,几何控制器负责管理顶点属性的输入输出。
  • 优秀的GPU编程者总是懂得如何让GPU尽量避免进入分歧状态,例如将趋于跳转和趋于不跳转的线程重新分配到不同的线程束中去,增加每一个线程束保持一致的机会。
  • 每个线程束都会占用一定数量的寄存器等硬件资源,每个线程束占用资源越多,每个SM内能够同时维护的线程束就越少。使用GPU的编程人员需要考虑的是尽量节约线程束使用到的寄存器等硬件资源,而不是像使用通用处理器时那样考虑更充分地利用硬件资源。
  • 图6.29展示了处理器中各种行为所需要的能量的大致数量关系。以运算逻辑单元进行一次运算所消耗的能量为1个单位,那么访问寄存器也大约需要1个单位能量;访问缓存大约需要6个单位的能量;访问主存储器,则需要超过200个单位的能量。因此,可以说我们的各类处理器芯片中消耗掉的绝大多数能量其实并没有用来做计算,而是用来存储和传输计算过程所需要的各类数据。那么,存储和传输数据的效率就将直接影响处理器芯片的整体能效表现。在保证完成计算任务的前提下,架构设计者必须竭尽全力降低对各级存储层次结构的访问次数。即使是因架构设计不足导致处理器大量访问芯片内的缓存和寄存器,产生的能耗也将很难让用户接受,更不用说频繁访问主存储器了。
  • [插图] 图6.29 处理器中各种行为所需要的能量 如何分析和解决访存问题?我们可以借助I/O复杂度这一理论工具。
  • I/O复杂度是洪加威和孔祥重于1981年提出的一种理论工具。它刻画了算法所蕴含的一种本征性质:随着算法处理的数据规模逐渐增加,算法表现出的运算密度也将增加。运算密度与参与运算的数据规模之间的函数关系被定义为I/O复杂度。
  • 例如,在k阶的矩阵乘法运算中(朴素矩阵乘法算法),输入数据一共有2k2个,输出数据有k2个,需要k3次乘法和约k3次加法。运算密度约为[插图]。随着k的增加,运算密度[插图]也一同增加,体现出朴素矩阵乘法的这种本征性质;I/O复杂度定义为运算密度[插图]相较于参与运算的数据量(n=2k2)的函数关系,可以渐进表示为[插图]。I/O复杂度增长越快,说明算法的数据局部性越好。在具有高I/O复杂度的算法上,通过扩大架构中一次运算操作能够处理的数据规模,就可以改良运算密度,相对地提高算力、降低访存压力。
  • I/O复杂度理论告诉我们,需要选择具有高I/O复杂度的算子作为基本运算,而向量运算的I/O复杂度仅为常函数。这是向量处理器性能受到访存制约的根本原因。为解决这一问题,我们设计的深度学习处理器需要重新考虑基础运算的选择,例如采用矩阵运算作为基本运算。
  • 1979年,初版BLAS发布,其中所有的算子都是向量运算。例如“AXPY”,是指泛化后的向量加法ax+y。后来这些算子都被归结为“一级算子”。1984年,第二版BLAS开始编辑,直到1988年发布。这一版加入了“二级算子”,即矩阵与向量间的运算。例如“GEMV”,是指泛化后的矩阵乘向量操作aAx+by。二级算子由于抽象层次更高、计算粒度更大,BLAS库实现时获得了更大的优化空间,最终程序总体性能往往会比采用一级算子实现的方案更高(见图6.30)。1990年,第三版BLAS发布。这一版加入了“三级算子”,即矩阵与矩阵之间的运算。例如“GEMM”,是指泛化后的矩阵乘法aAB+bC。采用三级算子后,程序性能获得了显著的提升,并且提升幅度比二级算子相对一级算子明显更大(见图6.30)。
  • 表6.1列举了各级BLAS算子的运算量、访存数据量和运算密度,I/O复杂度是运算密度关于数据量的函数。可以看出,虽然二级算子相比一级算子,运算密度有所改善,但改善幅度不超过2倍,I/O复杂度仍然为常函数(O(1))。但是,三级算子相比二级算子,I/O复杂度不再是常函数[插图],带来了足以引起质变的性能优化潜力。只是在通用处理器上进行程序优化,通过提升算子级别、改善运算密度,降低了对各级缓存、主存的访问压力,结果就足以产生巨大的性能差距了。如果在架构层面对这些I/O复杂度更良好的高级算子进行特别支持,还能够进一步降低对寄存器堆等处理器内部数据通路的访问压力。这些现象启发我们,如果深入处理器架构进行针对性的设计,或许应当选取更高级算子(例如矩阵运算)作为主要的基础运算,而非向量运算。
  • SIMT风格不直接操作向量,而是通过向量单元的分歧执行模拟多条并行的线程,通过线程超载和切换隐藏访存开销,常用于GPU。

 6.3 深度学习处理器

  • 深度学习处理器抛弃了高速缓存,用能够显式编程控制的便笺存储器作为片内存储层次。
  • 硬件运算单元的时分复用机制是指,硬件在每个计算周期仅用固定规模的一小块输入数据计算固定规模的一小块输出数据,下一个计算周期再去计算下一块输出数据,从而用小规模的电路实现大规模的深度学习算法。
  • 若神经网络层的输入或输出神经元数目不是Ti、To的整数倍,则产生模数效应,最后一次运算时只剩下凑不满整个硬件运算器规模的数据。那么在最后的运算过程中,需要屏蔽掉一些硬件神经元,为其输入额外的0使它们的运算过程不影响最终结果;因此,即使数据不满Ti或To个,这次计算仍然需要一个完整的计算周期来完成。
  • 如图6.31所示,全连接神经网络层有M个输入神经元、N个输出神经元时,完成该层运算共需要[插图]个计算周期。
  • [插图] 图6.32 卷积层算法到硬件的映射 2.访存 深度学习模型的访存有着显著的特点,如果加以利用,有着巨大的优化空间可供挖掘;反过来说,如果未能充分利用,也会造成处理器效率低下。首先,由于深度学习模型是由算子为基本元素组成的,深度学习模型对数据的操作以张量为单位进行,因此访存行为大多都是对整块数据的连续访问。向量处理器便是利用了这一点,成功降低了控制、寻址开销,连续的向量访存也使访存延迟的影响减小了。
  • 我们应当设置一块片内存储器来捕捉数据的重用机会,将数据尽量以大粒度暂存在芯片内部,可以显著降低对芯片外部主存储器的访问压力。
  • 因为每个算子应当如何进行分块与存储器的容量密切相关,智能计算系统的程序员有必要了解并主动管理片内存储器
  • 深度学习处理器采用结构简单的便笺存储器(scratchpad memory, SPM)来取代高速缓存。直白地说,便笺存储器就是一种不附加任何功能的片内存储器,宛如一张白纸,因此得名“便笺”。
  • 进行连续的访存比条带状访存开销更低,因此数据排布也是影响深度学习处理器计算效率的一个因素。
  • 这意味着便笺存储器需要按行编址、按行访问,与高速缓存的缓存行概念类似。便笺存储器的行尺寸确定后,向量运算指令、访存指令等其他指令集设计,以及支持它们的硬件结构,也都自然以行为单位对齐,以保证设计的一致性,降低互相转换的开销。
  • 若在深度学习框架实现中并非按照“NHWC”排布数据,则可能会因此产生额外的数据重排布、矩阵转置的开销,降低运算效率。但是与深度学习框架中“NHWC”略有不同的一点在于,由于模数效应,假如“C”维度的尺寸并非行尺寸的整数倍,应当以零补齐至整行分界处,然后再开始记录下一个点(“W”)的数据。否则不同点的数据混杂在便笺存储器的同一行中,将对硬件运算器造成额外的困扰,需要实现运算器掩蔽等机制才能处理,增加了硬件成本。因为需要补零对齐的行长度与处理器结构有关,需要深度学习框架、智能编程语言在实现时进行针对性的优化来配合,才能达到最佳效果。
  • 图6.34 卷积层算子的分块拆解
  • 通常来讲,合理的分块拆解策略是:如果卷积核/输出特征图过大,就沿输出特征通道维度拆分;如果输入/输出特征图过大,就沿特征图高、宽
  • 方向拆分。如果以上分块仍然无法将数据分解至便笺存储器能够容纳的粒度,再考虑沿输入通道维度拆解。还有其他维度可以进行分块拆解,例如卷积核的尺寸;但这些拆解会使运算密度快速劣化,加重访存负担。如果不是因硬件资源约束迫不得已,一般不会拆解它们。
  • 我们以图像风格迁移模型中的卷积层“conv4_2”为例。该层输入特征图的宽、高均为28,通道为512个;在四周边界各补宽度为1的零,边界补零后宽、高均为30;卷积核宽、高均为3,输出通道512个,步长为1。容易推算得到,输出特征图与输入特征图各维度尺寸是一致的,均为28×28×512。假设字长为16比特(2字节每字)​,便笺存储器容量为1MB,便笺存储器每行存储16个字,因此共编址32 768行;硬件上卷积运算单元每周期输入/输出数据的长度也均与便笺行宽保持一致,同为16。输入特征图共401 408个字,合784 KB;卷积核共2 359 296个字,合4.5 MB;输出特征图与输入相同,合784 KB。要完整存入全部输入/输出数据,需要6 MB的存储空间,超过了我们的便笺存储器容量,因此必须分块。我们将便笺存储器中一半的容量(512 KB)规划用于存储卷积核,另一半的容量用于存储输入/输出特征。因此,卷积核至少需要分解至原来大小的1/9,才能容纳;我们通过对输出特征通道数进行分解,来降低卷积核的尺寸。将输出通道均分为原来大小的1/9(512÷9≈56.9),无法得到整数。又考虑到通道数与硬件运算器尺寸之间的模数因素,分解成57个通道映射在运算器上难免产生算力的浪费,我们选定48个通道(16的3倍)作为划分大小。前480个输出通道均分为10次来计算,每次计算48个输出通道;最后一次计算处理剩余的32通道。如此确定输出通道的分块后,我们知道每一份卷积核占用便笺存储器的容量最大为432 KB,还剩余592 KB可用。592 KB不足以容纳输入特征图,而且还有输出特征图需要占用一部分容量,因此输入特征图也需要拆分,我们选择沿特征图高度方向进行拆分。进行一次对半拆分,输入特征图尺寸变为16×30×512,合480 KB,计算时需要注意预留1行、2列边界补零的空间,卷积核视野中将会额外看到1行对侧的特征图;输出特征图尺寸变为最大14×28×48,合18.4 KB,计算时注意输出通道已经被拆分过了,要和卷积核的拆分保持一致。这样拆
  • 分后,便笺存储器的容量已经足够容纳一次计算的全部数据了。
  • 如图6.35所示,便笺存储器以行(32字节)为单位编址,在总共32768行便笺地址中,0~13823行用于存放卷积核;13824~29183行用于存放输入特征图;29184~30359行用于存放输出特征图。主存储器以字节编址,以W表示卷积核在主存储器中的地址,X表示输入特征图在主存储器中的地址,Y表示输出的目标地址。
  • 我们在图6.35中标示出了循环内每一步(步骤1到步骤10)发生的数据传输。可以看到,目前该“程序”还有诸多模糊的地方,例如:具体要如何完成边界补零?卷积指令应该长什么样子?如何将片内便笺存储器中连续存放的输出数据(便笺[29184~30359]​)写入在主存储器中并不连续的48个输出特征图通道?这些问题都需要我们明确地定义深度学习处理器的指令集来进行回应。
  • 指令集是深度学习处理器体系结构的核心,是深度学习处理器对于底层程序员的抽象,是软硬件之间的界面。指令集的设计原则是把深度学习模型中可能用到的各种神经网络层和算子拆分成一些基本的操作,每个操作由一条指令来完成。如果指令集覆盖了各种深度学习算子的最大公约数,未来新发展出现的神经网络层就能由这些指令拼接组合出来。类似于在向量处理器上可以用加减乘除和跳转拼接组合出所有科学计算,在深度学习处理器上我们以卷积、矩阵乘等高级算子为基础,搭配向量加减乘除、跳转等指令作为补充。这样指令集方案就能兼顾计算效率和通用性,在卷积层、全连接层等包含深度学习模型主要运算量的神经网络层上最大化效率,同时不失完备的功能支持。
  • 与CDC STAR-100的情况不同,这次在DLP上采用STAR风格设计指令并不违反RISC原则。这是因为深度学习处理器采用了便笺存储器,便笺存储器可以视作深度学习处理器当中为大块
  • 张量数据准备的寄存器。
  • 采用STAR风格,将所有的指令都设计为处理任意大小的张量、向量数据(只要不超过便笺存储器的容量)​。通过指令域指明张量的形状、向量的长度,让运算器自主循环完成计算或数据传输。这一方面方便了程序员编程,另一方面可以省去分支循环的控制指令,指令流水线当中需要处理的指令条数显著降低,为我们大幅精简硬件指令流水线结构提供了基础。
  • 本书中的深度学习处理器采用图6.36中的指令集。该指令集符合RISC原则,只能通过专门的装载、存储指令访问主存,在主存与便笺存储器之间交换数据;所有的张量、向量计算都必须工作在便笺存储器上。我们还保留了标量处理部分,准备了64个寄存器;这部分标量指令设计再次采用了RISC原则,使用专门的标量装载、存储指令与便笺存储器交换数据,然后再通过便笺存储器间接地访存。指令集主要包含三类指令,分别用于运算、控制和数据传输。数据传输指令和运算指令分别有针对矩阵、向量和标量的指令。
  • 图6.36 DLP指令集
  • 图6.37展示了一个向量装载指令(VLOAD),将主存储器中的向量装载到便笺存储器中,寻址通过寄存器表示的基址和立即数表示的偏移量共同完成。向量存储指令(VSTORE)与它形式一致,只是传输方向相反;向量移动指令(VMOVE)则负责将连续的数据从便笺存储器的一处传输至便笺存储器的另一处。通过标量数据传输指令(SLOAD/SSTORE/SMOVE),可以在寄存器与便笺存储器之间进行数据交换。
  • 以矩阵装载指令(MLOAD)为例,从主存储器上存储的一个大矩阵内,拆解出一个小矩阵,然后装载到便笺存储器上来。由于矩阵的行向、列向上都能进行划分,小矩阵在数据摆放上是不连续的。深度学习处理器通过设计更复杂的循环控制机制,能够实现条带状(strided)访存,等同于在硬件内部执行这样的向量代码:
  • 通过对张量的形状进行重新诠释,矩阵传输指令已经能够支持在任意维度张量上,最外维与另一单一维度取中、其他维度取满的数据传输。例如在维度为(A, B, C, D, E, F, G)的七维张量中,一条矩阵传输指令可以支持在A维度上任取一段,并同时在B、C、D、E、F、G六个维度中再选择一个,在其上任取一段。更为复杂的条带状访问,稍稍牺牲效率和内存容量,通过多个矩阵传输指令之间的级联也能够完成。但是,如果硬件设计预算上仍有余量,可以考虑在指令集中直接实现针对更高维度张量的传输指令,这样对算法中偶发的复杂情况能够有更好的适应性。
  • 因为DLP指令集采用STAR风格带来的特性,即使硬件上没有采用矩阵乘法单元,而是采用了更低级的矩阵乘向量单元作为矩阵运算器,也并不妨碍DLP指令集提供矩阵乘法(MM)指令。只不过,矩阵乘法指令实际上是由硬件自主计数循环控制、通过连续执行MV指令实现的。采用类似方式,也可以直接实现更高级别的卷积(CONV)指令。不过需要注意的是,将一条卷积指令定义清楚,需要提供至少10个参数。倘若仍然采用图6.37所展示的编码格式,我们的DLP指令集采用的64位指令编码格式已经容纳不下这么多参数了。因此需要为卷积指令特别缩减操作码的位数(缩减至4位)​,或者扩展指令编码长度,或者采用便笺存储器的一行来间接地一次性提供更多参数值。
  • 在计算方面,深度学习处理器通过直接在硬件上实现高级算子,在相同的访存带宽预算下提供了更高的运算能力;在存储方面,深度学习处理器采用便笺存储器取代了高速缓存;在控制方面,深度学习处理器大幅简化了指令流水线中复杂的控制结构。
  • 分支预测:深度学习以计数循环为主要控制结构,条件分支相对罕用;即使使用条件分支,通常循环体也至少需要数百个时钟周期来执行。分支预测在每次遇到条件分支时节约两个时钟周期,对整体性能影响微乎其微。但分支预测器的硬件成本不菲(能占到整个处理器核心的20%)​,应省去。• 数据前递:数据前递机制在两条互相构成数据依赖的标量指令之间提前传递数据,使原本需要等待一两个时钟周期的指令能更紧凑地发射。但是,标量运算在深度学习模型中占比不到万分之一,节约一两个时钟周期对整体性能影响微乎其微。实现数据前递需要在硬件上配备一个较大的交叉开关阵列,成本同样不菲,应省去。• 乱序执行:在深度学习处理器上,允许指令乱序执行仍然是有积极意义的。例如,数据传输和矩阵运算两种指令的执行结构相互独立,一条矩阵运算指令正在长时间执行、导致后续运算指令被阻塞时,完全可以允许位置更靠后的数据装载指令提前开始工作,实现数据传输和运算的并行。但是,深度学习处理器一不负责处理外部设备中断、指令异常等例外情况,二不进行投机执行(分支预测器已经被移除)​,不会出现指令提前执行后又需要撤销的情况。因此,包括提交队列、写入/写出队列等用于实现投机执行、精确例外的指令流水线结构都可以省去。我们将在第7章讨论如何针对智能处理器的情况设计我们真正需要的“乱序执行”机制。• 寄存器重命名:深度学习处理器中的寄存器所承担的最主要功能是用于
  • 记录和传递运算指令参数(地址、张量形状等)​、记录循环状态,而不再是主要用来暂存操作数和运算结果。因为深度学习处理器使用标量指令处理的运算不到万分之一,向量、矩阵指令又几乎不使用寄存器存放数据,寄存器重命名作用微乎其微,可以省去。• 多发射:因为深度学习处理器每条指令所需的时钟周期非常长,很少有指令会集中在同一时钟周期内等待共同发射。即使有指令可以同时发射,改为排队发射、推迟数拍,对整体性能的影响也微乎其微。多余的指令发射单元应省去。另外,因为标量指令不再主要承担运算功能,多发射机制也已经取消,所以标量运算逻辑单元也仅须保留一个。
  • 图6.38 简化后的流水线结构
  • 添加了直接访存模块(Direct MemoryAccess, DMA)负责便笺存储器与主存储器之间的数据传输。
  • 图6.39 DLP的数据通路
  • 内存控制器能够处理的访存事务的粒度在字节至行之间,若要传输成千上万行的张量数据,需要处理器指令流水线不间断地向内存控制器发起处理事务请求。如果通过指令来完成,将占用指令流水线,阻塞运算的进行。为了使访存与运算能够充分同时工作,我们设置DMA来代理控制访存行为。可以简单地理解为,DMA是一个批量数据访问模块。只需要指定批量数据的访存信息,DMA就可以在一段时间内代理与内存控制器之间的交互,完成主存储器与片上便笺存储器之间的数据交互。这样,只需要一条简单的指令即可驱动DMA访问批量数据,立即解放出指令流水线继续执行其他运算指令,在数据传输过程中DMA不占用其他非访存指令的硬件资源。第7章将详细介绍如何利用DMA实现访存与运算之间的高效并行。
  • 数据从一个运算送至下一个运算时,既不需要存入物理寄存器堆(再由另一条运算指令取出)​,又不需要通过数据前递开关阵列进行路由,而是直接通过运算器硬件之间一对一的连线完成了传递。因此,在完成矩阵运算时,深度学习处理器的效率可远超通用处理器和向量处理器。我们将在第7章详细讨论矩
  • 阵运算单元的内部结构。
  • 控制器负责按照矩阵指令配置的参数产生硬件部件所需的控制信号。它负责解析发射的矩阵指令,必要时从通用标量寄存器读取所需参数,推算矩阵运算单元完成运算所需的运算周期,然后控制运算单元的时分复用。在整个矩阵指令的运算周期内,控制器产生相应的控制信号,控制便笺存储器提供的数据和写入结果的位置,使矩阵运算单元能够按照指令分解的顺序正确执行运算。
  • 在算法发展的推动下,20世纪80年代和20世纪90年代初,很多大公司、创业公司和研究机构(包括国外的英特尔、摩托罗拉、IBM、德州仪器等,以及国内的中国科学院半导体所和中科大等)也开展了神经网络计算机/芯片的研制,包括ETANN[插图](见图6.41)​、CNAPS[插图]、MANTRA I[插图]和预言神[插图]等。1989年,国家科委依托中国科学院计算所成立了国家智能计算机研究开发中心,作为我国研制智能计算机的总体单位。
  • 图6.41 英特尔公司的模拟电路神经网络芯片ETANN[插图](1989年
  • 国际上第一个深度学习处理器架构DianNao[插图]。DianNao和过去的神经网络芯片不同,它不再受到神经网络规模限制,可以灵活、高效地处理上百层、千万神经元、上亿突触的各种深度学习神经网络(甚至更大)​;相对传统通用处理器,DianNao可以取得两个数量级(甚至更高)的能效优势。随后中国科学院计算所和法国Inria又合作设计了国际上首个多核深度学习处理器架构DaDianNao[插图]、首个机器学习处理器架构PuDianNao[插图]。2015年,中国科学院计算所提出了国际上首个深度学习指令集“寒武纪”(Cambricon)[插图]。中国科学院计算所还研制了国际上首款深度学习处理器芯片“寒武纪1号”​。在生命科学中,寒武纪是生物大爆发的时代。以“寒武纪”为芯片团队取名,寄予团队对于推动人工智能大爆发的期盼。寒武纪系列处理器问世后,广泛应用于上亿台智能手机和服务器产品中,推动了深度学习处理器从理论走向实际,普惠大众。中国科学院计算所寒武纪团队及其合作者的工作开创了深度学习处理器体系结构方向,使其成为整个国际计算机体系结构领域的学术研究热点。例如,2016~2018年的国际计算机体系结构年会(International Symposium on Computer Architecture, ISCA)有近四分之一的论文引用寒武纪团队的成果,开展深度学习处理器相关研究工作。2018年,S cience杂志评价寒武纪为深度学习处理器的“开创性进展”​,并评价寒武纪团队在深度学习处理器研究中“居于公认的领导者行列”[插图]
  • 例如,2016年,谷歌推出了张量处理单元(Tensor Processing Unit, TPU),至今已迭代至第4版;2017年,英伟达开始向其GPU产品中加入矩阵运算单元“Tensor Core”​,标志着GPU开始由单纯的向量处理器架构演进为一种向量处理器与深度学习处理器的混合架构;2018年,华为将其自研深度学习处理器架构命名为“达芬奇架构”​,并开始推出基于达芬奇架构的昇腾系列处理器产品。

 6.4 大规模深度学习处理器

  • 单个深度学习处理器每时钟周期最多能够处理Ti×To个乘加运算(每个乘加运算对应神经网络中的一个神经元连接)​,而处理器芯片的时钟周期又受到工艺制约,因此矩阵运算器硬件规模成为深度学习处理器峰值算力的主要决定性因素。典型的Ti、To值一般设定在8到128之间,最大能够达到的峰值算力通常不超过100 TOPS[插图]。虽然这样的峰值算力已经远远超过了相同规格下的通用处理器、向量处理器能够达到的水平,但遇到当今深度学习大模型任务时,仅以这种水平的算力去应对,还是远远无法满足需求的。例如进行GPT-3的推理运算时,具有100 TOPS算力的单个深度学习处理器大约需要运算10秒左右才能得到1个词元;要生成一段完整的对话内容,需要数个小时。从应用角度来看,这是不可接受的。
  • 当算法规模与硬件规模之间不能整除时,运算数据需要补零,导致运算器、便笺存储器、主存储器、DMA等硬件资源整体利用率不能达到100%。将运算器规模扩得越大,能够以高利用率填充硬件资源的算法模型便越少,模数效应便会越严重、越频繁出现。在这个问题上,谷歌TPU的初期设计提供了一个典型的教训[插图]。如图6.43所示,在TPUv1设计[插图]中,谷歌的架构研发人员将矩阵运算单元设置成了256×256大小,理论峰值算力达到了92 TOPS。然而,这种设计在诸多深度学习模型上都表现出了严重的模数问题,实际任务中典型的算力利用率只能达到10%~20%。在TPUv2设计[插图]中,谷歌的架构研发人员吸取教训,将每核心内的矩阵运算单元规模缩小为原来的1/4,减至128×128,理论峰值算力降至23 TOPS,这才缓解了问题。为了弥补失去的峰值性能,谷歌将两个TPU核心放置在同一块芯片上,又在一块板卡上放置了四块TPU芯片,才达到了单卡184 TOPS的理论峰值算力。在TPUv3设计[插图]中,谷歌的架构研发人员又在每个核心中放置了双份的矩阵运算单元,搭配由工艺和电路设计进步带来的时钟频率提升,达到了单卡494 TOPS的理论峰值算力。
  • 图6.42 模数效应导致运算器利用率下降
  • 相比巨大的单体矩阵运算单元,多个分立的小型矩阵运算单元在使用时更加灵活,容易达成更高的利用率。但相应地,将矩阵运算单元切分成多个,会降低运算密度,导致硬件成本相应上升。
  • 多个核心之间需要采用某种片上网络(Network-on-Chip, NoC)与主存储器进行互联,然后通过在NoC上的单播、多播、同步等通信原语完成核间协作和对主存储器的访问。数据总线是一种简单的NoC,我们在本章将以数据总线为例,留待第7章介绍其他类型的NoC。
  • 时分复用的含义是,同一时刻只有一对组件可以占用总线进行通信;当有多对组件同时发出通信请求时,总线通过仲裁的方式决定这些组件占用总线通信的先后顺序。
  • UMA结构中的多个核心可以通过数据并行或模型并行的方式同时利用起来。
  • I/O复杂度理论指出,在进行高级算子的运算时,运算密度随着基本算子的规模增加而增加,维持相同运算量所需的访存将减少。
  • 由于有了局部存储器的缓冲,这些核心对局部存储器进行频繁的访问,访问不再落至主存储器上;局部存储器以更低的频次与主存储器交换数据,降低了对主存储器的访问压力。局部存储器以内的执行行为相对其外部是独立的,因此我们将这些核心连同局部存储器、控制器、运算器和DMA视作一个整体,称为一个深度学习处理器核心簇(cluster)。
  • 在这种结构下,每个深度学习处理器核心一般需要通过局部存储器来访问数据。越过局部存储器访问主存储器或其他簇中的局部存储器虽也可以间接地访问,但效率更低。因为访问各个存储器的效率不再一致,与UMA不同,这种结构称为非一致性访存模型(Non-Uniform Memory Access, NUMA)。
  • 利用高级算子的规模增加导致运算密度改良的性质,这样进行大规模扩展,适当增加每一层次中局部存储器的容量和访存带宽,就可以实现较高的整体利用率。如此可以构建非常大规模的深度学习处理器架构。因为这种构建方式遵循了系统结构上每一个层次的自相似性,所以我们借用几何学中分形的概念,将其称为分形计算模型(Fractal Parallel Model, FPM)[插图]。
  • 对分形计算模型的多核心深度学习处理器进行编程时,由于每个簇都对外进行了封装,程序只需要管理一个层次结构上算子的分解。分解分为两步:首先进行并行分解,使得簇中每一个核心都分配到等额的运算任务,能够并行开展运算;其次进行串行分解,将算子的粒度分解至合适的大小,直到核心内的局部存储器能够容纳为止。
  • 在每个DLP核内,运算器约占有60%的电路面积,比例显著高于控制、访存、便笺存储器等部分(见图6.47c)​。16个DLP核共占据芯片面积的一半,因此整块芯片上有30%的电路是真正用于运算的,这一比例远胜于通用处理器。这个事实证明了:深度学习处理器确实如我们所愿,有效减少了控制、寻址的负担,并能在同等访存能力下提供更强的运算性能。

 6.5 本章小结

  • 我们挖掘了不同种类处理器结构设计背后蕴含的理论原理,如运算密度和I/O复杂度,这些原理能够提供方向性的指导。

 习题

  • 6.6 标量是零维的张量,向量是一维的张量,矩阵是二维的张量。深度学习模型所使用的数据常常是三维、四维的张量,为什么以二维张量运算作为基本运算的处理器就可以称为深度学习处理器?

 第7章 深度学习处理器架构

  • 矩阵运算单元有两种经典实现方式,一种是通过内积单元的堆叠构成的矩阵乘向量单元、矩阵乘法单元,另一种是由乘加器、寄存器组成的网格状的脉动阵列机。
  • 内积单元(见图7.2c)则是在向量乘法器之后增设了加法树,将所有乘法计算的积加和为一个结果。内积是一级BLAS算子,与向量乘法具有固定的运算密度(0.33)不同,增大内积单元的规模可以略微改善运算密度。例如图7.2c所示长度为2的内积单元运算密度为0.6,而图7.2d所示长度为4的内积单元运算密度为0.78。继续增大规模,内积单元的运算密度极限为1。
  • 2025/07/12 发表想法

     

    原文:虽然内积运算密度较向量乘法稍高,但仍然不足以高效承担深度学习的主要运算。我们将多个内积单元叠加起来,如图7.3a所示,可以获得一个基本的矩阵乘向量单元。其中,每一个内积单元将相同的激活值与不同的权重进行内积运算,激活值向量相当于矩阵乘向量运算中的向量,而多个权重向量相当于矩阵乘向量运算中矩阵的各个行。图7.3a展示了2个长度为4的内积单元叠加形成的矩阵乘向量单元,每个周期[插图]可以完成高为2、宽为4的矩阵与高为4的列向量之间的乘法。 因为两个内积单元分别完成各自的功能,目前这个矩阵乘向量单元的运算密度(0.78)与内积单元是一样的,但是我们可以通过其他办法降低数据访问的成本。其一,考虑权重存储器的实现方式。因为各个内积单元所访问的权重数据不同且互不交叠,我们可以将权重存储分割开来,为每个内积单元独立设置。如图7.3b所示,我们将权重存储器做得尺寸更小、速度更快,并且在电路中贴近内积单元的位置放置。如此一来,权重访问的开销大大降低,每个周期访问权重数据的宽度也比较容易地扩展得很宽。因此,我们在统计运算强度时,不妨暂时忽略权重访问,将权重数据的存储器视为运算器结构内的一部分。

  • 如图7.3c所示,改用广播后,多个内积单元对激活值向量的访问开销与单个内积单元的情况非常接近,在统计运算强度时,不妨按照一次访问来计算。采用这两项改动后,这个矩阵乘向量单元的运算密度可以上升至2.33。
  • 图7.3 堆叠多个内积单元构成矩阵乘向量单元
  • 寒武纪架构即采用矩阵乘向量单元作为其矩阵运算单元。
  • 矩阵乘向量单元由于同时符合神经网络全连接层、卷积层的运算模式,在深度学习模型推理场景下有着最高的硬件利用率。虽然矩阵乘向量运算本身只是二级BLAS算子,但凭借着硬件利用率优势,矩阵乘向量单元仍然是一种非常常用的矩阵运算单元。但是,如果面向卷积等运算设计深度学习处理器,例如要设计用于深度学习模型训练的架构,可以进一步采用三级BLAS算子的矩阵乘法作为基本运算。
  • 可以看出,矩阵乘法单元的运算密度随着规模的增加而快速上升,这与I/O复杂度理论的预测一致——一个规模为N×N×N的矩阵乘法单元,其运算密度为[插图]。
  • 达芬奇架构即采用矩阵乘法单元作为其矩阵运算单元。
  • 我们知道,朴素矩阵乘法由三重循环组成,最内层循环描述的是左矩阵的行向量与右矩阵的列向量之间的内积运算,外层两重循环则遍历输出矩阵的每一个元素。
  • 第一个周期,首先读取一行激活值a, b,读取一列权重v, x,计算它们的内积,即得到输出矩阵的第一个数据av+bx;第二个周期,激活值保持不变,更换权重为y, z,计算它们的内积,得到输出矩阵的第二个数据ay+bz;第三个周期,因为输出矩阵的一行已经计算完毕,开始计算第二行,读取第二行激活值c, d,读取一列权重v, x,计算它们的内积,得到输出矩阵第二行的第一个数据cv+dx;第四个周期,激活值保持不变,更换权重为y, z,计算它们的内积,得到输出矩阵的最后一个数据cy+dz。为内积单元输入哪一行数据是由其调度单元(控制器)控制的,调度单元可以通过矩阵的尺寸、数据在便笺存储器中的起始地址来推算每个周期所需访问的便笺存储器地址,使便笺存储器在对应的周期为内积单元提供正确的数据。
  • 权重矩阵是按列访问的,因此便笺存储器存储权重时,可以将数据转置,以便笺存储器的一行对应矩阵的一列。
  • 图7.5 时分复用内积单元完成矩阵运算
  • 矩阵乘法单元中,各个内积单元组成了一个二维阵列,每个内积单元可以在一个周期内完成一个输出数据的计算,因此可以将输出矩阵的每一个数据映射至对应位置的内积单元上。这样,阵列每一列上的内积单元共享相同的激活值行向量,每一行上的内积单元共享相同的权重列向量。便笺存储器同时提供多行激活值、多列权重,并通过广播发送到对应的内积单元上,所有内积单元即可同时开始内积运算,在一个周期内产生输出矩阵的全部运算结果。
  • 图7.6 使用矩阵乘法单元在一个周期内完成矩阵运算
  • 矩阵乘向量单元的高效率是依靠切分存储权重的便笺存储器实现的,而且需要激活值的广播;矩阵乘法单元更依赖于激活值和权重的广播。在处理器芯片的电路设计中,广播是依靠电子元件的扇出(fan-out)实现的,即多个元件的输入信号取自同一个元件的输出。大规模的广播数据通路会导致电路扇出太大,就如同让一匹马同时拉动一连串车厢,虽然好过不采用数据共享的设计,但设计规模过大时将会不可避免地导致电路运行速度降低。另外,从前文的多张示意图中也可以看出,这两种运算器的内部结构都依赖于一系列横贯其间的长数据
  • 通路,也会导致电路面积大、功耗高、延迟长,物理设计困难。因此,深度学习处理器架构的设计者不可以无节制地扩大矩阵乘向量单元和矩阵乘法单元的规模,而要根据实际应用的情况辩证地决定。
  • 7.7所示,不同于堆叠内积单元带来的长通路、
  • 每一个运算单元内部都带有寄存器,可以将任何输入给它的数据缓冲一个时钟周期,然后输出给下一个运算单元。这样,从便笺存储器送来的输入数据不需要经过扇出电路同时广播至所有运算单元,而只需要送给第一个运算单元,然后只要花些时间,等待它依次传递至后续运算单元就可以实现数据共享了。因为数据每经过一个时钟周期向前前进一个运算单元,数据流动的方式酷似人体的血流和心跳,所以得名“脉动”​。在脉动阵列机中,所有的数据通路都是局部连接,只有距离最近的两个运算单元之间才相连;也不需要通过扇出进行多个运算单元间的数据共享。因此,在矩阵运算单元规模特别大时,采用脉动阵列机的方式实现,可以降低硬件成本,改善电路的面积、功耗、时延。
  • 图7.8 脉动阵列机的计算过程
  • 图7.9 脉动阵列机的数据输出
  • 1944年,英国巨人计算机二型(Colossus Mark II)采用了类似脉动阵列的方式构建。它用于破译纳粹德国的军事密文,属于高度机密,因而长期处于保密状态。战争结束后,巨人计算机被销毁,这一设计思想未能公诸于世。现在人们所采用的脉动阵列机概念是由孔祥重、查尔斯·莱瑟森(C. E. Leiserson)于1978年发明的[插图]。脉动阵列机是算法在集成电路上的实现方式,他们发明了对应多种算法的脉动阵列机,用于矩阵乘法、线性方程组求解、矩阵LU分解、最大公约数的计算等各种应用。
  • 通常只有专门针对深度学习大模型训练等规模极大的深度学习处理器设计,才会采用脉动阵列机作为矩阵运算单元。
  • 图7.10展示了一个4通道、4×4尺寸的特征图,每2×2窗口进行平均池化的结果。可以注意到,特征图的每一个点对应着便笺存储器中的一行(或多行)数据,即一个向量。进行池化即需要对滑动窗口范围内的多个向量进行平均运算。
  • 我们可以在向量运算器附近设累加寄存器,专门用来存放中间结果,更高效地支持类似于池化这样的连续向量运算。
  • 函数的定义域为(-∞,+∞),值域为(-1, 1)。因为该函数能将输入数值缩放至固定的值域范围内,而经常被深度学习模型作为激活函数采用。
  • 图7.14 实现分段函数功能的物理模块图
  • 在一些特殊情况下,如果需要特殊函数的精确计算,可以通过搭载特殊硬件单元或通过普通标量/向量指令进行编程实现。实现方式主要是依据各种函数的快速算法(例如Beame-Cook-Hoover快速倒数算法)​、数值方法(例如牛顿迭代法)等,对不同函数进行一事一议的设计。还可以采用分段线性表示或其他近似算法,先获取非常接近于精确值的近似估计,然后再经过数值方法对最终误差进行收敛得到精确值,可以实现既快速又精确的计算(例如“0x5f3759df”快速平方根倒数算法)​。
  • 图7.16 序列长度为4的前缀计算
  • 能够实现数据重排布的硬件结构一般称为交换网络(permutation network),这种结构起源于电话交换机,也是计算机网络设备中常用的基础结构。我们将数据重排布任务视作向量不同列之间开展的网络交换活动,在深度学习处理器芯片中构造交换网络。
  • 图7.20 两种交换网络的构建方法

 7.2 存储

  • 解决拥堵的思路有两方面。一方面是拓宽数据的通路,通过适当增加硬件成本,让便笺存储器具有同时服务更多访问请求的
  • 能力;另一方面是规划数据的流量,通过对数据进行分类处置,使整个深度学习处理器架构中数据访问不再聚焦于一点,分担便笺存储器的访问压力。通常而言,我们会同时从两方面考虑问题,以便达成架构设计的平衡。
  • 要进行更多的便笺存储器访问优化,便仰赖于对深度学习处理器中数据流的规划。深度学习程序的执行流程有规律,我们应当予以充分利用。
  • 回顾通用处理器结构,当取指令与取数据发生冲突时,采用了哈佛结构的分离式缓存来解决。这是因为指令与数据天然具有不同的访问特性,二者的地址空间极少交叠;指令对程序而言在绝大多数情况下是只读的,从不发生变化,而数据则可读可写。将指令存放在数据缓存中,不仅产生了缓存的访问冲突,也确实浪费了一部分硬件资源。因此,我们采用分离式缓存设计,将指令单独存储在一块缓存中,与数据分开。
  • 观察图6.2c可以看到,在真实的处理器芯片中,一级指令缓存与一级数据缓存具有相同的容量,但一级指令缓存占据的电路面积明显更
  • 小。
  • 深度学习程序中的数据可以划分为输入神经元数据、权重数据和输出神经元数据三种。运算时,矩阵运算器需要输入神经元数据和权重数据,写出输出神经元数据,数据流向也是单向的;而且权重数据与指令类似,在绝大多数情况下是只读的。因此我们可以将便笺存储器进行功能划分,例如采用三分离式设计,单独设置输入神经元存储器、权重存储器和输出神经元存储器,如图7.22a所示。数据按照既定的流向,从输入神经元存储器/权重存储器经过运算来到输出神经元存储器;如果数据偶尔需要从输出神经元存储器返回输入神经元存储器或权重存储器,则通过访存通路(此处为DMA)对数据进行传输
  • 计算机体系结构的核心在于约束;体系结构设计人员的职责就是要深入探究处理器所要的具体场景,为计算机的架构设计出一组高效而合理的约束方法。
  • 图7.22 分离式便笺存储器的多种设计方
  • 深度学习处理器中负责外部存储器访问的是直接访存模块(DMA)。
  • 在通用处理器内部通常是不设置直接访存模块(DMA)的,而是将访存设置在指令流水线当中。例如,在经典的MIPS风格五级指令流水线当中就专门设置了访存流水级(MEM),每一条指令(即使不是访存指令)在流水线中都会经过这一阶段。在这样的通用处理器上,需要单独使用一条装载(load)或存储(store)指令来访问一个数据,过程如图7.23所示。装载指令访问得到的数据可以通过流水线数据前递开关送入下一条计算指令的执行级(EX);计算指令的执行结果可以再通过前递送入下一条存储指令的访存级。为了实现前递,计算指令必须插入空泡,延迟一个时钟周期来执行,这一个时钟周期的延迟在通用处理器中经常被视为需要针对性优化的关键问题。
  • 图7.23 通用处理器MIPS风格五级指令流水线时空图
  • 通用处理器的访存粒度以标量为单位,持续时间在数个时钟周期的量级,对于深度学习应用而言过于细碎,造成控制开销大。假设需要处理大小为224×224×3的图像,通用处理器需要执行30万条装载/存储指令。大量的装载/存储指令不仅导致处理器浪费许多能量来完成它们的取指、译码、执行,还将填充、挤占指令流水线,干扰计算指令的执行。所以,深度学习处理器需要以更大粒度来设计访存指令(向量装载/存储指令、矩阵装载/存储指令)​,将访存过程的细粒度控制职责从指令流水线分离出来,下放给具体的执行单元。这个具体的执行单元就是直接访存模块(DMA)。
  • DMA是访存控制任务的代理者。如果不使用DMA,那么处理器必须通过指令操控访存的细粒度行为,每个时钟周期都亲力亲为控制数据总线。如图7.24a所示,在装载大量数据的时候,处理器的指令流水线被大量烦琐重复的装载指令所占据,没有余力处理计算。为了将处理器从这些访存细粒度控制任务中解脱出来,我们增设DMA来代理数据总线的控制任务。
  • 图7.24 DMA代理控制访存行为
  • 图7.25 深度学习处理器指令流水线时空图
  • 指令周期似乎只剩下一个流水线阶段,即DMA访存或执行两种之一。这并不是因为取指、译码等过程消失了,而是因为在深度学习处理器的指令周期中,DMA访存和执行能够持续成千上万个时钟周期,相对取指令、译码等过程的一两个时钟周期而言,访存和执行过程的时间占比已经过于悬殊,使得其他流水线阶段从图示上消失了。
  • 如图7.27所示。如果我们允许深度学习处理器在之前正在执行的指令尚未完成时,就先发射、执行之后的指令,就可以实现计算指令与访存指令的并行。
  • 为了确保程序执行正确,具有数据依赖的指令之间仍然必须保持顺序执行。在通用处理器中,依赖关系是由指令流水线硬件上的发射单元负责判断的;依赖的具体表现形式简单,即判断两条指令是否使用了同一个寄存器。而在深度学习处理器中,因为指令可以操作张量数据,但数据在存储器中并不一定连续排布。判断两条指令所访问的数据是否交叠变成了一项颇有难度的问题,需要求解线性同余方程组(丢番图问题)​。所以,深度学习处理器中额外添加了同步指令(sync),来显式地指导指令流水线是应该继续发射、并行执行,还是需要等待之前的指令完成以保证满足依赖关系。
  • 在编写深度学习处理器上的程序时,即使指令允许操作能够填满便笺存储器的整块数据,通常我们也倾向于只使用便笺存储器不超过一半的容量,以便将计算与访存并行。让计算指令与访存指令总是分别工作在便笺存储器的不同半区,也有利于设计高效的分组访问便笺存储器。
  • 图7.28 简化的硬件同步机制
  • 当数据分为更多组时,指令的最佳排布顺序可以按规律构建,形成软件流水线(software pipelining),如图7.29所示。其中,奇数组指令(load 1/3/5、compute 1/3/5、store 1/3/5)工作在便笺存储器的一半,而偶数组指令(load 2/4/6、compute 2/4/6、store 2/4/6)工作在便笺存储器的另一半,实现互不干扰的并行工作。软件流水线是一种古老的程序优化技巧,最初用于在通用处理器上挖掘指令级并行性;在这里,我们将其用于挖掘计算指令与访存指令之间的并行性。在通用处理器的编译器中也提出了多种自动构建软件流水线的方法,例如模数调度(modulo scheduling)[插图]等,可以迁移至用于深度学习处理器的编译器中,自动实现高效的计算和访存并行
  • 图7.29 由装载-运算-存储三阶段组成的软件流水线

 7.3 通信

  • 一个互联网络可以视作一个图,由深度学习处理器(核心)构成结点、物理通信链路构成边。构成图的规则称为拓扑。在互联网络中常用的拓扑有很多种,例如总线(bus,见图7.30a)​、环(ring,见图7.30b)​、二维网格(2D-mesh,见图7.30c)​、二维环面(2D-torus,见图7.30d)​、超立方
  • (hypercube,见图7.30e)​、胖树(fat-tree,见图7.30f)​、全连通网络(通过交叉开关阵列等方式实现,见图7.30g)等。其中,总线和环的实现成本较低,但是结点间连通度也较低,执行某些集合通信操作时性能可能受限;网格和环面的实现成本和结点间的连通度均适中;超立方、胖树、全连通网络具有较高的实现成本,但是结点间的连通度也较高,多数时候可以获得更好的通信性能。
  • 图7.30 常见的互联网络拓扑
  • 在训练过程中,深度学习程序的编写者可以采用数据并行、模型并行等多种并行模式。多对多归约(all-reduce)是分布式训练过程中最常用到的集合通信原语;如果采用模型并行中算子内并行的方式实现分布式训练,则还需要使用到多对多交换(all-to-all)集合通信原语。多对多归约与多对多交换是深度学习分布式训练过程中最为常用的两种集合通信原语(其余通信需求几乎全部为简单的点对点通信)。
  • 实现了多对多交换,即实现了所有不含归约运算的集合通信原语;实现了多对多归约,即实现了所有含归约运算的集合通信原语,以及障碍同步(barrier)原语。
  • 如果在该互联网络拓扑内能够高效地模拟一条环状链路,即互联网络拓扑中存在哈密顿环(Hamiltonian cycle)[插图],则在模拟的环路上采用环状链路的多对多归约算法就已经足够优秀,故没必要另外开发其他算法。
  • 二维网格的另一个优势在于执行多对多交换的速度更快,所以当用户选择算子内并行、混合并行模式时,能够在一定程度上提升系统的整体性能。如果进一步在二维网格的边缘增加回环通路,就形成了二维环面拓扑,二维环面拓扑相比二维网格的灵活度更高。谷歌的TPU机群(pod)即采用二维环面拓扑构建多个处理器芯片间的互联网络[插图]。
  • 更为极端的例子是英伟达DGX-2计算机,其中采用全连接拓扑构建全部16个GPU之间的高速互联网络。但是,DGX-2内部仅仅为搭建这个互联网络的交叉开关,就额外使用了12颗100mm2的芯片。这些芯片共消耗功率1200 W,功耗占比达到了全系统的12%[插图]。

 *7.4 设计优化

  • Strassen算法[插图]是一种采用分治法的矩阵乘法算法,可以使用更多的矩阵加减法运算代替矩阵乘法运算,将矩阵乘的时间复杂度降低至O(N2.81)。Coppersmith-Winograd算法[插图]进一步增加加减法运算取代乘法,将矩阵乘法的时间复杂度降低至O(N2.38);
  • 对两个长度为N的一维数列计算卷积,可以首先通过FFT将两个数列转换至频域,将卷积运算削减为对位乘法运算,从而将计算过程整体的时间复杂度从O(N2)降低至O(N log N)。类似的方法经过改造后,也可以运用于神经网络的卷积层。但由于将卷积神经网络层转换为适合FFT的数据表示这一过程带来了显著的算法开销,该方法仅适用于处理卷积核尺寸非常大的卷积运算。研究者发现,在使用FFT方法进行卷积神经网络的计算时,为了避免在时域与频域间反复变换,可以将参数与算子均映射到频域内,完全在频域内进行网络的训练,从而省去在卷积层之间额外进行的傅里叶变换与傅里叶逆变换过程[插图]。对于卷积核较小的卷积运算,可以运用Winograd最小滤波算法来加速。Winograd最小滤波算法运用中国余数定理,将短数列与长数列之间的卷积运算拆解为多个卷积,最终将总计算时间复杂度降低。将Winograd算法运用于神经网络卷积层时,形式是将卷积核和特征图分别进行线性变换,转换为新的矩阵形式
  • DWM算法针对这些问题,提出首先将大卷积核分解为数个小卷积核[插图]。采用DWM算法后,对更大的卷积核以及卷积步长不为1的情况也得以高效运用Winograd算法加速卷积计算。
  • 如果将关注点落至访存的时间复杂度(访存数据量)上,我们还会发现一些其他更实用的变换方式,例如算子融合。它将深度学习模型中多个算子融合在一起进行计算,使得前一个算子的计算结果直接用于下一个算子的输入,从而减少了神经网络层与层之间的中间结果造成的主存储器访问开销[插图]。
  • 这些新读取的数据还有可能通过合理利用片上便笺存储器空间,实现进一步的数据复用。采用这种计算策略,只需要读取输入特征图即可完成两层卷积,省去了中间特征图的存储和装载过程,降低了访存数据量,在多数情况下可以带来明显的性能提升。
  • 为了让训练过程更容易收敛,深度学习模型通常被设计为在一定程度上过参数化。深度学习模型的参数中存在相当一部分冗余数据,在计算过程中无实际贡献。
  • 这种压缩过程通过专门的深度学习处理器架构设计,完全在推理过程进行(例如稀疏剪枝);有时需要同时改动训练算法予以配合(例如结构化稀疏)。另外,通过将基本算术运算由比特并行改为逐比特串行,还能在每个数据内部寻找到无效的数据比特,节约一部分运算量,这种方式称为串行计算。
  • 删减作用较小的连接权重会对模型预测性能造成轻微的影响,这种影响通常可以通过剪枝后再次训练微调来复原,最终得到推理数值精度几乎无损的稀疏模型。模型的参数矩阵是含有大量零值的稀疏矩阵,通常采用压缩稀疏行(Compressed Sparse Row, CSR)等方式更高效地存储模型。
  • Cambricon-X[插图]是一种能够有效利用神经网络稀疏性的深度学习处理器架构。在存储架构上,它采用稀疏存储映射节约便笺存储器空间;在计算架构上,它通过索引模块选出需要计算的数据并传输到矩阵运算单元。如图7.37所示,假设有6个输入的神经元(0~5),3个输出的神经元(0~2),矩阵运算单元的规模是4×4,便笺存储器的行大小是4字。通过采用稀疏存储映射,可以将数据中的零排除,将需要存储和计算的数据由6行压缩至3行,运算速度可以相对提升一倍。
  • 为了描述剩余权重数据所在的位置,需要引入额外的索引信息,冲抵了一部分压缩节约的数据量;实现数据筛选模块需要引入复杂的交叉开关电路,其面积甚至会超过矩阵运算单元本身的面积。为了降低不规则分布的权重数据给硬件带来的挑战,研究者提出了结构化的稀疏剪枝,要求剪枝必须按照某种规律来进行。结构化稀疏的一种方式是限制在参数的某个特定维度(例如特征通道、卷积核)上进行剪枝操作,生成一个更小的稠密模型结构;另一种方式是限制权重数据按照某种规律来分布,例如要求剩余有效权重数据聚集成簇(称为“粗粒度稀疏”),或要求在每M个原始权重中必须包含且仅包含N个有效的权重(称为“M选N稀疏”)。有时,为了达成这些限制条件,必须在训练的过程中通过引入算法改动来配合架构创新。
  • Cambricon-S[插图]架构是一项基于粗粒度稀疏剪枝算法所作的算法与体系结构协同创新的工作。研究者发现,在模型训练的过程中权重会呈现出局部富集的现象,即较大的权重往往会聚集成小簇。以全连接层中的权重矩阵为例,如图7.38所示,其中白色像素点表示了绝对值大小在前10%的权重,可以看出绝对值较大的权重呈现簇状分布。基于以上观察,Cambricon-S提出首先进行粗粒度稀疏剪枝,然后再进行局部的细粒度稀疏剪枝,减小了索引信息的大小,提高了网络压缩比。
  • PE之间共享突触索引模块有助于减少索引模块的面积开销和数据传输的带宽需求;每个PE内部添加的局部索引模块进一步利用了不规则的稀疏性。与Cambricon-X相比,Cambricon-S架构性能提高了1.71倍,能效提高了1.37倍。
  • 稀疏剪枝方法需要某一权重或激活值完全为0才能进行压缩和加速,而串行计算能够进一步节约单个数值中的比特0部分,实现更细腻的压缩。例如,Stripes架构[插图]中采用了跳过单个数值中的比特0的思路。研究表明DNN计算所需的数值精度在不同网络之间、同一网络的不同层之间都存在显著差异。大多数加速器的实现都依赖于全局一刀切的方法,为所有数据设定统一而固定的数值精度。Stripes架构在比特串行的运算器中添加硬件部件,通过仅计算非零项、跳过无效的前缀和后缀来提升性能和能效。
  • 不同于以往科学计算的模式,深度学习模型本身预测的结果并不能达到100%的准确,而且神经网络的数值鲁棒性允许它接受一定程度的输入噪声和数值误差而不会显著影响模型性能。这就使得我们可以将难以计算的复杂算子替换为数值上相似但更容易计算的简单算子,通过近似加速深度学习模型的计算过程。常见的近似方法有数值量化、算法近似和随机计算等。
  • 常见的量化后数据格式有低位宽浮点数(例如FP4)、低位宽定点数(例如INT4、INT8)等。常见的高位宽数据到低位宽数据的转换方式有对称均匀量化、对数域量化等。
  • 数值量化通常只能用于深度学习推理过程中,因为深度学习模型的训练对数值误差更为敏感,直接采用数值量化技巧会导致性能显著恶化,甚至造成模型的崩溃。针对这一问题,研究者提出了一些量化训练算法来弥补,但是这些算法需要对张量数值进行统计,无法在现有的GPU或深度学习处理器上高效运行。通过增加专门的统计量化硬件支持,Cambricon-Q架构首次实现了高效的深度学习量化训练过程[插图]。
  • 一类算法近似方式是低秩分解方法,可以通过SVD分解[插图]、CP分解[插图]、Tucker分解[插图]等方法,将权重矩阵进行特征分解,分解成多个保留了主要特征的较小矩阵的乘积[插图],减少计算量。如图7.41所示,将计算从n个神经元到m个神经元的全连接层的权重矩阵按秩r进行分解,将权重矩阵的大小从mn减小到了r(m+n);同时,因为矩阵乘法符合结合律,通过改变运算次序,乘法运算次数也从mn减小到了r(m+n)。当r较小时,就显著减小了计算与访存的开销。
  • 另一类算法近似方式是差分计算。差分计算在数值之间的差分上(而非原始完整数值上)完成卷积、全连接等线性算子的运算,以便获得更高的稀疏性和量化潜力,降低运算强度。例如Diffy架构[插图]提出,由于图像具有局部相似性,所以图像的邻域差分值所包含的信息量更精简,比原值更容易表达。如图7.42所示,使用当前卷积窗口与上一个卷积窗口的差分值(邻域差分值)来计算卷积层,再将差分值的卷积结果与上一个窗口的卷积结果相加,就还原得到了原始的卷积结果。这种方法一方面缩小了输入数据的取值范围方便量化,另一方面差分值中会出现更多的零值可以进行剪枝,同时还保证了卷积运算的精度,达到了加速卷积、全连接层等线性运算的目的。
  • 图7.42 内积结果从一列传播到下一列的差分卷积
  • 在传统的数字计算中,需要使用二进制表示数值进行计算,涉及复杂的加法器、乘法器和逻辑门电路等运算器件。而随机计算中,数值被编码成随机比特串,以随机比特串中比特1出现的概率来表示数值。这种编码方式将复杂的数值计算映射到了概率计算中,大大简化了运算单元的设计与实现。例如,在随机计算中,乘法器可以仅通过一个与门实现,将两个随机数中出现1的概率相乘映射为两个随机数中每一位同时出现1的概率,如图7.43a所示。类似地,加法器也可以通过两路选择器实现,以[插图]的概率选择两个随机序列a和b中的值,输出的随机序列中1的数量为[插图],如图7.43b所示。随机计算虽然大大简化了运算,但却损失了运算的精度,且大大增加了数字的位宽。因此,随机计算一般只适用于对精度要求不高且操作数位宽很低的运算[插图]。
  • 随机计算中的运算器
  • [插图] 图7.45 基于忆阻器的存内计算范式 7.4.4.2 类脑架构与神经形态计算 神经形态计算是一种以类脑计算为基础的架构设计,旨在使用脉冲神经网络等生物启发的计算模型模拟人脑的神经元结构和运行方式,实现高效的神经功能模拟和智能功能。

 第8章 智能编程语言

  • 如图8.1所示,传统编程语言和智能计算系统间存在三方面的鸿沟:一是语义鸿沟,传统编程语言难以高效地描述高层智能计算语义,导致智能应用程序的开发效率较低;二是硬件鸿沟,传统编程语言难以高效地抽象智能计算硬件特性,导致最终生成的代码执行效率较低;三是平台鸿沟,智能计算硬件平台种类繁多且在不断增长,针对特定平台优化的程序难以实现跨平台可移植,即在不同平台上都可以正常执行并达到较高的计算效率。
  • 如图8.2所示,使用纯标量计算的C++语言编写的卷积运算包含7重循环,而采用具有向量(即array)语义的Python语言编写的卷积运算只需要4重循环即可完成。
  • 为进一步提高开发效率,除了直接提供智能核心操作(算子)级别的高层操作语义,智能编程语言的抽象层次还在不断提高,向高层次和专用化的方向发展,如面向语音识别的编程语言Kaldi[插图]和面向自动驾驶测试场景生成的编程语言Scenic[插图]等。图8.3提供了Scenic编程语言的示例。Scenic本质上是一种面向特定领域智能任务(即自动驾驶测试场景生成)的概率编程语言(Probability Programming Language),可以通过指定概率分布的方式来生成满足约束的物理世界及智能体。在这个例子中通过3行代码即可以生成一个典型的测试场景:车辆停在马路边缘左侧0.5m处,同时车头与马路边缘呈10°~20°夹角。当然,上述语言仅面向特定应用场景(即语音和自动驾驶),无法满足各种不同智能应用场景的普适需求。
  • 图8.2 使用不同语言实现的卷积运算示例 [插图] 图8.2 (续) [插图] 图8.3 面向自动驾驶测试场景生成的编程语言Scenic的示例代码 8.1.2 硬件鸿沟 与传统通用处理器相比,智能计算硬件在控制、存储及计算等逻辑上都有显著特点。
  • 以视觉处理场景下的推理任务为例,很多场景下(如图片分类和目标检测等)8位定点运算器的精度就可以很好地满足任务需求。如图8.6所示,针对典型的深度学习算法,与FP32运算相比,将数据格式缩减为INT8或FP16造成的精度损失几乎可以忽略。以典型的分类网络ResNet-50为例,改用INT8和FP16数据格式,相比原来单精度版本的Top-1准确率损失分别仅有0.1%和0.2%。此外,如表8.1所示,考虑到运算器的硬件实现,改用低位宽的运算器面积和功耗得到了极大的降低。以INT8运算器为例,与FP32运算器相比,其面积和功耗开销分别减少了85.54%和85.73%[插图]。
  • 传统编程语言中主要提供的是INT32和FP32等数据类型,导致难以利用智能计算系统中更加丰富和高效的运算单元,如FP16、BF16、INT4、INT2以及一位二进制数等数据类型。显然,越高层的编程语言对硬件的抽象层次越高,硬件特性也屏蔽得更加彻底。我们希望理想的智能编程语言既可以对特定智能任务有较高层次的抽象,又可以向用户提供足够丰富的硬件细节。这需要在抽象层次和硬件细节中寻找平衡点,同时满足高开发效率和高性能的需求。
  • 目前在传统CMOS工艺上已经有包括CPU、GPU、FPGA和ASIC等在内的多种不同形态。新型计算器件(如数模混合计算器件、光电混合计算器件以及忆阻器和非易失相变存储器件等)的出现,进一步丰富了底层的智能计算硬件。
  • 以图8.4为例,其中的矩阵乘法是在Intel x86的平台上进行的优化,最终调用了AVX指令对应的intrinsic函数,如果在没有AVX支持的平台(如ARM处理器)上,该程序将无法执行,从而带来可移植性的问题。通过提升语言的抽象层次,例如定义与算法语义更接近的API函数(如BLAS),而不是直接通过intrinsic函数编写底层指令,可以一定程度上填补不同平台的鸿沟。但是,这一解决方案又给性能可移植性(performance portability)带来了挑战,即在特定平台上优化好的程序,在新的硬件平台上执行效率可能会急剧下降。仍然以BLAS接口为例,如果要达到良好的性能可移植性,要求专家程序员在不同的平台(如x86、ARM以及GPU等)上都进行专门的定制优化,在语言层面只暴露定义良好、广泛接受的API给用户使用。在不同平台上的专门手工优化显然带来了极大的开发代价。
  • 现有的领域专用语言,如面向逻辑推理的Prolog、面向图像处理的Halide[插图]以及面向深度学习的TVM Relay[插图]等,遵循前述智能编程语言的设计原则,力图同时对特定领域的应用和硬件进行抽象。具体而言,Prolog以谓词逻辑为理论基础,针对特定问题(如约束求解、定理证明以及专家系统等)有较高的开发效率,然而由于其主要以搜索和回溯等方式来求解问题,运行效率是非常大的挑战。Halide将计算逻辑与优化逻辑相分离,需要专家程序员针对不同的硬件编写复杂的调度策略(schedule),如循环变换(loop transformation)、分块(tiling)以及线程绑定(thread binding)等,才能在特定平台上达到较好的性能,因此其开发效率仍然面临挑战,特别是针对种类繁多的底层硬件平台。最近的Relay/TVM本质上对深度学习处理器架构进行了一定程度的统一抽象,定义了包括并行(parallelism)、张量化(tensorization)以及延迟隐藏(latency hiding)等在内的核心调度原语(schedule primitive)。基于这些调度原语,采用机器学习的方法(而不是手工实现的方式)自动搜索最优的调度策略。这一思路提升了开发的抽象层次,性能也得到一定程度的保证。但是,随着人工智能算法和深度学习处理器的快速演进,其对智能算法以及硬件架构调度原语的抽象粒度/层次是否最为合理,是不是同时具备高开发效率、高性能和高可移植性(特别是性能可移植性)的理想智能编程语言,仍然是需要进一步深入探索的问题。
  • 的理想智能编程语言,仍然是需要进一步深入探索的问题。 [插图] 图8.7 传统通用编程语言、领域专用编程语言在开发效率、性能和可移植性三大设计原则方面的对比

 8.2 智能计算系统抽象架构

  • 以典型的多核处理器系统为例,整体上由包含控制和计算的处理器芯片以及代表存储的片外存储器组成。其中处理器芯片的计算部分又由包括控制和计算逻辑的计算核心,以及代表存储的片上存储器组成。对于每个核心,其中又包括微体系结构控制路径(如流水线控制)和计算单元(如ALU和FPU等),以及代表存储的局部高速存储器和寄存器等。基于上述观察,我们引入了层次化的智能计算系统硬件抽象。智能计算系统中的每一层都包含存储单元、控制单元和若干个计算单元。其中每个计算单元又进一步分解为子控制单元、子计算单元和子存储单元三部分。整个系统就以这样的方式递归构成,如图8.8所示。在最底层,每个叶节点都是具体的加速器,用于完成最基本的计算任务。
  • [插图] 图8.9 典型智能计算系统的层次化抽象 DLP智能计算系统提供了“服务器-板卡-芯片-核心簇-核心”五个层次。系统内可以直接控制和使用的存储器包括:主机端内存、DLP板卡上的片外存储器、核心簇存储器、核心上的神经元存储器、权重存储器以及寄存器等。应用程序可以通过运行时和驱动程序利用板卡上的计算资源,控制不同存储器之间的数据搬移。 下面详细讨论各不同层次的控制、存储和计算模型。
  • 典型智能计算系统中主机端CPU和设备端DLP都具备计算能力,CPU擅长处理串行控制任务,DLP擅长处理并行计算任务,因此需要将任务分发给不同架构的硬件计算单元(如CPU和DLP)进行异步执行。异步执行所必须的同步操作是通过主机端驱动和设备端驱动之间的通信机制完成的。大规模智能应用负载通常需要被划分到多个核心并发执行。核心簇级启动同一个核函数,核函数可以通过条件语句执行不同的路径,或执行不同数量的循环迭代,指令层面的同步由同步指令控制,多核任务的开始和结束的同步由硬件任务调度器控制。硬件任务调度器会从设备端驱动的任务队列中取出计算任务,根据任务类型所需占用的核心数以及所有核心的当前空闲状态来调度任务。
  • 核心簇存储器可以同片外存储器进行数据交换,由核心簇控制器控制。多个核心簇可以同时访问同一个片外存储器地址,由核间同步指令保证一致性。
  • 神经网络中具有高度并行性的计算,例如卷积、矩阵运算、池化、归一化、激活函数等,都是在设备端由每个核心内的功能单元完成。功能单元包括矩阵运算单元、向量运算单元和标量运算单元,支持FP16、INT8、BF16、INT4等多种数据格式。三种运算单元的具体设计可以参考7.1节。功能单元中卷积和矩阵乘的两个操作数分别存放在神经元存储器和权重存储器中。其他计算如启动程序、模型加载、数据预处理等,则是由主机端完成的。

 8.3 智能编程模型

  • [插图] 图8.10 异构编程的代码范例 主机端编程的目标是实现对整体计算过程的控制,包括设备获取、数据/参数准备、任务描述、核函数调用,以及结果获取等。主机端程序可以通过常见的编程语言(如C++语言)实现。 设备端编程的目标是利用DLP完成需要加速的计算部分。设备端程序主要由智能编程语言(如BCL语言)实现的核函数(kernel)构成。以BCL语言为例,核函数可以通过3种不同的函数构建,包括Entry函数、Func函数以及Device函数。Entry函数是核函数的入口函数,接受指向输入/输出张量的指针,通过调用Func函数或Device函数实现设备上的复杂运算。
  • 主机端编程的目标是实现对整体计算过程的控制,包括设备获取、数据/参数准备、任务描述、核函数调用,以及结果获取等。主机端程序可以通过常见的编程语言(如C++语言)实现。设备端编程的目标是利用DLP完成需要加速的计算部分。设备端程序主要由智能编程语言(如BCL语言)实现的核函数(kernel)构成。以BCL语言为例,核函数可以通过3种不同的函数构建,包括Entry函数、Func函数以及Device函数。Entry函数是核函数的入口函数,接受指向输入/输出张量的指针,通过调用Func函数或Device函数实现设备上的复杂运算。Func函数在编译时将会被内联,可以降低函数调用的开销,通常具有较好的性能(程序运行时间较短)。Device函数默认不被内联,当不被内联其对应的指令被多次调用时可以被重用,因此相比于Func函数具有更小的指令长度和代码尺寸。Device函数和Func函数之间可以互相调用。
  • 设备端程序同样可以是基于C++语言扩展的程序,其对应的编译器为设备端专用编译器。在分别编译得到主机端和设备端的目标文件后,再使用主机端链接器将两份目标文件及运行时库链接生成可执行程序。
  • 设备端驱动程序和硬件任务调度器会查询DLP空闲状态,并将核函数映射到多个核心簇或多个核心上执行。核函数的执行过程是设备端异步完成的,即DLP执行核函数和CPU执行主机端代码是可以并行的。
  • 为了支持多核并行,在逻辑上,把计算负载划分成taskDim个可并行的任务。一维的taskDim可以等价用三维taskDimX、taskDimY、taskDimZ表示,其中三维的任务索引taskIdX、taskIdY、taskIdZ也可以等价用一维的taskId表示。在物理上,任务被调度到不同的核心簇以及核心上执行,核心簇总的数目为clusterDim,索引为clusterId。每个核心簇包括coreDim个核心,核心的索引为coreId。对于任务规模(taskDim)较大的场景,系统会在时间维度或空间维度上展开taskDim/(clusterDim×coreDim)次。例如,一个UNION4类型的taskDim为64的任务在coreDim为4的DLP上要被展开执行64/(4×4)次。
  • 在智能编程语言中,每个核心对应一个BLOCK类型任务。BLOCK是编程模型层的基本调度单位,表示核函数中的任务会被调度到单个核心上执行。每个核心簇则对应一个UNION1类型任务,两个核心簇组成一个UNION2,四个核心簇组成一个UNION4,以此类推。我们将这种划分称为任务类型。任务类型明确了一次核函数启动所需的硬件核数,即在核函数的执行周期内需要一直占用多少物理核。BLOCK类型任务为单核任务,UNION类型任务为多核并行任务。图8.13展示了多个核函数场景下的多核并行示例。该示例包含三个主要核函数,Ker-nel1、Kernel2和Kernel3。
  • [插图] 图8.14 任务在核心层级的调度 8.3.3 存储空间 设备端的存储空间主要包括全局存储空间、共享存储空间以及本地存储空间。这三种存储空间之间可以相互通信,以满足不同任务需求。本地存储空间包括存放激活值的NRAM空间和存放权重的WRAM空间。图8.15展示了核函数执行中数据的移动过程,主要包括如下四个步骤:
  • 设备端的存储空间主要包括全局存储空间、共享存储空间以及本地存储空间。这三种存储空间之间可以相互通信,以满足不同任务需求。本地存储空间包括存放激活值的NRAM空间和存放权重的WRAM空间。图8.15展示了核函数执行中数据的移动过程,主要包括如下四个步骤:(1)根据任务索引从全局存储空间(片外)中选取部分数据,将数据搬移到片上共享存储空间。(2)将片上共享存储空间中的权重数据广播到所有核心以降低访存开销,根据任务索引将共享存储空间中的其他输入数据搬移到本地存储空间。(3)在核心上利用功能单元对本地存储空间的数据进行向量化或者张量化计算。(4)将运算结果从本地存储空间搬移到全局存储空间。
  • [插图] 图8.15 存储空间 为了提高整体吞吐,上述步骤涉及的指令被发射到专有的队列中流水线式执行。常见的队列包括:片外访存队列、片上访存队列以及张量/向量计算队列。队列间指令相互并行,数据依赖通过插入同步指令维护。不同任务的访存和计算可以通过软件流水进行并行,从而将计算和访存的耗时相互隐藏,提升整体吞吐。软件流水技术详见8.7.2.3节。

 8.4 智能编程语言基础

  • BCL是对C++语言的扩展,在原生C++语言功能的基础上,结合智能处理器的实际特点,扩展了一系列异构计算和智能应用相关的编程接口。与C++语言一样,BCL同样具有数据和函数两个基本要素。其中,数据是被处理的对象,而函数则用于描述数据处理的过程。
  • 可选的attribute可以是变量所处的地址空间(例如,__nram__表示神经元存储空间,__wram__表示权重存储空间),也可以是const/volatile修饰符。没有指定地址空间的数据,默认保存在栈空间。由const修饰的数据为只读数据;对于由volatile修饰的数据,编译器不会优化其存取操作。
  • 与C++语言一样,BCL中的变量可以有不同的数据类型,包括传统编程语言中的数字、字符、结构体、联合体、指针等,以及深度学习领域常见的FP16、BF16、INT8等数据类型,在BCL中分别对应half、bfloat16_t、int8_t数据类型。
  • 为了方便用户编程并提高处理效率,BCL提供了在不同存储空间之间进行数据搬移的内建函数__memcpy和__memcpy_async,如图8.20所示。__memcpy和__memcpy_async的区别是,前者会与运算指令串行执行,而后者可以与运算指令并行执行。
  • 数据搬移函数的dir参数用于指定数据搬移的方向。BCL支持的常见的数据搬移方向有:NRAM2GDRAM[插图]、NRAM2SRAM、NRAM2NRAM、GDRAM2NRAM、GDRAM2WRAM、GDRAM2SRAM、SRAM2WRAM、SRAM2NRAM。
  • BCL中常见的张量内建函数如图8.21所示,即矩阵-矩阵乘法__tensor_matmul和矩阵-向量乘法__tensor_mlp。张量内建函数的参数除了输入/输出数据的指针外,还包括输入和输出张量的维度信息,例如,M表示左矩阵lhs的列数和右矩阵rhs的行数,N表示左矩阵lhs的行数,K表示右矩阵rhs的列数。当K=1时,矩阵-矩阵乘法__tensor_matmul退化为矩阵-向量乘法__tensor_mlp。
  • 用于解决多核并行以及核内多条执行队列并行的数据依赖问题。同步内建函数主要分为三类:单核内的执行队列同步函数__sync()、核心簇内的局部同步函数__sync_cluster()和核心簇间的全局同步函数__sync_all()。其中,核心簇内的局部同步只保证一个核心簇内的所有核同步,而全局同步则是芯片内所有核心簇的所有核心都进行同步。
  • 图8.22给出用智能编程语言编写的程序示例。该示例实现了两个长度为1024的输入向量的对位乘法操作,考虑到NRAM的大小,每次向量计算处理64个数。函数参数列表中的两个指针都是GDRAM上的数,因此需要先把它们搬到NRAM上,原地计算乘法,然后再搬回GDRAM。在NRAM上申请临时空间需要加“__nram__”前缀。该代码示例仅使用一个处理器核心完成1024个输入数据的处理,完成相同功能的多核并行程序可以参考8.5.3节。

 8.5 智能应用编程接口

  • 机器学习应用既可以直接采用多种编程框架(如TensorFlow和PyTorch等)进行开发,也可以直接使用智能编程语言来开发,同时调用智能应用编程接口操作智能计算硬件。智能应用编程接口提供了一套面向智能计算设备的高层接口,主要可以分为两大类:核函数启动接口和运行时接口。其中核函数启动接口重点关注任务切分及硬件映射,如何将复杂任务切分成并发执行的多个任务并将其映射到底层硬件架构上;运行时接口重点关注设备管理、队列管理以及内存管理等。其中设备管理提供管理设备相关接口,如设备初始化、设备设置以及设备销毁等;队列管理提供队列创建、同步以及销毁等接口;内存管理主要提供内存分配和释放等接口。
  • Queue(队列)。用户开发的多个核函数可以绑定在同一个队列上交给任务调度器去调度执行,在队列内部的核函数按照被绑定的顺序,在本队列内部顺序执行;不在同一个队列中的核函数或异步拷贝按照运行时库的调度规则异步发射执行。
  • taskId(任务序号)。对应程序运行时所分配的任务编号,取值范围为[0, taskDim-1]。taskId的值对应逻辑规模降维至一维后的任务编号,即taskId=taskIdZ×taskDimY×taskDimX+taskIdY×taskDimX+taskIdX。
  • • DeviceGetAttribute(int* pValue, DeviceAttr_t attr, int ordinal);根据属性枚举值attr,获取编号为ordinal的设备的属性,并将结果输出到pValue,其中DeviceAttr_t枚举了设备的计算能力、存储空间、频率、指令集版本等属性,例如是否支持TF32数据类型AttrTF32ComputingSupported、最大可用内存AttrTotalMem-Size、核心主频AttrDlpClockRate、内存频率AttrMemClockRate、DLP指令集版本AttrISAVersion等。
  • QueueCreate(Queue_t *pQueue);在当前设备上创建一个队列,并返回指向新创建队列的指针pQueue。对于同一个设备,可以多次调用此接口创建多个队列。当主机端生产核函数的速度慢于设备端消费核函数的速度时,多个队列同时给设备端发射核函数可以减少设备端等待主机端的空闲时间。• QueueSync(Queue_t queue);同步队列中所有任务,等待任务执行完毕。此方法要在核函数调用符<<<>>>之后调用。• QueueDestroy(Queue_t queue);此接口用于销毁使用QueueCreate创建的队列,如果调用QueueDestroy时队列仍在执行某些操作,此接口将立即返回,而队列相关的资源会在队列清空后自动释放。
  • Memcpy(void *dst, void *src, size_t bytes, MemTransDir_t dir);从地址src拷贝bytes数据到地址dst, dir指定数据拷贝的方向(如主机端拷贝至设备端为MEM_TRANS_DIR_HOST2DEV,设备端拷贝至主机端为MEM_TRANS_DIR_DEV2HOST)。
  • 开发智能应用主要分为以下步骤:核函数编写、设备配置、主机/设备端数据准备、设备端内存空间分配、数据至设备端拷贝、调用核函数启动设备、运行结果获取和资源释放等。
  • 仍以图8.22所示的单核向量乘法核函数为例,可以利用DLP上的多核计算资源,进一步提升计算性能。为此,对原单核核函数扩展如图8.23所示。在该示例中,将1024个输入数据的计算任务平均分配给taskDim个计算任务,每个任务一次处理64个输入数据。为了简化代码逻辑,这里假设1024可以被taskDim×BASE_NUM整除。
  • 在主机端通过核函数调用符<<<>>>可以将智能编程语言编写的程序加载到深度学习处理器上执行。仍以图8.23所示的多核向量乘法核函数为例,调用核函数的代码如图8.27所示。其中,dim表示任务规模,pQueue表示核函数的执行队列,ktype表示任务类型。
  • 通过调用Memcpy接口可以把计算结果从设备端拷回主机端。图8.23所示的多核向量乘法核函数对应在数据拷贝代码如图8.28所示。其中,MEM_TRANS_DIR_DEV2HOST表示数据是从设备端拷贝至主机端。

 8.6 智能应用功能调试

  • 主流智能应用编程方法有两个层次:底层编程语言层和高层编程框架层。因此,智能应用的调试对象主要是基于智能编程语言通过编译器生成的机器码和基于编程框架通过框架级编译器生成的计算图,所对应的调试方法也分为编程语言级调试方法和编程框架级调试方法。
  • 编程语言一般会有相应的配套编译调试工具链和运行时环境辅助观察运行时状态信息,同时语言规范中也有调试相关的打印接口,此外还可以借助操作系统的异常处理及核心转储等机制辅助定位问题所在。编程框架是连接应用和底层软件的重要桥梁,涉及不同语言层次:用户API层主要使用Python或JavaScript等高级语言,方便编写具体应用;框架核心层主要采用C++语言来实现内部架构;框架底层调用目标架构编程语言或高性能库来充分挖掘底层硬件性能。后续将根据不同调试对象层次详细介绍各层次调试方法。
  • 在编译阶段收集的映射关系需解决两个关键问题:一是如何把经过编译器深度优化的二进制指令和原始程序源码关联起来。以常见的窥孔(peephole)优化为例,其中可能对指令序列进行了调整,再将其和源代码关联起来存在困难。二是如何以较低的时间和存储开销来详细地描述二进制程序与源代码的关系。为了解决该问题,需要定义合理的调试信息格式。这里以当前应用最广泛的DWARF调试格式为例介绍调试格式信息。
  • 当然,除了上述DIE项之外,针对数组、结构体、变量、宏等都有相应的DIE来进行描述。更详细的信息可以参考DWARF的设计手册[插图]。
  • 编程框架的调试方法也和前面介绍的编程语言类似,需要借助框架级调试接口或调试器,以在模型网络开发中快速查看核心数据结构等内容,此外还需要一些数据分析变换方法从其他维度去辅助网络级编程。
  • 常用的降维可视化方法有线性的主成分分析(Principal Component Analysis, PCA)和非线性的T-分布随机邻域嵌入(T-distributed stochastic neighbor embedding)
  • 为了实现和通用CPU打印接口的兼容,降低用户学习成本,智能编程语言也需要提供相应的格式化打印函数。由于基本编程模型是异构的,底层运行时系统具有异构通信和存储等特点,给实现与通用CPU兼容的打印接口带来了挑战:首先,异构打印没有即时性,设备端的计算为了追求高效一般不能频繁地被主机端打断,例如,运行在DLP上的带printf打印的核函数任务被发射执行后,必须调用同步接口等待完成才能获取打印结果;其次,异构打印拷贝开销大,主要是因为主机端和设备端一般都有各自独立的片外存储;最后,DLP一般是并行架构,对芯片内的多核或多线程并行打印提出了挑战。具体来说,如表8.6所示,智能语言并行打印的关键问题及解决方法主要集中在三方面。 表8.6 智能语言并行打印的关键问题及解决方法 [插图]
  • 为了实现和通用CPU打印接口的兼容,降低用户学习成本,智能编程语言也需要提供相应的格式化打印函数。由于基本编程模型是异构的,底层运行时系统具有异构通信和存储等特点,给实现与通用CPU兼容的打印接口带来了挑战:首先,异构打印没有即时性,设备端的计算为了追求高效一般不能频繁地被主机端打断,例如,运行在DLP上的带printf打印的核函数任务被发射执行后,必须调用同步接口等待完成才能获取打印结果;其次,异构打印拷贝开销大,主要是因为主机端和设备端一般都有各自独立的片外存储;最后,DLP一般是并行架构,对芯片内的多核或多线程并行打印提出了挑战。具体来说,如表8.6所示,智能语言并行打印的关键问题及解决方法主要集中在三方面。
  • 例外报错机制主要有两种,分别是调试模式下的断言(assert)机制和系统提供的核心转储(core dump)功能。断言是向程序员提供主动触发的检查机制,其主要作用在于开发者可以对有潜在问题的代码进行提前预判。在函数入口处进行入参合法性检查、在函数返回处进行返回值检查可以节省大量时间和精力。一般提供的断言信息主要有文件名、行号和函数名等。智能编程语言的断言示例如图8.33所示。
  • 断言机制一般用于开发调试阶段(会带来额外的执行开销);而核心转储机制可以在程序运行时对非法的软硬件行为触发硬件异常,并以特定格式生成核心转储文件,一般用于在非调试模式出错后协助定位问题。
  • 以Linux系统为例,程序执行异常或崩溃时,内核会将内存快照和关键程序状态保存为可执行文件所在目录下的文件。对于智能编程语言,由于编程对象通常是异构,需要在设备端计算核心陷入硬件异常时通过异构总线接口向主机端发起中断或直接将处理好的核心转储信息发送给主机端。
  • TensorFlow还提供了更详细的日志打印,可以通过TF_CPP_MIN_VLOG_LEVEL进行等级设置,等级越高打印的内容越多。由于TF_CPP_MIN_VLOG_LEVEL进行的是INFO层级的日志输出,因此TF_CPP_MIN_LOG_LEVEL不在INFO层级时,会屏蔽TF_CPP_MIN_VLOG_LEVEL的输出内容。
  • 面向编程语言的调试器除了具备传统编程语言调试器应具备的功能,如断点映射关系解析、断点设置及恢复、硬件异常上报等功能,还应具备在中断处打印或修改张量数据、转储/重新加载中间张量以及多核调试的状态管理和切换等智能语言特有的功能。我们以智能编程语言BCL的调试工具BCL-GDB为例详细介绍调试器的使用流程,包括调试前准备、调试器托管、状态查看及错误分析等。
  • 如果需要在设备端核函数的入口处停住,可以采用如图8.36所示的命令。
  • 多线程调试时通常需要进行调试线程的切换。可以用info命令查看当前线程并使用focus命令进行切换。图8.37的示例说明了采用focus命令将监控状态从(0, 0, 0)核切换到(0, 2, 1)核(三元组分别对应device、cluster、core)。
  • 当编译时添加了-g选项后,调试时可以直接根据变量名采用print命令来打印相关内容。寄存器内容则可以采用info registers命令进行查看。指定地址中的数据内容可以通过examine命令进行查看。更详细方法可以查看相应硬件编程语言的使用手册。
  • 图8.40提供了具体示例,介绍如何使用tfdbg的API来进行TensorFlow模型调试。在源码中插入LocalCLIDebugWrapperSession将待调试的会话sess用tfdbg进行封装,并指定ui_type="readline",可以在运行Python程序时进入tfdbg的调试命令行界面。
  • 由于DLP上常用的FP16、BF16、TF32以及INT8等数据类型,与CPU上常用的FP32/FP64等数据类型存在数值精度和表示范围的差异,不可避免地存在对DLP程序精度进行调试的需求。通常而言,DLP上的精度调试是通过和CPU上FP32的运算结果进行对比来实现的。
  • [插图]
  • [插图] 图8.49 精度调试示例及结果 可以看到使用向量数据类型转换函数__vector_float2half_tz将单精度浮点数FP32转为半精度浮点数FP16再调用__vector_half2float转回单精度浮点数FP32时出现了较大精度损失,我们使用调试器进行调试,如图8.
  • 可以看到使用向量数据类型转换函数__vector_float2half_tz将单精度浮点数FP32转为半精度浮点数FP16再调用__vector_half2float转回单精度浮点数FP32时出现了较大精度损失,我们使用调试器进行调试,如图8.50所示。
  • 以上所用的调试命令x为examine的缩写,其使用格式为x/<count/format/unit><addr>,其中,count、format和unit这三个为可选项。如上例所用的x/64f命令就表示以某地址立即数或指针为起始地址,以float格式打印64个unit数据。更详细的格式说明可参考GDB使用手册[插图]。从这个例子可以看出,通过调试器指定不同的打印格式,可以详细地查看数据的内存布局和具体数值,方便进行结果比对。

 8.7 智能应用性能调优

  • 首先介绍如何利用性能分析工具对程序进行性能剖析,找出潜在的性能优化点。之后结合具体实例介绍面向智能处理器的通用性能优化方法,包括如何利用本地存储空间、向量化、软件流水、多核并行、算子融合等。
  • 为了方便程序性能调优,需要了解硬件执行时间与状态信息。相关信息可以通过三类不同层级的性能分析工具来获取:一是函数级性能分析接口;二是应用程序级的性能分析工具;三是系统级的性能监控工具。其中,函数级性能分析接口是指BCL语言提供的主机端“通知”(notifier)计时函数或设备端的gettimeofday计时函数,主机端对计时函数接口主要负责统计异构执行时端到端的硬件耗时或软件耗时,设备端的计时函数接口主要负责统计某段代码的执行耗时。函数级性能分析接口需要用户修改源码,但开发者有时也希望在不修改源码并重新编译的情况下,能在程序外部监控程序的运行状态,这时就需要通过应用程序级性能分析工具或系统级性能监控工具实现。应用程序级性能分析工具dlp-perf既可以用于主机端与设备端的并行度调优,也可以用于设备端核函数的调优。系统级性能监控工具可用于多任务资源监控,方便进行任务调度和资源分配,提升多程序并发性能。
  • 主机端在调用核函数或其他异步执行的函数接口(例如dlpMemcpyAsync)之后会继续执行,因此需要一种通知类型的任务插入异步执行任务的前后,从而通过做差值来获取异步任务的耗时。通知是一种轻量级任务,该任务不像计算任务那样占用计算资源,而是通过驱动从硬件读取一些运行参数。通过将通知放置在计算任务前后,可以获取硬件执行状态或控制硬件运行。例如,性能通知可以获取计算任务运行起始和终止的时间戳;同步通知可以使多核间多个计算任务互相等待。用户可以在程序中按需使用相应的通知。对于性能通知而言,主要由驱动从硬件获取时间戳,因此也可以称为时间戳通知,其对用户提供的接口主要体现在主机端的运行时程序中。典型性能通知接口如表8.8所示。
  • 最后再通过Notifier-Duration接口获取两个通知事件之间的硬件时间差,结果保存在time_e2e_hw。也可以通过NotifierElapsedTime接口获取两个通知事件之间的端到端软件执行时间,结果保存在time_e2e_sw,如图8.51所示。
  • 其具体使用流程分为两个阶段:(1)采用record命令来运行可执行程序并生成相应的性能分析报告;(2)采用report或者kernel命令查看性能分析报告,获取包括执行时间、调用关系以及硬件性能计数器信息等。我们以图8.51所示的程序为例说明如何使用上述命令。
  • 除了使用report命令显示其整体执行时间外,也可以使用kernel命令显示硬件性能计数器信息,如访存带宽和运算器利用率等。如图8.53所示,可以看到kernel命令统计了写内存的数据量“write_bytes”、写内存的带宽“write_bw”、读内存的数据量“read_bytes”、读内存的带宽“read_bw”、向量运算单元执行周期“vfu_cycles”、向量运算单元利用率“vfu_utils”、标量运算单元执行周期“alu_cycles”、标量运算单元利用率“alu_utils”等硬件计数信息。
  • 片上存储空间(如前面所介绍的核内神经元存储空间、权重存储空间与核心簇存储空间)是离运算器最近的存储空间,也是读写效率最高的存储空间。因此针对会访问片外存储空间的智能应用,优先考虑使用NRAM和WRAM来代替LDRAM和GDRAM,以提升程序运行速度。图8.54给出了两个向量对位乘法的例子。
  • 智能处理器的计算和访存单元可以并行工作,用户可以显式控制无依赖的计算和访存指令并行工作,从而提高硬件的利用率和程序性能。在具体编程时,应当将有依赖的计算和访存指令在时序上分开,将无依赖的计算和访存指令放在一起并发执行。
  • 由于GDRAM2NRAM、
  • Compute、NRAM2GDRAM在DLP硬件上可以并发执行,三级流水将大块的数据拆分成小块,通过计算单元与GDRAM和NRAM之间访存的并行,可以将计算时间和访存时间互相隐藏起来。五级流水比三级流水多使用了核心簇存储空间(SharedRAM)。在多核场景下(具体指Union类型任务至少启动一个核心簇时)​,如果需要拷贝到NRAM空间或WRAM空间的数据是类似权重这样的数据时,可以先由GDRAM2SRAM拷贝到核心簇存储空间,然后多核并行执行SRAM2NRAM拷贝,这种拷贝方式相比GDRAM2NRAM的多核并行拷贝可以节省片外访存的带宽。同样,当数据写回GDRAM时,利用核心簇存储空间做合并写回也会有性能收益。
  • 图8.57 软件流水示意图
  • 通过软件流水优化,每次循环内处理的是第i块数据的加载、第i-1块输入数据的计算和第i-2块输出数据的写出,这三个步骤之间没有数据依赖,可以并行执行。为了实现软件流水,存放输入和输出数据的NRAM大小都应设为SIZE*2,从而避免第i-1块数据的计算与第i块数据的加载同时访问同一块存储空间。在实际应用中,还需要增加一些额外的处理逻辑,用于处理输入数据长度不能被SIZE整除的情况。
  • [插图] 图8.58 使用三级软件流水提升向量乘法效率 8.7.2.4 多核并行 针对(程序员可见的)多核,可以将一个任务分拆到多个核上并行计算,进一步提升程序性能。这里需要考虑两个问题:如何将计算任务拆分到多个核心上,以及拆分时如何保证性能。
  • 图8.58 使用三级软件流水提升向量乘法效率
  • 针对(程序员可见的)多核,可以将一个任务分拆到多个核上并行计算,进一步提升程序性能。这里需要考虑两个问题:如何将计算任务拆分到多个核心上,以及拆分时如何保证性能。以图8.56中采用了向量单元进行优化的程序为例,可以进一步通过内建变量taskId来定位每个核要处理的数据范围,得到如图8.59所示的4核并行代码。其中将原始长度为16384的向量均分为4份,每个核处理一份。
  • 对于单核版本,由于只需要一个Core,因此任务类型必须选择BLOCK类型,而且任务没有拆分,因此Dim3_t的X、Y和Z三个维度都必须设置为1。对于多核版本,本例中将任务拆分为4份,使用了4个Core,因此任务类型可以设置为UNION1类型,Dim3_t的X维度设置为4, Y和Z维度设置为1。
  • 图8.60 使用算子融合减少访存
  • 编程框架中经常会调用高性能库提供的融合算子来提升性能,例如axpy融合算子、softmax融合算子和MHA(Multi HeadAttention)融合算子。编程框架中的图编译优化模块也会使用算子融合来提升性能,相关内容详见5.4.2.2节。

 8.8 智能编程语言的应用

  • BCL智能编程语言是基于C++的扩展语言,在智能计算系统中起到承上启下的串联作用,既可以作为开发高性能算子库的底层语言,利用C语言的内嵌汇编和编译器内建函数做极致的手工优化,又可以利用C++语言的面向对象和模板编程等特性直接在PyTorch等框架中编写算子并集成进框架。
  • 类似CPU使用gcc、GPU使用nvcc编译器,BCL语言源码首先使用bcl-cc编译工具链编译出DLP-OPS高性能算子库,然后在深度学习框架的多后端架构中将DLP-OPS算子库封装进去,最后将框架算子API到多后端算子的下降流程打通。
  • 图8.61 编程语言开发的算子库集成进深度学习框架
  • 图8.62 编程语言集成进深度学习框架
  • 高性能库(如CPU上的OpenBLAS[插图]、MKL[插图]、GPU上的cuDNN[插图]、cuBLAS以及MLU上的CNNL[插图])提供了智能应用常见算子在特定平台上的高性能实现,方便用户以API的形式直接调用。
  • 高性能库中的算子开发流程一般为:(1)设计算子的API,根据输入/输出的张量描述符设计异构并行核函数的拆分策略;(2)使用智能编程语言进行算子的逻辑开发,编写相应的核函数;(3)使用设备端编译器将包含算子API和核函数的源码编译和链接成算子的对象文件;(4)通过主机端编译器将算子对象文件和主机端其他对象文件链接成为动态库;(5)编写相关测试用例调用算子动态库,并与CPU或GPU做精度和性能的对比测试。
  • 高性能算子开发的关键在于:核函数代码逻辑的开发与性能优化;高性能库算子接口API实现中的异构并行策略。
  • 高性能算子库一般需要提供基础一元二元运算、神经网络运算、基础线性代数类运算、随机数运算、机器视觉类运算等才能支撑起编程框架的完整推理和训练。对于异构计算平台,高性能算子库分为主机端程序和设备端程序。主机端主要负责API接口、上下文管理、设备管理、内存管理和核函数的发射前配置等工作,设备端主要由BCL编写的高性能核函数构成。
  • 如果核函数不适合执行在DLP上,例如DLP上的性能不如主机端的性能或者须执行DLP不支持的运算类型(如double类型)​,算子库可以将核函数用主机端的CPU实现来代替,以达到异构计算的最优性能。对于异构计算,算子库的开发依赖于BCL提供的异构混合编程和编译能力。
  • 高性能算子库中的每个算子都应包含主机端代码和设备端代码,如图8.64所示。其中主机端代码主要完成算子参数处理、高性能算子库接口封装和核函数并行规模策略计算等工作。设备端代码包含核函数的具体实现。使用核函数调用符<<<>>>,可以在主机端像调用主机端函数一样调用设备端核函数,参数传递支持C或C++的基础数据类型或结构化数据类型。图8.65是使用BCL语言实现的三级流水和五级流水的矩阵乘法C=A×B示例代码。为了简化示例代码,此处假设任务类型为UNION1,任务规模为{4, 1, 1},左矩阵A规模为4096×256,右矩阵B规模为256×256,输入数据类型为int8_t,输出数据类型为half。左右矩阵的数据布局分别为行优先和列优先。受限于片上存储空间容量,每个处理器核心执行一次矩阵乘法的运算规模设置为64×256×256。4个任务协同完成4096×255×256的矩阵运算,每个任务处理1024×255×256的矩阵块,即重复执行4次64×256×256的矩阵运算。
  • 图8.65 MatMul算子的三级流水和五级流水示例
  • PyTorch编程框架的设计理念为“Python First”​,所以集成自定义算子的基本思想就是使用Python语言的胶水能力将自定义算子嵌入到PyTorch框架的算子调用流程中。使用智能编程语言开发和集成自定义算子的流程如下。首先,参考8.8.1.2节实现主机端的算子主程序和设备端的算子核函数,其中主程序确定算子的输入/输出张量并定义算子接口,核函数实现算子的计算。其次,在PyTorch框架的torch.utils.cpp_extension模块中添加DLPExtension函数定义,添加方式类似CUDAExtension[插图]或CppExtension,自定义算子在框架中的实现和PyTorch框架的C++扩展机制可以参考5.3.3.2节。CUDAExtension和CppExtension是返回setuptools. Extension类的函数。其
  • 中,CppExtension扩展的对象为CPU,支持的语言是C++,提供了一些头文件和PyTorch C++相关的静态链接库、动态链接库;而CUDAExtension扩展的对象是GPU,支持的语言是CUDA C++。因此对于DLP的BCL编程语言,需要添加DLPExtension来完成编译器查找、编译参数指定等一系列操作。最后,编写setup.py脚本,使用setuptools工具将BCL源码编译为动态库,并通过pybind11将算子的API从BCL语言封装为Python语言。下面,我们以sigmoid自定义算子作为示例,说明如何实现代码逻辑并集成到PyTorch框架中。
  • sigmoid算子为单输入单输出,函数名称设为active_sigmoid_dlp(函数名称可根据实际需求更改)​,函数接口为torch::Tensor active_sigmoid_dlp(torch::Tensor x)。分别创建主机端和设备端的代码文件examples/dlp_extension/dlp_custom_ext/dlp/src/bcl_sigmoid.cpp和examples/dlp_extension/dlp_custom_ext/dlp/src/bcl_sigmoid_sample.dlp。算子的代码实现及框架集成包括以下四个步骤。(1)实现主机端的sigmoid主程序。主机端代码(.cpp文件)如图8.67所示,主程序调用函数bcl_sigmoid_kernel_entry实现对设备端上的数据计算。
  • (2)实现设备端的sigmoid核函数。设备端的代码(.dlp文件)如图8.68所示。在bcl_sigmoid_sample.dlp文件中实现了入口函数bcl_sigmoid_kernel_entry,并通过头文件对外暴露。该入口函数调用sigmoid核函数来实现算子计算。在核函数实现中,为了充分利用DLP硬件计算能力,使用了BCL提供的向量Builtin函数[插图]__vector_active_sigmoid来完成sigmoid的运算。使用向量计算接口必须注意以下两点:第一是输入数据和输出数据必须存放在NRAM上,因此必须在计算前使用memcpy将输入数据从GDRAM拷贝到NRAM上,在计算完成后将输出数据从NRAM拷贝到GDRAM上;第二是向量操作的输入规模如果不能被多核和多次循环整除,就需要增加分支来处理余数部分。
  • 图8.68 基于智能编程语言BCL实现的sigmoid核函数
  • 由于NRAM容量有限,不能一次性将所有数据全部拷贝到NRAM上执行,因此需要对原输入数据进行分块。分块的规模在满足NRAM大小和函数对齐要求的前提下由用户指定,这里设置为NRAM_LIMIT_SIZE=FLOOR_ALIGN(MAX_NRAM_SIZE/2,64)。输入数据的大小不一定是NRAM_LIMIT_SIZE的整数倍,因此最后可能会有一部分长度小于NRAM_LIMIT_SIZE、大于
  • 的余数段。
  • (3)通过pybind11暴露算子接口。如图8.69所示,通过pybind11[插图]暴露sigmoid算子接口后,该接口就可以在Python中用同名函数实现算子计算。
  • (4)编译生成动态库。使用python setup.py install命令进行编译,setup.py的代码如图8.70所示。其中,使用设备端的编译器将.dlp代码编译为.o文件,然后将主机端的.cpp代码编译为.so文件,并链接.dlp文件编译后得到的.o文件。编译完成后,会在本地生成一个动态库,一般格式为name.cpython*.so。以后就可以通过该动态库来使用sigmoid自定义算子。此外,如果需要支持反向传播,还需将其封装为torch.nn.functional函数形式来实现自动微分(AutoGrad),相关介绍可以参考5.2.2节

 习题

  • 8.2 假设某处理器的存储单元包括一块片上高速缓存和一块片外存储器,访问时间分别为4个时钟周期和150个时钟周期。如果工作负载在片上缓存的命中率为90%,而且处理器只有在片上缓存未命中的情况下才会访问片外存储器。那么,整个存储器层次的平均访问延迟是多少个时钟周期?
  • 8.7 图8.71是两段功能相同的代码,图8.71a是用标量语句写成的,图8.71b是用张量语句写成的,它们都是对一个向量内所有数据求和。假设硬件完成一个标量加法指令需要1个时钟周期,完成一个64元素的向量求和指令(sum_pooling)需要8个时钟周期。假设程序中其他运算和访存的时间忽略不计,张量程序的性能是标量程序的多少倍?

 第9章 大模型计算系统

  • GPT-4、T5、Gemini、BLOOM、
  • GLM、Llama 3等大模型的运行依赖于深度学习框架,具体而言其主要依托于微软的DeepSpeed、英伟达的Megatron-LM以及Meta FSDP等高级框架,这些高级框架再进一步调用以PyTorch为主的深度学习编程框架。
  • 图9.1 大模型计算系统的整体架构
  • 将文本数据作为基准,将多种模态数据与文本数据对齐,是实现高效实用的多模态大模型的有效方法
  • 预训练使用大量无标注的语料数据,旨在通过训练让大模型学习到通用的语言能力和知识。微调则是为了提升大模型在特定下游任务的表现,因此在微调阶段会使用特定任务的数据训练大模型。为了有效提升大模型的训练效率,可以使用混合精度训练、分布式训练等相关技术。而在微调过程中,还可以使用参数高效微调(Parameter Efficient FineTuning, PEFT)方法,仅更新大模型中的部分参数而不是所有参数,例如使用LoRA[插图]、Prefix Tuning[插图]、Adapter[插图]等方法。
  • 当前大语言模型(下文简称大模型)基于Transformer构成,其整体结构主要分为三类:仅编码器(encoder-only)结构,编码器-解码器(encoder-decoder)结构,以及仅解码器(decoder-only)结构。
  • 编码器计算整个输入序列的语义编码,适用于理解输入序列的语义,而解码器遵循了自回归(auto-regressive)结构,适合用于生成序列。
  • 当下,大模型中最流行的结构是以GPT系列为代表的仅解码器结构,使用解码器同时进行输入序列特征学习和后续序列生成,并且放弃编码器以提高计算效率。这种结构在自然语言生成任务中表现出色,也因此成为大模型的主流选择。
  • 在现有开源模型中,仅BLOOM公开了完整的训练过程。
  • 2017年,Google第一次提出了Transformer结构,并训练得到了参数量为2.1亿的编码器-解码器模型,是后续大模型工作的基石。2018年,Google基于Transformer结构进一步提出了BERT模型,该模型的参数量为3.4亿,使用64块TPU训练4天得到。2019年,OpenAI提出GPT-2[插图],其中最大模型的参数量为15亿,使用40 GB文本数据进行预训练得到。GPT-2是首个参数量超过10亿的语言模型,并且作为通用模型在多种自然语言任务上都取得了显著的效果提升。随后Google提出了T5[插图],其最大的模型包含110亿参数,使用1万亿词元的数据在1024块TPUv3集群上进行预训练得到。2020年,OpenAI继续推出GPT-3[插图],更是将参数量直接从百亿量级提升到千亿量级,预训练数据量也达到3000亿词元。2021年,OpenAI对GPT-3进行了改进和
  • 优化,推出了GPT-3.5 Turbo模型,这也是在2022年11月横空出世引起人工智能热潮的聊天机器人ChatGPT的基础模型。此
  • 2022年5月,Meta AI开源了最大参数量为1750亿的OPT[插图]模型,该模型使用1800亿词元进行训练,训练使用了包含992块A100 80GGPU的集群。同年,BigScience开源了最大参数量为1760亿的BLOOM[插图]模型,清华大学开源了1300亿参数的GLM[插图]模型,它们的训练都需要在几百块A100 GPU上运行几个月。此外,Google提出的PaLM[插图]模型,其参数量达到5400亿,需要在6144块TPUv4上进行训练。
  • 2023年3月,OpenAI再次推出GPT-4[插图]模型,其强大的功能更是一举将ChatGPT推广到了大众视野中。同时华为也提出了参数量高达1.085万亿的PanGu-Σ[插图],刷新了大模型的参数规模。
  • 图9.2 大模型进化树
  • BigScience在开源BLOOM模型参数时,同样开源了其具体训练超参、数据集以及实验记录等。因此,我们将在后续章节中以开源最为充分的BLOOM作为大模型驱动范例。

 9.2 大模型驱动范例:BLOOM

  • 嵌入层没有采用Transformer中添加位置编码的方式注入词元位置信息,而是使用注意力线性偏置(ALiBi)在多头注意力中添加静态偏置。
  • BLOOM-176B模型使用ROOTS语料库训练。ROOTS语料库是由BigScience研究团队提出的开源语料库,由498个数据集组成,包含46种自然语言和13种编程语言,文本共计1.61 TB。以上文本数据经过BigScience团队训练得到的分词器进行分词后,可以转化为1660亿(166B)个词元用于BLOOM-176B模型的训练。
  • Jean Zay超级计算机中单个计算节点的主要硬件信息如表9.2所示。通用计算方面,每个计算节点配置2颗通用处理器(CPU)以及512 GB内存;异构智能计算方面,每个节点配置8块英伟达A100 SXM4 80GB GPU(合计640 GB)​,该智能处理器支持FP32、TF32、FP16和BF16等多种数据类型;高速互联网络方面,集群采用英特尔公司的Omni-Path架构,单节点共计400 Gbit/s互联带宽;数据存储系统方面,集群使用统一的Spectrum Scale(GPFS)的并行文件系统,后端由全闪存硬盘和机械硬盘混合构建,文件系统在所有节点间共享。
  • 实际上,BLOOM-176B模型的训练过程中使用了分布式训练中的混合并行技术(相关介绍见5.5.3节)​,包括数据并行、张量并行和流水线并行。通常情况下,张量并行和流水线并行是正交的,即它们可以在不互相干扰的前提下同时运行,组合使用即可运行一个完整的大模型。此外,数据并行也与模型并行正交,这意味着这三种并行策略可以共存,结合这三种方法使得BLOOM-176B的训练在拓展到数百块GPU的同时保持高GPU利用率,提升数据吞吐量,加快训练速度。
  • 对于混合并行技术而言,其并行化维度可以用(d, t, p)三元组表示。如表9.4所示,d代表数据并行度,即在全局范围内复制d个模型副本同时处理数据,t代表张量模型并行度,即模型的大规模算子会被拆分至t块加速卡上共同计算,p代表流水线模型并行度,即模型会被拆分为p个阶段进行计算。整个模型的运算需要M块加速卡,一般情况需满足p×t×d=M。此外全局批量大小(global batchsize)为B,因此每一个模型副本在每一轮的输入批量大小即为B/d,实际运算时会再细分为若干个大小为b的微批量,因此每条流水线批量中的微批量数目m=B/(d×b)。
  • 表9.4 模型训练时的部分超参数[插图]
  • BLOOM-176B模型训练在384个智能处理器上进行。全局一共启动了384个计算进程,每个计算进程控制1个智能处理器,同时每个计算进程会根据自己的进程编号,主动加载对应的模型网络层参数。在正向传播过程中,输入数据被加载至图9.6中左侧第1列的智能处理器上开始进行计算,经过384个智能处理器的配合后,在最后1列的智能处理器上得到输出结果。实际上,模型训练时的数据并行度为8,代表全局范围内一共运行了8个模型副本,每48个智能处理器共同运行一个模型副本(例如GPU-[1-4], GPU-[33-36], GPU-[65-68]​,…,GPU-[353-356]共48块GPU共同运行了模型的第一个副本)​。模型并行度为4,代表大规模算子会被拆分为4个分区,并分配至4块智能处理器上分别计算。流水线并行度为12,代表模型会被拆分为12个阶段进行计算,具体而言,因为BLOOM-176B模型包含70个解码器块,第1个阶段分配了1个嵌入层与5个解码器块,第12个阶段分配了5个解码器块与1个嵌入层,其余阶段均分配6个解码器块。
  • 图9.6 BLOOM-176B模型在384个智能处理器上训练,且(d,t, p)=(8, 4, 12),B=2048, b=2
  • 对于BLOOM-176B模型的推理过程而言,因为只涉及正向传播,没有反向传播和梯度更新的过程,因此不需要数百个智能处理器。实际上,BLOOM-176B模型的推理使用Jean Zay超级计算机中的一个计算节点即可执行,具体计算时使用张量并行或者流水线并行的方法均可。
  • 我们接下来将介绍模型运行的算力需求,主要分析训练时计算一个批量数据所
  • 需的浮点运算(floating-point operation)次数。因为大模型通常使用多个相同的解码器块,因此我们分析其中一个解码器块即可。对于一个解码器块而言,其在正向传播时的浮点运算主要分为5个部分,分别是:1)计算输入矩阵I与相应权重矩阵的乘积得到查询矩阵Q、键矩阵K和值矩阵V; 2)计算查询矩阵Q和键矩阵的转置矩阵K⊤的内积,缩放后得到注意力权重;3)计算注意力权重矩阵与值矩阵V的乘积,获得注意力汇聚的结果;4)计算线性层,通过线性映射融合多头注意力结果;5)计算全连接前馈神经网络。相应的每个部分的浮点运算次数[插图]以及运算密度[插图]请参考表9.5中的数据,具体推导过程留作习题。
  • 在反向传播的过程中,由于需要计算神经元数据和权重数据的梯度,因此反向传播需要双倍的运算量,对于具有l个解码器块的模型,训练时一次批处理所需要的浮点运算次数为72blsh2+12bls2h[插图]。进一步地,将h=nd代入,可知模型结构里多头注意力数目和每个头内隐层维度大小均会给算力需求带来显著影响,算力需求与多头注意力数目和头内隐层维度均为二次函数关系。
  • 表9.5 一个解码器块在正向传播时的浮点运算量
  • 表9.6 BLOOM-176B模型训练一个批量的运算量
  • 在BLOOM-176B模型的训练过程中,与权重有关的张量数据就至少需要3525 GB的存储空间,其中最多的是优化器状态,包括优化器模型权重、优化器动量和优化器方差,小计2115GB,然后模型权重和梯度数据各占705 GB。其中占据空间最多的优化器状态在正向传播和反向传播时不需要使用,仅在梯度更新时使用,并且在数据并行时在全局范围内拥有多个同步更新的副本,这为后面优化训练时的权重存储空间提供了条件。
  • 单个激活值张量占用的存储空间主要受模型执行时微批量大小影响,模型训练时的并行策略会影响需要同时保存的激活值张量数目,这为后面优化神经元数据的存储空间提供了条件。
  • 对于推理任务而言,由于没有反向传播和梯度更新的过程,所以无须存储优化器状态数据和梯度数据,推理任务相比于训练任务,整体上对存储空间的需求较低,但是仅模型权重带来的存储空间的绝对数量仍然很高。BLOOM-176B模型在32位、16位、8位和4位的数据位宽下,模型权重数据需要的片外存储空间分别是705 GB、352 GB、176 GB和88 GB。
  • BLOOM-176B模型含有千亿级别参数量,这些参数需要在节点间进行同步,导致巨大的通信负担。在BLOOM-176B模型训练中,在Jean Zay超级计算机上使用6个计算节点容纳一个完整的模型,同时以32位单精度浮点数据类型表示的权重数据需要705 GB的存储空间,那么数据并行梯度数据同步的时间理论上可以在4.7秒之内完成。如果每个节点使用50 Gbit/s网络进行通信,梯度同步至少也需要37.6秒(2×705×8/(6×50)=37.6)才能完成,因此为集群配置高速互联网络是非常有必要的。
  • 除了通信数据量大以外,大模型训练的通信还具有两个特点:(1)通信次数多,无论数据并行、张量并行、流水线并行,均会产生必要的数据通信和同步。(2)通信分布不均匀,由于模型的前向和反向传播时的算子依赖关系,某些层可能需要等待其他层完成后才能通信,导致通信在时间上不均匀。

 9.3 大模型系统软件

  • 在常见的专为大模型设计的训练框架之中(如DeepSpeed[插图]、Megatron-LM[插图]、FSDP[插图]),都集成了上述各项优化,方便用户的使用。
  • 除了通过混合并行的方法尽量使用更多的计算卡参与大模型训练之外,还有一些针对大模型的计算优化方法被提出,如专用数据类型[插图][插图]和稀疏化[插图][插图]等。
  • 除了传统的单精度浮点数据类型(FP32)和半精度浮点数据类型(FP16)之外,各类智能硬件还设计了专用数据类型,在基于混合精度训练[插图]的大模型训练过程中广泛使用。例如,谷歌提出了BF16数据类型,英伟达在BF16的基础上提出了TF32(TensorFloat-32)数据类型。在英伟达A100GPU上,TF32数据类型的运算性能达到了FP32数据类型的8倍,而BF16数据类型的运算性能更是FP32数据类型的16倍,极大地加速了大模型的训练过程。此外,由于这些数据类型仅牺牲了一定的数据精度,但保留了较大的动态范围,在对精度有一定容忍度的大模型训练过程中,不会影响其收敛行为。稀疏注意力机制(sparse attention)则是DeepSpeed框架中为大模型训练引入的优化方法。注意力机制是大模型中最常见的
  • 基本算子,它能够有效捕获输入序列中词元之间的关系,是大模型的规模得以灵活扩张的基础。在典型的大模型中,GPT-4的最大输入序列长度可以达到惊人的32K。然而,这种超长序列的注意力机制的计算量,随着序列长度s成平方关系增长,即为O(s2),因此注意力机制的运算成为大模型训练的瓶颈。为了解决这一问题,DeepSpeed设计了稀疏注意力的方法,可以通过基于块的稀疏运算,将原始注意力机制的计算需求降低几个数量级。具体来说,稀疏注意力机制在原本全局注意力的基础上,额外引入了局部注意力和随机注意力的概念,如图9.8所示。它首先计算相邻词元之间的局部注意力,然后再以局部注意力的结果去计算全局注意力。稀疏注意力机制可以将计算量从原本的O(s2)减少到O(ws),其中1⩽w⩽s,取决于注意力机制的结构。此外,该方法不仅显著减少了注意力机制的运算量,还缓解了注意力机制的内存瓶颈。最终,通过稀疏注意力机制优化,DeepSpeed可以用6倍的加速比执行10倍长的输入序列,优化效果显著。
  • 图9.8 DeepSpeed中的稀疏注意力机制,其中黄色、绿色和橙色分别表示全局注意力、局部注意力和随机注意力
  • 在大模型训练时需要保存的数据,主要可分为以下四类:模型参数、优化器状态、激活值和梯度。模型参数主要由大模型训练得到的权重数据组成。优化器状态是反向传播过程中优化器中的数据。激活值是大模型正向传播得到的每层的输入与输出张量,也称为中间结果。梯度是在大模型训练时反向传播过程中产生的数据。面向大模型训练的存储优化不仅要考虑存储空间受限的问题(即上述庞大的数据量如何分配到有限的存储空间上)​,还需要考虑访存带宽受限的问题(即如何更快地完成数据的存取从而不影响大模型训练整体的性能)​。接下来,我们分别介绍ZeRO系列存储优化、重计算优化和注意力机制融合
  • 优化。其中,前两项是针对存储空间受限问题的优化,后一项是针对访存带宽受限问题的优化。
  • 在大模型的训练过程中,其模型参数的数量庞大,难以存放在单个GPU或者主机端内存之中。为了解决这一挑战,微软的研究团队在DeepSpeed框架之中,提出了ZeRO系列存储优化策略,分别是ZeRO、ZeRO-Offload和ZeRO-Infinity。ZeRO(零冗余优化器)是一种针对大模型训练中数据并行的存储优化技术。在传统的数据并行方法中,每个计算设备都会保存模型权重、梯度和优化器状态的完整副本,导致大量的存储冗余。ZeRO的核心思想是将这些数据分块并分散到各个设备,确保每个设备只保存一部分。实际使用时可以采取分级优化的方式,首先拆分占据空间最多的优化器状态,接着可以选择叠加梯度存储优化,以及进一步叠加模型权重存储优化。这样,它能够大幅减少每个设备的存储空间使用量,从而使得更大的模型和批量大小成为可能。ZeRO优化了数据并行带来的存储冗余,同时组合使用张量并行和流水线并行的方法,能够在一个分布式集群中高效地训练大模型。
  • ZeRO-Infinity[插图]技术在ZeRO-Offload的基础上,从另一个角度为超大规模的大模型训练提供了方法。它不仅涉及DLP片外存储与主机端内存之间的交互,更将部分数据,特别是激活值,迁移到了高速的存储介质(如NVMe SSD等)上。这种多层次的存储管理策略为大模型的训练提供了充分的灵活性,将存储层次从DLP片外存储扩展至了多种不同类型的存储介质上,从而能够支持万亿级别甚至100万亿级别的大模型的训练。
  • 重计算优化(recomputation)指的是在正向传播时不保存所有层的激活值,而仅保留部分层的计算结果作为检查点(checkpoint),然后在反向传播时再根据检查点重新计算所需的激活值。重计算优化的核心是用计算换存储,即增加反向传播时的计算量,减少大模型训练时的存储量。这可以在受限的硬件资源下,在计算效率跟存储使用之间做取舍,从而更好地支持大模型的训练。
  • 传统的重计算方法将神经网络按基本块进行划分(例如一个Transformer层)​,在正向传播时,仅保留该基本块的初始输入激活值作为检查点,而舍弃基本块内部的激活值。在反向传播时根据基本块与保存的检查点重新计算所有的激活值用于梯度计算。在基于Transformer架构的大模型中,这样的方法会额外增加30%~40%的计算量。为解决这一问题,Megatron[插图]设计了选择性重计算(selective activation re-computation)方法,通过对Transformer层内部计算量和存储量的量化分析,选择性地将中间层的激活值保留或舍弃,最终能够在引入可忽略不计的计算量的前提下,将激活值的存储使用减少为原来的1/5,具体的分析过程请见参考文献[插图]。
  • FlashAttention[插图]的工作就基于这样的思想对注意力模块的融合方法进行了探究,其提出的算法可以对带有softmax的矩阵乘法进行分块和融合,从而避免
  • 了O(s2)的片外访存。
  • 由于在softmax的维度(序列维度)进行了分块,分块计算时块内的最大值不等于整个序列维度的最大值,因此需要对m和l都进行动态更新,然后对之前的计算结果进行修正。通过FlashAttention优化能够获得最高3倍的性能提升,效果显著。
  • 通信优化针对此问题,旨在减少数据传输量、提高通信效率和减少通信与计算的竞争。相关优化技术如3D并行参数调优[插图]、梯度压缩[插图]和通信拓扑优化[插图]等,已被广泛研究和应用,有效地缓解了大模型分布式训练中的通信瓶颈,进而使实现更大规模的模型训练和更高的训练速度成为可能。
  • 我们以DeepSpeed中专为大模型训练引入的1-bit Adam算法优化为例,介绍大模型训练的通信优化方法。Adam是深度学习中常用的优化器,但在大模型训练需要的分布式环境中,传统的Adam算法需要传输32位的浮点数,导致通信开销巨大。为了解决这一问题,DeepSpeed推出了1-bit Adam算法。该算法将浮点数压缩为1位,大大减少了通信所需的带宽。具体来说,1-bit Adam在每个训练步骤中首先计算出梯度的均值和方差,然后使用这些统计数据将梯度量化为1位,从而将原始的32位梯度值压缩为1位,减少了通信的数据量。此外,1-bit Adam还采用了累积误差修正机制,确保量化过程中的误差不会累积。
  • 例如,BLOOM模型使用384个智能处理器训练105天,其中每周会遇到1~2个硬件故障,还多次出现导致5~10小时停机的严重问题。在这种背景下,为了减少故障对大模型训练的影响,大模型训练的稳定性优化
  • 成为大模型系统软件中一个重要的议题。
  • 在深度学习领域,检查点恢复技术作为回滚机制已被广泛采纳。这种技术的核心在于定期将模型的状态备份到稳定的非易失性存储中。一旦遭遇任务故障或失败,系统可以利用之前存储的检查点数据回滚至某一特定时刻的状态,从而确保其连续和稳定的运行。
  • 此外,训练过程需要定期评估模型在验证集上的性能,定期保存的检查点可以让程序员在不同的训练阶段进行模型评估,选择在验证集性能最好的模型进行部署或进一步研究。常见的策略是每隔固定的训练迭代次数就保存一次检查点,这样可以确保在训练过程中遇到任何问题时,都可以从近期的检查点恢复,而不会丢失太多的进展。如在BLOOM模型的训练中,研究人员每3小时(100次迭代)保存一个检查点,这样可以将每周因为故障引起的平均损失控制到约1.5小时的训练成果上。
  • 检查点恢复技术的实现可分为两个层级:系统级和应用程序级。从系统级的角度看,该技术涉及对整个程序状态的完备捕获,这意味着需要为保存状态分配更多的数据存储空间,但是用户不需要修改程序代码。应用程序级检查点恢复方法更为灵活,允许用户有选择性地、细粒度地去控制保存数据,这也意味着,用户需要在代码中明确定义并标识哪些部分应被包含在检查点中,并确定最佳的控制点,同时需要主动设置检查点生
  • 成以及恢复重启方式,这也带来了额外的编程复杂度和工作量。在各种深度学习框架中,应用程序级检查点技术已经得到了广泛的支持和采纳,应用级别的检查点恢复策略已经成为主流。
  • 在保存检查点的过程中,模型参数不能发生更新,训练的计算会暂停,这意味着如果能加速此过程,就有可能加快模型的训练速度。Check-N-Run[插图]、CheckFreq[插图]等工作通过差分检查点(differential checkpointing)、量化、压缩等方法减少检查点文件的大小,优化检查点数据的存储效率;Gemini[插图]、GPM[插图]等工作通过将检查点放入更高速的存储层次(主机端内存、NVMe固态硬盘等)中,提高数据写入的速度,减少存储检查点的时间,从而减少训练中断的时间;Gemini[插图]等工作通过优化调度的方式,减少数据传输与系统内其他消息传递产生的冲突,从而提高整体系统的吞吐量。
  • 常见的大模型专用推理框架中(如FasterTransformer[插图]和TensorRT-LLM[插图])​。值得一提的是,由于大模型推理往往在单个计算节点内完成,此处较少涉及通信相关的优化。
  • 相比于卷积神经网络,大模型的推理具有显著的动态性。该动态性体现在模型生成序列的长度和具体的任务相关,高度不可预测。针对这一特点,本节介绍三种相关的优化,包括通过动态任务切换的批处理优化方法、缓存过去的生成结果以避免重复计算的键值缓存优化方法,以及使用低成本模型动态替代的猜测采样优化方法。这些优化方法能显著提高大模型推理的吞吐性能。
  • 键值缓存机制将这些计算过的特征进行缓存,在下一轮计算前和新采样出的字的特征(​“算”字)进行拼接,然后进行注意力模块的计算。由于采样时并不关心之前的查询(Q值)​,因此键值缓存无须对查询数据进行缓存。这样的缓存策略可以降低推理时实际运算的序列长度,从而降低运算量,然而却需要以更大的存储占用为代价。假设模型的层数是nlayer,隐藏层维度是dmodel,数据类型为FP16,则每个词元的键值缓存需要额外占用2(键缓存和值缓存)×2(FP16数据类型占2字节)×s×h×l的存储空间。以BLOOM-176B模型为例,每个词元需要约4 MB的键值
  • 缓存空间。在典型长文本任务场景下,假设序列长度达到8192,则一共需要32 GB的存储空间,存储空间开销不可忽略。
  • 该方法使用低成本模型进行多步的采样,然后使用高成本的模型对采样结果进行验证,如果验证不通过,则重新采样。具体地,该方法的步骤如下:(1)使用低成本模型连续串行采样k次,并输出每个采样结果x的采样概率q(x)。(2)使用高成本模型对采样结果进行一次评估,并计算每个采样结果x的采样概率p(x)。(3)依次遍历每个采样结果以[插图]概率接受。若拒绝,则重新采样,并返回步骤(1)。(4)如果这次投机采样的结果均被接受,则返回步骤(1)对后续
  • 的词元开始下一次投机采样。
  • 大模型的推理对存储需求极高,从而限制了其部署的场景。本节介绍两种大模型特有的存储相关优化,包括键值缓存分页优化以及存在异常值场景下的量化优化。这些优化方法降低了模型部署对于存储的需求,同时也可以用来提升多任务场景下大模型服务的吞吐。
  • 大模型的量化难度主要体现在激活值的量化上,研究表明,激活值张量在通道维度(channel)上存在少量(约0.1%左右)的异常值。这些值取值大小远大于其他通道,如果使用一个缩放系数对整个张量进行量化,则会导致取值较小的通道(绝大部分)有严重的精度损失,导致整体精度较差。例如,在一个参数量为6.7B的大语言模型中,就会存在大约150 000个异常值,并且造成超过20%
  • 的精度损失[插图]。
  • 首先是仅权重量化,顾名思义,其只对权重进行量化。这类方法可以将权重量化到更低位宽,极大程度降低模型大小,甚至可以使大模型在个人笔记本上运行。该方法在实际运算时,在线地将量化的权重转换成和激活值相同的精度,从而利用现有的算子库进行运算加速。相比于不进行量化,该方法由于存在权重精度转换,引入了额外的计算开销,在推理速度上没有提升。该方法被用于GLM等模型中,这类模型激活值中异常值占比更高,不适合对激活值量化。然后是混合精度分解,其思路是对激活值张量中难以量化的部分保留高位宽,对容易量化的部分采取低位宽,该方法能在8比特位宽下做到精度近乎无损。在实现中,由于张量不同通道的量化难度不同,所以该方法将激活值按照难易程度进行分块,并对权重进行相应的分块,于是原来的矩阵乘法转变为分块矩阵乘法。然而因为该方法缺少高效的算子支持,所以其性能相比量化前没有显著的提升。

 9.4 大模型基础硬件

  • 通常情况下,CPU与DLP板卡会通过PCIe链路交换机(也称为PCIe交换芯片,PCIe Switch)间接连接,一是因为处理器的PCIe通道资源有限,这样可以节省资源去连接其
  • 他资源(例如以太网卡、磁盘阵列卡、NVMe硬盘等)​;二是因为通过PCIe Switch直接连接的DLP板卡之间可以直接通信,而不需要经过处理器转发
  • 值得注意的是,与CPU直接相连的网络接口卡(Network Interface Card, NIC)通常是接入业务网,承载服务器日常的IP通信任务,与PCIe Switch相连的网络接口卡通常接入高速互联网络,满足计算任务的高带宽和低延迟通信需求。
  • 在以深度学习应用为主的负载下,主机端CPU与设备端DLP需要进行大量数据交互,由于PCIe的灵活性,主机端与设备端的互联拓扑可以协同设计以满足负载应用的需求。如图9.13所示,第一种拓扑为平衡型,DLP板卡平均分配到了每个CPU,同一个PCIe Switch下的DLP板卡可以实现点对点的直接通信[插图],连接至不同PCIe Switch的DLP板卡需要通过处理器间的互联总线才能通信[插图];第二种拓扑是通用型,DLP板卡分为两组连接至不同的PCIe Switch,但是同时连接至了同一颗CPU,同一个PCIe Switch下的DLP板卡仍然可以实现点对点直接通信,连接至不同PCIe Switch的DLP板卡需要通过处理器内部转发才能通信,不涉及处理器间的互联总线;第三种拓
  • 扑是级联型,DLP板卡分为两组连接至不同的PCIe Switch,PCIe Switch之间采用级联的形式,所有DLP之间均可实现点对点直接通信,不需要通过处理器转发;第四种拓扑是直连型,DLP板卡直接连接到处理器,所有DLP之间均需要通过处理器内部转发才能通信。不同拓扑结构主要影响的是:(1)处理器与DLP板卡之间的总通信带宽;(2)DLP板卡之间互相通信的带宽;(3)DLP板卡之间互相通信的延迟。
  • 服务器厂商预先制造不同拓扑的服务器主板,智能处理器供应商制造行业标准的PCIe拓展卡(例如全高全长双槽位形态的寒武纪MLU370-X8和英伟达A100 PCIe GPU)​,可以减少智能处理器适配以及整体解决方案落地所需的时间和精力。
  • 上述几种拓扑结构下,单台服务器内的DLP之间通信均通过PCIe链路传输,然而从2010年发布PCIe 3.0版本至2022年发布PCIe 6.0,其链路带宽仅为12年前的8倍。目前广泛使用的PCIe 4.0协议在16通道(PCIe 4.0 x16)下仅提供32 GB/s的单向带宽,两个DLP之间双向带宽仅为64 GB/s,已无法满足大模型训练时张量并行带来的通信需求。为了解决这个问题,可以通过DLP-Link高速互联技术提高通信效率,DLP-Link高速互联相较于PCIe,使用了更大的传输位宽,并且因为应用场景更明确,摆脱了PCIe协议其他的限制,目前可以实现相较于PCI
  • 4.0 x16十倍以上的传输带宽。
  • 图9.13 服务器的不同拓扑对比
  • 因此可以考虑通过连接DLP至交换机的形式,将多个DLP通过交换机互相连接,实现DLP之间的最大速率连接,如图9.14b所示,任意两个DLP之间均可通过DLP交换机转发实现全速通信,只有DLP与CPU、DLP与网络接口卡的通信仍然需要通过PCIe链路实现。
  • 开放计算项目(Open Compute Project, OCP)下的开放加速器基础设施(Open Accelerator Infrastructure,OAI)子项目组提出了一种通用加速器基础设施的方案。该方案推出了更加开放的OCP加速器模块(OCP AcceleratorModule, OAM),使其与解决方案无关,并被定义为可被不同加速器供应商采用的通用外形,有助于在服务器上实现更优的智能处理器的互联。
  • 通信技术也并未停滞。例如,CXL(Compute Express Link)技术的问世,为处理器与加速器之间的通信建立了一个更高效、低延迟的桥梁,其性能相较于传统的PCIe链路有了质的飞跃。而基于400Gbit/s或更高带宽的先进网络技术,则为服务器间建立了一个大容量、低时延的通信网络,这对于支持大规模的分布式计算具有重要意义。
  • 横向扩展的一个主要挑战是,并非所有的算法都适合分布式计算模型,但事实上,大模型的计算非常适合使用分布式的计算模型,并且通过合理的拆分方式,在计算集群上仍然保持了很高的执行效率。
  • 同时为了确保节点之间对数据的统一访问以及高速通信,集群还应该配置统一的网络数据存储(Network Attached Storage, NAS)和多套互联通信网络。
  • 为应对大模型计算对通信带宽的需求,不同于传统高性能计算(HighPerformance Computing, HPC)集群通常为每个计算节点配备一张网络接口卡的配置,智能计算集群为每个智能处理器配置单独的一张网络接口卡,从而提供更大的通信带宽和更高的吞吐量。
  • 图9.15 以大量智能计算节点为核心构成的多机多卡集群
  • 高速互联网络是集群网络的核心,这一网络设计着重于满足计算任务的高带宽和低延迟需求。InfiniBand(IB)、Omni-PathArchitecture(OPA)和RDMA over ConvergedEthernet(RoCE)等先进的高性能互联技术都在此范畴内被广泛应用。除了高速互联网络外,业务网络一般采用以太网技术,承载日常的IP通信任务,用户对集群的访问以及文件传输、管 理服务器与计算服务器间的通信和集群的基本维护,均使用该网络。除此以外,随着数据量的增长和存储需求的多样化,部分集群开始配置专用的存储网络,减少网络存储的数据传输对计算任务和业务的干扰。
  • 这些拓扑结构的选择和设计,直接决定了系统的通信效率、容错性和扩展性。接下来介绍三种常见的网络
  • 拓扑结构(见图9.16)​:胖树(fat-tree)、蜻蜓拓扑(dragonfly)和三维环状拓扑(3D torus)。
  • 图9.16 网络拓扑对比
  • 张量模型并行带来的通信开销最大,因此应该将张量模型并行的范围控制在服务器本地,然后使用流水线并行来跨服务器扩展更大的网络模型。
  • 在传统数据中心中,网络传输通常基于以太网和TCP/IP协议,并且Linux等操作系统将内存划分为用户空间和内核空间,因此无论对于数据发送方还是接收方,均需要进行多次内存拷贝,并经过一系列的网络协议栈处理。多层协议处理需要消耗大量的CPU资源,在进行高速网络通信时会消耗更多,导致用于计算任务的CPU资源减少,此外用户态与内核态的上下文切换也会对应用的性能带来一些负面影响。为了解决这个问题,远程直接内存访问(Remote Direct Memory Access, RDMA)技术被广泛应用,它可以将数据直接从一台计算机的内存传输到另外一台计算机的内存中。相较于TCP/IP, RDMA零拷贝(zerocopy)减少用户空间和内核空间来回复制数据的开销,内核旁路(kernel bypass)减少了软件调用的开销(见图9.17)​,这些都不需要双方操作系统内核参与,因此RDMA具有高吞吐、低延迟和低CPU开销的特点。
  • 由于部分DLP与RDMA网络接口卡位于同一PCIe Switch,只有一条链路连接到CPU,这部分DLP与接口卡实际上共享了一条至CPU的链路。当它们都向CPU发送数据时,实际可用带宽会降低,DLP无法满速收发数据,导致通信效率降低,通过DLP与RDMA网络接口卡直接通信可以避免这个问题。

 习题

  • 9.2 计算BLOOM-176B模型的参数数量,写出表达式并代入实际值计算。可以使用词汇表大小(V=250680)以及表9.3中的符号。注:两个嵌入层的权重是共享的,参数矩阵互为转置

 后记

  • 如果把我们人类自己也看成一个智能计算系统,这样的系统使用周期很短,还要并发处理多项任务,且频繁受到外部中断,能做好一件事、做成一件事殊为不易。唯愿在短暂的剩余使用周期里,超频工作,争取为我国人工智能行业再多培养出一些具有系统思维的人才。


 来自微信读书

实验文档----智能计算系统官方网站

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐