2024年7月

PrimiHub
一款由密码学专家团队打造的开源隐私计算平台,专注于分享数据安全、密码学、联邦学习、同态加密等隐私计算领域的技术和内容。

在现代信息社会,密码学在保护信息安全中扮演着至关重要的角色。而归约证明(Reduction Proof)作为密码学中的一个重要工具,通过将一个问题的安全性归约为另一个已知问题的难解性,从而证明新问题的安全性。本文将详细介绍归约证明的概念、步骤及其在密码学中的应用,并通过具体实例和图示来帮助读者更好地理解这一重要技术。

归约证明的基本概念

归约证明的定义

归约证明是一种证明方法,通过将一个待证明的问题(目标问题)转换为另一个已知难解的问题(基准问题),来证明目标问题的难度不低于基准问题。简单来说,假设我们已经知道某个问题很难解决,如果能证明我们要研究的问题至少和这个已知的难题一样难解,那么就可以认为我们的问题也具有相应的安全性。

举例说明

设想我们有一个新的密码算法A,希望证明其安全性。已知离散对数问题(Discrete Logarithm Problem, DLP)是一个公认的难解问题。我们可以尝试通过归约证明:如果能够在多项式时间内破解算法A,那么也能在多项式时间内解决DLP。这就意味着破解算法A也是难解的,从而证明了算法A的安全性。

归约证明的步骤

归约证明一般包括以下几个步骤:

  1. 选择基准问题
    :选择一个公认的难解问题作为基准问题。
  2. 构造归约
    :设计一个多项式时间算法,将基准问题转换为目标问题。
  3. 验证归约
    :证明转换后的目标问题确实能够解决原始的基准问题。

步骤详解

选择基准问题

基准问题一般是公认的难解问题,如NP完全问题、大整数分解问题或离散对数问题。这些问题被认为在现有计算能力下无法在多项式时间内解决。

构造归约

构造归约的过程需要设计一个算法,将基准问题转换为目标问题。这要求归约过程在多项式时间内完成,以保证转换的有效性。

验证归约

验证归约的过程需要证明:如果能够在多项式时间内解决目标问题,那么就能在多项式时间内解决基准问题。这一步是归约证明的核心,确保目标问题的难度不低于基准问题。

graph TD;
A[基准问题] -->|归约| B[目标问题];
B -->|证明难解| C[算法安全性]

多项式时间(Polynomial Time)

指算法运行时间是输入规模的某个多项式函数。多项式时间的算法被认为是有效率的,因为其运行时间随着输入规模的增加而成多项式增长。

NP完全问题(NP-complete Problem)

NP完全问题是一类计算上非常困难的问题,任何NP问题都能在多项式时间内归约为它。如果能够找到一个多项式时间内解决NP完全问题的算法,那么所有NP问题都能在多项式时间内解决。

离散对数问题(Discrete Logarithm Problem, DLP)

离散对数问题是指给定一个大质数
\(p\)
和一个生成元
\(g\)
,找到x使得
\(g^x \equiv h \mod p\)
。这是一个计算上公认的困难问题,广泛应用于密码学。

归约证明的应用

公钥加密中的应用

在公钥加密系统中,归约证明常用于证明加密算法的抗攻击性。例如,RSA加密算法的安全性可以归约为大整数分解问题的难度。如果有人能够在多项式时间内破解RSA加密,那么他也能在多项式时间内完成大整数分解,这在目前的计算理论中被认为是极其困难的。

数字签名中的应用

在数字签名方案中,归约证明可以用来证明签名的不可伪造性。例如,椭圆曲线数字签名算法(ECDSA)的安全性可以归约为椭圆曲线离散对数问题的难度。

实例分析

RSA加密算法的归约证明

RSA加密算法的安全性可以归约为大整数分解问题的难度。具体步骤如下:

  1. 选择基准问题
    :大整数分解问题。
  2. 构造归约
    :设计一个算法,将大整数分解问题转换为破解RSA加密。
  3. 验证归约
    :证明如果能够破解RSA加密,那么就能够在多项式时间内完成大整数分解。

