🏠

FFT Reference and Host Orchestrated Transforms

一句话概括

本模块是 Versal ACAP 平台上AI Engine (AIE) 2D-FFT 计算的核心参考实现,它展示了如何将复杂的二维傅里叶变换分解为两个级联的一维 FFT 阶段,并通过主机程序精确编排 AIE 图与 PL 数据搬运核的协同工作。可以把它想象成一个精密的交响乐团:AIE 核是指演奏家(执行实际的 FFT 计算),PL 数据搬运核是舞台工作人员(负责数据的输入输出和转置),而主机程序则是指挥家——它决定何时开始、何时结束、如何协调各个声部的节奏。


问题空间与设计动机

为什么需要这个模块?

在信号处理、雷达、通信和图像处理领域,二维 FFT (2D-FFT) 是一种基础且计算密集型的操作。例如:

  • 合成孔径雷达 (SAR) 成像需要对大型数据矩阵进行 2D-FFT
  • 医学影像处理(如 MRI)依赖频域分析
  • 无线通信系统中的信道估计和均衡

直接在 CPU 或 GPU 上执行大规模 2D-FFT 会面临带宽瓶颈和延迟问题。Versal ACAP 平台通过 AI Engine 阵列提供了海量并行计算能力,但这也带来了新的挑战:

  1. 数据局部性挑战:2D-FFT 需要先对行进行 FFT,再对列进行 FFT,这中间涉及矩阵转置,如何在 AIE 和 PL 之间高效传输数据?
  2. 异构编程复杂性:需要协调 AIE 核(C++ 内核)、PL 逻辑(HLS 数据搬运核)和主机程序(XRT API)三大部分
  3. 性能验证需求:需要一个可靠的参考实现来验证自定义 FFT 实现的正确性

设计目标

本模块旨在提供:

  • 教学价值:展示 AIE 2D-FFT 的标准设计模式
  • 参考基准:作为其他优化实现的比较基准
  • 可扩展模板:支持从 32×64 到 1024×2048 等多种矩阵尺寸
  • 多实例配置:支持 x1、x5、x10 等不同并行度配置

核心抽象与心智模型

类比:工厂流水线

想象一个现代化的汽车装配厂:

组件 工厂类比 技术对应
AIE FFT 图 装配机器人工作站 FFTrows_graph + FFTcols_graph,执行实际的 FFT 计算
PL 数据搬运核 传送带和机械臂 dma_hls HLS 核,负责数据进出和格式转换
主机程序 中央控制系统 fft2d_hostapp_graph + datamover 类,编排整个流程
矩阵数据 待装配的汽车底盘 通过 128-bit AXI-Stream 传输的数据块

核心抽象层次

┌─────────────────────────────────────────────────────────────┐
│                    Host Application                         │
│              (fft_2d_aie_app.cpp - 指挥家)                   │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────┐    ┌─────────────────────────────────┐ │
│  │ datamover       │    │ fft2d_hostapp_graph             │ │
│  │ (PL DMA 控制)   │    │ (AIE Graph 控制)                │ │
│  └────────┬────────┘    └──────────────┬──────────────────┘ │
└───────────┼────────────────────────────┼────────────────────┘
            │ XRT API                    │ XRT Graph API
            ▼                            ▼
┌─────────────────────────────────────────────────────────────┐
│                  PL Logic (HLS Kernels)                     │
│              (dma_hls.cpp - 舞台工作人员)                    │
├─────────────────────────────────────────────────────────────┤
│  mm2s0 → dmaHls_rowsToCols → s2mm1                          │
│  (输入)    (行→列转置)        (输出验证)                      │
└─────────────────────────────────────────────────────────────┘
            ▲                            │
            │ AXI-Stream 128-bit         │
            └────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                 AI Engine Array (AIE)                       │
│              (graph.h/cpp - 演奏家们)                        │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────┐    ┌─────────────────┐                 │
│  │ FFTrows_graph   │───▶│ FFTcols_graph   │                 │
│  │ (行方向 FFT)     │    │ (列方向 FFT)     │                 │
│  └─────────────────┘    └─────────────────┘                 │
└─────────────────────────────────────────────────────────────┘

关键数据结构

1. 数据类型配置 (FFT_2D_DT)

// FFT_2D_DT == 0: cint16 (复数 16-bit 整数)
// FFT_2D_DT == 1: cfloat (复数浮点)

这个选择影响:

  • 精度 vs 资源:cint16 节省 DSP 资源但精度有限;cfloat 精度高但消耗更多资源
  • 数据传输粒度:cint16 每个 128-bit 传输 4 个样本;cfloat 传输 2 个样本
  • 内存占用:cfloat 数据量是 cint16 的两倍

