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 阵列提供了海量并行计算能力,但这也带来了新的挑战:
- 数据局部性挑战:2D-FFT 需要先对行进行 FFT,再对列进行 FFT,这中间涉及矩阵转置,如何在 AIE 和 PL 之间高效传输数据?
- 异构编程复杂性:需要协调 AIE 核(C++ 内核)、PL 逻辑(HLS 数据搬运核)和主机程序(XRT API)三大部分
- 性能验证需求:需要一个可靠的参考实现来验证自定义 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 的开销而设置的乘数。
架构详解
整体架构图
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 核承担了:
- 接收行 FFT 输出:从
FFTrows_graph读取数据 - 结果验证:检查第一行是否为全 1(预期结果),其余是否为 0
- 数据重排:将行优先的数据重新组织为列优先,供下一阶段使用
- 生成列 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_stageAPI 实现 32 点 FFTfft32_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 表示该核是计算密集型,应该优先分配资源
跨模块依赖关系
上游依赖(本模块依赖的其他模块)
-
prime_factor_fft_decomposition_graphs
- 本模块的 2D-FFT 使用了类似的分解思想(行/列分解)
- 共享了 AIE DSPLib 的 FFT 图组件
-
dma_source_kernel和dma_sink_kernel的设计理念相似- 都使用 HLS 实现 AIE-PL 数据接口
-
versal_integration_data_movers
- 本模块的主机代码使用了相同的 XRT API 模式
- 继承了对 xclbin 加载和设备管理的标准做法
下游依赖(依赖本模块的其他模块)
-
ifft4096_2d_graphs_and_characterization
- 基于本模块的 2D-FFT 架构扩展到 IFFT 场景
- 复用了行列分解的设计模式
-
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 区域。
扩展指南
添加新的矩阵尺寸支持
- 修改
Makefile中的维度定义:
MAT_ROWS=256 # 原来是 64
MAT_COLS=512 # 原来是 128
-
确保
FFT_ROW_CASCADE_LENGTH和FFT_COL_CASCADE_LENGTH适合新的点数 -
更新
system_configs/中的.cfg文件以反映新的资源需求
集成真实的 DDR 数据流
当前设计使用 PL 内部生成的测试数据。要接入真实数据源:
- 替换
mm2s0为从 DDR 读取的 MM2S 核 - 修改
s2mm1将结果写回 DDR - 在主机代码中添加 XRT Buffer Object 的分配和数据拷贝
参考 prime_factor_fft_system_integration 的实现方式。
性能调优建议
- 级联长度调整:增加
FFT_ROW_CASCADE_LENGTH可以提高并行度,但会增加资源消耗 - 窗口大小优化:调整
FFT_ROW_WS和FFT_COL_WS以平衡缓冲区和吞吐量 - 运行时比例微调:根据实际负载调整
runtime<ratio>,避免资源争用
总结
fft_reference_and_host_orchestrated_transforms 模块是理解 Versal AIE 2D-FFT 设计的最佳起点。它展示了:
- 分层架构:清晰的 AIE-PL-Host 三层分工
- 标准模式:使用 XRT API 编排异构计算资源
- 验证策略:内置的自我检查机制简化测试
- 可配置性:通过宏定义支持多种数据类型和矩阵尺寸
无论是作为学习材料、参考基准,还是扩展的基础,这个模块都提供了坚实的技术基础。