HLS 拆分/合并库 - 2023.2 简体中文

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

Document ID
UG1399
Release Date
2023-12-18
Version
2023.2 简体中文
重要: 要在代码中使用 hls::split<>hls::merge<> 对象,请按如下示例所示方式包含头文件 hls_np_channel.h

拆分/合并通道可供在数据流进程内使用,支持您创建一对多或多对一类型的通道,用于将数据分发给多项任务,或者聚集来自多项任务的数据。这些通道具有内置作业调度器,使用循环方法按顺序跨通道分发或收集数据,或者采用基于通道可用性来判定的负载均衡方法。

提示: 负载均衡可能导致 RTL/协同仿真内出现非确定性结果。在此情况下,您将需要重写测试激励文件,使其不受结果的顺序影响。

如下图所示,数据读取自输入串流,通过循环调度器机制进行拆分,并分发至关联的工作程序任务。当工作程序完成任务后,同样会采用循环调度器将合并的输出写入单一串流。

图 1. 拆分/合并数据流

拆分通道具有单一生产者和众多使用者,通常可用于将任务分发给一系列工作程序,在 RTL 中将分发逻辑加以抽象化并实现,从而提升性能并减少耗用的资源。从 N 个输出中的任一输出分发输入的操作可采用:

  • 循环法,其中使用者按固定轮换顺序读取输入数据,这样即可确保确定性行为,但不允许负载共享,且工作程序间存在动态可变的计算负载。
  • 负载均衡,其中尝试执行读取的首个使用者将读取首个输入数据,这样可确保良好的负载均衡,但存在非确定性结果。

每条合并通道都具有多个生产者和单一使用者,基于相反逻辑来工作:

  • 循环法,其中按固定轮换顺序合并生产者输出数据,这样即可确保确定性行为,但不允许负载共享,且工作程序间存在动态可变的计算负载。
  • 负载均衡合并通道,其中完成工作的首个生产者将首先写入通道,这存在非确定性结果。

拆分与合并的一般构想是借助 round_robin(循环)调度器,以确定性方式将数据分发给周围工作程序以供拆分,从工作程序读取数据以供合并。因此,如果所有工作程序都计算相同函数,那么结果与使用单一工作程序的计算结果相同,但性能更好。

如果工作程序执行不同的函数,那么设计必须确保按工作程序循环方式(分别从 out[0] 或 in[0] 开始),将正确的数据项发送到正确的函数。

规范

拆分/合并通道规范如下:
hls::split::load_balancing<DATATYPE, NUM_PORTS[, DEPTH, N_PORT_DEPTH]> name; 
hls::split::round_robin<DATATYPE, NUM_PORTS[, DEPTH]> name
hls::merge::load_balancing<DATATYPE, NUM_PORTS[, DEPTH]> name
hls::merge::round_robin<DATATYPE, NUM_PORTS[, DEPTH]> name
其中:
  • round_robin/load_balancing:指定用于该通道的调度器机制类型。
  • DATATYPE:指定通道上的数据类型。这其中存在与标准 hls::stream 相同的限制。DATATYPE 可设为:
    • 任何 C++ 原生数据类型
    • Vitis HLS 任意精度类型(例如,ap_int<> ap_ufixed<>
    • 用户定义的结构体(包含以上任意类型)
  • NUM_PORTS:表示拆分操作所需的写入端口数量 (1:num) 或者合并操作所需的读取端口数量 (num:1)。
  • DEPTH:可选实参,表示主缓冲器的深度,位于拆分之前或合并之后。此项可选,如不指定,则默认深度为 2。
  • N_PORT_DEPTH:可选字段,供循环法用于指定在拆分之后或合并之前应用的输出缓冲器的深度。此项可选,如不指定,则默认深度为 0。
    提示: 要指定可选 N_PORT_DEPTH 值,还必须指定 DEPTH
  • name:指定创建的通道对象的名称
GitHub 上的 mixed_control_and_data_driven 提供了如下所示示例:
#include "hls_np_channel.h"

const int N = 16;
const int NP = 4;

void dut(int in[N], int out[N], int n) {
#pragma HLS dataflow
  hls::split::round_robin<int, NP> split1;
  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);
}
提示: 以上示例显示了作为 hls::task 对象实现的工作程序。但这只是示例的一项特征,而非 split/merge 通道的要求。

拆分/合并的应用

拆分与合并的主要用途是支持多次计算引擎例化,以便充分利用 DDR 或 HBM 端口的带宽。在此情况下,生产者是负载进程,从 MAXI 读取数据突发,然后通过拆分通道将各个待处理的数据包发送到多个工作程序。如果工作程序需耗费大量时间,请使用循环协议,或者如果输入可变,则根据输入对执行时间进行负载均衡。使用者则执行相反操作,将数据写回 DRAM。

提示: 如果使用负载均衡,那么写回地址可随数据一起通过拆分与合并进行传递。

在拆分通道或合并通道两端,通过实现 hls::stream 对象的方式对这些通道进行建模。这意味着可将拆分或合并通道端连接到取 hls::stream 作为输入或输出的任何进程。此进程无需知晓通道连接的类型。因此,这两种类型的连接均可用于标准数据流或 hls::task 对象。

以下示例显示了单一生产者和多个使用者使用拆分的方式:

#include "hls_np_channel.h"
 
void producer(hls::stream<int> &s) {
  s.write(xxx);
}
 
void consumer1(hls::stream<int> &s) {
  ... = s.read();
}
 
void consumer2(hls::stream<int> &s) {
  ... = s.read();
}

void top-func() {
#pragma HLS dataflow
  hls::split::load_balancing<int, 4, 6> s; // NUM_PORTS=4, DEPTH=6
 
  producer(s.in, ...);
  consumer1(s.out[0], ...);
  consumer2(s.out[1], ...);
  consumer3(s.out[2], ...);
  consumer4(s.out[3], ...);
}