Vitis 编译器可将大型阵列映射到 PL 区域中的块 RAM 存储器。这些块 RAM 最多可包含 2 个访问点或端口。这可能限制应用性能,因为在硬件中实现时,无法并行访问阵列内的所有元素。
根据性能要求,您可能需要在同一个时钟周期内访问某一阵列的部分或全部元素。为此, pragma HLS array_partition 可用于指令编译器拆分阵列元素,并将其映射到较小的阵列或者映射到单个寄存器。编译器可提供 3 种类型的阵列分区,如下图所示。这 3 种分区类型分别是:
-
block
:原始阵列分割为原始阵列的连续元素块(大小相同)。 -
cyclic
:原始阵列分割多个大小相同的块,这些块交织成原始阵列的元素。 -
complete
:将阵列按其独立元素进行拆分。这对应于将存储器解析为独立寄存器。这是 ARRAY_PARTITION 编译指示的默认操作。
对于块分区和周期分区,factor
选项可指定要创建的阵列数量。在前图中,使用因子 2 将阵列分割为 2 个更小的阵列。如果阵列的元素数量并非该因子的整数倍,那么排列在后的阵列所含元素数量较少。
dimension
选项可用于指定对哪个维度进行分区。下图显示了使用 dimension
选项通过 3 种方式对以下代码示例进行分区的方式:void foo (...) {
// my_array[dim=1][dim=2][dim=3]
// The following three pragma results are shown in the figure below
// #pragma HLS ARRAY_PARTITION variable=my_array dim=3 <block|cyclic> factor=2
// #pragma HLS ARRAY_PARTITION variable=my_array dim=1 <block|cyclic> factor=2
// #pragma HLS ARRAY_PARTITION variable=my_array dim=0 complete
int my_array[10][6][4];
...
}
此图中的示例演示了如何通过对维度 3 进行分区来生成 4 个独立阵列,以及如何对维度 1 进行分区以生成 10 个独立分区。如果指定维度 0,则将对所有维度进行分区。
谨慎分区的重要性
完整的阵列分区会将所有阵列元素都映射到各独立寄存器。这样有助于提升内核性能,因为所有这些寄存器都可在同一周期内并发访问。
选择对特定维度进行分区
int A[64][64];
int B[64][64];
ROW_WISE: for (int i = 0; i < 64; i++) {
COL_WISE : for (int j = 0; j < 64; j++) {
#pragma HLS PIPELINE
int result = 0;
COMPUTE_LOOP: for (int k = 0; k < 64; k++) {
result += A[i ][ k] * B[k ][ j];
}
C[i][ j] = result;
}
}
ROW_WISE
和 COL_WISE
循环均已平铺并结合在一起,COMPUTE_LOOP
已完全展开。要并发执行 COMPUTE_LOOP
的每次迭代,代码必须并行访问矩阵 A 的每个列和矩阵 B 的每个行。因此,矩阵 A 应在第二个维度内进行拆分,矩阵 B 应在第一个维度内进行拆分。#pragma HLS ARRAY_PARTITION variable=A dim=2 complete
#pragma HLS ARRAY_PARTITION variable=B dim=1 complete
周期分区与块分区的选择
此处使用相同的矩阵乘法算法来演示如何通过了解底层算法的阵列访问模式,在周期分区与块分区之间进行选择,以及如何判定相应的因子。
int A[64 * 64];
int B[64 * 64];
#pragma HLS ARRAY_PARTITION variable=A dim=1 cyclic factor=64
#pragma HLS ARRAY_PARTITION variable=B dim=1 block factor=64
ROW_WISE: for (int i = 0; i < 64; i++) {
COL_WISE : for (int j = 0; j < 64; j++) {
#pragma HLS PIPELINE
int result = 0;
COMPUTE_LOOP: for (int k = 0; k < 64; k++) {
result += A[i * 64 + k] * B[k * 64 + j];
}
C[i* 64 + j] = result;
}
}
在此版本的代码中,A 和 B 现在均为一维阵列。要并行访问矩阵 A 的每个列和矩阵 B 的每个行,可按上述示例中所示方式来使用周期分区和块分区。为并行访问矩阵 A 的每个列,此处应用 cyclic
分区,并将 factor
指定为行大小,此处即 64。同样,为并行访问矩阵 B 的每个行,此处应用 block
分区,并将 factor
指定为列大小,即 64。
通过缓存来最大程度减少阵列访问
由于阵列映射到含有限数量的访问端口的块 RAM,因此重复进行阵列访问可能限制加速器的性能。您应熟练掌握算法的阵列访问模式,并通过在本地缓存数据来限制阵列访问次数,从而提高内核性能。
mem[N]
阵列执行了 3 次访问以创建求和结果。#include "array_mem_bottleneck.h"
dout_t array_mem_bottleneck(din_t mem[N]) {
dout_t sum=0;
int i;
SUM_LOOP:for(i=2;i<N;++i)
sum += mem[i] + mem[i-1] + mem[i-2];
return sum;
}
#include "array_mem_perform.h"
dout_t array_mem_perform(din_t mem[N]) {
din_t tmp0, tmp1, tmp2;
dout_t sum=0;
int i;
tmp0 = mem[0];
tmp1 = mem[1];
SUM_LOOP:for (i = 2; i < N; i++) {
tmp2 = mem[i];
sum += tmp2 + tmp1 + tmp0;
tmp0 = tmp1;
tmp1 = tmp2;
}
return sum;
}