Vivado HLS 程序优化

本文深入探讨了使用Vivado HLS进行高层次综合时的优化策略,包括如何通过流水线技术和指令指导改进矩阵乘法器设计,以实现每个时钟周期处理一个样本的目标。文章详细分析了循环和函数流水线的使用,以及如何解决循环依赖性和数据流限制,同时讨论了如何通过修改C源代码来消除意外依赖,以提升硬件性能。
摘要由CSDN通过智能技术生成

 前言(本文基于赛灵思官方HLS文档UG871中的7.1节):

          在使用高层次综合,创造高质量的RTL设计时,一个重要部分就是对C代码进行优化。

          Vivado HLS拥有自动优化的功能,试图最小化loop和function的latency,为了实现这一点,软件会在loop和function上并行执行尽可能多的操作(比如说,在function级别上,高级综合总是试图并行执行function)。 

          除了这些自动优化,我们可以手动进行程序优化,即用在不同的solution中添加不同的directive的方法,进行优化和性能对比。其中,对同一个工程,可以建立多个不同的solution(解决方案),为不同的solution添加directive可以达到如下目的: 

1、并行执行多个tasks,例如,同一个function的多次执行或同一loop的多次迭代。这是流水线结构。 

2、调整数组的物理实现((block RAM),函数,循环和端口(I/O),以提高数据的可用性,并帮助数据流更快地通过设计。 

3、提供关于数据dependency的信息,或者缺乏数据dependency,允许执行更多的优化。最终的优化是修改C源代码,以消除在代码中意外的dependency,但是这可能会限制硬件的性能。 

          本文使用的例子是设计一个矩阵相乘函数。 目标是在每一个时钟周期处理一个新的sample,并实现数据流接口。

一、优化matrix multiplier。

1、solution1(无优化)
        (1)矩阵乘法器设计,来显示如何优化基于loop的设计。设计目标是在每个时钟周期读取一个使用FIFO接口的sample,  同时最大限度地减少面积。此分析包括在loop级优化和在function级优化的方法。 

        (1)对比loops和function pipeline的使用,创建一个可以处理采样时钟的设计。

        (2)分析设计不符合性能要求的两个最常见的原因:loops dependency和数据流的限制(或瓶颈)。

Step 1: 创建并打开Project 
找到Design_Optimization lab1文件夹,依次在Command Prompt 窗口输入vivado_hls –f run_hls.tcl和vivado_hls –p matrixmul_prj

Step 2: 综合分析设计 
综合后的结果: 

这里写图片描述 
(1)图中,总的interval为80个时钟周期。因为每个输入数组中都有九个元素,所以设计每输入读取需要约九个周期; 
(2)interval比latency多一个时钟周期,所以没有在硬件上并行执行,即无流水;
(3)interval/latency的消耗主要是用于嵌套循环loops(本例有三种循环,每个循环均是迭代三次):
         1)Product inner loop: 
         ①有一个2个时钟周期的延迟。 
         ②总的迭代有6个时钟周期。 
         2)COL loop: 
        ①它需要1个时钟输入和1个时钟退出。  
        ②它需要8个时钟周期为每个迭代(1 + 6 + 1)。  
        ③总的有24个周期完成所有迭代。 
         3)顶层loops每次迭代需要26个时钟周期,总的loops迭代共78时钟周期。

       为了改善initiation interval,则需要加入流水:pipeline loops或pipeline整个function,并比较这两种结果。

      当pipelining loops时,loops的initiation interval是监控的重要度量指标。即使设计达到loop可以在每个时钟周期处理一个sample,函数的initiation interval仍然需要包含函数内的loops来完成所有数据的处理。

2、solution2:(Pipeline the Product Loop)

Step 1创建solution2,在Product loop下面插入pipeline directive(这里在Directive Editor下选择pipeline) 。

这里写图片描述 
        注意:当pipeline嵌套loop时,通过pipeline最内部Loop最大的好处就是,即有利于处理数据的sample。高级综合自动应用loop flattening,折叠嵌套loop,删除loop转换(本质上是创建一个更多迭代的单循环,但时钟周期整体较少)。

