前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >FPGA Xilinx Zynq 系列(二十八)Vivado HLS: 近视 之 算法综合

FPGA Xilinx Zynq 系列(二十八)Vivado HLS: 近视 之 算法综合

作者头像
FPGA技术江湖
发布2020-12-30 11:14:32
1.3K0
发布2020-12-30 11:14:32
举报
文章被收录于专栏:FPGA技术江湖
大侠好,欢迎来到FPGA技术江湖,江湖偌大,相见即是缘分。大侠可以关注FPGA技术江湖,在“闯荡江湖”、"行侠仗义"栏里获取其他感兴趣的资源,或者一起煮酒言欢。

今天给大侠带来FPGA Xilinx Zynq 系列第二十八篇,讲述Vivado HLS: 近视之算法综合等相关内容,本篇内容目录简介如下:

15. Vivado HLS: 近视

15.5 算法综合

15.5.1 实现的度量指标和约束

15.5.2 数据类型

15.5.3 流水线

15.5.4 数据流

15.5.5 算法例子研究:循环

15.5.6 数组

15.6 设计评估和优化

15.6.1 设计约束

15.6.2 合成指令

15.6.3 统计报告

15.6.4 设计迭代和优化

15.7 从 Vivado HLS 导出

15.7.1 Vivado IP Catalog (IP-XACT 格式)

15.7.2 DSP 的 System Generator

15.7.3 XPS 的 pcore

15.8 本章回顾

15.9 参考文献

本系列分享来源于《The Zynq Book》,Louise H. Crockett, Ross A. Elliot,Martin A. Enderwitz, Robert W. Stewart. L. H. Crockett, R. A. Elliot, M. A. Enderwitz and R. W. Stewart, The Zynq Book: Embedded Processing with the ARM Cortex-A9 on the Xilinx Zynq-7000 All Programmable SoC, First Edition, Strathclyde Academic Media, 2016。

Vivado HLS: 近视

15.5 算法综合

这一节的主题是从所设计的 C/C++/SystemC 代码来做功能性硬件的综合。由于本章的篇幅,无法详细对待所有的内容,所以我们只关注几个有重点兴趣、在用Vivado HLS 成功开发系统的设计方法中有代表性的话题。换句话说,本节接下去的内容应该被看作是对 HLS 的算法综合过程和可能性的 “ 浅尝 ”,而不是完整的指南。进一步的延伸内容,在 Xilinx User Guide 902, “Vivado Design Suite User Guide: High-Level Synthesis” [18] 中可以找到。

这一节一个特别的目的,是着重在于说明设计者可以做的控制,这种控制是指他可以通过使用指令来施加在最终综合出来的硬件实现上的控制;以及他有可能不对源代码做大幅度的修改就能现成地产生和比较各种方案。我们用循环的综合的情景研究来说明这个问题。

本章接下去的部分,会考虑精选的不同风格的 C 的设计中得到的算法综合和一些具有展示性的例子。不过,在那之前,重要的是识别出用来评估和约束 VivadoHLS 设计的通用的度量指标。

15.5.1 实现的度量指标和约束

由于是由 HLS 把 C/C++/SystemC 函数所描述的算法转换成硬件的,就需要有测 量数据来衡量结果实现的特征。尤其在比较不同的实现,也就是通过给 HLS 过程施加不同的指令形成一组 Vivado HLS“ 解决方案 ” 的时候很有用。

基于实现度量的限制,可以由用户输入,并作为设计约束来起作用。它们能影响 Vivado HLS 在执行高层综合时的作为:HLS 要尽可能地满足目标集,而如果无法满足,也要努力产生一个 “ 最佳效果 ” 的 RTL 设计。本节下面的部分用来定义各种可以用于基准测试 (benchmark)或 HLS 设计约束的度量指标。

面积 / 资源

对于一个实现最有意义的指标是构建电路所需的硬件成本,以 FPGA 或 PL 上的资源 (等价来说就是 “ 面积 ”)来计量。由于一个特定的目标芯片上的资源从根本上来说是固定的,在满足系统整体需求和条件的前提下,就有动力去最小化某个Vivado HLS 部件的成本。

默认地,Vivado HLS 会寻求面积的最小化,也就暗示会采用硬件的时分复用技术。这通常会导致延迟上升和吞吐率下降 (这两个指标下面定义)。

时钟周期、时钟速率和时钟不确定度

时钟周期(Clock Period)指标指的是最小周期,也就是一个设计所能支持的最大时钟频率的倒数。这是目标芯片的物理特性的函数,也是 HLS 所综合出来的 RTL设计的关键路径。这个关键路径被定义为 “两个时序逻辑单元之间最长的组合逻辑路径 ”,它直接限制了最高的时钟频率。关键路径一般是在硬件设计中通过流水线技术(也就是策略性地插入寄存器)来管理的,并以类似的方式受到到 Vivado HLS 中对流水线的使用情况的影响。

Vivado HLS 会提示用户指定目标时钟周期和时钟不确定度,这些因素合起来成为时序约束。只要有可能,HLS 工具会基于 Xilinx 技术库的数据,产生出满足目标减去不确定度值的设计。引入不确定指标是为了覆盖HLS阶段所不知道的其他因素,特别是 RTL 综合、布局和走线延迟 [18]。

延迟

