🏠

MNIST ConvNet 卷积层图变体 (Convolution Layer Graph Variants)

一句话概括

这是一个用于 AMD Versal AIE-ML 架构的卷积神经网络层硬件抽象层,它通过参数化的图(Graph)定义,将不同尺寸的 2D 卷积运算(1x1、3x3、5x5)映射到 AI 引擎阵列上——本质上是在解决"如何把深度学习中的卷积操作翻译成 AI 引擎能高效执行的并行数据流"这个问题。


问题空间:为什么需要这个模块?

想象你正在设计一个手写数字识别系统(MNIST),需要在 FPGA 的 AI 引擎上运行卷积神经网络。你面临的核心挑战是:

软件层面的卷积是直观的

# PyTorch 风格的伪代码
output = conv2d(input, weights, kernel_size=3, stride=1, padding=0)

但硬件层面的实现是复杂的

  • AI 引擎是VLIW 处理器阵列,每个引擎有特定的 SIMD 能力和内存限制
  • 权重和特征图需要在引擎之间流式传输,涉及复杂的 DMA 配置和数据重排
  • 不同卷积核尺寸(1x1 vs 3x3 vs 5x5)需要完全不同的并行化策略

未被选择的替代方案

方案 为什么没选 本模块的选择
纯 HLS 高层次综合 无法充分利用 AIE 的 SIMD 和专用 MAC 单元 使用 AIE 原生图编程模型
固定硬件加速器 IP 缺乏灵活性,无法适配不同网络结构 参数化图定义,可组合扩展
运行时软件调度 延迟不可预测,无法满足实时推理要求 编译时静态图编排,确定性执行

核心抽象:心智模型

把这个模块想象成一个**"卷积运算的装配流水线工厂"**:

┌─────────────────────────────────────────────────────────────┐
│                    dut_graph (测试顶层图)                     │
│  ┌──────────┐    ┌─────────────────┐    ┌──────────┐        │
│  │  IFM PLIO │───→│  conv2d_wX_graph │───→│ OFM PLIO │        │
│  │ (输入)    │    │  (卷积计算核心)   │    │ (输出)    │        │
│  └──────────┘    └─────────────────┘    └──────────┘        │
│         ↑                                       ↓           │
│  ┌──────────┐                            ┌──────────┐       │
│  │ WTS PLIO  │ (权重输入,1x1/3x3单端口,5x5四端口) │       │
│  └──────────┘                            └──────────┘       │
└─────────────────────────────────────────────────────────────┘

三个关键抽象层次

  1. PLIO (Programmable Logic I/O):FPGA 可编程逻辑与 AIE 阵列之间的"高速公路收费站"

    • 负责 DMA 数据传输的配置
    • 64-bit 位宽 (plio_64_bits) 是权衡了吞吐量和资源消耗的选择
  2. conv2d_wX_graph:特定卷积核尺寸的"生产车间"

    • w1:1x1 卷积,主要用于通道混合(pointwise convolution)
    • w3:3x3 卷积,经典的特征提取核
    • w5:5x5 卷积,更大的感受野,需要更复杂的并行化
  3. Kernel 位置绑定(仅 w5):显式的 AIE tile 分配

    • 在 5x5 卷积中,计算被拆分到 4 个 AIE 引擎(kkA-kkD)
    • 权重加载器也被分配到独立的 tile(weights[0..3])
    • 这是性能优化的关键:计算与数据加载并行化

架构详解与数据流

flowchart TB subgraph "Host/PL Domain" H[Host Application
main.cpp] DMA_IFM["DMA Source
IFM Data"] DMA_WTS["DMA Source
Weights Data"] DMA_OFM["DMA Sink
OFM Results"] end subgraph "AIE Array - dut_graph" PLIO_I[input_plio
PLIO_i] PLIO_W[input_plio
PLIO_w / PLIO_w0-3] PLIO_O[output_plio
PLIO_o] subgraph "conv2d_wX_graph" K_CONV[Convolution Kernels] K_WTS[Weight Loader Kernels
w5 only] end end H -->|init/run/end| dut DMA_IFM -->|AXI Stream| PLIO_I DMA_WTS -->|AXI Stream| PLIO_W PLIO_O -->|AXI Stream| DMA_OFM PLIO_I -->|stream| K_CONV PLIO_W -->|stream| K_WTS K_WTS -->|broadcast| K_CONV K_CONV -->|stream| PLIO_O

