HLS:矩阵乘法单元设计与SDK测试


一、引言

矩阵乘法,涉及数组优化、循环优化和接口优化等。是一个学习HLS非常好的Lab。

HLS新建工程这些就不记录了,在新建时有个需要注意,就是Clock Period的设置,如果设为10,会按照100MHz时钟来进行优化,如果一次运算15ms,就会分配到两个时钟来计算。但HLS工具预估是比较保守的,比如预估一次运算15ms,但实际上板卡后这个运算,就只要7ms。因此可以把周期稍微放大些/减轻约束压力,让HLS在做优化时,能放在一个周期内完成,但如果达不到约定的时钟频率,是可以进行更改的。在新建后的工程右键Project Settings,来进行更改。


二、程序框架

有Source、Include源代码和Test Beach目录。
在这里插入图片描述
用于实现矩阵乘法功能的代码,放在Source目录下。用于测试矩阵乘法功能的代码,放在Test Beach目录下。

新建文件:matrix_mul.cpp、matrix_mul.h和main.cpp。

在matrix_mul.h文件中,使用#ifdef 宏的方式,来避免重复声明某些内容。


三、初步设计

还未进行优化前的基本功能设计。

1、matrix_mul.h文件
任意精度定点数:在传统C中,char、int、short、long等的位宽都是固定的,8位、32位、16位和64位。如果电路中要求10位宽的数据,则常规变量无法实现,HLS为了能满足需求,搞了一种ap_int<>类型的数据变量,<>内放的是该类型变量的位宽,可以认为ap_int<8> A与char B,是两种类型相同的变量。

#ifndef __MATRIX_MUL__
#define __MATRIX_MUL__

// HLS提供的任意精度定点数文件
#include "ap_fixed.h"

// A元素对应行,与B元素对应列,进行相乘得到C
void matrix_mul(ap_int<8> A[4][4], ap_int<8> B[4][4], ap_int<16> C[4][4]);

#endif

2、matrix_mul.cpp文件
在这里插入图片描述

#include "matrix_mul.h"

void matrix_mul(ap_int<8> A[4][4], ap_int<8> B[4][4], ap_int<16> C[4][4]){
	for(int i=0;i<4;i++)
		for(int j=0;j<4;j++){
			C[i][j]=0;
			// 循环乘四次,并进行相加
			for(int k=0;k<4;k++){
				C[i][j]=C[i][j]+A[i][k]*B[k][j];
			}
		}
}

3、main.cpp文件

#include "matrix_mul.h"
#include <iostream>

int main(){
	ap_int<8>  A[4][4];
	ap_int<8>  B[4][4];
	ap_int<16> C[4][4];

	// 初始化赋值0、1、2、3、4、5...
	for(int i=0;i<4;i++)
		for(int j=0;j<4;j++){
		A[i][j]=i*4+j;
		B[i][j]=A[i][j];
	}
	matrix_mul(A, B, C);
	for(int i=0;i<4;i++){
		for(int j=0;j<4;j++)
			std::cout<<"C["<<i<<","<<j<<"]="<<C[i][j]<<std::endl;
	}
	return 0;
}

代码写完,第一步要跑C Simulation,用快捷键Ctrl + B,就可以进行编译了。生成exe文件,就证明没有语法错误。然后,在菜单栏里,有一个Run C Simulation,点后进行C仿真,来看结果是否正确。是纯C语言的仿真。
在这里插入图片描述
第二步进行综合,点菜单栏的Run C Synthesis。可能会出现下面这个“The function must be specified”错误,原因是可能有很多C函数,但哪个是顶层,需要确定下。在工程中右键Project Settings,选择Synthesis选项卡,将Top Function设置为matrix_mul,就可以跑综合了。
在这里插入图片描述


四、报告分析

综合完,会生成Synthesis的报告文件,下边对该文件的几个关键参数进行分析。