2. 窗口大小计算

// 行方向 FFT 窗口大小
#define FFT_ROW_TP_WINDOW_VSIZE MAT_ROWS * FFT_ROW_WS

// 列方向 FFT 窗口大小
#define FFT_COL_TP_WINDOW_VSIZE MAT_COLS * FFT_COL_WS

这里的 WS (Window Size multiplier) 是为了减少 ping-pong buffer 的开销而设置的乘数。


架构详解

整体架构图

graph TB subgraph Host["Host Application"] H[main function] DM[datamover class] FG[fft2d_hostapp_graph class] end subgraph PL["Programmable Logic (HLS)"] MM2S0[mm2s0
Input Generator] DMAC[dmaHls_rowsToCols
Transpose & Check] S2MM1[s2mm1
Output Validator] end subgraph AIE["AI Engine Array"] ROW[FFTrows_graph
Row-wise FFT] COL[FFTcols_graph
Column-wise FFT] end H -->|init/run/close| DM H -->|init/run/close| FG DM -->|xrtRunStart| MM2S0 DM -->|xrtRunStart| DMAC DM -->|xrtRunStart| S2MM1 FG -->|xrtGraphRun| ROW FG -->|xrtGraphRun| COL MM2S0 -->|AXI-Stream| ROW ROW -->|AXI-Stream| DMAC DMAC -->|AXI-Stream| COL COL -->|AXI-Stream| S2MM1

数据流详解

阶段 1:输入生成 (mm2s0)

void mm2s0(hls::stream<ap_axiu<128, 0, 0, 0>> &strmOut_to_rowiseFFT, int matSz)
  • 功能:生成脉冲输入信号用于测试
  • 数据模式:第一个元素为特定值(INP_DATA),其余为零
  • 用途:这种输入经过 2D-FFT 后应该产生全 1 的输出,便于验证正确性

阶段 2:行方向 FFT (FFTrows_graph)

class FFTrows_graph : public graph {
    dsplib::fft::dit_1ch::fft_ifft_dit_1ch_graph<...> FFTrow_gr;
    // 使用 Xilinx DSPLib 的 FFT 图
};
  • 算法:DIT (Decimation-In-Time) 基-2 FFT
  • 级联长度:可通过 FFT_ROW_CASCADE_LENGTH 配置,支持多核并行
  • 运行时比例:设置为 0.8,表示该图占用 80% 的 AIE 核时间

阶段 3:行列转置与中间检查 (dmaHls_rowsToCols)

这是整个设计的关键创新点。由于 AIE 图通常专注于计算,而这个 PL 核承担了:

  1. 接收行 FFT 输出:从 FFTrows_graph 读取数据
  2. 结果验证:检查第一行是否为全 1(预期结果),其余是否为 0
  3. 数据重排:将行优先的数据重新组织为列优先,供下一阶段使用
  4. 生成列 FFT 输入:构造新的脉冲输入(这次是沿列方向的脉冲)
void dmaHls_rowsToCols(
    hls::stream<ap_axiu<128, 0, 0, 0>> &strmInp_from_rowiseFFT,
    hls::stream<ap_axiu<128, 0, 0, 0>> &strmOut_to_colwiseFFT,
    int matSz, int rows, int cols, int &stg0_errCnt,
    ap_uint<128> goldenVal
)

阶段 4:列方向 FFT (FFTcols_graph)

结构与 FFTrows_graph 类似,但处理的是列方向的数据。

阶段 5:输出验证 (s2mm1)

void s2mm1(
    hls::stream<ap_axiu<128, 0, 0, 0>> &strmInp_from_colwiseFFT,
    int matSz, int &stg1_errCnt, ap_uint<128> goldenVal
)
  • 功能:验证最终输出是否全部为预期的 goldenVal
  • 错误计数:任何不匹配都会增加 stg1_errCnt

子模块划分

本模块包含两个主要的设计示例,分别展示了不同的 FFT 应用场景:

1. 2D-FFT AIE vs HLS 对比设计 (fft2d_aie_vs_hls)

位于 06-fft2d_AIEvsHLS/AIE/design/ 目录下,这是一个完整的系统级设计,包含:

  • AIE 源文件 (aie_src/): graph.h, graph.cpp —— 定义 2D-FFT 的 AIE 图结构
  • PL 源文件 (pl_src/): dma_hls.h, dma_hls.cpp —— HLS 实现的数据搬运和验证逻辑
  • 主机应用 (host_app_src/): fft_2d_aie_app.cpp —— 使用 XRT API 编排整个系统
  • 系统配置 (system_configs/): x1.cfg, x5.cfg, x10.cfg —— 不同并行度的配置文件

