展开循环以改善流水打拍 - 2021.2 Chinese

Vitis 高层次综合用户指南 (UG1399)

Document ID
UG1399
Release Date
2021-12-15
Version
2021.2 Chinese

默认情况下,在 Vitis HLS 中循环保持处于收起状态。这些收起的循环会生成硬件资源,供循环的每次迭代使用。虽然这样可创建资源节约型块,但有时可能导致性能瓶颈。

Vitis HLS 可提供使用 UNROLL 编译指示或指令来展开或部分展开 FOR 循环的功能。

下图显示了循环展开的优势以及展开循环时必须考量的影响。此示例假定阵列 a[i]b[i]c[i] 均已映射到块 RAM。此示例显示只需直接应用循环展开即可同时创建大量不同实现。

图 1. 循环展开详情
循环已收起
当循环已收起时,每次迭代都在单独的时钟周期内执行。此实现需耗时 4 个时钟周期,只需 1 个乘法器并且每个块 RAM 均可为单端口块 RAM。
循环已部分展开
在此示例中,循环已按因子 2 部分展开。此实现需 2 个乘法器和双端口 RAM 以支持在同一个时钟周期内读取或写入每个 RAM。但此实现只需 2 个时钟周期即可完成:相比于循环的收起版本,启动时间间隔和时延均减半。
循环已展开
在完全展开的版本中,可在单一时钟周期内执行所有循环操作。但此实现需 4 个乘法器。更重要的是,此实现需在同一个时钟周期内执行 4 次读操作和 4 次写操作的功能。由于块 RAM 最多仅有 2 个端口,因此此实现需对阵列进行分区。

要执行循环展开,可向设计中的每个循环应用 UNROLL 指令。或者,可向函数应用 UNROLL 指令,以展开函数作用域内的所有循环。

如果循环已完全展开,那么只要数据依赖关系和资源允许,即可并行执行所有操作。如果某一循环迭代中的操作需要上一次循环的结果,则这两次迭代无法并行执行,但一旦数据可用即可立即执行。完全展开并完全最优化的循环通常涉及循环主体中的多个逻辑副本。

以下示例演示了如何使用循环展开来创建最优化的设计。在此示例中,数据作为交织式通道存储在阵列中。如果按 II=1 来对循环进行流水打拍,则每经过 8 个时钟周期才会对每个通道执行依次读取和写入。

// Array Order :  0  1  2  3  4  5  6  7  8     9     10    etc. 16       etc...
// Sample Order:  A0 B0 C0 D0 E0 F0 G0 H0 A1    B1    C2    etc. A2       etc...
// Output Order:  A0 B0 C0 D0 E0 F0 G0 H0 A0+A1 B0+B1 C0+C2 etc. A0+A1+A2 etc...

#define CHANNELS 8
#define SAMPLES  400
#define N CHANNELS * SAMPLES

void foo (dout_t d_out[N], din_t d_in[N]) {
 int i, rem;

  // Store accumulated data
 static dacc_t acc[CHANNELS];

 // Accumulate each channel
 For_Loop: for (i=0;i<N;i++) {
 rem=i%CHANNELS;
 acc[rem] = acc[rem] + d_in[i];
 d_out[i] = acc[rem];
 }
}

factor 为 8 来对循环进行部分展开将允许并行处理每个通道(每 8 个样本为一组),前提是输入阵列和输出阵列同样按 cyclic 方式进行分区,以便在每个时钟周期内进行多次访问。如果此循环同时采用 rewind 选项来进行流水打拍,那么此设计将持续并行处理全部 8 个通道,前提是以流水打拍方式(即在顶层或者在数据流区域中)调用这些通道。

void foo (dout_t d_out[N], din_t d_in[N]) {
#pragma HLS ARRAY_PARTITION variable=d_i type=cyclic factor=8 dim=1
#pragma HLS ARRAY_PARTITION variable=d_o type=cyclic factor=8 dim=1

 int i, rem;

  // Store accumulated data
 static dacc_t acc[CHANNELS];

 // Accumulate each channel
 For_Loop: for (i=0;i<N;i++) {
#pragma HLS PIPELINE rewind
#pragma HLS UNROLL factor=8

 rem=i%CHANNELS;
 acc[rem] = acc[rem] + d_in[i];
 d_out[i] = acc[rem];
 }
}

部分循环展开不要求展开因子为最大迭代计数的整数倍。Vitis HLS 会添加出口检查以确保部分展开的循环的功能与原始循环相同。例如,给定以下代码:

for(int i = 0; i < N; i++) {
  a[i] = b[i] + c[i];
}

按因子 2 展开的循环可将代码有效变换为如下示例所示形式,其中 break 构造函数用于确保功能保持不变:

for(int i = 0; i < N; i += 2) {
  a[i] = b[i] + c[i];
  if (i+1 >= N) break;
  a[i+1] = b[i+1] + c[i+1];
}

由于 N 为变量,Vitis HLS 可能无法判定其最大值(它可能受输入端口驱动)。如果展开因子(在此例中为 2)是最大迭代计数 N 的整数因子,那么 skip_exit_check 选项会移除出口检查和关联的逻辑。展开的效果现在可表示为:

for(int i = 0; i < N; i += 2) {
  a[i] = b[i] + c[i];
  a[i+1] = b[i+1] + c[i+1];
}

这有助于最大限度降低面积并简化控制逻辑。