在 Vivado HLS 中,“ 延迟 (latency)” 这个术语采用的是它的一般定义, 也就是在给出输入,到获得对应的输出之间的时钟周期。延迟可以在层次结构中的不同层级上加以检验,从顶层函数到子函数,到循环或代码的指定段都可以。就循环而言,延迟指的是循环的每一轮都完成了,迭代延迟(每一轮的延迟,iterationlatency)这个术语是用来指循环的一轮的延迟。总的延迟等价于迭代延迟乘以循环的次数。

延迟也可以被指定为一个设计约束,也就是由用户定义最大可接受延迟,然后 Vivado HLS 工具优化设计 (如果可能)来满足这个需求。

初始间隔和吞吐率

迭代间隔 (Iteration Interval,II)是 Vivado HLS 设计的能接受的相邻两次输入之间的时钟周期数。没有指令的话,初始的间隔和延迟可能是相同的,因为Vivado HLS 默认的做法是为面积做优化,导致的结果就是一个串行的设计。不过,有策略地运用流水线可以降低迭代间隔到比设计的延迟小很多的程度。另一方面,这样也可能增加设计所用的面积,所以这里存在着权衡的问题。

初始的间隔是直接对应着吞吐率的,就像时钟周期和时钟频率的关系一样。吞吐率表达的是数据流经系统的速度。能达到的最好的初始间隔是 1,就是说每一个时钟周期可以接受一个新的输入数据,这样的话,这里的吞吐率就等同于时钟速率了。有时候,用了部分循环展开或复制一个已综合的函数,能实现更高程度的吞吐率。

15.5.2 数据类型

和接口综合一样,在高层综合中,数据类型的选择对于综合产生的硬件具有基础性的影响。使用比所需更长的字长,从用来创建设计所需的 PL 资源的角度来看,会导致不必要的昂贵的实现。它还可能潜在地影响其他实现度量指标,比如延迟、最大时钟频率和初始间隔。

正如在 15.3 节中所总结的,使用任意精度数据类型,是指定恰当的字长的有效手段,从而能为实现高效的整体实现做出贡献。

15.5.3 流水线

在硬件设计中会广泛使用 “ 流水线 (pipeline)” 这个术语来表达在电路中插入寄存器,以最小化关键路径(也就是时序逻辑单元之间最长的组合逻辑路径),从而最大化能实现的时钟频率。因此,我们可以认为数据输入沿着处理路径上的普通的同步的方式移动,而中间数据则保存在流水线寄存器里。或者也可以说,数据是 “ 在流水线里 ” 的。

当这个术语用来表示处理器操作的时候,它的意思是一个任务被分解成既定结构的一些子任务,每个子任务可以由处理器的不同的 “ 流水线级 ” 同时完成。流水线级的数量,以及每个级要做的操作,是处理器架构的固定的功能。比如,一个5 级流水线可以让处理器同时做:(一)取指令;(二)指令译码;(三)读数据;(四)执行和(五)写回结果,所有都在同一个时钟周期内完成,每一个操作有专门的流水线级来做。每个流水线级做连续的数据,所以每个时钟周期可以产生一个新的输出。

在 HLS 中,流水线的含义和以上两种都有联系,但是都有所不同。具体来说, 我们可以定义流水线的概念为把一个任务划分为多个子阶段,每个阶段是一个有依赖的操作的组,这些操作可以是组合逻辑的也可以是时序逻辑的。

为了进一步定义 HLS 的流水线,我们必须抽象掉底层操作的细节,认为流水线是和逻辑处理阶段的段相关的。和硬件的情况不同,这些段不一定要是物理的组合处理单元 (尽管可以是)。引入流水线是为了实现操作的重叠,和处理器里的流水线的概念是类似的。不过,和具有固定架构的处理器不同,在操作本身上没有限制,因为 FPGA 部分或 Zynq 的 PL 提供的是空白的画布,在那上面可以实现任意的功能。

采用流水线的动机,是有机会来实现并行计算,从而提升设计所支持的吞吐率。流水线可以作为一个指令在 Vivado HLS 中,在函数和循环的层面上施加。下面 几页,我们要考虑操作的流水线,而循环的流水线会在 15.5.5 节中涉及。

算法执行和数据依赖

可以假设,软件所表达的任何算法,都包含一组功能性步骤,或者说运算。每 一个步骤依赖于一组特定的数据就绪来作为输入,而在某些情况下,这些数据可能还需要经过一些预先的步骤才能就绪。因此,在构成算法的各个步骤之间,就隐含存在着数据依赖关系,并且需要按照一定的顺序来执行。

那么,这个算法的一个直接合成就会产生一组在逻辑上必须同时发生的运算, 因为这些运算之间存在数据依赖关系。换句话说,所有的运算都属于同一个处理阶段,它们必须全部完成,然后才能处理新的输入。无论这个阶段的执行是需要单个时钟周期还是多个时钟周期,重点是除非之前的计算全部完成,否则下一个输出是没有机会开始计算的。

作为一个简单的例子,考虑图 15.16 所给的函数,它由三个处理步骤组成:Op1、 Op2 和 Op3。第二个步骤 Op2,依赖于第一个步骤 Op1 的输出,而 Op3 则依赖于 Op2的输出。因为这样,再加上还没有存储器来保存中间结果,于是运算之间就存在着依赖:最后的输出只有当所有的运算都完成 (依次)之后才会产生。因此我们标记Op1、Op2 和 Op3 合起来作为一个处理阶段。

图 15.16: 存在运算之间的数据依赖的函数的例子

