组合三种范例 - 2021.2 Chinese

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

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

用户程序中,大部分最优化的主要焦点是函数和循环。现如今的最优化工具通常在函数/过程级别工作。每个函数都能转换为特定硬件组件。每个此类硬件组件都与类定义相似,该组件的许多对象(或实例)均可在最终硬件设计中创建和例化。每个硬件组件都将由许多更小的预定义组件组成,这些预定义组件通常用于实现基本函数(例如,加法、减法和乘法)。虽然不支持递归,但是函数可以调用其它函数。较小且调用次数较少的函数通常还可以内联到其调用方函数中,正如软件函数内联方式一样。在此情况下,实现函数所需的资源将汇总到调用方函数的组件中,这样能更好地共享公用资源。将设计构造为一组通信函数有助于在执行这些函数时提升推断并行度。

循环是程序中最重要的构造之一。由于循环主体多次迭代,因此可轻松利用该属性来提升并行度。可通过多种方式来对循环和循环嵌套进行变换(例如,流水打拍展开),从而提升并行执行的效率。这些变换使存储器系统最优化以及映射到多核和 SIMD 执行资源成为可能。科学与工程应用中的许多程序都表现为对大型数据结构进行各种运算。这些运算包括对阵列或矩阵进行简单的逐元素运算,或者也可能是具有循环进位依赖关系的更复杂的循环嵌套运算,如跨循环迭代的数据依赖关系。此类数据依赖关系会影响循环内可达成的并行度。在诸多此类情况下,必须对代码进行重构,才能在现代化的并行平台上有效执行并行循环迭代。

下图显示了 4 个连续任务(即,C/C++ 函数)A、B、C 和 D 的不同重叠执行的简单示例,其中,A 在 2 个不同阵列内分别为 B 和 C 生成数据,D 则使用来自 B 和 C 所生成的 2 个不同阵列的数据。假定这种“菱形”通信模式将运行 2 次,且 2 次运行彼此独立。

void diamond(data_t vecIn[N], data_t vecOut[N])
{
   data_t c1[N], c2[N], c3[N], c4[N];
   #pragma HLS dataflow
   A(vecIn, c1, c2);
   B(c1, c3);
   C(c2, c4);
   D(c3, c4, vecOut);
}

以上代码示例所示 C/C++ 源代码片段显示了这些函数的调用方式。请注意,任务 B 和 C 相互之间不存在数据依赖关系。下图对应完全顺序执行方式,其中黑色圆圈表示用于实现串行的某种同步形式。

图 1. 顺序执行 - 运行 2 轮

在菱形示例中,B 与 C 完全彼此独立。两者既不相互通信也不访问任何共享存储器资源,因此如果无需共享计算资源,那么两者可并行执行。由此可得结果如下图所示,一轮运行内形成了某种形式的分叉式连接并行运行。当 A 任务结束后,B 和 C 并行执行,而 D 则等待 B 和 C,但下一轮仍按顺序连续执行。

图 2. 一轮运行中的任务并行

此类执行方式可总结为 (A; (B || C); D); (A; (B || C); D),其中“;”表示串行连续,“||”则表示完全并行。这种形式的嵌套分叉连接并行运行对应于一种从属任务子类,称为串并行任务图。一般来说,任何从属任务的有向无环图 (DAG) 均可通过独立的分叉并连接类型同步来实现。此外同样值得注意的是,这正是在具有多个线程并使用共享存储器的 CPU 上运行多线程程序的方式。

在 FPGA 上,您可以探索其它可用的并行形式。先前的执行模式利用的是在单次调用内执行任务级别并行操作。那么重叠连续运行又会如何呢?如果每次运行之间真正彼此独立,但每个函数(即,A、B、C 或 D)复用前一轮的相同计算硬件,那么我们仍可能想要将 A 的第二次调用与 B 和 C 的第一次调用并行执行。这是一种跨调用的任务级流水线形式,由此可得结果如下图所示。现在,由于吞吐量受到所有任务间的最大时延的限制,而非所有任务时延总和的限制,因而吞吐量明显改善。虽然每轮运行的时延不变,但多轮运行的总时延得以缩短。

图 3. 利用流水打拍实现任务并行

但现在,当 B 首次运行从存储器中执行读取时,A 则已得到其首轮运行的结果,A 的第二轮操作可能已在相同存储器内执行写入。为避免在使用数据之前写入数据,您可以依靠某种形式的存储器扩展(即所谓的双重缓冲或 PIPO)来达成此交织操作。这种交织操作以任务间的黑色圆圈来表示。

有一种有效的方法可用于提升吞吐量和复用计算资源,即对运算符、循环和/或函数进行流水打拍。如果每个任务现在都能与自身重叠,您即可在一轮运行内实现任务并行操作,同时跨多轮运行实现任务流水打拍,这两者都属于宏观级别并行度的例证。多任务内流水打拍则是微观级别并行度的例证。现在,由于每一轮运行依赖于任务间的最小吞吐量而不是任务的最大吞吐量,因此每一轮的总体吞吐量得以进一步提升。最后,根据通信数据的同步方式,仅当生成全部数据 (PIPO) 或者以逐元素方式 (FIFO) 生成全部数据后,才有可能在一轮运行内出现某种程度的额外重叠。例如,在下图中,B 和 C 都比 A 更早启动并以流水打拍方式执行,而 D 则假定仍必须等待 B 和 C 完成。这是最后一种类型的单轮运行内重叠,当 A 通过 FIFO 串流访问(表现为不含圆圈的直线)来与 B 和 C 通信时,才能实现这种方式的重叠。同样,D 也能与 B 和 C 重叠,前提是采用的通道为 FIFO 而不是 PIPO。但不同于前几种执行模式,使用 FIFO 可能导致死锁,因此需要正确设置这些串流 FIFO 的大小。

图 4. 单轮运行内的任务并行和流水打拍、多轮运行的流水打拍以及单一任务内的流水打拍

总之,本节中所演示的三种范例为您展示了如何在避免多线程和/或并行编程语言的复杂操作的同时下,仍能在设计中实现并行操作。生产者使用者范例搭配串流通道即可轻松组合小型系统与大型系统在内的各种系统。如上文所述,串流接口支持轻松耦合并行任务,亦或是分层数据流网络也不在话下。这其中部分原因是由于编程语言 (C/C++) 能够灵活支持此类规范,并且还有各种工具可用于在当今 FPGA 器件上可用的异构计算平台上实现这些规范。