Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lab 3: Migrating from Vitis HLS

Goal: Port an existing Vitis HLS kernel to TAPA by replacing HLS-specific constructs with their TAPA equivalents.

Prerequisites: Lab 1: Vector Addition and familiarity with the TAPA task model.

After this lab you will understand:

  • The mechanical substitutions that cover most Vitis HLS kernels
  • Why the dataflow-in-a-loop pattern must be restructured in TAPA
  • How tapa::hls::stream supports incremental migration of large codebases

Quick reference: Vitis HLS → TAPA

Vitis HLSTAPANotes
#include <hls_stream.h>#include <tapa.h>TAPA includes its own stream types
T* port + #pragma HLS INTERFACE m_axitapa::mmap<T> port (by value)Remove all m_axi pragmas
hls::stream<T>&tapa::istream<T>& or tapa::ostream<T>&Direction is explicit in TAPA
#pragma HLS dataflow + direct callstapa::task().invoke(...)Tasks run concurrently
Top function contains computationMove computation into child tasksTAPA upper-level tasks are orchestration-only
hls::stream<T> local variabletapa::stream<T> local variableSame syntax; depth is enforced during software simulation (default depth: 2)

Types, waits and registers

Vitis HLSTAPANotes
#include <ap_int.h>#include <tapa.h>Not needed at all in software simulation
ap_uint<W> / ap_int<W>tapa::u<W> / tapa::i<W>Self-implemented for the host; aliases the vendor type when synthesizing
ap_fixed<W, I, Q, O, N> / ap_ufixed<...>tapa::fixed<W, I, Q, O, N> / tapa::ufixed<...>Same for the modes: AP_RND is tapa::q_mode::rnd, AP_SAT is tapa::o_mode::sat
ap_axiu<W, U, I, D> / ap_axis<...>tapa::axis<tapa::u<W>, U, I, D> / tapa::axis<tapa::i<W>, ...>Parameterized by payload type, as the vendor's own hls::axis is
hls_vector.h, hls::vector<T, N>tapa::vec_t<T, N>
ap_wait()tapa::wait()
ap_wait_n(n)tapa::wait(n)
a hand-rolled HLS_REG helpertapa::reg(x), tapa::reg<T, Depth>(x)A real pipeline register — see the note in the API reference if yours registered only the return interface

Pragmas

Every directive below becomes a C++ attribute; the full grammar is in the API reference. tapa analyze warns on any vendor pragma it still sees and names the portable form.

Vitis HLSTAPA
#pragma HLS pipeline II = N[[tapa::pipeline(N)]] on the loop, or on the function when the pragma was at function scope
#pragma HLS pipeline off[[tapa::pipeline(false)]]
#pragma HLS pipeline II = N enable_flush[[tapa::pipeline(N, "flp")]] — Vitis deprecated enable_flush in favour of style = flp
#pragma HLS unroll [factor = N][[tapa::unroll]] / [[tapa::unroll(N)]]
#pragma HLS loop_tripcount min = A max = B[[tapa::tripcount(A, B)]]
#pragma HLS loop_flatten off[[tapa::flatten(false)]]
#pragma HLS latency min = A max = B[[tapa::latency(A, B)]]
#pragma HLS dependence variable = v ...[[tapa::dependence("v", ...)]]
#pragma HLS expression_balance[[tapa::balance]]
#pragma HLS array_partition variable = a complete dim = D[[tapa::partition("complete", -1, D)]] on the declaration
#pragma HLS bind_storage variable = a type = T impl = I[[tapa::storage("T", "I")]]
#pragma HLS resource variable = a core = C[[tapa::storage(...)]] (legacy spelling)
#pragma HLS aggregate variable = a[[tapa::aggregate]]
#pragma HLS array_map variable = a instance = i[[tapa::array_map("i", -1)]]
#pragma HLS bind_op variable = a op = o impl = i[[tapa::bind_op("o", "i")]]
#pragma HLS inline on a helperthe inline keyword on that helper
#pragma HLS inline off on a helperomit the inline keyword
#pragma HLS inline region at a call sitemark the callee inline
#pragma HLS interface ...nothing — TAPA synthesizes the top interface
#pragma HLS stream variable = q depth = Nthe depth template argument, tapa::stream<T, N>

Note

inline region names a region rather than a function, so there is no attribute for it. Where the region is exactly one call — the usual case — marking the callee inline is equivalent: the callee is inlined into that call site, which is what the region asked for. Where the region spans more than one call, split it or accept the wider scope.

