用 MNN 在 CPU 上跑 AI 推理:阿里端侧推理框架上手记
- 为什么要关注 MNN?
- 从零搭建 MNN 环境
- 模型转换:从 PyTorch 到 .mnn
- 用 Python 写推理代码
- 实测延迟:0.033ms 一次推理
- MNN 在 AI 部署管线里的位置
- 和 ONNX Runtime 的简单对比
- 踩坑记录
- 总结
- 系列文章
这是 AI 实操第 4 天。第 2 天导出了 ONNX,今天用阿里开源的 MNN 在纯 CPU 上跑端侧推理,并验证转换后精度不掉。 本文首发地址 https://h89.cn/archives/670.html 项目地址 https://gitee.com/chenjim/cockpit-ai-from-zero
为什么要关注 MNN?
训练好一个 PyTorch 模型,只是万里长征走了一半。真正头疼的是怎么把它塞进手机 App、IoT 设备或者边缘盒子。这些设备没有 GPU,没有 CUDA,内存也紧巴巴的——你不可能把 PyTorch 整个运行时搬上去,光依赖库就几百兆,手机根本吃不消。
这就轮到端侧推理框架出场了。它们做的事情很简单:把训练好的模型压缩、优化,然后在没有 GPU 的普通 CPU 上快速算出结果。阿里开源的 MNN(Mobile Neural Network)就是其中的佼佼者,专为手机和嵌入式设备设计,支持 x86、ARM、ARM64,甚至 RISC-V。
MNN 的核心理念可以用八个字概括:训练靠云端,推理在端侧。模型在 GPU 集群上训练好,经过转换压缩,部署到用户设备上跑推理。数据不用出设备,延迟也低到几乎无感,还能保护用户隐私——一举多得。
和 Google 的 TFLite、腾讯的 NCNN 相比,MNN 的特点是性能调得比较极致,支持的后端多(CPU、GPU、NPU),而且对 ARM 架构做了深度优化。阿里自己在手机淘宝、优酷、钉钉等几十款 App 里都在用,生产环境验证过的。
从零搭建 MNN 环境
MNN 不像 PyTorch 那样 pip install 就完事——它需要从源码编译。好处是你可以按需裁剪,只编译你要的部分,不会装一堆用不上的东西。
编译主要产出两样东西:
MNNConvert:模型转换工具,把 ONNX 或 TensorFlow 模型转成.mnn格式pymnn:Python 推理接口,编译后pip install装上就能在 Python 里调用
编译过程走一遍 CMake 三板斧(以下路径假设你在 day04-M-mnn 目录下执行):
git clone --depth 1 https://github.com/alibaba/MNN.git MNN
bash MNN/schema/generate.sh
cmake -B MNN/pymnn_build \
-DMNN_BUILD_CONVERTER=ON \
-DMNN_BUILD_SHARED_LIBS=OFF \
-DMNN_SEP_BUILD=OFF \
-DMNN_USE_SSE=ON \
-DCMAKE_BUILD_TYPE=Release .
make -C MNN/pymnn_build MNN MNNConvertDeps MNNConvert -j8
pip install MNN/pymnn/pip_package/
MNN_BUILD_CONVERTER=ON 是编译模型转换器,MNN_USE_SSE=ON 打开 x86 的 SSE 指令加速。这两个选项加上 Python 接口,足够完成本模块的 ONNX→MNN 转换和推理任务。不需要训练、音频或 OpenCV 模块时,不要额外打开,否则编译时间会变长。编译大概几分钟到十几分钟。
装好后验证一下:
import MNN
print(MNN.__version__)
看到输出版本号就说明成了,可以开始用了。
模型转换:从 PyTorch 到 .mnn
MNN 不直接吃 PyTorch 模型,它吃的是 ONNX 这个中间格式。ONNX 是微软和 Facebook 联合推出的 AI 模型交换标准,就像一个万能转接头——PyTorch 能导出 ONNX,MNN 也能读取 ONNX。所以流程是:
PyTorch → ONNX → MNNConvert → .mnn
转换用命令行工具,一行搞定:
/path/to/MNNConvert \
-f ONNX \
--modelFile mnist_cnn.onnx \
--MNNModel mnist_cnn.mnn \
--bizCode biz
-f ONNX 指定输入格式,--modelFile 是 ONNX 文件的路径,--MNNModel 是输出路径,--bizCode 是个业务标识,随便填就行。
这一步做完,你会得到一个 .mnn 文件。这个文件通常比原始 ONNX 小一些,原因主要有三:一是做了算子融合,把多个计算步骤合并成一个;二是做了计算图优化(如常量折叠),去掉了运行时才计算的冗余节点;三是做了元数据裁剪,去掉了 ONNX 中不适合端侧的冗余信息。注意默认转换不做权重量化,权重仍然是 FP32;如果需要进一步缩小体积,可以额外开启 INT8 量化。
用 Python 写推理代码
MNN 的 Python SDK 叫 pymnn,核心概念是 Interpreter(加载模型)、Session(运行推理)和 Tensor(存输入输出数据)。
代码跑起来分四步:先加载 .mnn 模型,创建输入变量,然后生成随机输入做验证,接着跑 benchmark 测延迟,最后用 MNIST 测试集前 100 张真实图片验证准确率。完整推理脚本见 infer_mnn.py,转换脚本见 convert.sh。
import MNN
import MNN.expr as expr
import MNN.nn as nn
import numpy as np
import torchvision
import torchvision.transforms as transforms
# 第一步:加载 .mnn 模型(固定 4 线程)
module = nn.load_module_from_file(
"mnist_cnn.mnn", ["input"], ["output"], thread_num=4
)
# 第二步:创建输入变量,指定 NCHW 布局
input_var = expr.placeholder([1, 1, 28, 28], expr.NCHW)
# 第三步:随机输入验证单次推理
input_data = np.random.randn(1, 1, 28, 28).astype(np.float32)
input_var.write(input_data)
output_var = module.forward(input_var)
output = output_var.read()
print(f"预测类别: {output.argmax()}")
# 第四步:真实 MNIST 图片验证准确率
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,)),
])
test_dataset = torchvision.datasets.MNIST(
root="../data", train=False, download=True, transform=transform
)
correct = 0
for i in range(100):
image, label = test_dataset[i]
input_var.write(image.unsqueeze(0).numpy())
pred = module.forward(input_var).read().argmax()
if pred == label:
correct += 1
print(f"100 张真实图片准确率: {correct / 100:.2%}")
nn.load_module_from_file(..., thread_num=4) 把推理线程固定为 4 个。MNN 默认会使用全部 CPU 核心,但手机端通常只给模型分配固定核心;固定线程后,延迟数据更稳定,也更接近端侧真实场景。expr.placeholder 创建输入变量,write 把 numpy 数据填进去,forward 执行推理,read 把结果读回 numpy。这套 Module API(Express/VARP) 是 MNN 推荐的高阶接口,内部会自动处理数据布局转换,不容易踩坑。
实测延迟:0.033ms 一次推理
空跑没意思,来点硬数据。我用的是 MNIST 手写数字识别模型——两层卷积加全连接,参数不到 30 万,推理一张 28x28 的灰度图。
在本地 CPU 上固定 4 线程跑了 100 次推理,用 time.perf_counter() 精确计时(纳秒级精度),结果如下:
| 指标 | 延迟 |
|---|---|
| 平均 (avg) | 0.034 ms |
| 中位数 (P50) | 0.033 ms |
| 99 分位 (P99) | 0.059 ms |
| 最快 (min) | 0.030 ms |
| 最慢 (max) | 0.059 ms |
算一下吞吐量:1 秒除以 0.033 毫秒,约等于 30,000 次推理每秒。注意这是在普通 CPU 上跑出来的,不是 GPU。
真实数据验证也通过了:MNIST 测试集前 100 张图片,MNN 的 Module API 准确率是 99%(即 99/100 张正确),和 day01 全量 10,000 张测试的 99.12% 基准一致。这说明模型转换没有引入精度损失,推理结果可信。
MNN 快的原因:算子是 C++ 手写的,x86 和 ARM 都有汇编级调优;内存预分配好反复用,避免动态申请的开销;计算图做了常量折叠和算子融合,省了解析图的时间。
MNN 在 AI 部署管线里的位置
把整个流程拉通来看,MNN 只是 AI 部署管线中的最后一环:
| 阶段 | 做什么 | 工具 |
|---|---|---|
| 训练 | GPU 集群上设计训练模型 | PyTorch / TensorFlow |
| 导出 | 转成标准中间格式 ONNX | torch.onnx.export() |
| 转换 | 优化并转成端侧专用格式 | MNNConvert / TFLite / NCNN |
| 部署 | 集成到手机 App 或边缘设备 | MNN / TFLite / CoreML |
| 推理 | 在用户设备上跑模型算结果 | MNN 的 CPU/GPU/NPU 后端 |
这五层各司其职:PyTorch 管训练不管部署,ONNX 是格式桥梁不管性能,MNN 只管端侧运行不管训练。这种分层设计让开发者可以专注模型结构,部署的事交给专业的推理引擎去操心。
和 ONNX Runtime 的简单对比
如果你用过 ONNX Runtime,可能会好奇:ONNX Runtime 也能在 CPU 上跑推理,为什么还要多此一举转成 MNN?
区别在于定位不同。ONNX Runtime 的目标是"什么硬件都能跑",它支持 GPU、CUDA、TensorRT、OpenVINO 等十几种后端,适合服务器场景。但也正因为要兼容这么多后端,它的二进制体积比较大,不太适合塞进手机 App。
MNN 反过来,目标就是"手机上跑得最快"。它只关心 Arm 和 x86 CPU、以及手机上的 GPU 和 NPU,不做多余的抽象。所以 MNN 的库体积更小(核心库几百 KB),启动速度更快,在手机 CPU 上的性能通常优于 ONNX Runtime。
服务器上用 ONNX Runtime,手机上用 MNN,各取所长。
踩坑记录
编译前记得跑 schema/generate.sh 生成 flatbuffers 头文件,漏了会编译失败。改了 CMake 参数后最好删掉 pymnn_build 目录重来,缓存偶尔会混淆。pip install 必须在 MNN 源码下的 pymnn/pip_package/ 里执行。
模型转换后一定要用真实数据验证准确率,不能只看延迟。MNN 官方 FAQ 也把"模型转换后与其他框架结果不一致"列为热点问题,推荐用 tools/script/ 下的 testMNNFromOnnx.py 等脚本做一致性验证。本次实操中,如果直接使用 PyMNN 的 Interpreter-Session API(input_tensor.fromNumpy() / output_tensor.getNumpyData()),准确率只有 14%;改用推荐的 Module API(MNN.nn.load_module_from_file + MNN.expr.placeholder) 后,准确率恢复到 99%,与 ONNX 一致。原因可能是 PyMNN 的 Interpreter-Session 接口在数据布局(如内部 NC4HW4 与 NCHW 转换)上与本机环境存在兼容问题。因此建议优先使用 Module API。
另外 getNumpyData() 返回的是 MNN 内部内存的直接映射,不复制数据。想修改的话记得调 .copy(),否则报内存错误。
总结
MNN 让我印象最深的是两点:一是编译系统设计得清晰,CMake 选项分门别类,想裁剪什么功能一目了然,不是那种又大又全的"框架全家桶";二是推理接口简洁,和 numpy 无缝衔接,写起来就像在写普通的 Python 数据处理代码,对做算法的人很友好。
端侧推理是 AI 落地绕不开的一关。MNN、TFLite、NCNN、CoreML 这些框架虽然背后的团队不同,但设计思路高度一致:训练框架只管训练,中间格式做桥梁,推理引擎专门为端侧硬件深度优化。这层分工让不同领域的工程师各司其职,算法工程师不用操心部署细节,客户端工程师也不用理解模型训练的复杂性。
从 PyTorch 到 ONNX 再到 MNN,每一步都在缩小"模型能跑"和"模型好用"之间的距离。
第 5 天会换到 NVIDIA 的 TensorRT,把 ONNX 编译成 GPU 专用的 Engine,看看更激进的优化能带来多少加速。
系列文章
本系列「车载端侧 AI 工程化从零上手」共 10 篇,建议按序阅读:
- 零基础用 PyTorch 识别手写数字:MNIST 实战入门
- 把 PyTorch 模型变成 ONNX:导出、验证和可视化一次学会
- MLflow 实验追踪:从入门到上手
- 用 MNN 在 CPU 上跑 AI 推理:阿里端侧推理框架上手记
- 小白用 TensorRT 给模型加速:ONNX 转 Engine 踩坑实录
- 零基础上手:高通 SNPE 模型转换实战(从 ONNX 到 DLC)
- 零基础搞懂 SNPE 模型量化:INT8 精度损失 = 0% 的秘密
- Python 工程化重构:从 print 脚本到 pytest 项目
- 车载语音指令识别:从 44% 到 96% 的调优之路
- 车载语音指令识别:SNPE 转换与真机部署实测记录
本文链接:用 MNN 在 CPU 上跑 AI 推理:阿里端侧推理框架上手记 - https://h89.cn/archives/670.html
版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。