🏠

MNIST ConvNet 层与完整网络图 (mnist_convnet_layer_and_full_network_graphs)

一句话概括

本模块实现了完整的 MNIST 手写数字识别卷积神经网络在 AMD Versal AI Engine-ML (AIE-ML) 上的硬件映射,采用分层验证、逐层组装的设计哲学——每个网络层(卷积、池化、全连接)都可独立测试验证,最终通过图组合机制构建完整推理流水线。


问题背景与设计动机

我们要解决什么问题?

在嵌入式 AI 加速器上部署神经网络时,开发者面临三个核心挑战:

  1. 调试复杂性:完整网络包含数十个算子,任何一个内核的错误都会导致整个系统失效,定位问题如同大海捞针。
  2. 资源映射不确定性:AIE-ML 的 tiles 数量有限,如何合理分配计算、缓冲区和权重存储是个 NP-hard 问题。
  3. 性能可移植性:不同网络结构(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)│
           └─────────────┘

架构详解与数据流

架构概览

flowchart TB subgraph "卷积层图变体 (Convolution Layer Graph Variants)" C1[conv2d_w1
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 网络数据流

flowchart LR subgraph "输入" IFM[IFM
输入特征图
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 网络虽然规模不大,但作为教学示例和基准测试,需要展示最佳的资源利用方式。显式分配允许开发者:

  1. 将计算内核和权重存储放在相邻的 tiles(减少延迟)
  2. 精确控制 bank 分配(避免冲突)
  3. 为不同的并行度(如 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层完整网络 (组装所有上述层)

跨模块依赖

flowchart TB subgraph "本模块组件" direction TB MNIST_M[mniat_app.cpp
完整网络] 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 宽度
  • ✅ 显式控制避免资源冲突
  • ❌ 增加了代码复杂度和调试难度

新贡献者须知

快速上手检查清单

  1. 理解 AIE-ML 基础

  2. 搭建验证环境

    # 典型的验证流程
    cd aie/conv2d_w3
    make run  # 编译、仿真、对比输出
    
  3. 修改现有层

    • 复制现有的 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);   // 在有效范围内

调试技巧

  1. 从简单开始:先让 max_pooling2d_w2 运行成功(无权重输入,最简单)
  2. 对比参考输出:每个测试用例都有 data/ofm_o_ref.txt 参考输出
  3. 检查 connect 语句:确保所有端口都有且仅有一条连接
  4. 验证 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 层网络 极高

相关模块


总结

mnist_convnet_layer_and_full_network_graphs 模块展示了如何在 AIE-ML 上系统性地实现和验证神经网络。其核心价值在于:

  1. 可验证性:每个层都可独立测试,确保集成前正确性
  2. 可组合性:子图作为组件可灵活组装成不同网络结构
  3. 可控性:显式资源分配允许精细优化性能和资源利用率
  4. 教学性:从简单到复杂,逐步展示 AIE-ML 图编程的最佳实践

对于新加入团队的开发者,建议按照这个模块展示的路径学习:先理解单个层的实现,再研究层间的组合模式,最后掌握完整网络的优化技巧。

On this page