0
点赞
收藏
分享

微信扫一扫

HLS实践 - 06 - 优化设计


HLS优化概述

使用HLS创建高质量 RTL 设计的一个关键部分是能够将优化应用于 C 代码。 HLS总是试图最小化循环和函数的延迟。为此,在循环和函数内,它尝试并行执行尽可能多的操作。 在函数级别,HLS总是尝试并行执行函数。

除了这些自动优化之外,指令还用于:

  • 并行执行多个任务,例如,同一函数的多次执行或同一循环的多次迭代。 这就是流水线。
  • 重构阵列(块 RAM)、函数、循环和端口的物理实现,以提高数据的可用性并帮助数据更快地通过设计。
  • 提供有关数据相关性或缺乏数据相关性的信息,从而允许执行更多优化。

最后的优化技术是修改 C 源代码,以消除代码中可能限制硬件性能的非预期依赖性。

对比使用循环和函数流水线来创建每个时钟可以处理一个样本的设计。 本示例进行分析设计未能满足性能要求的两个最常见原因:循环依赖性和数据流限制或瓶颈。

练习目的

本练习使用矩阵乘法器设计来展示如何充分优化大量基于循环的设计。 设计目标是使用 FIFO 接口每个时钟周期读取一个样本,同时最小化面积。该分析包括将在循环级别进行优化的方法与在功能级别进行优化的方法进行比较。

分析报告

添加完成后,进行C综合,然后查看生成的报告。

HLS实践 - 06 - 优化设计_HLS

性能估计表示,间隔为106个时钟周期。 由于每个输入阵列中有 9 个元素,因此设计每次读取输入大约需要 9 个周期。间隔和延迟一样长,因此此时硬件中没有并行性。延迟/间隔是嵌套循环造成的。

Product 的内循环:具有 3 个时钟周期的延迟。所有迭代总共有9个时钟周期。

Col 循环:进入循环 Product 需要 1 个时钟,退出循环需要 1 个时钟。所以每次迭代需要 11个时钟周期 (1+9+1)。有 33 个周期来完成所有迭代。

顶层循环每次迭代有 35 个时钟周期的延迟,循环的所有迭代总共有 105 个时钟周期。

改进启动间隔

您可以执行以下两种操作之一来改进启动间隔:流水操作循环或流水操作整个函数。 首先将循环流水线化,然后将这些结果与流水线化整个函数进行比较。
在流水线化循环时,循环的启动间隔是要监控的重要指标 。 如本练习所示,即使设计达到循环可以在每个时钟周期处理一个样本的阶段,函数的启动间隔仍报告为函数中包含的循环完成处理函数的所有数据所需的时间。

流水操作 Product 循环

新建解决方案然后选择Product循环标签添加PIPELINE指令,如下图所示:

HLS实践 - 06 - 优化设计_HLS_02

添加完成后进行C综合。在综合过程中可以看到这样的警告:

HLS实践 - 06 - 优化设计_数组_03

提醒在流水操作中无法强制执行指定好的依赖约束。所以在综合报告显示,Product 循环以 2 的间隔进行流水线化,但顶层循环的间隔并未流水线化。

HLS实践 - 06 - 优化设计_时钟周期_04

打开分析透视图。在性能视图中,展开循环 Row_Col 和 Product。选择状态 C1 下的写操作。右击选择Goto Source,

HLS实践 - 06 - 优化设计_HLS_05

问题是携带依赖 。 这是循环的一次迭代中的操作与同一循环的不同迭代中的操作之间的依赖关系。例如,当 k=1 和 k=2 时的操作(其中 k 是循环索引)。

第一个操作是第 60 行对数组 res 的加载(内存读取操作)。第二个操作是第 60 行对数组 res 的存储(内存写操作)。

由于 += 运算符,第 60 行是从数组 res 读取和对数组 res 的写入。 默认情况下,阵列映射到块 RAM,性能视图中的详细信息可以显示发生此冲突的原因。这里的流水操作的问题稍后解决。

