HLS优化实战之数组

目录

引例

分析问题

优化思路

数组提高吞吐量

 数组提高利用率

结语


引例

        最近在开发一个HLS的算法,因为用的是ZYNQ7020,资源并不是很丰富,于是遇到了一个很大的问题:BRAM资源不够用了!

         我是找遍了百度和CSDN,但是大家清一色的都是在说一些什么:数组分割又映射,怎么配置啥的,而且,十篇博客有九篇用的图片都是某个视频截图。

        其实,这样做也没啥错,但我始终信奉“纸上得来终觉浅”,虽然你说的是对的,但你不亲自试验一样你怎么知道效果好不好呢?

分析问题

        还是老样子,咱又不是程咬金,咋能上去就干三板斧,是吧?我们看到很明显的,BRAM资源是超出了7020的280限制,为什么呢?首先我们需要展开Memory选项卡,进行问题分析。

        

        “罪魁祸首”找到了,原来是 我代码里定义的 一个8bit 的500x500的数组(Temp)、以及一个8bit的416x416的数组(Res)、还有一个32bit的参数数组,它们中的每个都被HLS分配了128个18K的BRAM,这样就直接导致了BRAM资源的不足,于是我们 就要想办法进行资源的优化。就本篇博客而言,主要集中在对RAM资源的优化上。

        在HLS中,函数的接口往往被综合成为协议,如ap_memory等,但是作为在代码内部的数组,它们往往会被综合成FPGA的RAM资源,如BRAM 和 LutRAM(LUT资源构成的RAM),另外还有URAM等等,其中的要义在于:

BRAM和URAM为专用的RAM资源,属于硬资源,而LUTRAM属于牺牲LUT资源换取存储空间的一种手段,一般一个N输入查找表能够存储2^{N}Bit数据。

优化思路

        HLS优化的两个关键性考虑因素是 面积速度。如果想要提高速度,那么相应的需要消耗更多的资源,反之也成立。有人会疑惑,既然这是个二选一的问题,为什么还需要我们人工进行优化约束呢?全让EDA软件自动执行不就行了,这里我们需要明确我们优化的目的:让综合软件朝着我们想要的方向进行综合,达到最佳性价比的策略。

         针对本节,我们的优化对象 数组也有两套综合策略

  • 提升吞吐量
  • 提升资源利用率

        针对本篇文章遇到的状况,我们需要着力于研究如何减少18KBRAM的使用,让我们的电路可以成功综合。

         下面我们先来介绍一下总体的综合策略,然后再进行实战的验证。

数组提高吞吐量

        为了数组RAM的吞吐量,我们常用的操作是:

  • 使用多口RAM,即使用RESOURCE指令(可以同时进行读写操作)
  • 进行数组分割,即使用ARRAY_PARTITION 指令(可以同时读取不同地址的资源)

对于使用多口RAM的思路,我们需要知道关于RAM的基础知识

  • 单口RAM 只有一个地址接口,同一时刻,只能选择读或者写。
  • 伪双口RAM 有两个地址接口,同一时刻,可以一个地址读,一个地址写,但不能同时读或者同时写。
  •  真双口RAM 有两个地址接口,完全独立。

        真双口好呀,但是资源消耗也大,所以我们应该根据实际的用途来决定选择使用哪个端口策略进行优化。

        例如一次计算需要分别读取一个数组的两个数据,或者写入两个数据这时候用真双口RAM就比较合适,反之,如果同时进行的只有一读一写,那么伪双端口将比真双口更加合适,或者读写不同时进行,那么就没有使用双口RAM的必要。

         有可能它们性能表现都是一样的,但是资源上将会有不同的消耗,这就是我们优化的终极目标:性价比

 数组提高利用率

        为了提高数组利用率,我们可以采用以下方法:

  • 算法优化,对数组进行复用。
  • 使用ARRAY_MAP指令,将多个杂散的数组合并成一个大数组。
  • 使用ARRAY_RESHAPE,扩大数组位宽。

        首先谈算法优化,由于HLS是使用C/C++等高级计算机语言进行开发的,因此很多初学者也喜欢带着从前写C/C++代码的“坏习惯”来写HLS,例如变量命名不规范,喜欢写一些复杂的逻辑,还有一个就是:喜欢随意定义数组/变量。造成这些问题的最根本原因就在于:

