ZED Board从入门到精通系列(八)——Vivado HLS实现矩阵相乘

终于到了HLS部分。HLS是High Level Synthesis的缩写,是一种可以将高级程序设计语言C,C++,SystemC综合为RTL代码的工具。

 

生产力的发展推动了设计模式。在电子技术初级阶段,人们关注的是RLC电路,通过建立微分方程求解电路响应。门级电路是对RLC的初步封装,人们进而采用布尔代数、卡诺图进行电路设计与分析。之后随着集成电路进一步发展,门电路可以集成为寄存器、触发器、ROM等宏单元,设计工具也变得更为高度模块化。算法级别的电路设计,则一直没有特别好的工具,直到出现了HLS。HLS可以将算法直接映射为RTL电路,实现了高层次综合。从这个层面上讲,System Generator也是一种高层次综合工具,因为它将matlab算法描述综合为RTL代码。如果今后机器学习、人工智能获得重大突破,或许会出现将人类自然语言综合为RTL代码的工具,不知我们是否能见证它的面世。

 

HLS的学习资源可以参考http://xilinx.eetrend.com/article/5096。本节给出较为通用的矩阵与向量相乘例子,从全串行到全并行进行了一步步优化实现。

矩阵实验室Matlab是比较常用的数学仿真软件。本博主用的是R2013a版本。为了验证矩阵向量相乘正确性,我们先用matlab生成测试矩阵和向量,并利用matlab计算结果。代码如下:

clear;
clc;
close all;

N = 5;

A = randi([1,100],N,N);
b = randi(100,N,1);

c = A*b;

KKK_SaveToCHeaderFile(A,'A.h');

KKK_SaveToCHeaderFile(b,'b.h');
KKK_SaveToCHeaderFile(c,'c.h');

这里给出的是A*b = c的简单例子,A为5X5矩阵,b为5X1向量,结果c为5X1向量。其中KKK_SaveToCHeaderFile()是将矩阵、向量保存为C语言数组的子函数,定义如下:

function [] = KKK_SaveToCHeaderFile(var,fn)
fid = fopen(fn,'w');
var = reshape(var.',1,[]);
fprintf(fid,'%d,\r\n',var);
fclose(fid);

给出测试例程中,A如下:

82	10	16	15	66
91	28	98	43	4
13	55	96	92	85
92	96	49	80	94
64	97	81	96	68

b如下:

76
75
40
66
18

得到的c如下:

9800
15846
16555
23124
22939

运行matlab脚本之后,生成三个文件:A.h,b.h,c.h,这些是作为HLS程序的输入数据和参考结果。下面我们用HLS工具实现上述矩阵X向量的功能。第一步,运行Vivado HLS。

选择第一项,Create New Project,建立新工程MatrixMultiply

 

输入路径和工程名之后,点Next。

添加顶层模块文件。这里我们Top Functions输入MatrixMultiply,然后New File...,新建一个.c文件,命名为MatrixMultiply.c(后缀不要省略!),然后点Next

添加顶层文件测试脚本。这里New一个文件TestMatrixMultiply.c(后缀不要省略!),然后Add前面用Matlab生成的A.h,b.h,c.h,如下图所示:

点Next,选择解决方案配置,如下图所示

其余保持默认,只修改Part Selection部分,改为ZedBoard。改完后,Finish即可进入主界面,如下图所示

 

可以看出,Vivado HLS界面很像很像Xilinx SDK,不同的是前者负责PL部分开发,后者负责PS软件编写,定位不同决定了二者今后的路必然走向分歧。

 

将MatrixMultiply.c内容改为:

 

typedef int data_type;
#define N 5

void MatrixMultiply(data_type AA[N*N],data_type bb[N],data_type cc[N])
{
	int i,j;
	for(i = 0;i<N;i++)
	{
		data_type sum = 0;
		for(j = 0;j<N;j++)
		{
			sum += AA[i*N+j]*bb[j];
		}
		cc[i] = sum;
	}
}

 

将TestMatrixMultiply.c内容改为:

<p>#include <stdio.h>
typedef int data_type;
#define N 5</p><p>const data_type MatrixA[] = {
#include "A.h"
};
const data_type Vector_b[] = {
#include "b.h"
};
const data_type MatlabResult_c[] = {
#include "c.h"
};</p><p>data_type HLS_Result_c[N] = {0};
void CheckResult(data_type * matlab_result,data_type * your_result);
int main(void)
{
 printf("Checking Results:\r\n");
 MatrixMultiply(MatrixA,Vector_b,HLS_Result_c);
 CheckResult(MatlabResult_c,HLS_Result_c);
 return 0;
}
void CheckResult(data_type * matlab_result,data_type * your_result)
{
 int i;
 for(i = 0;i<N;i++)
 {
  printf("Idx %d: Error = %d \r\n",i,matlab_result[i]-your_result[i]);
 }
}
</p>

首先进行C语言仿真验证,点这个按钮:

结果如下:

从C仿真输出看到,仿真结果与matlab计算结果一致,说明我们编写的C程序MatrixMultiply是正确的。

 

