下表着重讲解了 HLS 设计的各项要素,这些要素有助于您判定何时应用控制驱动的任务级并行度 (TLP) 或数据驱动的 TLP。
控制驱动的 TLP | 数据驱动的 TLP |
---|---|
HLS 设计需要通过控制信号来启动/停止进程 | HLS 设计使用完全数据驱动的方法,无需控制信号来启动/停止进程 |
设计需要非本地存储器访问权限 | 设计使用纯串流数据传输 |
设计需要与外部软件应用进行交互 | 设计具有数据相关的多重速率行为:
|
设计具有多个进程,并且这些进程运行的执行数相同 | 可在 C 语言仿真和 RTL 仿真中观察到任务级并行度。 |
需要 RTL 仿真来对并行效果进行建模 |
正如上表所示,所提供的两张形式的任务级并行度具有不同的用例和优势。但有时,要将整个应用设计成纯数据驱动的 TLP,就不可能同时还将设计的某些部分构造为纯串流设计。在此情况下,适合使用混用控制驱动/数据驱动的建模来创建该应用。请参阅来自 GitHub 的以下 mixed_control_and_data_driven 示例。
void dut(int in[N], int out[N], int n) {
#pragma HLS dataflow
hls_thread_local hls::split::round_robin<int, NP> split1;
hls_thread_local hls::merge::round_robin<int, NP> merge1;
read_in(in, n, split1.in);
// Task-Channels
hls_thread_local hls::task t[NP];
for (int i=0; i<NP; i++) {
#pragma HLS unroll
t[i](worker, split1.out[i], merge1.in[i]);
}
write_out(merge1.out, out, n);
}
在以上示例中,有两个不同的区域,一个是数据流区域,其中包含 read_in/write_out 函数,这些函数中的顺序语义将予以保留,例如,先执行 read_in
,后执行 write_out
,另一个是任务通道区域,其中包含 4 项任务(因为在此示例中 NP = 4)的动态例化以及一些特殊类型的通道,称为 split
通道或 merge
通道。split(拆分)通道具有单个输入和多个输出,在此例中,拆分通道具有 4 个输出,如 HLS 拆分/合并库 中所述。同样,merge(合并)通道具有多个输入和单个输出。
此外,对于端口,这些通道也支持内部作业调度器。在以上示例中,合并通道与拆分通道均已选中循环调度器来将传入数据逐一分配到这 4 个任务,从 worker_U0
开始。如果已选中负载均衡调度器,那么传入数据已分配给首个可用的工作程序任务(这会导致非确定性仿真,因为每次运行仿真时,此顺序都可能不同)。由于这是纯任务通道区域,只要这 4 项任务的传入串流中存在数据,这些任务就会立即并行执行。请参阅 GitHub 上提供的 merge_split 示例,获取有关这些概念的更多示例。
请务必注意,虽然以上代码可能看似在循环中“调用”了每个任务,并且每次执行循环主体时,任务都可能连接到不同的通道对,但实际上,这种用法暗示静态例化,即:
- 每次执行
dut()
时,都只能执行一次t[i](...)
调用。 - 高于
i
的循环必须完全展开,以便推断 RTL 中对应的一组 4 个实例。 - 测试激励文件只能调用一次
dut()
函数。 - 每个拆分输出或合并输入必须仅限绑定到一个
hls::task
实例。
虽然规范的顺序对于 hls::task
对象无关紧要,但对于控制驱动的数据流网络,Vitis HLS 必须能够看到存在的进程链,例如,从 read_in
到 write_out
。Vitis HLS 使用调用顺序来定义此进程链,对于 hls::tasks
而言,此调用顺序同时也是声明顺序。这意味着该模型必须在数据流区域内定义从 read_in
函数到 hls::task
区域以及最后到 write_out
函数的显式顺序,如以上示例所示。
- 如果基于控制的进程(如,常规数据流)为每个
hls::task
生成一条串流,那么必须先调用该进程,然后才能调用代码中的任务声明 - 如果基于控制的进程耗用来自某个
hls::task
的一条串流,那么必须先调用代码中的任务声明,然后再调用该进程
违反上述规则可能导致意外结果,因为每个 NP hls::task
实例都静态绑定到 t[i](...)
的首次调用中使用的通道。
下图显示了此混用任务通道与数据流的图例: