🏠

第5章:数学指挥家——构建大规模二维FFT

想象一下,你是一家大型电影院的放映师,手头有一张65536×65536像素的巨幕电影胶片,每一个像素的颜色都需要在瞬间转换到另一种颜色空间——你不能一格一格地处理,那样会等到天荒地老。你需要一套分工明确的流水线:有人负责横向处理,有人负责纵向处理,有人负责搬运胶片,有人负责把它们按正确的顺序摆好。

这正是我们在这一章要做的事情:把一个复杂度为\(O(N^2)\)的64K点二维FFT,分解成Versal芯片上AIE和PL可以并行消化的小任务。


5.1 从二维FFT的数学原理到"分工流水线"

在开始写代码之前,我们先搞清楚二维FFT到底在算什么,以及为什么它天然适合拆分成小任务。

二维FFT的"行-列分解"魔法

二维FFT有一个超棒的性质:二维FFT = 先做所有行的一维FFT,再做所有列的一维FFT(反过来也行)。

Think of it as:你要给一块巨型蛋糕的每一颗葡萄干都撒上糖霜。你可以先把每一行的葡萄干都撒好,然后把蛋糕转90度,再把每一列(原来的行)也撒好。

flowchart LR A[2D Image Matrix
64x64 = 4096 points] -->|Step 1: Row-wise| B[1D FFT on Every Row] B -->|Step 2: Intermediate
Transpose Buffer| C[Matrix Transposed] C -->|Step 3: Column-wise
treated as Rows| D[1D FFT on Every Column] D -->|Step 4: Output| E[2D FFT Result]

这个分解的意义

  • 我们不需要设计一个复杂的"二维FFT核",只需要设计一个高性能的"一维FFT核"
  • 行与行之间、列与列之间完全独立,可以并行处理
  • 我们可以把一维FFT进一步拆成更小的部分(比如基2/基4蝶形运算)

64K点FFT的二次分解

65536点(即64K点)的一维FFT还是太大了,单个AIE tile算不过来。我们可以用Cooley-Tukey算法再拆一次: $$FFT(N) = FFT(N1) \times FFT(N2) + \text{旋转因子}$$

例如 \(65536 = 256 \times 256\),我们可以把64K点FFT拆成256个256点FFT,加上一些旋转和转置操作。


5.2 系统架构总览:AIE负责计算,PL负责搬运

现在我们有了分解方案,接下来要把这些任务分配给Versal芯片上不同的"员工":

  • AIE阵列:负责最核心的一维FFT蝶形运算(数学计算专家)
  • PL(可编程逻辑):负责把数据从DDR搬到AIE,再搬回来,以及中间的转置操作(物流和仓储经理)
  • PS(处理系统):负责发号施令,配置DMA,检查结果(老板)

整体数据流图

graph TD subgraph "PS - Processing System (Boss)" Host[Host App
Python/C++] end subgraph "PL - Programmable Logic (Logistics)" DMA_In[DMA MM2S
Read from DDR] DMA_Out[DMA S2MM
Write to DDR] Transpose[Transpose Buffer
Reorder Data] end subgraph "AIE - AI Engine Array (Math Experts)" RowFFT[Row FFT Graph
N parallel tiles] ColFFT[Column FFT Graph
M parallel tiles] end Host -->|Configure| DMA_In Host -->|Configure| DMA_Out DDR_Mem[DDR Memory] -->|Raw Image| DMA_In DMA_In -->|Stream 0| RowFFT RowFFT -->|Stream 1| Transpose Transpose -->|Stream 2| ColFFT ColFFT -->|Stream 3| DMA_Out DMA_Out -->|FFT Result| DDR_Mem

走查一遍这个流程

  1. 老板(Host)物流经理(DMA_In) 去仓库(DDR)取原始图像数据
  2. DMA_In 把数据拆成流,发给 行计算专家(RowFFT)
  3. RowFFT 算完所有行的FFT,发给 中转仓库(Transpose) 换位
  4. 中转仓库 把数据发给 列计算专家(ColFFT)
  5. ColFFT 算完所有列的FFT,让 DMA_Out 送回仓库
  6. 老板 去仓库验收结果

5.3 深入AIE内部:一维FFT图的构建

现在我们把镜头拉近,看看AIE阵列内部的RowFFT是怎么工作的。

构建AIE数据流图(Graph)

Think of AIE的graph就像React的组件树——你可以把小的组件(kernel)组合成大的组件,每个组件只管自己的输入输出,不管内部怎么实现。

classDiagram class Kernel { <> void butterfly(input_stream, output_stream) } class Buffer { <> ping-pong storage } class FFT256Graph { <> +input plio_in +output plio_out } class RowFFTGraph { <> +input [N] plio_in +output [N] plio_out } FFT256Graph *-- Kernel : uses FFT256Graph *-- Buffer : uses RowFFTGraph *-- FFT256Graph : contains N instances

一个简化的AIE Graph代码示例

即使你是第一次见,也能看懂大概意思:

// 这是一个简化的256点FFT子图
class FFT256Graph : public adf::graph {
public:
    adf::input_plio in;  // 输入端口,像React的props
    adf::output_plio out; // 输出端口,像React的render返回值