1、Performance Estimates,性能估计。
1°Timing:包含clock,时钟约束。
2°Latency:在Summary中,包含了Latency与Interval,初步设计中,Latency为169,表示这个矩阵乘法的函数模块,需要耗时169个时钟周期。Interval是这次矩阵乘法与下一次,需要间隔多少周期,由于没有进行流水线的设计,这里需要间隔169个时钟周期,才能进行下一个的矩阵运算,因此Interval等于169。在Detail中,有循环内部的一些细节,可以看到每个循环需要的一些周期数,有些循环可能需要状态跳转,会消耗一部分的时钟周期。
后续优化,主要也是照着Latency来进行优化。

2、Utilization Estimates,资源利用率估计。
一些资源使用的情况。

3、Interface,端口情况。
模块的参数就是端口,可以通过参数来生成不同的接口,不设置为缺省,默认是存储器里的数据。还有一些信号,ap开头,为控制信号。


五、优化操作

初步设计中,完全没有利用到FPGA的并行性,现考虑一些优化操作,来提高性能。

1、行列相乘部分
行列相乘后相加的代码,我们希望只用一个周期就计算出来。两个方法,1是利用参数循环展开,2是告诉编译器,这部分编译时,需要1个周期来完成,这样也会自动展开。

		C[i][j]=0;
		for(int k=0;k<4;k++){
			C[i][j]=C[i][j]+A[i][k]*B[k][j];
		}

手动循环展开。在功能模块界面,右边有一个Directive,里边有一个for Statement,点开有三个循环,选中最里边那个子循环,右键Insert Directive,打开Vivado HLS Directive Editor这个界面。选择UNROLL,还有Source File和Directive File的选项,Source File是将参数放在C源代码文件中,Directive File是把参数单独放在一个源代码文件中,一般选择Source File就行了。
在这里插入图片描述
Note:本来这方式在这里是可以的,但有个C[i][j]=0。如果循环展开的话,这个赋值操作,也会占用一个周期。因此,干脆直接将第二级子循环的Iteration Latency(某个循环消耗的Latency)设置为1,就可以实现上述代码一个周期的目的,如何实现呢?
在右边Directive选中第二级循环,右键Insert Directive,打开Vivado HLS Directive Editor后,选择PIPELINE,将里边的II,就是Iteration Latency,设置为1。操作完,会出现#pragma HLS PIPELINE II=1的命令。该操作可强制让每个运算在一个周期内完成,共16个运算,也就是只需要16个周期。

2、数组部分PARTITION
在这里插入图片描述
单、双口存储器的限制,存储器读写冲突,会出现下图那个错误,导致没法一次读取出4个数据,进行行列相乘。如果把数组A的存储器竖着(二维)切成四个,就可以一次读出四个数据。同理,数组B的存储器横着(一维)切成四个,也可以实现一次读出四个数据。不知道这个原因时,可以先进行仿真,来找到问题的关键。
在这里插入图片描述
使用#pragma HLS ARRAY_PARTITION variable=A complete dim=2来完成切割。

3、数组部分RESHAPE
将原来深度为16,位宽为8的存储器,变成深度为4,位宽为32的存储器。A数组按二维堆起来,B数组按一维堆起来。
使用#pragma HLS ARRAY_RESHAPE variable=A complete dim=2来完成组合。

PARTITION一次可以读相同或不同的四个数据地址,但RESHAPE一次读的四个数据,是同一个地址的。通过实验波形,可以更加深入区分这两种操作。all是导出所有信号的波形。
在这里插入图片描述
至少消耗的时钟周期为16*interval+latency,除此外还有写状态转换的消耗。interval可以被约束为1,那latency呢?latency也可以由#pragma HLS LATENCY min=5 max=5来约束。

经过上述操作,综合后可以发现,只用了18个周期便可以完成了,至于为什么不是16个周期,是因为还有一个周期进行启动,并且写操作也有一个周期的延迟。
在这里插入图片描述


六、接口优化