从处理数据的角度来说,一个阶段内的数据依赖意味着一次只能输出一个数据,这里的三个运算必须相继运行每一组输入数据,只有当当前的输出好了,才能接受下一个数据。因此数据样版周期就等于 Op1、Op2 和 Op3 总的处理时间,从而直接决定了设计所支持的数据吞吐率。另一个重要的指标:输入到输出的延迟,就等价于三个运算的延迟的和。这两者都在图 15.17 中以波形的形式表达出来了。

也许你已经发现,图 15.17 中没有画时钟信号,这是有意为之的。在抽象的层面上,Op1、Op2 和 Op3 可能代表组合路径上的逻辑,也可能代表一个时序运算。实际上,因为流水线具有隔离阶段的效果,这就会导致吞吐率的提升,而无论电路本身是时序的还是组合逻辑的。

图 15.17: 没有流水线时的吞吐率和延迟

算法的流水线操作

流水线就意味着处理过程被分割成较小的阶段,每个阶段可以同时处理不同的数据。换句话说,导致一组运算被捆绑在一起的那些数据依赖被打开了,这样这些运算就可以并行执行了。

从硬件来说,实现这样的阶段分离,所用的方式就是在新的、较小的阶段之间插入寄存器,这样数据就能被保存在这些寄存器里了。这样做,一个直接的后果就是由于存在采样周期 (数据进出寄存器所需的时间,这是一个缺点),整体的从输入到输出的延迟变大了,但是由于每个阶段都不长,所以采样周期是可以被降低的。后面这一点直接影响了吞吐率,而吞吐率通常被认为是更为重要的性能指标。进一步的好处是,原来的处理过程代表了一个组合逻辑路径,而插入流水线寄存器可能还会导致最大支持的时钟频率的提高。

从面向硬件的角度看,流水线函数和图 15.18 所示的信号流图是等价的。和之前的场景一样,最终的输出是依赖于经由 Op1、Op2 和 Op3 一路传输而来的信号,但是现在,图中有存储器来保存中间结果了。结果就是三个运算器现在就可以同时自由地运算了,每个运算器各自对相继的三个输入数据做运算,如图 15.19 所示。这样就去掉了运算之间的数据依赖性,而每一个运算器可以被认为代表了一个独立的处理阶段。

图 15.18: 经由流水线把运算划分成独立的阶段

新的波形图表现了较小的阶段 (每个实际上就是单个运算),而且还确认了相应的数据采样周期的缩短。这就意味着吞吐率得到了三倍的提升。而且显然这些运算器可以同时计算相继的数据的结果,比如,Op1 可以在计算完了第 k 个数据并传递给 Op2 之后,立刻就计算第 k+1 个数据。由于插入的流水线寄存器的输入和输出之间有额外的延时,每个阶段所需的时间有所延长,因此现在的延迟会变大,但这个是可以接受的额外开销。

图 15.19: 有了流水线之后的表达吞吐率和延迟的波形

15.5.4 数据流

按照上一节的讨论,流水线是一种提高由软件描述所产生的硬件的并行性,从而改善吞吐率的方法。流水线可以在函数内的一个代码段或一个循环的层面上做。

数据流流水线 (或直接简单说 “ 数据流 ”)优化是类似的概念,只是它作用于设计层次中更高的层级上:它是用来改善函数之间的并行性的。比如,一个顶层设计可能包含四个函数:F1、F2、F3 和 F4,其间是存在数据依赖的。对这个顶层函数的直接综合会形成子函数依次执行、没有重叠的情况,就和图 15.16 中所描绘的运算的合并成组 (就是被标记为一个处理阶段)是类似的。有了数据流流水线之后,就能做和流水线等价的优化了,就是说在函数之间插入寄存器来把这些函数划分成分离的可以并行执行的阶段。

鉴于函数比单个操作(在流水线里已经讨论过了)要复杂不少,数据流优化实际上要更进一步,要分析函数的内容和函数之间的依赖性。这样也许能在函数间实现更深的 “ 重叠 ” 度,从而能缩短整体的执行延迟并提升吞吐率。比如,也许一旦 F1 函数完成了 50%,就开始 F2 函数,而不是等待 F1 整个完成。这很容易用例子来说明,下面我们就在一个日常场景中做相应的数据流优化。

假设我们要去咖啡馆,那里只有一个人服务,我们叫他 Penelope。Penelope 要做多个工作 (函数)来服务你:(一)问候你然后拿到你点的单;(二)准备好你要的食物;(三)做你的咖啡然后 (四)收款。Penelope 一次只能服务一个人,依次把上述任务做一遍,也就是说,延迟 (就是服务一个顾客的总的时间)和吞吐率 (服务顾客的速度)是有限的。

为了说明问题,假设定义以下的函数:

  • Function F1: 点单 3 个时间单位
  • Function F2: 准备食物 2 个时间单位
  • Function F3: 准备咖啡 4 个时间单位
  • Function F4: 收款 3 个时间单位

这样的话,就要花 Penelope 总共 12 个时间单位来服务每一位顾客。她一次只能服务一位顾客,所以顾客的吞吐率是每 12 个时间单位 1 位,而每一位顾客必须等待 12 个时间单位才能得到要购买的东西。

增加更多的职员可以改善这个情况。假设 Penelope 现在有 Cameron、Hamish 和 Isla 帮忙。Cameron 会问候顾客然后得到订单,Hamish 会根据订单准备食物,Isla会收银,而 Penelope 会做咖啡。不仅是顾客的吞吐率会提升,延迟也会降低,因为每一位要做的工作是可以重叠的。比如,Cameron 可以首先问饮料的单 (这样Penelope 就可以开始做事了),然后问食物的单 (这样 Hamish 就可以开始准备食物),这样的话,Penelope 的工作是在 Cameron 完成之前就开始了的。类似的,Isla也能在订单一下好就立刻做她的收银的工作,而那时候 Penelope 和 Hamish 还没做完她们的服务工作呢。

