写在前边的话

本博客是转载B站高亚军老师所讲解的内容。觉得高老师讲的太快了,稍不留神就会跳过去很多。本人看了看视频,截了个图,写了个总结。如侵则删。

一、基本概念,pipeline,unrolling

第一章,先上代码,注意代码中的注释,非常重要。后边几章就不上代码了。

//头文件部分
#ifndef FOROPT_H_
#define FOROPT_H_

#include <ap_int.h>

#define N	3
#define WX 	8
#define	BW	16

typedef ap_int<WX>		dx_t;
//当然也可以用ap_uint<>;代表无符号的数据
//ap_fixed<W,Q>,用来定义定点有符号小数
//ap_ufixed<W,Q>,用来定义定点无符号小数
typedef ap_int<BW>		db_t;
typedef ap_int<BW+1>	do_t;

void foo(dx_t xin[N],dx_t a,db_t b,db_t c,do_t yo[N]);

#endif

源代码部分:

#include "for_opt.h"

void foo(dx_t xin[N],dx_t a,db_t b,db_t c,do_t yo[N]){
    int i=0;
/*
 *总循环的次数是N,N称之为LOOP trip COUNT
 *
 *总的操作流程如下(考虑到IP是一个一直工作的,也就能理解为啥这个模块是循环往复的):
 *循环开始
 * C0:获取b,c的数据;
 * C1:获取xin的i个地址
 * C2:读取xin[i]中的数据
 * C3:完成相应的计算
 * C1:获取xin的i个地址
 * C2:读取xin[i]中的数据
 * C3:完成相应的计算
 * ...
 *循环结束
 *
 *循环开始
 * ...
 * */
/*
 * for循环一次需要3个时钟周期,就是上边的  C1,C2,C3
 * 那么这个3就是LOOP iteration latency.(iteration:迭代)
 *
 * i次for循环与i+1次for循环的间隔是3,那么LOOP iteration Interval(LOOP II)也是3
 *
 * loop latency =3*N,也就是循环次数N与单次循环所占的时钟周期的乘积
 *
 * 如果loop latency 加上取b,c两数的时钟周期,就是整个function latency=3*N+1
 *
 * 从这个函数的第一次初始化开始,到下一次初始化结束,这个称之为函数初始间隔(interval)
 * 那么function initial interval(Function II)=11
 *
 * */
    loop:
    for(i=0;i<N;i++){
        yo[i]=a*xin[i]+b+c;
    }

}

点击C synthesis就能查看生成后的综合报告了。函数的initial interval =10,这证明软件版本有所优化了。

如果想要查看仿真波形,编写一个简单的main.c。这个main是不规范的,规范的测试文件需要与真实值对比,进而让函数返回0(正确)还是其他值(错误)。

#include "stdio.h"
#include "for_opt.h"

db_t a=1,b=2,c=3;
dx_t aa[N]={1,2,3};
do_t yo[N];

int main(){
	foo(aa,a,b,c,yo);//传递数组中的数据时,要用地址作为接口

	return 0;
}

运行C/RTL联合仿真。选择all,等待完成。

对于for循环常见的优化就是pipeline,在directive窗口中选中for循环的的标志,然后选择pipeline即可。如图:

为什么要用pipeline呢?看个图就明白了:

也就是说,可以使FPGA尽可能的同步的处理大量的数据。(在第i次循环处理第m步的时候,第i+1次循环正在处理m-1步)

除了pipeline也可以对for循环unrolling(展开、铺开)。因为for循环在默认情况下是折叠的。所谓折叠就是:每次循环都是采用的同一块电路,只是电路被分时复用了。所谓展开就是把这一块电路复制了,可能复制成N份,也可能复制成N/2份(我们是可以选择的)。

比如在一个循环次数为6for循环中,可以把它展开成3for循环,每个for循环只计算2步。(个人认为,在for循环很长的时候,可以采用此方法)

