3. AI Engine核心架构与计算模型
AI Engine 是 Versal 器件中承担高吞吐信号处理的核心计算阵列。理解它的关键,不是记住某个模块名称,而是建立一条从“指令如何发射”到“数据如何流动”再到“结果如何累加”的完整计算模型。本节围绕 VLIW 向量处理器、SIMD 数据通路、累加器与数据移动单元,以及定点与浮点运算能力展开,帮助你在后续编写内核时知道每条指令真正作用在什么资源上。
VLIW:一条指令包驱动多个执行槽
每个 AI Engine 内核是一个 VLIW(超长指令字)向量处理器。它每个时钟周期取出一条“指令包”,包内可同时包含多个并行操作槽,典型包括:向量算术/逻辑槽、标量槽、地址生成槽、加载/存储槽以及控制流槽。也就是说,VLIW 把“并行性”交给编译器在编译期静态调度,硬件不再做复杂的乱序发射,从而在固定主频下获得可预测的吞吐。
这种模型对写代码有直接影响:循环展开、软件流水、数据对齐都会显著影响指令包能否被填满。若相邻操作存在数据依赖,编译器只能插入空槽,周期数上升;若把独立乘加、地址递增、数据搬运安排得当,一个周期内可同时推进多条数据通路。因此 AI Engine 的性能往往不是“主频决定”,而是“指令包填充率决定”。
SIMD 数据通路:向量寄存器与并行乘加
AI Engine 的向量单元采用 SIMD 结构。向量寄存器按固定宽度组织,例如 512 位或 1024 位,可被解释为多个 16 位、32 位或浮点元素。以 512 位寄存器承载 16 个 int32 为例,一条向量乘加指令即可在一个周期内完成 16 组乘加,这就是 SIMD 带来的并行度。实际可用宽度与数据类型、内核配置相关,需以具体器件手册为准。
SIMD 的代价是数据必须“整齐”。若输入来自非对齐缓冲区,或元素类型混用,加载时就需要额外的对齐、拼接或重排操作,占用本可用于计算的槽位。因此工程上常把数据按向量宽度对齐,并尽量让同一循环体内数据类型一致,减少拆包与打包。
累加器:把乘加结果安全地“攒”起来
向量乘加的输出不会直接丢弃,而是进入累加器。累加器位宽通常大于输入元素位宽,例如输入 int16、累加器 40 位或 48 位,这样在长序列求和时不易溢出。累加器还常配合饱和、舍入和移位操作,把高位宽结果压缩回目标输出格式。
以 16 抽头 FIR 滤波为例:输入 x[n] 为 int16,系数 h[k] 为 int16,每个输出 y[n] = Σ h[k]·x[n−k]。若用 int32 累加,16 个 int16 乘积之和最大约 16×2^30,已接近 int32 上限;改用 40 位累加器则余量充足。实际内核中,编译器会把乘加映射到向量 MAC,累加器在多个周期内持续累加,最后统一做舍入与饱和输出。判断结果是否正确,除了看数值,还要看是否发生饱和标志、是否出现非预期的截断。
数据移动单元:让计算单元不饿死
AI Engine 的性能瓶颈往往不在乘加,而在数据供给。数据移动单元负责在 AXI4-Stream、本地存储器、共享存储器和向量寄存器之间搬运数据。它支持直接内存访问式的批量传输,也支持流式接口的握手。若数据搬运与计算不能重叠,向量单元就会空转。
工程上常用“双缓冲”结构:当缓冲区 A 正在被计算时,DMU 把下一块数据搬入缓冲区 B;计算完成后交换角色。这样加载延迟被隐藏在计算之后。判断双缓冲是否生效,可以观察内核的 stall 周期占比,或看流接口是否持续有数据吞吐。
定点与浮点的选择
定点运算吞吐高、资源省,适合滤波、相关、矩阵乘等对动态范围要求可控的场景;浮点运算动态范围大、精度好,适合波束成形、雷达、通信等对数值稳定性要求高的场景。选择时先估算信号动态范围与所需信噪比:若定点累加器余量足够且量化误差可接受,优先定点;若中间结果跨度大、易溢出或需要高精度,则用浮点。
一个可参考的内核参数示例
下面给出一个 16 抽头 FIR 内核的示例参数表,用于说明各资源如何配合。参数为教学示例,实际值需按器件手册与设计目标调整。
| 项目 | 示例取值 | 说明 |
|---|---|---|
| 输入数据类型 | int16 | 与 ADC 输出位宽匹配 |
| 系数数据类型 | int16 | 量化后的滤波器系数 |
| 向量宽度 | 512 位 | 可容纳 32 个 int16 |
| 每周期乘加数 | 16 组 | 受 SIMD 与指令包限制 |
| 累加器位宽 | 40 位 | 抑制长序列求和溢出 |
| 输出格式 | int16,饱和 | 舍入后压缩回 16 位 |
| 缓冲策略 | 双缓冲 | 隐藏加载延迟 |
把这张表与前面的计算模型对应起来:VLIW 负责调度乘加、地址和搬运;SIMD 让 16 组乘加并行;40 位累加器保证 16 抽头求和安全;DMU 通过双缓冲持续供数。理解这条链路后,再去看内核代码中的向量类型、累加器声明和流接口,就能判断每一行代码在消耗哪类资源、可能在哪里形成瓶颈。