实现矩阵向量乘法的简单代码
#define SIZE 8
typedef int BaseType;
void matrix_vector(BaseType M[SIZE][SIZE], BaseType V_In[SIZE], BaseType V_Out[SIZE]) {
BaseType i, j;
data_loop:
for (i = 0; i < SIZE; i++) {
BaseType sum = 0;
dot_product_loop:
for (j = 0; j < SIZE; j++) {
sum += V_In[j] * M[i][j];
}
V_Out[i] = sum;
}
}
矩阵向量乘法的优化(流水线pipline和并行运行(unroll))
手动展开矩阵向量乘法内部循环实例
#define SIZE 8
typedef int BaseType;
void matrix_vector(BaseType M[SIZE][SIZE], BaseType V_In[SIZE], BaseType V_Out[SIZE]) {
BaseType i, j;
data_loop:
for (i = 0; i < SIZE; i++) {
BaseType sum = 0;
V_Out[i] = V_In[0] * M[i][0] + V_In[1] * M[i][1] + V_In[2] * M[i][2] +
V_In[3] * M[i][3] + V_In[4] * M[i][4] + V_In[5] * M[i][5] +
V_In[6] * M[i][6] + V_In[7] * M[i][7];
}
}
循环的展开可以由Vivado HLS在流水线中自动执行,也可以通过使用#pragma HLS unroll或者流水线外的等价指令来实现。
存储权衡和数据分区
通常,大量数据存储在片外存储器如DRAM、闪存或网络连接的存储器中,但是数据访问时间通常很长,大约为几十到几百(或更多)个周期。由于大量的电流必须流过长电线已访问片外存储器,所以使用片外存储消耗的能量也比较大。相反,片上存储器可以快速访问并且功耗要低得多,只是它可以存储的数据量有限。有一种常见的操作模式类似于通用CPU的内存层次结构中的缓存效果,它是将数据重复地加载到块中的片上存储器上。
当我们选择片上存储器的时候,需要在嵌入式存储器(例如Block RAM)或触发器(FF)之间权衡。**基于触发器的存储器允许在一个时钟内对不同地址的数据进行多次读取,也可以在一个时钟周期内读取、修改和写入基于触发器的存储器。**然而,即使在资源配置最好的设备中,FF的数量通常也限制在大约10万字节左右。实际上,以便有效地使用其他FPGA资源,大多数基于FF的存储器应该小得多。Block RAM(BRAM)提供更高的容量,拥有Mbytes的存储量,其代价是有限的可访问性。例如,单个BRAM可以存储大于1到4千字节的数据,但是在每个时钟周期只可以对该数据的两个不同的地址进行访问。此外,BRAM需要尽可能减少流水线操作(比如,读操作必须具有至少一个周期的延迟)。因此,我们的基本的权衡点在于工程所需的带宽与容量。
如果说数据的吞吐量我们需要考虑的头号问题,则所有数据都将存储在FF中。这将允许任何元素在每个时钟周期内被访问尽可能多的次数。但是,随着矩阵阵列变大,这种方案也将变得不可行。在矩阵向量乘法的情况下,存储1024位乘以1024位矩阵的32位整数将需要大约4兆字节的存储器。即使使用BRAM来存储,由于每个BRAM存储大约4KBytes,也需要大约1024个BRAM块。另一方面,使用单个大型基于BRAM的内存意味着我们一次只能访问两个元素。这明显降低了性能,如图4.7所示,它需要在每个时钟周期访问多个数组元素(V_In[]的所有8个元素以及M[][]的8个元素)。在实际工程中,大多数设计需要更大的阵列分布存放在更小的BRAM存储器中,这种方法称为阵列分区。较小的数组(通常用于索引较大的数组)可以完全划分为单独的标准变量并映射到FF。匹配流水线选择和数组分区以最大限度地提高运算符使用率和内存使用率是HLS设计探索的一个重要方面。
#define SIZE 8
typedef int BaseType;
void matrix_vector(BaseType M[SIZE][SIZE], BaseType V_In[SIZE], BaseType V_Out[SIZE]) {
#pragma HLS array_partition variable=M dim=2 complete
#pragma HLS array_partition variable=V_In complete
BaseType i, j;
data_loop:
for (i = 0; i < SIZE; i++) {
#pragma HLS pipeline II=1
BaseType sum = 0;
dot_product_loop:
for (j = 0; j < SIZE; j++) {
sum += V_In[j] * M[i][j];
}
V_Out[i] = sum;
}
}
通过流水线操作并将部分循环展开应用于 dot_product_loop,我们也可以得到类似的结果。下图显示了将矩阵向量乘法代码的内部循环展开2倍的结果。
#define SIZE 8
typedef int BaseType;
void matrix_vector(BaseType M[SIZE][SIZE], BaseType V_In[SIZE], BaseType V_Out[SIZE]) {
#pragma HLS array_partition variable=M dim=2 cyclic factor=2
#pragma HLS array_partition variable=V_In cyclic factor=2
BaseType i, j;
data_loop:
for (i = 0; i < SIZE; i++) {
BaseType sum = 0;
dot_product_loop:
for (j = 0; j < SIZE; j+=2) {
#pragma HLS pipeline II=1
sum += V_In[j] * M[i][j];
sum += V_In[j+1] * M[i][j+1];
}
V_Out[i] = sum;
}
}
你可以发现,循环的边界现在增加到2;每个循环迭代需要2个矩阵M[][]和向量Vin[]每次迭代并执行两次乘法而不是一次。使用这种方式循环展开后,对应于原始循环的两次迭代,Vivado HLS可以并行地在两个表达式中实现这些操作。请注意,如果没有适当的数组分区,展开内部循环可能不会提高性能,因为并发读取操作的数量受到内存端口数量的限制。在这种情况下,我们可以将来自偶数列的数据存储在一个BRAM中,将来自奇数列的数据存储在另一个BRAM中。这是因为展开的循环总是执行一次偶数迭代和一次奇数迭代。
将重点转移到DFT
#include <math.h> //Required for cos and sin functions
typedef double IN_TYPE; // Data type for the input signal
typedef double TEMP_TYPE; // Data type for the temporary variables
#define N 256 // DFT Size
void dft(IN_TYPE sample_real[N], IN_TYPE sample_imag[N]) {
int i, j;
TEMP_TYPE w;
TEMP_TYPE c, s;
// Temporary arrays to hold the intermediate frequency domain results
TEMP_TYPE temp_real[N];
TEMP_TYPE temp_imag[N];
// Calculate each frequency domain sample iteratively
for (i = 0; i < N; i += 1) {
temp_real[i] = 0;
temp_imag[i] = 0;
// (2 * pi * i)/N
w = (2.0 * 3.141592653589 / N) * (TEMP_TYPE)i;
// Calculate the jth frequency sample sequentially
for (j = 0; j < N; j += 1) {
// Utilize HLS tool to calculate sine and cosine values
c = cos(j * w);
s = sin(j * w);
// Multiply the current phasor with the appropriate input sample and keep
// running sum
temp_real[i] += (sample_real[j] * c - sample_imag[j] * s);
temp_imag[i] += (sample_real[j] * s + sample_imag[j] * c);
}
}
// Perform an inplace DFT, i.e., copy result into the input arrays
for (i = 0; i < N; i += 1) {
sample_real[i] = temp_real[i];
sample_imag[i] = temp_imag[i];
}
}