编写代码之前没有进行整体规划,写代码时缺少对整体的把握 

        举个栗子,例如我要写一个图像二值化的算法,IP核实现接收一个500x500x3的图像,处理输出一个 500x500的图像,入门的新手小白马上反手就定义了两个 500x500的数组,美名其曰:Gray_OUT and Binary_OUT,先灰度化嘛,然后再通过阈值对灰度化结果进行二值化嘛!当综合报告出来的时候傻眼了:两个数组就用了256个18KBRAM!其实我们完全可以吧二值化的数据再装回灰度化的数组里,这样只需要一个500x500的数组进行实现了。

        然后我们再谈ARRAY_MAP指令,即数组映射。

         在HLS中,两个不同命名的数组A和B,被综合出来原则上是两个不同的RAM,数组映射的功能就是将两个数组拼合成一个大的数组AB,里面的数据数量为N+M,见上图。

         初学者会有疑问:啊这,有啥用呀?我本来两个数组好好的,还可以同时异步读写,你这样干不是把数据吞吐率拉低了吗?雀食,用了MAP指令后数据吞吐率确实会降低

        但是,效果也是很明显的,那就是节省资源利用率。还是举个栗子,例如A数组存储100Bit的数据,B数组存储200Bit的数据,如果俩数组都用BRAM进行综合,那么需要消耗两个18KBRAM,没办法,BRAM最小单位就是1个,我总不能切成两瓣吧?但是一个18K的BRAM就可以存储18x1024bit的数据!这时我们如果使用MAP,那么18K的BRAM只会用到一个,这将大大提高资源的利用率!

        当然了,18KBRAM可不是这么好对付的,需要对深度和位宽进行综合考量,然后最重要的是要顺应18KBRAM的综合策略,这个我们下节实战再讲。

        需要注意的一点是,千万不要用ARRAY_MAP合并两个不同位宽的数组,综合软件会将RAM数据宽度设置为位宽较大的数组,然后起到适得其反的效果。

        笔者将上述的Temp数组(8bit)和一个32bit位宽的小数组进行合并,结果给爷整笑了:居然用了一千多个18KBRAM !

         ARRAY_RESHAPE指令类似于PARTITION和MAP的合体,但我觉得更像PARTITION

         因为从本质上来说,它是对一个数组进行操作,和PARTION进一出N和MAP进N出一不同,RESHAPE是进一出一,就像橡皮泥一样,体积不变,形状改变。

换句话说就是:

数据bit数不变,位宽和地址改变。

        你看如果把两个8bit数据合并成一个16bit的数据,读一回数据的时间就相当于读了两个数据出来,如果这时我们的算法恰好是要一次性读取两个数据之类的,RESHAPE指令就能很好地满足我们的需求。

结语

        上篇就这样结束了,主要讲述一些理论性的知识,让大家能够在实战中体会到数组优化的威力,跑去了诸多小细节,例如各个指令怎么配置,各个指令的格式之类的。正所谓,大道至简,HLS其实重在学会解决问题的方法,而不是填鸭式第学习怎么插入指令,机械式地去套用优化模板,我们行动之前一定要先思考。

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
在 Vivado HLS 中,可以使用数据流的方式输出数组,具体步骤如下: 1. 在函数原型中定义输出数组的指针类型和数组大小,例如: ```c++ void function_name(data_type* output_array, int array_size); ``` 2. 在函数内部,使用 `#pragma HLS INTERFACE s_axilite port=output_array bundle=control` 指令将输出数组标记为数据流接口,并指定它属于哪个 AXI4-Lite 控制总线。 3. 在输出结果之前,使用 `#pragma HLS STREAM variable=output_array depth=depth_value` 指令将输出数组标记为数据流变量,并设置数据流深度。 4. 在函数结束前,使用 `#pragma HLS INTERFACE ap_ctrl_none port=return` 指令将函数返回值标记为无控制端口。 下面是一个示例代码: ```c++ #include <hls_stream.h> void function_name(data_type* output_array, int array_size) { #pragma HLS INTERFACE s_axilite port=output_array bundle=control #pragma HLS INTERFACE s_axilite port=array_size bundle=control #pragma HLS INTERFACE ap_ctrl_none port=return #pragma HLS STREAM variable=output_array depth=10 hls::stream<data_type> output_stream("output_stream"); // 数据流逻辑 for (int i = 0; i < array_size; i++) { data_type output_data = ...; output_stream.write(output_data); } // 从数据流中读取数据到输出数组 for (int i = 0; i < array_size; i++) { output_array[i] = output_stream.read(); } } ``` 在这个示例代码中,我们使用了 `hls::stream` 类型来实现数据流输出。我们首先将输出数组标记为数据流接口,并使用 `#pragma HLS STREAM` 指令将其标记为数据流变量。然后,我们使用 `hls::stream` 类型创建了一个输出数据流 `output_stream`,并在数据流逻辑中将计算结果写入数据流。最后,我们从数据流中读取数据到输出数组中,并将函数返回值标记为无控制端口。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值