端到端数据流追踪

以一次推理迭代为例(处理 4 张 MNIST 图像):

  1. 初始化阶段 (aie_dut.init())

    • 权重通过 PLIO 端口预加载到 AIE 本地内存
    • 对于 w5,4 组权重分别加载到 4 个独立 tile,实现并行访问
  2. 执行阶段 (aie_dut.run(1))

    • 输入特征图(IFM)从 DDR 经 DMA → PLIO → AIE 阵列
    • AIE 内核执行乘加运算(MAC),利用 8-way SIMD 加速
    • 输出特征图(OFM)经反向路径写回 DDR
  3. 清理阶段 (aie_dut.end())

    • 刷新流水线,确保所有数据写出完成

三种卷积变体的设计差异

conv2d_w1 (1x1 卷积)

// 简化的连接模式
connect<>(wts_i.out[0], dut.wts_i);   // 单权重端口
connect<>(ifm_i.out[0], dut.ifm_i);
connect<>(dut.ofm_o,    ofm_o.in[0]);

设计特点

  • 最简单的数据流:每个输出像素只依赖对应位置的输入和权重
  • 单权重输入端口足够(权重数量少)
  • 无需显式 kernel 位置绑定,编译器自动分配

适用场景:通道降维/升维(如 MobileNet 的 pointwise 层)

conv2d_w3 (3x3 卷积)

// 与 w1 相同的连接模式
connect<>(wts_i.out[0],  dut.wts_i);
connect<>(ifm_i.out[0],  dut.ifm_i);
connect<>(dut.ofm_o,     ofm_o.in[0]);

设计特点

  • 虽然核尺寸增大,但仍使用单权重端口
  • 内部通过行缓冲(line buffer)机制处理 3x3 滑动窗口
  • 数据复用发生在 kernel 内部,而非图级别

适用场景:标准特征提取层(如 ResNet 的基础卷积)

conv2d_w5 (5x5 卷积) —— 最复杂的设计

// 多权重端口:权重分片存储
std::array<input_plio,4> wts_i;
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]);

// 显式 kernel 位置绑定
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);

为什么 w5 需要特殊处理?

方面 w1/w3 w5
权重数量 9 (3x3) 25 (5x5)
单 tile 内存压力 可接受 可能溢出
计算并行度 单 kernel 足够 需要 4-way 并行
权重加载带宽 单端口满足 需要 4 端口并行加载

Tile 布局策略

    Column 18    Column 19    Column 20    Column 21
Row 0 [W_loader_A] [W_loader_B] [W_loader_C] [W_loader_D]
Row 1 [Compute_A ] [Compute_B ] [Compute_C ] [Compute_D ]

这种垂直相邻布局的优势:

  • 权重 loader 与计算 kernel 在同一列,局部内存访问延迟最低
  • 行内并行(4 列同时计算),行间流水(权重预加载)

关键设计决策与权衡

1. 显式位置绑定 vs 自动布局

决策:仅在 w5 中使用显式 location<kernel> 绑定

权衡分析

  • 显式绑定优点:可预测的性能,手动优化数据局部性
  • 显式绑定缺点:代码与硬件拓扑耦合,移植性差
  • 选择理由:w5 的计算强度需要精细控制,而 w1/w3 可由编译器处理

2. 单文件测试 vs 模块化复用

观察到的代码结构:

  • 每个卷积尺寸有独立的 conv2d_wX_app.cpp
  • 共享的图定义在对应的 conv2d_wX_graph.h

隐含的设计意图

  • 这些 dut_graph 类是测试桩(test harness),不是生产代码
  • 实际产品应使用 full_mnist_convnet_graph 进行层组合

3. 64-bit PLIO 位宽选择

input_plio::create("PLIO_i", plio_64_bits, "data/ifm_i.txt");

为什么是 64-bit?

  • AIE-ML 支持 32-bit、64-bit、128-bit PLIO
  • 64-bit 是吞吐量与资源消耗的甜点
    • 足以饱和单个 AIE 引擎的 MAC 能力
    • 不会过度消耗宝贵的 PL-AIE 互联资源

