4. AI Engine阵列架构与编程模型

AI Engine 是 Versal 自适应 SoC 中承担高吞吐、低时延信号处理与矩阵运算的核心计算资源。理解 AI Engine,不能只把它看成“很多个 DSP”,而应把它看成由大量同构 tile 组成的二维计算阵列:每个 tile 内部有独立的向量处理器、标量处理器和本地内存,tile 之间通过专用的 AXI Stream 与共享内存网络互联,数据以流或内存访问的方式在阵列中流动。编程模型的关键,就是让计算与数据搬运在空间上并行、在时间上流水。

一、AI Engine tile 的内部结构

一个 AI Engine tile 可以理解为一个小型异构计算单元。其核心包括:

  • 向量处理器(Vector Processor):负责定点和浮点向量运算,支持 128-bit 或 256-bit 宽度的 SIMD 操作,是卷积、FFT、矩阵乘等密集计算的主力。
  • 标量处理器(Scalar Processor):负责控制流、地址计算、循环调度、条件判断和与外部接口的交互,通常运行 C/C++ 标量代码。
  • 本地内存(Local Memory):每个 tile 拥有独立的程序内存和数据内存,数据内存通常为 32 KB 左右,用于存放输入/输出缓冲和中间结果。
  • 流接口(Stream Interface):支持 AXI4-Stream 输入输出,用于 tile 之间以及 tile 与 NoC 之间的高速数据传递。
  • 共享内存接口:允许 tile 访问相邻 tile 的内存或全局内存,实现更灵活的数据交换。

向量处理器和标量处理器共享本地内存,但职责不同:标量处理器负责“怎么算”,向量处理器负责“算得快”。在编程时,标量代码通常用于循环控制、地址生成和边界处理,而向量代码则通过 intrinsic 或向量化编译指令映射到向量单元。

AI Engine Tile 内部结构与数据流 AI Engine Tile 向量处理器 SIMD 128/256-bit MAC / FFT / 矩阵运算 定点 + 浮点 标量处理器 控制流 / 循环调度 地址计算 / 条件判断 C/C++ 标量代码 本地内存(Local Memory) 程序内存 + 数据内存(约 32 KB) | 输入缓冲 / 输出缓冲 / 中间结果 AXI Stream 输入 AXI Stream 输出 控制/同步 共享内存接口 / 相邻 Tile 互联 向量计算 标量控制 本地内存 流接口 共享内存互联

二、内存层次与数据流编程范式

AI Engine 的内存层次分为三级:寄存器级(向量寄存器、标量寄存器)、本地内存级(每个 tile 的 32 KB 数据内存)和全局内存级(通过 NoC 或共享内存访问的 DDR/LPDDR)。数据流编程的核心思想是:让数据从全局内存流入 tile 本地内存,在本地内存中完成计算,再将结果流出,尽量减少对全局内存的随机访问。

在编程模型上,AI Engine 支持两种主要范式:

  • 数据流图(Data Flow Graph, DFG):用节点表示计算核(kernel),用边表示数据流。每个 kernel 在独立的 tile 上运行,通过 AXI4-Stream 或共享内存交换数据。这种模型天然适合流水线并行,例如雷达脉冲压缩、5G 信道估计等。
  • 单核编程模型:在单个 tile 内,用 C/C++ 编写标量控制代码和向量 intrinsic 代码,手动管理本地内存的读写和 DMA 搬运。适合算法原型验证和细粒度优化。

实际工程中,通常先用 DFG 划分计算任务,确定每个 kernel 的输入输出端口和缓冲深度,再对热点 kernel 进行单核优化。例如,一个 256 点复数 FFT 可以拆分为:输入重排 kernel、蝶形运算 kernel、输出重排 kernel,每个 kernel 占用一个 tile,通过 stream 串联。

三、一个可参考的 kernel 配置示例

下面给出一个 AI Engine kernel 的简化配置示例,用于说明数据流编程中常见的参数设置。该示例假设输入为 1024 个 16-bit 定点复数样本,输出为 1024 个 16-bit 定点复数样本,采用流接口输入输出。

// 示例:AI Engine kernel 端口与缓冲配置(合理假设,非实测)
// 输入:1024 个 cint16 样本,通过 AXI4-Stream 输入
// 输出:1024 个 cint16 样本,通过 AXI4-Stream 输出
// 本地内存:输入缓冲 4 KB,输出缓冲 4 KB,中间缓冲 8 KB

kernel my_fft_kernel {
  input  stream<cint16> in_data;   // 输入流
  output stream<cint16> out_data;  // 输出流

  // 本地内存分配
  cint16 in_buf[1024];   // 4 KB
  cint16 out_buf[1024];  // 4 KB
  cint16 tmp_buf[2048];  // 8 KB

  // 标量控制:循环读取输入流,写入本地缓冲
  for (int i = 0; i < 1024; i++) {
    in_buf[i] = readincr(in_data);
  }

  // 向量计算:调用 FFT intrinsic
  fft_1024(in_buf, tmp_buf, out_buf);

  // 标量控制:将结果写回输出流
  for (int i = 0; i < 1024; i++) {
    writeincr(out_data, out_buf[i]);
  }
}

在这个示例中,标量处理器负责流读写和循环控制,向量处理器负责 FFT 运算。本地内存被划分为输入缓冲、输出缓冲和中间缓冲,避免频繁访问全局内存。实际编译时,需要根据 tile 的本地内存容量和 stream FIFO 深度调整缓冲大小,否则会出现内存溢出或流阻塞。

实践提示:在 AI Engine 编程中,数据流的方向和缓冲深度决定了系统的吞吐率。如果某个 kernel 的输入流速率高于其处理速率,FIFO 会满,导致上游阻塞;反之,如果处理速率高于输入速率,FIFO 会空,导致下游等待。因此,在 DFG 设计阶段,应估算每个 kernel 的吞吐率(样本/周期)和延迟(周期数),并据此设置 FIFO 深度。

四、编程时需要注意的关键点

  • 向量化对齐:向量处理器要求数据按 128-bit 或 256-bit 对齐,否则会触发非对齐访问,降低性能。在本地内存中分配缓冲时,应使用对齐属性或手动填充。
  • 标量与向量协同:标量代码和向量代码共享本地内存,但向量运算通常需要先将数据从内存加载到向量寄存器,计算后再写回。应尽量减少内存与寄存器之间的搬运次数。
  • 流接口的背压:AXI4-Stream 支持背压(backpressure),当下游 FIFO 满时,上游会暂停。编程时应确保每个 kernel 都能正确处理流控信号,避免死锁。
  • 共享内存访问冲突:多个 tile 同时访问同一块共享内存时,会产生仲裁开销。应尽量让每个 tile 访问独立的本地内存,或通过 stream 传递数据。

总结来说,AI Engine 阵列的编程模型是“空间并行 + 时间流水”的结合:空间上,每个 tile 独立执行自己的 kernel;时间上,数据以流的方式在 tile 之间传递,形成流水线。掌握 tile 内部结构、内存层次和数据流编程范式,是发挥 Versal AI Engine 算力的基础。