4. AI Engine内存体系与数据流

AI Engine 的计算性能能否被真正发挥,很大程度上不取决于 MAC 单元本身,而取决于数据能否以足够带宽、足够低的延迟送到向量寄存器。理解 AI Engine 的内存层级,本质上是在理解一条“数据从外部 DDR 到计算单元”的完整通路,以及这条通路上每一级的容量、带宽和访问约束。

一、内存层级:从向量寄存器到外部 DDR

AI Engine 的内存体系可以按“离计算越近、容量越小、带宽越高”的规律划分为若干层。最靠近计算的是向量寄存器(Vector Register),每个 AI Engine Tile 拥有 8 个 256-bit 的向量寄存器,用于存放当前参与 MAC 运算的操作数。它们不是通用存储,而是与向量 ALU 直接耦合的暂存单元,一次运算的输入输出都在这里完成。

再往外是本地内存(Local Memory),每个 Tile 通常有 32 KB 的数据内存,用于存放本 Tile 的输入数据块、系数和中间结果。本地内存是 Tile 私有的,访问延迟低,但容量有限,无法容纳整帧数据。多个 Tile 之间需要交换数据时,则通过共享内存(Shared Memory)或 Tile 间直接通路完成。共享内存位于 Tile 阵列的特定区域,可被相邻 Tile 访问,用于实现跨 Tile 的数据复用与协作。

最外层是外部 DDR,容量大但访问延迟高、带宽受限。数据从 DDR 进入 AI Engine 阵列,必须经过 DMA 搬运,而不能由计算单元直接访问。因此,AI Engine 的数据流设计核心,就是规划好“DDR → 本地内存 → 向量寄存器”这条链路上每一级的搬运时机、数据块大小和缓冲策略。

AI Engine 内存层级与数据搬运通路 外部 DDR 大容量 / 高延迟 带宽受限 DMA 引擎 S2MM / MM2S 描述符驱动搬运 本地内存 32 KB / Tile 低延迟私有 向量寄存器 8 × 256-bit MAC 操作数 共享内存 / Tile 间通路 跨 Tile 数据复用 · 相邻 Tile 访问 · 协作缓冲 带宽约束关键点 • DDR 带宽是全局瓶颈:多 Tile 同时搬运时需仲裁,实际可用带宽低于理论峰值 • 本地内存容量决定数据块大小:块越大,DMA 启动开销占比越低,但缓冲压力越大 • 向量寄存器装载需与 MAC 流水对齐:装载延迟未隐藏会造成计算单元空转 • 共享内存访问存在端口竞争:多 Tile 同时读写会降低有效吞吐

二、DMA 与数据搬运机制

AI Engine 的数据搬运由 DMA 完成,典型模式是 S2MM(Stream to Memory-Mapped) 和 MM2S(Memory-Mapped to Stream)。在 AI Engine 编程模型中,更常用的是基于 aie::buffer 或 input_buffer/output_buffer 的抽象,由编译器生成 DMA 描述符。一个描述符通常包含源地址、目的地址、传输长度和步长信息。

实际工程中,数据搬运常采用双缓冲(Double Buffering)或乒乓缓冲策略:当计算单元处理缓冲区 A 时,DMA 同时把下一块数据搬入缓冲区 B,从而把搬运延迟隐藏在计算时间之后。判断双缓冲是否有效的依据是:单块数据的 DMA 搬运时间是否小于该块的计算时间。若搬运时间大于计算时间,则计算单元会等待数据,此时需要减小数据块、提高 DMA 并发度或优化数据布局。

下面给出一个典型的 AI Engine 核函数数据搬运结构示例,用于说明本地内存缓冲与向量寄存器装载的关系:

// 示例:双缓冲输入 + 向量寄存器装载
#include <aie_api.h>
#include <aie_kernel.h>

#define BLOCK_SIZE 256

void vector_mac_kernel(input_buffer<int16, adf::extents<BLOCK_SIZE>> &in,
                       output_buffer<int32, adf::extents<BLOCK_SIZE>> &out) {
    auto inIter = aie::begin_vector<16>(in);
    auto outIter = aie::begin_vector<16>(out);

    for (int i = 0; i < BLOCK_SIZE / 16; i++) {
        aie::vector<int16, 16> v = *inIter++;   // 从本地内存装载到向量寄存器
        aie::accum<acc32, 16> acc = aie::mul(v, v); // 向量 MAC
        *outIter++ = acc.to_vector<int32>();    // 结果写回本地内存
    }
}

这段代码中,begin_vector<16> 负责把本地内存中的数据按 16 个 int16 一组装载进向量寄存器,MAC 运算在寄存器上完成,结果再写回本地内存。真正的 DDR 到本地内存的搬运由 graph 层的 input_buffer 和 DMA 自动完成,核函数只面对本地内存。这种分层设计让计算代码不必关心 DDR 地址和搬运细节,但要求设计者在 graph 层合理设置缓冲深度和块大小。

三、内存带宽约束分析

带宽约束分析的核心是回答一个问题:当前数据流设计下,DDR 带宽和本地内存访问带宽是否足以支撑目标计算吞吐? 可以用一个简化的估算方法:

  • 设目标采样率为 fs,每样本需要搬运的字节数为 B,则单通道数据率 = fs × B。
  • 若系统有 N 个通道或 N 个并行 Tile,则总数据率 = N × fs × B。
  • 将该总数据率与 DDR 实际可用带宽比较,若接近或超过,则必须通过数据复用、片上缓存或降低精度来缓解。

例如,一个 8 通道、每通道 100 MSPS、每样本 2 字节的采集系统,总输入数据率为 8 × 100M × 2 = 1.6 GB/s。若 DDR 实际可用带宽约为 3~4 GB/s,则输入已占用近一半带宽,再叠加输出和系数读取,压力明显。此时可考虑在 AI Engine 内部做多通道合并处理,减少重复搬运,或把部分系数固化在本地内存中。

带宽分析时不要只看理论峰值。DDR 的实际可用带宽受刷新、仲裁、访问模式(连续 vs 随机)影响,通常只能达到理论值的 60%~75%。设计时应留出至少 30% 的余量。

四、数据流设计中的常见判断依据

在实际工程中,判断内存与数据流设计是否合理,可以观察以下几个信号:

观察现象 可能原因 调整方向
计算单元利用率低,频繁等待 DMA 搬运时间大于计算时间 减小数据块、增加缓冲深度、提高 DMA 并发
DDR 带宽接近饱和 数据复用不足,重复搬运多 片上缓存中间结果、合并通道处理
本地内存溢出或编译报错 缓冲块过大,超出 32 KB 限制 缩小块大小、分时复用本地内存
共享内存访问冲突明显 多 Tile 同时读写同一区域 调整 Tile 映射、错开访问时序

这些判断依据不是孤立的,而是相互关联的。例如,减小数据块虽然能缓解本地内存压力,但会增加 DMA 启动次数和描述符开销,可能反而降低有效带宽。因此,内存体系与数据流设计本质上是一个多约束下的权衡过程:在 DDR 带宽、本地内存容量、向量寄存器装载延迟和计算吞吐之间找到平衡点。理解每一级的容量与带宽边界,才能在实际工程中做出有依据的设计决策,而不是靠反复试错。