MNIST ConvNet 层与完整网络图 (mnist_convnet_layer_and_full_network_graphs)
一句话概括
本模块实现了完整的 MNIST 手写数字识别卷积神经网络在 AMD Versal AI Engine-ML (AIE-ML) 上的硬件映射,采用分层验证、逐层组装的设计哲学——每个网络层(卷积、池化、全连接)都可独立测试验证,最终通过图组合机制构建完整推理流水线。
问题背景与设计动机
我们要解决什么问题?
在嵌入式 AI 加速器上部署神经网络时,开发者面临三个核心挑战:
- 调试复杂性:完整网络包含数十个算子,任何一个内核的错误都会导致整个系统失效,定位问题如同大海捞针。
- 资源映射不确定性:AIE-ML 的 tiles 数量有限,如何合理分配计算、缓冲区和权重存储是个 NP-hard 问题。
- 性能可移植性:不同网络结构(Kernel Size、Stride、Padding 变化)需要重新优化数据流。
为什么选择分层图组合方案?
想象一下搭建乐高积木:你不会一次性把所有零件混在一起拼装,而是先按说明书把轮子、底盘、车身分别组装好,确认每个子模块工作正常后,再组合成完整汽车。
本模块采用完全相同的策略:
- conv2d_w1/w3/w5:三种不同卷积核尺寸的独立验证环境
- max_pooling2d_w2/w4:两种池化窗口的独立图
- dense_w7:全连接层的独立测试平台
- mnist:将上述所有子图作为组件组合而成的完整网络
核心抽象与心智模型
核心抽象一:AIE Graph 作为可组合组件
┌─────────────────────────────────────────────────────────┐
│ AIE Graph 结构 │
├─────────────────────────────────────────────────────────┤
│ Input PLIO ──► Sub-graph (内核组合) ──► Output PLIO │
│ │ │
│ └─ 从文件读取测试数据 (txt) │
└─────────────────────────────────────────────────────────┘
每个 dut_graph 类继承自 graph,包含:
- PLIO (Programmable Logic I/O):连接 AIE 阵列与外部 PL/DDR 的接口
- Sub-graph:实际执行计算的子图(如
conv2d_w3_graph) - Connect 语句:定义数据流拓扑
核心抽象二:Tile 级别的资源分配
AIE-ML Tile Grid (x, y coordinates)
y=0 [W0] [W1] [W2] [W3] <- 权重存储 Kernels
y=1 [K0] [K1] [K2] [K3] <- 计算 Kernels
W = weights.kk (权重加载/广播)
K = kkA, kkB... (MAC 计算内核)
在 conv2d_w5 中可以看到显式的 Tile 分配:
location<kernel>(dut.kkA) = tile(18,1); // 计算内核 A
location<kernel>(dut.weights[0].kk) = tile(18,0); // 对应权重加载
核心抽象三:分层测试金字塔
┌─────────────┐
│ mnist │ 完整网络 (集成测试)
│ (7 layers) │
└──────┬──────┘
┌───────────────┼───────────────┐
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│conv2d_w1│ │conv2d_w3│ │conv2d_w5│ 卷积层 (单元测试)
│ (1x1) │ │ (3x3) │ │ (5x5) │
└─────────┘ └─────────┘ └─────────┘
┌─────────┐ ┌─────────┐
│max_pool │ │max_pool │ 池化层 (单元测试)
│ _w2 │ │ _w4 │
└─────────┘ └─────────┘
┌─────────────┐
│ dense_w7 │ 全连接层 (单元测试)
│ (10 outputs)│
└─────────────┘
架构详解与数据流
架构概览
1x1卷积] C3[conv2d_w3
3x3卷积] C5[conv2d_w5
5x5卷积] end subgraph "池化层图变体 (Max Pooling Layer Graph Variants)" P2[max_pooling2d_w2
2x2池化] P4[max_pooling2d_w4
4x4池化] end subgraph "全连接层图 (Dense Classification Layer Graph)" D7[dense_w7
全连接层] end subgraph "完整MNIST网络图 (Full MNIST ConvNet Graph)" MNIST[mnist
7层完整网络] end C1 & C3 & C5 & P2 & P4 & D7 -.->|组件组装| MNIST
典型数据流:以 conv2d_w3 为例
┌─────────────────────────────────────────────────────────────────────┐
│ conv2d_w3_app.cpp 数据流 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ data/ifm_i.txt data/wts_i.txt │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ │
│ │ PLIO_i │ │ PLIO_w │ PLIO = 64-bit 接口 │
│ │ (input) │ │ (input) │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ └───────────┬───────────┘ │
│ ▼ │
│ ┌───────────────────┐ │
│ │ conv2d_w3_graph │ 实际的卷积计算子图 │
│ │ (MAC阵列 + 累加) │ │
│ └─────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ PLIO_o │ 输出结果 │
│ │ (output) │ (ofm_o.txt) │
│ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
完整 MNIST 网络数据流
输入特征图
4 images/batch] end subgraph "Layer 1: Conv2d_1x1" C1[conv2d_w1
1x1 Conv] end subgraph "Layer 2: MaxPool 2x2" P2[max_pooling2d_w2
2x2 Pool] end subgraph "Layer 3: Conv2d_3x3" C3[conv2d_w3
3x3 Conv] end subgraph "Layer 4: MaxPool 2x2" P4[max_pooling2d_w2
2x2 Pool] end subgraph "Layer 5: Conv2d_5x5" C5[conv2d_w5
5x5 Conv
4 parallel kernels] end subgraph "Layer 6: MaxPool 4x4" P6[max_pooling2d_w4
4x4 Pool] end subgraph "Layer 7: Dense" D7[dense_w7
Fully Connected
10 outputs] end subgraph "输出" OFM[OFM
分类结果
10-class logits] end IFM --> C1 --> P2 --> C3 --> P4 --> C5 --> P6 --> D7 --> OFM
关键设计决策与权衡
决策一:独立验证层 vs 端到端开发
| 方案 | 优点 | 缺点 | 本模块选择 |
|---|---|---|---|
| 端到端开发 | 开发速度快,快速看到结果 | 调试困难,问题定位慢 | ❌ |
| 分层独立验证 | 可逐层验证正确性,便于定位问题 | 需要更多测试基础设施 | ✅ |
权衡分析:虽然分层验证增加了测试代码量,但对于硬件加速器开发而言,调试周期极长(每次综合、布局布线可能需要数小时)。通过确保每个子图在集成前都经过验证,可以将集成阶段的调试时间从"天"缩短到"小时"。
决策二:显式 Tile 分配 vs 自动布局
// 显式 Tile 分配示例(来自 mnist_app.cpp)
location<kernel>(dut.layer_w1.kk) = tile(18,1);
location<kernel>(dut.layer_w1.weights.kk) = tile(18,0);
| 方案 | 优点 | 缺点 | 使用场景 |
|---|---|---|---|
| 自动布局 | 开发简单,快速迭代 | 可能产生次优结果,资源冲突难调试 | 原型阶段 |
| 显式分配 | 精确控制资源,可预测性能 | 需要理解硬件架构,工作量大 | 生产部署 |
权衡分析:本模块选择显式分配,因为 MNIST 网络虽然规模不大,但作为教学示例和基准测试,需要展示最佳的资源利用方式。显式分配允许开发者:
- 将计算内核和权重存储放在相邻的 tiles(减少延迟)
- 精确控制 bank 分配(避免冲突)
- 为不同的并行度(如 conv2d_w5 的 4 路并行)分配独立的 tiles
决策三:文件 I/O 测试 vs 内存映射测试
所有子图都使用 PLIO 从文本文件读取测试数据:
ifm_i = input_plio::create("PLIO_i", plio_64_bits, "data/ifm_i.txt");
| 方案 | 优点 | 缺点 | 本模块选择 |
|---|---|---|---|
| 文件 I/O | 易于生成和修改测试向量,便于离线分析 | 需要文件系统支持,I/O 延迟高 | ✅ |
| 内存映射 | 低延迟,高性能,适合生产部署 | 测试向量生成复杂,调试困难 | 部分使用 |
权衡分析:作为设计教程和验证环境,文件 I/O 方案提供了最大的灵活性。开发者可以轻松:
- 使用 Python/MATLAB 生成测试向量
- 对比硬件输出与软件参考模型的差异
- 在仿真环境中快速迭代(无需重新综合硬件)
代码结构解析
独立层测试的统一模式
所有独立层测试(conv2d_w1/w3/w5、max_pooling_w2/w4、dense_w7)遵循相同的代码结构:
// 1. 包含子图定义头文件
#include "xxx_graph.h"
// 2. 定义 dut_graph 类继承自 graph
class dut_graph : public graph {
public:
xxx_graph dut; // 实际的计算子图
input_plio ifm_i; // 输入特征图 PLIO
input_plio wts_i; // 权重 PLIO (如需要)
output_plio ofm_o; // 输出特征图 PLIO
dut_graph(void) {
// 3. 创建 PLIO 接口,绑定到数据文件
ifm_i = input_plio::create("PLIO_i", plio_64_bits, "data/ifm_i.txt");
// ...
// 4. 连接 PLIO 到子图端口
connect<>(ifm_i.out[0], dut.ifm_i);
// ...
}
};
// 5. 实例化并运行
dut_graph aie_dut;
int main(void) {
aie_dut.init();
aie_dut.run(NUM_ITER);
aie_dut.end();
return 0;
}
完整 MNIST 网络的复杂性
与独立层相比,mnist_app.cpp 展示了完整网络的复杂度:
// 输入接口:1 个特征图 + 7 个权重输入
input_plio ifm_i;
input_plio wts1_i, wts3_i, wts7_i;
std::array<input_plio,4> wts5_i; // conv2d_w5 需要 4 个权重输入
output_plio ofm_o;
mnist_graph dut;
// 显式的 Tile 分配(约 60 行 location 约束)
location<kernel>(dut.layer_w1.kk) = tile(18,1);
// ... 为每个 kernel、buffer、stack 分配具体位置
conv2d_w5 的特殊性:多核并行
conv2d_w5 展示了更复杂的并行模式:
class dut_graph : public graph {
public:
conv2d_w5_graph dut;
input_plio ifm_i;
std::array<input_plio,4> wts_i; // 4 个权重输入
output_plio ofm_o;
dut_graph(void) {
// 连接 4 个权重输入到并行 kernel
connect<>(wts_i[0].out[0], dut.wts_i[0]);
connect<>(wts_i[1].out[0], dut.wts_i[1]);
connect<>(wts_i[2].out[0], dut.wts_i[2]);
connect<>(wts_i[3].out[0], dut.wts_i[3]);
// 显式分配 tiles: 4 个计算核 + 4 个权重加载核
location<kernel>(dut.kkA) = tile(18,1);
location<kernel>(dut.kkB) = tile(19,1);
location<kernel>(dut.kkC) = tile(20,1);
location<kernel>(dut.kkD) = tile(21,1);
location<kernel>(dut.weights[0].kk) = tile(18,0);
location<kernel>(dut.weights[1].kk) = tile(19,0);
location<kernel>(dut.weights[2].kk) = tile(20,0);
location<kernel>(dut.weights[3].kk) = tile(21,0);
}
};
依赖关系与模块交互
模块内层级关系
mnist_convnet_layer_and_full_network_graphs
│
├── 卷积层图变体 (convolution_layer_graph_variants)
│ ├── conv2d_w1 ──► 1x1 卷积 (pointwise)
│ ├── conv2d_w3 ──► 3x3 卷积 (标准卷积)
│ └── conv2d_w5 ──► 5x5 卷积 (4路并行)
│
├── 池化层图变体 (max_pooling_layer_graph_variants)
│ ├── max_pooling2d_w2 ──► 2x2 池化
│ └── max_pooling2d_w4 ──► 4x4 池化
│
├── 全连接层图 (dense_classification_layer_graph)
│ └── dense_w7 ──► 全连接层 (7->10 outputs)
│
└── 完整 MNIST 网络图 (full_mnist_convnet_graph)
└── mnist ──► 7层完整网络 (组装所有上述层)
跨模块依赖
完整网络] CONV1[conv2d_w1_app.cpp] CONV3[conv2d_w3_app.cpp] CONV5[conv2d_w5_app.cpp] POOL2[max_pooling2d_w2_app.cpp] POOL4[max_pooling2d_w4_app.cpp] DENSE[dense_w7_app.cpp] end subgraph "依赖的头文件 (子图定义)" H1[conv2d_w1_graph.h] H3[conv2d_w3_graph.h] H5[conv2d_w5_graph.h] HP2[max_pooling2d_w2_graph.h] HP4[max_pooling2d_w4_graph.h] HD[dense_w7_graph.h] HM[mnist_graph.h] HW[wts_init_graph.h] HN[num_iter.h] end subgraph "外部依赖" GRAPH_LIB[graph.h
AIE 图基础库] PLIO_LIB[plio.h
PLIO 接口库] MAKE[Vitis Platform
Makefile.graph] end CONV1 --> H1 CONV3 --> H3 CONV5 --> H5 POOL2 --> HP2 POOL4 --> HP4 DENSE --> HD MNIST_M --> HM MNIST_M --> HW MNIST_M --> HN H1 & H3 & H5 & HP2 & HP4 & HD & HM --> GRAPH_LIB H1 & H3 & H5 & HP2 & HP4 & HD & HM --> PLIO_LIB MAKE -.->|外部构建依赖| MNIST_M
设计权衡与工程决策
权衡一:显式 Tile 分配 vs 编译器自动布局
代码证据(来自 mnist_app.cpp):
// 约 60 行的显式 location 约束
location<kernel>(dut.layer_w1.kk) = tile(18,1);
location<stack>(dut.layer_w1.kk) = bank(18,0,1);
location<buffer>(dut.layer_w1.kk.in[0]) = { bank(18,0,2), bank(18,0,3) };
// ... 每个 kernel 都有类似约束
决策理由:
- ✅ 确定性:每次编译得到相同的硬件布局
- ✅ 性能可控:手工优化关键路径的 tile 距离
- ✅ 资源冲突可见:明确知道哪些 tiles/banks 被占用
- ❌ 维护成本:修改网络结构时需要重新调整分配
何时使用自动布局:快速原型验证功能正确性时。
权衡二:文件 I/O 测试 vs 内存 DMA 传输
代码证据:
// 所有子图使用文件 I/O
ifm_i = input_plio::create("PLIO_i", plio_64_bits, "data/ifm_i.txt");
ofm_o = output_plio::create("PLIO_o", plio_64_bits, "data/ofm_o.txt");
决策理由:
- ✅ 测试向量和预期结果可用版本控制
- ✅ 可用标准工具(Python/MATLAB)生成和对比
- ✅ 仿真环境支持(无需实际硬件 DMA)
- ❌ 不适用于生产部署(延迟高)
生产环境替代:使用 mm2s/s2mm DMA 内核进行内存映射传输。
权衡三:多核并行 (conv2d_w5) vs 单核串行
代码证据(conv2d_w5_app.cpp):
// 4 路并行内核
std::array<input_plio,4> wts_i;
connect<>(wts_i[0].out[0], dut.wts_i[0]);
// ... 连接 4 个权重输入
// 显式 4 个计算 tile
location<kernel>(dut.kkA) = tile(18,1);
location<kernel>(dut.kkB) = tile(19,1);
location<kernel>(dut.kkC) = tile(20,1);
location<kernel>(dut.kkD) = tile(21,1);
决策理由:
- ✅ 5x5 卷积计算量大,单核无法实时处理
- ✅ 4 路并行匹配 AIE-ML 的 SIMD 宽度
- ✅ 显式控制避免资源冲突
- ❌ 增加了代码复杂度和调试难度
新贡献者须知
快速上手检查清单
-
理解 AIE-ML 基础
- 阅读 AIE_ML_Design_Graphs 了解图编程模型
- 理解 tile、kernel、buffer、stream 概念
-
搭建验证环境
# 典型的验证流程 cd aie/conv2d_w3 make run # 编译、仿真、对比输出 -
修改现有层
- 复制现有的
conv2d_w3_app.cpp作为模板 - 修改子图头文件引用
- 调整 PLIO 连接和 location 约束
- 复制现有的
常见陷阱与避坑指南
陷阱 1:Location 约束不匹配
// 错误示例:kernel 和权重 tile 距离过远
location<kernel>(dut.kk) = tile(18,1);
location<kernel>(dut.weights.kk) = tile(25,0); // 距离太远!
// 正确做法:相邻 tiles 减少通信延迟
location<kernel>(dut.kk) = tile(18,1);
location<kernel>(dut.weights.kk) = tile(18,0); // 同列相邻
陷阱 2:Buffer Bank 分配冲突
// 危险:同一 bank 被多个 buffer 使用
location<buffer>(dut.kk.in[0]) = bank(18,0,0);
location<buffer>(dut.kk.in[1]) = bank(18,0,0); // 冲突!
// 安全:分散到不同 banks
location<buffer>(dut.kk.in[0]) = bank(18,0,0);
location<buffer>(dut.kk.in[1]) = bank(18,0,1); // 不同 bank
陷阱 3:PLIO 位宽与数据格式不匹配
// 64-bit PLIO 期望的数据格式:
// 每个 64-bit 字包含 2 个 int32 或 4 个 int16 或 8 个 int8
// 如果 text 文件数据格式错误,会导致解析失败
plio_64_bits, "data/ifm_i.txt" // 确保文件格式正确!
陷阱 4:Tile 坐标越界
// AIE-ML 阵列有固定的行列数(如 50x8)
location<kernel>(dut.kk) = tile(100, 10); // 错误:可能越界!
// 应查阅具体器件的数据手册
location<kernel>(dut.kk) = tile(18, 1); // 在有效范围内
调试技巧
- 从简单开始:先让
max_pooling2d_w2运行成功(无权重输入,最简单) - 对比参考输出:每个测试用例都有
data/ofm_o_ref.txt参考输出 - 检查 connect 语句:确保所有端口都有且仅有一条连接
- 验证 location 约束:确保没有资源冲突(同一 bank 被多个 buffer 使用)
子模块索引
本模块包含以下子模块,每个都有独立文档:
| 子模块 | 描述 | 复杂度 |
|---|---|---|
| conv2d_w1 | 1x1 卷积层 (Pointwise Conv) | 低 |
| conv2d_w3 | 3x3 卷积层 (Standard Conv) | 中 |
| conv2d_w5 | 5x5 卷积层 (4路并行) | 高 |
| max_pooling2d_w2 | 2x2 最大池化 | 低 |
| max_pooling2d_w4 | 4x4 最大池化 | 低 |
| dense_w7 | 全连接层 | 中 |
| mnist | 完整 7 层网络 | 极高 |
相关模块
- AIE_ML_Design_Graphs - AIE-ML 设计图的总体概览
- lenet_ml_system_dma_integration - 另一个 CNN 参考设计
- AIE_ML_PL_HLS_Integration - PL/AIE 集成方法
总结
mnist_convnet_layer_and_full_network_graphs 模块展示了如何在 AIE-ML 上系统性地实现和验证神经网络。其核心价值在于:
- 可验证性:每个层都可独立测试,确保集成前正确性
- 可组合性:子图作为组件可灵活组装成不同网络结构
- 可控性:显式资源分配允许精细优化性能和资源利用率
- 教学性:从简单到复杂,逐步展示 AIE-ML 图编程的最佳实践
对于新加入团队的开发者,建议按照这个模块展示的路径学习:先理解单个层的实现,再研究层间的组合模式,最后掌握完整网络的优化技巧。