4. 权重分离加载模式

w5 中权重与 IFM 使用独立的 PLIO 端口:

input_plio   ifm_i;      // 特征图输入
std::array<input_plio,4> wts_i;  // 权重输入(4 端口)

优势

  • 权重可一次性加载后复用多次(若 batch > 1)
  • IFM 和权重走独立的 DMA 通道,避免争用

代价

  • 更多的 PLIO 端口消耗
  • 更复杂的 host 端数据准备

跨模块依赖关系

flowchart TD subgraph "当前模块" W1[conv2d_w1_app.dut_graph] W3[conv2d_w3_app.dut_graph] W5[conv2d_w5_app.dut_graph] end subgraph "上层模块" FULL[full_mnist_convnet_graph] end subgraph "同层模块" MAX[max_pooling_layer_graph_variants] DENSE[dense_classification_layer_graph] end subgraph "底层基础设施" VITIS[Vitis_Platform_Creation
Makefile.graph] end FULL -.->|组合使用| W1 FULL -.->|组合使用| W3 FULL -.->|组合使用| W5 FULL -.->|组合使用| MAX FULL -.->|组合使用| DENSE W1 -->|依赖| VITIS W3 -->|依赖| VITIS W5 -->|依赖| VITIS

依赖说明

  1. 向上依赖:本模块的图定义被 full_mnist_convnet_graph 组合使用,构成完整网络

  2. 平级协作:与 max_pooling_layer_graph_variantsdense_classification_layer_graph 共同组成 MNIST 网络的层集合

  3. 基础设施依赖

    // 隐式依赖 Vitis 平台创建流程
    // Makefile.graph 提供构建规则和硬件平台配置
    

新贡献者必读:陷阱与注意事项

⚠️ 隐式契约

  1. 数据文件格式

    input_plio::create(..., "data/ifm_i.txt");
    
    • 文本文件中的数值必须是十六进制格式的定点数
    • 每行一个样本,顺序必须符合 AIE 内存布局(通常是 channel-first)
  2. 迭代语义

    aie_dut.run(1);  // 1 iteration = 4 images
    
    • 注释说明 1 次迭代处理 4 张图像
    • 这是由底层 kernel 的 vectorization factor 决定的,修改 kernel 时必须同步更新注释
  3. 权重端口数量

    • w1/w3:单权重端口
    • w5:4 个权重端口
    • host 端代码必须根据卷积尺寸选择正确的数据加载模式

🔧 扩展指南

若要添加新的卷积变体(如 7x7):

  1. 参考 w5 的模式,评估是否需要更多并行度
  2. 计算权重总量,决定 weight loader 的数量
  3. 设计 tile 布局,考虑:
    • 计算 kernel 之间的通信开销
    • 权重 loader 到计算 kernel 的距离
  4. 更新 host 端的 PLIO 连接和数据文件准备逻辑

🐛 常见调试问题

症状 可能原因 排查方向
仿真挂起 PLIO 数据文件格式错误 检查 txt 文件是否包含非法字符
输出全零 权重未正确加载 验证 w5 的 4 个权重文件都存在且非空
结果错位 数据布局不匹配 确认 IFM 是 NCHW 还是 NHWC 格式
性能不达标 Kernel 位置冲突 检查是否有多个 kernel 绑定到同一 tile

子模块文档

本模块包含三个独立的卷积变体实现,各自有详细的子模块文档:

  • conv2d_w1:1x1 点卷积,最简单的数据流
  • conv2d_w3:3x3 标准卷积,行缓冲优化
  • conv2d_w5:5x5 大核卷积,4-way 并行与显式位置绑定

总结

convolution_layer_graph_variants 模块是 MNIST ConvNet 教程中承上启下的关键层:

  • 对下:封装了 AIE 硬件的复杂性,提供声明式的图编程接口
  • 对上:为完整网络组装提供可复用的、经过验证的卷积层构建块
  • 核心价值:展示了如何根据卷积核尺寸的不同,选择不同的并行化策略——从小核的简单流到大型核的显式分布式计算

理解这个模块的关键在于认识到:它不是通用深度学习框架,而是针对 AIE-ML 架构的手工优化硬件映射。每一行代码都反映了在有限硬件资源约束下的深思熟虑。

On this page