Advanced Dataflow Region Examples - Advanced Dataflow Region Examples - 2026.1 English - UG1399

Vitis High-Level Synthesis User Guide (UG1399)

Document ID
UG1399
Release Date
2026-06-23
Version
2026.1 English

In addition to the basic dataflow coding style discussed above, more advanced features are available.

Unrolling and partitioning for parameterized task-level parallelism

The first additional support is a mechanism to parameterize task-level parallelism, via:

  • Unrolled dataflow loops within a dataflow region. A dataflow pragma is required to ensure that the correctness of the region is checked before unrolling, and after unrolling the code must satisfy the canonical dataflow conditions.
  • Partitioned arrays, where each partition is passed as an independent variable (array or scalar) to one process.

For example, the following code creates a pipeline of N identical processes all performing the same functionality and cascaded via a chain of hls::streams, where N is a compile-time constant:

void dut(int in[M], int out[M]) {
  #pragma HLS dataflow
  hls_thread_local hls::stream<int> chan[N+1]; // arrays of hls::streams are fully partitioned automatically
  read_in(in, chan[0]);
  hls_thread_local hls::task t[N]; // array of worker processes
  for (int i=0; i<N; i++) {
    #pragma HLS unroll
    #pragma HLS dataflow
    t[i](worker, chan[i], chan[i+1]);
  }
  write_out(chan[N], out);
}

Here is another example that instead uses a chain of PIPOs (or streamed arrays if one adds #pragma HLS stream variable=chan):

void dut(int in[M], int out[M]) {
  #pragma HLS dataflow
  int chan[N+1][M]; // partitioned into N+1 arrays of M elements
  #pragma HLS array_partition complete dim=1 variable=chan
  read_in(in, chan[0]);
  for (int i=0; i<N; i++) {
    #pragma HLS unroll
    #pragma HLS dataflow
    worker(chan[i], chan[i+1]);
  }
  write_out(chan[N], out);
}

Finally, the following example performs a partial partitioning and a partial unrolling, still achieving the goal of creating a dataflow network that satisfies the single producer single consumer requirements after partitioning and unrolling. It leads to two parallel processes, each acting on different arrays:

void dut(int a[10], int b[10]) {
  #pragma HLS array_partition cyclic factor=2 variable=a 
  #pragma HLS array_partition cyclic factor=2 variable=b
  for (int i=0; i<10; i++) {
    #pragma HLS dataflow
    #pragma HLS unroll factor=2
    compute(a[i], b[i]); // read a[i], write b[i] 
  }
}

Special Support for Loop-carried Dependencies

The rule that there cannot be loop-carried dependencies in a dataflow region is relaxed for 3 situations:

  • stream and stream of blocks, see below;
  • static scalar within a single non-dataflow process reading (from previous call) before writing, possibly followed by readers: in this case, any loop-carried dependence is satisfied within the process itself;
  • channel array with a single writer textually before a single reader (see #pragma HLS stream type=shared): in this case, flow dependencies remain forward but they can be loop-carried.

Streams to Implement Feedback and Loop-carried Dependencies

As discussed above, hls::streams and hls::stream_of_blocks can be used to transfer data backwards (to processes that are lexically earlier) and implement loop-carried dependencies under user control.

Care must be taken to ensure that processes that read data from these feedback streams do not attempt to read from them until some data has been produced by later processes. The most common way to satisfy this requirement is to use a variable to skip the reading on the first execution. This can be:

  • Either a static variable counting in the process function (paying attention that the process is not duplicated, as previously explained);
  • Or the loop counter of a surrounding dataflow loop, for example, as in the following code, computing (in an odd way, for illustration) the partial sums of array elements:
void read_and_add(int a[N], hls::stream<int> &b, hls::stream<int> &f, int i) {
#pragma HLS pipeline II=1 style=flp // flushing pipeline is needed to avoid deadlocks
  int t=0;
  if (i>0) t = f.read(); // reading previous sum, except first time
  b.write(a[i]+t); // adding and passing
}

void write_and_feedback(hls::stream<int> &a, int b[N], hls::stream<int> &f, int i) {
#pragma HLS pipeline II=1 style=flp 
  int aa=a.read(); 
  if (i<N-1) f.write(aa); // passing back current sum
  b[i] = aa; // writing it out
}

void dut(int a[N], int b[N]) {
  for (int i=0; i<N; i++) {
#pragma HLS DATAFLOW  
    static hls::stream<int> f; // feedback stream, here could also be declared without static, above the loop
	hls::stream<int> c; // forward stream
      
	read_and_add(a, c, f, i);
	write_and_feedback(c, b, f, i);
  }
}
  • Or an hls_thread_local variable in the hls::task, with the same use (hls_thread_local is better in the hls::task context because it ensures no hidden communication between multiple instances of the hls::task).