椭圆曲线数字签名算法(ECDSA)

ECDSA的安全性可以归约为椭圆曲线离散对数问题(ECDLP)。具体步骤如下:

  1. 选择基准问题
    :椭圆曲线离散对数问题。
  2. 构造归约
    :设计一个算法,将ECDLP转换为破解ECDSA签名。
  3. 验证归约
    :证明如果能够伪造ECDSA签名,那么就能够在多项式时间内解决ECDLP。

归约证明的局限性

尽管归约证明是一个强大的工具,但它也存在一些局限性:

  1. 基准问题的依赖性
    :归约证明依赖于基准问题的难解性。如果基准问题被攻破,那么基于该归约证明的安全性也会受到质疑。
  2. 归约过程的复杂性
    :构造归约过程可能非常复杂,有时难以找到合适的基准问题和归约方法。
  3. 实际应用的挑战
    :归约证明在理论上能保证安全性,但在实际应用中可能遇到未预见的挑战和漏洞。

结论

归约证明在密码学中起着至关重要的作用,通过将新问题归约为已知难解问题,我们能够更有信心地评估新算法的安全性。尽管归约证明有其局限性,但它依然是密码学研究中不可或缺的工具。希望本文通过详细的介绍和实例分析,能够帮助读者更好地理解归约证明的概念和应用。

PrimiHub
一款由密码学专家团队打造的开源隐私计算平台,专注于分享数据安全、密码学、联邦学习、同态加密等隐私计算领域的技术和内容。

CaiT通过LayerScale层来保证深度ViT训练的稳定性,加上将特征学习和分类信息提取隔离的class-attention层达到了很不错的性能,值得看看

来源:晓飞的算法工程笔记 公众号

论文: Going deeper with Image Transformers

Introduction


自ResNet出现以来,残差架构在计算机视觉中非常突出:

其中函数
\(g_l\)

\(R_l\)
定义了网络如何更新第
l
层的输入
\(x_l\)
。函数
\(g_l\)
通常是恒等式,而残差分支
\(R_l\)
则是网络构建的核心模块,许多研究都着力于残差分支
\(R_l\)
的变体以及如何对
\(R_l\)
进行初始化。实际上,残差结构突出了训练优化和结构设计之间的相互作用,正如ResNet作者所说的:残差结构没有提供更好的特征表达能力,之所以取得更好的性能,是因为残差结构更容易训练。

目前很火的ViT网络可认为是实现了一种特定形式的残差架构:在将输入图像转换为一组
\(x_0\)
的向量之后,网络交替进行自注意力层 (SA) 与前馈网络 (FFN) 处理:

其中
\(\eta\)
是LayerNorm算子。

对于卷积神经网络和应用于NLP或语音任务的Transformer,如何对残差架构的残差分支进行归一化、加权或初始化受到了广泛关注。作者也在ViT上对不同初始化、优化和架构设计之间的相互作用进行了分析,并且提出了LayerScale层。LayerScale层包含一个初始权值接近于零的可学习对角矩阵,加在每个残差模块的输出上,可以有效地改进更深层架构的训练。

此外,作者还提出了class-attention层。类似于编码器/解码器架构,显示地将用于token间特征提取的transformer层与将token整合成单一向量进行分类的class-attention层分开,避免了两种目标不同的处理混合的矛盾现象。

通过实验验证,论文的主要贡献如下:

  • LayerScale能够显着促进了训练收敛并提高了深度更大的ViT的准确性,仅需在训练时向网络添加了数千个参数(对比总参数量可以忽略不计)。
  • 具有class-attention的架构提供了更高效的class embedding的处理。
  • 在Imagenet-Real和Imagenet V2 matched frequency上,CaiT无需额外的训练数据就达到了SOTA性能。在ImageNet1k-val上,CaiT与最先进的模型 (86.5%) 相当,但仅需要更少的 FLOPs (329B vs 377B)和更少的参数(356M vs 438M)。
  • 在迁移学习方面也取得了相当的结果。

Deeper image transformers with LayerScale


作者的目标是在提高Transformer架构的深度同时,提升图像分类训练优化的稳定性。在ViT和DeiT两项工作中,都没有研究仅在Imagenet上训练时,更大的深度可以带来任何好处:更深的ViT架构性能反而更低,而DeiT则只考虑了12层的架构。

图1展示了可能有助于优化的主要变体,图a是标准的预归一化结构。图b则是Fixup、ReZero和SkipInit这类引入可学习标量
\(a_l\)
的结构,该类结构会同时去掉预归一化层和学习率warmup:

ReZero简单地初始化为
\(\alpha = 0\)
,而Fixup则初始化为
\(\alpha = 1\)
并进行其他修改:采用不同的权值的初始化策略,添加了几个偏置权值。但在作者的实验中,即使对超参数进行了调整,这些方法也难以收敛。

经过观察,移除warmup和层归一化是导致Fixup和T-Fixup训练不稳定的原因。因此作者重新引入这两部分,使Fixup和T-Fixup在DeiT模型上收敛,如图1c所示。当深度增加时,以较小的值初始化的可学习标量
\(a_l\)
确实有助于收敛。

  • Our proposal LayerScale

作者提出的LayerScale对输出进行通道级别的乘法,而不是单个标量,如图1d所示,将权值更新与特定输出通道关联。公式上,可认为LayerScale是对每个残差分支输出的对角矩阵乘积:

其中参数
\(\lambda_{l,i}\)