Warning

Two vendor constructs have no portable form, and tapa analyze says so rather than suggesting one: #pragma HLS stream ... off (a stream that is not a FIFO) and #pragma HLS dataflow inside a leaf task. Keep them and expect the remark.

```admonish note title="An unqualified reg becomes ambiguous" Vitis declares reg at global scope in utils/x_hls_utils.h, and TAPA declares tapa::reg. While the argument is an ap_* type, an unqualified reg(x) finds only the vendor's. Once the argument moves into namespace tapa — the moment a value becomes a tapa::ufixed or a tapa::u<W> — argument-dependent lookup finds tapa::reg too and the call stops compiling.

They are not the same function. The vendor's is PIPELINE II=1 plus LATENCY min=1 max=1 and INLINE off; tapa::reg is a volatile protocol-region shim with a depth parameter. Qualify the call for the one you meant rather than letting the lookup decide, and if you switch, expect the hardware to change.


---

## Example 1: Basic VecAdd migration

The full before and after files are at [example_1_before.cpp](code/vitis-hls/example_1_before.cpp) and [example_1_after.cpp](code/vitis-hls/example_1_after.cpp).

### Step 1: Replace the include

```diff
-#include <hls_stream.h>
-#include <hls_vector.h>
+#include <hls_vector.h>
+#include <tapa.h>

TAPA provides its own stream types, so hls_stream.h is no longer needed. Other HLS headers such as ap_int.h and hls_vector.h are still supported and can be included as usual.

Step 2: Replace pointer arguments with tapa::mmap<T>

Vitis HLS uses raw pointers annotated with #pragma HLS INTERFACE m_axi to indicate off-chip memory. TAPA replaces this with tapa::mmap<T> passed by value, and no pragma is needed:

