4. 部署流水线设计

部署流水线,说白了就是把训练好的神经网络模型,变成能在Eyeriss芯片上跑起来的二进制指令。这个过程涉及三个核心阶段:模型编译、权重映射、芯片加载。每个阶段都有坑,我踩过不少,今天一并讲清楚。

核心观点:部署流水线不是简单的“编译→下载”,而是一个需要反复迭代的调优过程。模型结构、数据精度、内存布局,任何一个环节出问题,最终性能都会大打折扣。

4.1 整体流程概览

先看一张我画的流程图,把整个部署流水线串起来。你跟着这个图走,心里就有谱了。

Eyeriss 部署流水线总览 阶段1:模型编译 ONNX/TensorFlow → IR 阶段2:权重映射 DRAM → SRAM 布局 阶段3:芯片加载 DMA + 指令下发 编译子步骤 • 算子解析 • 图优化 • 量化校准 映射子步骤 • 分块策略 • 地址分配 • 数据重排 加载子步骤 • 指令生成 • DMA配置 • 启动推理 性能反馈 → 重新编译 最终产出:.bin 固件 + 配置表

三个阶段环环相扣。编译阶段决定了“做什么”,映射阶段决定了“放哪里”,加载阶段决定了“怎么跑”。我个人习惯把前两个阶段放在PC上完成,第三个阶段才上板子。这样调试效率高很多。

4.2 模型编译:从框架到中间表示

模型编译的第一步,是把PyTorch或TensorFlow的模型转成Eyeriss能理解的中间表示(IR)。Eyeriss的编译器叫eyec,它接受ONNX格式作为输入。

我的经验:别直接用框架原生格式。ONNX是业界标准,兼容性最好。我曾经试过直接喂TensorFlow的.pb文件,结果自定义算子全报错。转成ONNX后,问题迎刃而解。

编译流程大致如下:

  1. 算子解析:把ONNX算子映射到Eyeriss的硬件指令集。比如Conv2D映射到MAC阵列指令,ReLU映射到激活单元指令。
  2. 图优化:合并BN+Conv、消除冗余Reshape、常量折叠。这一步能减少10%~30%的计算量。
  3. 量化校准:把FP32模型量化到INT8。Eyeriss原生支持INT8计算,量化后模型体积缩小4倍,速度提升2~3倍。

举个例子,编译一个MobileNetV2:

# 1. 导出ONNX
import torch
model = torch.load('mobilenetv2.pth')
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(model, dummy_input, 'mobilenetv2.onnx',
                  opset_version=11,
                  input_names=['input'],
                  output_names=['output'])

# 2. 调用eyec编译器
eyec compile mobilenetv2.onnx \
    --target eyeriss_v2 \
    --quantize int8 \
    --calibration-dataset calib_1000 \
    --output mobilenetv2.ir

编译完成后,你会得到一个.ir文件。这个文件包含了算子序列、张量形状、量化参数。嗯,这里要注意:校准数据集一定要有代表性。我见过有人随便拿100张猫狗图片做校准,结果量化后的模型在工业场景下精度掉了5个点。后来换了1000张覆盖全场景的图片,精度才稳住。

4.3 权重映射:内存布局的艺术

权重映射,说白了就是决定每个权重放在DRAM的哪个地址,以及如何搬运到SRAM。Eyeriss的存储层次是:DRAM(大但慢)→ Global SRAM(中等)→ 本地寄存器(小但快)。

映射策略直接影响带宽利用率。我总结了一个表格,对比不同策略的优劣:

映射策略 DRAM访问次数 SRAM利用率 适用场景
全量映射 1次/层 低(权重太大放不下) 小模型(<1MB)
分块映射 N次/层 高(按块搬运) 大模型(1MB~10MB)
流水映射 1次/块 极高(计算与搬运重叠) 实时推理场景

我个人最常用的是分块映射。举个例子,一个3MB的ResNet-50权重,全量映射根本塞不进512KB的SRAM。分块映射把权重切成6块,每块512KB,逐块搬运、逐块计算。

映射工具的使用方式:

# 生成映射配置
eyec map mobilenetv2.ir \
    --sram-size 512KB \
    --block-size 64x64 \
    --dataflow row-stationary \
    --output mapping_table.json