Step 2 综合设计到RTL级 
        在综合过程中,我们得到Console pane中报告的信息,显示loop flattening是loop Row上执行,默认内部Interval target为1由于依赖关系不能在loop Product上完成。 

这里写图片描述 
图中表明,虽然Product loop已经被pipeline,interval为2,但是顶层loop没有被pipeline。顶层loop不能pipeline的原因是,loop flattening只发生在loop Row,在loop Col 到Product loop上没有loop flattening。(下面解释loop flattening不能flatten所有nested loop的原因)

Step 3 打开Analysis窗口,选择state C1的write operation,右击选择Goto Source 

这里写图片描述 
        状态C1下的写操作是由于代码在Product loop前就已经设置res为0。因为res是在顶层函数,在RTL里这是在写入一个端口:这个操作必须发生在loop Product执行之前。因为它不是一个内部操作,而是会对I / O行为产生影响,这种操作不能移动或优化。这可以阻止Product loop被flatten进入row_col loop。

       更重要的是,对于Product loop来说,为什么只有II为2是可能flatten的?

       这个问题被称作carried dependency,这个dependency发生在一个loop的迭代操作和相同loop的不同迭代操作。例如,一个操作分别发生在K = 1时,当K = 2时(其中k是循环指数)。 

       第一个操作是在line 60数组res上的存储(内存读操作)。 
       第二个操作是在line 60数组res上的下载(内存写操作)。 
       从图中可以看到line 60是从数组res的读取(由于+=操作符)和写入数组res。数组默认映射到RAM block,下面Performance View的细节解释了为什么会发生这种冲突。

这里写图片描述 
       成功的schedule中,Product loop的下一次迭代如上所示。在这个schedule中,initiation interval(II)= 2,即loop操作每两个周期重启。任何block RAM之间的访问没有冲突。(没有突出显示的单元迭代重叠。) 

       不成功的schedule显示了为什么loop不能在II = 1时pipeline。此时,下一次迭代将需要1个时钟周期后开始。当第二次迭代尝试一个地址去读入时,第一次迭代中写入block RAM的操作仍然发生。

      这些地址是不同的,并且都不能在同一时间被应用到block RAM。 

       因此,你不能将Product loop的initiation interval 设置为1。下一步是pipeline Col loop。这将自动展开Product loop,并创建更多的operators,因此需要更多的硬件资源,但它确保在Product loop的不同迭代之间没有dependency。
3、solution3( Pipeline the Col Loop)

Step 1创建solution3,在Col Loop下面插入pipeline directive 

这里写图片描述

Step 2 Run C Synthesis 
这里写图片描述 
在综合过程中,在控制台窗格中报告的信息显示loop Product被展开,loop flattening是在loop Row上执行,默认initiation intervalv为1的目标不能在loop Row_Col上实现,这是由于数组a上memory资源的限制。

这里写图片描述 
        综合报告显示,如上所述,对loop Row_Col的interval只有2:目标是每个周期处理一个sample。你可以再一次使用Analysis窗口来证明为什么不实现initiation target。

Step 3 打开Analysis perspective,在Performance View里,展开Row_Col loop 

这里写图片描述 
        在数组a中有三个读操作。两个读操作开始于C1状态,第三个读操作开始与C2状态。数组被实现为block RAMs和数组,这是参数的函数是实现块内存端口。在这两种情况下,一个block RAM最大只能有两个端口(对于双端口block RAM来说)。访问数组a通过一个单一block RAM接口,没有足够的端口能够在一个时钟周期中读取所有三个值。 
另一种查看该资源限制的方法是使用到Resource窗口。

Step 4打开Resource tab,扩展Memory Ports 
        状态C1下面两个读操作与那些状态C2下的读操作重叠,因此,只有一个单一的周期是可见的:很显然该资源被用于在多个状态。即使当端口a的问题得到解决,相同的问题会发生在端口b:它也有三个读操作。 

这里写图片描述

        高级综合允许数组被partitioned, mapped together和 re-shaped。这些都允许在不改变源代码的情况下,对数组进行修改。

4、solution4(Reshape the Arrays)