核心类

  • fft2d_hostapp_graph: 封装 AIE 图的 XRT 操作
  • datamover: 封装 PL DMA 核的 XRT 操作
  • FFT2D_graph: AIE 顶层图,包含行/列 FFT 子图数组

2. 32 点 Radix-2 FFT 参考实现 (fft32_r2_reference)

位于 13-FFT-DFT-on-AIE/fft32_r2/ 目录下,这是一个更基础的 FFT 实现,适合学习 AIE 内核编程:

  • 内核实现 (fft32_r2_kernel.h/cpp): 手写 Radix-2 FFT 内核,展示如何使用 AIE API
  • 图定义 (fft32_r2_graph.h): 简单的单核 FFT 图
  • 应用入口 (fft32_r2_app.cpp): 仿真和测试用的主程序

核心类

  • fft32_r2_kernel: 使用 aie::fft_dit_r2_stage API 实现 32 点 FFT
  • fft32_r2_graph: 单核 FFT 图,用于演示基本连接模式
  • dut_graph: 设备测试图,添加 PLIO 接口

关键设计决策与权衡

1. 为什么使用 HLS 而非 RTL 实现数据搬运?

选择的方案:使用 Vitis HLS 编写 dma_hls

权衡分析

方面 HLS 优势 潜在代价
开发效率 C++ 抽象,快速迭代 对精细时序控制较弱
可维护性 代码可读性强,易于修改 生成的 RTL 可能不是最优
验证 可使用 C 仿真快速验证算法 需要额外的协同仿真
资源利用 现代 HLS 工具优化良好 极端情况下可能不如手写 RTL

设计意图:这是一个教程性质的设计,优先考虑可读性和可修改性,让开发者能够快速理解数据流并尝试自己的修改。

2. 为什么在 PL 中进行行列转置而非 AIE?

选择的方案:在 dmaHls_rowsToCols 中完成行列数据重排

替代方案:可以在 AIE 中使用专门的转置内核

权衡分析

  • PL 方案的优势

    • AIE 核专注于 FFT 计算,保持其通用性
    • PL 的 BRAM 更适合随机访问模式的转置操作
    • 可以利用 HLS DATAFLOW 优化吞吐量
  • PL 方案的劣势

    • 增加了 PL 逻辑的复杂度
    • AIE-PL 接口成为潜在瓶颈

设计意图:遵循关注点分离原则,让 AIE 做它最擅长的(并行 FFT 计算),让 PL 处理数据重组。

3. 为什么使用脉冲输入进行自我验证?

选择的方案:输入脉冲信号,验证输出是否为全 1

数学原理

  • 脉冲信号的 2D-DFT 是全 1 矩阵
  • \(\mathcal{F}\{\delta[m,n]\} = 1\) 对所有频率分量

优势

  • 无需外部参考数据文件
  • 可以在硬件运行时实时验证
  • 简化了测试流程

局限性

  • 只能验证基本功能,不能检测所有类型的错误
  • 对于实际应用场景,需要额外的真实数据测试

4. 数据类型选择的考量

cint16 vs cfloat

#if FFT_2D_DT == 0  // cint16
   #define INP_DATA 0X00010001
   #define GOLDEN_DATA 0X0001000100010001
#elif FFT_2D_DT == 1  // cfloat
   #define INP_DATA 0x3fc000003fc00000  // 1.5 in IEEE 754
   #define GOLDEN_DATA 0x3fc000003fc00000
#endif

设计考虑

  • cint16(默认):适合大多数信号处理应用,资源效率高
  • cfloat:用于需要高精度或动态范围的应用(如科学计算)

关键影响:数据类型决定了 128-bit 总线上能传输多少样本:

  • cint16: 4 样本/周期 (\(128 / (16 \times 2 \text{ (real+imag)}) = 4\))
  • cfloat: 2 样本/周期 (\(128 / (32 \times 2) = 2\))

这直接影响了系统的最大吞吐量。

5. 运行时比例 (runtime<ratio>) 的设置

runtime<ratio>(*FFTrow_gr.getKernels()) = 0.8;  // FFT 图使用 80%
runtime<ratio>(k_kernel) = 0.9;                  // 单核 FFT 使用 90%

设计原理

  • 不为 1.0 是为了给调度器留出余地,避免时序冲突
  • 较高的 ratio 表示该核是计算密集型,应该优先分配资源

跨模块依赖关系

上游依赖(本模块依赖的其他模块)

  1. prime_factor_fft_decomposition_graphs

    • 本模块的 2D-FFT 使用了类似的分解思想(行/列分解)
    • 共享了 AIE DSPLib 的 FFT 图组件
  2. fft_dma_data_movers

    • dma_source_kerneldma_sink_kernel 的设计理念相似
    • 都使用 HLS 实现 AIE-PL 数据接口
  3. versal_integration_data_movers

    • 本模块的主机代码使用了相同的 XRT API 模式
    • 继承了对 xclbin 加载和设备管理的标准做法

