文章目录

  • 矩阵乘法的优化
    • 矩阵的reshape
    • 缓存的加入
    • PIPELINE REWIND
  • 卷积神经网络加速
    • 分块划分
    • 访问和内存的流水化
  • 卷积层量化和稀疏
    • 模型量化
      • 带宽
    • 稀疏化
    • 存储
  • 脉动阵列
    • 实际操作
      • producer和consumer
    • 完整的脉动阵列
    • 卷积到脉动阵列的映射

矩阵乘法的优化

原程序

#include "matrixmul.h"
void matrixmul(mat_a_t a[MAT_A_ROWS][MAT_A_COLS],mat_b_t b[MAT_B_ROWS][MAT_B_COLS],result_t res[MAT_A_ROWS][MAT_B_COLS])
{// Iterate over the rows of the A matrixRow: for(int i = 0; i < MAT_A_ROWS; i++) {// Iterate over the columns of the B matrixCol: for(int j = 0; j < MAT_B_COLS; j++) {res[i][j] = 0;// Do the inner product of a row of A and col of BProduct: for(int k = 0; k < MAT_B_ROWS; k++) {res[i][j] += a[i][k] * b[k][j];}}}}

课上提供的修改程序

#include "matrixmul.h"
void matrixmul(mat_a_t a[MAT_A_ROWS][MAT_A_COLS],mat_b_t b[MAT_B_ROWS][MAT_B_COLS],result_t res[MAT_A_ROWS][MAT_B_COLS])
{#pragma HLS ARRAY_RESHAPE variable = a complete dim = 1
#pragma HLS ARRAY_RESHAPE variable = b complete dim = 2
#pragma HLS INTERFACE ap_fifo port = a
#pragma HLS INTERFACE ap_fifo port = b
#pragma HLS INTERFACE ap_fifo port = resmat_a_t a_row[MAT_A_ROWS];mat_b_t b_copy[MAT_B_ROWS][MAT_B_COLS];int tmp = 0;// Iterate over the rows of the A matrixRow: for(int i = 0; i < MAT_A_ROWS; i++) {// Iterate over the columns of the B matrixCol: for(int j = 0; j < MAT_B_COLS; j++) {#pragma HLS PIPELINE rewindtmp=0;// Cache each row (so it's only read once per function)if(j==0)Cache_Row:for (int k =0;k<MAT_A_ROWS;k++)a_row[k]=a[i][k];// Cache all cols (so they are only read once per function)if(i==0)Cache_Col:for (int k =0;k<MAT_B_ROWS;k++)b_copy[k][j]=b[k][j];// Do the inner product of a row of A and col of BProduct: for(int k = 0; k < MAT_B_ROWS; k++) {tmp += a_row[k] * b_copy[k][j];}res[i][j] = tmp;}}
}

矩阵的reshape

矩阵乘法应该是A的每一行的元素乘上B的每一列的对应元素的叠加为结果矩阵的一个元素。
所以程序的运算逻辑最内层的循环是进行A的第i行元素与b的第j列元素的乘积累加。
其外层是对矩阵B的列遍历,可以计算出目标矩阵第i行的所有元素。
最外层的循环则是对行的遍历。

因此最内层每次取的是A的行,B的列,应当分别按不同的维度拆分。
以便于拆分后可以同时读取这些数据,进行流水操作。

对应

#pragma HLS ARRAY_RESHAPE variable = a complete dim = 1
#pragma HLS ARRAY_RESHAPE variable = b complete dim = 2

缓存的加入

可以看到在修改后的程序中,在最内层的乘积累加过程进行之前,先进行了行缓存和列缓存的生成,即为a_row和b_row,然后接下来的运算过程直接从缓存中取数
这是因为在运算之时,如果直接从外部取数的话会比较慢,通过缓存将其置于该模块的BRAM上就可以实现高速的读取运算。

一个疑问: 为什么b_row不直接用b_row[k]=b[k][j];而是要把b_row设置为二维数组,第二维度没用到啊,实际上只是使用了一个b的列向量。

将rewind部分注释掉,仅观察添加缓存后的性能对比,有

PIPELINE REWIND

(By 高亚军老师的HLS教学视频)
一个示例:

图中结构完成的任务为

Loop:
for (i=0;i<3;i++){op_Read; //读取op_Computer;  //计算op_Write; // 写入
}

对于第一个图而言,完成第一个Loop的流水线操作后,中间有一个其他操作将两次循环分隔开了,因此其是部分流水线而不是完全流水线的。

在rewind之后成为第二个图,可见next loop的执行被放在了第一个loop的read2之后,也就是说,后续循环的计算操作也并入了同一个流水,是完全流水线的。

个人理解: rewind的作用就是在循环内流水的基础上让循环间也流水。

在加入流水线和缓存后其综合对比如下

卷积神经网络加速

分块划分


对于卷积运算而言

Out[cho][r][c]=∑chi=0CHin−1∑kr=0K−1∑kc=0K−1W[cho][chi][kr][kc]×In[chi][S×r+kr][S×c+kc]Out[cho][r][c] = \sum\limits_{chi=0}^{CHin-1} \sum\limits_{kr=0}^{K-1} \sum\limits_{kc=0}^{K-1}W[cho][chi][kr][kc]\times In[chi][S\times r + kr][S\times c + kc]Out[cho][r][c]=chi=0∑CHin−1​kr=0∑K−1​kc=0∑K−1​W[cho][chi][kr][kc]×In[chi][S×r+kr][S×c+kc]

在处理大规模数据的时候,可以考虑通过对CHin和CHout进行循环分块来进行加速。
(否则急速增长的硬件资源消耗可能突破FPGA的限制)

如,原先设计好了加速器尺寸为

CHin CHout R C Rin Cin K
4 4 14 14 16 16 3

现有一组更大的数据

CHin CHout R C Rin Cin K
8 8 14 14 16 16 3

可以将其分块为4x4,然后调用原加速器4次。
并利用片下DRAM存储卷积层运算数据

片上资源仅能容纳一个输入数据块,一个输出数据块,一个权重数据块。每次切换数据块的时候都要和DRAM进行数据交互。

所以首先定义Input,Width和Output的指针,并定义了m_axi类型的接口

void test(float* In_ddr,float* W_ddr,float* Out_ddr)
{#pragma HLS INTERFACE m_axi depth = 32 port = In_ddr
#pragma HLS INTERFACE m_axi depth = 32 port = W_ddr
#pragma HLS INTERFACE m_axi depth = 32 port = Out_ddr

接下来如同其他优化一样进行数组展开

 float In[4][Rin][Cin];
#pragma HLS array_partition variable = In complete dim = 1float Out[4][R][C];
#pragma HLS array_partition variable = Out complete dim = 1float W[4][4][K][K];
#pragma HLS array_partition variable = W complete dim = 1
#pragma HLS array_partition variable = W complete dim = 2

可以通过对指针的读写操作来完成对片下DRAM的读写操作。

对块进行操作,最外层循环每次自增一块的大小

Output_Channel_Tiling:
for(int cho = 0; cho<CHout;cho+=4)
{Input_Channel_Tiling:for(int chi=0; chi<CHin; chi++){//各种代码}
}

进行从DRAM读取Input数据块,有

//load In tile from DRAMfor ( int L_ri = 0;L_ri < Rin; L_ri++){#pragma HLS PIPELINEfor (int L_ci=0;L_ci<Cin;L_ci++){for(int L_chi = 0;L_chi<4;L_chi++){In[L_chi][L_ri][L_ci] = *In_ddr++;}}}

然后读取权重数据块

// load W tile from DRAMfor (int L_cho = 0;L_cho < 4;L_cho++){#pragma HLS PIPELINEfor (int L_chi = 0;L_chi < 4; L_chi ++){for (int L_kr = 0;L_kr<K;L_kr++){for(int L_kc =0; L_kc<K;L_kc++){W[L_cho][L_chi][L_kr][L_kc] = *W_ddr++;}}}}

内部的数据处理仍然用之前并行度为4x4的加速器进行计算。

Kernel_Row;for(int kr=0; kr<K; kr++){Kernel_Column:for(int kc=0; kc<C; kc++){Row:for(int r=0; r<K; r++){Column:for(int c=0; c<K; c++){Output_Channel_HW:for(int cho_hw = 0;cho_hw<4;cho_hw++){Input_Channel_HW:for(int chi_hw = 0; chi_hw<4;chi_hw++){Out[cho_hw][r][c] += In[chi_hw][r+kr][c+kc] * W[cho_hw][chi_hw][kr][kc];}}}}}}

最后要将数据写到DRAM中

     //offload Out tile to DRAMfor(int L_ro = 0;L_ro<R;L_ro++){#pragma HLS PIPELINEfor (int L_co = 0;L_co < 4;L_co++){for (int L_cho = 0;L_cho<4;L_cho++){*Out_ddr++ = Out[L_cho][L_ro][L_cho];}}}

这样就可以实现对不同规模的卷积层的加速。

访问和内存的流水化

刚才那个图是线性的,在进行读取的时候卷积模块歇了,如果能改成流水结构则能快很多
常见的方法是double buffer
将buffer设为同样大小的两组,buffer0和buffer1

在处理一个buffer的数据进行卷积运算的时候,另一个buffer可以进行数据搬运。

我们将各个功能都用函数封装起来

void Load_In(float* In_ddr, float In[4][Rin][Cin])
{//稀里哗啦
}
void Load_W(float* W_ddr, float W[4][4][K][K])
{//劈里啪啦
}void Convolution(float In[4][Rin][Cin], float W[4][4][K][K], float Out[4][R][C])
{//乒乒乓乓
}void Offload_Out(float* Out_ddr, float Out[4][R][C])
{//叮叮当当
}

然后用process函数来对这些进行调用

void Process(float* In_ddr, float* W_ddr, float In_0[4][Rin][Cin], float W_0[4][4][K][K],float In_1[4][Rin][Cin], float W_1[4][4][K][K] ,float Out[4][R][C],int flag)
{//#pragma HLS DATAFLOWif (flag == 0){ //Load In tile from DRAMLoad_In(In_ddr, In_0);//Load W tile from DRAMLoad_W(W_ddr, W_0);Convolution(In_0, W_0, Out);}else{  //Load In tile from DRAMLoad_In(In_ddr, In_1);//Load W tile from DRAMLoad_W(W_ddr, W_1);Convolution(In_1, W_1, Out);}return;
}

通过不断切换flag就可以切换输入的buffer了。

在实现的函数中,给每个数组都加了一个备份,用01来划分,在使用process时候不断切换flag来切换buffer。

void test(float* In_ddr, float* W_ddr, float* Out_ddr)
{#pragma HLS INTERFACE m_axi depth=32 port=In_ddr
#pragma HLS INTERFACE m_axi depth=32 port=W_ddr
#pragma HLS INTERFACE m_axi depth=32 port=Out_ddrstatic float In[4][Rin][Cin];
#pragma HLS array_partition variable=In complete dim=1static float Out[4][R][C];
#pragma HLS array_partition variable=Out complete dim=1static float W[4][4][K][K];
#pragma HLS array_partition variable=W complete dim=1
#pragma HLS array_partition variable=W complete dim=2int flag = 0;Output_Channel_Tiling:for(int cho=0; cho<CHout; cho+=4){Input_Channel_Tiling:for(int chi=0; chi<CHin; chi+=4){Process(In_ddr, W_ddr, In_0, W_0,In_1,W_1,Out,flag);flag = 1-flag;}//Offload Out tile to DRAMOffload_Out(Out_ddr, Out);         }return;
}

除此之外还可以通过dataflow来实现double buffer,这次我们在test中不必显式的声明两份数组了,在process函数中只需要定义DATAFLOW,然后直接调用即可。

void Process(float* In_ddr, float* W_ddr, float In[4][Rin][Cin], float W[4][4][K][K], float Out[4][R][C])
{#pragma HLS DATAFLOW//Load In tile from DRAMLoad_In(In_ddr, In);//Load W tile from DRAMLoad_W(W_ddr, W);Convolution(In, W, Out);return;
}

卷积层量化和稀疏

模型量化

就是用资源开销少,效率高的定点数取代浮点数

神经网络模型对精度有鲁棒性√(就是误差传递小吧)

带宽

以结构体封装数据

typedef struct{unsigned char R,G,B;
}RGB_pixel;void top(RGB_pixel* Pixel,...){#pragma HLS data_pack variable=Pixel;
}typedef ap_uint<24>RGB_pixel;void top(RGB_pixel* Pixel,...)
{...RGB_pixel tmp_pixel = Pixel[0];unsigned char R = (unsigned char)(tmp_pixel(7,0));unsigned char G = (unsigned char)(tmp_pixel(15,8));unsigned char B = (unsigned char)(tmp_pixel(23,16));
...
}

unsigned char 是8位的,所以RGB三个连起来的RGB_pixel类型是24位,其中分别表示RGB。

稀疏化

  1. 设计合适的判断逻辑,剔除对0的计算,但是要耗费额外性能进行判断,所以需要好好考虑
  2. 将卷积计算转化为矩阵乘法计算,其本质就是稀疏矩阵乘法计算

存储

CSR压缩方式
可以参考之前写过的稀疏矩阵向量乘法

脉动阵列


以3x3的矩阵乘法为例
用9个PE(Processing Elements)进行并行计算

利用array partation将A按照行维度存在三个buffer,B按照列维度存在三个buffer(数据广播 Data Broadcast)

有很多的扇入扇出(fan-in/fan-out),总之就是很麻烦,频率低

脉动阵列则是如下图所示。


可以形成沿对角线的流水线运算。

实际操作

由于dataflow是函数级流水,因此要把各个功能的函数封装。

设计PE和四个方向都有链接,从上方和左方读数,向右方和下方输出。

void PE(hls::stream<float> &A_pre, hls::stream<float> &B_pre, hls::stream<float> &A_nxt, hls::stream<float> &B_nxt, float C)
{float A_tmp, B_tmp;K_Loop:for(int k=0; k<K; k++){#pragma HLS PIPELINE//Shift-in A & BA_pre >> A_tmp;B_pre >> B_tmp;//MACC += A_tmp * B_tmp;//Shift-out A & BA_nxt << A_tmp;B_nxt << B_tmp;}return;
}

从左侧和上侧读取,然后进行乘累加计算,再向右侧和下侧写入。

我们再添加上加载数据的buffer,就可以形成之前示意图的结构

//buffer代码
void Load_A(int m, float A[M][K], hls::stream<float> &A_nxt)
{for(int k=0; k<K; k++){A_nxt << A[m][k];}return;
}void Load_B(int n, float B[K][N], hls::stream<float> &B_nxt)
{for(int k=0; k<K; k++){B_nxt << B[k][n];}return;
}

producer和consumer

hls中,stream都必须要有producer和consumer,但是最右边和最下边都缺少consumer

所以添加两个方法对其进行消耗

void Drain_A(hls::stream<float> &A_pre)
{for(int k=0; k<K; k++){float drain;A_pre >> drain;}return;
}void Drain_B(hls::stream<float> &B_pre)
{for(int k=0; k<K; k++){float drain;B_pre >> drain;}return;
}

完整的脉动阵列

完整的脉动阵列为

使用PE_array描述脉动阵列的整体结构,其内容写在注释中

void PE_array(float A[M][K], float B[K][N], float C[M][N])
{hls::stream<float> A_inter[M][N+1];
#pragma HLS STREAM variable=A_interhls::stream<float> B_inter[M+1][N];
#pragma HLS STREAM variable=B_inter
/**************************************************/
/*
使用横向的Ainter和纵向的Binter来指定A和B中的元素
使用HLS STREAM进行定义
*/
/**********************************************/
#pragma HLS DATAFLOWfor(int m=0; m<M; m++){#pragma HLS UNROLLLoad_A(m, A, A_inter[m][0]);/A_inter[m][0]是stream接口}for(int n=0; n<N; n++){#pragma HLS UNROLLLoad_B(n, B, B_inter[0][n]);/B_inter[0][n]是stream接口}C_Row:for(int m=0; m<M; m++){#pragma HLS UNROLLC_Col:for(int n=0; n<N; n++){#pragma HLS UNROLLPE(A_inter[m][n], B_inter[m][n], A_inter[m][n+1], B_inter[m+1][n], C[m][n]);//将对应的接口互联}}//Drainfor(int m=0; m<M; m++){#pragma HLS UNROLLDrain_A(A_inter[m][N]);//最后连接消耗无用consumer的接口}for(int n=0; n<N; n++){#pragma HLS UNROLLDrain_B(B_inter[M][n]);}return;
}

个人理解: 整个程序是在把stream接口进行连接。

卷积到脉动阵列的映射


在卷积的运算过程中,每一个输入特征,每一个输入数据(橙色数据块)需要和一个橙色的权重数据块内积,得到一个输出结果的pixel

将橙色数据块展开为输入矩阵的一行,权重数据块展开成权重矩阵的一列,其内积就是输出矩阵的一个元素。

卷积窗口在输入特征图上滑动,就可以得到输出矩阵了。

如此以来就变成矩阵乘法了。

(卷积过程中有重复,可以开辟一块内存进行数据复用。)

xilinx 暑期学校学习笔记(四) 加速代码与量化、稀疏相关推荐

  1. FPGA入门 Xilinx暑期学校学习Day2

    早上的课程流程 1.SEA开发板简介 这部分没怎么听,自己用的EGO1,听起来SEA好用一些,而且本次课程的实验指导书用的也是那个板子,EGO1要多花点时间了. 2.FPGA开发流程 利用Vivado ...

  2. Gentle.Net学习笔记四:修改代码,使用Oracle数据库

    开始使用Gentle.Net的时候,我使用编译好的类库,可是不久就发现,如果要更好的利用Gentle.Net,你就不得不做一些修改,所以,还是使用源代码的方式为好.    使用源代码,Gentle.N ...

  3. 吴恩达《机器学习》学习笔记四——单变量线性回归(梯度下降法)代码

    吴恩达<机器学习>学习笔记四--单变量线性回归(梯度下降法)代码 一.问题介绍 二.解决过程及代码讲解 三.函数解释 1. pandas.read_csv()函数 2. DataFrame ...

  4. C#可扩展编程之MEF学习笔记(四):见证奇迹的时刻

    前面三篇讲了MEF的基础和基本到导入导出方法,下面就是见证MEF真正魅力所在的时刻.如果没有看过前面的文章,请到我的博客首页查看. 前面我们都是在一个项目中写了一个类来测试的,但实际开发中,我们往往要 ...

  5. JSP学习笔记(四十九):抛弃POI,使用iText生成Word文档

    POI操作excel的确很优秀,操作word的功能却不敢令人恭维.我们可以利用iText生成rtf文档,扩展名使用doc即可. 使用iText生成rtf,除了iText的包外,还需要额外的一个支持rt ...

  6. esp8266舵机驱动_arduino开发ESP8266学习笔记四—–舵机

    arduino开发ESP8266学习笔记四-–舵机 使用时发现会有ESP8266掉电的情况,应该是板上的稳压芯片的限流导致的,观测波形,发现当舵机运转时,电源线3.3V不再是稳定的3.3V,大概是在3 ...

  7. 【Xilinx AX7103 MicroBalze学习笔记6】MicroBlaze 自定义 IP 核封装实验

    目录 实验任务 实验框图 创建自定义 IP 封装 IP IP 封装界面配置 硬件设计(Vivado部分) Block Design搭建 添加 IP 库 约束文件 软件设计(SDK部分) 往期系列博客 ...

  8. STM32F103学习笔记四 时钟系统

    STM32F103学习笔记四 时钟系统 本文简述了自己学习时钟系统的一些框架,参照风水月 1. 单片机中时钟系统的理解 1.1 概述 时钟是单片机的脉搏,是单片机的驱动源 用任何一个外设都必须打开相应 ...

  9. JavaScript学习笔记(四)(DOM)

    JavaScript学习笔记(四) DOM 一.DOM概述 二.元素对象 2.1 获取方式 (1).通过ID获取一个元素对象,如果没有返回null (2).通过`标签名`获取一组元素对象,,如果没有返 ...

最新文章

  1. 软件测试培训怎么学?有没有发展前景?
  2. k8S中的MySQL如何扩容_Kubernetes的etcd多节点扩容实战技巧
  3. Linux用命令修改dpi,Ubuntu17.10通过dpi更改系统字体大小比例的方法
  4. [BZOJ4815][CQOI2017]小Q的表格 数论+分块
  5. 在Tomcat中配配置数据源汇总
  6. linux eclipse安装、新建并运行java程序
  7. 自动运维化tools篇1:用expect完成用户密码的批量修改
  8. Android ActionBar的Overlay模式如何不遮盖顶部内容的问题
  9. H.264笔记之三——环路内滤波
  10. 暴风影音2011 去广告补丁V1.1
  11. SLAM算法 - 3D激光匹配算法
  12. EXCEL实战技巧与数据分析(二)数据透视表
  13. “夏栀的博客”网站一期建站通知贴
  14. 残酷事实:程序员没有真正的「睡后收入」,解决办法是利用「复利思维」放大「复业收入」...
  15. 《少有人走的路1:心智成熟的旅程》第一部分:自律 - 问题和痛苦
  16. vim 配置文件 ,高亮+自动缩进+行号+折叠+优化
  17. ng-bind、ng-value和ng-model
  18. 机器学习——朴素贝叶斯分类
  19. dnn降噪_芯片量产已超百万,「探境科技」发布AI双麦降噪语音识别方案
  20. 想转行网络安全行业,究竟是参加培训班还是靠自学?

热门文章

  1. C4D模型工具—切割边
  2. 安卓逆向 AndroidManifest.xml 编辑 解码与编码工具
  3. 计算机房等电位连接,机房防静电地板等电位接地方案
  4. 华为emui11鸿蒙,鸿蒙2.0系统回退EMUI11工具下载
  5. kafka官网示例说明--KafkaConsumer
  6. 结构结构结构结构结构结构结构结构结构
  7. ubuntu虚拟机双网卡无法上网问题
  8. 【GDPMS】项目管理实战公益培训第十二期
  9. python 判断天干地支年份
  10. 解决WPS字体缺失问题的四种方法