MLIR-AIE 1.4.5.dev
xrt_test_wrapper.h
Go to the documentation of this file.
1/* Copyright (C) 2025-2026 Advanced Micro Devices, Inc.
2 * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */
3
4// When built for the HRX backend (NPU_RUNTIME=hrx / -DUSE_HRX=ON), this header
5// becomes a thin shim that pulls in the HRX drop-in instead of the XRT impl, so
6// example test.cpp files keep `#include "xrt_test_wrapper.h"` unchanged.
7#ifdef TEST_UTILS_USE_HRX
8#include "hrx_test_wrapper.h"
9#else
10
11#include "cxxopts.hpp"
12#include "test_utils.h"
13
14#include "xrt/xrt_bo.h"
15#include "xrt/xrt_device.h"
16#include "xrt/xrt_hw_context.h"
17#include "xrt/xrt_kernel.h"
18
19#include <cstring>
20#include <fstream>
21#include <iostream>
22#include <sstream>
23#include <tuple>
24#include <utility>
25#include <vector>
26
27struct args {
28 int verbosity;
29 int do_verify;
30 int n_iterations;
32 int trace_size;
33 std::string instr;
34 std::string xclbin;
35 std::string kernel;
36 std::string trace_file;
37};
38
39struct args parse_args(int argc, const char *argv[]) {
40 // ------------------------------------------------------
41 // Parse program arguments
42 // ------------------------------------------------------
43 cxxopts::Options options("XRT Test Wrapper");
44 cxxopts::ParseResult vm;
46
47 struct args myargs;
48
49 test_utils::parse_options(argc, argv, options, vm);
50 myargs.verbosity = vm["verbosity"].as<int>();
51 myargs.do_verify = vm["verify"].as<bool>();
52 myargs.n_iterations = vm["iters"].as<int>();
53 myargs.n_warmup_iterations = vm["warmup"].as<int>();
54 myargs.trace_size = vm["trace_sz"].as<int>();
55 myargs.instr = vm["instr"].as<std::string>();
56 myargs.xclbin = vm["xclbin"].as<std::string>();
57 myargs.kernel = vm["kernel"].as<std::string>();
58 myargs.trace_file = vm["trace_file"].as<std::string>();
59
60 return myargs;
61}
62
63uint32_t getParity(uint32_t n) {
64 int count = 0;
65 while (n > 0) {
66 if (n & 1) { // Check if the least significant bit is 1
67 count++;
68 }
69 n >>= 1; // Right shift to check the next bit
70 }
71 return (count % 2 == 0) ? 0 : 1; // 0 for even parity, 1 for odd parity
72}
73
74uint32_t create_ctrl_pkt(int operation, int beats, int addr,
75 int ctrl_pkt_read_id = 28) {
76 uint32_t ctrl_pkt = ((ctrl_pkt_read_id & 0xFF) << 24) |
77 ((operation & 0x3) << 22) | ((beats & 0x3) << 20) |
78 (addr & 0x7FFFF);
79 ctrl_pkt |= (0x1 ^ getParity(ctrl_pkt)) << 31;
80 return ctrl_pkt;
81}
82
83/*
84 ******************************************************************************
85 * Typed buffer descriptors for the variadic XRT test wrapper
86 ******************************************************************************
87 *
88 * The host buffer ABI is the runtime_sequence operand list: the kernel takes
89 * its data buffers in order, then -- only when enabled -- a control-packet
90 * buffer, then a trace buffer, each appended at the tail. The wrapper builds
91 * the kernel argument list to match exactly, so it works for any number of
92 * inputs and never passes a buffer the kernel did not declare.
93 */
94
95// A typed input buffer: element type, element count, and initializer.
96template <typename T>
97struct InBuf {
98 using elem_type = T;
99 int volume;
100 void (*init)(T *, int);
101};
102
103template <typename T>
104InBuf<T> make_in(int volume, void (*init)(T *, int)) {
105 return InBuf<T>{volume, init};
106}
107
108// A typed output buffer.
109template <typename T>
110struct OutBuf {
111 using elem_type = T;
112 int volume;
113 void (*init)(T *, int);
114};
115
116template <typename T>
117OutBuf<T> make_out(int volume, void (*init)(T *, int)) {
118 return OutBuf<T>{volume, init};
119}
120
121/*
122 ******************************************************************************
123 * XRT based test wrapper for N inputs and 1 output
124 ******************************************************************************
125 *
126 * Inputs are passed as InBuf<T> descriptors (any number, any types). The
127 * verifier is called as verify(in0*, in1*, ..., out*, out_volume, verbosity)
128 * and returns an error count.
129 *
130 * Host buffer indices follow the runtime_sequence operand list:
131 * group_id(3 + i) for data buffer i, then (if enabled) control packets, then
132 * (if trace_size > 0) the trace buffer -- each appended at the tail.
133 */
134template <typename Verify, typename TOut, typename... TIns>
135int setup_and_run_aie(Verify verify_results, std::tuple<InBuf<TIns>...> inputs,
136 OutBuf<TOut> output, struct args myargs,
137 bool enable_ctrl_pkts = false,
138 int num_trailing_params = 0) {
139
140 srand(time(NULL));
141
142 constexpr int num_inputs = sizeof...(TIns);
143 // Some designs place scalar/param buffers AFTER the output in the host ABI.
144 // The IRON `transform` runtime sequence, for example, is ordered
145 // [inputs, output, params]. `num_trailing_params` counts how many of the
146 // trailing entries in `inputs` are actually such params: they are bound to
147 // kernel arg slots *after* the output instead of before it. The verifier
148 // still receives all buffers in declared order (inputs..., params..., out).
149 const int num_real_inputs = num_inputs - num_trailing_params;
150
151 // Load instruction sequence
152 std::vector<uint32_t> instr_v = test_utils::load_instr_binary(myargs.instr);
153 if (myargs.verbosity >= 1)
154 std::cout << "Sequence instr count: " << instr_v.size() << "\n";
155
156 // Start the XRT context and load the kernel
157 xrt::device device;
158 xrt::kernel kernel;
159
160 test_utils::init_xrt_load_kernel(device, kernel, myargs.verbosity,
161 myargs.xclbin, myargs.kernel);
162
163 // The fixed scalar args occupy kernel arg slots 0..2 (opcode, instr, ninstr).
164 // Data buffers start at slot 3, i.e. group_id(3 + data_index).
165 constexpr int kFirstDataArg = 3;
166 // Output slot follows the *real* inputs; trailing params come after it.
167 const int out_arg = kFirstDataArg + num_real_inputs;
168 // Kernel arg slot for the idx-th declared `inputs` entry (real input or
169 // trailing param).
170 auto data_arg_slot = [&](int idx) {
171 return (idx < num_real_inputs) ? (kFirstDataArg + idx)
172 : (out_arg + 1 + (idx - num_real_inputs));
173 };
174
175 // Instruction buffer (kernel arg slot 1).
176 auto bo_instr = xrt::bo(device, instr_v.size() * sizeof(int),
177 XCL_BO_FLAGS_CACHEABLE, kernel.group_id(1));
178 void *bufInstr = bo_instr.map<void *>();
179 memcpy(bufInstr, instr_v.data(), instr_v.size() * sizeof(int));
180 bo_instr.sync(XCL_BO_SYNC_BO_TO_DEVICE);
181
182 if (myargs.verbosity >= 1)
183 std::cout << "Writing data into buffer objects.\n";
184
185 // Allocate + initialize the input buffer objects, in order. Each input lands
186 // at the next data arg slot.
187 std::vector<xrt::bo> in_bos;
188 in_bos.reserve(num_inputs);
189 std::tuple<TIns *...> in_ptrs;
190
191 // The verifier's SIZE argument is the first input's element count, matching
192 // the historical wrapper contract (verify iterates the primary input).
193 int first_in_volume = 0;
194
195 auto alloc_inputs = [&](auto &...inb) {
196 int idx = 0;
197 auto one = [&](auto &desc, auto &ptr_slot) {
198 using T = typename std::remove_reference_t<decltype(desc)>::elem_type;
199 auto bo = xrt::bo(device, desc.volume * sizeof(T), XRT_BO_FLAGS_HOST_ONLY,
200 kernel.group_id(data_arg_slot(idx)));
201 T *mapped = bo.template map<T *>();
202 desc.init(mapped, desc.volume);
203 bo.sync(XCL_BO_SYNC_BO_TO_DEVICE);
204 ptr_slot = mapped;
205 in_bos.push_back(std::move(bo));
206 if (idx == 0)
207 first_in_volume = desc.volume;
208 idx++;
209 };
210 std::apply([&](auto &...slots) { (one(inb, slots), ...); }, in_ptrs);
211 };
212 std::apply(alloc_inputs, inputs);
213
214 // Output buffer follows the real inputs (any params come after it).
215 auto bo_out = xrt::bo(device, output.volume * sizeof(TOut),
216 XRT_BO_FLAGS_HOST_ONLY, kernel.group_id(out_arg));
217 TOut *bufOut = bo_out.map<TOut *>();
218 output.init(bufOut, output.volume);
219 bo_out.sync(XCL_BO_SYNC_BO_TO_DEVICE);
220
221 // Control-packet and trace buffers are appended (in that order) only when
222 // enabled, matching what trace lowering appends to the runtime_sequence.
223 // Control packets are a trace-side feature, so they are only part of the ABI
224 // when tracing is also enabled.
225 const bool use_ctrl_pkts = enable_ctrl_pkts && myargs.trace_size > 0;
226 int next_arg = out_arg + 1 + num_trailing_params;
227
228 xrt::bo bo_ctrlpkts;
229 uint32_t *bufCtrlPkts = nullptr;
230 int ctrlpkt_arg = -1;
231 if (use_ctrl_pkts) {
232 ctrlpkt_arg = next_arg++;
233 bo_ctrlpkts = xrt::bo(device, 8, XRT_BO_FLAGS_HOST_ONLY,
234 kernel.group_id(ctrlpkt_arg));
235 bufCtrlPkts = bo_ctrlpkts.map<uint32_t *>();
236 }
237
238 xrt::bo bo_trace;
239 char *bufTrace = nullptr;
240 int trace_arg = -1;
241 if (myargs.trace_size > 0) {
242 trace_arg = next_arg++;
243 // Allocate a larger trace buffer (driver workaround) that also holds the
244 // 8-byte control packet response when control packets are enabled.
245 int alloc_trace_size = myargs.trace_size * 4;
246 bo_trace = xrt::bo(device, alloc_trace_size, XRT_BO_FLAGS_HOST_ONLY,
247 kernel.group_id(trace_arg));
248 bufTrace = bo_trace.map<char *>();
249 memset(bufTrace, 0, myargs.trace_size);
250 }
251
252 // Set control packet values.
253 if (use_ctrl_pkts) {
254 bufCtrlPkts[0] = create_ctrl_pkt(1, 0, 0x32004); // core status
255 bufCtrlPkts[1] = create_ctrl_pkt(1, 0, 0x320D8); // trace status
256 if (myargs.verbosity >= 1) {
257 std::cout << "bufCtrlPkts[0]:" << std::hex << bufCtrlPkts[0] << std::endl;
258 std::cout << "bufCtrlPkts[1]:" << std::hex << bufCtrlPkts[1] << std::endl;
259 }
260 }
261
262 // Sync trace/ctrlpkt buffers to device.
263 if (myargs.trace_size > 0) {
264 bo_trace.sync(XCL_BO_SYNC_BO_TO_DEVICE);
265 if (use_ctrl_pkts)
266 bo_ctrlpkts.sync(XCL_BO_SYNC_BO_TO_DEVICE);
267 }
268
269 // ------------------------------------------------------
270 // Initialize run configs
271 // ------------------------------------------------------
272 unsigned num_iter = myargs.n_iterations + myargs.n_warmup_iterations;
273 float npu_time_total = 0;
274 float npu_time_min = 9999999;
275 float npu_time_max = 0;
276
277 int errors = 0;
278
279 // ------------------------------------------------------
280 // Main run loop
281 // ------------------------------------------------------
282 for (unsigned iter = 0; iter < num_iter; iter++) {
283
284 if (myargs.verbosity >= 1)
285 std::cout << "Running Kernel.\n";
286
287 auto start = std::chrono::high_resolution_clock::now();
288 unsigned int opcode = 3;
289
290 // Build the kernel argument list to match the declared host ABI exactly:
291 // opcode, instr, ninstr, data buffers..., [ctrlpkt], [trace].
292 xrt::run run(kernel);
293 run.set_arg(0, opcode);
294 run.set_arg(1, bo_instr);
295 run.set_arg(2, instr_v.size());
296 for (int i = 0; i < num_inputs; i++)
297 run.set_arg(data_arg_slot(i), in_bos[i]);
298 run.set_arg(out_arg, bo_out);
299 if (ctrlpkt_arg >= 0)
300 run.set_arg(ctrlpkt_arg, bo_ctrlpkts);
301 if (trace_arg >= 0)
302 run.set_arg(trace_arg, bo_trace);
303 run.start();
304 run.wait();
305
306 auto stop = std::chrono::high_resolution_clock::now();
307 bo_out.sync(XCL_BO_SYNC_BO_FROM_DEVICE);
308 if (myargs.trace_size > 0)
309 bo_trace.sync(XCL_BO_SYNC_BO_FROM_DEVICE);
310
311 if (iter < myargs.n_warmup_iterations)
312 /* Warmup iterations do not count towards average runtime. */
313 continue;
314
315 // Copy output results and verify they are correct
316 if (myargs.do_verify) {
317 if (myargs.verbosity >= 1)
318 std::cout << "Verifying results ..." << std::endl;
319 auto vstart = std::chrono::system_clock::now();
320
321 // verify(in0*, in1*, ..., out*, first_input_volume, verbosity)
322 errors += std::apply(
323 [&](auto *...in_ptr) {
324 return verify_results(in_ptr..., bufOut, first_in_volume,
325 myargs.verbosity);
326 },
327 in_ptrs);
328
329 auto vstop = std::chrono::system_clock::now();
330 float vtime =
331 std::chrono::duration_cast<std::chrono::seconds>(vstop - vstart)
332 .count();
333 if (myargs.verbosity >= 1)
334 std::cout << "Verify time: " << vtime << "secs." << std::endl;
335 } else {
336 if (myargs.verbosity >= 1)
337 std::cout << "WARNING: results not verified." << std::endl;
338 }
339
340 // Write trace values if trace_size > 0 and first non-warmup iteration
341 if (myargs.trace_size > 0 && iter == myargs.n_warmup_iterations) {
342 test_utils::write_out_trace(((char *)bufTrace), myargs.trace_size,
343 myargs.trace_file);
344 }
345
346 // Write out control packet outputs
347 if (use_ctrl_pkts) {
348 uint32_t *ctrl_pkt_out =
349 (uint32_t *)(((char *)bufTrace) + myargs.trace_size);
350 if (myargs.verbosity >= 1) {
351 std::cout << "ctrl_pkt_out[0]:" << std::hex << ctrl_pkt_out[0]
352 << std::endl;
353 std::cout << "ctrl_pkt_out[1]:" << std::hex << ctrl_pkt_out[1]
354 << std::endl;
355 }
356 int col = (ctrl_pkt_out[0] >> 21) & 0x7F;
357 int row = (ctrl_pkt_out[0] >> 16) & 0x1F;
358 if ((ctrl_pkt_out[1] >> 8) == 3)
359 std::cout << "WARNING: Trace overflow detected in tile(" << row << ","
360 << col << ". Trace results may be invalid." << std::endl;
361 }
362
363 // Accumulate run times
364 float npu_time =
365 std::chrono::duration_cast<std::chrono::microseconds>(stop - start)
366 .count();
367
368 npu_time_total += npu_time;
369 npu_time_min = (npu_time < npu_time_min) ? npu_time : npu_time_min;
370 npu_time_max = (npu_time > npu_time_max) ? npu_time : npu_time_max;
371 }
372
373 // ------------------------------------------------------
374 // Print verification and timing results
375 // ------------------------------------------------------
376
377 // TODO - Mac count to guide gflops
378 float macs = 0;
379
380 std::cout << std::endl
381 << "Avg NPU time: " << npu_time_total / myargs.n_iterations << "us."
382 << std::endl;
383 if (macs > 0)
384 std::cout << "Avg NPU gflops: "
385 << macs / (1000 * npu_time_total / myargs.n_iterations)
386 << std::endl;
387
388 std::cout << std::endl
389 << "Min NPU time: " << npu_time_min << "us." << std::endl;
390 if (macs > 0)
391 std::cout << "Max NPU gflops: " << macs / (1000 * npu_time_min)
392 << std::endl;
393
394 std::cout << std::endl
395 << "Max NPU time: " << npu_time_max << "us." << std::endl;
396 if (macs > 0)
397 std::cout << "Min NPU gflops: " << macs / (1000 * npu_time_max)
398 << std::endl;
399
400 if (!errors) {
401 std::cout << "\nPASS!\n\n";
402 return 0;
403 } else {
404 std::cout << "\nError count: " << errors << "\n\n";
405 std::cout << "\nFailed.\n\n";
406 return 1;
407 }
408}
409
410#endif // TEST_UTILS_USE_HRX
struct args parse_args(int argc, const char *argv[])
std::vector< uint32_t > load_instr_binary(std::string instr_path)
void init_xrt_load_kernel(xrt::device &device, xrt::kernel &kernel, int verbosity, std::string xclbinFileName, std::string kernelNameInXclbin)
void parse_options(int argc, const char *argv[], cxxopts::Options &options, cxxopts::ParseResult &result)
void write_out_trace(char *traceOutPtr, size_t trace_size, std::string path)
void add_default_options(cxxopts::Options &options)
void(* init)(T *, int)
void(* init)(T *, int)
int n_warmup_iterations
std::string trace_file
std::string xclbin
std::string kernel
int n_iterations
std::string instr
uint32_t create_ctrl_pkt(int operation, int beats, int addr, int ctrl_pkt_read_id=28)
uint32_t getParity(uint32_t n)
InBuf< T > make_in(int volume, void(*init)(T *, int))
OutBuf< T > make_out(int volume, void(*init)(T *, int))
int setup_and_run_aie(Verify verify_results, std::tuple< InBuf< TIns >... > inputs, OutBuf< TOut > output, struct args myargs, bool enable_ctrl_pkts=false, int num_trailing_params=0)