通俗易懂的Quartus HLS vs Vitis HLS 优化技巧及对比
随着深度学习应用的普及,FPGA作为强大且高效的硬件加速器,其高性能和低延迟特性备受关注。为了简化FPGA开发流程,高级综合工具(HLS)如Intel的Quartus HLS和Xilinx的Vitis HLS相继推出,为用户提供了基于C/C++的编程方式,从而降低了开发门槛。然而,如何使用这些工具进行有效的优化仍然是一个挑战。本文将深入讲解基于Quartus HLS和Vitis HLS的优化技术,并通过示例进行对比,帮助读者快速理解并上手。
在另一方面,随时网络上有很多专业的教程,但是写的可谓是乱七八糟,尽可能凑专业名词,让人很难理解,我撰写本文的一个初心,也是帮助大家理解,所以如果描述有偏差,请见谅!
1. 基础概念
Quartus HLS: Intel推出的高级综合工具,支持C++和SystemC语言,用于将高层次代码转换为硬件描述语言(HDL)。
Vitis HLS: Xilinx推出的高级综合工具,支持C、C++和SystemC语言,目标是将高层次代码转换为可在FPGA上执行的硬件实现。
2. 常见优化技术
2.1 循环展开 (Loop Unrolling)
循环展开是将循环中的多个迭代展开为一组独立的语句,从而减少循环控制开销,并增加并行度。
简单例子:
假设我们有如下的循环,用于对一个数组进行操作。
// 原始代码for (int i = 0; i < 8; i++) { array[i] = array[i] * 2;}
展开后的代码:
如果我们将循环展开2倍,代码会变成如下:
for (int i = 0; i < 8; i += 2) { array[i] = array[i] * 2; array[i + 1] = array[i + 1] * 2;}
展开4倍,则变成:
for (int i = 0; i < 8; i += 4) { array[i] = array[i] * 2; array[i + 1] = array[i + 1] * 2; array[i + 2] = array[i + 2] * 2; array[i + 3] = array[i + 3] * 2;}
这样做的好处是减少了循环控制的开销,可以提高效率。
Quartus HLS:
void loop_unroll_example(int a[128], int b[128]) { #pragma unroll 4 for (int i = 0; i < 128; i++) { b[i] = a[i] * 2; }}
Vitis HLS:
void loop_unroll_example(int a[128], int b[128]) {#pragma HLS unroll factor=4 for (int i = 0; i < 128; i++) { b[i] = a[i] * 2; }}
对比:
Quartus HLS使用
#pragma unroll
指令,并指定展开倍数。Vitis HLS使用
#pragma HLS unroll
指令,并使用factor
参数指定展开倍数。本质上两者实现相似,都是为了增加并行度,但语法和参数可能略有不同。
2.2 循环流水化 (Loop Pipelining)
循环流水化是指将一个循环的多个迭代进行重叠执行,提高硬件资源的利用率和软件的执行效率。
假设你有一条生产线,需要依次完成取料、加工和装箱三个步骤:
第一步:取料
第二步:加工
第三步:装箱
如果让一个人(单个迭代)每次都先取料,再加工,再装箱,这样三个步骤需要按顺序来。
但如果采用流水化,让三个工人同时参与:
工人A取料后,立刻交给工人B。
工人B加工后,立刻交给工人C。
工人C装箱完成后,继续处理下个循环。
这样,每个时间节点都能处理多个步骤,从而提高整体效率。
Quartus HLS:
void loop_pipeline_example(int a[128], int b[128]) { #pragma ii 1 for (int i = 0; i < 128; i++) { b[i] = a[i] * 2; }}
Vitis HLS:
void loop_pipeline_example(int a[128], int b[128]) {#pragma HLS pipeline II=1 for (int i = 0; i < 128; i++) { b[i] = a[i] * 2; }}
对比:
Quartus HLS使用
#pragma ii
指令,指定循环间隔 (Initiation Interval)。Vitis HLS使用
#pragma HLS pipeline
指令,并指定II
参数。两者语法不同,但功能一致,都是为了将循环内的不同迭代重叠执行。
2.3 数组分块 (Array Partition)
数组分块是将一个大数组分拆成多个小数组,从而增加数据访问的并行度。
简单例子:
假设我们有如下的数组,有8个元素。
int array[8] = {1, 2, 3, 4, 5, 6, 7, 8};
分块后的操作:
我们将数组分成两部分,每部分4个元素。
int part1[4] = {1, 2, 3, 4};int part2[4] = {5, 6, 7, 8};
这样可以同时访问part1和part2,从而提高处理速度。
Quartus HLS:
void array_partition_example(int a[128], int b[128]) { #pragma partition variable=a cyclic factor=4 for (int i = 0; i < 128; i++) { b[i] = a[i] * 2; }}
Vitis HLS:
void array_partition_example(int a[128], int b[128]) {#pragma HLS array_partition variable=a cyclic factor=4 for (int i = 0; i < 128; i++) { b[i] = a[i] * 2; }}
对比:
两者都使用
#pragma
指令,并指定cyclic
分块方式和factor
分块因子。语法一致,但HLS工具不同,两者目的都是为了增加数据的并行访问能力。
3. 综合应用实例:卷积层优化
以下是一个基于深度卷积层的优化实例,展示了如何同时在Quartus HLS和Vitis HLS中进行循环展开、流水化和数组分块优化。
Quartus HLS:
void convolution(float input[32][32], float output[30][30], float kernel[3][3]) { #pragma ii 1 for (int i = 1; i < 31; i++) { #pragma unroll 3 for (int j = 1; j < 31; j++) { float result = 0; #pragma unroll for (int ki = 0; ki < 3; ki++) { for (int kj = 0; kj < 3; kj++) { result += input[i + ki - 1][j + kj - 1] * kernel[ki][kj]; } } output[i - 1][j - 1] = result; } }}
Vitis HLS:
void convolution(float input[32][32], float output[30][30], float kernel[3][3]) { #pragma HLS pipeline II=1#pragma HLS array_partition variable=input cyclic factor=2 for (int i = 1; i < 31; i++) {#pragma HLS unroll factor=3 for (int j = 1; j < 31; j++) { float result = 0;#pragma HLS unroll for (int ki = 0; ki < 3; ki++) { for (int kj = 0; kj < 3; kj++) { result += input[i + ki - 1][j + kj - 1] * kernel[ki][kj]; } } output[i - 1][j - 1] = result; } }}