4. 部署流水线设计
部署流水线,说白了就是把训练好的神经网络模型,变成能在Eyeriss芯片上跑起来的二进制指令。这个过程涉及三个核心阶段:模型编译、权重映射、芯片加载。每个阶段都有坑,我踩过不少,今天一并讲清楚。
核心观点:部署流水线不是简单的“编译→下载”,而是一个需要反复迭代的调优过程。模型结构、数据精度、内存布局,任何一个环节出问题,最终性能都会大打折扣。
4.1 整体流程概览
先看一张我画的流程图,把整个部署流水线串起来。你跟着这个图走,心里就有谱了。
三个阶段环环相扣。编译阶段决定了“做什么”,映射阶段决定了“放哪里”,加载阶段决定了“怎么跑”。我个人习惯把前两个阶段放在PC上完成,第三个阶段才上板子。这样调试效率高很多。
4.2 模型编译:从框架到中间表示
模型编译的第一步,是把PyTorch或TensorFlow的模型转成Eyeriss能理解的中间表示(IR)。Eyeriss的编译器叫eyec,它接受ONNX格式作为输入。
我的经验:别直接用框架原生格式。ONNX是业界标准,兼容性最好。我曾经试过直接喂TensorFlow的.pb文件,结果自定义算子全报错。转成ONNX后,问题迎刃而解。
编译流程大致如下:
- 算子解析:把ONNX算子映射到Eyeriss的硬件指令集。比如Conv2D映射到MAC阵列指令,ReLU映射到激活单元指令。
- 图优化:合并BN+Conv、消除冗余Reshape、常量折叠。这一步能减少10%~30%的计算量。
- 量化校准:把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
...
加载到芯片的方式有两种:
- JTAG加载:调试阶段用,速度慢但可控。通过OpenOCD把.bin文件烧写到Flash。
- 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 "部署完成!"
这个脚本我用了两年,迭代了十几个版本。最初只有编译和加载两步,后来发现映射环节最容易出问题,才单独拆出来。你刚开始用的时候,建议每跑完一步都检查一下中间文件,别一口气跑到底。出了问题再回头排查,成本高得多。