简介
案例研究的目标是通过展示逐步最优化,使用 HLS 指标来提升读取或写入循环或函数的吞吐量。这些最优化将通过执行从全局存储器到内核的高效数据传输来改善系统的内核时间和吞吐量。以下 transfer_kernel
示例会对变量大小和 NUM_ITERATIONS 执行简单的 DDR 读/写。
1 #include "config.h"
2 #include "assert.h"
3 extern "C" {
4 void transfer_kernel(wd* in,wd* out, const int size, const int iter ) {
5 ···
6 wd buf[256];
7 int off = (size/16);
8
9 read_loop: for (int i = 0; i <off; i++)
10 {
11 buf[i] = in[i];
12 }
13
14 write_loop: L1: for (int i = 0; i < iter; i++) {
15 L2: for (int j = 0; j <off; j++) {
16 #pragma HLS PIPELINE II=1
17 out[j+off*i] = buf[j];
18 }
19 }
20 ···
21 }
22 }
此案例研究分为 4 个步骤:
- 设置内核运行时间基线,其中端口宽度设为 512 位
- 通过更改时延参数来提升性能
- 改善写入循环的自动突发推断。
- 使用多个端口和未完成写入操作数无法再继续改善
步骤 1:设置内核基线,端口位宽为 512 位
使用默认设置来为内核时间设定基线。运行期间,自动突发会针对读取和写入循环执行以下推断:
- 由于该工具可以预测连续存储器访问模式,故而读取循环会达成流水线突发。因此流水打拍请求会读取 DDR 变量大小。
- 写入外层循环 L1 将达成顺序突发,因为编译器会基于所有组合进行迭代,并发现由于在编译时大小未知,因此在 L2 循环开始前,它会在 L1 循环内插入一项 if 条件。与此同时,最内层循环 L2 会达成流水线突发。L2 循环会发出写入变量大小请求,同时,L1 会等待 L2 循环的所有数据从 DDR 返回后再开始下一次 L1 迭代。
构建并运行应用后,可使用 Vitis 分析器工具来查看构建进程或者运行汇总生成的报告,以便评估性能。请复查 Vitis HLS 的“Synthesis Report”(综合报告)中提供的“Burst Summary”(突发汇总)。它会确认读取循环和写入循环的突发是否成功。
在 Vitis 分析器中,“Profile Summary”(剖析汇总)报告和“Timeline Trace”(时间线轨迹)报告同样是很实用的工具,可用于分析经 FPGA 加速的应用的性能。在“Profile Summary”(剖析汇总)中,Kernels & Compute Unit: Kernel Execution(内核与计算单元:内核执行)会报告基线构建中 transfer_kernel
所需的总时间。
步骤 2:改善性能时延
Vitis HLS 使用的默认时延为 64 个内核周期,在某些情况下可能过高。时延取决于系统特性。对于本示例,时延从默认值减少到 21 个内核周期。使用 INTERFACE 编译指示或指令可更改此代码以指定时延,如以下示例所示:
1 #include "config.h"
2 #include "assert.h"
3 extern "C" {
4 void transfer_kernel(wd* in,wd* out, const int size, const int iter ) {
5 #pragma HLS INTERFACE m_axi port=in0_index offset=slave latency=21
6 #pragma HLS INTERFACE m_axi port=out offset=slave latency=21
7 ...
构建和运行应用,并使用 Vitis 分析器来复查构建进程或运行汇总所生成的报告。在 Vitis HLS 中复查“Synthesis Report”(综合报告),并检查HW Interface(硬件接口)表,查看是否已应用指定的时延。
复查Burst Summary(突发汇总)以检查该进程是否成功。
检查“Profile Summary”报告中的“Kernel Execution”(内核执行),留意因设置接口时延而达成的性能提升。
步骤 3:改善写入循环的自动突发推断
编译器对自动突发推断持悲观态度,因为在编译时大小和循环次数均未知。您可修改代码以帮助编译器推断流水打拍突发,如下所示。
1 #include "config.h"
2 #include "assert.h"
3 extern "C" {
4 void transfer_kernel(wd* in,wd* out, const int size, const int iter ) {
5 #pragma HLS INTERFACE m_axi port=in offset=slave latency=21
6 #pragma HLS INTERFACE m_axi port=out offset=slave latency=21
7
8 int k=0;
9 wd buf[256];
10 int off = (size/16);
11
12 read_loop: for (int i = 0; i <off; i++)
13 {
14 buf[i] = in[i];
15 }
16
17 write_loop: for (int j = 0; j <off*iter; j++) {
18 #pragma HLS PIPELINE II=1
19 out[k++] = buf[j%off];
20 }
21 }
22 }
构建和运行应用,并使用 Vitis 分析器来复查构建进程或运行汇总所生成的报告。“Synthesis Report”会确认提供给编译器的突发提示已修复了写入循环的顺序突发。Burst and Widening Missed(错失突发与拓宽)消息与将端口拓宽至 512 位有关。由于此示例的端口宽度已设为 512 位,因此可忽略此消息。如果在您的代码中宽度并非 512 位,那么您可能需要集中精力解决这些消息中所示问题。
检验“Profile Summary”报告中的“Kernel Execution”,并留意因步骤 2 的时延更改而达成的性能提升以及当前步骤中写入循环的流水线突发。
总结
Vitis HLS 接口指标无法进一步改善性能。此案例研究示例并不含并发读取或写入,因此以多个端口为目标对此案例并无帮助。在此示例中,该工具已为最大吞吐量实现了流水线突发,因此未完成读取和写入数量同样可以忽略。内核时间方面无法确认进一步的性能改善。
正如此案例研究所示,实现有效的加载存储函数取决于 HLS 接口指标,包括端口宽度、突发访问、时延、多个端口以及未完成的读取和写入数量。AMD 建议遵循如下准则来改善系统性能:
- 端口宽度:最大程度为接口增加端口宽度,例如,对于每个 AXI 端口的位宽,使用
hls::vector
或ap_(u)int<512>
作为端口的数据类型。 - 分析并发存储器读取/写入,并采用单一专用/独立端口来执行并发访问。
- 流水线突发:AXI 时延参数不影响流水打拍突发,建议用户编写代码以达成流水打拍突发,这样可以显著提升性能。
- 顺序突发:AXI 时延参数对顺序突发具有显著影响,降低工具的默认时延的时延数将能改善性能。
- 未完成操作数:在大多数情况下,当突发长度 >=16 时,默认未完成数应已足够。对于大小小于 16 的突发,AMD 建议将未完成操作数的默认值 16 加倍。
- 数据重排序:始终建议达成流水打拍突发,但有时候,由于存储器访问模式所限,编译器只能达成顺序突发。为了提升性能,开发者还可思考如何通过不同方法将数据存储到存储器中。例如,通过以列为主的方式来访问 DRAM 中的数据效率非常低。相比在内核中实现专用数据移动器,在软件中对数据进行转置并改用以行为主的方式来存储将显著简化硬件访问模式,因此这种方式更有效。