完整可点击跳转
目录
- 硬件加速器的设计方法
- 高层次综合HLS
- HLS与电路地对应关系
- HLS的设计规范
- HLS优化
- 延迟优化
- 降低单个循环的延迟
- 循环展开(Unroll)
- 循环展平(Flatten)
- 多个循环的并行化
- 循环合并
- 循环函数化
- 数据流执行(Dataflow)
- 吞吐量优化
- 循环/函数流水线
- 数据流
- 优化调试
硬件加速器的设计方法
一般地,硬件加速器的设计可采用完全硬件化和部分硬件化2种设计方法。
完全硬件化 的设计方法需要根据网络结构,使用HDL(Hardware Description Language)或HLS(High-Level Synthesis)实现神经网络的每一个层。该方法的优点是硬件化程度高,可实现网络层次的流水线,并且能够获得很高的加速效果,但显然存在开发难度大、开发周期长、硬件资源消耗较多以及通用性差的缺点。
部分硬件化 的设计方法首先从神经网络的运算类型出发进行考虑,通过分析和必要的测试,得出神经网络内部各类运算的占比,然后有针对性地选择占比较大的运算进行加速;对于占比较小的运算,则仍然使用软件实现。部分硬件化设计方法的理论依据是Amdahl’s law。该方法具有开发难度较低、开发周期较短、硬件资源消耗较少的优点,同时还能够使加速器具有一定的通用性和灵活性,并且在某些情形下能够获得与完全硬件化方法相近的性能。
高层次综合HLS
HLS与电路地对应关系
一般地,软件程序中的函数最终会综合成为相应的电路模块实体,而程序中的控制流和数据流则由HLS工具中的调度和绑定程序(Scheduling and Binding Processes)映射到硬件电路当中。
软件成分 | 对应地硬件组成 |
---|---|
函数 | 模块 |
函数的参数 | 模块地输入/输出端口 |
操作符 | 功能单元 |
变量 | 线网(wire)或寄存器(reg) |
数组 | 存储器 |
控制流 | 控制逻辑 |
HLS的设计规范
虽然HLS可将高级语言描述的程序转换成硬件电路,但HLS并没有强大到可以处理任何代码。许多在软件编程中常用的概念在硬件中很难实现,所以有时需要将HLS与HDL结合,从而使得设计更加灵活。
HLS工具通常需要用户提供附加信息(通过suggestion或#pragma)来帮助完善程序,因此我们说HLS工具会同时"限制"又"加强"了一门语言。例如,HLS一般无法进行动态内存分配,且大部分HLS工具对标准库的支持也非常有限;此外,使用HLS编程时,应当避免使用系统调用和递归语句,以尽量降低程序的复杂程度。除去这些设计限制,HLS的处理范围非常广(包括DMA,数据流,Scratchpad Memory等),优化效率也较高。
一般地,使用HLS设计开发硬件电路时,应遵循的规范如下:
- 不使用动态内存分配,如malloc()、free()、new和delete等
- 不使用系统调用,如abort()、exit()、printf()等(可在测试代码中使用系统调用,但在需要综合的代码中,系统调用将被自动忽略或删除)
- 不使用递归语句
- 减少使用指针对指针的操作
- 减少使用标准库函数(HLS支持math.h中的常用函数,但仍存在不兼容)
- 减少使用C++中的函数指针和虚函数
HLS优化
延迟和吞吐量是电路设计中常用的2个性能指标。延迟值得是从输入数据到输出结果之间的耗时,而吞吐量则是两次输出结果之间的时间差。
![[…/log/Pasted image 20240301145327.png]]
延迟优化
降低单个循环的延迟
循环是代码中最常见的结构之一,如何降低循环的延迟显然是延迟优化的关键问题。
循环展开(Unroll)
HLS使用一个硬件模块实现循环体。如果循环语句的循环次数为n,则该硬件模块将被执行n次。假如现在每次循环执行m次循环体,那么完成相同的功能只需要n/m次,这就是循环展开的基本思想。循环展开的本质是牺牲更多的资源来换取加速效果。
在HLS中,可使用#pragma HLS UNROLL factor=<int>
的制导语句来告诉编译器哪个地方需要做循环展开。其中,参数factor
用于指示循环体应该被复制多少次。当某个循环被展开了m次后,HLS编译器将生成m个硬件模块并行执行。例如以下,对程序进行factor为2的展开:
for (int i = 0; i< N; i++){
#pragma HLS unroll factor=2a[i] = b[i] + c[i];
}
上述代码等效于:
for (int i = 0; i < N