-void load_input(hls::vector<uint32_t, NUM_WORDS>* in,
+void load_input(tapa::mmap<hls::vector<uint32_t, NUM_WORDS>> in,
-  hls::vector<uint32_t, NUM_WORDS>* in1,
-  hls::vector<uint32_t, NUM_WORDS>* in2,
-  hls::vector<uint32_t, NUM_WORDS>* out, int size) {
-#pragma HLS INTERFACE m_axi port = in1 bundle = gmem0
-#pragma HLS INTERFACE m_axi port = in2 bundle = gmem1
-#pragma HLS INTERFACE m_axi port = out bundle = gmem0
+  tapa::mmap<hls::vector<uint32_t, NUM_WORDS>> in1,
+  tapa::mmap<hls::vector<uint32_t, NUM_WORDS>> in2,
+  tapa::mmap<hls::vector<uint32_t, NUM_WORDS>> out, int size) {

tapa::mmap<T> supports element-indexed reads and writes (mem[i]) just like a pointer, so the body of each task usually does not need to change.

Step 3: Replace hls::stream<T>& with directional TAPA streams

Vitis HLS hls::stream<T>& is bidirectional — the same type is used whether the stream is read or written. TAPA makes direction explicit:

-void compute_add(hls::stream<hls::vector<uint32_t, NUM_WORDS>>& in1_stream,
-                 hls::stream<hls::vector<uint32_t, NUM_WORDS>>& in2_stream,
-                 hls::stream<hls::vector<uint32_t, NUM_WORDS>>& out_stream,
+void compute_add(tapa::istream<hls::vector<uint32_t, NUM_WORDS>>& in1_stream,
+                 tapa::istream<hls::vector<uint32_t, NUM_WORDS>>& in2_stream,
+                 tapa::ostream<hls::vector<uint32_t, NUM_WORDS>>& out_stream,

Use tapa::istream<T>& for streams the task reads from, and tapa::ostream<T>& for streams the task writes to. The read() and << operators work the same as in Vitis HLS.

Step 4: Replace local hls::stream<T> declarations

Local streams declared inside the top-level function become tapa::stream<T>:

-  hls::stream<hls::vector<uint32_t, NUM_WORDS>> in1_stream("input_stream_1");
-  hls::stream<hls::vector<uint32_t, NUM_WORDS>> in2_stream("input_stream_2");
-  hls::stream<hls::vector<uint32_t, NUM_WORDS>> out_stream("output_stream");
+  tapa::stream<hls::vector<uint32_t, NUM_WORDS>> in1_stream("input_stream_1");
+  tapa::stream<hls::vector<uint32_t, NUM_WORDS>> in2_stream("input_stream_2");
+  tapa::stream<hls::vector<uint32_t, NUM_WORDS>> out_stream("output_stream");

tapa::stream<T> accepts a name string for the same debugging purpose as hls::stream<T>. To set a custom depth, use tapa::stream<T, DEPTH>. For stream arrays, use tapa::streams<T, ARRAY_SIZE, DEPTH>.

Note

The default stream depth in TAPA is 2, matching the Vitis HLS default. Unlike Vitis HLS, TAPA enforces the depth during software simulation, which helps catch backpressure bugs before synthesis.

Step 5: Replace #pragma HLS dataflow with tapa::task().invoke(...)

Vitis HLS uses #pragma HLS dataflow to signal that a sequence of direct function calls should run as concurrent processes. TAPA replaces this with an explicit task graph:

-#pragma HLS dataflow
-  load_input(in1, in1_stream, size);
-  load_input(in2, in2_stream, size);
-  compute_add(in1_stream, in2_stream, out_stream, size);
-  store_result(out, out_stream, size);
+  tapa::task()
+      .invoke(load_input, in1, in1_stream, size)
+      .invoke(load_input, in2, in2_stream, size)
+      .invoke(compute_add, in1_stream, in2_stream, out_stream, size)
+      .invoke(store_result, out, out_stream, size);

All tasks in a tapa::task().invoke(...) chain run concurrently. The top-level function becomes pure orchestration — it declares streams, then hands everything off to child tasks.


Example 2: Dataflow-in-a-loop

The full before and after files are at example_2_before.cpp and example_2_after.cpp.

Vitis HLS permits #pragma HLS dataflow inside a for loop. Each iteration starts a new concurrent dataflow region:

// Vitis HLS: dataflow region restarts each iteration
size /= NUM_WORDS;
for (int i = 0; i < size; i++) {
#pragma HLS dataflow
  load_input(in1, in1_stream, i);
  load_input(in2, in2_stream, i);
  compute_add(in1_stream, in2_stream, out_stream);
  store_result(out, out_stream, i);
}

TAPA does not allow computation in upper-level tasks. A top-level TAPA task may only declare streams and invoke child tasks — it cannot contain loops or arithmetic. The solution is to move the loop into each child task:

// TAPA: loop lives in the child tasks
void load_input(tapa::mmap<hls::vector<uint32_t, NUM_WORDS>> in,
                tapa::ostream<hls::vector<uint32_t, NUM_WORDS>>& inStream,
                int size) {
  size /= NUM_WORDS;
  for (int i = 0; i < size; i++) {
#pragma HLS pipeline II = 1
    inStream << in[i];
  }
}

The top-level task then becomes:

void vadd(...) {
  tapa::stream<...> in1_stream(...);
  tapa::stream<...> in2_stream(...);
  tapa::stream<...> out_stream(...);

  tapa::task()
      .invoke(load_input, in1, in1_stream, size)
      .invoke(load_input, in2, in2_stream, size)
      .invoke(compute_add, in1_stream, in2_stream, out_stream, size)
      .invoke(store_result, out, out_stream, size);
}

The child tasks stream data to each other for the full duration; no synchronization is needed between iterations because each task has its own loop that runs from start to finish.


HLS-compat helpers for incremental migration

If you have a large existing codebase, TAPA provides tapa::hls::stream<T> as a drop-in replacement for hls::stream<T>. Unlike tapa::stream<T>, it uses effectively infinite depth in software simulation, so producers never block. This lets you keep direction-agnostic stream passing patterns while still running software simulation.

tapa::hls::stream<T> is available via #include <tapa.h> — no additional include is needed.

// Before (Vitis HLS):
hls::stream<float>& s

// After (TAPA compat, passes software simulation without depth tuning):
tapa::hls::stream<float>& s

Use this as a stepping stone: get software simulation passing with tapa::hls::stream, then replace with directional tapa::istream<T>& / tapa::ostream<T>& before shipping.

Note

tapa::hls::stream synthesizes correctly — the generated RTL FIFO is identical to tapa::stream<T, N>. The reason to replace it before hardware build is that the infinite simulation depth hides backpressure bugs. Switching to directional streams with a tuned depth catches those bugs during software simulation, before they appear on hardware.


Next step: Lab 4: Custom RTL Modules