之前使用的都是ap_memory存储器的接口,但模块一般挂接到SoC上,这里将接口改为s_axilite类型,即作为slave设备挂载在SoC上。
在这里插入图片描述
基本命令是:#pragma HLS INTERFACE s_axilite port=A。
实际中发现,如果只添加这个命令,interface接口上,还是有ap_start与ap_done模块使能控制信号,这个需要和axi信号加以区分下,在这里,axi只做了数据传输的工作,但模块需要先使能工作才行,就是通过ap_start来完成的。为了让ap_start和ap_done这些信号也受CPU的控制,可以添加#pragma HLS INTERFACE s_axilite port=return。
在这里插入图片描述
将ap_start与ap_done都变成axi接口综合后的结果。
在这里插入图片描述
最后,点击report RTL,就能将工程输出为一个IP,在工程中调用。
在这里插入图片描述


七、上板测试

使用的FPGA型号是xc7z020clg400-1。环境简陋,将就着看。
在这里插入图片描述

1、平台搭建。
创建最小系统的Zynq核,并将DDR、UART0配置好,同时引出100MHz的PL时钟,还有一个复位信号。总线互联模块是一接一的,CPU启动工作matrix_mul模块,通过一个AXI口访问,将16个数据从AXI发送到模块,并检测是否done完成。每次改完,记得生成wapper和generate output两步。

在这里插入图片描述

2、生成并导出bitstream,同时launch SDK。
这个没啥复杂的,传统流程。需要留意下,matrix_mul模块的寄存器地址。
在这里插入图片描述

3、SDK开发
新建一个application project的HelloWorld模板,把串口调通之后,开始进行模块的测试,测试的代码如下。为了看懂这个代码,需要去学习几个AXI外设控制的函数。

#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "stdio.h"
#include "xil_io.h"
#include "xparameters.h"
#include "xmatrix_mul_hw.h"
#include "xmatrix_mul.h"

int main()
{
	int data;
	int state;
	// 如果不使用大括号,会出现这个错误:missing braces around initializer
	char A[4][4] = {{0,1,2,3},{4,5,6,7},{8,9,10,11},{12,13,14,15}};
    char B[4][4] = {{0,1,2,3},{4,5,6,7},{8,9,10,11},{12,13,14,15}};
	short C[4][4];

	// 定义一个XMatrix_mul类型的变量xmatrix_mul,该变量为自动生成的。
	XMatrix_mul xmatrix_mul;
    init_platform();
	
	// 将HLS生成的IP赋值给xmatrix_mul,用于后续操作,并将状态信息赋值给state,用于判断是否初始化成功。
    state = XMatrix_mul_Initialize(&xmatrix_mul, XPAR_MATRIX_MUL_0_DEVICE_ID);
    if(state != XST_SUCCESS)
    {
    	print("XMatrix_mul_Initialize fail!!\n\r");
    	return XST_FAILURE;
    }

	// 由于A是将一行组合成一个数据进行存储,所以接下来是将组合好的新的数值写进RAM。
	// 这个是和之前进行RESHAPE操作相匹配的,输入的数据是32位的。
	// Xil_Out32函数是将一个32位的值,写入到一个特定的地址中去。
	// XMATRIX_MUL_AXILITES_ADDR_A_V_BASE是matrix_mul模块内addr寄存器的偏置地址
	// XPAR_MATRIX_MUL_0_S_AXI_AXILITES_BASEADDR是matrix_mul模块内addr寄存器的基地址
	// 基地址+偏置地址,就可以完成对该地址的操作。
	for(int i=0;i<4;i++)
	{
		u32 tp = 0;
		tp = A[i][0] + (A[i][1] << 8) + (A[i][2] << 16) + (A[i][3] << 24);
		Xil_Out32((XMATRIX_MUL_AXILITES_ADDR_A_V_BASE+XPAR_MATRIX_MUL_0_S_AXI_AXILITES_BASEADDR) + (i*4), (tp));
	}

	// 由于B是将一列组成一个数据进行存储,所以接下来是将组合好的新的数值写进RAM。
	for(int i=0;i<4;i++)
	{
		u32 tp = 0;
		tp = B[0][i] + (B[1][i] << 8) + (B[2][i] << 16) + (B[3][i] << 24);
		Xil_Out32((XMATRIX_MUL_AXILITES_ADDR_B_V_BASE+XPAR_MATRIX_MUL_0_S_AXI_AXILITES_BASEADDR) + (i*4), (tp));
	}

	// 第一次启动后可以自启动,不必再初始化。
    XMatrix_mul_EnableAutoRestart(&xmatrix_mul);
    // 启动电路,将ap_start置为1,开始计算。
    XMatrix_mul_Start(&xmatrix_mul);
    print("Test Start!!!\n\r");

	// 数据输出
	// 判断计算完成的条件是ap_done为1,当不是1时说明还尚未完成,就一直读取判断,直至算完。
    data  = XMatrix_mul_IsDone(&xmatrix_mul);
    while(data != 1)
    {
    	data  = XMatrix_mul_IsDone(&xmatrix_mul);

    }

    for(int i =0;i<4;i++)
    {
    	int tp;
    	for(int j =0;j<2;j++)
    	{
    		tp = XMatrix_mul_ReadReg((XMATRIX_MUL_AXILITES_ADDR_C_V_BASE + XPAR_MATRIX_MUL_0_S_AXI_AXILITES_BASEADDR),4*(2*i+j));
    		C[i][2*j+1] = tp >> 16;
    		C[i][2*j]   = tp - C[i][2*j+1] * 65536;
    	}

    }

    for(int i=0;i<4;i++)
    {
    	for(int j=0;j<4;j++)
    	{
    		printf("C[%d][%d]:%d\n",i,j,C[i][j]);
    	}
    }

    print("Test End!!!\n\r");
    cleanup_platform();
    return 0;
}