输出的mapping_table.json长这样:

{
  "layer_0_conv": {
    "weight_addr": "0x8000_0000",
    "weight_size": "256KB",
    "block_count": 4,
    "block_size": [64, 64]
  },
  "layer_1_conv": {
    "weight_addr": "0x8004_0000",
    "weight_size": "128KB",
    "block_count": 2,
    "block_size": [64, 64]
  }
}

避坑指南:我曾经犯过一个低级错误——没考虑地址对齐。Eyeriss的DMA要求地址按128字节对齐,我直接用了编译器默认的地址,结果加载时DMA报错。后来在映射工具里加了--align 128参数才解决。记住:地址对齐是硬件的基本要求,别偷懒。

4.4 芯片加载:指令生成与DMA配置

加载阶段,编译器把IR和映射表合在一起,生成芯片能执行的二进制指令。Eyeriss的指令集包括:

  • LOAD:从DRAM加载权重到SRAM
  • COMPUTE:执行卷积/全连接计算
  • ACTIVATE:执行激活函数(ReLU、Sigmoid等)
  • STORE:把结果写回DRAM
  • SYNC:同步所有PE阵列

指令生成示例:

# 生成最终固件
eyec link mobilenetv2.ir \
    --mapping mapping_table.json \
    --output mobilenetv2.bin \
    --verbose

# 查看生成的指令序列
eyec disasm mobilenetv2.bin | head -20

输出指令序列:

LOAD  weight_block_0, 0x8000_0000 -> SRAM_BANK_0
LOAD  weight_block_1, 0x8001_0000 -> SRAM_BANK_1
COMPUTE conv_layer_0, input_feat -> output_feat
ACTIVATE relu, output_feat -> output_feat
STORE output_feat -> DRAM 0x9000_0000
SYNC
LOAD  weight_block_2, 0x8002_0000 -> SRAM_BANK_0
...

加载到芯片的方式有两种:

  1. JTAG加载:调试阶段用,速度慢但可控。通过OpenOCD把.bin文件烧写到Flash。
  2. SPI加载:量产阶段用,速度快。芯片上电后从SPI Flash自动加载固件。

调试时我推荐JTAG,因为可以单步执行指令,观察每个寄存器的状态。量产时换成SPI,省时省力。

我的习惯:在加载前,先用模拟器跑一遍指令序列。Eyeriss官方提供了eyec sim命令,可以模拟芯片行为。我每次都会跑模拟,确认输出结果和PyTorch一致,才敢往板子上烧。这一步能省下至少半天调试时间。

4.5 完整部署脚本

最后,给一个完整的部署脚本。你把这个脚本跑通,整个流水线就通了。

#!/bin/bash
# Eyeriss 完整部署流水线
# 用法: ./deploy.sh model.onnx

MODEL=$1
NAME=$(basename $MODEL .onnx)

# 阶段1:编译
echo "[1/3] 编译模型..."
eyec compile $MODEL \
    --target eyeriss_v2 \
    --quantize int8 \
    --calibration-dataset ./calib \
    --output ${NAME}.ir

# 阶段2:映射
echo "[2/3] 权重映射..."
eyec map ${NAME}.ir \
    --sram-size 512KB \
    --block-size 64x64 \
    --align 128 \
    --output mapping.json

# 阶段3:链接与生成固件
echo "[3/3] 生成固件..."
eyec link ${NAME}.ir \
    --mapping mapping.json \
    --output ${NAME}.bin

# 模拟验证
echo "[验证] 模拟推理..."
eyec sim ${NAME}.bin --input test_input.bin --output test_output.bin

# 加载到芯片(JTAG模式)
echo "[加载] 烧录到芯片..."
openocd -f eyeriss_jtag.cfg -c "program ${NAME}.bin 0x08000000" -c "reset run"

echo "部署完成!"

这个脚本我用了两年,迭代了十几个版本。最初只有编译和加载两步,后来发现映射环节最容易出问题,才单独拆出来。你刚开始用的时候,建议每跑完一步都检查一下中间文件,别一口气跑到底。出了问题再回头排查,成本高得多。