下游依赖(依赖本模块的其他模块)

  1. ifft4096_2d_graphs_and_characterization

    • 基于本模块的 2D-FFT 架构扩展到 IFFT 场景
    • 复用了行列分解的设计模式
  2. large_scale_ifft2d_pipeline_and_support_blocks

    • 将本模块的概念扩展到更大规模(64K 点 IFFT2D)
    • 借鉴了主机编排的模式

新贡献者注意事项

常见陷阱

1. 矩阵尺寸宏定义的混淆

// 注意:这些宏会根据数据类型改变含义!
#define MAT_SIZE_128b (MAT_SIZE / 4)  // cint16: /4
#define MAT_SIZE_128b (MAT_SIZE / 2)  // cfloat: /2

陷阱:修改数据类型后忘记更新相关的尺寸计算,导致缓冲区溢出或数据截断。

建议:始终使用预定义的宏(如 MAT_SIZE_128b)而不是手动计算。

2. 迭代次数的换算

// 主机代码中的关键换算
if (iterCnt == -1)
    graph_itercnt = iterCnt;  // 无限运行
else
    graph_itercnt = (iterCnt * MAT_ROWS);  // 注意乘以行数!

陷阱:AIE 图的迭代次数是基于行的,因为每行数据处理触发一次图运行。

原因:2D-FFT 需要处理 MAT_ROWS × MAT_COLS 个元素,但图每次处理一行,所以需要 ITER_CNT × MAT_ROWS 次图运行。

3. PLIO 命名与实例编号的关联

std::string rows_plioIn_str = "DataIn" + std::to_string(fftRows_grInsts*2);
std::string cols_plioIn_str = "DataIn" + std::to_string(fftCols_grInsts*2 + 1);

陷阱:行和列的 PLIO 使用不同的编号规则(行用偶数,列用奇数),这与实例索引相关。

调试建议:如果连接失败,检查 xclbin 中的 kernel 名称是否与代码中构造的名称匹配。

4. 约束文件的版本差异

#if FFT_2D_DT==1
   if(FFT_ROW_TP_POINT_SIZE >= 1024 && FFT_2D_DT==1 && FFT2D_INSTS==10 )
      location<graph>(*this) = area_group({{aie_tile, fftRows_grInsts*4, 0, fftRows_grInsts*4+3, 7}, ...});
#else
   location<graph>(*this) = area_group({{aie_tile, 5 + fftRows_grInsts*2 , 0, 2*fftRows_grInsts+6, 2}, ...});
#endif

陷阱:cfloat 模式和 cint16 模式使用完全不同的 AIE tile 布局约束。

原因:cfloat 运算需要更多的 DSP 资源和更大的 tile 区域。

扩展指南

添加新的矩阵尺寸支持

  1. 修改 Makefile 中的维度定义:
MAT_ROWS=256  # 原来是 64
MAT_COLS=512  # 原来是 128
  1. 确保 FFT_ROW_CASCADE_LENGTHFFT_COL_CASCADE_LENGTH 适合新的点数

  2. 更新 system_configs/ 中的 .cfg 文件以反映新的资源需求

集成真实的 DDR 数据流

当前设计使用 PL 内部生成的测试数据。要接入真实数据源:

  1. 替换 mm2s0 为从 DDR 读取的 MM2S 核
  2. 修改 s2mm1 将结果写回 DDR
  3. 在主机代码中添加 XRT Buffer Object 的分配和数据拷贝

参考 prime_factor_fft_system_integration 的实现方式。

性能调优建议

  1. 级联长度调整:增加 FFT_ROW_CASCADE_LENGTH 可以提高并行度,但会增加资源消耗
  2. 窗口大小优化:调整 FFT_ROW_WSFFT_COL_WS 以平衡缓冲区和吞吐量
  3. 运行时比例微调:根据实际负载调整 runtime<ratio>,避免资源争用

总结

fft_reference_and_host_orchestrated_transforms 模块是理解 Versal AIE 2D-FFT 设计的最佳起点。它展示了:

  1. 分层架构:清晰的 AIE-PL-Host 三层分工
  2. 标准模式:使用 XRT API 编排异构计算资源
  3. 验证策略:内置的自我检查机制简化测试
  4. 可配置性:通过宏定义支持多种数据类型和矩阵尺寸

无论是作为学习材料、参考基准,还是扩展的基础,这个模块都提供了坚实的技术基础。

On this page