目录
1.简介
2. XRT
2.1 XRT vs OpenCL
2.2 Takeways
2.3 XRT C++ APIs
2.4 Device and XCLBIN
2.5 Buffers
2.5.1 Buffer 创建
2.5.1.1 普通 Buffer
2.5.1.2 特殊 Buffer
2.5.1.3 用户指针 Buffer
2.5.2 Data Transfer
2.5.2.1 read/write API
2.5.2.2 map API
2.5.2.3 copy API
2.5.2.4 sync 函数
2.5.3 其他 Buffer API
2.5.3.1 DMA-BUF API
2.5.3.2 Sub-buffer
2.5.3.3 Buffer Info
2.6 Kernel and Run
2.6.1 获取 Kernel Object
2.6.2 Executing the kernel
2.6.3 Other kernel APIs
2.7 User Managed Kernel
2.7.1 Creating xrt::ip
2.7.2 Allocating buffers
2.7.3 Read / Write
2.8 Graph
2.8.1 Graph 简介
2.8.2 Graph Object
2.8.3 Reset Graph
2.8.4 Graph 执行
2.8.4.1 固定次数执行
2.8.4.2 无限执行 Graph
2.8.5 Graph 周期
2.8.6 运行时参数
2.8.7 DMA sync
2.9 Error API
2.10 Profiling
2.10.1 Create Event
2.10.2 Start Profiling
2.10.3 Read Profiling
2.10.4 Stop Profiling
2.11 XRT Async
2.11.1 基本概念
2.11.2 异步执行同步任务
2.11.3 通过队列执行多任务
2.11.4 队列事件同步
1.简介
1)XRT 主要功能:
- FPGA Image 下载:Download Accelerator Binaries onto Platform
- 数据搬运:Data movement between Host and Accelerators
- 执行管理:Trigger, Sequence and Synchronize Computations
- 板卡管理:Board Recovery, Debug, Power Management
2)优势:
- 面向开发者的简易开发体验:XRT 简化了硬件通信层实现,将其与标准软件集成为一体,使开发者无需硬件专业知识即可使用 FPGA。
- 开源与灵活:XRT 开源,开发者可直接使用或根据需求修改,支持高度自定义。
- 通用跨平台 API:提供统一的 API,支持边缘、本地和云端加速应用开发,应用在不同硬件平台间无缝移植。
- 多样化的抽象层级:开发者可根据需求选择适合的抽象级别,从高级 Python 绑定到精细控制的 C++ API。
- 动态功能交换支持:支持动态切换加速器二进制文件(DFX)。
3)XRT 说明文档
XRT Native APIs — XRT Master documentationhttps://xilinx.github.io/XRT/2024.1/html/xrt_native_apis.html?highlight=device
2. XRT
2.1 XRT vs OpenCL
1)Xilinx OpenCL extension
- 基于 OpenCL 标准的 API,被扩展以支持 Xilinx FPGA 的特定功能。
- OpenCL 是一个开放标准的跨平台编程框架,允许编写代码来在多种类型的处理器上执行,包括 CPU、GPU 和 FPGA。
- 使用 Xilinx OpenCL 扩展,开发者可以利用 OpenCL 的便利性,同时访问 FPGA 的高级功能。
2)XRT Native APIs
这是 XRT 提供的一组原生 API,允许直接与 XRT 交互。
2.2 Takeways
Slide 1
Slide 2
2.3 XRT C++ APIs
| Component | C++ Class | Header Files |
|----------------------|-------------|-------------------------------------------------|
| Device | xrt::device | #include <xrt/xrt_device.h> |
| XCLBIN | xrt::xclbin | #include <experimental/xrt_xclbin.h> |
| Buffer | xrt::bo | #include <xrt/xrt_bo.h> |
| Kernel | xrt::kernel | #include <xrt/xrt_kernel.h> |
| Run | xrt::run | #include <xrt/xrt_kernel.h> |
| User-managed Kernel | xrt::ip | #include <experimental/xrt_ip.h> |
---------------------------------------------------------------------------------------|
| Graph | xrt::graph | #include <experimental/aie.h> |
| | | #include <experimental/graph.h> |
|--------------------------------------------------------------------------------------|
使用 APIs 的大致逻辑如下:
- 打开 Xilinx 设备并加载 XCLBIN;
- 创建 bo (Buffer objects) 以将数据传输到内核的输入和输出;
- 使用 Buffer 类成员函数在主机和设备之间进行数据传输(在内核执行之前和之后);
- 使用 Kernel 和 Run 对象来下载和管理在 FPGA 上运行的计算密集型任务。
2.4 Device and XCLBIN
Device 和 XCLBIN 类提供基本的基础设施相关接口,主要用途是:
- 打开一个设备
- 将编译后的内核二进制文件(或 XCLBIN)加载到设备上
最简单的加载 XCLBIN 的代码如下:
auto device = xrt::device(0);
auto xclbin_uuid = device.load_xclbin("a.xclbin");
- 使用 xrt::device 类的构造函数来打开设备(编号为0)
- 成员函数 xrt::device::load_xclbin 用于从文件名加载 XCLBIN。
- 成员函数 xrt::device::load_xclbin 返回 XCLBIN 的 UUID,这是打开内核所必需的。
2.5 Buffers
Buffer 用于在 Host 和 Device 之间传输数据。
2.5.1 Buffer 创建
2.5.1.1 普通 Buffer
类构造函数 xrt::bo 用于分配一个 4K 对齐的 Buffer Object。
默认创建 Normal Buffer,可以通过 Flag 指定其他类型。
例如,创建一个常规 Buffer Object:
auto bank_grp_arg0 = kernel.group_id(0); // Memory bank index for kernel argument 0
auto bank_grp_arg1 = kernel.group_id(1); // Memory bank index for kernel argument 1
auto input_buffer = xrt::bo(device, buffer_size_in_bytes, bank_grp_arg0);
auto output_buffer = xrt::bo(device, buffer_size_in_bytes, bank_grp_arg1);
Buffer Object 在用户 Heap 内存空间中分配一个指针,并在指定的内存 Bank 中分配一个设备缓冲区。
示例中,第二个参数表示缓冲区大小,以字节为单位。
第三参数用于指定 Bank 索引(以指定缓冲区位置),有两种方法:
- 通过 Kernel 参数:示例中,通过 xrt::kernel::group_id() 成员函数来传递 Bank 索引。此成员函数接受参数索引(位置),XCLBIN 自动检测相应的 Bank 位置。
- 直接传递 Bank 索引: 可以从 xbutil examine --report memory 输出所观察到:
>> xbutil examine --report memory
----------------------------------------------------------
----------------------
[0000:00:00.0] : edge
----------------------
Memory Topology
HW Context Slot: 0
Xclbin UUID: 3d2d55xx-xxxx-xxxx-xxxx-xxx50ebdcxxx
Index Tag Type Temp(C) Size Base Address
---------------------------------------------------
0 DDR MEM_DDR4 N/A 2 GB 0x0
2.5.1.2 特殊 Buffer
1)xrt::bo::flags::normal
- 常规缓冲区(默认)。
2)xrt::bo::flags::device_only
- 仅设备缓冲区(仅由 kernel 使用,没有主机回指指针)。
3)xrt::bo::flags::host_only
- 仅主机缓冲区(缓冲区直接驻留在主机内存中,直接与 kernel 传输)。
4)xrt::bo::flags::p2p
- P2P 缓冲区,一种仅适用于点对点传输的特殊设备缓冲区。
5)xrt::bo::flags::cacheable
- 缓存可用的缓冲区可用于主机 CPU 频繁访问缓冲区时(适用于边缘平台)。
2.5.1.3 用户指针 Buffer
xrt::bo() 构造函数也可以使用用户提供的指针。用户指针必须对齐到 4K 边界。
// Host Memory pointer aligned to 4K boundary
int *host_ptr;
posix_memalign(&host_ptr, 4096, MAX_LENGTH*sizeof(int));
// Sample example filling the allocated host memory
for(int i=0; i<MAX_LENGTH; i++) {
host_ptr[i] = i;
}
auto mybuf = xrt::bo(device, host_ptr, MAX_LENGTH*sizeof(int), kernel.group_id(3));
2.5.2 Data Transfer
- Host 与 Device 之间通过 Buffer read/write API 进行数据传输
- Host 与 Device 之间通过 Buffer map API 进行数据传输
- Buffer 与 Buffer 之间通过 Buffer copy API 进行数据传输
2.5.2.1 read/write API
1)Host >> Device
xrt::bo::write()
xrt::bo::sync(XCL_BO_SYNC_BO_TO_DEVICE)
示例:
auto input_buffer = xrt::bo(device, buffer_size_in_bytes, bank_grp_idx_0);
// Prepare the input data
int buff_data[data_size];
for (auto i=0; i<data_size; ++i) {
buff_data[i] = i;
}
input_buffer.write(buff_data);
input_buffer.sync(XCL_BO_SYNC_BO_TO_DEVICE);
2)Device >> Host
xrt::bo::sync(XCL_BO_SYNC_BO_FROM_DEVICE)
xrt::bo::read()
注意:
- xrt::bo::sync、xrt::bo::write、xrt::bo::read 等有重载版本,可以通过指定大小和偏移量用于部分缓冲区同步/读取/写入。对于上述代码示例,假设缓冲区大小和偏移量=0 为默认参数。
- 如果缓冲区是通过用户指针创建的,则在 xrt::bo::sync 调用前后不需要 xrt::bo::write 或 xrt::bo::read。
- 对于仅缓冲区(使用 xrt::bo::flags::device_only 标志创建)的设备, xrt::bo::sync() 操作不是必需的,只有 xrt::bo::write() (或 xrt::bo::read() )对于 DMA 操作就足够了。至于仅缓冲区的设备,没有主机后端存储, xrt::bo::write() (或 xrt::bo::read() )直接对(或从)设备内存执行 DMA 操作。
2.5.2.2 map API
The API xrt::bo::map() allows mapping the host-side buffer backing pointer to a user pointer. The host code can subsequently exercise the user pointer for the data reads and writes. However, after writing to the mapped pointer (or before reading from the mapped pointer) the API xrt::bo::sync() should be used with direction flag for the DMA operation.
API xrt::bo::map() 函数将在设备上分配的内存缓冲区映射到主机代码中可以使用的指针。
通过这种映射,主机应用程序可以直接访问和操作内存,就像它是主机内存空间中的常规指针一样。
auto input_buffer = xrt::bo(device, buffer_size_in_bytes, bank_grp_idx_0);
auto input_buffer_mapped = input_buffer.map<int*>();
for (auto i=0;i<data_size;++i) {
input_buffer_mapped[i] = i;
}
input_buffer.sync(XCL_BO_SYNC_BO_TO_DEVICE);
2.5.2.3 copy API
xrt::bo::copy() API 在两个缓冲区对象之间进行深度复制(如果平台支持深度复制,请参阅内存到内存(M2M)中描述的 M2M 功能详细信息)。如果平台不支持深度复制,数据传输将通过浅复制进行(数据传输通过主机进行)。
dst_buffer.copy(src_buffer, copy_size_in_bytes);
2.5.2.4 sync 函数
这个 sync 函数主要用于确保 Host 和 Device 之间的数据一致性。它通过刷新或使 CPU 缓存失效来保证数据的最新状态,并且会记录同步操作的日志。
并且需要注意,这里的同步操作并不直接执行 DMA(Direct Memory Access),而是通过刷新 CPU 缓存来确保数据的一致性。
sync 函数的源码在如下目录:
<XRT-2024.1>/src/runtime_src/core/common/api/xrt_bo.cpp:448
---
virtual void
sync(xclBOSyncDirection dir, size_t sz, size_t offset)
{
// One may think that host_only BOs should not be synced, but here
// is the deal: The sync does not really do DMA, but just flush
// the CPU cache (to_device) so that device will get the most
// up-to-date data from physical memory or invalid CPU cache
// (from_device) so that host CPU can read the most up-to-date
// data device has put into the physical memory. As of today, all
// Xilinx's Alveo devices will automatically trigger cache
// coherence actions when it reads from or write to physical
// memory, but we still recommend user to perform explicit BO sync
// operation just in case the HW changes in the future.
// if (get_flags() != bo::flags::host_only)
handle->sync(static_cast<xrt_core::buffer_handle::direction>(dir), sz, offset);
m_usage_logger->log_buffer_sync(device->get_device_id(), device.get_hwctx_handle(), sz, dir);
}
2.5.3 其他 Buffer API
2.5.3.1 DMA-BUF API
XRT 提供缓冲区导出和导入 API,主要用于在设备(P2P 应用)和进程之间共享缓冲区。从 xrt::bo::export_buffer() 获取的缓冲区句柄本质上是一个文件描述符,因此在不同进程间发送需要合适的 IPC 机制(例如,UDS 或 Unix 域套接字)来将一个进程的文件描述符转换为另一个进程的文件描述符。
示例:从 Device_1 向 Device_2(在同一主机进程中)导出缓冲区的情况。
auto buffer_exported = buffer_device_1.export_buffer();
auto buffer_device_2 = xrt::bo(device_2, buffer_exported);
- 缓冲区 buffer_device_1 是在设备 1 上分配的缓冲区
- buffer_device_1 由成员函数 xrt::bo::export_buffer 导出
-
新缓冲区 buffer_device_2 由构造函数 xrt::bo 导入用于 device_2
2.5.3.2 Sub-buffer
xrt::bo 类的构造函数也可以用于从父缓冲区中分配一个子缓冲区,具体方法是指定起始偏移量和大小。
在以下示例中,从父缓冲区创建了一个大小为 4 bytes 的子缓冲区,offset=0:
size_t sub_buffer_size = 4;
size_t sub_buffer_offset = 0;
auto sub_buffer = xrt::bo(parent_buffer, sub_buffer_size, sub_buffer_offset);
2.5.3.3 Buffer Info
- xrt::bo::size()
- xrt::bo::address()
// 申请 10 bytes 数据
boHandle1 = pyxrt.bo(dev, 10, pyxrt.bo.normal, memlist[0].get_index())
print(boHandle1.size())
---
10
print(boHandle1.size())
---
0x152bb000
2.6 Kernel and Run
要在设备上执行内核,必须从当前加载的xclbin创建一个内核类(xrt::kernel)对象。内核对象可用于在硬件实例(计算单元或CU)上执行内核函数。
运行对象(xrt::run)代表内核的一次执行。内核执行完成后,如果需要,可以重用运行对象来调用相同的内核函数。
2.6.1 获取 Kernel Object
内核对象(Kernel Object)由设备、XCLBIN UUID 和 kernel name 使用 xrt::kernel() 构造函数创建,如下所示:
auto xclbin_uuid = device.load_xclbin("a.xclbin");
auto mm2s_khdl = xrt::kernel(device, xclbin_uuid, "mm2s");
2.6.2 Executing the kernel
内核执行与 Run Object 相关联。内核可以通过 xrt::kernel::operator() 执行,该命令按顺序接受所有内核参数。内核执行 API 返回一个与执行对应的运行对象。
// 1st kernel execution
auto rhdl = khdl(buf_a, buf_b, scalar_1);
rhdl.wait();
// 2nd kernel execution with just changing 3rd argument
rhdl.set_arg(2, scalar_2); // Arguments are specified starting from 0
rhdl.start();
rhdl.wait();
xrt::kernel 类提供了重载的 operator() 运算符,用于使用逗号分隔的参数列表执行内核。
- 内核执行使用 xrt::kernel() 运算符和返回 xrt::run 对象的参数列表。这是一个异步 API,在提交任务后返回。
- 成员函数 xrt::run::wait() 用于阻塞当前线程,直到当前执行完成。
-
成员函数 xrt::run::set_arg() 用于在下次执行前设置一个或多个内核参数。在上面的示例中,仅更改了最后一个(第 3 个)参数。
- 成员函数 xrt::run::start() 用于使用新参数启动下一个内核执行。
2.6.3 Other kernel APIs
前一节的示例展示了在内核执行时如何获得一个 xrt::run 对象(内核执行返回一个运行对象)。
auto rhdl = khdl(buf_a, buf_b, scalar_1);
然而,在内核执行之前也可以获得一个 xrt::run 对象。流程如下:
- 使用带有内核参数的 xrt::run 构造函数打开一个运行对象。
- 通过成员函数 xrt::run::set_arg() 设置下一次执行的内核参数。
- 通过成员函数 xrt::run::start() 执行内核。
- 通过成员函数 xrt::run::wait() 等待执行结束。
在等待内核执行结束时超时:成员函数 xrt::run::wait() 阻塞当前线程,直到内核执行完成。为了指定超时,支持的 API xrt::run::wait() 也接受以毫秒为单位的超时时间。
2.7 User Managed Kernel
1)XRT Managed Kernel (xrt::kernel)
- 通过标准化的 AXI-Lite 控制接口执行内核。
- 由 XRT 自动管理,用户无需直接处理控制寄存器。
- 在主机代码中使用 xrt::kernel 对象表示。
2)User Managed Kernel (xrt::ip)
- 使用自定义的 AXI-Lite 控制接口。
- 需要用户手动管理控制寄存器(读/写操作)。
- 在主机代码中使用 xrt::ip 对象表示,以区别于 XRT 管理的核。
2.7.1 Creating xrt::ip
auto xclbin_uuid = device.load_xclbin("kernel.xclbin");
auto ip = xrt::ip(device, xclbin_uuid, "ip_name");
一个 IP 对象只能以独占模式打开。这意味着在同一时间,只有一个线程/进程可以同时访问 IP。这是出于安全原因,因为多个线程/进程同时读写 AXI-Lite 寄存器可能会导致竞态条件。
2.7.2 Allocating buffers
与 XRT 管理的内核 xrt::bo 对象用于创建 IP 端口的缓冲区类似。然而,必须通过提供内存 Bank 的枚举索引来显式指定内存银行的位置。
auto buf_in_a = xrt::bo(device, DATA_SIZE, xrt::bo::flags::host_only, 8);
auto buf_in_b = xrt::bo(device, DATA_SIZE, xrt::bo::flags::host_only, 8);
2.7.3 Read / Write
从 AXI-Lite 寄存器空间读取和写入到 CU(由主机代码中的 xrt::ip 对象指定),需要从 xrt::ip 类中调用所需的成员函数:
- xrt::ip::read_register
- xrt::ip::write_register
int read_data;
int write_data = 7;
auto ip = xrt::ip(device, xclbin_uuid, "foo:{foo_1}");
read_data = ip.read_register(READ_OFFSET);
ip.write_register(WRITE_OFFSET,write_data);
2.8 Graph
2.8.1 Graph 简介
在 Versal ACAPs 中,带有 AI 引擎,可以使用 XRT Graph 类( xrt::graph )及其成员函数动态加载、监控和控制 AI 引擎数组上执行的图。
关于设备和缓冲区的说明:在基于 AIE 的应用中,设备和缓冲区有一些额外的功能。因此,建议使用类 xrt::aie::device 和 xrt::aie::buffer 来指定设备和缓冲区对象。
2.8.2 Graph Object
该 xrt::graph 对象可以使用当前加载的 XCLBIN 文件的 uuid 打开,如下:
auto xclbin_uuid = device.load_xclbin("kernel.xclbin");
auto graph = xrt::graph(device, xclbin_uuid, "graph_name");
Graph Object 可用于在 AIE Tile 上执行 Graph Function。
2.8.3 Reset Graph
成员函数 xrt::graph::reset() 用于通过禁用/启用 Tile 来实现重置指定的 Graph。
auto device = xrt::aie::device(0);
// load XCLBIN
...
auto graph = xrt::graph(device, xclbin_uuid, "graph_name");
// Graph Reset
graph.reset();
成员函数 xrt::aie::device::reset_array() 用于重置整个 AIE 数组。但在此 AIE 重置功能调用后,PDI 丢失,因此必须加载一个特殊的仅 XCLBIN 的 AIE(此流程仅适用于高级用户)。
2.8.4 Graph 执行
XRT 提供了基本的图形执行控制接口,用于初始化、运行、等待和终止图形的特定次数迭代。
2.8.4.1 固定次数执行
一个图可以执行固定次数的迭代,然后是 busy-wait 或 time-out wait。
1)Busy Wait 方案
Graph 可以通过使用迭代参数通过 xrt::graph::run() API 执行固定次数的迭代。随后,应使用 xrt::graph::wait() 或 xrt::graph::end() API(带参数 0)等待 Graph 执行完成。
// 从复位状态开始
graph.reset(); // 将图重置到初始状态
// 运行图 3 次迭代
graph.run(3); // 启动图并让其运行 3 次迭代
// 等待直到图运行完成
graph.wait(0); // 使用 graph.wait(0) 等待图的运行完成。如果希望再次执行图,可以继续后续操作
// 再次运行图 5 次迭代
graph.run(5); // 启动图并让其运行 5 次迭代
// 结束图的操作
graph.end(0); // 使用 graph.end(0) 结束图的操作,0 表示立即结束。如果你不再需要执行图,可以使用此方法
2)Timeout wait 方案
如上例所示,xrt::graph::wait(0) 执行忙等待,并暂停执行直到图形完成。如果需要,可以通过 xrt::graph::wait(std::chrono::milliseconds) 实现等待的超时版本,该版本可以用来等待指定的毫秒数,如果图形未完成,则可以在此期间做其他事情。下面展示了一个示例:
// start from reset state
graph.reset();
// run the graph for 100 iteration
graph.run(100);
while (1) {
try {
graph.wait(5);
}
catch (const std::system_error& ex) {
if (ex.code().value() == ETIME) {
std::cout << "Timeout, reenter......" << std::endl;
// Do something
}
}
// Do something
}
2.8.4.2 无限执行 Graph
Graph 在以迭代参数 0 调用 xrt::graph::run() 时无限运行。
当图无限运行时,可以使用 API xrt::graph::wait() 、 xrt::graph::suspend() 和 xrt::graph::end() 在经过一定数量的 AIE 周期后挂起/结束图操作。
API xrt::graph::resume() 用于再次执行无限运行的图。
// 从复位状态开始
graph.reset(); // 将图重置到初始状态
// 无限运行图
graph.run(0); // 启动图并让其无限运行,0 表示不限制运行周期
graph.wait(3000); // 在之前的启动时间点后,暂停图的运行,等待 3000 个 AIE 周期
graph.resume(); // 重新启动之前暂停的图,并让其无限运行
graph.suspend(); // 立即暂停图的运行
graph.resume(); // 再次重新启动之前暂停的图,并让其无限运行
graph.end(5000); // 在之前的启动时间点后,结束图的操作,等待 5000 个 AIE 周期
上述示例中:
- 成员函数 xrt::graph::run(0) 用于无限执行 Graph
-
成员函数 xrt::graph::wait(3000) 在图开始后 3000 个 AIE 周期暂停 Graph。
-
如果 Graph 已经运行了超过 3000 次 AIE 循环,则 Graph 立即暂停。
-
- 成员函数 xrt::graph::resume() 用于重启挂起的 Graph。
-
成员函数 xrt::graph::suspend() 用于立即挂起 Graph。
-
成员函数 xrt::graph::end(5000) 在从上一个 Graph 开始后的 5000 个 AIE 周期后结束 Graph。
-
如果图已经运行了超过 5000 次 AIE 循环,则 Graph 立即结束。
-
使用 xrt::graph::end() 消除了重新运行 Graph(不加载 PDI 和再次重置 Graph)的能力。
-
2.8.5 Graph 周期
成员函数 xrt::graph::get_timestamp() 可用于确定在图形启动和停止之间消耗的 AIE 周期。
在这个示例中,计算了 3 次迭代消耗的 AIE 周期。
// start from reset state
graph.reset();
uint64_t begin_t = graph.get_timestamp();
// run the graph for 3 iteration
graph.run(3);
graph.wait(0);
uint64_t end_t = graph.get_timestamp();
std::cout<<"Number of AIE cycles consumed in the 3 iteration is: "<< end_t-begin_t;
2.8.6 运行时参数
- 成员函数 xrt::graph::update() 用于更新 RTP(Runtime Parameter)。
- 成员函数 xrt::graph::read() 用于读取 RTP(Runtime Parameter)。
graph.reset();
graph.run(2);
float increment = 1.0;
graph.update("mm.mm0.in[2]", increment);
// Do more things
graph.run(16);
graph.wait(0);
// Read RTP
float increment_out;
graph.read("mm.mm0.inout[0]", &increment_out);
std::cout<<"\n RTP value read<<increment_out;
2.8.7 DMA sync
AIE 缓冲区类 xrt::aie::bo 支持成员函数 xrt::aie::bo::sync() ,可用于在全局内存和 AIE 之间同步缓冲区内容。以下代码展示了示例:
auto device = xrt::aie::device(0);
// Buffer from global memory (GM) to AIE
auto in_bo = xrt::aie::bo (device, SIZE * sizeof (float), 0, 0);
// Buffer from AIE to global memory (GM)
auto out_bo = xrt::aie::bo (device, SIZE * sizeof (float), 0, 0);
auto inp_bo_map = in_bo.map<float *>();
auto out_bo_map = out_bo.map<float *>();
// Prepare input data
std::copy(my_float_array,my_float_array+SIZE,inp_bo_map);
in_bo.sync("in_sink", XCL_BO_SYNC_BO_GMIO_TO_AIE, SIZE * sizeof(float), 0);
out_bo.sync("out_sink", XCL_BO_SYNC_BO_AIE_TO_GMIO, SIZE * sizeof(float), 0);
-
创建输入和输出缓冲区( in_bo 和 out_bo ),并映射到用户空间
-
成员函数 xrt::aie::bo::sync 用于数据一致性同步,它使用了以下参数:
- 关联的 GMIO 端口的名称。
- 缓冲区传输方向:
- GMIO > Graph:XCL_BO_SYNC_BO_GMIO_TO_AIE
- Graph > GMIO:XCL_BO_SYNC_BO_AIE_TO_GMIO
- 缓冲区的大小和偏移量。
2.9 Error API
通常情况下,XRT API 可能会遇到两种类型的错误:
- 同步错误(Synchronous error) :错误可能由 API 本身抛出。主机代码可以捕获这些异常并采取必要的措施。
- 异步错误(Asynchronous error) :来自底层驱动、系统、硬件等的错误。这些错误通常不会直接由 API 抛出,而是需要通过其他机制(如回调函数或事件监听)来检测和处理。
XRT 提供了 xrt::error 类及其成员函数,用于将异步错误传递到用户空间的主机代码中。这有助于在出现问题时进行调试。
- 成员函数 xrt::error::get_error_code() - 获取给定错误类的最后一个错误代码及其时间戳。
- 成员函数 xrt::error::get_timestamp() - 获取最后一个错误的时间戳。
- 成员函数 xrt::error::to_string() - 获取给定错误代码的描述字符串。
graph.run(runInteration);
try {
graph.wait(timeout);
}
catch (const std::system_error& ex) {
if (ex.code().value() == ETIME) {
xrt::error error(device, XRT_ERROR_CLASS_AIE);
auto errCode = error.get_error_code();
auto timestamp = error.get_timestamp();
auto err_str = error.to_string();
/* code to deal with this specific error */
std::cout << err_str << std::endl;
} else {
/* Something else */
}
}
以上代码展示了
在 xrt::graph::wait() 发生超时后,调用了 xrt::error 类的成员函数来获取异步错误代码和时间戳调用了成员函数 xrt::error::to_string() 来获取错误信息字符串。
2.10 Profiling
可以使用 XRT 性能分析类(xrt::aie::profiling)及其成员函数来配置 AI Engine 硬件资源,以进行性能分析和事件追踪。
2.10.1 Create Event
类构造函数 xrt::aie::profiling 用于创建性能分析事件对象,如下所示:
auto event = xrt::aie::profiling(device);
性能分析对象可用于执行分析功能,并通过调用性能分析 API 来收集性能统计信息。
2.10.2 Start Profiling
成员函数 xrt::aie::profiling::start() 用于根据作为参数传递的分析选项启动 AI Engine 中的性能计数器。此函数会配置 AI Engine 中的性能计数器并开始性能分析。
auto graph = xrt::graph(device, xclbin_uuid, "graph_name");
auto handle = event.start(xrt::aie::profiling::profiling_option option, std::string& port1, std::string& port2, int value);
// run graph
...
s2mm_run.wait();
它返回一个句柄(handle),以供后续的读取(read)和停止(stop)操作使用。
2.10.3 Read Profiling
xrt::aie::profiling::read 函数将返回与性能分析句柄关联的当前性能计数器值。可以通过使用性能分析事件对象来调用该函数,如下所示:
long long cycle_count = profile.read();
2.10.4 Stop Profiling
xrt::aie::profiling::stop 函数将停止与性能分析句柄关联的性能分析,并释放相应的硬件资源。
event.stop();
double throughput = output_size_in_bytes / (cycle_count *0.8 * 1e-3);
// Every AIE cycle is 0.8ns in production board
std::cout << "Throughput of the graph: " << throughput << " MB/s" << std::endl;
2.11 XRT Async
2.11.1 基本概念
xrt::queue 是一个轻量级、通用的队列实现,它与核心 XRT Native API 的数据结构完全分离。
XRT 队列的实现需要在代码中包含头文件 #include <experimental/xrt_queue.h>。主机代码需使用 g++ -std=c++17 进行编译。
注意:如果一个 API 具有同步行为,主机线程会阻塞,直到该任务完成。例如:
buffer.sync(XCL_BO_SYNC_BO_TO_DEVICE);
xrt::queue 队列具有以下特性:
- 任何在队列上入队的任务都会与原始的主机线程并行运行。因此,主机线程不会等待其完成,可以并行执行其他任务。
- 在队列上入队的任务本身必须是同步的。
- 所有在队列上入队的任务将按照入队顺序严格完成(严格的顺序执行)。
- 在队列上入队的任务可以是任何 C++ 可调用对象(Callable),这可以通过 C++ lambda 表达式方便地表示。
- 当一个同步任务在队列上入队时,会返回一个事件(xrt::queue::event)。该事件可以用于多种目的,例如:
- 主机线程可以等待该事件,以与生成事件的 queue::enqueue(task) 进行同步。
- 该事件可以入队到其他队列中,以实现队列之间的同步。
2.11.2 异步执行同步任务
auto bo0 = xrt::bo(device, vector_size_bytes, krnl.group_id(0));
auto bo0_map = bo0.map<dtype*>();
.... // fill buffer content
xrt::queue my_queue;
auto sync_event = queue.enqueue([&bo0] {bo0.sync(XCL_BO_SYNC_BO_TO_DEVICE); });
myCpuTask(b); // here we can perform other host task that will run parallel to the above bo::sync task
sync_event.wait(); // stall the host-thread till sync operation completes
上述代码展示了同步 API xrt::bo::sync 通过 xrt::queue 进行入队操作。
xrt::queue 的参数是一个使用了C++ lambda捕获缓冲区对象的未命名可调用对象。这种技术对于从主机线程异步执行任何同步任务非常有用,并且在这个任务进行的同时,主机线程可以并行执行其他操作(如上述代码中的 myCpuTask())。
xrt::queue::enqueue() 的返回类型是 xrt::queue::event,随后通过 xrt::queue::event::wait() 阻塞函数与主机线程进行同步。
2.11.3 通过队列执行多任务
每个新的 xrt::queue 可以被视为一个与主机线程并行运行的新线程,它会按照任务提交(入队)的顺序执行一系列同步任务。
例如,有 4 个任务 A、B、C 和 D:
- 任务 A:将数据从主机传输到设备(缓冲区 bo0)
- 任务 B:执行内核并等待内核完成执行
- 任务 C:将数据从设备传输到主机(缓冲区 bo_out)
- 任务 D:检查返回数据 bo_out
为了实现正确的功能,上述四个任务需要按顺序执行。为了使其与主机线程并行执行,可以将这四个任务通过队列入队,如下所示:
xrt::queue queue;
queue.enqueue([&bo0] {bo0.sync(XCL_BO_SYNC_BO_TO_DEVICE); });
queue.enqueue([&run] {run.start(); run.wait(); });
queue.enqueue([&bo_out] {bo_out.sync(XCL_BO_SYNC_BO_FROM_DEVICE); });
queue.enqueue([&bo_out_map]{my_function_to_check_data(bo_out_map)});
用户可以在主机代码中创建并使用任意数量的队列,以实现任务的并行重叠执行。
2.11.4 队列事件同步
让我们假设在上述示例中,需要在执行内核之前进行两次从主机到设备的缓冲区传输。如果使用单一队列,代码将会如下所示:
xrt::queue main_queue;
main_queue.enqueue([&bo0] {bo0.sync(XCL_BO_SYNC_BO_TO_DEVICE); });
main_queue.enqueue([&bo1] {bo1.sync(XCL_BO_SYNC_BO_TO_DEVICE); });
main_queue.enqueue([&run] {run.start(); run.wait(); });
main_queue.enqueue([&bo_out] {bo_out.sync(XCL_BO_SYNC_BO_FROM_DEVICE); });
在上述代码中,由于使用的是单一队列(main_queue),针对缓冲区 bo0 和 bo1 的主机到设备的数据传输将按顺序发生。为了实现 bo0 和 bo1 的并行数据传输,需要为其中一个缓冲区使用单独的队列,并且还需要确保内核仅在两个缓冲区传输都完成之后才执行。
xrt::queue main_queue;
xrt::queue queue_bo1;
main_queue.enqueue([&bo0] {bo0.sync(XCL_BO_SYNC_BO_TO_DEVICE); });
auto bo1_event = queue_bo1.enqueue([&bo1] {bo1.sync(XCL_BO_SYNC_BO_TO_DEVICE); });
main_queue.enqueue(bo1_event);
main_queue.enqueue([&run] {run.start(); run.wait(); });
main_queue.enqueue([&bo_out] {bo_out.sync(XCL_BO_SYNC_BO_FROM_DEVICE); });
上述代码中,通过两个独立的队列分别提交了 bo0 和 bo1 的主机到设备数据传输,以实现并行传输。
为了在这两个队列之间实现同步,将 queue_bo1 返回的事件(bo1_event)提交到 main_queue 中,类似于任务入队操作。因此,在该事件完成之前,任何在该事件之后提交的其他任务都不会执行。
因此,在上述代码示例中,main_queue 中的后续任务(例如内核执行)将等待 bo1_event 完成。通过将一个队列的 enqueue 操作返回的事件提交到另一个队列中,我们可以在队列之间实现同步。