Step 1 创建solution4,在Directive里给数组a和数组b插入ARRAY_RESHAPE,选择dimension分别为2和1 

这里写图片描述

Step 2 Run C Synthesis 
        综合报告显示顶层loop Row_Col现在是每个时钟周期处理数据的一个sample。 

这里写图片描述 
• 顶层模块需要12个时钟周期才能完成。 
• 经过3次循环,Row_Col loop输出sample(迭代延迟)。 
• 然后每个周期读取1个sample(Initiation Interval)。 
• 9次iterations/samples(Trip count)完成所有samples。 
• 3 + 9 = 12个时钟周期 
函数可以完成并返回开始处理下一组数据。现在,把block RAM接口设置为FIFO接口,数据流形式。
5、solution5(Apply FIFO Interfaces)

Step 1 创建solution5,在Directive里给数组a,b,res插入INTERFACE,在mode里选择ap_fifo 

这里写图片描述

Step 2 Run C Synthesis,Console窗口报错 
这里写图片描述
•在line57写[ 0 ] [ 0 ]。 
•然后在line60写[ 0 ] [ 0 ]。 
•然后在line60一个写[ 0 ] [ 0 ]。 
•然后在line60一个写[ 0 ] [ 0 ]。 
•在line57写入[ 0 ] [ 1 ](在增量指标J后)。 
•然后在line60写[ 0 ] [ 1 ]。

连续四个写入地址[ 0 ] [ 0 ]不能构成一个数据流模式,而是随机存取。 

这里写图片描述 

        检查代码后发现在数组a和数组b存在类似的问题。它是使用一个FIFO接口访问代码已经写好的数据,这是不可能实现的。在使用FIFO接口时,Vivado Hls的优化directives会有不足,因为当前代码执行了一定的读写顺序。

       下面pipeline整个function,对比这两种方法的差异。

6、solution6(Pipeline the Function)

Step 1 创建solution6,删掉loop Col下面的Directive,给matrixmul函数插入PIPELINE Directive 

这里写图片描述

Step 2 Run C Synthesis,并比较各个report 

这里写图片描述 
       solution6在较少的时钟就可以完成,并可以每5个时钟周期开始一个新的transaction。然而,消耗的资源也大幅增加,因为所有的循环在设计中被打开。

      Pipelining loops允许循环保持rolling,从而提供了一个很好的方法来控制area。当pipeline一个函数时,函数中包含的所有 loops都是打开,这是一个pipeline的要求。流水线功能设计可以每5个时钟周期处理一组(9个)新的samples。这超过了每个时钟处理1个sample的要求,因为高级综合的默认行为是产生一个最高性能的设计。pipeline function会产生最好的性能,然而,如果它超过所需性能,它可能需要多个额外的directives。

一、优化 I/O Accesses的C代码

进一步优化需要重新编写代码,下面介绍如何修改matrixmul.cpp的代码,来帮助克服一些在代码中固有的性能限制。 
Step 1 创建并打开Project 
找到Design_Optimization lab2文件夹,依次在Command Prompt 窗口输入vivado_hls –f run_hls.tcl和vivado_hls –p matrixmul_prj

Step 2 打开matrixmul.cpp源代码 

这里写图片描述 
审查代码并确认以下: 
•之前的directives在这里(包括FIFO接口)以pragmas形式指定的代码。 
• for循环已被添加到缓存行和列读取。 
•当最终的结果是计算为每个值时,一个临时变量被用于累计,并且端口res只能被写入。 
•因为对于for循环缓存行和列需要多个周期去执行读取,pipeline directive已应用于Col for循环,来确保这些缓存for循环自动打开。

Step 3 Run C Synthesis 

这里写图片描述 
该设计已被完全综合,每个时钟周期读取一个使用数据流FIFO接口的sample。

三、总结

      本文介绍了如何分析pipelined loops,并准确地理解哪些限制阻止优化目标的实现。以及对function 和 loop 进行pipeline的优点和缺点。代码中意外dependencies可以阻止硬件设计目标实现,如何通过修改源代码来克服它们。

######## 转载请注明出处 https://blog.csdn.net/gentleman_qin/article/details/80043789 ########

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值