图 15.20 和图 15.21 描绘了咖啡店的这两个不同的场景。原本,依图 15.20 所 示,每 12 个时间单位 (类似于时钟周期)只有一位顾客能得到服务,顾客还必须等待 12个时间单位来完成他们的交易。这是因为 Penelope必须亲自做每一件事情,一次只能完成一个任务 (或 “ 工作 ”)。

图 15.20: 没有数据流优化的咖啡店的例子

注意到,加入更多的职员之后 (如图 15.21 所示),每四个时间单位就可以服务一位顾客了,而不是每 12 个时间单位。另外还降低了每个顾客需要等待的时间,从 12 个单位降到了 4 个。吞吐率上的改善是由于并行性的提升 (现在不同的工作可以同时做),而延迟的降低 (一位顾客必须等待的时间)则受益于工作的重叠。只是增加更多的职员,但是还是让她们每一位依次从事所有的工作(就像 Penelope原本所做的那样),那么还是会让 Bob、Bert 和朋友们等候 12 个时间单位才能得到所下单的东西的!

图 15.21: 进行数据流优化的咖啡店例子

在图 15.21 中我们注意到,吞吐率是受 Penelope 的做咖啡的工作限制的,这个工作要 4 个时间单位,而其他所有的工作都只需更少的时间就可以完成。如果在这个工作中也能加入流水线,比如使用一个或更多的咖啡师来并行地做咖啡原液、打奶等等,那么就可能实现进一步的改善。

硬件电路的设计也是类似的,加入更多的并行性(就像是雇佣更多的职员)能改善性能。显然这不是 “ 免费 ” 的,因为职员是要付薪水的 (!),但是为了获得性能的改善,也许这是值得的。

数据流优化的使用可以以类似的方式通过指令作用在流水线上。正如我们在这个例子中所看到的,数据流优化的动机是从并行处理中提升可能获得的吞吐率,同时通过操作的重叠来降低延迟。

15.5.5 算法例子研究:循环

在软件编程中大量使用循环,这形成了表达以某种方式重复的运算的非常简洁和自然的方法。在 HLS 里也是以类似的方式在使用循环,比如枚举实例化和连接电路元件。不过,重要的不同是,在 Vivado HLS 里,设计者可以通过指令的机制来要 求循环以不同的方式被综合。这和 HDL 里的循环的使用正相反,在 HDL 里,表达循环的代码被直接转换成硬件,通常会形成预设的固定的架构。

作为一种重要的软件结构,Vivado HLS 很好地支持了循环的硬件综合。通过指令可以做几种循环优化,无需或只需很少的软件代码的修改,就能改变实现出来的结果的架构。在本节剩下的部分,我们会考虑循环的默认综合方式,及可以通过指令来实现的架构变化。我们选择了简单的代码例子来使得必要的 HLS 相关的重点更清晰。

默认的循环综合

默认地,Vivado HLS 是对面积做优化的,因此除非设计者直接指定,否则循环会自动地 “ 滚动 ” (不展开),就是说会时分复用一组最少的硬件。这就意味着循环所描述的重复性的计算会以单片实现循环体的硬件来实现。作为一个简单的展示性的例子,假如设计一个循环来计算两个各含有 12 单元数组的每个单元的和,那么理论上会用到单个加法器 (循环体)来实现,然后这个加法器根据循环迭代的次数被共用 12 次。

循环的每一轮都会有一定的延迟,在这个例子中,延迟是由函数输入输出时存储器接口的交互所形成的。根据 15.4.3 和 15.4.4 节所讨论的默认接口协议,存储器接口是因为用了数组参数而被调用的。另外进入和离开循环还需要额外的时钟周期。

图 15.22 给出了这个例子的代码。对采用默认设置的 HLS 综合的分析表明,整体的延迟是 26 个时钟周期:12 次迭代每次 2 个周期 (包括从存储器读输入、做加法和把输出写回存储器),而另两个时钟周期是用于进入和离开循环。图 15.23 表达的是这个循环的执行情况。

图 15.22: 两个数组的单元相加的循环的例子代码

图 15.23: 从加法循环中解析出数据通路和控制逻辑

简单循环架构的变化

默认的滚动的循环实现也许不会总是符合期望的,不过还有其他方式。可以用指令来指定实现的架构,具体总结如下:

展开的 — 在不展开的实现中,根据循环体产生了硬件上的单个实例,这个实例要得到最大程度的共享。展开的循环意味着从循环体所产生的硬件要创建 N次,这里 N 就是循环迭代的次数。实际上,如果设计中还具有其他限制因素,如寄存器的运行,这个实例的数量可能少于 N。显然这个展开的版本的缺点是比不展开的设计要消耗芯片上多得多的面积,但是优点是提升了吞吐率。

部分展开 — 这是在完全展开和完全不展开两种实现版本之间的权衡,通常当不展开的实现不能达到足够高的吞吐率的时候被使用。如果说不展开的架构代表的是最小的硬件成本但是最大的时间共享(最低的吞吐率),而展开的架构代表的是最大的硬件成本但是最小的共享(最高的吞吐率),那么我们也许可以试试在这两者之间找到一个不同的平衡。通过施加指令做控制,各种不同的权衡也许是可能。