接下来进行综合,按C仿真后面那个三角形按钮,得到结果如下:

注意到,计算延迟为186个时钟周期。这是未经过优化的版本,记为版本1。

为了提高FPGA并行计算性能,我们接下来对它进行优化。

 

打开MatrixMultiply.c,点Directives页面,可以看到我们可以优化的对象。

 

 

注意到矩阵和向量相乘是双层for循环结构。我们先展开最内层for循环,步骤如下:

 

右键点击最内侧循环,右键,然后Insert Directive...

弹出对话框如下,Directives选择UNROLL,OK即可,后面所有都保持默认。

 

再次综合后,结果如下

可见效果非常明显,延迟缩短到51个时钟周期。

用同样方法,展开外层循环,综合后结果如下:

计算延迟又降低了1/3!!!

可是代价呢?细心的你可能发现占用资源情况发生了较大变化,DSP48E1由最初的4个变为8个后来又成为76个!!!

FPGA设计中,延迟的降低,即速度提高,必然会导致面积的增大!

 

循环展开是优化的一个角度,另一个角度是从资源出发进行优化。我们打开Analysis视图,如下所示:

 

从分析视图可以看出各个模块的运行顺序,从而为优化提供更为明确的指引。我们发现AA_load导致了延迟,如果所有AA的值都能一次性并行取出,势必会加快计算效率!

回到Synthetic视图,为AA增加Directives:

选择Resources,再点Cores后面的方框,进入Vivado HLS core选择对话框

按上图进行选择。使用ROM是因为在计算矩阵和向量相乘时,AA为常数。确认。

仍然选择AA,增加Directives,如下图:

选择数组分解,mode选择完全complete,综合后结果如下图:

延迟进一步降低,已经降到11个时钟周期了!!!是否已经达到极限了呢???

答案是否定的。我们进入Analysis视图,看一下还有哪些地方可以优化的。经过对比发现bb也需要分解,于是按照上面的方法对bb进行资源优化,也用ROM-2P类型,也做全分解,再次综合,结果如下:

 

 

发现延迟进一步降低到8个时钟周期了!!!

老师,能不能再给力点?

可以的!!!!

我们进入分析视图,发现cc这个回写的步骤阻塞了整体流程,于是我们将cc也进行上述资源优化,只不过资源类型要变为RAM_2P,因为它是需要写入的。综合结果:

 

 

整体延迟已经降低到6个clk周期了!!!

再看Analysis视图:

 

延迟已经被压缩到极限了。。。。

 

老师,还能再给力点嘛?

答案是可以的!!!!

我们前面的所有运算都是基于整形数int,如果将数值精度降低,将大大节省资源。

注意现在DSP48E1需要100个!

看我们如何将资源再降下来。这就需要借助“任意精度”数据类型了。

 

HLS中除了C中定义的char,shrot,int,long,long long 之外,还有任意bit长度的int类型。我们将代码开头的data_type定义改为:

#include <ap_cint.h>
typedef uint15 data_type;

 

由于matlab生成的随机数在1~100以内,乘积范围不会超过10000,于是取15bit就能满足要求。

 

首先验证下结果的正确性,用C Simulation试一下。结果如下:

 

看来结果是正确的(当然也不排除数位不够,溢出后的结果相减也是0,需要你自己决定数值位宽)

 

综合一下,结果如下:

 

延迟缩短了一半,DSP48E1减少到原来的1/4!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

 

我和我的小伙伴们都震惊了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

 

再看Analysis视图

 

 

可以发现我们的资源利用率已经达到极致,时序已经压缩到无以复加,实现了全并行计算,系统时钟完全可以达到100MHz,延迟仅3CLK,约30ns,相比matlab,得到约数百倍加速(matlab进行矩阵——向量相乘时采用浮点计算)。

 

通过本文实验,可以发现利用Vivado HLS实现从最初的C串行实现到全并行实现的步步优化,总结一下优化步骤:

(1)粗优化(循环展开、子函数内联)

(2)访存优化(块存储分散化、多端口存取)

(3)精优化(数值位宽优化、流水线优化)

(4)总线化(利用AXI4、AXI-Stream总线接口,降低整体访存需求)

利用HLS可以将原来的C算法快速部署到FPGA上,减少直接进行硬件编程的工作量。在很多情况下,优化手段可以和CUDA进行类比,相互借鉴。CUDA其实更接近软件接口,而HLS更接近硬件编程接口,或许今后两者会在新的层次上融合为统一架构语言。

时间: 2024-11-02 15:39:52

ZED Board从入门到精通系列(八)——Vivado HLS实现矩阵相乘的相关文章

ZED Board从入门到精通系列(七)——Vivado+SDK实现MP3播放

本文将给出通过Vivado IDE开发Zynq平台上PS裸机应用程序的流程.通过与本系列博客(三)对比,读者将看到Vivado开发更高效.快捷.   MP3我们都听过,现在我们可以用ZED-Board来听.板子上有音频芯片ADAU1761,可以实现录音.放音,但不具有MP3解码功能.Zynq 双核ARM9做MP3软件解码应该是可以实现的,但是博主本人有一颗VS1003,可以实现MP3硬件解码,软件将得以简化,对MP3解码原理感兴趣的可以深入研究如何利用CortexA9+ADAU1761实现MP3

