端侧部署流水线:从 PyTorch 到手机推理
ncnn 架构见 精读: ncnn
MNN 架构见 精读: mnn(用户正在学习)
YOLO-Master 部署难点见 跨赛道: YOLO 到 端侧
检测框架部署横评见 目标检测部署链
调研元信息
| 项 |
值 |
| 日期 |
2026-06-22 |
| 环境 |
macOS / Apple Silicon |
| 实测状态 |
未实测完整链路,以文档 + 源码为准 |
| 流水线段 |
全链路覆盖:Train, Export, Deploy |
一句话定位
本文描述端到端部署流水线——PyTorch 模型如何经过导出、转换、量化最终跑在手机上——以及每一步的常见失败模式和引擎选型决策。
日常类比
把模型部署比作「把一本中文小说翻译成盲文并印刷在口袋书上」:
- 导出(ONNX)= 先翻译成英文(通用中间语言)
- 转换(PNNX/MNNConvert)= 再翻译成盲文(引擎专用格式)
- 量化(INT8)= 缩印成口袋版(精度换体积)
- 推理(ncnn/MNN)= 盲人读者实际阅读(设备上执行)
每步都可能「翻译失真」:复杂句式翻不了(动态分支)、缩印太小看不清(量化精度损失)。
全链路总览
flowchart LR
subgraph Train[训练阶段]
T1[PyTorch Model] --> T2[TorchScript]
end
subgraph Export[导出阶段]
T2 --> E1[ONNX]
T2 --> E2[PNNX 直通]
E1 --> E3[onnx-simplifier]
end
subgraph Convert[转换阶段]
E3 --> C1[onnx2ncnn]
E2 --> C2[ncnn param+bin]
E3 --> C3[MNNConvert]
C1 --> C2
C3 --> C4[MNN mnn文件]
end
subgraph Quantize[量化阶段]
C2 --> Q1[ncnn2table PTQ]
C4 --> Q2[MNN量化工具]
Q1 --> Q3[ncnn INT8]
Q2 --> Q4[MNN INT8]
end
subgraph Deploy[部署阶段]
Q3 --> D1[ncnn推理]
Q4 --> D2[MNN推理]
end
阶段一:导出(Export)
路径对比
| 路径 |
工具 |
优点 |
缺点 |
| PyTorch -> ONNX |
torch.onnx.export |
生态最广,几乎所有引擎都支持 |
静态图,动态分支需特殊处理 |
| PyTorch -> PNNX |
pnnx 工具 |
ncnn 专属直通车,保留更多语义 |
仅 ncnn 可用 |
| Ultralytics one-liner |
yolo export format=ncnn |
全自动,零配置 |
仅 Ultralytics 模型 |
Ultralytics 一键导出 [已有]
# 标准 YOLO 模型,一条命令搞定
yolo export model=yolov8n.pt format=ncnn
yolo export model=yolov8n.pt format=onnx
yolo export model=yolov8n.pt format=coreml
支持 format: onnx, ncnn, coreml, tflite, openvino, torchscript, engine(TensorRT)。
PNNX 手动导出 [源码]
# Step 1: 导出 TorchScript
import torch
model = MyModel()
x = torch.randn(1, 3, 640, 640)
mod = torch.jit.trace(model, x)
mod.save("model.pt")
# Step 2: PNNX 转换
pnnx model.pt inputshape=[1,3,640,640]
# 产出: model.ncnn.param, model.ncnn.bin, model_pnnx.py 等 7 文件
导出阶段失败模式
| 失败模式 |
原因 |
解法 |
| 动态 shape 不支持 |
ONNX opset 低于模型需求 |
升级 opset(推荐 13+) |
| 条件分支丢失 |
torch.onnx 不支持 if/else |
用 torch.where 重写,或 PNNX |
| 自定义算子缺失 |
ONNX 无对应标准 op |
注册自定义 op 或拆分子图 |
| MoE 路由导出失败 |
动态 top-k + 条件激活 |
见跨赛道文档的 fallback 方案 |
阶段二:转换(Convert)
ncnn 转换链
| 工具 |
输入 |
输出 |
适用场景 |
| onnx2ncnn |
ONNX |
.param + .bin |
通用 ONNX 模型 |
| PNNX |
TorchScript |
.param + .bin + .py |
PyTorch 模型直通 |
| caffe2ncnn |
Caffe |
.param + .bin |
遗留 Caffe 模型 |
PNNX 优势:保留 PyTorch 算子语义,支持 F.silu / F.layer_norm 等 ONNX 无法精确表达的操作。
MNN 转换链
MNNConvert -f ONNX --modelFile model.onnx --MNNModel model.mnn --bizCode myapp
# 或从 TFLite/Caffe/TorchScript 转换
转换阶段失败模式
| 失败模式 |
原因 |
解法 |
| 算子不支持 |
ncnn/MNN 未实现某 ONNX op |
拆分为已支持 op 组合,或自定义 Layer |
| Shape 推断失败 |
动态维度无法静态分析 |
固定 batch/输入尺寸,或用 PNNX |
| 精度偏差 |
FP32 转换存在数值差异 |
对比逐层输出,定位有问题的 Layer |
| PNNX 静默失败 |
反常 float 常量 (issue 6614) |
检查模型中是否有 denormalized float |
阶段三:量化(Quantize)
ncnn PTQ 量化 [文档]
# Step 1: 准备校准图片列表
find calibration_images/ -name "*.jpg" > imagelist.txt
# Step 2: 生成量化表
ncnn2table model.param model.bin imagelist.txt model.table \
mean=[104,117,123] norm=[0.017,0.017,0.017] shape=[224,224,3] \
pixel=BGR thread=4 method=kl
# Step 3: 量化模型
ncnn2int8 model.param model.bin model_int8.param model_int8.bin model.table
MNN 量化 [文档]
# MNN 的量化集成在 MNNConvert 中
MNNConvert -f ONNX --modelFile model.onnx --MNNModel model_quant.mnn \
--bizCode myapp \
--weightQuantBits 8 \
--compressionParamsFile quant_config.json
ncnn vs MNN 量化对照
| 维度 |
ncnn |
MNN |
| 方法 |
PTQ(ncnn2table) |
PTQ + QAT 均支持 |
| 校准算法 |
KL 散度 / ACIQ / EQ |
KL + 学习步长 |
| 混合精度 |
手动指定 layer |
自动敏感度分析 |
| INT4 |
不支持 |
实验性支持 |
| 工具链集成 |
独立工具 |
集成在 MNNConvert 中 |
量化阶段失败模式
| 失败模式 |
原因 |
解法 |
| 精度暴跌 |
校准数据分布与实际不匹配 |
扩大校准集 + 混合精度 |
| 特定层溢出 |
激活值范围极端 |
该层保留 FP16/FP32 |
| 通道间差异大 |
权重分布不均匀 |
per-channel 量化 |
| MoE 路由精度敏感 |
路由决策依赖 softmax 精度 |
路由层保留 FP32 |
阶段四:部署推理(Deploy)
ncnn 推理代码骨架(C++)
#include "net.h"
ncnn::Net net;
net.opt.use_vulkan_compute = true; // GPU 加速
net.load_param("model.param");
net.load_model("model.bin");
ncnn::Mat in = ncnn::Mat::from_pixels(img, ncnn::Mat::PIXEL_BGR, w, h);
in.substract_mean_normalize(mean, norm);
ncnn::Extractor ex = net.create_extractor();
ex.input("input", in);
ncnn::Mat out;
ex.extract("output", out); // 按需递归,未用到的层不执行
MNN 推理代码骨架(C++)
#include <MNN/Interpreter.hpp>
auto net = MNN::Interpreter::createFromFile("model.mnn");
auto session = net->createSession(config);
auto input = net->getSessionInput(session, nullptr);
// ... 填充输入
net->runSession(session);
auto output = net->getSessionOutput(session, nullptr);
部署阶段引擎选型
| 维度 |
ncnn |
MNN |
ORT Mobile |
| 体积 |
极小(约1MB) |
中(约5MB) |
大(约10MB+) |
| GPU 加速 |
Vulkan(跨平台) |
OpenCL+Metal |
NNAPI/CoreML |
| NPU |
不支持 |
部分支持 |
NNAPI 代理 |
| LLM |
不支持 |
内建支持 |
不支持 |
| 学习曲线 |
中(C++ 为主) |
陡(代码量大) |
平(标准 API) |
| 社区生态 |
最大(23K star) |
次之(9K star) |
微软维护 |
端到端示例:YOLOv8 到手机
sequenceDiagram
participant Dev as 开发者
participant UL as Ultralytics
participant PNNX as PNNX工具
participant ncnn as ncnn引擎
participant Phone as 手机
Dev->>UL: yolo export model=yolov8n.pt format=ncnn
UL->>PNNX: 自动调用 PNNX
PNNX->>ncnn: 生成 yolov8n.ncnn.param + .bin
Dev->>ncnn: ncnn2table 量化(可选)
Dev->>Phone: 集成 libncnn + 模型文件
Phone->>Phone: ncnn::Net 加载 + Vulkan 推理
Ultralytics 的 yolo export format=ncnn 内部自动完成 TorchScript -> PNNX -> ncnn 全链路。
失败模式总结表
| 阶段 |
常见问题 |
根因 |
challenges.md 对应 |
| Export |
MoE 条件分支导出 |
动态控制流 |
目标检测 #8 |
| Convert |
算子不支持 |
格式碎片化 |
移动推理 #9 |
| Quantize |
精度暴跌 |
校准分布偏差 |
移动推理 #8 |
| Deploy |
Vulkan 设备兼容 |
GPU 碎片化 |
移动推理 #7 |
| Deploy |
内存峰值 OOM |
峰值控制缺失 |
移动推理 #5 |
| 全链路 |
动态 shape |
静态优化假设 |
移动推理 #6 |
证据等级
| 标签 |
来源 |
| [源码] |
/tmp/ncnn tools/pnnx/, /tmp/MNN tools/converter/ |
| [已有] |
deep-dive-ncnn.md, deep-dive-mnn.md, challenges.md (两赛道) |
| [文档] |
ncnn wiki (量化/编译), Ultralytics export docs, MNN docs |
| [Issue] |
ncnn #6614 (PNNX 静默失败), YOLO-Master #42 (ONNX 动态导出) |