以图 15.23 的上半部分而言,这一部分描绘的是一个不展开的架构,完全或部分的展开循环会导致数据通路资源 (加法器)的数量增加,但是需要共享的程度减少。与此同时,在此图的下半部分,那个大型的中央的对数组元素进行加法运算的部分会需要更少的时钟周期来完成。当完全展开的时候,这个实现实际上就没有循环了,那么进入和离开循环的时钟周期也被省下了。

基于这些观察,决定选择循环的不展开、展开还是部分展开的实现就是基于应用的特定的需求的了,特别是要考虑目标的吞吐率和对面积利用率的约束这些指标。

优化:合并循环

某些情况下,代码可能有前后相继的两个循环。比如,图 15.22 里的加法循环后面可能跟了一个类似的循环,要对两个数组的元素做乘法。假设两个循环都是不展开的 (采用默认的模式),这种情况下一个可能的优化就是合并两个循环,这样就只有一个循环了,在这个循环体内既做加法也做乘法。

循环合并的好处也许不是立即显现的,但是它和设计的控制因素有关(还记得在 14.4.5 节里,作为 HLS 过程的一部分,C 源码会被分析、分解为数据通路和控制部件)。控制是以有限状态机(Finite State Machine,FSM)的形式实现的,每一 个循环对应着至少一个状态。因此在循环合并后,FSM 可以得到简化,因为总体上的循环减少了,所以 FSM 的状态也就减少了。图 15.24 和图 15.25 中的代码例子就能说明这个事情。第一个例子里有两个独立的循环,一个做数组的加法而另一个做数组的乘法,而第二个例子是把这两个循环合并成一个循环的效果。

add_loop 里有 12 次加法运算 (要两个时钟周期)的迭代,而 mult_loop 里有 12 次乘法运算 (要 4 个时钟周期)。因此,这两个循环总的延迟分别是 24 和 48 个个时钟周期。把两个循环合并的效果就是新结合起来的循环的延迟降低到了原本两个循环的最大的那个,也就是 48 个时钟周期。由于去掉了循环转移,因此还多消除了一个时钟周期,就是图 15.24 里的 “ 进入 / 离开 ” 状态。

用 Vivado HLS 指令可以自动地控制循环的合并,因此就不需要对源码做直接的修改了。图 15.25 里的代码只是为了演示的需要,就是为了说明 “ 合并(merge)”这个指令施加以后的效果的。

图 15.24: 在一个函数内的相继的两个做加法和乘法的循环

图 15.25: 合并后的加法和乘法的循环

要注意由于存在兼容性和要合并的循环的次数的限制,对于循环合并还是有一些实际的限制的。在我们的简单的例子中,两个循环的次数是一样的,但是情况并非总是如此。关于这个问题的指引,在 [18] 中可以找到。

嵌套的循环

另一个常见的情况是嵌套的循环,也就是在一个循环内部放另一个,而且嵌套的层次还可以是多层的。作为一个两层嵌套的例子,假设我们把数组加法的例子从线性数组扩展到 2 维数组。从数学上来说,这和做两个矩阵的加法是等价的,如公式 (1):

现在,为了加这两个数组,我们必须枚举所有的行,然后对于每一行,要枚举所有的列,把每个数组对应元素的两个值加起来。写出这个矩阵加法运算的程序,分别需要一个外部的和一个内部的循环来相应地遍历行和列,如图15.26里的代码。根据公式 (1),有 3 行 4 列数据,这就决定了嵌套的循环的次数 (注意下标是从零开始的,这是通行用的做法)。

进一步的,这个思路也可以作用于三维的数组,甚至更高的维度,只要增加循环结构中的嵌套层次就可以了。

图 15.26: 嵌套地做二维数组加法的循环

优化:循环扁平化

遇到嵌套的循环的时候,我们可以做 “ 扁平化 (flattening)”。这意味着在高层综合的时候,循环的递进层次实际上会被消除掉,但是算法 —— 也就是循环所作的运算 —— 会被保留。扁平化的好处和合并是类似的:与进入或离开循环的转换相关的额外的时钟周期被避免了,就意味着算法执行所经过的总体时间就减少了,从而改善了所获得的吞吐率。

为了深入解释扁平化,有必要澄清循环和循环体这两个术语。对于循环,我们指的是整个代码结构,那个结构里的一组语句会重复确定的次数。在循环中的语句,就是要重复的那些语句,就是循环体。比如,column_loop 是一个循环,而在column_loop 里的语句就是对应的循环体。

当循环嵌套的时候,还是以两层嵌套的结构作为例子,外面的循环体包含了另 一个循环,也就是那个内部的循环。外面的循环体 (包括了那个内部的循环)要执行一定的次数,比如图 15.25 的例子里的 row_loop 要重复 3 次,因此内部的循环column_loop 要执行 3 次。内部循环的每次执行,要重复内部循环的循环体一定的次数,同样的,在我们的例子中,矩阵单元 f[j][k] 的计算要执行 4 次,这里 j 是行的下标而 k 是列的下标。

进入和离开循环的额外开销意味着每次执行内部循环,就需要额外的两个时钟周期,其中一个用于进入内部循环,而另一个用于离开。

为了澄清这一点,图 15.27 描绘了我们的矩阵加法例子的控制流及相关的时钟周期。这里表达的是原本的循环结构。扁平化的过程就 “ 解开 ” 了内部的循环,从而降低了与进入和离开循环相关的时钟周期的数量。具体来说,就是图 15.27 中的 “enter_inner(进入 inner)” 和 “exit_inner(离开 inner)” 两个状态被取消了。这两个状态原本会重复 3 次,所以这个例子里就有总共 6 个时钟周期被省下来了。

