数据驱动的任务级并行度 - 2023.2 简体中文

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

Document ID
UG1399
Release Date
2023-12-18
Version
2023.2 简体中文

数据驱动的任务级并行度所使用的任务通道建模样式要求您以静态方式显式例化和连接任务与通道。这些任务不受任何函数调用/返回语义控制,而是始终保持运行,等待其输入串流上的数据。此建模样式中的任务通常仅包含串流类型的输入和输出。但您可以向 TLP 设计中的阵列和标量 I/O 引入未同步指针,如 数据驱动的 TLP 中未同步的 I/O 中所述。

数据驱动的 TLP 模型即存在等待处理的数据时执行的任务。在 Vitis HLS 中,C 语言仿真原先仅限于查看顺序语义和行为。凭借数据驱动的模型,即可在仿真期间通过 FIFO 通道查看并行任务的并发性质及其交互。

Vitis HLS 工具中实现数据驱动的 TLP 会使用简单的类来进行任务建模 ( hls::task ) 和通道建模 ( hls::stream/hls::stream_of_blocks )

重要: 虽然 Vitis HLS 支持对顶层函数使用 hls::tasks,但对于顶层函数中的接口,则无法使用 hls::stream_of_blocks

以如下所示简单任务通道示例为例:

#include "test.h"
 
void splitter(hls::stream<int> &in, hls::stream<int> &odds_buf, hls::stream<int> &evens_buf) {
    int data = in.read();
    if (data % 2 == 0)
        evens_buf.write(data);
    else
        odds_buf.write(data);
}
 
void odds(hls::stream<int> &in, hls::stream<int> &out) {
    out.write(in.read() + 1);
}
 
void evens(hls::stream<int> &in, hls::stream<int> &out) {
    out.write(in.read() + 2);
}
 
void odds_and_evens(hls::stream<int> &in, hls::stream<int> &out1, hls::stream<int> &out2) {
    hls_thread_local hls::stream<int> s1; // channel connecting t1 and t2      
    hls_thread_local hls::stream<int> s2; // channel connecting t1 and t3
 
    // t1 infinitely runs function splitter, with input in and outputs s1 and s2 
    hls_thread_local hls::task t1(splitter, in, s1, s2); 
    // t2 infinitely runs function odds, with input s1 and output out1
    hls_thread_local hls::task t2(odds, s1, out1); 
    // t3 infinitely runs function evens, with input s2 and output out2 
    hls_thread_local hls::task t3(evens, s2, out2); 
}

特殊的 hls::task C++ 类为:

  • 源代码中新的对象声明,此声明需要特殊限定符。hls_thread_local 限定符的必要性体现在,它能在例化函数(示例中的 odds_and_evens)的多次调用之间,使对象(和底层线程)保持活动状态。

    hls_thread_local 限定符的必要性仅体现在用于确保数据驱动的 TLP 模型的 C 语言仿真与 RTL 仿真展现相同的行为。在 RTL 中,这些函数一旦启动,就会保持处于始终运行模式下。为了确保 C 语言仿真期间的行为不变,hls_thread_local 是必需的,它用于确保每项任务仅启动一次,并且即使被多次调用仍能保持相同状态。如无 hls_thread_local 限定符,则每次新调用该函数都会产生新的状态。

  • 任务对象可隐式管理无限运行函数的线程,为其传递一组实参,这些实参必须为 hls::streamhls::stream_of_blocks
    提示: 不支持任何其他类型的实参。尤其是,even 常量值不能作为函数实参来传递。如需向任务主体传递常量,请将函数定义为模板函数,并将常量作为模板实参传递到此模板函数。
  • 提供的函数(以上示例中的 splitter/odds/evens)被称为任务主体,它周围有隐式无限循环包围,以确保任务保持运行并等待输入。
  • 提供的函数包含流水打拍循环,但需将其设为可刷新的流水线 (FLP) 才能阻止死锁。该工具会自动选择正确的流水线样式,用于给定的流水打拍循环或函数。
重要: hls:task 不应作为函数调用来处理,需改为将 hls::task 视作与通道静态绑定的持久实例来处理。因此,您自行负责确保包含 hls::tasks 的任意函数的多次调用保持统一,或者确保这些调用使用相同的 hls::tasks 和通道。

通道由特殊模板化 hls::stream(或 hls::stream_of_blocks)C++ 类来进行建模。此类通道具有以下属性:

  • 在数据驱动的 TLP 模型中,hls::stream<type,depth> 对象行为与具有指定深度的 FIFO 类似。此类串流默认深度为 2,用户可覆盖该值。
  • 按顺序对这些串流执行读取和写入。这暗含意义是一旦从 hls::stream<> 读取数据项,就无法重复读取该数据项。
    提示: 对不同串流执行访问不经过排序,例如,写入串流和读取另一串流的顺序可通过调度器来更改。
  • 串流可采用局部定义或全局定义。全局作用域内定义的串流遵循的规则与任何其他全局变量相同。
  • hls_thread_local 限定符对于串流(以下示例中的 s1s2)也是必需的,它能在例化函数(以下代码示例中的 odds_and_evens)的多次调用之间,使相同串流保持活动状态。

下图显示了 Vitis HLS 中上述代码示例的图形化表示法。在此图中,绿色箭头为 FIFO 通道,蓝色箭头表示例化函数 (odds_and_evens) 的输入和输出。任务显示为蓝色矩形框。

图 1. hls::task 示例的数据流图示

鉴于实际上读取空串流属于阻塞读取,因此可能由于下列原因引发死锁:

  • 设计本身内部的进程的生产和耗用率不平衡。
    • 在 C 语言仿真期间,仅限下列原因才会导致死锁:某个进程周期或者从顶层输入启动的进程链尝试读取空的通道。
    • 在 C/RTL 协同仿真期间以及在硬件 (HW) 中运行时,可能由于某个进程周期尝试写入已满的通道或者读取空的通道而发生死锁。
  • 如果测试激励文件提供的数据太少,不足以生成该测试激励文件在检查计算结果时所需的所有输出,则会发生死锁。

因此,当设计包含 hls::task 时,会自动例化死锁检测器。该死锁检测器会检测到死锁并停止 C 语言仿真。此外,还会使用 C 语言调试器(如 gdb)进行进一步调试,观察仿真的 hls::tasks 尝试读取空的通道时,全部遭到阻塞的具体位置。使用 Vitis HLS GUI 能轻松完成此操作,正如有关调试死锁的 handling_deadlock 示例中所示。

总之,如果您的设计需要完全数据驱动的纯串流类型的行为,且无需任何种类的控制,那么推荐使用 hls::task 模型。此类模型对于反馈和动态多重速率设计也都很有用。只要任务间存在周期性依赖关系,设计中就会发生反馈。动态多重速率模型中如果生产者写入数据或者使用者读取数据的速率与数据相关,那么就只能由数据驱动的 TLP 来处理。如需获取示例,请参阅 GitHub 上的 simple_data_driven 设计。

提示: 静态多重速率设计中如果生产者写入数据或者使用者读取数据的速率与数据无关,则数据驱动的 TLP 和控制驱动的 TLP 均可用于对其进行管理。例如,生产者在某一串流内为每次调用写入 2 个值,使用者为每次调用读取 1 个值。