\(\lambda^{'}_{l,i}\)
是可学习权值,初始化为一个固定的小值
\(\varepsilon\)

  • 深度小于18时,设置为
    \(\varepsilon=0.1\)
  • 深度小于24时,设置为
    \(\varepsilon=10^{-5}\)
  • 对于更深的网络,设置为
    \(\varepsilon=10^{-6}\)

该公式类似于其他归一化策略,如ActNorm或LayerNorm,但是在残差分支的输出上执行。此外,实际目的也有很大区别:

  • ActNorm是数据相关的初始化,使输出具有零均值和单位方差,就像batchnorm操作。而LayerScale用较小的值初始化对角线,使其对残差分支的初始影响很小。因此,LayerScale更接近于ReZero、SkipInit、Fixup和T-Fixup等方法:先训练接近恒等函数的网络,然后在训练过程中让网络逐步集成额外参数。
  • LayerScale在优化方面提供了更多的多样性,而不仅仅是通过一个可学习的标量调整,这也是LayerScale优于现有方法的决定性优势。

添加这些参数不会改变架构的特征表达能力,因为也可以集成到SA和FFN层的矩阵参数中,无需更改网络的实现。

Specializing layers for class attention


CaiT架构如图2右,设计核心旨在规避ViT架构要求权值训练同时优化两个相互矛盾的目标的问题。两个矛盾的目标分别是:

  • 学习token之间的自我注意。
  • 总结token间对分类有用的信息。

为此,CaiT的核心就是将上面两个矛盾完全分隔开。

Later class token

作为对比,在网络中间中插入Class token,这样前面的层可以专注于执行自我注意计算。作为不受矛盾目标影响的baseline,作者还考虑了将输出的平均池化用于分类的做法

Architecture

CaiT包含两个不同的处理阶段:

  • self-attention阶段与ViT转换器类似,但没有Class token。
  • class-attentio阶段是一组层,将token集合成class token,随后将输入到线性分类器中。

class-attention阶段依次交替由多头类注意(CA)和FFN组成的层,在这个阶段只有class token会更新。

Multi-heads class attention

CA的作用是从token中提取信息,与SA 类似,但CA只计算class toekn
\(x_{class}\)

\(x_{class}\)
与冻结的token
\(x_{patches}\)
的集合之间的注意力。

定义具有
h
个head和
p
个token的网络,
d
为token维度,将多头类注意力参数化为投影矩阵
\(W_q、W_k、W_v、W_o \in \mathbb{R}^{d\times d}\)
和偏置
\(b_q, b_k, b_v, b_o \in R_d\)
。基于上述定义,CA参数分支的计算可公式化为:

  • 先将输入token扩充为
    \(z=[x_{class}, x_{patches}]\)
    ,执行以下映射:

  • 计算类注意力权重,其中
    \(Q\cdot K^T\in\mathbb{R}^{h\times 1\times p}\)

  • 将注意力用于加权得到残差分支输出:

  • 将输出叠加到
    \(x_{class}\)
    中以进行后续处理。

CA从特征token中提取有用信息整合到class token中。在实验中发现,第一个CA和FFN模块提供了主要的性能提升,叠加第二个模块就足以达到性能提升上限。

Complexity

CA函数在内存和计算方面也比SA更轻量,因为CA只计算class token和token集合之间的注意力:

  • 对于CA,
    \(Q\in\mathbb{R}^d\)

    \(Q\cdot K^T\in\mathbb{R}^{h\times 1\times p}\)
  • 对于SA,
    \(Q\in\mathbb{R}^{p\times d}\)

    \(Q\cdot K^T\in\mathbb{R}^{h\times p\times p}\)

这意味着,与token数量成二次方的计算复杂度在CaiT层中变为线性计算负责度。

Experiments


Preliminary analysis with deeper architectures

直接扩展网络深度,对不同训练超参数进行分析。

对比不同随机深度丢弃比例以及不同归一化策略的性能。

不同层的残差分支的权重可视化,使用LayerScale的权重会比较平稳。

Class-attention layers

对图2的三种架构进行对比分析,最后是对比不同的SA和CA组合比例。

Our CaiT models

不同大小的CaiT模型性能对比以及对应的超参。

Results

与SOTA模型对比,
\(\uparrow\)
代表使用高像素finetune,
\(\gamma\)
代表使用Deit的蒸馏训练。

从ImageNet预训练迁移到其它分类数据集的性能对比。

Ablation

从DeiT过渡到CaiT的性能对比。

Head数量对性能的影响。

数据增强Crop Ratio对性能的影响。

Conclusion


CaiT通过LayerScale层来保证深度ViT训练的稳定性,加上将特征学习和分类信息提取隔离的class-attention层达到了很不错的性能,值得看看。



如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

前段时间写过一篇介绍神经网络的入门文章:
神经网络极简入门

那篇文章介绍了神经网络中的基本概念和原理,并附加了一个示例演示如何实现一个简单的神经网络。

不过,在那篇文章中并没有详细介绍神经网络在训练时,是如何一步步找到每个神经元的最优权重的。
本篇介绍神经网络训练时,常用的一种权重更新的方式--
梯度下降

1. 回顾神经网络

首先,回顾一下神经网络模型主要包含哪些部分:
image.png
如上图所示,核心部分有:

  • 神经元:图中黑色圆圈部分,接受输入,产生输出
  • 层:神经元的集合,图中蓝色,绿色框,一个层一般包含一个或多个神经元

image.png
神经元对输入进行两步计算:

  • 对各个输入按照权重求和
  • 求和的结果再经过一个激活函数,得到一个输出值

神经网络的训练过程,就是给每个神经元找到一个合适的
权重

使得神经网络最后的输出(
\(Output\)
)与目标值相差最小。

神经网络的结构不难,难点在于神经元和层多了之后,计算量暴增,需要强大的硬件支持。

2. 初始权重分配

下面回归本篇的主题,也就是神经网络中权重是如何更新和确定的。

我们知道,神经网络之所以如此流行,是因为基于它的模型,准确度远远好于传统的机器学习模型。
而神经网络模型的好坏取决于每个神经元的权重是否合理。

先假设做一个简单的神经网络,看看神经网络模型如何从
输入值
计算出
输出值
的。
image.png
这个网络中,假设我们的激活函数用
\(y=\frac{1}{(1+e^x)}\)
那么,根据神经元的计算方法,先求和
\(x_1w_{1,1}+x_2w_{2,1}\)

再用激活函数得到:
\(y_1=\frac{1}{1+e^{(x_1w_{1,1}+x_2w_{2,1})}}\)
同理可得:
\(y_2=\frac{1}{1+e^{(x_1w_{1,2}+x_2w_{2,2})}}\)

\(y_2=\frac{1}{1+e^{(x_1w_{1,3}+x_2w_{2,3})}}\)

最终可得输出:
\(z_1=\frac{1}{1+e^{(y_1 w^{'}_{1,1}+y_2 w^{'}_{2,1}+y_3 w^{'}_{3,1})}}\)
从上面的计算过程可以看出,
\(x_1,x_2\)
是输入值,无法改变;
\(y_1,y_2,y_3\)

\(z_1\)
是计算产生的,也无法改变。
在神经网络中,我们能够调整优化的就是权重值
\(w_{1,1},...,w_{2,3}\)
以及
\(w^{'}_{1,1},...,w^{'}_{3,1}\)

理论上,初始化神经网络时,可以设置任意的权重,通过不断的训练最终得到合适的权重。
但实际情况下,模型的训练并不是万能的,初始权重设置的不好,对于训练花费的时间和训练结果都会造成不利的影响。

比如,初始权重
设置的太大
,会导致应用在数据上的激活函数总是处于斜率非常平缓的位置(如下图虚线红框处),
从而降低了神经网络学习到更好权重的能力。。
image.png

此外,还有一个问题是不要设置零值的权重,这也会导致神经网络丧失学习更好权重的能力。

所以,设置初始权重时:

  • 选择随机的,值比较小权重,常见的范围是
    0.01~0.99

    -1.0~1.0
    (不要选择0)
  • 权重的分配最好与实际问题关联,比如实际问题中,知道某些输入值的重要性高,可以初始较大的权重

3. 误差的反向传播

训练神经网络,除了设置初始权重之外,另一个重要的部分就是计算误差。
误差就是根据训练结果与实际结果的差距。
image.png
比如上图,训练结果是
\(o_1\)
,实际结果是
\(t_1\)
,误差就是
\(e=t_1-o_1\)

根据这个误差
\(e\)
,来计算上一层中各个神经元计算后的误差。

误差一般是根据神经元权重所占的比例来分配的。
比如,假设上图的神经网络中,最后一层的初始权重
\(w^{'}_{1,1}=0.58, w^{'}_{2,1}=0.21, w^{'}_{3,1}=0.36\)

最后的误差为
\(e=2\)

image.png

那么,根据
\(w^{'}_{1,1}, w^{'}_{2,1}, w^{'}_{3,1}\)
的权重在这一层所占的比例来更新这一层的误差:
\(e_{y1}=e\times \frac{w^{'}_{1,1}}{w^{'}_{1,1}+w^{'}_{2,1}+w^{'}_{3,1}}=2\times \frac{0.58}{0.58+0.21+0.36}\approx 1.01\)
同理可得:
\(e_{y2}\approx 0.37\)

\(e_{y3}\approx 0.63\)

然后再根据
\(e_{y1},e_{y2}, e_{y3}\)
去更新上一层的误差。
image.png
这样,从后往前,就得到了每个神经元的计算所产生的误差。
因为误差是从后往前计算的,所以也成为误差的
反向传播

4. 优化权重的思路

通过误差的反向传播计算出每个神经元的误差,目的就是基于这个误差来更新神经元的权重值。

  • 当神经元的误差较大时,尝试减小神经元的权重值;
  • 当神经元的误差较小时,尝试增加神经元的权重值。

这也就是
梯度下降
算法的思路。

那么权重每次更新多少合适呢?
每次更新步长太小,将导致计算量过大,经过很长时间的迭代才能找到
最优值
,如下:
image.png
而且,更新步长太小,还会导致找到
次优值
之后,就以为已经找到
最优值
,如下:
image.png

不过,每次权重更新步长过大,也会有问题,有可能会错过最优值,在最优值附近来回横跳,如下:
image.png

所以,计算出误差之后,更新权重不是一次就能完成的。
一般来说,会尝试用多种不同的步长来更新权重,看看哪种步长更新的权重会使得最后的误差最小。

5. 总结

总的来说,神经网络的训练,关键点主要有:

  1. 确定初始权重
  2. 误差反向传播
  3. 尝试不同步长更新权重,尽量找出最优值(也就是使得最终误差最小的权重)

整个训练过程大致如下:
image.png
上图中,结束训练的条件是
误差<阈值
,有的时候,可能会出现很长时间之后误差始终都大于阈值,无法结束训练。
这时,可以加一个条件,
误差<阈值
或者迭代次数到达
1000次
(可以任意次数),就结束训练。

重构服务的一些想法

最近对一个服务进行了大重构(不仅仅是代码的重构,还有构建、部署和单元测试等),之前很多实践的经验都应用上了,实践下来效果比较满意。

模块设计

需要明确服务的核心功能

  1. 执行时机(被谁驱动)
  2. 执行内容
  3. 和非核心功能的关系

从模块话的角度看,这三个部分其实都可以独立实现,这样更利于单元测试用例的编写,扎实的单元测试覆盖率大大提高对稳定性的信心。

执行时机一般都是外部驱动,如收到任务、请求甚至内部定时器驱动。

核心功能的执行内容一般不多,但是实现需要严谨,不要轻易放过错误。因为被非核心功能依赖,这部分的稳定可以减少非核心功能不必要的防御性代码,

和非核心功能的关系参考内核的各种 HOOK 实现,核心功能提供 HOOK 点,非核心功能在这些 HOOK 点上被执行,这样各模块就被解藕了。

构建

尽量选择依赖少的三方库,除非没得选(一般是性能要求)。

构建尽量静态链接,如果存在三方依赖但手动实现简单的话,那么可以考虑手动进行实现。

CI 构建的方式应该和手动构建方式并存(前者调用后者),在规模不是非常大的情况下 CI 速度是没有本地编译的速度快的,这样对开发环境的更新可以效率高一些。

三方工具的构建应该持久化构建过程,比如使用 Dockerfile 来保存构建过程,这样有信创这种需求过来一般稍加修改即可。

C++ 尽量使用 cmake,不用写 makefile及可以生成 compile_commands.json。

代码规范

锁只出现在公有函数中,私有函数只实现功能,不考虑资源同步的情况(如果必须控制小粒度另说)。

业务代码优先定义好接口,由外部逻辑调用接口进行驱动,业务直接实现接口即可,接口定义好坏的一个判断参考:后续增加业务无需修改驱动逻辑,新增接口实现业务逻辑就被集成进去。

代码尽量模块化,模块化清晰一个判断参考:能够直接通过单元测试启动这个模块。

函数实现显式化阻塞逻辑,然后交给外部调用决定是否需要启动线程/协程将其放至后台。

Go 的函数参数只放必要参数,比如创建一个命名空间,那么命名空间名称就是关键的参数,超时时间这种就可以设计成 option 实现。

性能

以 profile 为准,不重写标准库函数,一般性能热点不会出现在这些地方,况且标准库也是会进化的(如早期 nginx 优化的 memcpy 早已比不上现在 glibc 的实现了)

优先实现功能,功能收敛后进行 profile,根据热点进行优化,在实现的时候可以使用一些常见的优化,如 C++ 中的
thread_local
,Go 中的
sync.Pool

单元测试

核心功能尽量 100% 覆盖

非核心功能覆盖到关键路径

日志

日志分文件,不同的模块可以通过不同的日志文件进行区分

日志分等级

  • 在服务内部没有状态改变的情况,默认等级为 INFO 且日志不会增加,定时器的逻辑不能使用 INFO
  • 热点路径的日志只使用 TRACE 等级,并且日志代码语句放在日志等级判断逻辑里面,避免不必要的 CPU/Memory 消耗。

日志可动态调整,应对出现问题又保持现场的场景(生产环境)

部署

支持 systemd/docker 的方式

本文分享自华为云社区
《深度解读昇腾CANN模型下沉技术,提升模型调度性能》
,作者:昇腾CANN。

AI模型的运行通常情况下需要CPU和NPU(昇腾AI处理器)等AI专用处理器协同工作,CPU所在位置称为主机端(Host),而NPU所在位置称为设备端(Device)。对于采用Host调度的AI模型来说,Host下发Task的时序和Device执行Task的时序是异步的,如果Device执行Task的速度比Host下发Task的速度快,则Device会处于空闲状态。比如,大模型场景的增量推理或训练的FineTune阶段,都是计算量较小的场景,此时很容易出现单个算子的Host下发时间比Device上的算子执行时间还长,从而导致Device间歇处于空闲状态。这种现象通常称为Host Bound,这种模型也称为Host Bound模型。

如何减少Host Bound模型的Device空闲时间,从而优化模型执行性能显得尤其重要,GE(Graph Engine)图引擎通过图模式的Host调度和模型下沉调度的方式,可提升模型调度性能,缩短模型E2E执行时间。

1 模型Host调度

Host CPU将模型中的算子依次下发到Device执行(如下图中的标号①所示),每一个算子在执行流上以1个或多个Task的形式存在,昇腾AI处理器依次拉取执行流上的Task执行(如下图中的标号②所示),我们称这个过程为Host调度。Host调度需要Host和Device频繁交互,在实际的训练或推理场景中,模型会运行多次,每次运行都会触发Host把模型上的所有算子遍历下发一遍。

图1 Host调度:

1.png

1.1 Device Bound与Host Bound

如果Device上Task执行速度比Host下发慢,则Device上Task调度开销和算子执行时间成为瓶颈,这种模型通常称为Device Bound模型,如图2所示。Device Bound的模型,Task的下发开销会被已下发Task的执行时间掩盖起来。

图2 Host调度场景Device Bound执行时序分析:

2.png

如果Device上Task执行速度比Host下发快,则Host调度开销成为瓶颈,这种模型通常被称为Host Bound模型,如图3所示。Host Bound的模型,Task的下发开销不能完全被已下发Task的执行时间掩盖,Device执行时序上存在间歇的空闲状态。

图3 Host调度场景Host Bound执行时序分析:

3.png

1.2 单算子模式Host调度与图模式Host调度

在前面几期介绍中,我们知道当前业界主流的深度学习框架提供了Eager执行(Eager Execution,即时执行)模式与图执行模式。Eager模式也叫单算子模式,它是一种Host调度模式,由Host CPU逐个下发算子,一个算子的下发流程包含Python处理、Python到C++数据结构转换、Tiling计算,申请算子的Workspace内存和输出内存,Launch等Host操作。为了加速单算子Host调度,在PyTorch中,昇腾适配层采用了生产者—消费者双线程模式加速,生产者线程主要负责Launch之前的处理动作,消费者线程主要负责Launch算子。

相比于单算子模式,图模式的Host调度可以避免总是返回Python调用栈,避免冗余流程与数据结构转换,并且可以直接使用图编译阶段完成的Infer Shape与Tiling计算结果。因此,图模式Host单线程调度与单算子模式Host双线程调度相比,调度性能持平或略优。

2 模型下沉调度

图模式调度可以降低Host侧的调度耗时,并一定程度减少模型执行E2E耗时,但如何降低Device上执行时序的空闲时间仍是需要考虑的问题。对于静态shape模型,昇腾支持下沉调度,可大幅优化Host调度性能。

所谓静态shape模型,是指模型每次执行的输入tensor shape是固定不变的,模型中的所有算子,给定输入shape后,都可以确定自己的输出shape,那么该模型为静态shape模型。

静态shape模型在编译时即可确定所有算子的输入输出shape,结合昇腾内存复用算法,可完成模型级内存编排;静态shape模型在编译时还可提前完成所有算子的Tiling计算等Host侧计算。综上,静态shape模型可以在编译时完成所有执行时的Host调度工作,这是静态shape模型下沉调度的理论基础。

2.1 下沉调度原理

模型下沉调度分为两个阶段,模型加载和模型执行。

图4 模型下沉示意图:

4.png

  • 模型加载

模型加载的具体动作和Host调度类似,即遍历图中的所有算子并将其整体下发至Device流上,区别在于下发到流上不立即执行。模型加载是一次性的动作,在首次模型执行时完成模型加载,如图4中的过程①所示。

  • 模型执行

模型加载完成之后,可以像下发单算子Task一样,向执行流下发一个模型执行Task,昇腾AI处理器调度到该Task时(如图4 “执行流”中的E),执行模型中所有Task(如图4中的过程③)。如果需要多次运行模型,仅需多次下发模型执行Task,如图4中的过程②所示。

Host Bound调度和模型下沉调度的时序比较如图5所示,可以看出模型下沉执行的开始有一个模型下发的头开销,模型下沉执行E2E会有一个相对于Host调度的收益,模型的下沉头开销越小,收益将越大。

图5 模型下沉调度Host/Device时序分析:

5.png

每次模型下发时,支持更新模型的Feature Map内存地址和输入输出内存地址,如果模型的Feature Map内存和模型输入输出内存发生了更新,则在模型下沉头开销(即上图中的m_l_t,后文有介绍)中会完成模型内算子相关地址的刷新。

2.2 下沉效果

模型下沉执行存在如下优势:

  • 减少CPU的负载

对于模型下沉执行的方式,执行时不需要Host和Device频繁交互,调度的开销仅包含模型下沉的头开销和Task从流上调度到加速器上的开销。总的来说,模型下沉执行的方式,Host CPU的负载降低了,在一定程度上整机、集群的功耗也会降低。

  • 减少Rank之间的通信抖动

对于Host调度执行方式,集群场景下由于不同卡上的Host下发时序不一定完全同步,卡间集合通信可能存在一定程度上的抖动。而下沉执行方式则不存在该问题,因为Task已提前排布到Device上。

  • 提升E2E的收益

由于Task已提前下沉到了Device,对于Host Bound的模型,E2E会有性能提升。以Host Bound的LLaMA-7B模型的推理场景为例,图6是Host调度的Profiling性能数据,图7是模型下沉执行的Profiling性能数据。通过Profiling结果可以看出该模型是一个Host Bound模型,采用Host调度方式,Device上计算时间和空隙的时间比例接近1:1,采用模型下沉执行方式,Device空隙大幅减少,并且端到端有18ms的收益,吞吐量整体提升37%。

图6 LLaMA-7B Decoding Host调度Profiling:

6.png

图7 LLaMA-7B Decoding模型下沉Profiling:

7.png

2.3 下沉头开销

下沉执行头开销包括以下几部分:

1、模型输入输出Tensor到内部InputData/OutputData数据结构转换。如图8中的stage1_t。

2、如果模型的Feature Map内存和模型输入输出内存有变更,刷新相关联算子的相关地址。如图8中的stage2_t。

3、如果模型的输入不支持零拷贝,(如模型的输入为Host内存),则下发异步拷贝Task。如图8中的stage3_t。

4、下发模型执行Task。如图8中的stage4_t。

图8 模型下沉头开销拆分:

8.png

以盘古71B增量推理模型为例(模型输入输出总个数约1600个,模型内节点总数约6300个,模型运行时,Feature Map内存地址不变更,10个输入输出内存地址变更),当前模型下沉的头开销约2ms,关于头开销的性能优化在持续进行中。

3 更多介绍

GE模型下沉技术的相关介绍就到这里,欢迎大家关注后续技术分享。如需获取更多学习资源请登录
昇腾社区

点击关注,第一时间了解华为云新鲜技术~