下一步是流水线化上面的循环,Col 循环。 这会自动展开 Product 循环并创建更多运算符,从而创建更多硬件资源,但它确保 Product 循环的不同迭代之间没有依赖性。

流水操作 Col 循环

新建解决方案然后选择Col循环标签添加PIPELINE指令,并移除Product中的流水操作,如下图所示:

HLS实践 - 06 - 优化设计_数组_06

在综合期间,控制台窗格中报告的信息同样会显示循环 Product 已展开,循环展平已在循环 Row 上执行,并且由于阵列 a 的内存资源限制,无法在循环 Row_Col 上实现默认启动内部目标 1。

HLS实践 - 06 - 优化设计_数组_07

查看综合报告显示,如上所述,循环 Row_Col 的间隔只有两个:目标是每个循环处理一个样本。 可以使用分析透视图突出显示未实现启动目标的原因。

HLS实践 - 06 - 优化设计_HLS_08

上图显示了对数组 a 的操作,数组 a 有三个读操作。 两个操作从周期1开始,第三个读取操作从周期2 开始。

在资源的视图中可以看到数组a的使用情况,

HLS实践 - 06 - 优化设计_数组_09

在图中,数组a在三个时钟周期内都有调用,说明这部分资源存在复用。端口 b 和端口 a相似,也会出现同样的问题:它也必须执行 3 次读取。

重构数组

所以仅仅使用流水操作无法满足我们的目标要求,HLS允许对数组进行分区、映射和重构。这些技术允许在不更改源代码的情况下修改对数组的访问。

因为 Product 循环的循环索引是 k,所以两个数组都应该沿着它们各自的 k 维度进行分区:设计需要在每个时钟周期访问两个以上的 k 值。

对于数组 a,这是维度 2,因为它的访问模式是 ​​a[i][k]​​​ ; (对于数组a来说,在最内层的循环中,k值改变读取的是数组下一行的值,也就是二维数组的第一列,所以维度是2)对于数组 b,这是维度 1,因为它的访问模式是 ​​b[k][j]​​,(对于数组b来说,在最内层的循环中,k值改变读取的是数组下一列的值,也可以理解为是一维数组的一行数据,所以维度是1)

所以根据上面的描述添加相关指令,对数组a添加重构指令,维度为2,对数组b添加重构指令,维度为1。如下图:

HLS实践 - 06 - 优化设计_迭代_10

添加完成后进行C综合,并分析综合报告。

HLS实践 - 06 - 优化设计_时钟周期_11

综合报告显示顶层循环 Row_Col 现在正在以每个时钟周期 1 个样本处理数据。顶层模块需要 13 个时钟周期才能完成。Row_Col 循环在 4个周期(迭代延迟)后输出一个样本。然后它在每个周期(启动间隔)读取 1 个样本。9 次迭代后,它完成所有样本计算。4 + 9 = 12 个时钟周期。

流水操作整个函数

新建解决方案然后选择顶层的matrixmul标签添加PIPELINE指令,并移除Col中的流水操作,如下图所示:

HLS实践 - 06 - 优化设计_时钟周期_12

添加完成后运行C综合,然后分析报告,进行对比流水内部循环和整个函数的区别。

HLS实践 - 06 - 优化设计_数组_13

该设计现在以更少的时钟完成,并且可以每 5 个时钟周期开始一个新的事务。 然而,由于设计中的所有环路都展开了,因此面积和资源大幅增加。流水线循环允许循环保持滚动,从而提供了控制区域的好方法。 流水线化整个函数时,函数中包含的所有循环都被展开,这是流水线化的要求。 流水线功能设计可以每 5 个时钟周期处理一组新的 9 个样本。 这超出了每个时钟 1 个样本的要求,因为高级综合的默认行为是生成具有最高性能的设计。

