参考陈云霁等《智能计算系统》。
深度学习处理器设计要点:
- 分析算法的计算特性和访存特性
- 根据算法特性,确定深度学习处理器的微体系结构,包括指令集,流水线,运算部件,访存部件等
计算特征分析:
这里首先介绍下作为分析实例的VGG。VGG的诞生受到“更多层的神经网络是否可以提升图片分类精度”这一疑问的启发。常见的VGG网络结构如下:
ConvNet配置 | |||||
A 11个权重层 | A-LRN 11个权重层 | B 13个权重层 | C 16个权重层 | D 16个权重层 | E 19个权重层 |
输入(224*224大小的RGB图像) | |||||
conv3-64 | conv3-64 LRN | conv3-64 conv3-64 | conv3-64 conv3-64 | conv3-64 conv3-64 | conv3-64 conv3-64 |
最大池化 | |||||
conv3-128 | conv3-128 | conv3-128 conv3-128 | conv3-128 conv3-128 | conv3-128 conv3-128 | conv3-128 conv3-128 |
最大池化 | |||||
conv3-256 conv3-256 | conv3-256 conv3-256 | conv3-256 conv3-256 | conv3-256 conv3-256 conv1-256 | conv3-256 conv3-256 conv3-256 | conv3-256 conv3-256 conv3-256 conv3-256 |
最大池化 | |||||
conv3-512 conv3-512 | conv3-512 conv3-512 | conv3-512 conv3-512 | conv3-512 conv3-512 conv1-512 | conv3-512 conv3-512 conv3-512 | conv3-512 conv3-512 conv3-512 conv3-512 |
最大池化 | |||||
conv3-512 conv3-512 | conv3-512 conv3-512 | conv3-512 conv3-512 | conv3-512 conv3-512 conv1-512 | conv3-512 conv3-512 conv3-512 | conv3-512 conv3-512 conv3-512 conv3-512 |
最大池化 | |||||
全连接层-4096 | |||||
全连接层-4096 | |||||
全连接层-1000 | |||||
softmax |
VGG的特点:
- 由于神经网络层数增加后,会遇到梯度爆炸、梯度消失等问题,VGG基于预训练策略,利用浅层VGG网络训练出来的权重参数初始化深层VGG网络的部分层。
- VGG中的图像基本通过卷积和池化调整,为了避免深层网络的大量卷积池化造成图片边缘特征 弱化,VGG在卷积层做边界扩充以使输出图像和输入如下尺寸相同,再通过池化窗口为2*2,池化步长为2使图像长宽减半;
- 相比于AlexNet使用3*3,5*5,11*11多种卷积核,VGG基本完全使用3*3卷积核,直观上,在浅层部分使用大卷积核可以获得更大的感受野,但在VGG中,连续的2层3*3卷积等同于5*5卷积的感受野,连续5层3*3卷积等同于11*11卷积核的感受野,3*3的卷积核带来同样的感受野,更少的参数;
- VGG在每一层卷积中加入非线性激活函数RELU,因此多层卷积可以增强决策函数的区分能力,提升图片分类的准确率。
以VGG网络的计算特征分析为例,其计算过程分为正向传播和反向传播,正反向计算模式相似,以正向传播为例。
分析计算特征的本质时分析神经网络计算中是否存在固定重复的计算模式,即找到计算模式的最大公约数,并为固定的共有计算模式设计运算指令、运算器。
VGG19网络包含三个典型算子:卷积层——卷积算子、池化层——池化算子、全连接层——全链接算子。
全连接算子
x[i]:第i个输入神经元
y[j]:第j个输出神经元
//全连接算子
//No:输出神经元个数
//Ni:输入神经元个数
y(all)=0;
for (j=0;j<No;j++)
for (i=0;i<Ni;i++)
{
y[j]+=W[j][i]*x[i];
if (i==Ni-1)
y[j]=G(y[j]+b[j]);
}
分析上述代码段,可知全连接层的计算特点:
层 | 计算类型 | 乘加操作个数 | 激活函数操作个数 |
全连接层 | 矩阵乘向量 向量的元素操作 | No*Ni个乘加 | No |
卷积层:
nor,noc,j:第j个输出特征图上(nor,noc)位置
Nif:输出特征图个数
Kc,Kr:卷积核的列,行数
r,c:输入进行卷积的左上起始位置
nor=0;
for (r=0;r<Nir;r+=sr)// sr是垂直方向的卷积步长
{
noc=0;
for (c=0;c<Nic;c+=sc) //sc是水平方向的卷积步长
{
for (j=0;i<Nof;j++)
sum[i]=0;
for (j=0;j<Nof;j++)
for (i=0;i<Nif;i++)
for (kr=0;kr<Kr;kr++)
for (kc=0;kc<Kc;kc++)
sum[j]+=W[kr][kc][j][i]*x[r+kr][c+kc][i];
for (j=0;i<Nof;j++)
Y[nor][noc][j]=G(b[j]+sum[j]);
noc++;
}
nor++;
}
分析上述代码段,可知卷积层的主要计算特点:
层 | 计算类型 | 乘加操作个数 | 激活函数操作个数 |
卷积层 | 矩阵内积 向量的元素操作 | Nif*Nof*Nor*Noc*Kr*Kc个乘加 | Nof*Nor*Noc |
池化层:
最大池化运算:
平均池化运算:
//最大池化运算
nor=0;
for (r=0;r<Nir;r+=sr)
{
noc=0;
for (c=0;c<Nic;c+=sc)
{
for (i=0;i<Nif;i++)
value[i]=0;
for (i=0;i<Nif;i++)
{
for (kr=0;kr<Kr;kr++)
{
for (kc=0;kc<Kr;kc++)
{
//average pooling
value[i]+=X[kr][kc][i]
//max pooling
value[i]=max(X[kr][kc][i], value[i])
}
}
for (i=0;i<Nif;i++)
{
//average pooling
Y[nor][noc][i]=value[i]/Kc/Kr
//max pooling
Y[nor][noc][i]=value[i]
}
}
noc+=1;
}
nor+=1;
}
层 | 计算类型 | 乘加操作个数 | 激活函数操作个数 |
池化层 | 向量的元素操作 | average pooling: Nif*Nor*Noc*Kr*Kc个比较+Nif*Nor*Noc个除法 max pooling: Nif*Nor*Noc*Kr*Kc个加法 | 无 |
综上所述, VGG网络中设计的主要操作均为向量或矩阵(高维向量)的操作,即可向量化操作。事实上,深度神经网络的操作基本上都是向量和矩阵的操作,这些操作占据总计算时间的比例通常在99%以上。更重要的是,这些矩阵和向量操作提供了非常高的数据并行度,而且通常相应的计算只有非常简单的控制。
访存特征分析:
根据深度学习的计算特征为深度学习处理器设计专门的、强大的计算指令和计算模块并不一定会带来计算速度的显著提升,这是由于除了计算能力,访存能力也可能制约处理速度。直观上看,一个芯片里能放到运算器数量和芯片的面积成比例增长,而一个芯片的访存带宽是和芯片周长成比例增长的。他们的增长速度不匹配,势必导致随着计算能力的增强,访存一定会成为新的瓶颈,因此需分析深度学习的访存特征,提升数据访问的局部性、降低访存带宽的需求,匹配深度学习处理器强大的计算能力。
深度学习网络中访存有两个重要的特性:
- 可解耦性:权重、输入神经元、输出神经元的访存行为完全不一致,从而可单独拆分出不同的访存数据流
- 可重用性:神经网络中的权重、输入神经元和输出神经元(中间结果)在计算过程中可以被多次调用
以上提到的VGG网络中的全连接层、卷积层、池化层为例。
全连接层:
结合全连接层代码与全连接层结构,
对于一个Ni输入,No输出的全连接网络,每个输入神经元都参与到No个输出神经元的计算中,因此是可重用的,重用距离为Ni,即上一个输出已经通过遍历Ni个输入神经元计算完,某一输入才会进行下一次重用;可以看出,虽然输入神经元可重用,但在输入神经元数量很大,重用距离会很长,同时片上缓存空间无法放下Ni个输入神经元,因此全连接过程,输入神经元共需No*(Ni+1)(这里我认为使No*Ni)次访存;对于输出神经元,每个输出神经元都在内循环中迭代进行权重与输入神经元的乘加操作,因此是重用距离=1的操作,适合片上缓存;再考虑权重,第i个输入神经元和第j个输出神经元之间都有独立的权重,因此权重不可重用。
若想降低访存带宽,一个常见的技巧是对程序进行变换来改变数据的重用性。循环分块(loop tiling)技术通过对循环进行分块处理,可以在不增加计算量, 不改变程序语义的情况下改变数据的重用距离,其基本思想是将输入神经元或输出神经元划分为小块,每次取部分输入数据计算多个输出神经元的部分值。这就意味着,我们可以更好地利用神经网络当中数据的可重用性,尽可能提高数据的重用次数,降低无效重复的访存次数。
全连接循环分块伪代码:
//全连接算子
//No:输出神经元个数
//Ni:输入神经元个数
y(all)=0;
for (ii=0;ii<Ni;ii=+Ti)
{
for (j=0;j<No;j++)
for (i=ii;i<ii+Ti;i++)
{
y[j]+=W[j][i]*x[i];
if (i==Ni-1)
y[j]=G(y[j]+b[j]);
}
}
采用如上方法,输入神经元的访存距离将为Ti,同时通过将数额u神经元的大小进行分块,使其便于在片上缓存空间缓存,因此所有输入神经元的访存次数降为Ni+1(这里我认为是Ni)。
同理可以衍生出分为多块,输出分块等。。
卷积层
假设一个卷积层有Nif个Nir*Nic大小的特征图,Nof个输出特征图,Nif*Nof个Kr*Kc大小的卷积核。每个输入特征图都会被用于计算Nof个输出特征图,因此至少被重用Nof次,每个输出特征图都迭代乘加输入特征图Nif次,因此至少被重用Nif次,对于指定的输入特征图和输出特征图,卷积核使共享的,因此卷积核也被重用。
卷积层的循环分块代码如下所示,由于一个卷积层有Nif*Nof*Kr*Kc个卷积系数,难以放在第一次片上缓存(L1 cache),可以对输入特征图的通道或输出特征图的通道分块。
for (rr-0;rr<Nir;rr+Tr)//对输入特征图的垂直方向进行分块
{
for (cc=0;cc<Nic;cc+Tc)//对输入特征图的水平方向进行分块
{
for(jjj=0;jjj<Nof;jjj+=Tjj)//对输出特征图的通道进行分块,Tjj为外层循环分块大小
{
nor=0;
for (r=0;r<rr+Tr;r+=sr)// sr是垂直方向的卷积步长
{
noc=0;
for (c=0;c<cc+Tc;c+=sc) //sc是水平方向的卷积步长
{
for (jj=jjj;jj<jjj+Tjj;jj+=Tj)
{
//对输出特征图的通道进行进一步分块,Tj为内层循环分块大小
for (j=jj;j<jj+Tj;j++)
sum[j]=0;
for (j=jj;j<jj+Tj;j++)
for (ii=0;ii<Nif;ii+=Ti)
//对输入特征图的通道进行分块
for (i=ii;i<ii+Ti;i++)
for (kr=0;kr<Kr;kr++)
for (kc=0;kc<Kc;kc++)
sum[j]+=W[kr][kc][j][i]*x[r+kr][c+kc][i];
for (j=0;i<Nof;j++)
Y[nor][noc][j]=G(b[j]+sum[j]);
}
noc++;
}
nor++;
}
}
}
}
池化层:
对于池化层,如果池化窗口大于步长,部分输入可以重用,否则没有重用数据。池化层循环分块伪代码如下:
//最大池化运算
for (rr-0;rr<Nir;rr+Tr)//对输入特征图的垂直方向进行分块
{
for (cc=0;cc<Nic;cc+Tc)//对输入特征图的水平方向进行分块
{
for (iii=0;iii<Ni;iii+=Tii)//对输入特征图通道进行分块,Tii为外层循环分块大小
{
nor=0;
for (r=0;r<Nir;r+=sr)
{
noc=0;
for (c=0;c<Nic;c+=sc)
{
for (ii=iii;ii<ii+Tii;ii+=Ti)
{
for (i=0;i<ii+Ti;i++)
value[i]=0;
for (i=0;i<ii+Ti;i++)
for (kr=0;kr<Kr;kr++)
for (kc=0;kc<Kr;kc++)
{
//average pooling
value[i]+=X[kr][kc][i]
//max pooling
value[i]=max(X[kr][kc][i], value[i])
}
for (i=0;i<Nif;i++)
{
//average pooling
Y[nor][noc][i]=value[i]/Kc/Kr
//max pooling
Y[nor][noc][i]=value[i]
}
}
noc+=1;
}
nor+=1;
}
}
}
}
下表总结了全连接层、卷积层、池化层中数据的可重用情况,另外,如果算法涉及批处理,全连接层的权重还可以在batch内重用,进一步降低访存带宽需求。
层 | 可重用 | 不可重用 |
全连接层 | 输入神经元、输出神经元 | 突触权重 |
卷积层 | 输入神经元、输出神经元、突出权重 | 无 |
池化层 | 当池化窗口大于步长时,部分输入神经元可重用 | 当池化窗口小于步长时,输入神经元、输出神经元不可重用 |