Skip to content

Commit

Permalink
Update 04-Discrete-Fourier-Transform.md
Browse files Browse the repository at this point in the history
  • Loading branch information
vickyiii authored Jun 11, 2018
1 parent 9a4b18b commit 62bff71
Showing 1 changed file with 45 additions and 8 deletions.
53 changes: 45 additions & 8 deletions 04-Discrete-Fourier-Transform.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ data_loop:
```
图4.4 实现矩阵向量乘法的简单代码

## 4.3 矩阵向量乘法的优化
##4.3 矩阵向量乘法的优化

矩阵向量乘法是DFT计算的核心。输入的时域向量将乘以一个固定特殊值的矩阵。输出的结果是与输入时域信号表示相对应的频域矢量。
在本节中,我们讨论如何在硬件中实现矩阵向量乘法。我们把这个问题分解成最基本的形式(见图4.4)。这使我们能够更好地将讨论集中在算法优化上,而不是集中在使用功能正确的DFT代码的所有复杂点上。我们将在下一节中构建一个DFT内核。
Expand All @@ -140,7 +140,7 @@ data_loop:

图4.5显示了包含一个乘法和一个加法运算符的矩阵向量乘法的顺序结构。我们创建逻辑以访问存储在BRAM中的V_In和矩阵M。计算V_Out的每个元素并存储到BRAM中。这种体系结构本质上是将图4.4中的代码合成为无指令的结果。它不占用大量面积,但任务延迟和任务间隔相对较大。

## 4.4 流水线和并行运行
##4.4 流水线和并行运行

在矩阵乘法的例子中,我们可以很大程度地利用并行思想来解决问题。首先关注每次迭代循环执行的内部循环表达式 $sum+= V_in [j] * M [i] [j]$。乘法运行时,计数变量SUM在每次迭代中都被重复利用并赋予新的值。如图4.6所示,这个内部循环可以重新表述,此时变量SUM已被完全消除,并在较大表达式中替换为多个中间值。
```
Expand Down Expand Up @@ -186,7 +186,7 @@ data_loop:

现在你可能已经观察到,我们可以在不同的层次级别上进行流水线操作,包括算法级别,循环级别和功能级别。此外,不同级别的流水线在很大程度上也是独立的!我们可以在顺序循环中使用流水线操作符,或者我们可以使用顺序操作符来构建流水线循环,也可以构建大型功能的流水线实现。这些功能可以像原始运算单元一样在Vivado HLS 中共享。我们实例化了多少运算单元,它们的个体成本以及使用频率如何才是最重要的。

## 4.5 存储权衡和数据分区
##4.5 存储权衡和数据分区

到了本小节,我们已经假定数组中的数据 V_In[],M[][]和V_Out[]可以随时访问,但是实际上,数据的放置的位置对整个处理器的性能和资源使用情况有重要影响。在大多数处理器系统中,内存架构是固定的,我们只能调整程序以尝试最大程度地利用可用的内存层次结构,例如注意尽可能减少寄存器溢出和缓存丢失。在HLS设计中,我们还可以利用不同的存储器结构,并尝试找到最适合特定算法的存储器结构。通常,大量数据存储在片外存储器如DRAM、闪存或网络连接的存储器中,但是数据访问时间通常很长,大约为几十到几百(或更多)个周期。由于大量的电流必须流过长电线已访问片外存储器,所以使用片外存储消耗的能量也比较大。相反,片上存储器可以快速访问并且功耗要低得多,只是它可以存储的数据量有限。有一种常见的操作模式类似于通用CPU的内存层次结构中的缓存效果,它是将数据重复地加载到块中的片上存储器上。

