Tasks and Dataflow - 2023.2 English

Vitis High-Level Synthesis User Guide (UG1399)

Document ID
UG1399
Release Date
2023-12-18
Version
2023.2 English

hls::task also supports the definition of tasks inside of dataflow regions. The dataflow region allows the definition of processes that access external arrays mapped to M_AXI, scalar, or PIPO arguments from upper levels of the design hierarchy. This requires dataflow processes identified by the #pragma HLS dataflow statement, synchronized via ap_ctrl_chain, that read data from any non-streamed C++ data structure and output it as hls::stream or hls::stream_of_blocks channels for connection to hls::tasks. Tasks can then output streams that are read by other dataflow processes and written to M_AXI, scalar, or PIPO arguments. Tasks must be declared after the processes or regions that produce their input streams, and before the processes or regions that consume their output streams.

Important: Because hls::task objects cannot read or write M_AXI, scalar, or PIPO arguments dataflow processes must read or write these interfaces and write or read stream channels to hls::tasks as shown in the example below.

The following example illustrates tasks and dataflow processes together. The top-level function (top-func) is a dataflow region that defines sequential functions write_out() and read_in(), as well as hls::task objects and hls::stream channels.

#include "hls_task.h"

// This is an I/O dataflow process
void write_out(int* out, int n, hls::stream<int> &s2) {
  for (int i=0; i<n; i++)
    out[i] = s2.read();
}
// This is an I/O dataflow process
void read_in(int* in, int n, hls::stream<int> &s1) {
  for (int i=0; i<n; i++)
    s1.write(in[i]);
}
// This is an hls::task body
void func1(hls::stream<int> &s1, hls::stream<int> &s3) {
  // No while(1) needed! This will be a task
  s3.write(... + s1.read());
}
// This is an hls::task body
void func2(hls::stream<int> &s3, hls::stream<int> &s2) {
  // No while(1) needed! This will be a task
  s2.write(... * s3.read());
}
// This could legally be at the top of the design hierarchy
void top-func(int *in, int *out, int n) {
#pragma HLS dataflow
  hls_thread_local hls::stream<int> sk3;
  hls_thread_local hls::stream<int> sk1;
  hls_thread_local hls::stream<int> sk2;
 
  read_in(in, n, sk1);                              // can access stream, scalar or array; calling order matters
  hls_thread_local hls::task t2(func2, sk3, sk2);   // can access only stream; instance order does not matter
  hls_thread_local hls::task t1(func1, sk1, sk3);   // can access only stream; instance order does not matter
  write_out(out, n, sk2);                           // can access stream, scalar or array; calling order matters
}

#pragma HLS DATAFLOW is required for the two sequential functions, but the hls::task objects do not require it. Internally, Vitis HLS will automatically split top-func, including both regular dataflow processes and hls::task processes into two dataflow regions:

  1. One dataflow region using ap_ctrl_chain that contains regular dataflow processes, like read_in() and write_out(), in the order in which they appear in the C++ code, and a call to the ap_ctrl_none region below
  2. A second dataflow region using ap_ctrl_none containing the task and channels.

As a result of this, you can expect to see two levels of hierarchy in the Dataflow viewer in the Vitis HLS GUI.