4、实验输出的测试。
在这里插入图片描述


八、补充部分

1、关于ap控制信号。
控制信号方面,着重关注的是ap_start与ap_done,在ap_start变为高电平后,模块开始工作,工作结束时ap_done拉高一个时钟周期,如果要继续工作,则ap_start继续为高电平,反之变为低电平,停止该模块的工作。在这里插入图片描述
2、关于SDK方面。
之前因为没有板卡,且对AXI有一部分的恐惧,但这次实验后,发现外设的访问并没有想象中的难,只需要寄存器地址就可以了,就像下面这张表列出来的。至于AXI内部的很多控制信号,CPU的Core其实已经帮忙做完了。
在这里插入图片描述
3、最后的matrix_mul.cpp代码。

// matrix_mul.cpp
#include "matrix_mul.h"

void matrix_mul(ap_int<8> A[4][4], ap_int<8> B[4][4], ap_int<16> C[4][4]){
	#pragma HLS INTERFACE s_axilite port=return
	#pragma HLS INTERFACE s_axilite port=B
	#pragma HLS INTERFACE s_axilite port=C
	#pragma HLS INTERFACE s_axilite port=A
	#pragma HLS ARRAY_RESHAPE variable=B complete dim=1
	#pragma HLS ARRAY_RESHAPE variable=A complete dim=2
	for(int i=0;i<4;i++)
		for(int j=0;j<4;j++){
			#pragma HLS PIPELINE II=1
			C[i][j]=0;
			// 循环乘四次,并进行相加
			for(int k=0;k<4;k++){
				C[i][j]=C[i][j]+A[i][k]*B[k][j];
			}
		}
}

九、时间与参考

一些关键时间点的记录:

2021年04月09日:看了一些课,学了HLS的一些用法。
2021年04月10日:搭建并测试了HLS写的矩阵乘法模块的准确性。
后续可能做的工作:如果有Pynq板卡,计划将本实验作为Pynq入门实验。

参考性的资料:

warning: missing braces around initializer的解决
基于ZYNQ的HLS矩阵乘法加速实验


  • 5
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学不懂啊阿田

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值