Expand Down Expand Up @@ -347,20 +347,57 @@ void dft(IN_TYPE sample_real[N], IN_TYPE sample_imag[N]) {
}
```
![图4.15:DFT的基线代码.](images)

如果你要使用你设计的CORDIC(如从第3章开始),那么此代码需要做什么修改? 改变CORDIC核心的准确性会使DFT硬件资源使用情况发生变化吗? 它会如何影响性能?

请使用HLS实现DFT的基线代码,实现后查看报告,与乘法和加法相比,实现三元函数的相对成本是多少? 对哪些操作尝试优化更有意义?通过流水线操作内循环可以实现什么性能?

## 4.7 DFT 优化
##4.7 DFT 优化

上一节的基线的DFT实现使用了相对较高的精度双数据类型。实现浮点运算尤其是双精度浮点运算通常代价很高并且需要很多流水线操作。我们可以从图4.16中看到这显着影响了循环的性能。通过流水线操作,这些高延迟操作的影响不那么重要,因为可以同时执行多个循环执行。此代码中的例外是用于累加结果的变量temp real []和temp imag []。这个累加是一种循环,并在流水线化内循环时限制了可实现的II。该运算符的依赖性如图4.17所示。

降低计算的精度是一种可能的解决方案。这种方法在实际应用时是有价值的,因为它减少了每个操作所需的资源,减少了存储值所需的内存,并且通常也减少了操作的延迟。例如,我们可以使用32位浮点型或16位半型来替代双精度型。许多信号处理系统完全避免了浮点数据类型,并使用定点数据类型3.5。对于常用的整数和定点精度,每个加法可以在一个循环中完成,从而使循环在II = 1处流水线化。
一种可能的解决方案是降低计算的精度。这种方法在实际应用时是有价值的,因为它减少了每个操作所需的资源,减少了存储值所需的内存,并且通常也减少了操作的延迟。例如,我们可以使用32位浮点型或16位半型来替代双精度型。许多信号处理系统完全避免了浮点数据类型,并使用定点数据类型3.5。对于常用的整数和定点精度,每个加法可以在一个循环中完成,从而使循环在II = 1处流水线化。

如果将所有数据类型从双精度型更改为浮点型,那么图4.15中的代码综合结果会发生什么变化?还是从一倍到一半?或到一个固定的点值?这是如何改变性能(间隔和延迟)和资源使用情况的?它是否会更改输出频率域采样值?

用浮点累加实现II = 1的普适解决方案是用不同的顺序处理数据。看图4.17,我们看到由于j循环是内部循环,所以重复是存在的(用箭头表示)。如果内循环是i循环,那么在下一次迭代开始之前我们就不需要累加的结果。我们可以通过交换两个循环的顺序来实现这一点。这种优化方式通常被称为循环交换或流水线交织处理[40]。由于外循环i内部的额外代码,我们可能不是很容易地发现可以重新排列循环。由于S矩阵是对角对称的,因此i和j可以在w的计算中交换。

结果是我们现在可以在内部循环中实现1的II。但是我们需要为临时实值和临时图像数组设置额外的存储器,存储计算的中间值一直到再次需要这些数据。

![图4.16:DFT的高层体系结构图,如图4.15所示。 这不是该体系结构的综合视图,例如,它缺少与更新循环计数器i和j的相关内容。该图想向读者提供关于如何合成这种体系结构的近似概念。这里我们假定浮点运算符需要4个时钟周期。.](images/dft_behavior_baseline.jpg/dft_sequential_arch.jpg)

如果将所有数据类型从双精度型更改为浮点型,那么图4.15中的代码综合结果会发生什么变化? 还是从一倍到一半? 或到一个固定的点值? 这是如何改变性能(间隔和延迟)和资源使用情况的? 它是否会更改输出频率域采样值?
![图4.17:图4.16中的行为的流水线版本。在这个设计案例下,由于每个浮点的添加需要4个时钟周期才能完成,并且在下一个循环开始之前需要上一个循环的结果加入计算(以红色显示相关性),所以设置循环的启动间隔为4个间隔。所有迭代的依赖关系汇总在右图中。.](images/)

重新排列图4.15中代码的循环,并显示您可以使用1的II来管理内部循环。

根据DFT中S矩阵的结构,我们可以应用其他优化方式来完全消除三角运算。回想一下,S矩阵的每个元素的复矢量是通过单位圆的固定旋转角度来计算的。S矩阵的行S[0][]对应于单位圆周的零旋转,行S[1][]对应于单次旋转,并且随后的行对应于单位圆周更大角度的旋转。我们可以发现,第二行S[1][]相对应的向量覆盖了来自其他行的所有向量,因为8个列向量每个绕单位圆旋转45°,一共则围绕单位圆旋转了一圈。我们可以通过研究图9.11来直观地确认这个现象。这样我们可以只存储第二行这一次旋转中的正弦和余弦值,然后索引到这个存储器中的值以计算相应行的必要值。这只需要2×N=O(N)个存储单元可以有效减少存储器的O(N)的大小。对于1024个点的DFT,存储器的存储需求将减少到1024×2个条目。假设有一个32位的固定值或浮点值,则只需要8KB大小的片上存储器。显然,与明确存储整个S矩阵相比,明显减少了存储容量。我们将矩阵S的这个一维存储表示为$s^{'}$
$$
S{’}=S[1][.]=\begin{bmatrix}
1 & s &s^{2} &\ldots &s^{n-1}\\
\end{bmatrix}
$$
{% hint style=‘tip’ %}
导出一维数组$s^{'}$对应于数组S的行号i和列元素j的输入的访问模式的公式。也就是说,我们如何才能索引到一维S数组以访问元素S(i; j)呢?
{% endhint %}

为了进一步提高性能,我们可以应用一种与矩阵向量乘法非常相似的技术。之前我们发现了提高矩阵向量乘法的性能需要对M[][]数组进行分区。但是如果用$s^{'}$ 表示S矩阵则意味着不再有一种有效的方法来划分$s^{'}$ 以增加我们在每个时钟上读取的数据量。S的每一个奇数行和列都包括$s^{'}$ 的每个元素。因此,我们无法像对S一样对$s^{'}$ 的值进行分区。这样增加我们从存储$s^{'}$ 的内存中读取数据端口的数量的唯一方法是复制存储。幸运的是,不像与必须读取和写入的内存,复制只读的数组的存储是相对容易的。事实上,Vivado HLS将只对在初始化且从未例化的只读存储器(ROM)自动执行此优化。这种功能的一个优点是我们可以简单地将sin()和cos()调用移动到数组初始化中。在大多数情况下,如果此代码位于函数的开头并仅初始化阵列,则Vivado HLS能够完全优化三角函数计算并自动计算ROM的内容。

{% hint style=‘tip’ %}
设计一个利用$s^{'}$——S矩阵的一维版本的体系结构。这种体系结构是如何影响到所需的存储空间的?与使用二维的S矩阵的实现相比,这会改变逻辑利用率吗?
{% endhint %}

为了有效地优化设计,我们必须考虑代码的每个部分。往往是最薄弱的环节决定了设计的整体性能,这意味着,如果设计有一个瓶颈,将显著影响设计的性能。当前版本的DFT可以对输入和输出数据进行就地操作,即它存储结果相同的数组作为输入数据,输入数组 sample_real 和 sample_imag 都是有效的存储器端口,也就是说,你可以把这些参数的数组存储在相同的存储位置。这样,在任意给定的周期,我们只能获取其中一个阵列的一个数据,这可能会在函数中并行的乘法和加法运算方面产生瓶颈。这就是我们为什么必须将所有的输出结果存储在一个临时数组的原因,然后将所有这些结果复制到函数结尾处的“sample”数组中。如果我们没有执行就地操作,则不需要这样做。

{% hint style=‘tip’ %}
修改DFT函数接口,使输入和输出存储在单独的数组中。这会如何影响你的可以执行优化?它如何改变性能? 区域结果如何?
{% endhint %}

更普适的解决浮点累加以实现II = 1的方案是以不同的顺序处理数据。如图4.17,我们看到由于j循环是内部循环,所以重复是存在的(用箭头表示)。如果内循环是i循环,那么在下一次迭代开始之前我们就不需要累加的结果。我们可以通过交换两个循环的顺序来实现这一点。这种优化方式通常被称为循环交换或流水线交织处理[40]。由于外循环i内部的额外代码,我们可能较难发现可以重新排列循环。因为S矩阵是对角对称的,所以i和j可以在w的计算中交换。
##4.8 结语
在本章中,我们探究了离散傅里叶变换(DFT)的硬件实现和优化方法。DFT是数字信号处理的基本操作,需要采样时域的信号并将其转换到频域。在本章的开头,我们描述了DFT的数学背景。这对于理解下一章(FFT)中所做的优化很重要。本章的其余部分集中介绍了指定和优化DFT以在FPGA上进行高效的实现。

结果是我们现在可以在内部循环中实现1的II。 但是我们需要为临时实值和临时图像数组设置额外的存储器,存储中间值直到再次需要这些数据
由于DFT的核心是执行矩阵向量乘法,所以我们最初花费了一些时间来描述在执行矩阵向量乘法的简化代码上的指令级优化。这些指令级优化由HLS工具完成。我们用这个机会来阐明HLS工具执行指令优化的过程,希望如上的优化过程能让你直观地了解到工具优化的结果

本章后节,我们为DFT提供了正确的功能实现方案,讨论了一些可以改善性能的优化,具体来说就是将系数阵列划分为不同存储器以提高吞吐量。阵列的分区优化通常是构建最高性能体系结构的关键方法。


0 comments on commit 62bff71

Please sign in to comment.