作者:Future_sxy
联系邮箱:[email protected]
单位:武汉大学电子信息学院
CNN网络参数配置,通过AXI4-LITE总线进行读写,内部讲滑动窗模块和CNN计算分离开了,所有目前是两个AXI4-LITE 进行参数配置。
滑动窗寄存器地址配置如下:(待优化,加入padding,目前不支持)
寄存器偏移地址 | 寄存器名称 | 说明 | 备注 |
---|---|---|---|
0x00 | slv_reg0 | Data_horizontal | 输入计算(图片)数据的横向长度 |
0x04 | slv_reg1 | Data_vertical | 输入计算(图片)数据的纵向向长度 |
0x08 | slv_reg2 | Kernel_size | 卷积、池化核的大小(如 5x 5大小,这里就写5) |
0x0c | slv_reg3 | Stride | 卷积、池化核的步进设置 |
0x10 | slv_reg4 | Refresh | 刷新,重置 |
CNN计算寄存器地址配置如下:
寄存器偏移地址 | 寄存器名称 | 说明 | 备注 |
---|---|---|---|
0x00 | slv_reg0 | Conv_Kernel_Size | 初始值为CONV_KERNEL_SIZE,卷积核大小(如5x5,就写5) |
0x04 | slv_reg1 | Conv_Kernel_Num | 初始值为CONV_KERNEL_NUM,卷积核个数 |
0x08 | slv_reg2 | Pool_Kernel_Size | 初始值为POOL_KERNEL_SIZE,池化核大小(如2x2,就写2) |
0x0c | slv_reg3 | Pool_Kernel_Num | 初始值为POOL_KERNEL_NUM,卷积核个数,目前只支持1 |
0x10 | slv_reg4 | FULLCON_Input_Size | 初始值为FULLCON_INPUT_SIZE,输入层的数据个数(128x1:就写128)flatten后 |
0x104 | slv_reg5 | FULLCON_Output_Size | 初始值为FULLCON_OUTPUT_SIZE,输出数据个数 |
0x18 | slv_reg6 | cnn_start | 初始值为0,当写该位从0变为1时,自动启动cnn计算 |
0x1c | slv_reg7 | select_mode | 选择计算模式: IDLE =5'b00000; 静默模式 CONV =5'b00001; 卷积计算 RELU =5'b00010; 激活计算 POOL_Max =5'b00100; 最大池化 POOL_Mean =5'b00101; 平均池化 FULLCON =5'b01000; 全连接计算 CONV_RELU =5'b10000; 卷积+RELU计算 |
0x20 | slv_reg8 | scaler | 初始值为SCALE_RIGHT |
0x24 | slv_reg9 | scalel | 初始值为SCALE_LEFT |
加速器的数据传输都是通过DMA+AXIS-Data -FIFO的方式进行传输,加速器内部的数据借接口主要是AXI-Stream
接口定义 | 接口位宽 | 说明 |
---|---|---|
axis-clk | 1 | 时钟(50MHz) |
axis-resetn | 1 | 复位,低有效 |
axis-data | 8 or 64 | 数据通讯(8bit的传输单个计算数据,64位宽的一次传输8个权重数据) |
axis_valid | 1 | 主机发送有效 |
axis-ready | 1 | 从机接收有效 |
axis-last | 1 | 最后一个数据时拉拉高 |
CNN加速器内部主要是滑动窗数据转换,和CNN控制计算两个模块,其他部分都是数据缓存。其中未来使用中断来捕获计算速度,因此通过cnn_done信号接入中断引脚,在start开始时开始计时,接收到cnn_done后停止计时。
本项目设计的卷积神经网络加速器的整体架构如下所示,内部包括滑动窗模块、卷积计算模块、池化计算模块、ReLU 激活函数计算模块和全连接计算模块。
简单的来说,整个过程就是选择计算模式,投喂数据,开始计算,获取数据。在数据存储方面,包含卷积输入数据 Buffer,卷积权重 Buffer,全连接输入数据 Buffer、全连接权重 Buffer。其中池化计算模块数据缓存和卷积输入数据 Buffer 共用,ReLU 激活函数计算模块输入可选卷积输出数据通路或以卷积权重缓存 Buffer 导入外部新输入数据。
本文的 CNN 加速器架构的核心为脉动阵列(Systolic Array)。脉动阵列是一个流水线型二维的处理单元(Processing Element, PE),其有着高效的数据移动及高效流水计算,适合于加速 GEMM,并且在工业界得到广泛运用。脉动阵列是由多个 PE 处理单元之间相互连接组成的二维计算阵列,下图所示是一种脉动阵列加速矩阵乘法的实现,其内部 PE 处理单元采用定制的计算结构,数据矩阵 b首先载入阵列,通过 PE 处理单元之间的流动完成各个模块数据的更新存储,存储完毕后,传入数据矩阵 A,与数据 B 进行定制的计算,将计算结果继续传给相邻模块。最终在脉动阵列的最后输出数据矩阵 A 和数据矩阵 B 的计算结果 C。脉动阵列流水线式的计算架构能够提高数据计算的吞吐率和效率,从而实现加速计算。
通过对卷积操作分析,可以将卷积计算分为两个步骤,首先是数据与卷积核的卷积计算转换为矩阵计算,这一步也就是 im2col的过程,而根据卷积的计算过程,可以看作卷积核窗口在原始数据上的不断移动,可以通过用硬件实现数据缓存更新,模仿滑动窗的操作,提取每一次卷积计算的数据,进而完成第一步的转换。第二个步骤是将转换完成的数据进行矩阵乘法操作,对于矩阵乘法可以使用脉动阵列的方式完成乘累加操作,从而在硬件上完成卷积操作。
下图是加速器中卷积计算模块的整体架构, 其包括两部分:滑动窗模块和矩阵乘法计算模块。滑动窗模块接收传入的原始卷积计算数据,通过设定的卷积计算参数,完成滑动取出一个窗口的数据,根据滑动的计数输出数据是否有效,直至完成整个输入数据的滑动。矩阵乘法模块,接收滑动窗数据转换模块的输出数据,通过流水线整合,传入脉动阵列,完成计算。
注意这里的阵列中一列就对应一个卷积核,四列的话,可以对同一个数据进行四个卷积计算,多通道合并为大位宽输出。
具体而言,在滑动窗模块的核心部分为内部缓存 Buffer 和行列有效计数,实现输出滑动窗覆盖的数据及其是否有效标志。图 3.5为该模块的状态机转换,包含 Idle、Start、Horizontal、Vertical、Done 四个状态。当该模块检测到输入数据输入有效时,Start 信号拉高,状态进入 Start 状态。此时内部大小为 𝐷𝐴𝑇 𝐴_𝐻𝑂𝑅𝐼𝑍𝑂𝑁𝑇 𝐴𝐿 ∗ 𝐾𝐸𝑅𝑁𝐸𝐿_𝑆𝐼𝑍𝐸 的缓存 Buffer 不断存入输入的原始数据,并进行前后传递覆盖,输入的数据会类似 FIFO 机制存入 Buffer缓存,但是缓存的长度仅为预设大小,随着前后传递数据,最先输入的数据在超过 Buffer大小后会进行丢弃,替换为新的输入数据。
当检测到缓存 Buffer 第一次装满数据时,状态从 Start 转到 Horizontal 状态, 进行数据输出,并根据设置的步进,进行间隔输出有效信号,内部每一个周期内会进行一次数据的传递,所以当 𝑆𝑡𝑟𝑖𝑑𝑒 = 2 时,有效信号高电平每间隔一个时钟周期输出一次。在忽略填充时,当输入图片数据的横向长度 Data_Horizontal、竖向长度 Data_Vertical、卷积核尺寸Kernel_Size 以及滑动窗步进 Stride 确定的情况下,可以确定每一行输出的有效滑窗个数Valid_Num.默认不考虑填充,具体公式如下:
𝑉𝑎𝑙𝑖𝑑_𝑁𝑢𝑚 = ⌊(𝐷𝑎𝑡𝑎_𝐻𝑜𝑟𝑖𝑧𝑜𝑛𝑡𝑎𝑙 − 𝐾𝑒𝑟𝑛𝑒𝑙_𝑆𝑖𝑧𝑒)/𝑆𝑡𝑟𝑖𝑑𝑒⌋ + 1 (3.1)
在横向移动时,输出有效窗口数据个数达到 Valid_Num 值后,此状态下有效值便会一直保持低电平。当 Stride 大于 1 并且横向一行数据完成移动时,Hor2Ver_Flag 拉高,状态跳转到竖直移动 Vertical 状态,等待完成移动 Stride 行输入数据,即等待对应时钟周期后Vertical_Full_Flag 拉高,状态跳转回横向移动 Horizontal 状态。在两个状态下,根据输入数据计数器数值来判断是否完成所有输入数据的转换,其中在Vertical 状态下完成所有输入数据转换的判断依据为计数器数值达到阈值Data_Whole_Size,其阈值公式为:
𝐷𝑎𝑡𝑎_𝑊ℎ𝑜𝑙𝑒_𝑆𝑖𝑧𝑒 = 𝐷𝑎𝑡𝑎_𝐻𝑜𝑟𝑖𝑧𝑜𝑛𝑡𝑎𝑙 ∗ 𝐷𝑎𝑡𝑎_𝑉𝑟𝑡𝑖𝑐𝑎𝑙 (3.2)
在 Horizontal 状态下完成所有输入数据的转换的判断依据为计数器数值达到阈值Data_Complete_Size,其阈值公式为:
𝐷𝑎𝑡𝑎_𝐶𝑜𝑚𝑝𝑙𝑒𝑡𝑒_𝑆𝑖𝑧𝑒 = 𝐷𝑎𝑡𝑎_𝑊ℎ𝑜𝑙𝑒_𝑆𝑖𝑧𝑒 + 𝐷𝑎𝑡𝑎_𝐻𝑜𝑟𝑖𝑧𝑜𝑛𝑡𝑎𝑙 − 𝐾𝑒𝑟𝑛𝑒𝑙_𝑆𝑖𝑧𝑒 (3.3)
当满足上述条件之一时,当前的状态会转换到 Done 状态,等待下一个周期返回到默认初始 Idle 状态。
值得一提的是,为了提高整体的适用性和配置性,该模块同时设计了小于硬件预留资源的向下兼容,即系统提前在硬件资源设置最大可使用范围,如配置DATA_HORIZONTAL 为28,KERNEL_SIZE 为 5,那么对于不超过该范围的卷积滑窗操作,可以不断变化输入数据尺寸 Data_Horizontal 和卷积核尺寸 Kernel_Size 均可适用,提升了整体模块的灵活性。
(全大写的变量为硬件预先资源设置大小数据,如 DATA_HORIZONTAL,KERNEL_SIZE; 仅首字母大写的变量为动态可变化的参数,如 Data_Horizontal,Kernel_Size)
权重恒定型 PE 单元主要结构,权重早于输入数据流入 PE 处理单元,输入权重类似流动的方式传入每个 PE,在权重加载完毕后,每一个 PE 模块中便保存了一个权重参数,之后输入数据依次流入 PE 处理单元,每个单元不仅向周围的单元传递输入数据还要输出上一次的输入与权重的乘积与前一个单元输出数据的部分和,这样在整个脉动阵列中乘积和不断从输入传递到输出,完成整个计算操作。阵列的 PE 数量可以根据网络结构进行修改并在最大硬件预设资源内对计算进行向下兼容。
在上文提到的卷积脉动阵列架构图中,输入激励和输出结果部分都包含了一个流水线缓存模块,因为脉动阵列的原理使得数据的流入和结果的数据都是流水线方式的,因此需要对传入的同一组数据进行流水线整合,将同组数据分不同周期传入脉动阵列,如图 3.11所示,一个四通道输入的阵列结构,需要进行计算的四个数据,分别需要在第一个周期、第二个周期、第三个周期、第四个周期输入,因此在不同输入通道内加入不同长度的缓存 Buffer,以实现上述功能。在输出端同样,不同竖直方向的计算结果也是在不同周期输出脉动阵列的,因此采样相同的设计,将输出的数据,进行整合,从而不同周期输出阵列的数据,因为缓存的加入,可以实现同周期输出计算结果。
池化操作也可以分为两个步骤,池化操作数据提取和池化数据计算。类似卷积操作中的对输入数据的部分提取和矩阵乘法计算处理的流程。池化模块的整体架构如图,整体包含滑动窗模块和池化计算模块。
因为在卷积模块设计的时候,考虑到了通用性和向下兼容,所以对于池化模块的滑动窗设计,仅需在原先滑动窗基础上设置滑动步进等于池化核尺寸即可。不必重新设计。
在池化计算中最常见的操作为最大池化核和平均池化,因此在本模块中对池化 PE 处理单元内部进行了两种计算的设计,算法 3为池化模块的伪代码设计,接口包括两个输入数据,一个输出数据以及最大池化平均池化的操作选择。内部定义了寄存器完成流水型数据传递和保存,输出数据通过选择信号选择输出数据的较大者还是两者只和,最终将结果传递到下一个 PE 单元。
Data: Data_Input1, Data_input2,Max_Mean_Select
Result: OutData
initialization;
Data1_Reg = Data_input1;
Data2_Reg = Data_input2;
Mean_Reg = Data1_Reg + Data2_Reg ;
Max_Reg = Data1_Reg > Data2_Reg ? Data1_Reg:Data2_Reg;
OutData = Max_Mean_Select ? Max_Reg : Mean_Reg ;
池化模块的 PE 处理单元结构,同上面伪代码所示,输入接口包括两个输入数据 Data_Input1,Data_input2,计算模式选择 Max_Mean_Select。输出经过 PE 计算单元处理后的数据。其中在选择最大池化模块计算时,通过选择器,对输入数据 1 和输入数据 2 进行大小比较,选择其中的较大者作为输出。对于平均池化模块,数据 1 和数据 2 会先进行加法计算,在最后的一个 PE 模块输出端口再进行平均化操作,输出最终的数值。值得注意的是,在硬件设计中数据计算均为二进制,因此有效位为整数位,例如 𝐼𝑛𝑝𝑢𝑡_𝐷𝑎𝑡𝑎1 =2,𝐼𝑛𝑝𝑢𝑡_𝐷𝑎𝑡𝑎2 = 3, 此时经过加法操作后平均输出的结果为 2,即小数位全部丢弃并向下取整数。
对于常见的激活函数中存在大量的复杂指数计算,对于硬件加速设计需要消耗大量的计算资源,因此在本项目设计时选取相对常用且易实现的 ReLU 函数进行硬件映射。
考虑到激活函数层通常紧跟卷积层设计,因此 ReLU 模块的阵列也是一维的,ReLU 激活函数模块的整体架构,核心为 ReLU 激活函数 PE 计算单元. 其输入数据尺寸同卷积脉动阵列模块的输出尺寸,因此继承了卷积模块的向下兼容性。同时卷积模块的输出数据是由多通道低位宽数据整合而成的单个高位宽数据,因此在 ReLU 控制模块中主要实现多通道数据的分离,以及数据的输出和整体逻辑控制。
ReLU 模块的代码设计如算法 4所示,输入包括输入数据和两个缩放因子,通过输入数据的正负选择对应的输出,以及完成量化中的缩放设置。其中不同于卷积和池化模块的PE 结构,ReLU 模块的 PE 处理单元为纯逻辑组成,根据输入数据实时做出计算。PE 处理单元,可以看到其主要结构为选择器,根据输入数据的符号位判断输入数据的正负,从而选择输出原数据或是输出为 0。同时考虑到网络模型的量化,在此 PE 单元中预设了缩放因子,为后续量化缩放权重等预留空间。
Data: Data,Scaler,Scalel
Result: OutData
initialization;
OutData = Data>0 ? (Data«Scalel)»Scaler : 0 ;
激活函数模块的设计为在模块调用时可以选择直接与卷积模块联合使用,减少数据的搬移,直接进行 ReLU 操作,提高加速器的性能。
全连接层的计算与卷积计算有很大的相似之处。但是全连接层相较于卷积层不需要滑窗的过程,相反,全连接的输入需要对前一层网络的输出数据进行展开。全连接计算模块整体也分为两部分:全连接控制模块和由输出恒定型 PE 处理单元组成的脉动阵列。
由于全连接模块的输入层展开为一维,如输入数据尺寸为 1×FC_Horizontal,权重数据尺寸为 FC_Horizontal×Output_Size。那么全连接计算脉动阵列的尺寸便为 1×Output_Size,即横向尺寸为 1,纵向尺寸等于输出尺寸。同时在该部分为了达到向下兼容性和灵活性,对于更大尺寸全连接计算的输出,可以在软件端进行拆分,拆分成多个小尺寸的计算传入加速器,从而有效提高可加速计算的范畴。
在输出不变数据流模式中,全连接模块 PE 处理单元的伪代码如算法 5所示,通过输入权重和激励数据,不断在内部的寄存器中完成乘法的累加,直到输出有效时完成数据的计算。
Data: Data, Weight,InValid
Result: OutData, OutWPS,OutValid
initialization;
Data_Reg = Data;
Weight_Reg =Weight ;
Valid_Reg = InValid;
Sum_reg = InValid ? Data_Reg * Weight_Reg +Sum_reg: Sum_reg;
OutWPS = InValid ? Weight_Reg:Sum_reg;
OutData=Data_Reg;
OutValid= Valid_Reg;
全连接模块 PE 单元的结构图,通常全连接层的输出结果尺寸小于输入数据尺寸,但是其权重数据参数通常较大,因此在全连接计算 PE 处理单元结构不同于卷积 PE处理单元的权重恒定型模式,而是采用输出恒定型的 PE 处理单元模式。输入数据和输入的权重在 PE 处理单元之间不断流动,等待所有输入数据和权重流动完成后,每个 PE 处理单元缓存的乘累积部分和输出到阵列输出接口,完成计算。
使用PYNQ-Z2开发板进行部署测试,SOC为zynq硬核。
使用PYNQ的jupyter进行开发,相关代码和bit,hwh文件在software_project文件内,镜像使用的是pynq-z2-3.0版本image也在文件内。
bitsteam文件中:
1.Accelerator_Alexnet.bit针对AlexNet网络进行的硬件配置,AlexNet网络没有进行具体的应用测试,权重和数据都是随意设置的数据,存在AlexNet_16文件夹中,可以自己修改;
2.Accelerator.bit是一般的网络层设计,可以允许单个的卷积等计算测试。
3.Accelerator_number.bit是针对Lenet最原始的网络进行的硬件配置,也跑通过了手写数字识别测试。
number文件中是28*28大小的部分minist数据集照片,资源链接:https://pan.baidu.com/s/1gRTuQb3F9rKZ-eCYE5S6qA?pwd=tdc5 提取码:tdc5;
Lenet_new_weight是转换后的卷积权重,全连接权重数据。
number_txt用来存储量化后的输入数字图片(-127,127)代码中有写。(相关Lenet数字识别的权重和量化数据来自B站UP主“雪天鱼“”的开源,在此感谢。)
有问题欢迎写邮件联系作者,共同交流探讨。