循环变量i(请注意是循环变量i

声明成int i;和声明成ap_int<4> i;在生成后的模块中所占用的资源是不变的,变量的范围决定了资源量,并不是声明类型决定了资源量。

二、for循环的合并(MERGE

假如有两个毫不相关的for循环,我们期望的电路如右边所示。

但实际上,这两个for循环是串行执行的,只有先执行完加法才能执行减法(循环都是8for循环的切换需要额外的时钟周期)

 

这时候我们希望能对这两个for循环合并。在合并之前,需要理解一个新的概念,那就是region.比如:loop_region{},在两个花括弧之间就是这个region,有了region才能对for循环进行合并(MERGE)。

 

合并循环能够在一定程度上降低latency,并且消耗的资源更少。

如果循环边界不同呢?

合并之后的trip count变成了4max={N,M}

如果两个for循环的边界分别是一个常数和一个变量(variable),应该怎么处理呢?

这时候是不能直接进行合并的。

如果循环边界都是变量:

这时如果强行合并也会显示错误信息。

如果想要合并,应该采取以下的处理方法(这里假设有:J<K):

 

三、for循环优化之DATAFLOW

首先来看个简单的例子,正常的流程肯定是执行完A再执行B最后执行C

如果按照上图的流程,肯定是串行处理的。这里就能用到dataflow了,给出简易的优化图:

降低latency提高了数据吞吐率。

 

 

Dataflow使用的时候是有限制的,这里也举两个例子:

1:当loop1输出一组数据,被两组循环引用的时候是不能用dataflow的:

    如果中间添加一个loop_copy是可以对上述例子进行优化的(loop_copy仅仅是吧temp1拷贝成了temp2temp3两份数据,两个输出对应两个输入):

 

2loop1输出的两个数据一个绕过了loop2一个没有绕过loop2,输出的数据被loop3使用,这时不能用合并。也不能用dataflow.

解决方案就是loop2中添加了一个copy模块,这样就能用dataflow了。

 

对于ABC之间的通道的类型是啥样的呢,我们可以配置。可以是ping-pong RAM也可以是FIFO如果参数是个scalar(标量)、pointer(指针)或reference(引用)那么HLS会把它归类为FIFO。如果参数是个数组,通道的类型可能是FIFO(数据流是按顺序)也可能是RAM(这时就会把通道变成一个ping-pong RAM)。

可以选择默认的配置方式,当然我们也可以通过工具来设定这个通道是ping-pong RAM还是FIFO(注意FIFO的深度)。

 

四、嵌套for循环的优化

嵌套for循环分为以下几种:

  1. perfect loop nest
  2. semi-perfect loop nest
  3. imperfect loop nest(1)
  4. imperfect loop nest(2)

1.perfect loop nest的优化

举个优化perfect loop nest的例子.只对内部做流水处理和只对外部做流水处理的结果如下:

结果这样是因为,我们对外部的循环做流水,内部的循环也会跟着做流水,因此这时候所消耗的资源有所增加。

 

如果我们只对内部的循环做流水,trip count变成了8.

 

2.imperfect loop nest的优化

(1)对最内层做流水

如果只对product部分(最内层)做流水,结果如下图:

(2)对中间层做流水

如果只对col部分(中间层)做流水,这时候就trip count成了9.

 

(3)对最外层做流水

如果对最外部的for循环做流水,这时候的trip count是最小的。但是DSP48消耗的也是最多的。

(4)3种优化方式的对比

不加任何约束、最内部、中间层、最外层4种情况作比较,资源和速度的对比(可以发现中间层优化效果的性价比最高):

(5)矩阵乘法的优化

实际上我们可以对矩阵乘法做出优化,首先看矩阵乘法的流程,在这个流程中ab取值都取了27次,一共取值51次:

 

因此在Xilinx官方的例程中给出了矩阵乘法优化的代码。将a矩阵的每一行和b矩阵的每一列都做了个缓存。这样做的好处是避免端口重复多次读取数据,减少了寻址的次数,从而加速了矩阵运算的过程。

优化后的流程以及自己做的流程图(也不知道理解的对不对):

五、for循环的其他优化方法

1.for循环的并行性

For循环的并行执行。Merge是可以的(如果两个循环次数不一样就不能并行执行了)。

上述执行后的latency结果是一致的,但是资源节省了一半。

采用allocation可以使两个函数并行执行。

ALLOCATION instances=Accumulator limit=2 function

这个语法就是把Accumulator这个函数复制了2份。结果如下(pipeline + allocation):

2.在循环流水中使用rewind

Rewind的优化:

从综合的结果来看,pipeline + rewind还是具有很大优势的:

 

如果一个函数中包含了多个for循环,这时是不能执行rewind的。

3.for循环边界是变量时候的处理方法(3种)

循环边界是变量的时候:

如何处理这种情况呢?

(1)使用tripcount指令

Tripcount不会影响综合后的结果,不对综合做任何优化,只是比较不同的solution比较方便。

(2)定义循环边界用ap_int<W>/ ap_uint<W>

循环最边界LOOP_N定义成ap_int<W>,那么trip count的最大值是15.使用这种方法,能大大减少资源的使用。

(3)使用assert语句

//loop_n是循环边界,是变量
//LOOP_N是循环次数最大不能超过的值
assert(loop_n<LOOP_N);

(4)上述三种方法的对比

三种方法的对比(很显然assert这种方式,是最好的):

 

 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