FPGA 与硬件加速
FPGA 神经网络加速器的数据流设计
梳理 FPGA 上神经网络加速器的数据复用、流水线、片上缓存和 AXI 传输组织方式。
FPGA 神经网络加速器的设计难点通常不是乘加单元本身,而是数据如何稳定、连续、低开销地进入计算阵列。
当网络规模变大后,单纯堆 DSP 并不能保证吞吐提升。计算阵列需要输入特征、权重和部分和持续供应;如果数据搬运跟不上,DSP 会大量空转。FPGA 上的神经网络加速器设计,本质上是在片上存储、外部带宽、流水线深度和控制复杂度之间做平衡。
这篇笔记整理一个面向 CNN 类工作负载的数据流设计框架。它适合学习和研究原型阶段使用,不包含具体板卡上的性能结论。
数据流目标
- 减少 DDR 访问次数
- 提高片上 buffer 复用
- 保持流水线连续
- 让 AXI burst 更规则
- 降低控制状态复杂度
这些目标之间并不总是一致。例如,更大的 tile 可以提高数据复用,但会占用更多 BRAM/URAM;更深的流水线可以提高频率,但会让 valid 对齐和边界处理变复杂;更灵活的尺寸支持会提升通用性,但会增加控制逻辑和验证成本。
第一版原型通常应该优先保证数据流清晰和验证可控,而不是追求最复杂的调度策略。
常见结构
DDR
└─ AXI DMA
└─ Line Buffer
└─ Compute Array
└─ Output Buffer
└─ AXI DMA
这个结构可以拆成几个阶段:
| 阶段 | 主要任务 | 关键问题 |
|---|---|---|
| DDR 读取 | 从外部存储搬运输入和权重 | burst 长度、地址连续性、带宽利用 |
| 片上缓存 | 保存当前 tile 的数据 | BRAM 容量、端口冲突、双缓冲 |
| 窗口生成 | 形成卷积窗口 | padding、stride、边界 valid |
| 计算阵列 | 执行 MAC | DSP 映射、并行度、流水线 |
| 输出写回 | 写回结果或部分和 | 累加位宽、写回顺序、带宽 |
为什么数据流比 MAC 数量更重要
以一个 卷积为例,每个输出点需要 次乘加。如果每次乘加都从 DDR 读取输入和权重,外部带宽会成为主要瓶颈。片上数据复用的目标就是让一次 DDR 读取的数据参与尽可能多的计算。
输入特征图的空间复用可以来自滑动窗口:
row 0: x00 x01 x02 x03 ...
row 1: x10 x11 x12 x13 ...
row 2: x20 x21 x22 x23 ...
3x3 window moves by one column:
old: x00 x01 x02 new: x01 x02 x03
x10 x11 x12 x11 x12 x13
x20 x21 x22 x21 x22 x23
两个相邻窗口之间大部分数据是重叠的。如果没有 line buffer 和 window buffer,就会重复从外部存储读取相同数据。
复用分析
卷积层中的输入特征图可以在空间维度复用,权重可以在输出通道维度复用。若片上存储不足,需要分块处理。
| 复用对象 | 常见策略 | 风险 |
|---|---|---|
| 输入特征 | 行缓存、窗口缓存 | 边界处理复杂 |
| 权重 | 通道分块 | 权重加载开销 |
| 输出 | 局部累加 | 累加位宽增长 |
更具体地说,数据复用可以分成三类。
输入复用
输入复用主要利用卷积窗口的重叠。对 3x3 卷积来说,窗口每向右移动一列,只需要引入一列新数据。line buffer 可以保存前几行数据,window buffer 可以保存当前窗口。
Input Stream → Line Buffer → Window Buffer → MAC Array
边界处理是输入复用中的常见复杂点。padding 区域可以选择在读入阶段补零,也可以在 window generator 中根据坐标产生 zero valid。
权重复用
同一组权重会用于多个空间位置。因此可以把当前输出通道或输出通道 tile 的权重预加载到片上 buffer 中,再扫描输入特征图。
权重 buffer 的大小取决于:
其中 和 是输出通道和输入通道的 tile 大小。tile 越大,权重复用越好,但片上存储压力也越大。
输出复用
当输入通道被分块处理时,输出需要跨多个 tile 累加。此时可以把部分和保存在片上 buffer 中,等所有输入通道处理完成后再写回 DDR。
输出复用的主要问题是累加位宽和 buffer 容量。int8 乘法的输出通常不能直接用 int8 保存,需要更宽的累加类型。
Tile 设计
卷积层可以抽象成多个维度的 tile:
| 维度 | 含义 | 设计影响 |
|---|---|---|
T_h | 输出高度 tile | line buffer 深度、边界处理 |
T_w | 输出宽度 tile | burst 连续性、窗口滑动 |
T_ic | 输入通道 tile | 输入和权重 buffer 大小 |
T_oc | 输出通道 tile | MAC 并行度、输出 buffer |
一个 tile 的计算顺序可以写成:
for oc_tile:
clear partial sums
for ic_tile:
load input tile
load weight tile
compute and accumulate
write output tile
如果有双缓冲,可以在计算当前 tile 时预取下一组输入或权重:
time →
Buffer A: load tile 0 | compute tile 0 | load tile 2 | compute tile 2
Buffer B: | load tile 1 | compute tile 1 | load tile 3
双缓冲能隐藏部分数据搬运延迟,但会增加控制和存储资源。第一版设计可以先不用双缓冲,等瓶颈明确后再加入。
AXI 传输
在 Zynq 或带 AXI 总线的 FPGA 系统中,数据搬运通常通过 AXI DMA、AXI master 或 HLS 生成的 m_axi 接口完成。AXI 访问性能和地址连续性密切相关。
需要关注:
- 起始地址是否对齐。
- burst 是否足够长。
- 是否频繁跨行或跨 tile 跳转。
- 读写通道是否能与计算重叠。
- PS 和 PL 之间的数据 cache 是否需要 flush/invalidate。
一个比较规整的数据布局可以减少地址生成复杂度:
NCHW layout:
addr = base + (((n * C + c) * H + h) * W + w) * bytes
如果数据布局与计算访问顺序不匹配,可能需要在软件侧预处理,或者在硬件侧增加重排 buffer。二者都不是免费的,需要根据系统瓶颈选择。
HLS 与 RTL
HLS 适合快速验证数据流结构,但接口、数组分割和 pipeline pragma 需要严格检查。关键模块可以逐步替换成手写 RTL。
HLS 中常见优化包括:
#pragma HLS PIPELINE II=1
#pragma HLS ARRAY_PARTITION variable=weight complete dim=1
#pragma HLS DATAFLOW
这些 pragma 的效果必须通过综合报告和仿真验证确认。比如 PIPELINE II=1 可能因为存储端口冲突而无法达到,ARRAY_PARTITION 可能显著增加寄存器或 LUT 消耗,DATAFLOW 需要检查 FIFO 深度和死锁风险。
手写 RTL 的优势是控制更精细,尤其适合固定结构的数据通路、复杂握手和特定时序优化。缺点是开发周期更长、验证要求更高。
一个实用路径是:
- 用 Python 固定算法和定点化行为。
- 用 HLS 或高级模型快速探索 tile 和数据流。
- 对瓶颈模块改写手写 RTL。
- 保留统一测试向量和结果检查脚本。
- 在板级测试前完成仿真级接口和边界验证。
边界和异常情况
数据流设计最容易在边界情况下出错:
- 输入宽度不是 tile 宽度的整数倍。
- 输出通道数不能整除并行度。
- padding 区域参与窗口生成。
- stride 大于 1 时窗口移动不连续。
- 最后一个 burst 长度不足。
- 下游
ready拉低导致流水线停顿。
这些情况应该进入 directed test,而不是只依赖随机测试。特别是 valid/ready 接口,需要主动插入 stall,检查窗口和输出是否错位。
一个检查清单
在确定某个数据流前,可以用下面的问题自查:
| 问题 | 目的 |
|---|---|
| 每个输入元素平均被复用多少次? | 判断 line buffer 是否有效 |
| 权重加载是否被多个输出位置共享? | 判断权重 buffer 是否值得 |
| 部分和在哪里保存? | 判断输出 buffer 和累加位宽 |
| AXI 访问是否连续? | 判断外部带宽利用 |
| tile 边界如何处理? | 避免最后一块出错 |
| stall 时状态是否保持一致? | 保证系统集成可靠 |
阶段性总结
当前笔记只提供设计框架,具体资源占用和性能数据必须由实际板卡、频率、位宽和网络结构决定。
FPGA 神经网络加速器的数据流设计,核心是让数据搬运和计算阵列匹配。输入、权重和输出的复用方式决定了片上 buffer 组织;tile 大小决定了资源和带宽平衡;AXI 访问形态决定了系统级效率。第一版设计应先追求清晰和可验证,再根据实测瓶颈逐步优化。