流水线整个函数产生的是最佳性能。 但是,如果它超过了所需的性能,则可能需要多个额外的指令来减慢设计速度。流水线循环为您提供了一种控制资源的简单方法,可选择部分展开设计以满足性能要求。

使用FIFO接口

完成对内部数据处理后,处理函数的接口。设计要求使用FIFO接口。现在进行新建解决方案,然后在a、b、res这三个数组进行设置使用FIFO接口。添加接口指令如下:

HLS实践 - 06 - 优化设计_数组_14

完成指令添加后点击运行C综合,此时无法正常综合,报错如下:

HLS实践 - 06 - 优化设计_时钟周期_15

HLS实践 - 06 - 优化设计_迭代_16

从图中的代码中,数组 res 按以下顺序执行写操作(MAT_B_COLS = MAT_B_ROWS = 3):

  • 在57行写入​​[0][0]​​;
  • 在60行写入​​[0][0]​​;
  • 在60行写入​​[0][0]​​;
  • 在60行写入​​[0][0]​​;
  • 在第 57 行写入​​[0][1]​​(在索引 J 增加之后);
  • 在60行写入​​[0][0]​​;

对地址 ​​[0][0]​​ 的连续四次写入不构成流式访问模式 ; 这是随机访问。所以无法使用流接口。 C 代码的性质(指定对相同地址的多次访问)阻止了流接口的应用。在流接口中,必须按顺序访问这些值。

读取数组 a 和 b 存在类似的问题。 不可能使用 FIFO 接口通过编写的代码进行数据访问。 要使用 FIFO 接口,Vivado 高级综合中可用的优化指令是不够的,因为代码当前强制执行特定的读取和写入顺序。 进一步优化需要重写代码。

下图显示了之前的代码的数据IO访问模式。

HLS实践 - 06 - 优化设计_HLS_17

由于变量 i、j 和 k 从 0 到 3 迭代,下半部分显示了读取 a、b 和写入 res 时生成的地址。 此外,在每个 Product 循环开始时, res 设置为零值。

对于具有顺序流访问的硬件设计,端口访问只能是以红色突出显示的那些。 对于读端口,数据必须在内部缓存,以确保设计不必重新读取端口。 对于写端口 res,数据必须保存到一个临时变量中,并且只能在红色显示的周期内写入端口。

所以要对设计进行拓展实现图中的数组的赋值操作。main函数修改成下面:

#include "matrixmul.h"

void matrixmul(
mat_a_t a[MAT_A_ROWS][MAT_A_COLS],
mat_b_t b[MAT_B_ROWS][MAT_B_COLS],
result_t res[MAT_A_ROWS][MAT_B_COLS])
{
mat_a_t a_row[MAT_A_ROWS];
mat_b_t b_copy[MAT_B_ROWS][MAT_B_COLS];
int tmp = 0;

// Iterate over the rowa of the A matrix
Row: for(int i = 0; i < MAT_A_ROWS; i++) {
// Iterate over the columns of the B matrix
Col: for(int j = 0; j < MAT_B_COLS; j++) {
// Do the inner product of a row of A and col of B
tmp=0;
// Cache each row (so it's only read once per function)
if (j == 0)
Cache_Row: for(int k = 0; k < MAT_A_ROWS; k++)
a_row[k] = a[i][k];

// Cache all cols (so they are only read once per function)
if (i == 0)
Cache_Col: for(int k = 0; k < MAT_B_ROWS; k++)
b_copy[k][j] = b[k][j];

Product: for(int k = 0; k < MAT_B_ROWS; k++) {
tmp += a_row[k] * b_copy[k][j];
}
res[i][j] = tmp;
}
}
}

指令修改成下图:

HLS实践 - 06 - 优化设计_迭代_18

此时进行C综合可以完成综合,对比之前设计,此时的延迟进一步被优化,而资源仅仅在FF和LUT进行少量的增加。

HLS实践 - 06 - 优化设计_迭代_19

references

  1. UG871


举报

相关推荐

0 条评论