在我们简单的 3x4 矩阵加法例子中,省下来的相当于 6 个时钟周期,不过在其他的例子中,这可能会相当地高(特别是外部循环要迭代很多次的场合,或是层叠的层次很多的时候),因此循环的扁平化显然是有明确的需求的。和循环的合并类似,扁平化可以经由指令来实现,而不会牵涉到手工修改代码来直接解开循环。不过,对于某些形式的代码,可能还是需要一些人工的重新安排才能实现更好的扁平化的循环结构 [18]。

图 15.27: 矩阵加法中的控制流,没有做扁平化 (圆圈中的数字是时钟周期)

循环流水化

对用 C 写的循环的直接解析,就是让循环体的执行是相继进行的,也就是说, 循环的每一轮不能在前一轮结束之前就开始。从硬件的角度看,这样就会翻译出单组硬件(就是从循环体推导出的硬件),在任一时刻,它只能执行循环的一轮迭代,这个硬件会在时间上被共享若干次,这个次数则就是循环迭代的次数。

这和我们之前在 15.5.3节关于流水线的讨论很像。在那里我们发现如果一组运算被合并成一个处理阶段的话,吞吐率就会受到限制。在循环而言,循环体 (就是那组重复的计算)形成了这样的一个阶段,没有流水线的话,就会让所有阶段的计算以队列的方式进行,而且在这样的队列中,所有的运算的执行也是以队列的方式进行的。实际上,循环体的所有迭代的所有运算,是一个接着一个发生的,就和第 314页上的图 15.17 是类似的。因此要完成循环的执行所需的总的时钟周期数 , 就是:

这里 是循环迭代的次数 , 是执行循环体内所有运算所需的时钟周期 , 并且表示进入和离开循环所需的额外开销。

在循环中插入流水线意味着循环体内要有寄存器来隔离所实现的运算。鉴于循环体要重复多次,这样做就表示了在循环的第 j+1 次迭代中的运算可以在第 j 次迭代完成之前就开始。实际上,在任一瞬间,对应多个不同迭代的运算可能同时在进行。

由于将循环流水化了,用来实现循环体的硬件就能更充分地被利用,从而循环的性能,以吞吐率和延迟来说就都得到了改善。因此加入流水线这条指令的效果是可观的,尤其是当循环体内存在多个运算和当循环要做很多轮迭代的时候。

对于嵌套的数组,需要去考虑在哪个层叠的层次上做流水线。在层叠的某个层次上的流水线,就会导致所有其下的 (也就是嵌套在内的循环)被展开,这样可能会产生一个比期望更为昂贵的实现。因此,只在最内层的循环 (比如图 15.27 里的column_loop)上做流水线,才能在性能和资源利用率之间取得良好的平衡。

15.5.6 数组

在 Vivado HLS 中,数组类型通常表示的是存储,因此数组一般会被综合成存储 器。

在 HLS 过程中推导出的存储器是被映射在 PL 的物理资源上的,可能是Block RAM,也可能是由逻辑片所构成的分布的 RAM,因此知道所综合出的存储器的大小和形状,以及如何映射到芯片上可用的资源是非常重要的。实际上,往往需要对所综合出来的存储器施加影响,以实现更好的到物理存储器资源的映射,设计者可以用指令来做到这一点。

有不少数组优化可以用,下面详细列出了如何通过指令来指定这些优化。注意这些优化与相同名称的接口指令类似,但是这里这些指令是针对电路中的单元的。

  • Resource — 资源。设计者可以选择把基于 C 的 HLS 源代码中的数组映射到特定的存储器资源中。
  • Array Map — 数组映射。可以把几个小的数组组合成单个较大的数组。这样带来的好处是总的需要的存储器资源减少了 (比如可以用单个 Block RAM 来 实现组合起来的全部存储了,而不再是为四个独立的存储部分各自分配一个Block RAM)。映射可以是水平的 (数组连接起来形成单元数较多的数组), 也可以是垂直的 (数组的单元被组合起来,使得数组的一个单元占据较大的字长)。
  • Array Partition — 数组划分。这个指令可以被认为是 Array Map 的反面, 因为它让设计者可以决定把一个大的数组划分成一组较小的数组。做划分往往是为了改善存储器访问的综合速率,比如一个大的双端口的 RAM可以实现每个时钟周期访问两次,而四个较小的双端口 RAM可以在一个时钟周期内实现总计八次访问(每个两次)。最极端的情况,数组划分可以把一个数组划分成独立的寄存器单元。
  • Array Reshape — 数组重塑。这个指令让一个有许多单元、每个单元较小的数组,重塑成一个单元数量少、每个单元较大的数组。采用这个指令的动机是减少所需的存储器访问次数。
  • Stream — 流。采用这个流指令把一个数组综合成 FIFO 而不是 RAM。

当基于不同大小的数组综合出来的存储器要组合起来的时候,由 [18] 中所描述的工具来做适配。从这里所介绍的数组指令的选择可以清楚地看到,设计者可以根据需要用不同 的方法来塑造数组。

在某些情况下,需要合并数组来优化资源利用。而另一些情况,可能更重要的是优化存储器带宽,那么就可以把数组分离在几个较小的存储器中,这样就能有更多可用的存储器访问端口了。

因此数组的调整可以被认为是提供给 HLS 设计者的灵活而有力的技术,无论理论上的实现目标是最小化资源耗用还是最大化性能。

