🏠

矩阵与检测算法模块 (Matrix and Detection Algorithms)

一句话概括

本模块展示了如何在 Versal AI Engine 上实现通用矩阵乘法 (GeMM)霍夫变换 (Hough Transform) 这两种经典的计算密集型算法,通过与 DSP/PL 实现的对比,帮助开发者理解 AIE 架构在矩阵运算和特征检测场景下的性能优势与编程范式。


问题空间:为什么需要这个模块?

想象你正在设计一个实时图像处理系统或雷达信号处理链路。这类应用有两个共同特点:

  1. 计算密集:需要执行大量的矩阵乘法(如神经网络推理)或参数空间投票(如直线检测)
  2. 实时性要求:必须在严格的时间预算内完成计算

传统的解决方案有两种:

  • 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、启动执行、收集结果。


架构总览

graph TB subgraph Host["Host Application"] H[main.cpp
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);
};

关键机制

  1. 缓冲区大小计算

    #define MATA_SZ (((GEMM_SIZE_ZP_A) * GEMM_SIZE / CASC_LN) / 8) * ITER_CNT * ((GEMM_SIZE_ZP_B/SPLIT) / DIM_B)
    

    这些宏根据矩阵维度、零填充大小、级联长度等参数计算出 DMA 传输的数据量。

  2. 硬件自检golden_check() 方法从 PL kernel 的 ap_return 寄存器读取错误计数,这是 AIE vs DSP 对比测试的关键 —— 硬件在内部完成了结果验证,无需回传大量数据到主机。

  3. 独占模式

    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_isig_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_hls PL kernel + XRT 显式控制

    • 优点:精细控制 DMA 传输时机,适合批量数据处理
    • 缺点:需要额外的 PL 资源实现 DMA 引擎
  • 霍夫变换: 使用 input_plio/output_plio + 仿真文件

    • 优点:更接近最终硬件的流式接口,便于仿真验证
    • 缺点:需要 PL 侧实现对应的 stream 源/汇

3. 并行粒度:粗粒度 vs 细粒度

示例 并行策略 权衡考量
GeMM 级联链流水线(细粒度) 最大化数据复用,减少 DDR 访问,但需要精心设计数据流
霍夫变换 空间分块并行(粗粒度) 每个 tile 独立,易于扩展,但边界处理需要额外考虑

依赖关系

外部依赖

graph LR subgraph ThisModule["matrix_and_detection_algorithms"] G[gemm_aie_app.cpp] H[hough_tile_app.cpp] end subgraph External["External Modules"] XRT[xrt_aie.h
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 连接关系)

被依赖关系

本模块作为叶节点教程,主要被以下场景引用:


新贡献者必读:陷阱与最佳实践

⚠️ 常见陷阱

  1. 迭代计数混淆

    // GeMM 中有两个迭代概念:
    ITER_CNT      // Data Mover 的传输次数
    GRAPH_ITER_CNT // AIE Graph 的运行次数
    
    // 当 ITER_CNT == -1 时,表示无限循环模式(用于功耗测试)
    // 此时 MATA_SZ/MATB_SZ/MATC_SZ 也被设为 -1
    

    后果: 混淆两者会导致数据传输与计算不匹配,产生死锁或错误结果。

  2. Graph 句柄泄漏

    // 错误:忘记关闭 graph
    gemm_graph gr;
    gr.init(dhdl, top, 0);
    gr.run();
    // 缺少 gr.close()!
    
    // 正确:使用 RAII 或确保 close()
    

    后果: XRT 资源泄漏,多次运行后可能耗尽句柄配额。

  3. PLIO 文件名硬编码

    sig_i[tt] = input_plio::create(..., "data/sig_i_"+std::to_string(tt)+".txt");
    

    后果: 仿真时如果数据文件缺失或格式错误,aiesimulator 会报错但信息不明确。务必检查:

    • 文件路径相对于工作目录是否正确
    • 文本文件格式是否符合 PLIO 期望(通常是十六进制或十进制数值)
  4. 物理位置冲突

    std::vector<int> LOC_X({16, 17, ...});  // 起始列 16
    std::vector<int> LOC_Y({0, 0, ...});    // 起始行 0
    

    后果: 如果指定的 AIE tile 坐标已被其他逻辑占用,布局布线会失败。Versal 器件的 AIE 阵列尺寸因型号而异,需查阅器件手册。

✅ 最佳实践

  1. 渐进式验证

    • 先在 aiesimulator 中验证功能正确性
    • 再使用 hw_emu 验证系统集成
    • 最后上板测试
  2. 性能分析要点

    • 使用 Vitis Analyzer 查看 graph 的调度结果
    • 关注 stall 百分比,识别内存瓶颈
    • 检查级联链的利用率
  3. 调试技巧

    • 启用 DEBUG 宏获取详细日志
    • 使用 printf 在 AIE kernel 中打印中间结果(仅仿真有效)
    • 利用 xrtRunWait() 的返回值判断执行状态

相关模块链接

On this page