ZED Board从入门到精通(三):从传统ARM开发到PS开发的转变

ARM已经在国内流行得一塌糊涂,各类教程.开发板(S3C2440,6410)层出不穷,归结下来,传统ARM开发包括以下几个步骤: (1)硬件电路板设计(对于Zedboard,相当于设计逻辑电路,PL工程师负责): (2)基本模块裸机代码测试(UART,DDR2,其他外设): (3)移植操作系统(如Linux,uCLinux,uCOS等): (4)编写相应操作系统的驱动程序(可从(2)中移植过来): (5)编写应用程序(或移植已有的应用程序).界面设计(Qt): 一个有ARM开发经验的工程师,接触

ZED Board从入门到精通(一):ZYNQ结构简介

ZYNQ-7000是第一代可扩展处理平台(Extensible Processing Platform,EPP),同时具有软件可编程.硬件可编程.IO可编程的特性,为此Xilinx强调了"All Programmable的"概念.下面对其做一简要介绍,便于读者建立初步框架. ZYNQ芯片内包含一个丰富特性的基于双核ARM Cortex-A9的处理子系统(Processing System,PS)和Xilinx 28nm可编程逻辑(Programmable Logic,PL).PS除了核

ZED Board从入门到精通(二):AXI简介

距离上次发帖时间有点长了,其实这段时间一直在思考. 市面上已经有专门讲ZYNQ的书籍了,我看过的有这两本. 这两本书怎么说呢,我觉得第二本更像是官方文档的堆砌吧(不喜勿喷),洋洋洒洒近600页,真正我想看的内容却少之又少.第一本书更适合入门(其实相当于傻瓜教程,你拿到书,拿到板子之后马上就能开始做实验),但语法错误.名词错误.软件版本不同造成的错误有很多,附加的光盘第一个实验内容就有错!有时辛辛苦苦搭建起来的环境,因为书上有错造成实验失败是很痛苦的,浪费感情. 建议先读一读第一本,对ZYNQ建立

MySoft.Data从入门到精通系列(五)【数据更新】

前一章讲了如何利用MySoft.Data进行数据的插入,利用DbSession可以实现各种数据增.删.改.查等各种复杂的处理,本章着重讲解一下数据的更新: 数据更新在日常开发中占据中非常重要的地位,尽次于查询.下面就讲解一下如何利用DbSession来进行数据的更新. 继续引用前面的DbSession配置,如下: /// <summary> /// 数据库访问类 /// </summary> public static class DataAccess { /// <summ

MSDN Webcast:ASP.NET MVC2程序开发入门到精通系列课程

课程讲师:苏鹏 MSDN特邀讲师北京工业大学软件工程硕士,微软最有价值专家(ASP.NET MVP),微软MSDN特约讲师.曾于微软亚洲工程院MSN组工作,现任中国网通四分公司技术支持与项目部开发经理.具有多年电信系统与OA系统实施经验. 课程下载: ASP.NET MVC2程序开发入门到精通系列课程(1):MVC架构概述 ASP.NET MVC2程序开发入门到精通系列课程(2):MVC范例分享 ASP.NET MVC2程序开发入门到精通系列课程(3):MVC中的View实现技巧(上) ASP.

一起谈.NET技术,MSDN Webcast:ASP.NET MVC2程序开发入门到精通系列课程

课程讲师:苏鹏 MSDN特邀讲师北京工业大学软件工程硕士,微软最有价值专家(ASP.NET MVP),微软MSDN特约讲师.曾于微软亚洲工程院MSN组工作,现任中国网通四分公司技术支持与项目部开发经理.具有多年电信系统与OA系统实施经验. 课程下载: ASP.NET MVC2程序开发入门到精通系列课程(1):MVC架构概述 ASP.NET MVC2程序开发入门到精通系列课程(2):MVC范例分享 ASP.NET MVC2程序开发入门到精通系列课程(3):MVC中的View实现技巧(上) ASP.

Hibernate从入门到精通(八)一对多单向关联映射

上次的博文Hibernate从入门到精通(七)多对一单向关联映射我们主要讲解了一下多对一单向关联映射, 这次我们继续讲解一下一对多单向映射. 一对多单向关联映射 在讲解一对多单向关联之前,按 照我们的惯例首先看一下其相应的类结构图和代码.具体如下: public class Classes { private int id; private String name; private Set students; public int getId() { return id; } public vo

Docker从入门到精通系列(1)---第一个web应用

本文为minimicall原创文章,转载需注明出处:http://blog.csdn.net/minimicall 在继<阿里云部署Docker>之后,我决定系统的出一系列<Docker从入门到精通>的深度记录文章,这源于有一天图灵出版社的一个编辑联系我,问我有没有兴趣写Docker方面的书籍进行出版. 本文的目标是建立一个Docker web app.这样,你就可以直观的感受,docker是如何部署一个web应用. 首先,我们建立一个空目录来存放我们需要的文件. 我们建立的是一个