15.6 设计评估和优化

本章到处都提到过,在 Vivado HLS 里开发的设计是基于这样一种结构 —— 其中的源代码是固定的,而一组变量 (“ 解决方案 ”)则是由施加不同的约束和指令来产生的。设计者可以调整这些参数来探索各种可能性,逐步向着最优的解决方案发展。

在这一节里,我们先不来变动这些参数,而是专门考虑设计者运用 Vivado HLS 所提供的机制来微调设计的步骤。

15.6.1 设计约束

在设计过程可以施加特定的约束来限制所产生的解决方案的某些特性。最常用的约束类型是时序约束,通常是在时钟周期上设一个上限(当然其他时序属性也是可以定义的)。另一个可能是限制设计的延迟,即在给了输入到观察到的对应的输出之间的时钟周期的个数,对此可以给定上限或下限。除了时序的特性外,设计者还可以约束用来实现所需的功能的资源。

HLS 过程可以根据所施加的约束产生不同的结果。比如,如果规定了最大延迟,那么所产生的设计可能就会用到更多的资源来实现所需的算法。而另一方面,如果资源利用情况是受限的,那么结果的实现很可能就会采用时分复用,从而就呈现更高的延迟。

15.6.2 合成指令

在本章的讨论中我们已经看到过,存在几种类型的指令可以让设计者来影响综合得到的硬件的某些特性。比如,我们注意到可以施加接口约束来要求采用某个特定类型的协议,而流水线指令会影响并行性、延迟和吞吐率。

指令可以是以 TCL 命令的方式集合在专门的文件中施加,也可以以 pragma 的方式嵌入在C/C++/SystemC源代码中。每一种方法都有不同的理由适用于不同的场合。举个常见的例子,通常一个设计的接口是首先定义的而且是固定的,因此以 pragma来施加接口指令就能让这些设置在所有的解决方案中都是一样的。而另一方面,在积极地探索算法综合设计空间的时候,就适合把指令从源代码中分离出来,这样就易于指令的使用,一旦需要就可以创建出 “ 新鲜 ” 的解决方案。

15.6.3 统计报告

Vivado HLS 所产生的每一个解决方案,都会产生一个相应的报告来放各种统计数据,包括时序 (时钟)性能的估计、延迟和资源的利用情况 (注意这些是估计的,要知道完整的细节直到做了 RTL综合和实现中的更花时间的阶段之后才能有)。报告也给出了所综合的接口的完整细节。如果有的话,报告还会包括设计中每个循环的细节,包括循环的次数 (迭代的次数)、延迟和循环间隔。

进一步的选项是产生从一组解决方案得到统计数字的综合报告。Vivado HLS 会 提示用户从所有的解决方案中做一些选择,然后准备出相应的报告。这个总结报告是在一组解决方案中比较统计数字的有用的方法,通过比较可以识别出最适合需求的解决方案,或是与施加某种指令相关的趋势。

15.6.4 设计迭代和优化

从前面的讨论应该可以明显看出,设计者可以运用指令和约束对 HLS 的结果施加可观的控制。每个解决方案所产生的报告会有助于找到诸如存储器瓶颈、过度的循环延迟和对资源的过分使用。于是他 / 她就可以微调已有的指令,或换用更好的来使得综合的过程朝向更优化的解决方案发展。

15.7 从 Vivado HLS 导出

Vivado HLS 可以导出设计为几种不同的格式。这是为了能让 Vivado HLS 的 IP 能方便地与 Vivado 和 ISE 设计套件中的其他开发工具集成。

在输出的阶段,有机会可以 “ 评估 ” 设计,就是说会做 RTL 综合和实现的阶段,这个阶段会产生出一份深入的报告,确认资源利用和时序性能的实际数值。这个阶段可以用 VHDL 或 Verilog 作为 RTL 语言来进行。

15.7.1 Vivado IP Catalog (IP-XACT 格式)

基本的选项是从HLS输出成IP-XACT格式,这样这个模块就可以被集成进Vivado IP Integrator 设计中了。这样做的时候,会有选项让设计者可以给这个 IP 包贴标 签,以及加入作者和版本的数据。结果是一个在相关的解决方案目录中的“impl\ip” 子目录中的一个 zip 文件,这个文件就是那个 IP Catalog 包。

一旦形成了 IP-XACT 格式,从 Vivado HLS 产生的这个 IP 就能方便地共享和分发了。

15.7.2 DSP 的 System Generator

进一步的选项是把 HLS 设计输出成一个用于 System Generator 的 IP 包。这样做的时候,用户会被要求指定是用 ISE 还是 Vivado 来做逻辑综合与实现的工具。也就是说,如果最终的 System Generator 系统 (包括最初从 HLS 来的 IP)是打算用 ISE 来做综合的,那么在从 Vivado HLS 输出的时候就应该选择 ISE。

15.7.3 XPS 的 pcore

用 XPS 工具做嵌入式系统设计的用户,能把 Vivado HLS 的 IP 输出成一个 pcore (*XPS 的设计 *),这样就能方便地集成进一个基于 XPS 的系统了。

15.8 本章回顾

本章给出了 Vivado HLS 开发工具的详细描述,HLS 提供了从基于 C 的软件描述来快速开发硬件设计的工具。

尽管单凭单一章节并没有足够的篇幅来说明这个工具的全部特性,但是我们也涵盖了 Vivado HLS 环境、数据类型的使用 (包括采用任意精度格式的工具)和各 种接口与算法综合的功能,还给出了几个概念性和基于代码的例子来表明我们的观点。

