Xilinx Vitis HLS教程1--Vitis HLS概述[01/12]
前言1 软件工程师的设计原则1.1 简介1.2 吞吐量和性能1.3 架构问题1.4 FPGA编程的3大范式1.4.1 生产者-消费者范式1.4.2 流式数据范式1.4.3 流水线范式1.5 3种范式结合前言
项目中使用到Xilinx Zynq UltraScale+ MPSoC ZCU102 开发板,领导指示研究"xilinx软件自带的c语言综合器"。对于一个硬件一窍不通的我来说,甚至不知道问题是什么,怎么用,怎么学。
花了两天时间看了板子的硬件组成,上电启动步骤,主要参考文件:
评估板套件快速上手指导软件安装、板卡启动,其中又涉及UART驱动、Tera终端等的安装硬件组成,详细介绍每个硬件组成的位置、功能电路图(并没有看,但是3用户手册中有对它的引用)
以上只能做了一点硬件实物的了解,软件的一概没有接触到。
现在找到一个vitis教程,还有一些网站资料,汇总如下:
vitis软件统一平台介绍
自 .2 起,Xilinx SDK、SDSoC™ 和 SDAccel™ 开发环境被整合为一个多用途的 Vitis™ 统一软件平台, 用于应用加速和嵌入式软件开发。xilinx社区门户
专注于 Xilinx 技术的社区网站。这里将有来自您、Xilinx 和 Xilinx 合作伙伴在内的整个社区的设计和信息分享。随意浏览、使用信息,并添加自己的设计。xilinx非官方讨论
搜索PMOD接口时无意中来到这个网站,有些中文的资料,后续可以翻看。vitis入门资源
包含两个模块,get-started和document,其中的document又分为4大块:应用加速开发流、嵌入式软件开发流、Vitis HLS和AI引擎。
这个任务本来就源自于领导让看看chisel是否能加速硬件开发,调研汇报后,领导表示:“1. 我最近自己也看了一下,感觉不如hls。2. xilinx软件自带的c语言综合器可以看看。3. 这个可能入门更简单点。”
因此,本文学习Vitis HLS,记录主要观点+自问自答。
Vitis HLS概述
HDL 硬件描述语言,Hardware Description Languages
HLS 高层次综合,High-Level Synthesis
MBD 基于模型的硬件设计
FPGA Field-Programmable Gate Array,现场可编程门阵列
QoR Quality of Results,结果质量
1 软件工程师的设计原则
1.1 简介
本主题面向想要了解从 C/C++ 编写的软件算法合成(synthesis)加速硬件过程的软件开发人员。本文档向开发人员介绍了需要了解的基本概念,以便设计和创建优秀的可合成(synthesizable )软件,并使用高层次综合 (HLS,High-Level Synthesis) 工具将其成功转换为硬件。本文档中的讨论将是工具不可知论者(tool-agnostic),所引入的概念是大多数 HLS 工具的共同点。这里介绍的主要概念应该为具有 RTL 设计经验的人员熟悉。然而,审查这些材料可以有效地加强这些概念的重要性:帮助您了解如何接近 HLS,特别是如何构建 HLS 代码以实现高性能设计。
synthesis怎么翻译,合成?综合?怎样都不太合适,下文保持英文形式。
1.2 吞吐量和性能
Throughput 吞吐量定义:每单位时间内执行特定动作的数量或每单位时间内产生结果的数量。
例如,“memory bandwidth"指memory系统的吞吐量。
Performance 性能定义:同时考虑吞吐量和低功率。
1.3 架构问题
当多线程和不断增加的 CPU 速度无法再cover数据处理速率时,就需要使用多个 CPU 内核(简称“多核”)和超线程(hyperthreading )来提高吞吐量。
在当今无处不在的智能手机、游戏和在线视频会议的世界中,正在处理的数据的性质已经改变。为了实现更高的吞吐量,必须将工作负载移近memory和/或移动到专门的功能单元。因此,新的挑战是设计一个新的可编程架构,这样就可以保持足够的可编程性,同时实现更高的并发性和更低的电力成本——FPGA。
FPGA可以配置为自定义硬件电路,该电路与专用硬件行为一致。FPGA的计算元素粒度及其灵活,小至基本逻辑门,大至DSP之类的完整算术逻辑。
在更高的粒度下,用户指定组合式逻辑单元称为内核(kernel),然后可以战略性地放置在 FPGA 设备上承担各种角色。
1.4 FPGA编程的3大范式
虽然FPGA可以用低层次的HDL语言(如Verilog、VHDL)进行编程,但是有一些高层次综合HLS工具,可以用C/C++之类的高级语言下书写算法描述,再转换成低层次HDL语言。这样的好处主要是,您可以保留高级语言(如C、C++)的优势,编写高效代码,这是软件开发人员的长项,比学习HDL更容易。
但也有几大劣势:
并行性
高级语言(如C、C++)程序基本上服务于冯诺依曼架构,也就是说程序中的所有指令严格按照顺序执行。为了提高性能,HLS工具必须在一堆顺序代码中推断出并行代码,也就是找出哪些指令相互依赖,哪些指令互不相干。这个问题并不简单。不可合成
此外,一个优秀的软件开发人员编写代码往往有良好的编程规范,例如递归、动态内存分配、运行时类型推导,等等。许多此类技术在硬件上没有直接对等的电路,给HLS工具带来了第二大挑战。也就是说,现成的高级语言开发的软件,并不一定能转换成HDL。HLS工具需要检查不可合成的(non-synthesizable)结构,并指示重新编码。极致性能
再次,即使软件程序自动转换成硬件了,QoR也不一定达到了极致。
综上所述,您需要了解编写在FPGA上执行的最佳软件实践。下文先讨论一些宏观结构性优化技巧,再聚焦于一些微观技巧。
1.4.1 生产者-消费者范式
一般的多线程开发方法:有一个主线程,先做一些初始化,然后fork出来一系列子线程,子线程承担并行计算任务,在所有并行计算完成后,主线程再收集结果并写出。在这个过程中,程序员首先要理清,哪些部分互不依赖可以并行进行,哪些部分必须精确串行执行。这种fork/join的并行方式也适用于FPGA,不过FPGA提高吞吐量的关键模式是生产者消费者范式,将其应用于顺序程序,并自动转换为并行程序,以提高性能。
假设step3写出和step2处理数据可以有重叠,也就是边处理边写出,而不是等待所有处理都结束再开始写出,那么就具有了并行的潜力,耗时比不带重叠的方式要短。我们称step2为生产者,step3为消费者。
在CPU中,生产者消费者范式的性能有限,因为线程之间的切换和L1 cache都有额外开销。在FPGA,这些开销可以忽略不计。
最简单的例子是单生产者-单消费者范式,两者借助有限大小的缓冲区(buffer)进行沟通。缓存满时,生产者可以选择等待或者丢弃数据。消费者每取出一份数据,就notify通知生产者又可以塞数据了。缓存空时,消费者也可以等待。一旦生产者塞入一份数据,就唤醒沉睡中的消费者。
单生产者-单消费者范式可以通过进程间通信(inter-process communication)实现,典型地用监视器(monitor)或信号量(semaphores)。如果设计的不好,可能陷入死锁(deadlock)状态,两个进程都停滞不前,等待被唤醒。
然而,就单生产者-单消费者而言,通信模式强映射到先进先出 (FIFO) 或乒乓 (PIPO) buffer实现。这种类型的通道提供高效的数据通信,而无需依赖信号量、互斥(mutex)或监视器进行数据传输。这种基于锁(lock)的设计性能差,难以使用和调试。PIPO 和 FIFO 是受欢迎的选择,因为它们避免了端到端原子同步( atomic synchronization)。
生产者-消费者编程范式(两者通信封装在buffer中)使程序员免于担心memory models或其他不确定性行为(如条件竞争)。基于这种范式设计出来的网络称之为“数据流网络”(dataflow network):接受数据流,对流上的数据做处理,再输出数据流。如此,并行程序的复杂性就被抽象掉了。
注意,step1导入数据和step4导出数据应该尽量压缩(例如将一些动作放到step2),以获得更大的并发性。最后,喂数据要足够快而不至于拖累整体性能。
1.4.2 流式数据范式
stream是一个非常重要的概念:它代表无限的、连续更新的数据集,“无限”指不知道size或者不限制size。steam可以理解成在源进程和目的进程之间单向流动的数据,数据可以是纯标量也可以是buffer。
流式范式迫使您从数据访问模式(或序列)的角度进行思考。在软件中,数据的随机存储访问仿佛没有代价,但是在硬件层面,进行顺序访问(可以转换为stream)增益却非常大。将算法分解成用stream数据进行通信的生产者-消费者范式有很多好处。1. 它允许程序员以顺序方式定义算法,而由编译器负责提取并行化部分;2. 任务间的同步等复杂性被抽象掉了;3. 它允许生产者和消费者任务同时处理数据,这是提高吞吐量的关键;4. 代码更简洁。
正如1.2.4.1小节所述,生产者消费者范式的通信方式常为FIFO或PIPO缓存。先进先出buffer简单来说就是一个固定大小的队列,主要优点是只要队列有一份数据,消费者就可以立即开始工作。先进先出buffer的主要缺点是,生产/消费速率不同,不恰当的size可能会导致死锁,在多生产者-多消费者场景这个问题尤其突出。乒乓buffer也就“双buffer”,就是一个buffer保存旧数据,这样消费者可以看到完整的数据;同时另一个buffer是生产者正在创建新一轮数据。当新一轮数据块完成并且有效时,消费者和生产者交替访问两个buffer。因此,使用乒乓buffer可提高设备的整体吞吐量,并有助于防止最终的瓶颈。PIPO的主要优点是能自动匹配生产/消费速率,并创建一个既高性能又无死锁的通信通道。(PIPO缺点是什么?)
下面举个加法的实例,task1、2产生随机数,task3取数据做加法,task4输出结果。task之间的连接通常是FIFO队列。
1.4.3 流水线范式
pipeline是你在日常生活中也能经常用到的一个抽象概念,一个很好的例子是汽车工厂的生产线。其中每个特定任务,如安装发动机、安装车门、安装车轮,往往由一个单独的工作站完成。工作站之间并行执行任务,很可能每个站都在不同的汽车上操作。同一辆汽车一旦完成了一项任务,它就会被移到下一站。完成任务所需的时间变化可以通过缓冲(在工作站之间的空间中容纳一辆或多辆车)和/或通过拖延(暂时停止上游站)来适应,直到下一个站可用。
例如,组装一辆汽车需要A\B\C三道工序,每个任务耗时20,10,30分钟。
组装第一辆车的耗时称为的”iteration latency"(迭代延迟)。如图所示,流水线技术并不会缩短latency,但是能扩大系统的吞吐量。除第一辆车以外,组装出其他车的时间间隔称为“initiation interval"(启动间隔)。生产3辆车的总时间称为"total latency":
total latency = iteration latency + initiation interval * (number of items - 1)。
pipeline是可以用于多种级别抽象的典型宏观架构优化范式。例如,上例是task级别流水线,pipeline也可以应用于指令级别( instruction-level pipelining,ILP)。pipeline技术是生产者-消费者范式中保持buffer非空、保持繁忙工作的关键。
由于pipeline在不同时间执行相同函数的资源也相同,因此它被认为是静态优化技术的一种。因为它需要对每个任务的lantency完全了解。也正因此,低层次指令流水线技术不能用于task latency不确定的数据流网络。
1.5 3种范式结合
用户程序的最主要优化点在于函数和循环。
函数优化
当今的优化工具典型地在函数级别下功夫。每个函数可以转换为一个特定的硬件组件。每个这样的硬件组件如同一个class申明,在最红的硬件设计中可以创建多个改组件的实例。每个硬件组件由由很多个更小的预定义组件组合而成,例如加减乘除。虽然不支持递归,但是支持函数之间互相调用。小而不常用的函数也可以写成内嵌形式,如同软件函数中的inline。此时,inline函数可以使用外层函数的资源。将设计构造为一组通信函数有助于在执行这些函数时推断并行性。
循环优化
循环体的反复执行天然就适合进行并行化,常见的转换有pipeline和unroll。这些转换可以是存储系统优化,可以映射到多核,也可以映射到SIMD指令。在自然科学和工程领域,经常看到对大数据矩阵的element-wise操作,例如卷积。这些操作可以用嵌套循环表示,如果循环之间存在数据依赖,那还需要解依赖、重写算法,再做并行化。
举个例子,4个任务分别A\B\C\D,形成diamond通信模式,输入输出分别为vecIn和vecOut:
void diamond(data_t vecIn[N], data_t vecOut[N]){data_t c1[N], c2[N], c3[N], c4[N];#pragma HLS dataflowA(vecIn, c1, c2);B(c1, c3);C(c2, c4);D(c3, c4, vecOut);}
可见B\C之间没有依赖关系,如果顺序调用两次,则流水形如: (A; B ; C; D;A; B ; C; D) ,称为“串联任务图”;
B\C并行执行,则流水形如: (A; (B || C); D); (A; (B || C); D) ,称为“串并联任务图”;类似的可以处理其他有向无环任务图。
在 FPGA 上,您可以探索哪些其他形式的并行性是可用的。比如两次调用之间有没有依赖?两次任务之间有没有依赖?系统的整体吞吐量取决于单个任务的最低吞吐量,这个例子就涉及所提到的3大范式:生产者-消费者、流式数据、pipeline。