硬件的核心是并行编程,它主要包括两大部分:多流水并行、流水内部打拍。
1 多流水并行编程是在硬件内部形成多条流水,和cpu多个核心 类似,但是数量可以远远超过cpu核数,一般实现方案有两种:fifo和ram
1) fifo:将执行流程拆解成多个模块,模块间通过fifo连接起来,每个模块独立一个流水,模块的运行受控于fifo是有数据和控制命令。这块有点像软件的多线程通过无锁队列传输数据的方式。
2) ram:是一个模块将数据写入, 另外一个模块读出处理,这个方式的优势是可以一个生产者、多个消费者处理数据。难点在于通知机制和模块间同步,一般可以用fifo传递信号。
如果用到ram跨函数传输,使用dataflow有三个条件:
1) 需要使用stable表示跨函数的raw和参数不用考虑相互关系(需要在顶层--dataflow的地方或用到的两个函数加上stable);
2) 使用的两个函数只能一个写一个读,不允许单个变量两个函数都有读写,当然多个变量之间没关系(比如 a、b在func1、func2使用,a func1写func2读,b func1读func2写实可以的);
3) 如果函数有ram数组类型的参数需要保证函数顺序对ram的参数先写后读,使用dependence宏无效
总体来说fifo的方式一般够用,ram的方式用的场景比较少。另外,在dataflow场景中,有个原则,尽量生产者把消费者需要的信息都提供,消费者尽量减少计算
2 流水内部打拍是指在一个模块内部运行流程周期是M周期,如果需要执行N次,那么总时延是M*N,但是如果流水内部运行流程拆解成多个步骤,每个步骤1拍完成(既拆成M个步骤),然后设计的时候能保证第X步骤生成的中间值在后续步骤使用是不被破坏,就可以每拍启动一次,这样的好处是执行N次的时间是 M+N-1次。
流水打拍可以提前跳出,但是提前跳出的条件需要再一拍内完成,否则就无法完成1拍的流水打拍,比如:根据ram类型判断,因为ram访问需要1拍的时间,导致流水打拍需要两拍才能完成,总就变成2*N+M-2了
dependence参数对循环中数组操作有作用,例子如下:
int a[100];int old,new;
int val;
for( int i=0;i<50;i++ )
{
#pragma HLS pipeline
/// 步骤1
a[new]=val;
val = a[old]+1;
/// 其它
........
}
上述例子中编译器会假设最坏的情况,old和new相等,这个时候由于有相互依赖,II=2,但是实际的逻辑中设计new不会和old相等,这个时候可以使用dependence关闭依赖关系
流水内部打拍设计有两个难点:如何将步骤拆解成1拍完成,产生的中间值后面使用不被后续打拍破坏
1) 步骤拆解成1拍,主要遇到的问题是原子操作的拆解,比如:128b*128b的乘法,如果是组合电路,1拍很难满足。所以需要设计算法拆解。当然也可以2拍来打拍,但是时延将会变成M+2N-2,成倍数上升。
2)产生的中间值后面使用不被后续打拍破坏,一般做法有两种,将中间变量做成数组(数组的长度不小于步骤数),通过多次寄存器赋值实现。例子如下:
int a,b,c,d;
step1: b=a;
step2: c=b;
step3: d=b+c;
上述例子,如果流水打拍会出现以下场景(按照a=1、2、3、4):
初始状态:a=1,b=0,c=0,d=0
一拍后: a=2,b=1,c=0,d=0
二拍后: a=3,b=2,c=1,d=0
三拍后: a=4,b=3,c=2,d=2+1
四拍后: a=5,b=4,c=3,d=3+2
而我们想要的是第一个结果是 d=1+1,第二个是d=2+2....
改造方案1(待验证)
int a,b,c,d; int b1;
step1: b=a;
step2: c=b; b1=b;
step3: d=b1+c;
初始状态:a=1,b=0,c=0,d=0, b1=0
一拍后: a=2,b=1,c=0,d=0, b1=0
二拍后: a=3,b=2,c=1,d=0, b1=1
三拍后: a=4,b=3,c=2,d=1+1,b1=2
四拍后: a=5,b=4,c=3,d=2+2,b1=3
这样就做到我们想要的结果了
改造方案2:
int a[3],b[3],c[3],d;
step1: b[i%3]=a[i%3];
step2: c[i%3]=b[i%3];
step3: d=b[i%3]+c[i%3];
初始状态:a[0]=1,b[0]=0,c[0]=0,d=0
a[1]=0,b[1]=0,c[1]=0,d=0
a[2]=0,b[2]=0,c[2]=0,d=0
一拍后: a[0]=1,b[0]=1,c[0]=0,d=0
a[1]=2,b[1]=0,c[1]=0,d=0
a[2]=0,b[2]=0,c[2]=0,d=0
二拍后: a[0]=1,b[0]=1,c[0]=1,d=0
a[1]=2,b[1]=2,c[1]=0,d=0
a[2]=3,b[2]=0,c[2]=0,d=0
三拍后: a[0]=4,b[0]=1,c[0]=1,d=1+1
a[1]=2,b[1]=2,c[1]=2,d=1+1
a[2]=3,b[2]=3,c[2]=0,d=1+1
四拍后: a[0]=4,b[0]=4,c[0]=1,d=2+2
a[1]=5,b[1]=2,c[1]=2,d=2+2
a[2]=3,b[2]=3,c[2]=3,d=2+2
五拍后: a[0]=4,b[0]=4,c[0]=4,d=3+3
a[1]=5,b[1]=5,c[1]=2,d=3+3
a[2]=6,b[2]=3,c[2]=3,d=3+3
这样分别在第3拍、4拍、5拍获取到正确的值
方案1
优点:消耗资源少,对于每个步骤少并且明确是几拍的非常有效
缺点:对于步骤多复杂的场景影响大,而且一旦步骤变化需要重新计算;大于一拍打拍比较难处理
方案2
优点:是结构简单;对于步骤变化不敏感(只要数组的长度不小于步骤数);支持大于1拍打拍;缺点:浪费资源
流水打拍pipeline的使用范围,pipeline可以在循环和函数中使用,在循环中使用,会将循环内部的执行过程流水打拍;函数中使用pipeline还需要再好好研究下。
有一点可以确定的是当循环或者函数进行流水打拍时,所在循环体或者函数体包括循环,这些循环会自动展开,也就是说流水打拍会尽量让每个步骤最短时延(它把循环当做一个其中步骤)
如果循环体或函数体包含循环,但是不想让循环展开(资源或者设计局限),应该怎么办?