有一个贯穿本章的主题,就是设计者如何能通过使用指令和约束来影响根据输入的 C 代码所做的综合,从而产生出不同的 “ 解决方案 ”。作为这个讨论的一部分,我们总结了关键性能和实现的度量指标,并给出了例子来表明设计者具有怎样能力使用指令来控制这些指标。

最后,本章还指出了在 Vivado HLS 中所产生的设计可以方便地输出以集成进更大的系统项目中,无论是 IP Integrator、XPS 还是 System Generator 都可以。

15.9 参考文献

说明:所有的 URL 最后在 2014 年 6 月访问过。

[1] D. C. Black, J. Donovan, B. Bunton and A. Keist, SystemC: From the Ground Up, 2nd Edition, Springer, 2009.

[2] M. Burton, J. Aldis, R. Günzel and W. Klingauf, “Transaction Level Modelling: A reflection on what TLM is and how TLMs may be classified”, Forum on Design Languages, 2007, pp. 92-97.

[3] D. Gajski and R. Kuhn, “Guest Editors’ Introduction: New VLSI Tools”, Computer, vol. 16, no.12, pp.11 - 14, December 1983.

[4] D. Gadski, T. Austin and S. Svoboda, “What Input-Language is the Best Choice for High Level Synthesis (HLS)?”, panel session, Proceedings of the 47th ACM/IEEE Design Automation Conference (DAC), pp. 857-858, June 2010.

[5] GNU, The GNU C Reference Manual. 位于 : http://www.gnu.org/software/gnu-c-manual/gnu-c-manual.html

[6] D. Große and R. Drechsler, Quality-Driven SystemC Design, Springer, 2010.

[7] IEEE Computer Society, “IEEE Standard for Standard SystemC Language Reference Manual”, IEEE Std 1666-2011, January 2012.

[8] B. W. Kernighan and D. M. Ritchie, The C Programming Language, Prentice Hall, 1978.

[9] S. G. Kochan, Programming in C: A Complete Introduction to the C Programming Language, 3rd Edition, Sams Publishing, 2005.

[10]M. C. McFarland, A. C. Parker, and R. Camposano, “The High-Level Synthesis of Digital Systems”, Proceedings of the IEEE, vol. 78, no.2, pp 301 - 318, Feb 1990.

[11]G. Martin and G. Smith, “High Level Synthesis: Past, Present and Future”, IEEE Design and Test of Computers, Vol. 26, Issue 4, July/August 2009, pp. 18 - 24.

[12]A. Mathur, E. Clarke, M Fujita, and R. Urard, “Functional Equivalence Verification Tools in High-Level Synthesis Flows”, IEEE Design & Test of Computers, July/August 2009, pp. 88 - 95.

[13]M. Meredith and S. Svoboda, “The Next IC Design Methodology Transition is Long Overdue”, Open SystemC Initiative, February 2010. 位于 : http://www.accellera.org/resources/articles/icdesigntrans/community/articles/icdesigntrans/ ic_design_transition_feb2010.pdf

[14]D. M. Ritchie, “The Development of the C Language”, Proceedings of the 2nd History of Programming Languages Conference, Cambridge, Massachusetts, April 1993.

[15]Xilinx, Inc., “UG634 - AccelDSP Synthesis Tool User Guide”, v11.4, December 2009.位于 : http://www.xilinx.com/support/documentation/sw_manuals/xilinx11/acceldsp_user.pdf

[16]Xilinx, Inc., “UG835 - Vivado Design Suite Tcl Command Reference Guide”, v2014.1, April 2014. 位于 : http://www.xilinx.com/support/documentation/sw_manuals/xilinx2014_1/ug835-vivado-tclcommands.pdf

[17]Xilinx, Inc., “UG871 - Vivado Design Suite Tutorial: High Level Synthesis”, v2014.1, May 2014.位于 : http://www.xilinx.com/support/documentation/sw_manuals/xilinx2014_1/ug871-vivado-highlevel-synthesis-tutorial.pdf

[18]Xilinx, Inc, “UG902 - Vivado Design Suite User Guide: High-Level Synthesis”, v2014.1, May 2014.位于 : http://www.xilinx.com/support/documentation/sw_manuals/xilinx2014_1/ug902-vivado-highlevel-synthesis.pdf

[19]Xilinx, Inc., “UG998 - Introduction to FPGA Design with Vivado High-Level Synthesis”, v1.0, July, 2013.位于 : http://www.xilinx.com/support/documentation/sw_manuals/ug998-vivado-intro-fpga-designhls.pdf

第二十八篇到此结束,下一篇将带来第二十九篇,开启第十六章用 Vivado 高层综合做设计等相关内容。欢迎各位大侠一起交流学习,共同进步。

END

后续会持续更新,带来Vivado、 ISE、Quartus II 、candence等安装相关设计教程,学习资源、项目资源、好文推荐等,希望大侠持续关注。

大侠们,江湖偌大,继续闯荡,愿一切安好,有缘再见!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-05-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 FPGA技术江湖 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
GPU 云服务器
GPU 云服务器(Cloud GPU Service,GPU)是提供 GPU 算力的弹性计算服务,具有超强的并行计算能力,作为 IaaS 层的尖兵利器,服务于生成式AI,自动驾驶,深度学习训练、科学计算、图形图像处理、视频编解码等场景。腾讯云随时提供触手可得的算力,有效缓解您的计算压力,提升业务效率与竞争力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档