矩阵与检测算法模块 (Matrix and Detection Algorithms)
一句话概括
本模块展示了如何在 Versal AI Engine 上实现通用矩阵乘法 (GeMM) 和 霍夫变换 (Hough Transform) 这两种经典的计算密集型算法,通过与 DSP/PL 实现的对比,帮助开发者理解 AIE 架构在矩阵运算和特征检测场景下的性能优势与编程范式。
问题空间:为什么需要这个模块?
想象你正在设计一个实时图像处理系统或雷达信号处理链路。这类应用有两个共同特点:
- 计算密集:需要执行大量的矩阵乘法(如神经网络推理)或参数空间投票(如直线检测)
- 实时性要求:必须在严格的时间预算内完成计算
传统的解决方案有两种:
- CPU/GPU:灵活性高,但功耗和延迟往往无法满足嵌入式边缘设备的要求
- FPGA 逻辑 (PL):可以定制化数据通路,但对于复杂的算术运算(如浮点 MAC),资源消耗大且难以达到最高频率
AMD Versal 架构引入了 AI Engine (AIE) —— 一种专为矢量计算优化的处理器阵列。本模块的核心问题是:如何将 GeMM 和霍夫变换这两种算法高效地映射到 AIE 架构上?
为什么选择这两个算法?
| 算法 | 计算特征 | AIE 适配价值 |
|---|---|---|
| GeMM | 规则的矩阵-矩阵乘法,高度并行,内存访问模式可预测 | 展示 AIE 的 SIMD 矢量单元和级联链 (Cascade Chain) 如何高效处理规则数据流 |
| 霍夫变换 | 图像空间到参数空间的非线性映射,需要三角函数和累加投票 | 展示 AIE 如何处理带有条件判断和非均匀内存访问的模式识别任务 |
心智模型:如何理解这个模块?
将 AIE 程序设计想象成组建一支工厂流水线团队:
┌─────────────────────────────────────────────────────────────────┐
│ AIE 程序设计心智模型 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 原料入库员 │ ───→ │ 流水线工人 │ ───→ │ 成品检验员 │ │
│ │ (Data Mover)│ │ (AIE Kernels)│ │ (Data Mover)│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ↑ ↓ │
│ └────────────── 仓库管理 (Graph) ──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
在这个类比中:
-
AIE Kernel(流水线工人):执行实际计算的核函数,每个 AIE 核心可以运行一个或多个 kernel。它们擅长 SIMD 矢量运算,通过专用级联链可以高效传递中间结果。
-
Graph(仓库管理):定义了 kernel 之间的连接关系、数据流向和并行度。它不负责具体计算,而是编排"谁把什么数据送给谁"。
-
Data Mover(原料入库员/成品检验员):位于 PL (Programmable Logic) 侧的 DMA 引擎,负责在 DDR 内存和 AIE 阵列之间搬运数据。它们是 CPU 主机与 AIE 世界的"翻译官"。
-
Host App(厂长):运行在 ARM 或 x86 上的控制程序,负责加载比特流、初始化 graph、启动执行、收集结果。
架构总览
xrtDeviceOpen
xrtGraphRun] end subgraph PL["Programmable Logic (PL)"] DM[dma_hls Kernel
AXI-MM Data Mover] end subgraph AIE["AI Engine Array"] GG[gemm_graph / dut_graph
ADF Graph Definition] AK[AIE Kernels
MAC Operations] end subgraph Memory["External Memory (DDR)"] BUF[Input/Output Buffers] end H -->|XRT API| DM H -->|XRT API| GG DM <-->|AXI-MM| BUF DM <-->|AXI-Stream| AK GG -.->|orchestrates| AK
两个核心示例的数据流
1. GeMM (General Matrix Multiply)
DDR Memory → [dma_hls] → AIE Array (gemm_graph)
↓
┌─────────────────────┐
│ 多个 AIE Core 并行 │
│ 执行矩阵分块乘法 │
│ 级联链传递部分和 │
└─────────────────────┘
↓
[dma_hls] ← AIE Array → DDR Memory
↓
硬件 Golden Check
GeMM 的实现采用了分块 (tiling) 和级联 (cascading) 策略:
- 大矩阵被切分成适合 AIE 本地内存的小块
- 多个 AIE core 通过级联链连接,形成一条计算流水线
- 每个 core 负责一部分 MAC 运算,并将中间结果传递给下一个 core
2. Hough Transform
输入图像分块 → [PLIO] → 32个 AIE Tile 并行处理
↓
┌─────────────────────┐
│ 每个 Tile 处理一个 │
│ 图像子区域的投票 │
│ ρ-θ 参数空间累加 │
└─────────────────────┘
↓
[PLIO] ← 各 Tile 输出 → 合并结果
霍夫变换的实现采用了空间并行化策略:
- 输入图像被划分为
NR × NC = 4 × 8 = 32个 tile - 每个 AIE tile 独立处理一个图像区域,在本地累加投票
- 通过
input_plio/output_plio与 PL 侧进行流式数据交换
核心组件详解
1. gemm_graph 类 (GeMM 应用)
文件: AI_Engine_Development/AIE/Design_Tutorials/10-GeMM_AIEvsDSP/AIE/design/host_app_src/gemm_aie_app.cpp
这是一个主机端控制类,封装了对 AIE graph 的 XRT 操作:
class gemm_graph {
xrtGraphHandle gemm_aie_gr; // XRT graph 句柄
public:
int init(xrtDeviceHandle dhdl, const axlf *top, char insts);
int run(void);
void close(void);
};
关键设计决策:
| 决策 | 选择 | 原因 |
|---|---|---|
| 迭代控制 | GRAPH_ITER_CNT 宏定义 |
支持无限循环模式 (ITER_CNT == -1) 用于功耗测试 |
| Graph 重置 | 每次 run() 前调用 xrtGraphReset() |
确保干净状态,避免上次运行的残留数据影响 |
| 多实例支持 | GEMM_INSTS 数组 |
允许同时运行多个独立的 GeMM 实例进行对比测试 |
内存所有权模型:
gemm_aie_gr句柄由xrtGraphOpen()分配,必须由xrtGraphClose()释放- 遵循 RAII 原则,但此类未实现析构函数自动关闭 —— 调用者必须显式调用
close()
2. datamover 类 (GeMM 配套)
文件: 同上
这是 PL 侧 DMA 内核的封装:
class datamover {
xrtKernelHandle dma_hls_khdl; // PL kernel 句柄
xrtRunHandle dma_hls_rhdl; // 运行句柄
uint32_t instance_errCnt; // 错误计数(从 ap_return 寄存器读取)
public:
void init(xrtDeviceHandle dhdl, const axlf *top, char insts);
void run(void);
void waitTo_complete(void);
void golden_check(uint32_t *errCnt, char insts);
void close(void);
};
关键机制:
-
缓冲区大小计算:
#define MATA_SZ (((GEMM_SIZE_ZP_A) * GEMM_SIZE / CASC_LN) / 8) * ITER_CNT * ((GEMM_SIZE_ZP_B/SPLIT) / DIM_B)这些宏根据矩阵维度、零填充大小、级联长度等参数计算出 DMA 传输的数据量。
-
硬件自检:
golden_check()方法从 PL kernel 的ap_return寄存器读取错误计数,这是 AIE vs DSP 对比测试的关键 —— 硬件在内部完成了结果验证,无需回传大量数据到主机。 -
独占模式:
dma_hls_khdl = xrtPLKernelOpenExclusive(dhdl, top->m_header.uuid, dma_hls_obj);使用独占模式打开 kernel,确保可以安全读取
ap_return寄存器。
3. dut_graph 类 (霍夫变换应用)
文件: AI_Engine_Development/AIE/Design_Tutorials/17-Hough-Transform/aie/hough_tile_app.cpp
这是ADF (AI Engine Data Flow) Graph 定义,直接继承自 adf::graph:
class dut_graph : public graph {
public:
static constexpr int RR = 216; // 图像行数
static constexpr int CC = 240; // 图像列数
static constexpr int RHO_MAX = 322; // ρ 参数范围
static constexpr int NR = 4, NC = 8; // Tile 网格: 4行 × 8列
static constexpr int NT = NR * NC; // 总 Tile 数: 32
static constexpr int THETA_NUM = 128; // θ 离散化数量
hough_tile_graph<RR,CC,NR,NC,RHO_MAX> dut; // 参数化子图
std::array<input_plio, NT> sig_i; // 32个输入端口
std::array<output_plio, NT> sig_o; // 32个输出端口
dut_graph(void) : dut(LOC_X, LOC_Y) { // 传入物理位置约束
for (int tt = 0; tt < NT; tt++) {
sig_i[tt] = input_plio::create("PLIO_i_"+std::to_string(tt), plio_64_bits, "data/sig_i_"+std::to_string(tt)+".txt");
sig_o[tt] = output_plio::create("PLIO_o_"+std::to_string(tt), plio_64_bits, "data/sig_o_"+std::to_string(tt)+".txt");
connect<stream,stream>(sig_i[tt].out[0], dut.sig_i[tt]);
connect<stream,stream>(dut.sig_o[tt], sig_o[tt].in[0]);
}
}
};
关键设计决策:
| 决策 | 选择 | 原因 |
|---|---|---|
| 模板参数化 | hough_tile_graph<RR,CC,NR,NC,RHO_MAX> |
编译时确定所有尺寸,允许编译器进行激进优化 |
| 物理位置绑定 | LOC_X, LOC_Y 数组 |
显式指定每个 tile 的 AIE 阵列坐标,优化布线延迟 |
| PLIO 宽度 | plio_64_bits |
每周期传输 64 位,平衡带宽与资源消耗 |
| 仿真数据文件 | .txt 文件路径 |
支持 AIE 仿真器 (aiesimulator) 离线调试 |
内存所有权模型:
sig_i和sig_o是值类型的std::array,由dut_graph实例拥有input_plio/output_plio对象通过静态工厂方法create()构造,生命周期与 graph 绑定- ADF runtime 负责底层缓冲区的分配和释放
设计权衡分析
1. AIE vs DSP/PL:何时选择什么?
本模块的两个教程都提供了 AIE 实现与其他方案的对比:
| 维度 | AIE 方案 | DSP/PL 方案 | 适用场景 |
|---|---|---|---|
| 开发效率 | 高级 C++ 描述,自动调度 | RTL/HLS 手动优化 | 快速迭代选 AIE,极致优化选 PL |
| 功耗效率 | 专用矢量单元,低功耗 | 通用逻辑,功耗较高 | 电池供电/热受限场景优先 AIE |
| 灵活性 | 固定架构,有限定制 | 完全可编程 | 非标算法选 PL,标准算子选 AIE |
| 延迟确定性 | 软件流水线,有 jitter | 硬连线,确定性 | 硬实时控制选 PL |
2. 数据搬移策略:PLIO vs GMIO
两个示例采用了不同的主机-AIE 通信方式:
-
GeMM: 使用
dma_hlsPL kernel + XRT 显式控制- 优点:精细控制 DMA 传输时机,适合批量数据处理
- 缺点:需要额外的 PL 资源实现 DMA 引擎
-
霍夫变换: 使用
input_plio/output_plio+ 仿真文件- 优点:更接近最终硬件的流式接口,便于仿真验证
- 缺点:需要 PL 侧实现对应的 stream 源/汇
3. 并行粒度:粗粒度 vs 细粒度
| 示例 | 并行策略 | 权衡考量 |
|---|---|---|
| GeMM | 级联链流水线(细粒度) | 最大化数据复用,减少 DDR 访问,但需要精心设计数据流 |
| 霍夫变换 | 空间分块并行(粗粒度) | 每个 tile 独立,易于扩展,但边界处理需要额外考虑 |
依赖关系
外部依赖
xrt_kernel.h
xrt_bo.h] ADF[adf/adf_api/XRTConfig.h] HG[hough_tile_graph.h] GP[graph.cpp] end G --> XRT G --> ADF G --> GP H --> HG
- XRT Runtime: AMD 的统一运行时库,提供对 AIE、PL、DDR 的统一抽象
- ADF API: AI Engine Data Flow 的 C++ 编程接口
- hough_tile_graph.h: 霍夫变换的子图定义(位于同一教程目录)
- graph.cpp: GeMM 的 graph 定义(包含 kernel 连接关系)
被依赖关系
本模块作为叶节点教程,主要被以下场景引用:
- 系统级集成教程(如 AIE_Design_System_Integration)
- 性能对比基准测试
新贡献者必读:陷阱与最佳实践
⚠️ 常见陷阱
-
迭代计数混淆
// GeMM 中有两个迭代概念: ITER_CNT // Data Mover 的传输次数 GRAPH_ITER_CNT // AIE Graph 的运行次数 // 当 ITER_CNT == -1 时,表示无限循环模式(用于功耗测试) // 此时 MATA_SZ/MATB_SZ/MATC_SZ 也被设为 -1后果: 混淆两者会导致数据传输与计算不匹配,产生死锁或错误结果。
-
Graph 句柄泄漏
// 错误:忘记关闭 graph gemm_graph gr; gr.init(dhdl, top, 0); gr.run(); // 缺少 gr.close()! // 正确:使用 RAII 或确保 close()后果: XRT 资源泄漏,多次运行后可能耗尽句柄配额。
-
PLIO 文件名硬编码
sig_i[tt] = input_plio::create(..., "data/sig_i_"+std::to_string(tt)+".txt");后果: 仿真时如果数据文件缺失或格式错误,
aiesimulator会报错但信息不明确。务必检查:- 文件路径相对于工作目录是否正确
- 文本文件格式是否符合 PLIO 期望(通常是十六进制或十进制数值)
-
物理位置冲突
std::vector<int> LOC_X({16, 17, ...}); // 起始列 16 std::vector<int> LOC_Y({0, 0, ...}); // 起始行 0后果: 如果指定的 AIE tile 坐标已被其他逻辑占用,布局布线会失败。Versal 器件的 AIE 阵列尺寸因型号而异,需查阅器件手册。
✅ 最佳实践
-
渐进式验证
- 先在
aiesimulator中验证功能正确性 - 再使用
hw_emu验证系统集成 - 最后上板测试
- 先在
-
性能分析要点
- 使用 Vitis Analyzer 查看 graph 的调度结果
- 关注 stall 百分比,识别内存瓶颈
- 检查级联链的利用率
-
调试技巧
- 启用
DEBUG宏获取详细日志 - 使用
printf在 AIE kernel 中打印中间结果(仅仿真有效) - 利用
xrtRunWait()的返回值判断执行状态
- 启用
相关模块链接
- AIE_ML_Design_Graphs - ML 相关的 AIE 设计图示例
- AIE_Design_System_Integration-baseline_aie_pl_integration_examples - AIE-PL 集成基础示例
- AIE_Design_Graphs_and_Algorithms-frequency_domain_transforms_and_spectral_graphs - 频域变换相关算法