    FFT256Graph() {
        // 创建输入输出PLIO(连接PL的接口)
        in = adf::input_plio::create("DataIn", adf::plio_64_bits, "data/input.txt");
        out = adf::output_plio::create("DataOut", adf::plio_64_bits, "data/output.txt");

        // 实例化计算核
        adf::kernel fft_kernel = adf::kernel::create(fft256_pipeline); // 这是具体的计算函数

        // 连接起来(就像电线)
        adf::connect<>(in.out[0], fft_kernel.in[0]);
        adf::connect<>(fft_kernel.out[0], out.in[0]);

        // 手动指定这个核放在哪个tile上(就像给员工安排座位)
        adf::location<kernel>(fft_kernel) = adf::tile(2, 0);
    }
};

关键点解释

  • PLIO:AIE和PL之间的接口,就像USB接口
  • Kernel:具体的计算函数,运行在单个AIE tile上
  • Location约束:我们手动指定kernel的位置,因为我们要确保数据传输路径最短,避免拥堵(还记得第4章的交通堵塞吗?)

5.4 从x1到x10:系统扩展的艺术

现在我们有了一个能工作的FFT系统。如果我们想让它处理得更快,比如同时处理10个图像帧,怎么办?

扩展策略:复制粘贴流水线

Think of it as:一条奶茶店生产线不够快,你就平行开10条一模一样的生产线,每条都有自己的配料员、摇茶员和出餐口。

graph TD subgraph "x1 Single Instance" DMA1[dma_hls_0] FFT1[fft_2d_0] end subgraph "x10 Multi Instance" DMA0[dma_hls_0] DMA10[dma_hls_9] FFT0[fft_2d_0] FFT10[fft_2d_9] end Host --> DMA1 Host --> DMA0 Host --> DMA10 DMA1 --> FFT1 DMA0 --> FFT0 DMA10 --> FFT10

系统配置文件的秘密

在Vitis里,我们通过.cfg文件告诉编译器怎么连接这些模块。这就像乐高的说明书

看一个x10配置的片段:

# 连接第0条流水线
stream_connect=dma_hls_0.strmOut_to_rowiseFFT:fft_2d_0.strmFFTrows_inp
stream_connect=fft_2d_0.strmFFTrows_out:dma_hls_0.strmInp_from_rowiseFFT
# ... 中间省略 ...
# 连接第9条流水线
stream_connect=dma_hls_9.strmOut_to_rowiseFFT:fft_2d_9.strmFFTrows_inp
stream_connect=fft_2d_9.strmFFTrows_out:dma_hls_9.strmInp_from_rowiseFFT

5.5 AIE vs HLS:两个计算选手的对比

在这个教程模块里,我们提供了两种实现FFT的方式:

  1. AIE实现:用AIE阵列做计算
  2. HLS实现:用PL逻辑做计算

这就像两个厨师做同一道菜:

  • AIE厨师:手脚麻利,擅长批量处理(向量运算),但需要你把菜切好配好递到手里
  • HLS厨师:可以自定义切菜配菜的全过程,更灵活,但批量处理没那么快

对比表

维度 AIE实现 HLS实现
计算方式 向量处理器阵列 自定义硬件逻辑
编程方式 C++ graph + kernel C++ HLS
最适合 规则的、大规模的并行计算 灵活的、控制密集型逻辑
数据搬运 依赖PL的DMA 可以自己包含DMA逻辑

关键洞察:最好的设计通常是混合设计——AIE负责算,PL负责搬数据和做 glue logic。


5.6 动手试一试:运行2D FFT设计

现在我们来看看怎么跑通这个设计。(参考fft2d_aie_vs_hls模块)

步骤1:选择配置

首先,你要决定是跑AIE版本还是HLS版本,是跑x1、x5还是x10。

就像你去餐厅点餐:

# 选择AIE版本,x10配置
cd Design/aie_x10

步骤2:编译设计

这一步就像把菜单给后厨,让他们准备食材和工具

# 编译AIE图
v++ -c --mode aie ...

# 编译PL内核
v++ -c --mode hw ...

# 把它们链接在一起
v++ -l --mode hw ...

步骤3:硬件仿真或上板运行

如果你没有硬件板,可以先跑硬件仿真(hw_emu),这就像在厨房模拟出餐流程

# 跑硬件仿真
./host_app --xclbin fft2d.xclbin --size 64x64

5.7 本章总结与下章预告

在这一章里,我们学会了:

  1. 分解算法:把二维FFT拆成行FFT、转置、列FFT
  2. 分工协作:让AIE负责计算,PL负责搬运
  3. 构建系统:用AIE graph把计算核连起来
  4. 水平扩展:复制整个流水线来处理更多数据
  5. 对比选择:知道什么时候用AIE,什么时候用HLS

你已经从一个只会看红绿灯的新手,变成了一个能指挥整个计算乐团的指挥家!

下章预告:真正的实战——把CNN搬到AIE-ML上

在下一章(也是最后一章),我们将迎来终极挑战:把一个用于MNIST手写数字识别的卷积神经网络(CNN),完整地映射到AIE-ML阵列上

我们会用到这一章学到的所有技能:

  • 分解算法(把CNN拆成卷积层、池化层、全连接层)
  • 构建graph(把每层连起来)
  • 数据搬运(用DMA把权重和图片送进去)

准备好了吗?我们终点见!

On this page