智能计算系统复习
# 什么是智能计算系统
智能计算系统是智能的物质载体。现阶段的智能计算系统通常是集成 CPU 和智能芯片的异构系统,软件上通常包括一套面相开发者的智能计算编辑环境 (包括编程框架和编程语言)
- 现今采用异构智能计算系统的主要原因(异构智能计算系统的优点):
- 近十年通用 CPU 计算能力增长几乎停滞,而智能计算能力的需求在不断以指数增长,二者形成剪刀差
- 异构系统在提高性能的同时,也带来编程上的困难(异构智能计算系统的缺点)
智能计算系统技术分层:
- 应用层
- 算法层
- 系统层
- 芯片层
如何解决一个 AI 的任务:
- 输入
- 建模:深度学习基础、深度学习应用
- 实现:编程框架、Bang
- 运行:架构基础、架构设计、标准与评测
- 输出:运行环境搭建、运行与调试、应用与开发
# 神经网络基础
浅层神经网络特点:
- 需要数据量小、训练速度快
- 对复杂函数的表示能力有限,泛化能力受到制约
神经网络训练方法:
- 正向传播
- 正向传播 (推断) 是根据输入,经过权重、激活函数计算出隐层,将输入的特征向量从低级特征逐步提取为抽象特征,直到得到最终输出结果的过程
- 反向传播
- 反向传播是根据正向传播的输出结果和期望值计算出损失函数,再通过链式求导,最终从网络后端逐步修改权重使输出和期望值的差距变到最小的过程
- 反向传播的作用是将神经网络的输出误差反向传播到神经网络的输入端,并以此来更新神经网络中各个连接的权重
如何缩小计算值与真实值之间的误差:
通过反向传播进行反馈,调节权重值
神经网络的模型训练:
调整网络拓扑结构
- 隐层的设计
- 隐层节点的作用是提取输入特征中的隐藏规律,每个节点都赋予一定权重
- 隐层节点数太少,则网络从样本中获取信息的能力就越差,无法反映数据集的规律
- 隐层节点数太多,则网络的拟合能力过强,可能拟合数据集中的噪声部分,导致模型泛化能力变差
- 隐层的设计
选择合适的激活函数
- 激活函数:在神经元中,输入的数据通过加权求和后,还被作用于一个函数 G,函数 G 就是激活函数
- 激活函数给神经元引入了非线性因素,使得神经网络可以任意逼近任意非线性函数,因此神经网络可以应用到众多的非线性模型中
- 激活函数需具备的性质:
- 可微性:当优化方法是基于梯度的时候,这个性质是必须的
- 输出值的范围:当激活函数输出值是有限的时候,基于梯度的优化方法会更稳定。因为特征的表示受有限权值的影响会更显著;当激活函数的输出是无限的时候,模型的训练会更加高效,不过在这种情况下,一般需要更小的学习率
- Sigmoid 函数:
- tanh 函数:,tanh 函数解决了 sigmoid 函数存在非 0 均值输出的问题,但没改变梯度消失问题
- Relu 函数:,ReLU 能够在 x>0 时保持梯度不衰减,从而缓解梯度消失问题
选择合适的损失函数
- 损失函数, 是模型预测值,是神经网络模型参数 w 的函数,记作
- 从 w 角度看,损失函数可以记为
- 常用损失函数:
- 均方差损失函数,
- 交叉熵损失函数,,其中 m 为训练样本总数量,i 为分类类别
- 交叉熵损失函数能够有效克服使用 sigmoid 函数时,均方差损失函数出现的参数更新慢的问题
- 神经网络中损失函数的特性
- 同一个算法的损失函数不是唯一的
- 损失函数是参数 (w,b) 的函数
- 损失函数可以评价网络模型的好坏,损失函数越小说明模型和参数越符合训练样本 (x,y)
- 损失函数是一个标量
- 选择损失函数时,挑选对参数 (w,b) 可微的函数
欠拟合与过拟合
欠拟合:训练考虑的维度太少,拟合函数无法满足训练集,误差太大
过拟合:训练考虑的维度太多,使得拟合的函数很完美的接近训练数据集,但泛化能力差,对新数据预测能力不足
神经网络的层数增加,参数也跟着增加,表示能力大幅度增加,极容易出现过拟合现象
参数范数惩罚、稀疏化、Bagging 集成,Dropout、提前终止、数据集扩增等正则化方法可以有效抑制过拟合
正则化思路:
在损失函数中增加一个惩罚项,惩罚高阶参数,使其趋近于 0
正则化项
- 通过 正则化后,w 权重值变小,网络的复杂度降低,对数据拟合的也更好
正则化项是各个参数的绝对值之和
- 正则化通过加入一个符号函数,使得当 为正时,更新后的 变小,当 为负时,更新后的 变大,因此正则化后的效果就是让 接近 0,这样网络中的权重也会接近 0,也就减小了网络复杂度,防止了过拟合
Bagging 集成方法:
- Bagging 训练不同的模型来共同决策测试样例的输出,不同的模型即使在同一个训练数据集上也会产生不同的误差
- Bagging 可以多次重复使用一个模型、训练算法和目标函数进行训练
- Bagging 的数据集从原始数据集中重复采样获取,数据集大小与原始数据集保持一致
- 模型平均是减小泛化误差的一种可靠方法
稀疏化:
- 训练时让网络中的很多权重或神经元为 0
- 90% 的权重或神经元为 0
- 降低正向传播时的计算量
- 稠密 MLP、突触稀疏、神经元稀疏、动态稀疏
Dropout 正则化:
- Dropout 正则化是通过在训练时暂时修改神经网络来实现的
- Dropout 正则化思路:在训练过程中随机地 "删除" 一些隐层单元,在计算时无视这些连接
其他正则化方法:
- 提前终止
- 在训练过程中返回验证误差达到最低的参数设置,就可以获得验证集误差更低的模型,这种策略称之为提前停止
- 多任务学习
- 多任务学习通过合并多个任务的样例来减少神经网络的泛化误差
- 数据集增强
- 使用更多数据进行训练,可对原数据集进行变换形成新数据集添加到训练数据集中
- 参数共享
- 强迫两个模型(监督模式下的训练模型和无监督模式下的训练模型)的某些参数相等,使其共享唯一的一组参数
交叉验证:
最简单的验证方式:将数据集划分为测试集和训练集
- 不同的划分方式下,得到的 MSE(Mean Squared Error)变动比较大
- 缺点:最终模型与参数的选取将极大程度依赖于你对训练集和测试集的划分方法,只有部分数据参与了模型训练
Leave-one-out cross-validation 验证方法
- 每次取出一个数据作为测试集的唯一元素,而其他 n-1 个数据都作为训练集用于训练模型和调参。最终训练处 n 个模型,得到 n 个 MSE。将这 n 个 MSE 取平均得到最终的 MSE
- 缺点:计算量过大,耗费时间很长
K - 折交叉验证
- 不重复地每次取其中一份做测试集,将其他 K-1 分做训练集训练模型,之后计算该模型在测试集上的,最后再将 K 次的 取平均得到最后的
- Leave-one-out cross-validation 是一种特殊的 K 折交叉验证 (K=n)
- 优点:所有样本都被作为了训练集和测试集,每个样本都被验证一次,相比 Leave-one-out cross validation,计算成本低,耗时减少
# 深度学习
适合图像处理的卷积神经网络:
卷积层如何检测特征
- 检测复杂边缘
- 将权重作为参数,在训练中学习
- 检测复杂边缘
卷积神经网络两个重要特征:局部连接、权重共享
- 可以有效减少权重参数,避免过拟合,为增加卷积层数提供可能
卷积运算可转换为矩阵相乘
- 卷积的相乘再相加过程可转换为向量内积
- 多输入输出通道卷积可转换为矩阵相乘
卷积层
- 3D 卷积、1x1 卷积、转置卷积、扩张卷积、可分卷积、平展卷积、分组卷积
- 边界扩充 (padding)
- 扩大输入图像 / 特征图的尺寸并填充像素
- 防止深度网络中图像被动持续减小
- 强化图像边缘信息
- 卷积步长 (stride)
- 滑动滤波器时每次移动的像素点个数
- 与 pad 共同确定输出图像迟勋
池化层
- Max Pooling/Avg Pooling/L2 Pooling
- 主动减小图片尺寸,从而减少参数的数量和计算量,控制过拟合
- 不引入额外参数
- 池化提供一定程度的平移不变性
- Max Pooling 可保留特征最大值,提高提取特征的鲁棒性
全连接层
- 卷积层和池化层构成特征提取器,全连接层则为分类器
- 将特征提取得到的高维特征图映射成一维特征向量,该特征向量包含所有特征信息,可转化为各个类别的概率
Softmax
- 通常作为网络的最后一层,对输出进行归一化,输出分类概率
- 凸显其中最大的值并抑制远低于最大值的其他分量
- Softmax 层输入、输出数据规模相同
- 公式:f(z_j)=e^{z_j}/\sum_{i=0}^{n}e^
浅层学习局部特征,深层学习整体特征
卷积神经网络结构
- 为何选择深而非广的网络结构
- 深度网络可以从局部到整体理解图像
- 学习复杂特征时(例如人脸识别),浅层的卷积层感受野小,学习到局部特征,深层卷积层感受野大,学习到整体特征
- 深度网络可以减少权重数量
- 以深度换宽度,用多个小卷积替代一个大卷积,在获得更多样特征的同时所需权重数量也更少
- 深度网络可以从局部到整体理解图像
神经网络初始化:
- Xavier 初始化
- Glorot 条件:为保证神经网络模型的稳定性和有效性,避免梯度消失或梯度爆炸,对模型的初始化需满足两个条件:
- 前向传播时每一层激活值的方差保持一致
- 反向传播时每一层对状态的梯度方差保持一致
- Glorot 条件:为保证神经网络模型的稳定性和有效性,避免梯度消失或梯度爆炸,对模型的初始化需满足两个条件:
- Kaiming 初始化
梯度下降法 GD:
原理:损失函数关于参数 w 的负梯度方向时损失函数下降最快的方向,因此用负梯度方向对参数进行更新
mini-batch 随机梯度下降法
- 每次迭代随机选取一个 mini-batch 的样本计算梯度并进行参数更新
SGD 的缺点:
- 选择合适的学习率十分困难
- SGD 容易收敛到局部最优点而且可能困在鞍点
带动量的随机梯度下降法:
- 通过积累历史梯度,减小梯度方向改变,抑制梯度震荡,加快收敛速度
NAG:添加矫正因子的 Momentum
- 原理:先用当前速度更新一遍参数,再用更新的临时参数计算梯度
- 优势:相比 Momentum 梯度方向更加稳定,进一步减少震荡
AdaGrad:使每个参数获得不同的学习率
- 原理:设置全局学习率,每次通过全局学习率逐参数的除以历史梯度平方和的平方根,使得每个参数的学习率不同
- 效果:对更新频率高的参数使用较小的学习率,对更新频率低的参数使用较大的学习率
- 优势:对于稀疏梯度的效果较好,稳定性高
- 局限性:梯度的平方和使训练后期学习率会快速缩小,导致参数更新提前停止
RMSprop:AdaGrad 的改进
- 原理:增加了一个衰减系数控制历史梯度的积累量,对梯度计算指数衰减的移动平均,丢弃时间靠前的历史梯度
- 优势:可以缓解 AdaGrad 训练后期学习率快速缩小的问题,善于处理非平稳的目标,目前常用在训练 RNN 相关的深度学习模型中
Adam:带有动量项的 RMSprop
- 原理:利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率
- Adam 结合了 AdaGrad 善于处理稀疏梯度和 RMSprop 善于处理非平稳目标的优点,为不同参数计算不同的自适应学习率
- 适用于大多非凸优化,适用于大数据集和高维空间
应用于图像分类的卷积神经网络:
- AlexNet
- 多个卷积层,ReLU;Dropout、数据增强等
- VGG
- 结构组成:
- input
- conv->conv->pool
- conv->conv->pool
- conv->conv->conv->pool
- conv->conv->conv->pool
- conv->conv->conv->pool
- fc->fc->fc
- softmax
- 相同感受野,多层网络权值更少
- VGG 成功的原因
- 更深的卷积神经网络,更多的卷积层和非线性激活函数,提升分类准确率
- 使用规则的多层小卷积替代大卷积,减小参数数量,提高训练收敛速度
- 部分网络层参数的预初始化,提高训练收敛速度
- 结构组成:
- Inception
- BatchNorm 效果
- 可提高收敛速度、训练速度
- 可选择更高的学习率,方便调参
- BatchNorm 的缺点
- 计算 batch-level 的统计量较贵
- 依赖 batch 的大小,难于在有限硬件资源上复现模型性能
- 不利于分布式训练
- BatchNorm 效果
神经网络退化:收敛到极值点而非最值,误差大
- ResNet
- 优点:在解附近时,权重的反应更灵敏,更容易学习获得最优解
Image Style Transfer
内容损失函数:
- 只取 conv4_2 单层特征来计算内容损失
- 计算内容图片特征和噪声图片特征之间的欧式距离
- (gram 矩阵)
- :第 l 层的 用于计算风格损失的权重
- :初始风格图片
- :生成图片
- :风格图片在第 l 层第 i 个特征图和第 j 个特征图的内积
- :生成图片在第 l 层第 i 个特征图和第 j 个特征图的内积
- :第 l 层的输出特征图的大小
- :第 l 层的输出特征图的数目
- :用于计算内容特征的层数
- :生成图片在第 l 层第 i 个特征图上位置 j 处的特征值
- :内容图片在第 l 增第 i 个特征图上位置 j 处的特征值
- :内容图片
- :生成图片
, 越大,内容越具象
Real-Time Image style transfer
- Image Transform Net
- 深度卷积网络,参考 DCGAN 的思想:用步幅卷积替代 pooling、每个卷积层后接 BatchNorm 和 Relu
- 增加残差结构,使网络更易训练
在 neural style transfer 中,优化算法每一轮迭代更新是 (B)
A. 内容图像 C 中的像素值 B. 生成图像 G 中的像素值 C. 神经网络参数 (w,b) D. 正则化参数
神经网络量化:
为什么要对神经网络进行量化?
- 深度神经网络的规模不断增大,其计算和存储成本越来越高
- 近年来,大模型的崛起和不断发展更是加剧了成本需求
什么是神经网络量化
- 通过将高精度 (浮点数) 数据离散化得到的低精度 (位宽) 数据进行网络的表示和计算
量化的优势:
- 减少计算量、提升计算速度、减少计算能耗、减少存储空间、减小乘法器面积
前向传播的量化:将激活的权重都量化后再计算卷积和全连接
反向传播的量化:将激活、权重、激活梯度都量化后,再计算卷积和全连接的反传,获得当前激活梯度和权重梯度
# 编程框架的使用
为什么需要编程框架
- 有必要将算法中的常用操作封装成组件提供给程序员,以提高深度学习算法开发效率
- 算法理论复杂
- 代码实现工作量大
Pytorch
张量
- 张量是计算图是数据载体
- 张量对应了神经网络中在各个节点之间传递、流动的数据
- 张量可以看做是 n 维数组,数组的维数即为张量的阶数
- 张量的数据格式
- 多维数组以何种线性存储方式存储
- PyTorch、GPU 中采用 NCHW,TensorFlow 中采用 NHWC (N:batch size;C: 通道数;H: 高度;W: 宽度)
- 数据在计算设备上按照 1 维来存储:NCHW 按照 W->H->C->N,NHWC 按照 C->W->H->N 的顺序存储
操作
定义计算操作方法
原位 (in-place) 操作
- 指在存储原张量的内存上直接计算更新张量值,而不是先复制张量再计算更新。其标志是在原操作语句后添加 "_"
- 与 Python 中 +=,*= 类似
- 原位操作能够节省内存占用,在进行深度学习算法推理时,使用原位操作能够有效减少模型占用的内存
- 原位操作会覆盖原张量,如果模型训练时使用原位操作来更新张量梯度,则每次迭代计算所得梯度值将被覆盖,从而破坏模型的训练过程
- 对于多个张量同时引用一个张量的情况,对该张量进行原位操作会影响其他张量操作
操作广播机制
对于参与计算操作的多个张量,如果张量维度不匹配,可以使用 Pytorch 的广播机制对不匹配的张量维度进行扩展,最终将这些张量均扩展为维度相同
能进行广播机制的条件:
- 每个张量都有至少 1 个维度
- 从张量末尾的维度开始对齐扩展,在对齐后的同一维度中,仅下列情况之一才允许进行广播操作:
- 1、维度尺寸相同
- 2、维度尺寸不同但其中一个维度尺寸为 1
- 3、其中一个张量没有该维度
对于维度数量相同的张量,比较每个维度对应的维度尺寸,若维度尺寸不同但其中一个维度为 1,则将其维度扩展为另一张量的尺寸
对于维度数量不同的张量,首先从张量末尾的维度开始对齐扩展,对缺少的维度尺寸补 1,再沿每个维度方向进行尺寸对比及扩展
原位操作的广播
- 如果是执行原位操作的张量需要维度扩展或改变,则编译报错
- 如果作为原位操作参数的张量需要维度扩展或改变,则仍可通过广播机制完成张量操作
计算图
- 编程框架中使用有向图来描述计算过程。有向图中包含一组节点和边
- 支持通过多种高级语言来构建计算图 (C++/Python)
- 计算图对应了神经网络的结构
- 节点一般用来表示各类操作,包括数学运算、变量读写、数据填充等,也可以表示输入数据、模型参数、输出数据
- 边表示节点之间的输入输出关系。分为两类
- 一类是传递数据的边。传递的数据即张量
- 一类是表示节点之间控制依赖关系的边。这类边不传递数据,只表示节点执行顺序:必须前序节点计算完成,后序几点才开始计算
- 为什么采用计算图
- 自动求导、统一的计算模型、高效执行、并行性
- 静态图 vs 动态图
- 静态图
- 先定义整张图,再运行
- 可以对图进行全局优化,获得更快的运算速度
- 调试不方便
- 动态图
- 即时运行,网络模型可在运行时修改
- 代码编写灵活,可立即获得执行结果,调试方便
- 优化不方便
- TensorFlow1.x:静态图,PyTorch:动态图
- 静态图
Example:,画出对应的计算图
基于 Pytorch 的推理模型实现
读取输入图像(一般使用 transform 来转换格式)
- 可以利用 PIL、OpenCV、torchvision.io 等来读取输入图像
- PIL:读取格式为 (H,W,C)
- OpenCV:imread (filename) 加载图像文件,OpenCV 中表示彩色图像使用的是 BGR 格式
- torchvision.io 读入图像,保存 3 维 RGB 或灰度张量
构建神经网络
- 自定义模型
- PyTorch 提供 torch.nn、torch.nn.Module、torch.nn.functional 等模块
- 直接调用预训练模型
- PyTorch 提供 torchvision.models,包含了用于处理不同人物的各种模型,如图像分类、语义分割、目标检测、关键点检测等
- 自定义模型
实例化神经网络模型
- 完成神经网络模块的定义,包括 init () 方法和 forward () 方法定义
- 实例化模型结构,将结构相关参数传递给 module
- 定义模型的输入数据
- 将模型输入传入实例化后的模型,获取模型输出
- 完成了模型结构的定义以及模型中参数的初始化后,即可对神经网络模型进行实例化,并将模型的输入数据传递给模型,获取输出结果
调试
神经网络模型优化
- 使用 torch.nn.utils.prune 对神经网络模型进行剪枝操作
- 使用 torch.quantization 进行神经网络模型的量化
- 动态量化
- 神经网络模型的量化,指将模型中的权重和 / 或激活函数从多位宽的浮点格式,转换为低位宽的整数型格式
- 缩放系数与参与量化张量的数值范围有关
- 权重参数的数值范围是固定的,其缩放系数也是固定的
- 动态量化:每一层激活数据的数值范围随计算过程变化,需要动态确定缩放系数,根据缩放系数动态完成数据格式转换
基于 PyTorch 的模型训练实现
- PyTorch 提供了两个数据加载原语:torch.utils.data.Dataset 和 torch.util.data.DataLoader,实现构建数据集、加载数据集等功能
- 1、加载训练数据集
- torch.utils.data.Dataset
- 数据集的抽象类,自定义数据集需继承该类
- 复写 getitem () 方法,定义数据读取及数据预处理方法
- 通过复写其中 len () 方法,定义统计数据集规模的方法
- torch.utils.data.DataLoader
- 使用 Dataset 构建完成的自定义数据集,以及 torchvision.datasets 内建的数据集,可以作为参数传递给 torch.util.data.DataLoader 类,实现数据集加载
- torch.util.data.DataLoader(dataset,batch_size=1,shuffle=None,num_workers=0)
- torch.utils.data.Dataset
- 2、构建模型
- 模型参数初始化方法
- 使用 torch.nn.init 模块
- 使用 torch.nn.Module.apply 函数
- 使用 self.modules
- 模型参数初始化方法
- 3、模型训练
- 首先需要定义损失函数的计算方法
- 然后构建优化器实现对模型的梯度计算及更新
- 反向传播过程中,可以利用内建的性能分析工具、梯度检查函数等,验证训练过程的正确性和有效性
- 损失函数定义
- 可以自定义,也可直接使用 PyTorch 提供的内建损失函数
- 自定义损失函数可以通过定义模块实例来实现
- 构建计算图来计算梯度
- 计算图的主要作用在于能够在前向计算过程中保存所有中间节点的计算结果,便于反向传播时构建反向传播路径,并利用链式法则完成自动求导
- 计算图在一次反向传播后会被立即销毁,释放存储空间,下次调用时需要再次创建
- 因此只有在训练时计算图是必须的,而如果只是单纯的推理,可以选择不创建计算图,可以节省存储占用和资源消耗
- 使用 torch.no_grad () 禁用计算图
- 计算图的反向传播
- 可以使用 tensor.backward () 函数计算当前张量相对于计算图中所有叶节点的梯度
- 每次调用 backward () 后,计算图会被释放掉
- 计算图的叶节点
- 叶节点分为两种情况
- requires_grad=False 的张量
- 由用户直接创建而不是通过某些计算得到的、且 requires_grad=True 的张量
- 在反向传播时,仅有 requires_grad=True 的节点才会计算梯度,而其中仅有叶张量的.grad 属性 (j 即其梯度值) 会被保存在内存中。非叶张量如果想保留.grad 属性,需要设置其 retain_grad 参数
- 叶节点分为两种情况
- 通过 detach () 方法修改计算图
- tensor.detach () 会返回一个新张量,该张量从当前计算图中剥离,成为一个新的叶张量,新张量的 requires_grad=False,即不需要计算梯度
- 返回的张量与原张量共享共同内存,对原张量或新张量的原位修改 (如尺寸、stride 等的原位修改) 均会报错
- 自定义函数的梯度计算
- 在使用 loss.backward () 计算梯度时,如果 loss 向前计算使用的均为 PyTorch 中的内建操作函数,则 PyTorch 能自动根据各操作对应的梯度计算方法完成自动求导
- torch.autograd 包提供了用于自动求导的类和函数
- 如果在模型中使用了某些不可微的函数,或需依赖非 PyTorch 库 (如 Numpy),则需要自定义操作的前向计算和反向计算方法,以便于 PyTorch 利用链式法则完成自动求导
- 为了保证自定义函数的正确性,可以使用 torch.autograd.gradcheck ()、torch.autograd.detect_anomaly () 等函数来验证梯度计算功能的准确性
- 使用 torch.optim 包优化梯度
- 支持常用梯度优化算法:Adadelta、Adagrad、Adam、RMSprop、SGD、LBFGS
- torch.opim.Optimizer (params,defaults):所有优化器的基类
- params 为需要优化的模型参数列表
- defaults 为包含了如 learning rate 等优化选项的字典
- 4、模型的保存与恢复
- torch.save () 保存模型
- 可以仅保存模型的 state_dict,模型保存格式为.pt 或.pth
- 推理时装载模型的 state_dict
- 在神经网络进行推理时,首先完成模型的初始化,再使用 model.load_state_dict () 装载模型参数
- 恢复模型的检查点文件
- 可以使用 torch.load 恢复检查点文件中参数
- 可用于推理或继续训练
- torch.save () 保存模型
# 编程框架机理
编程框架设计
- 设计原则
- 简洁性
- 框架提供一套抽象机制,用户仅需关心算法本身和部署策略
- 易用性
- 熟悉的开发范式
- 直观且用户友好的接口
- 高效性
- 如采用静态图编程方式,可以生成完整的计算图并进行全局优化,从而尽量提高用户应用程序的运行效率
- 支持深度学习编译技术,多层级表示优化,充分利用用户硬件的计算能力
- 支持多机多卡条件的分布式训练,从而高效支持大规模深度学习任务
- 简洁性
- 整体架构
- 计算图构建模块:完成从输入的用户程序到编程框架内部原始计算图的转换过程,编程框架的入口模块
- 分布式训练模块:应对更大规模的神经网络,将训练、推理任务从一台设备扩展到多台设备
- 深度学习编译模块:对计算图分别进行图层级和算子层级的编译优化,从而提升单设备上的执行效率
- 计算图执行模块:将优化后的计算图中的张量和操作映射到指定设备上进行具体执行,并给出编程框架的输出结果
计算图构建
计算构建:正向图与反向图构建
- 计算图由两个基本元素构成:张量和张量操作。计算图是有向图,有向边指明了张量流动方向
正向传播
输入张量经过搭建的神经网络层层计算递进,并最终获得计算结果的过程
构建形式:
动态图:在执行函数时,按照函数顺序逐条语句地生成节点,立即计算并返回结果;易调试单性能优化空间有限
计算图在函数运行过程中逐步构建的
立即 (eager) 模式:每次调用语句就立即执行计算
静态图:在执行计算之前构建好所有图上节点,在图运行时才计算整个计算图并返回结果;不易调试但性能好
整个网络的结构会在开始计算前就建立完成计算图
框架执行时接收整个计算图而不是单一语句
反向传播
正向计算得到的结果和目标结果存在损失函数值,对其求导得到梯度,并使用该梯度更新参数
常用求导:
手动求导:手动用链式法则求解出梯度公式,代入数值,得到最终梯度值
- 缺点:
- 对于大规模计算的深度学习算法,手动用链式法则进行梯度计算并转换成计算机程序非常困难
- 需要手动编写梯度求解代码,且模型变化,算法也需修改
- 缺点:
数值求导:利用导数的原始定义求解
- 优点:易操作;可对用户隐藏求解过程
- 缺点:计算量大,求解速度慢;可能引起舍入误差和截断误差
符号求导:利用求导规则对表达式进行自动操作,从而获得导数
- 缺点:表达式膨胀问题
自动求导:介于数值求导和符号求导的方法,对基本算子应用符号求导法,代入数值保留中间结果,应用于整个函数
计算图结构天然适用于自动求导:计算图将多输入的复杂计算表达成了由多个基本二元计算组成的有向图,并保留了所有中间变量,有助于程序自动利用链式法则进行求导
优点:灵活,可以完全向用户隐藏求导过程;只对基本函数运用符号求导法
计算分两步执行
原始函数建立计算图,数据正向传播,计算出中间节点,并记录计算图中的节点依赖关系
反向遍历计算图,计算输出对于每个节点的倒数\bar{x}_i=\dfrac{\delta y_i}
对于前向计算中一个数据 连接多个输出数据 的情况,自动求导中,将这些输出数据相对于该数据的导数累加
\bar{x}_i=\bar{y}_j\dfrac{\delta y_j}{\delta x_i}+\bar{y}_k\dfrac{\delta y_k}
计算图执行
将计算图中的张量和操作 (算子) 映射到给定设备上具体执行
设备管理
- 设备是编程框架中计算图执行时的硬件实体,每个设备都具体负责计算图中的张量存放和算子运算
- 常见通用处理器 (如 CPU) 和领域专用处理器 (如 GPU 和 DLP 等)
- 添加对领域专用处理器的设备管理支持 (三个模块)
- 设备操作:初始化设备运行环境、获取设备句柄和关闭并释放设备等
- 执行流管理:设备上抽象出来的管理计算任务的软件概念
- 在异构编程模型下,完成设备上任务执行的下发和同步操作
- 执行流创建、执行流同步和执行流销毁等
- 事件管理:表示设备上任务运行的状态和进展;事件创建、事件记录和事件销毁等基本操作
张量实现
逻辑视图:形状、布局、步长、偏移量、数据类型和设备等。是编程框架者能直接控制和表达的基本属性
物理视图:设备上的物理地址空间大小、指针、数据类型等。对框架使用者不可见
一个物理视图可以对应多个逻辑视图:切片的结果不是新的物理视图,而是原本物理视图下的一个新的逻辑视图
PyTorch 中的张量抽象
通过张量 (Tensor) 抽象类来分别表示张量数据结构中的逻辑视图和物理视图:
TensorImpl 类:张量抽象的实现,包含了维度信息,步长信息,数据类型,设备,布局等逻辑视角的张量信息
StorageImpl 类:张量的存储实现,包含了内存指针、数据总数等物理视角的张量信息
张量内存分配
从逻辑视图到物理视图的转换需要完成对张量的内存分配,即对张量进行内存管理
根据设备的类型不同,张量管理的方式不同
即使分配:GPU
需要分配张量内存时,立即从系统中申请一块合适大小的内存空间
内存池分配:CPU
预先分配一块固定大小的内存池,需要时从内存池中分配内存
自我维护:内存块的拆分与合并
优点:节约设备内存使用,减少设备内存碎片化
算子执行
- 获取算子执行序列
- 实现算子:前端定义、后端实现、前后端绑定
- 查找并调用算子
计算图的执行过程 = 每个算子独立执行的过程
计算图 -> 执行序列 (确保正确的数据流和依赖关系)
针对每个算子进行算子实现
- 前端定义、后端实现、前后端绑定
分派执行
- 查找适合给定输入的算子实现
- 并调用相应的实现来执行具体的计算任务
执行序列
- 分析计算图结点之间的依赖关系 -> 执行序列
- 拓扑排序算法 (可有多种可行的结果)
算子实现
正向传播实现和反向传播实现分离
用户接口 (前端) 和具体实现 (后端) 分离
算子实现流程:
- 前端定义:在编程框架中配置算子信息,包含算子的输入、输出以及相关接口定义,最后生成前端接口
- 在配置文件中添加算子正向传播函数和反向传播函数的对应
- 后端实现:使用 C++ 或其他高级的编程语言,编写算子的底层实现代码,完成算子的计算逻辑部分实现
- 表层实现:不同设备之间的抽象函数接口
- 底层实现:具体到每个设备上的实际代码实现
- 前后端绑定:编程框架将前端定义的算子与后端的具体实现进行绑定
- 同一个算子可能会有多个后端实现的代码
- PyTorch 使用分派机制来管理前后端对应关系
- 前端定义:在编程框架中配置算子信息,包含算子的输入、输出以及相关接口定义,最后生成前端接口
分派执行
- 获得算子执行序列 -> 实现对应算子 -> 对算子分派执行
- 分派执行:在运行时根据输入张量的类型和设备类型查找并调用合适的算子实现方法
- Dispatcher 计算分派键,并由此到对应的内核函数
- 算子:Dispatcher 的调度对象,代表了具体的计算任务
- 分派键:根据输入张量和其他信息计算,可简单地理解为与硬件平台相关联的标识符
- 内核函数:特定硬件平台上实现算子功能的具体代码
深度学习编译
为什么需要深度学习编译器
- 编程框架中早期优化方式存在的问题
- 框架维护成本高:对于新硬件和新算子,都需要程序员手动进行算子开发,开发数量呈平方级增长
- 性能受限:性能受限于程序员人工优化算子的能力,且没有充分探索计算图的优化空间
- 在深度学习框架中引入深度学习编译机制
- 减少人工开发工作量:可针对不同硬件平台进行代码生成
- 便于性能优化:(图层级) 对完整的计算图进行静态分析和全局优化;(算子层级) 利用自动调优技术优化算子,最大限度提升硬件利用率
- 编程框架中早期优化方式存在的问题
什么是深度学习编译器
- 接收以计算图形式表示的深度学习任务,并在指定硬件平台上生成高性能代码
- 多个层级中间表示 & 多个层级优化
- 图层级优化:子图替换、常量折叠、公共子表达式删除、布局优化以及算子融合等
- 算子层级优化:自动调优,基于搜索和基于多面体模型的方法
深度学习编译器与编程框架的关系
- 深度学习编程框架
- 自行适配厂商提供的计算库或者手写算子来支持不同硬件,这带来了极高的框架维护成本
- 深度学习编译器
- 提供跨平台同一的抽象和优化,较为灵活的适配不同的上层级编程框架和底层硬件平台
- 经过图层级优化和算子层级优化后,自动生成在目标硬件平台上的高性能算子
- 深度学习编程框架
图层级编译优化
- 不关心特定算子的具体执行过程,关心数据在图中流动过程
- 图优化方法:
- 子图替换:将原计算图中的节点 (计算操作) 替换为功能等价但运算逻辑更优的算子
- 常量折叠:如 为定值,则计算其值后带入此定值
- 公共子表达式删除
- 代数化简:将代价高的计算换为等价代价低的运算(如表达式中出现乘 0,此表达式结果直接为 0)
- 布局优化:输入布局影响执行性能 (使用 Tensor Core 计算相同数据时,采用 NHWC 格式的性能普遍优于 NCHW 格式)
- 算子融合
- 纵向:
- 函数调用有开销,外设的函数调用开销巨大
- 将多个小算子融合为一个大算子进行执行
- 横向
- 多个小的矩阵乘可以合并为一个大的矩阵乘
- 实现:对源码遍历,在遍历过程中对图进行变换
- 纵向:
算子层级编译优化
- 接受图层级优化后的计算图节点作为输入,将其下降到算子层中间表示上,最终生成目标硬件后端上的代码
- 算子层级中间表示
- 抽象建模一个计算及其在设备上的具体执行流程
- 算子调度
- 针对目标硬件后端上的计算特性和访存特性进行优化
- 通过循环变换来匹配目标平台的体系结构特性 (包括计算特性和访存特性)
- 算子的具体实现通常表现为嵌套循环程序
- 循环分块 (tiling) 优化、循环向量化 (Vectorize) 等
- 优点:提升缓存命中率和 (在 CPU 平台上) 使用向量化加速
- 一个完整的调度是由多个调度原语构成的
- 自动调优
- 自动确定最优的调度配置
- 通过搜索的方式确定合适的调度配置
- 空间探索:一个点代表一种配置
- 性能测量:测试某种配置下的程序性能
- 代价模型:对性能进行评估,并选择配置
分布式训练
为什么需要分布式训练
- 大模型及其相关应用蓬勃发展
- 参数数量的增加带来了模型的表达能力和拟合能力提高
- 庞大的训练数据使得模型能够学习到更全面的知识和对数据分布的理解 -> 许多从前难以实现的任务变得可行
- 更大的参数量和更多的训练数据导致训练过程的算力墙和存储墙
- 模型大 & 数据多
- 单个计算设备的资源有限,无法存储整个模型的参数或者计算全部的数据集,提升单个设备性能成本远远高于多个设备
- 分布式训练技术:拆分任务(拆分训练数据;拆分模型 (计算图))并由多个设备共同协作完成计算
- 大模型及其相关应用蓬勃发展
分布式训练基础
- 分布式架构和分布式同步策略是分布式训练的基础
- 分布式架构:组织和管理分布式训练任务的方式,以最大程度地利用计算资源和提高训练效率
- 分布式同步策略:在分布式环境中,为了保证计算节点之间的一致性和正确性,对不同计算节点之间的操作进行协调和同步的策略
- 常使用一下两种架构实现分布式训练
- 参数服务器:中心化的 Parameter Servers 架构,"中心化" 是指将模型参数进行中心化管理,以此实现模型参数的同步
- 将所有节点分为中心点和计算节点两类
- 中心点用于存储参数和梯度更新
- 计算节点用于完成中心节点下发的实际计算任务,仅与中心节点通信以更新和检索共享参数
- 优点:
- 灵活:通过改变中心节点数量适应不同的负载和数据规模
- 高效地参数共享:由中心节点统一管理模型参数
- 缺点:
- 单点故障:单个中心节点故障会影响整个系统
- 数据一致性问题:多个计算节点可能同时读取和更新模型参数
- 网络通信开销:受通信宽带的限制,中心节点成为系统的瓶颈
- 集合通信:去中心化的 Collective Communication 架构中,每个训练节点都有当前全局最新参数,节点间的参数同步通常采用多次设备之间的点对点通信完成的
- 集合通信指一个进程组的所有进程都参与全局通信操作
- 集合通信没有中心节点 (也被称为去中心化的架构)
- 每个计算节点都有当前全局最新参数
- 节点间的参数同步通常采用多次设备之间的点对点通信完成的
- 对芯片的算力和芯片之间的网络通信要求较高
- 集合通信的基础操作:发送、接收、赋值、组内进程障碍同步以及节点间进程同步
- 基础操作组合后可以得到集合通信中常用的通信原语
- 通信原语
- 一对多原语:Broadcast、Scatter
- 多对一原语:Gather、Reduce
- 多对多原语:All-to-All、All-Gather、All-Reduce、Reduce-Scatter
- 一对多广播 (Broadcast):将一个进程的数据广播到所有进程,常用于分享模型参数
- 一对多散射 (Scatter):将一个进程的数据按索引散射到多个进程,常用语数据分发
- 多对一收集 (Gather):从多个进程收集数据到一个进程,用于收集梯度
- 多对多收集 (All-Gather):从多个进程收集数据,并广播到所有进程,常用语数据同步
- 多对一归约 (Reduce):从多个进程收集数据,并按某种运算 (如求和运算) 归约到一个进程,常用于梯度累加
- 多对多归约 (All-Reduce):从多个进程收集数据,并按某些运算归约,再广播到所有进程,常用于数据同步与梯度累加
- 多对多归约散射 (Reduce-Scatter):从多个进程收集数据,按某种运算归约到一个进程,将该进程的数据按索引散射到对应进程上,用于更新权重
- 多对多交换 (All-to-All):将每个进程中的数据索引发射到其他进程,每个进程接收数据后以发送进程号为索引存储到对应的数据块中,常用于数据同步和信息传递
- 参数服务器:中心化的 Parameter Servers 架构,"中心化" 是指将模型参数进行中心化管理,以此实现模型参数的同步
- 分布式同步策略
- 设备之间的通信采用不同的同步策略
- 同步通信
- 需要等待全部计算节点完成本轮计算后才进行通信
- 时序性和顺序性
- 使用同步障确保计算节点之间的数据一致性
- 可能会导致较大的延迟和通信开销
- 同步障的存在确保全部设备完成通信后才可开始下一轮计算
- 异步通信
- 每个节点可以随时和其他设备进行通信
- 更加灵活
- 提高整个分布式训练系统的计算利用率
- 但不能保证数据的一致性
- 每个设备可以随时处理自己收到的信息,不会因为同步障而带来互相等待的开销
- 同步通信
- 选择合适的分布式同步策略对于保证分布式系统的正确性、性能和可扩展性至关重要
- 数据并行:对输入数据进行分区
- 往往用于解决单节点算力不足的问题。其中,每个设备共享完整的模型副本,输入数据会被分发给这些设备,减小单个设备的负载
- 模型并行:对模型参数进行分区
- 往往用于解决单节点内存不足的问题。一般模型并行分为算子内并行和算子间并行
- 算子内并行:大型算子计算所需内存超过单设备内存容量,对单算子切分;按行切分和按列切分
- 算子间并行:模型的总内存需求超过单设备的内存容量,在算子间进行切分
- 模型并行空泡现象:算子间并行中,下游设备需要等待上游设备计算完成,因此下游设备容易长期处于空闲状态
- -> 利用流水线技术缓解空泡现象 (流水并行)
- 流水并行
- 在模型并行中构建流水线,并利用流水线调度。该训练系统中,模型的上下游算子会被分配到不同的流水阶段,每个设备负责一个流水阶段
- 混合并行:同时对输入数据和模型参数进行分区
- 数据并行:对输入数据进行分区
- 分布式计算步骤:
- 将输入进行分区
- 将每个分区分发给不同的计算节点,实现并行计算
- 合并每个计算节点的输出,得到和单节点等价的计算结果
- 设备之间的通信采用不同的同步策略
- 分布式架构和分布式同步策略是分布式训练的基础
分布式训练框架实现
- 实现一个分布式训练框架
- 利用分布式架构和分布式同步策略
- 支持常见的分布式训练方法
- 达到高效利用设备资源的目的
- 分布式训练框架中最主要的两个模块
- 划分模块:划分训练任务;原始计算图可以拆分为多个子计算图
- 数据并行划分:对输入数据进行划分
- 模型并行划分:对模型参数进行划分
- 混合划分:对输入数据和模型参数都进行划分
- 通信模块:管理节点之间的通信;在设备之间进行通信,实现模型参数初始化和同步
- 支持基础通信操作和常见的通信原语
- 模型参数发送:初始化时需要将模型参数发送到各个设备数据
- 参数梯度平均:计算时需要对各个设备参数梯度平均
- 用成熟的通信库作为通信模块的基础
- 支持基础通信操作和常见的通信原语
- 划分模块:划分训练任务;原始计算图可以拆分为多个子计算图
- 实现一个分布式训练框架
# 深度学习处理器原理
通用计算机架构
为什么选用通用处理器
- 普及、廉价、灵活、适合小模型,少量数据,延迟和成本敏感的推理场景
冯诺依曼架构
- 包含控制器、运算器、存储器和输入 / 输出
- "存储程序":指令从主存储器中取出
高速缓存
- 用来弥补主存和运算器之间的剪刀差
- 自动暂存最近读取的数据,以备不久之后再次使用
- 通常使用 SRAM 实现
哈佛结构
- 指令缓存和数据缓存分离
- 允许同时进行取指和访存,互补干扰
精简指令集结构
- 关键原则:通过专门的 load/store 指令访存
- 实践中,处理器内部将复杂指令首先译为 RISC 微码
分支指令:由运算器计算出下一条指令的地址
- 分支指令计算完成前,暂停取值
多发射:多条互不相关的指令可以同时发射
- 可以同时利用多个运算器
地址生成单元:专用于计算访存地址的运算器
- 可以高效地支持多种寻址模式
寄存器重命名:将寄存器编号与物理寄存器相分离
- 消除伪相关,提高指令同时执行的机会
乱序执行,有序提交
- 实现精确例外,可以撤销已执行的指令
写入 / 写出队列:暂存已执行、未提交的访存指令
- 可以连续发起 load/store 指令,未提交可以撤销
数据前递:上一指令运算结果送入下一指令运算单元
- 可以省去连续运算时反复写入、读出寄存器的动作
分支预测:未确定跳转方向时,按猜测方向投机执行
- 预测正确时:减少等待时间,提高了流水线效率
- 预测错误时:不予提交,撤销错误执行的指令
为什么运算只占 1%
- 以标量作为基本运算粒度
- 需要更多指令来执行
- 任意指令间都潜在依赖,控制很复杂
- 内存墙现象
- 越来越大、越来越多层次的缓存
- 以标量作为基本运算粒度
控制、寻址
- 降低控制开销:循环展开
- 降低寻址开销:强度削减
- 均为通用技巧,现代编译器已经尽力而为
- 没有非常有效的优化方法
- 通用处理器为通用性而设计,深度学习只是一种应用
- 虽然深度学习程序行为规整,仍需较多指令才能定义清晰
- 每访存一个数据,都必须计算其地址,并控制循环条件
访存
- 分块 (tiling):将运算分解至固定尺寸的区块处理
- 通用处理器上实现矩乘,这两种实现方式:递归 (分治法),迭代 (三重循环)
向量指令:允许一条指令并行操作多个数据
向量处理器结构:
- 以通用处理结构为基础
- 可以保留标量部分,用于控制、寻址和少量标量计算
- 也可以完全由向量运算组成
- 相对独立地设计向量部分
- 更宽的寄存器、运算、数据通路
- 可以借用标量地址生成单元,访问向量数据
- 可以增加向量 / 标量数据交换通路,方便控制
- 以通用处理结构为基础
向量处理器的优势
- (相比标量通用处理器) 运算密度增加,控制寻址开销摊薄
- 针对并行应用较为灵活
向量处理器的缺陷:由于运算增加,访存瓶颈更加严峻
I/O 复杂度
- 程序执行时间同时受到两个因素制约
- I/O 时间
- 访存读取操作数
- 写回结果
- 读 / 写中间结果
- 计算时间
- 数据已准备好后,运算单元完成全部计算所需时间
- 执行时间I/O 数据量 最大访存带宽
- 执行时间 计算量 峰值运算能力
- I/O 时间
- I/O 数据量至少包含最初输入数据 + 最终输出数据
- 向量化不会改善访存
- 标量乘:1 次运算,2 个输入,1 个输出,每次运算 I/O 量为 3
- 向量乘:n 次运算,2n 个输入,n 个输出,平均到每个运算上,I/O 量仍为 3
- 向量处理器需等比例增加运算和带宽
- I/O 复杂度理论用来刻画运算量和 I/O 数据量的关系
- 程序执行时间同时受到两个因素制约
深度学习处理器概述
- 为什么需要深度学习处理器
- 深度学习应用广泛
- 图像识别、语音处理、自然语言处理、博弈游戏等领域
- 已渗透到云服务器和智能手机的方方面面
- 通用 CPU/GPU 处理人工神经网络效率低下
- 深度学习应用广泛
- 深度学习处理器发展的三个因素:architecture (结构)、Technology (科技)、Application (应用) 0
- 深度学习处理器的能效和通用性处于 ASICs 和 GPU 之间
深度学习处理器 (DLP)
基本运算单元从向量运算扩展到矩阵运算
深度学习处理器在相同带宽下,提供更高运算能力
访存
- 缓存 vs 便笺存储
- 深度学习访存行为规律,更适合使用便笺存储
控制:深度学习程序控制流以计数循环为主
启发:可以特别设计计数跳转指令
- 大多数跳转无需分支预测
以精简的通用处理器结构为基础
- 省去了分支预测器
- 取消数据缓存,I/O 直连外部主存
- 添加便笺存储器和直接访存模块 (DMA)
- DMA:代理主存与便笺存储器之间的大块连续数据搬运
- 添加矩阵指令的控制单元
- 矩阵指令在提交队列中,与标量 / 访存指令同步
深度学习处理器优势:
- 一条指令完成复杂的线性运算,控制开销低
- 同等访存,能达到更强的运算能力
DLP 的规模扩展
- 利用率问题:简单放大矩阵运算单元,导致利用率低下
多核 DLP
- 相比单体、巨大的矩阵运算单元,分立的多个小矩阵运算单元运用更灵活
- 但相应的,增加硬件成本 (特别是带宽)
- 按需设计
- 多核 DLP 结构
- 采用总线式结构,共享主存数据通路
- 发生争用时,总线进行仲裁,决定数据通路的通行权
- 将一份完整的 DLP 结构视为一个核
- 一致内存访问 (UMA) 模型
- 总线竞争将成为瓶颈
- 以 UMA 多核为基础,添加局部存储器、运算器,添加局部 DMA,与外部存储直接相连,将一份完整的 UMA 多核 DLP 结构,视为一个核
- 采用总线式结构,共享主存数据通路
- 相比单体、巨大的矩阵运算单元,分立的多个小矩阵运算单元运用更灵活
大规模深度学习处理器
- 分型 DLP:以相同规则任意扩展的 DLP 结构
- 同一份分治程序运行在所有的局部控制器上
- 无论 DLP 规模多大,在使用者的视角是一样的
- 分型 DLP:以相同规则任意扩展的 DLP 结构
# 深度学习处理器架构
计算
- 三种计算单元
- 矩阵、向量、标量
- 矩阵运算单元
- 一种实现:由内积单元堆叠而成
- 计算和 I/O 比例 = 1:3
- 多个内积单元组成矩阵乘向量单元
- 近端数据 (权值) 存储在内积单元附近的电路中
- 采用小而快的存储器
- 所有内积单元共享激活值,采用广播
- 矩阵乘向量单元
- 计算密度已经较好
- 矩阵乘矩阵单元
- 优势:规模大时,理论较好
- 困难:连线复杂,距离远,扇出多
- 规模不大时,未取得实际优势
- 脉动阵列机
- 由网络状的处理单元组成,每个处理单元都能执行简单的计算操作,例如乘法和加法
- 数据以脉冲的形式流动,每个处理单元在每个脉冲周期执行一次计算
脉动阵列机 vs 矩阵乘矩单元
- 优势:
- 计算 - I/O 比例更高 (脉动阵列机为 1:0.2;矩阵乘矩阵单元为 1:0.4)
- 电路采用局部短连接
- 扇出少
- 困难:
- 延迟高,需要等待启动 / 排空
- 专用性更强,高效支持矩乘 / 卷积,但很难改造为同时支持其他功能
- 优势:
向量和标量单元
- 主要功能
- 池化、归一化
- Dropout、ReLU、Sigmoid、Softmax 等特殊变换
- 求最大 / 最小值,排序,计数,前缀求和等
- 数据重新排布
- 池化 / 均一化:运算单元结构 - 支持 AvgPool、MaxPool、BatchNorm
- 主要功能
精确计算特殊函数
- 分段线性插值在深度学习推理任务中,基本满足需求
- 精确计算可以采用硬件或软件的方法
- 各函数的快速数值算法
- 数值方法
- 分段插值 / 快速估计 + 数值方法
数据重排布
- 以向量为单位,很难使向量上不同位置的数据相遇
- 因为便笺访问是对齐的
- 使用排列网络
计算小结:
- 矩阵运算单元
- 可设计为矩阵乘向量单元、矩阵乘法单元、脉动阵列机等
- 向量 / 标量运算单元
- 增设累加寄存器,可以实现池化
- 一组硬件可以同时支持多种功能
- 采用分段线性近似可以计算特殊函数
- 增设前缀计算、重排布等功能,有助于拓展通用性
- 增设累加寄存器,可以实现池化
访存:
- 访问便笺存储器
- 便笺存储器大多采用 SRAM 实现
- 便笺是 DLP 核当中的数据枢纽
- 缓解拥堵:拓宽道路、规划车流
- 多端口 SRAM (拓宽道路)
- 增加一个端口,面积 + 50%~100%
- 面积意味着成本、能耗、延时
- 分组 SRAM (拓宽道路)
- 开关阵列面积~O (分组数量 ^2)
- 分组冲突
- 通用处理器中,采用哈佛结构解决取值 - 取数冲突 (规划车流)
- 分离式编筏存储器 (二分离 / 三分离)(规划车流)
- 按数据划分
- 神经元 / 权值
- 输入神经元 / 输出神经元 / 权值
- 按功能划分
- 向量 / 标量
- 矩阵 / 向量 / 标量
- 按处理阶段划分
- 输入数据 / 累加器
- 对数据进行分流:提高处理效率;对使用方式进行了约束 (损失通用性)
- 体系结构设计人员的职责:寻找一组高效、合理的约束
- 按数据划分
- 访问外部存储器
- 通用处理器的访存
- 持续数个周期
- 访存和计算争用取指译码资源
- 简化硬件模型描述
- 计算模块:随时执行收到的指令
- DMA 模块:随时执行收到的指令
- 指令发射模块:
- 计算指令发射到计算模块
- 访存指令发射到 DMA 模块
- 遇到 sync 时:阻塞,直到整个处理器空闲下来,再发射新的指令
- 通用处理器的访存
- 与计算的协同
访存小结:
- 便笺存储器时 DLP 核心的数据枢纽
- 访问便笺可能成为瓶颈
- 拓宽道路:增加端口、设计为分组 SRAM (代价:硬件开销增加)
- 规划车流:根据算法特征,采用分离式设计模式 (代价:降低通用性)
- 通过软件流水线 (而不再是硬件) 使计算 / 访存并行起来
- 指令重新排序,不需要乱序执行
- 显式控制同步,不需要依赖检查
通信:
任务划分模式:
- 数据并行:全局归约
- 通过一个环,就可以高效实现全局归约
- 模型并行
- 算子并行:全局交换
- 流水线并行:局部通信
- 数据并行:全局归约
常用物理链路设计
- 总线
- 性能差,成本低
- 片上网络
- 性能较好,成本可控
- 交叉开关阵列
- 性能最佳,成本高
- 总线
通信结构的设计原则:
- 逻辑上:环状链路足以高效完成通信
- 物理上:链路设计适当增加冗余,按需配置环路
- 综合考虑性能和成本约束做出选择
优化设计:
- 常用优化设计:
- 变换:对算法进行变换,削减计算强度
- 快速矩阵乘法算法
- 快速卷积算法
- 算子融合
- 压缩:对算法进行压缩,直接减少算法的参数量和计算量
- 网络裁剪
- 结构化稀疏
- 串行计算
- 近似:对算法进行近似替代,降低计算成本
- 数值量化
- 思路:将数值替换为更容易计算得近似值
- 方法:用低精度数值近似高精度,辅以误差校正
- 算法近似
- 思路:从算法上将计算分解,提取出主要部分
- 低秩分解
- 差分计算
- 随机计算
- 思路:将数值编码成随机比特串进行计算
- 数值量化
- 非传统结构和器件:探索采用 CMOS 数字电路以外的技术,改写计算范式
- 存内计算
- 思路:利用内存结构,在内存部分完成一些计算功能
- 神经拟态计算
- 思路:数值可以用模拟物理量来表达
- 计算发生在权值的存储矩阵内
- 存内计算
- 变换:对算法进行变换,削减计算强度
# 智能编程语言
为什么需要智能编程语言
- 语义鸿沟 -> 开发效率低
- 硬件鸿沟 -> 执行效率低
- 平台鸿沟 -> 可移植性差
语义鸿沟
- 不同编程语言实现同一种运算使用的语句数量不同
硬件鸿沟
- 智能计算硬件在控制、存储和计算等方面有独特性
- 传统编程语言难以有效描述上述硬件特点
- 不同层次编程语言和硬件特性带来的性能影响
- 存储逻辑上
- 智能处理器一般采用程序员可见的 SPM,而不是通用平台上程序员透明的 Cache
- 计算逻辑上
- 智能处理器提供了面向智能计算的定制运算单元,如 16 位浮点、Brain 浮点等,当前编程语言层面主要提供的是 int 和 fp32 等数据类型,难以高效利用这些运算单元
平台鸿沟
- 功能可移植性
- 采用特定平台专用语言所编写的程序能够在别的平台上正常运行
- 矩阵乘法中调用的 AVX intrinsic 函数在 ARM 上无法运行
- 性能可移植性
- 在特定平台上优化好的程序,在新的硬件平台仍然保证有较高的执行效率
- 理想的编程语言需抽取不同硬件平台的共性特征,在此基础上提取性能关键特征作为语言特性提供给用户
智能计算系统抽象架构
抽象硬件架构
- 层次化的智能计算系统抽象硬件架构
- 智能计算系统中每一层都包含存储单元、控制单元和若干个计算单元
- 每个计算单元又进一步分解为子控制单元和子存储单元三部分,整个系统以这样的方式递归构成
- 在最底层,每个叶节点都是具体的加速器,用于完成最基本的计算任务
- 层次化的智能计算系统抽象硬件架构
典型智能计算系统
- 多卡的 DLP 服务器抽象为五个层次
- 服务器级
- 板卡级
- 芯片级
- 处理器簇级
- 处理器核级
- 多卡的 DLP 服务器抽象为五个层次
控制模型
- 指令是实现对计算和存储进行控制的关键。为了设计高效的指令集、需要充分分析智能领域的典型计算模式,提炼最具有代表性的操作,并进行针对性设计
- 对智能算法进行抽象
- 控制
- 数据传输
- 计算:标量、向量和矩阵运算等
- 逻辑操作:标量和向量运算等
- 关注计算与存储的交互
- 尽可能将计算与存储并行
- 对智能算法进行抽象
- 指令是实现对计算和存储进行控制的关键。为了设计高效的指令集、需要充分分析智能领域的典型计算模式,提炼最具有代表性的操作,并进行针对性设计
计算模型
- 程序员可见的主要包括定制运算单元和并行计算架构
- 定制运算单元
- 利用智能应用误差容忍的特性,采用定制的低位宽运算单元以提升处理能效
- 由于智能应用的多样性与复杂性,对于哪种低位宽最为合适并未形成统一结论
- 并行计算架构
- 任务切分与同步
- 程序员需关注如何对任务进行切分,将任务尽量均衡地分配到大量并行计算单元上执行
- 对于每个层次中的计算单元,需要有相应的计算同步机制,以保证切分后任务间的依赖关系
存储模型
- 智能应用中存在大量数据集的内存访问,因此合理地组织存储层次和计算单元同样重要,需要两者协同设计以平衡计算与访存,实现高效的智能计算
- 分为全局存储和本地存储
智能编程模型
异构编程模型
分类及流程
异构编程模型的编译与链接流程
- 整体采用分离式编程方式:主机端代码和设备端代码
异构编程模型从用户接口角度分类
- 构建全新的异构并行编程语言
- 对现有编程语言进行异构并行扩展
- 语言扩展:OpenCL,CUDA
- 新语言:Copperhead,Merge
编译器支持
- 编译器支持:是异构并行编程模型的核心
- 任务划分:编程模型需要向程序员提供并行编程接口,方便程序员定义和划分任务。编译器负责底层的任务划分,使得程序可以在并行架构上高效执行
- 数据分布:对编译器和底层运行时系统而言,需要根据算法和硬件架构的特点,通过合适的数据分布指导后续编译和运行时优化
- 数据通信:由于设备端通常有多级存储空间、编译器需要支持各种地址空间声明,以方便程序员显式控制存储数据的地址空间
- 并行同步:设备端程序一般要求感知多个核的并行处理,因此需要提供对同步机制的支持
- 编译器支持:是异构并行编程模型的核心
运行时支持
- 运行时支持
- 完成任务映射及调度,即指定任务具体在哪个设备或计算单元上以何种顺序执行
- 分为主机端和设备端
- 主机端:控制部分和串行任务在主机端执行
- 设备端:计算部分和并行任务在设备端执行
- 运行时支持
DLP 编程模型
Kernel 定义
- 定义在设备端 DLP 上的核心计算任务
- 与异构编程模型的概念一致,DLP 上执行的任务叫 Kernel,资源允许的情况下 DLP 可以同时执行多个并行的 Kernel
编译器支持
- 指定 DLP 上核心计算任务如何高效地翻译成目标代码
- 任务划分
- 并行内建变量
- 任务调度类型:
- 表示 Kernel 运行调度时需要的内核
- BLOCK 类型:Kernel 为单核任务,按单核进行调度
- UNIONx 类型:Kernel 为多核并行任务
- 数据通信:为 DLP 的复杂存储层次提供支持
- 隐式数据管理:GPR 标量数据,由编译器隐式插入 Load/Store 指令
- 显式数据管理:DRAM/NRAM/WRAM/SRAM 间向量及张量数据
- 主机 - DLP 间 DRAM 数据
- 同步支持:为并行架构提供支持
- 抽象硬件架构中由 Clip-Cluster-Core 的层次结构,Core 内还支持指令队列之间的并行,因此智能编程语言需要提供至少三种不同类型的同步操作:同步一个 Core 内所有的指令队列;同步一个 Cluster 内部的所有核;同步任务执行的所有核
- 内建运算:为用户编程提供支持,提高开发效率
运行时支持
- 指定 DLP 上核心计算任务以何种方式映射到计算单元
- 任务调度单位
- 将 Kernel 中的任务在时间或空间维度展开
- 调度单位需要用户在编程时指定。运行时只有当空闲的硬件资源数大于调度单位时,Kernel 才会被调度
- 队列
- 管理需要执行的任务,队列既可以单独工作,也可以协同工作
- 运行时不断把任务放到队列中,一旦硬件计算资源有空闲,就从队列中取出一个任务执行
异构计算系统组成
- 通用处理器
- 控制设备 (简称主机端),负责控制和调度等工作
- 领域处理器
- 从设备 (简称设备端),负责大规模的并行计算或领域专用计算任务
- 二者协同完成完整计算任务
- 通用处理器
典型异构计算系统
- GPU 为核心、FPGA 为核心、TPU 为核心、DLP 为核心
智能编程语言基础
语法概述
智能编程语言考虑基于过程式语言
- 当前大多数语言都是过程式的,可以减少用户学习成本
- 当前主流智能算法可以描述为明确的过程,适合采用过程式语言描述
定义函数与 C 语言一致:
int add_func(int a,int b){ int c=a+b; return c; }
数据类型
- int8_t:1 字节整数
- uint8_t:1 字节无符号整数
- int16_t:2 字节整数
- uint16_t:2 字节无符号整数
- int32_t:4 字节整数
- uint32_t:4 字节无符号整数
- half:半精度浮点数据类型,采用 IEEE-754 fp 16 格式,2 字节
- float:IEEE-754 fp 32 格式浮点类型,目前仅支持类型转换计算,4 字节
- char:对应 C 语言 char 类型
- bool:对应 C 语言 bool 类型
- 指针:指针类型(8 字节)
宏、常量与内置变量
- 宏和常量,宏不仅可以定义常量数据,也可定义一段代码;常量是不可修改的数据,只能在初始化时被赋值
- 内置变量,编程语言本身包含的常量和变量,不需用户定义即可直接使用
- coreId:DLP 中核的编号
- clusterId:DLP 中簇的编号
- taskId:程序运行时分配的任务编号
I/O 操作语句
不同层次的智能处理节点由各自的本地存储,需要提供不同存储层次间的数据搬移
典型的 NUMA 架构
- 不同处理器核对应不同位置存储器的访问速度不同
- 对于核 0 而言,其访问设备内存 DDR0 的速度比访问 DDR1 的速度更快。DDR0 可看作是本地存储,DDR1 可看作全局存储
- 片上存储,除了单核内 NRAM 和权值 WRAM,还有一类共享存储,可用于簇内多核共享
针对搬移操作类型,可以在智能编程语言中定义相应的内建函数_memcpy,方便用户进行不同类型的数据搬移
void _memcpy(void* dst,void* src,uint32 bytes,Direction_t dir);
标量计算语句
- 标量即单个数据的计算,标量计算是编程语言的基本功能
- 智能编程语言的标量计算语句由两种形式:运算符号 (+,-,*,/);内建函数 (abs,max,min)
- 智能编程语言的标量计算由编译器映射到标量计算单元,虽然吞吐量上不及张量计算,但具有良好的通用性和灵活性
- 标量即单个数据的计算,标量计算是编程语言的基本功能
张量计算语句
张量计算是智能编程语言的主要特点,可以通过内建函数直接映射到张量单元
__vector_add(DType* dst,const DType* lhs,const DType* rhs,int elem_num); //向量对位加 __vector_sub(DType* dst,const DType* lhs,const DType* rhs,int elem_num); //向量对位减 __vector_mul(DType* dst,const DType* lhs,const DType* rhs,int elem_num); //向量对位乘 __vector_relu(DType* dst,const DType* src,int elem_num); //向量Relu __vector_argmax(DType* dst,const DType* src,int elem_num); //向量ArgMax
控制流语句
- 与通用编程语言一样,智能编程语言同样需要由分支和循环等控制流语句
- 分支语句:处理程序选择逻辑,与传统编程语言类似
- 循环语句:处理程序循环逻辑,与传统编程语言类似
- 同步语句:解决多核间并行数据依赖问题,保证最终计算结果正确
- 核内同步 (__sync)
- Cluster 内同步 (__sync_cluster)
- 全局同步 (__sync_all)
- 与通用编程语言一样,智能编程语言同样需要由分支和循环等控制流语句
串行程序示例
向量中每个数求平方,每次处理 64 个数
#define BASE_NUM 64 void __dlp_entry__ mySquare(float* in,float* out,int size){ int quotient = size/BASE_NUM; int remainer = size%BASE_NUM; __nram__ float tmp[BASE_NUM]; for(int i=0;i<quotient;i++){ __memcpy(tmp,(in+i*BASE_NUM),(BASE_NUM*sizeof(float)),GDRAM2NRAM); __vec_mul(tmp,tmp,tmp,BASE_NUM); __memcpy((out+i*BASE_NUM),tmp,(BASE_NUM*sizeof(float)),NRAM2GDRAM); } if (remainder!=0){ __memcpy(tmp,(in+quotient*BASE_NUM),(remainder*sizeof(float)),GDRAM2NRAM); __vec_mul(tmp,tmp,tmp,remainder); __memcpy((out+quotient*BASE_NUM),tmp,(remainder*sizeof(float)),NRAM2GDRAM); } }
并行程序示例
矩阵乘法实例,在 4 个核上并行执行,每个核上代码一致
计算 1*32 和 32*32 的矩阵乘法,最终得到 4*32 的结果
通过运行时 API 来指定任务规模 (4*1*4) 及调度方式 (UNION1)
void __dlp__entry__ mm(int* left,int* right,int* out){ //设备端 if(taskID==0){ __nram__ int tmp[42][42]; __write_zero(tmp,4*32*sizeof(int)); __memcpy(out,tmp,4*32*sizeof(int),NRAM2GDRAM); } __sync_all(); for(int j=0;j<32;j++){ for(int k=0;k<32;k++){ out[taskIdX*32+j]+=left[taskIdX*32+k]*right[k*32+j]; } } } //主机端 //任务规模 Dim_t dim; dim.x=4;dim.y=1;dim.z=1; int left[4][32];int right[32][32];int out[4][32]; ... KernelParamsBuffer_t params; GetKernelParamsBuffer(¶ms); KernelParamsBufferAddParam(params,&left_dev,sizeof(void*)); KernelParamsBufferAddParam(params,&right_dev,sizeof(void*)); KernelParamsBufferAddParam(params,&out_dev,sizeof(void*)); ... //启动4个核并执行矩阵乘法 InvokeKernel((void*)(&mm),dim,params,UNION1,queue); SyncQueue(queue); //调度类型
智能应用编程接口
- Kernel 函数接口
- 主要关注任务切分和到硬件的映射
- 为了充分利用并行资源,需要在 Kernel 内部对任务进行有效切分,同时在主机配置和调用相应的 Kernel 函数接口
- 任务切分的内置变量
- coreDim (核维数)、coreId (核序号)、clusterDim (簇维数)、clusterId (簇序号)、taskDim (任务维数)、taskId (任务序号)
- 主机端 Kernel 函数接口
- 用于将智能编程语言编写的程序加载到深度学习处理器上执行
- Kernel 函数相关的接口主要关注 Kernel 参数设置和 Kernel 调用
- InvokeKernel(const void *kernel,dlpDim3_t dim,FunctionType_t ktype,void **args,size_t reserved,Queue_t queue);
- DLP 还可以通过编译器和运行时的配合,实现 foo<<<>>>() 语法糖
- 运行时接口
- 主要关注设备管理、队列和内存管理等
- 包括设备管理、队列管理和内存管理等接口
- 设备管理:主要涉及设备获取和设置、属性获取操作
- Init 和 Destroy 可由运行时库隐式自动完成,无需用户感知
- GetDevice (int *pOrdinal); 获取主机端当前线程上下文所使用的设备序号
- SetDevice (int ordinal); 为当前主机端线程设置所使用的设备序号
- DeviceGetAtrribute (int *pValue,DeviceAttr_t attr,int devive); 获取设备属性
- 队列管理
- 队列是用于执行任务的环境
- 计算任务可以下发到队列中执行
- 同一个队列可以容纳多个任务
- 队列具有以下属性:
- 串行性、异步性、并行性
- QueueCreate(Queue_t *pQueue);
- QueueSync(Queue_t queue);
- QueueDestroy(Queue_t queue);
- 内存管理
- 内存管理主要分为主机端内存管理、设备端内存管理和主机与设备端内存拷贝三类
- 设备管理:主要涉及设备获取和设置、属性获取操作
- 使用示例
__dlp_entry__ void kernel(int *input,int len,int *output){ //完成Device端的Kernel函数编写
int sum=0;
for(int i=0;i<len;i++){
sum+=input[i];
}
*output=sum;
}
//对于每个智能编程语言编写的程序,有且仅有一个标记为__dlp_entry__的函数,表示整个函数的入口,其返回值必须是void
//Host端代码
//1、设备隐式初始化和获取设备属性
int main(){
int ordinal=-1;
GetDevice(*ordinal);
int value=0;
DeviceGetAttribute(&value,AttrClusterCount,ordinal);
printf("device:%d,AttrClusterCount:%d.\n",ordinal,value);
return 0;
}
//设备的初始化和销毁由运行时库隐式自动完成
//使用GetDevice可以获取当前的设备序号
//使用DeviceGetAttribute可以获取当前设备的属性
//2、主机/设备端数据准备
//3、设备端内存空间分配
half* d_input;
half* d_output;
half* dlp_result;
hostMalloc(dlp_result,data_num*sizeof(half));
devMalloc((void**)&d_input,data_num*sizeof(half));
devMalloc((void**)&d_output,data_num*sizeof(half));
//4、数据至设备端拷贝
Memcpy(d_input,h_a_half,data_num*sizeof(half),HOST2DEV);
//5、调用Kernel启动设备
//Runtime APL
void *args[]={&d_input,&size,&d_output};
InvokeKernel((const void*)kernel,dim,func_type,0,queue);
//Grammar sugar.
kernel<<<dim,func_type,queue>>>(d_input,size,d_output);
//6、运行结果获取
Memcpy(dlp_result,d_output,data_num*sizeof(half),DEV2HOST);
//7、资源释放
devFree(d_input);
devFree(d_output);
hostFree(dlp_result);
QueueDestroy(pQueue);
智能编程模型实例:BANG 异构编程
BANG 语言是针对 MLU 硬件提出的编程语言,兼顾云边端等不同目标平台,提高高性能机器学习计算支持
- 提供通用的异构编程模型,方便用户扩展自己的应用程序
- 提供高效的编程接口,充分发挥底层硬件特性
- 基于 C/C++ 语言的扩展,简单易用
BANG 异构编程:流程
- 主机端和设备端程序分开编程并分别编译,最后链接成一个可执行程序
- 主机端为 C/C++ 程序,通常调用 CNRT 运行时接口完成以下步骤
- 1、准备输入数据;2、拷贝输入数据到 MLU;3、准备 Kernel 函数;4、创建 Queue;5、指定 Kernel 任务规模以及调度类型;6、启动 Kernel;7、MLU 到主机的输出数据拷贝;8、资源的释放
- cnrtGetDevice (获取设备),cnrtQueueCreate (创建一个新的 Queue,默认异步运行),cnrtQueueDestroy (销毁 Queue),cnrtQueueSync (直到之前 Queue 中所有的 Function 都完成,阻塞其他 Function),cnrtInvokeKernel (通过在 MLU 上给定的参数块,启动 Kernel),foo<<<>>>()(编译器和运行时配合完成的语法糖,可替代 cnrtInvokeKernel),cnrtMalloc (分配给定空间的设备内存),cnrtFree (释放指针指向的空间),cnrtMemcpy (从源地址拷贝指定字节数到目的地址)
- 设备端使用 BANG 语言特定的语法规则和接口进行编程
BANG 程序编译与链接
- BANG 的源码可以写在同一份源码文件中,编译器自动完成主机端和设备端的编译和链接
- foo.mlu 混合编程源码被 cncc 前端 driver 拆分成主机和设备源码
- 主机端源码使用 clang 进行编译,得到主机端的二进制对象文件
- 设备端源码根据指定的单一 arch 或多 arch,分别编译成目标 arch 的对象文件
- 设备端多 arch 的 bin 对象被设备端链接器链接成 fatbin,并包装成主机端可识别的对象文件
- 主机端链接器将主机和设备端对象文件链接为可执行文件
- BANG 的源码可以写在同一份源码文件中,编译器自动完成主机端和设备端的编译和链接
张量计算语句实例:BANG 数学库
- BANG 语言将 MLU 的数学类和神经网络类指令封装成库函数
- BANG 数学库函数是在 MLU 架构上进行高性能编程的关键
- 使用约束:张量计算通常是对批量数据进行操作
- 源和目的都是 NRAM 上的数据
智能应用功能调试
- 功能调试接口
- __assert(bool flag):abort the kernel if flag is false
- __abort(): exit the kernel with error code -1
- exit(int status): exit the kernel with code status
- printf ("< 格式化字符串>",< 参数变量 >):将字符打印到屏幕
- BANG 调试:格式化输出
- 在设备侧代码中可以使用 printf 实现格式化输出,输出结果默认打印到控制台
- 功能调试工具
- 面相智能编程语言 BCL 的调试器
- 整体流程:调试前准备、调试器托管、状态查看及错误分析
- 调试前准备:
- 配置调试目标设备号
- 增加调试信息时,编译时使用 - g 选项
- 调试器托管
- 通过调试器执行被调试的程序来实现托管。由于是异构程序,必须从主机端程序启动。
- 针对多核架构 DLP 考虑在不同核间切换,可以用 info 命令查看调试焦点,并使用 focus 命令进行切换
- 采用 break 命令可以根据函数名、代码行号、指令地址以及 kernel 入口来增加断点。在 break 命令中可以使用 if 语句配置条件断点。断点的查看和删除则可以分别使用 info 和 delete 命令来完成
- 状态查看
- 查看变量内容:调试时可直接根据变量名采用 print 命令来打印相关内容
- 查看寄存器内容:寄存器内容则可以采用 info registers 命令进行查看
- 查看地址内容:指定地址中的数据内容可以通过 examine 命令查看
智能应用性能调优
- 性能分析方法
- 识别瓶颈的过程
- 先找到耗时长的部分 -> 通知接口
- 再通过硬件计数器分析硬件执行特征 -> 硬件计数器接口
- 识别瓶颈的过程
- 性能调优工具
- 在程序外部监控程序的运行状态,分析其执行瓶颈,找到优化空间
- 可以分为两类:
- 应用级性能剖析工具
- 采用 record 命令来运行可执行程序并生成相应的性能分析报告
- 采用 report 或者 kernel 命令查看性能分析报告,获取包括执行时间、调用关系以及性能计数器等信息
- 系统级性能监控工具
- 系统级性能监控工具主要利用驱动,通过读取寄存器的方式来收集硬件的静态和动态信息
- 应用级性能剖析工具
- 性能调优方法
- DLP 核函数调优的核心思想:如何充分利用大规模的并行计算单元
- 使用片上存储
- 向量化:将大量标量计算合并为张量计算,使用智能编程语言的向量计算语句改写代码
- 软件流水
- 智能处理器的计算和访存单元可以并行空座
- 编程时可以显示将无依赖的计算和访存指令放在一起,从而提高硬件的利用率和程序性能
- 计算和访存并行最常用的方法是三级流水
- 多核并行
- 针对 (程序员可见的) 多核,可以将一个任务分拆到多个核上并行计算,进一步提升程序性能
- DLP 核函数调优的核心思想:如何充分利用大规模的并行计算单元
智能编程语言的应用
- 高性能库算子开发
- 高性能库提供了常见算子在特定平台上的高性能实现,方便用户以 API 的形式直接调用
- 高性能库算子开发的关键在于:
- Kernel 代码逻辑的开发与优化
- 高性能库算子接口 API 的使用
- 编程框架算子开发
- 原理及流程
- 编程框架为智能计算硬件和应用程序之间的桥梁,提供了丰富的算子和多架构后端的运行时系统
- 从编程框架内部看,智能编程语言既提供了开发设备端核函数代码的能力,又提供了主机端异构执行的运行时支持
- 使用智能编程语言直接在编程框架中添加算子,可分为:
- 框架内部注册添加算子
- 利用框架的扩展能力添加自定义算子
- 原理及流程
# 大模型计算系统
大模型算法分析
- 大模型的获得和使用
- 训练:在训练阶段,需要使用大量的语料数据和大规模的计算资源,从头训练大模型的模型参数
- 推理:推理是指在大模型训练完成后,使用大模型完成相关的任务
- 预训练:预训练是使用大量无标注得到语料数据,旨在通过训练让大模型学习到通用的语言能力和知识
- 微调:微调则是为了提升大模型在特定下游任务的表现,因此微调阶段使用特定任务的数据训练大模型
- 大模型算法分类
- 编码器 - 解码器结构
- 仅编码器结构
- 仅解码器结构
- 大语言模型是多模态大模型的基础
大模型驱动反例:BLOOM
BLOOM-176B 模型的训练过程:
- 混合并行技术
- 张量并行(ALL-Reduce 通信)
- 张量并行是一种将张量沿特定维度分割为 N 个块的技术
- 这样每个设备仅保存该张量的 1/N 个块
- MLP:矩阵按照行和列切分
- Self-Attention:按照 Head 切分和按照行切分
- 数据并行 (ALL-Reduce 通信) 与流水线并行
- 并行组
- 每一个组内的智能处理之间会通过通信库实现数据通信,是实际通信时的操作单元
- 进一步可以分为数据并行组
- 张量模型并行组
- 和流水线模型并行组
BLOOM-176B 模型的推理过程
自回归推理
- 硬件:1 个计算节点
- 并行计算策略:张量并行或者流水线并行皆可
计算分析
- 对于一个解码器块而言,正向传播时的浮点运算主要分为 5 个部分
- 随着序列变长,注意力计算量占总比逐渐提升
- 但注意力的运算密度始终低于智能处理器的运算密度
- 多头注意力的运算可能成为大模型训练计算中的一个瓶颈
- 总浮点计算量:
- 反向传播计算量 = 正向传播计算量 * 2
- 微批量 b=2,全局批量 B=16 时,需 34.8FLOPs
- 忽略存储容量,一块 A100 需要算数十分钟
存储分析
- 实际应用中,还需要存放神经元数据,这进一步增加了对存储空间的需求
通信分析
- 除了通信数据量大以外,大模型训练的通信还具有一下特点:
- 通信次数多,无论数据并行、张量并行、流水线并行,均会产生必要的数据通信的同步
- 通信分布不均匀,由于模型的前向和反向传播时的算子依赖关系,某些层可能需要等待其他层完成后才能通信,导致通信在时间上不均匀
- 除了通信数据量大以外,大模型训练的通信还具有一下特点:
大模型系统软件
- 为什么采用大模型系统软件
- 传统的深度学习系统软件已经难以满足大模型的特殊需求
- 为了解决模型在训练和推理过程中遇到的独特挑战,如模型并行化、存储管理、通信优化等
- 更加注重资源利用的高效性、分布式计算的优化、以及模型的可扩展性
- 考虑如何在有限的硬件资源上实现超大规模模型的有效训练,如何通过模型裁剪、混合精度训练等技术
- 训练场景中计算相关优化:稀疏注意力机制
- 通常情况下当前词与相邻若干词存在关联,与很远的词关联比较弱
- 所有词计算自注意力 -> 信息冗余 -> 注意力存在稀疏性
- 通过基于块的稀疏运算,将原始注意力机制的计算需求降低几个数量级
- 稀疏注意力机制在原本全局注意力的基础上,额外引入了局部注意力和随机注意力的概念
- 训练场景中计算相关优化:专用数据类型
- 除了传统的单精度浮点数类型和半精度浮点数据类型之外,各类智能硬件还设计了专用数据类型,在基于混合精度训练的大模型训练过程中广泛使用
- 使用 TF32 代替 FP32 可以几乎不降低精度的情况下,提升运算速度
- 训练场景中计算相关优化:ZeRO 系列存储优化
- 优化器状态被分配到所有数据并行的 GPU 上,而不是被复制,并在训练过程中使用基于 all-gather/broadcast 的通信集合即时重建
- 训练场景中计算相关优化:重计算优化
- 正向传播时的激活值需要一直保留到反向传播时使用,占用了很大的存储空间
- 重计算优化指的是在正向传播时不保存所有层的激活值,而是仅保留部分层的计算结果作为检查点,然后在反向传播时再根据检查点重新计算所需的激活值
- 选择性重计算,通过对 Transformer 层内部计算量和和存储量的量化分析,选择性的将中间层的激活值保留或舍弃,最终能够在引入可忽略不计的计算量的前提下,将激活值的存储使用减少 5 倍。
- 计算换存储,计算增加约 30%~40%
- 训练场景中计算相关优化:注意力机制融合优化
- 长序列时,Attention 的计算中间结果存储需求显著增加,因此较长的上下文长度会引发较大的访存量,进而影响了整体训练的性能
- 训练场景中通信相关优化
- 旨在减少数据传输量、提高通信效率和减少通信与计算的竞争
- 相关优化技术如 3D 并行参数调优、梯度压缩和通信拓扑优化等,已被广泛研究和应用
- 典型的如 DeepSpeed 中专为大模型训练引入的 1-bit Adam 算法优化,可以在保持模型精度的同时,最大减少 5 倍的通信量,并获得最高 3.3 倍的训练性能提升
- 推理场景中计算相关优化:批处理优化
- 多个任务直接批处理
- 连续批处理方法
- 推理场景中计算相关优化:键值缓存优化
- 键值缓存
- 在处理一个序列时,通过缓存过去的生成结果以避免重复计算的方法,从而减少大模型推理的计算量
- 键值缓存
- 推理场景中存储相关优化:键值缓存分页优化
- 借鉴操作系统中的分页思想,提高系统对存储的利用率
- 推理场景中存储相关优化:量化优化
大模型计算节点:计算节点的拓扑结构
- 单个大模型计算节点主要包括若干 CPU 构成的控制单元、主机端存储单元和若干 DLP 板卡构成的计算单元
- 不同的拓扑结构主要影响是:
- 处理器与 DLP 板卡之间的总通信带宽
- DLP 板卡之间互相通信的带宽
- DLP 板卡之间互相通信的延迟
大模型计算节点:智能处理器的互联
大模型计算集群:计算集群的系统结构
- 多机多卡集群配置大量的计算节点
- 配置若干登录管理节点进行集群管理工作
- 同时为了确保节点之间的对数据的统一访问以及高速通讯,集群还应该配置统一的网络数据存储和多套互联通信网络
大模型计算集群:计算集群的网络拓扑
- 对于大模型训练而言,张量模型并行带来的通信开销最大,因此应该将张量模型并行的范围控制在服务器本地,然后使用流水线并行来跨服务器扩展更大的网络模型
大模型计算集群:计算集群的网络传输
- 较于 TCP/IP,RDMA 零拷贝(zero copy)减少用户空间和内核空间中来回复制数据的开销,内核旁路(kernel bypass)减少了软件调用的开销,这些都无需双方操作系统内核参与,因此 RDMA 具有高吞吐、低延迟和低 CPU 开销的特点