模型压缩那篇讲的是「服务器侧」的优化,这篇讲更极端的场景:手机、摄像头、车机、MCU——没有 GPU 服务器、电量有限、可能断网。端侧 AI 的典型需求:人脸解锁(数据不能上传)、实时美颜(云端延迟不可接受)、离线翻译(飞机上也要用)。
下面以拍照分类串起导出、转换、量化和移动端调用。多次格式转换会增加兼容风险;如果目标运行时能直接支持训练框架的导出格式,应优先比较更短的路径,而不是机械地经过每一个中间格式。
前置阅读:建议先读 模型压缩与部署(量化基础)、CNN 详解。
端侧的四个约束(和服务器完全不同)
| 约束 | 服务器 | 手机端 |
|---|---|---|
| 算力 | A100 随便用 | NPU/DSP 算子支持有限,CPU 兜底 |
| 内存 | 几十 GB | 应用可用几百 MB,模型最好 <50MB |
| 功耗 | 不关心 | 持续推理掉电发热,用户会卸载 |
| 网络 | 稳定 | 可能离线——端侧存在的根本理由 |
约束决定了端侧模型的风格:小模型 + 整数运算。MobileNet(depthwise separable conv)、EfficientNet-Lite、ShuffleNet 这些名字就是为端侧生的。
端侧推理引擎选型
| 引擎 | 平台 | 特点 |
|---|---|---|
| TFLite | Android 为主,跨平台 | Google 官方,转换工具链最全,NPU 委托(GPU/NNAPI delegate) |
| Core ML | iOS | Apple 官方,深度集成 ANE(Apple Neural Engine),能耗比最优 |
| NCNN | 移动端跨平台 | 腾讯开源,无第三方依赖,ARM 优化极致,国内 App 常用 |
| MNN | 移动端跨平台 | 阿里开源,异构调度强 |
| ONNX Runtime Mobile | 跨平台 | 模型格式统一时的选择,体积裁剪后可接受 |
双端项目现实:Android 用 TFLite/NCNN,iOS 用 Core ML——两个转换管道都要维护。好消息是转换输入都可以是 ONNX,训练侧不用分家。
全流程实战:PyTorch → Android
第一步:训练时就想好部署
最大的坑是「训练时随手写的层,转换时不支持」。纪律:只用目标引擎算子白名单里的层。TFLite 兼容列表里,GridSample、某些索引操作、自定义 CUDA 层都是雷。替换表常备:F.interpolate(bilinear 可以,bicubic 老版本不行)、避免 in-place 操作的歧义、reshape 别用 -1 跨 batch 维。
第二步:导出与转换
# PyTorch → ONNX(opset 别追新,13 兼容性最好)
torch.onnx.export(model, dummy, "model.onnx", opset_version=13,
input_names=["input"], output_names=["output"])
# ONNX → TFLite(用 onnx-tf 桥,或 PyTorch 直转走 ai_edge_torch)
import onnx
from onnx_tf.backend import prepare
tf_rep = prepare(onnx.load("model.onnx"))
tf_rep.export_graph("model_tf")
# 然后 TFLiteConverter 转 .tflite(略,标准流程)
第三步:量化(端侧必做)
import tensorflow as tf
converter = tf.lite.TFLiteConverter.from_saved_model("model_tf")
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_data_gen # 校准集 200 张
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8 # 全整数量化(NPU 要求)
converter.inference_output_type = tf.int8
tflite_model = converter.convert()
open("model_int8.tflite", "wb").write(tflite_model)
注意全整数量化(输入输出也 int8)和「仅权重 int8、激活 float」的区别:前者才能跑满 NPU/DSP,后者只是省体积。代价是输入要处理量化参数(scale/zero_point)——Android 侧图像预处理得跟着改。
第四步:Android 集成核心代码
val interpreter = Interpreter(loadModelFile(context, "model_int8.tflite"),
Interpreter.Options().apply {
addDelegate(NnApiDelegate()) // 委托给 NPU/DSP,失败自动回退 CPU
numThreads = 4
})
// 输入:Bitmap → int8 量化张量
val input = preprocess(bitmap, quantScale, quantZeroPoint)
val output = Array(1) { ByteArray(1001) }
interpreter.run(input, output)
val topIdx = output[0].indices.maxBy { output[0][it] }
iOS 侧对应物:coremltools 转换(ct.convert(model, compute_units=ct.ComputeUnit.ALL) 自动调度 ANE),Swift 里 Vision 框架三行跑起来,比 Android 省心一档。
性能比较:哪些变化值得记录
仅凭模型名称无法给出手机端延迟。没有配套设备、模型文件与运行日志时,不应填入看似精确的实测值。可以先按下面的比较维度准备验证:
| 对照 | 需要记录 | 结果怎样解释 |
|---|---|---|
| 浮点与 INT8 | 文件体积、任务指标、端到端延迟 | 体积减少不代表延迟同比减少 |
| CPU 与硬件委托 | 支持算子、设备切换、预热后延迟 | 部分回退可能抵消加速收益 |
| 单次与持续推理 | 温度、功耗、长时间延迟变化 | 冷机的快结果不能代表持续使用 |
Android 与 iOS 的模型格式和后端不同,应分别记录部署产物,不能把同一行精度直接套用到两个平台。
云端协同:不是二选一,是分层
把请求送往云端之前,需要定义离线行为:继续使用端侧低精度结果、提示稍后重试,还是暂停功能?回退策略不能悄悄上传原本承诺留在设备上的数据。端云协同首先是产品与数据边界的约定,其次才是推理引擎选择。
成熟产品的架构通常是混合:
端侧小模型(快、省、隐私)
├── 置信度高 → 直接返回(占 70%+ 请求)
└── 置信度低 → 上传云端大模型复核
其他协同模式:端侧做前置(人脸检测)云端做后置(识别);端侧常驻 + 模型 OTA 热更新(TFLite 模型可以从服务器下发,不用发版)。我在 vLLM 调优里做的云端优化和端侧是同一枚硬币的两面——把推理放在离用户最近且算得动的地方。
踩坑排查
| 现象 | 原因 | 解法 |
|---|---|---|
| 转换报「op not supported」 | 用了冷门层 | 训练时就用白名单算子;或自定义算子(贵) |
| 量化后精度暴跌 | 校准集不具代表性 | 用真实分布的 300~500 张校准 |
| NPU 上比 CPU 还慢 | 模型含 NPU 不支持算子,来回切换设备 | 看 delegate 日志,让全图落在单一设备 |
| App 集成后输出全错 | 预处理不一致(归一化/RGB vs BGR/量化参数) | 逐像素对齐 Python 与端侧预处理 |
| 持续推理手机发烫降频 | 热设计没考虑 | 降帧率推理;批处理合并;NPU 优先 |
| 模型更新要发版 | 模型打进 assets | 模型文件服务器下发 + 版本号校验 |
练习
- 把 MobileNetV3 转成 INT8 TFLite,在电脑上先用 tflite-runtime 验证输出与 PyTorch 一致(误差 <2%)。
- 写 Android Demo:相册选图 → 推理 → 显示 top-3 类别和耗时,对比 NNAPI 开/关的延迟。
- 做一次「量化敏感度」实验:分别量化全模型 vs 只量化卷积层(第一层和输出层保持 FP32),对比精度和延迟。
- 设计一个云端协同方案:为「拍照翻译」App 划分端侧和云端的职责,画出请求分流逻辑。
面试常问
Q:端侧推理和云推理的取舍维度? 延迟(端侧无网络往返)、隐私(数据不出设备)、成本(云端按调用计费 vs 端侧零边际成本)、可靠性(离线可用)、能力上限(端侧模型小、精度天花板低)、更新灵活性(云端随时更新,端侧需下发)。纯端适合:高频低延迟 + 隐私敏感 + 任务固定;纯云适合:大模型 + 频繁迭代 + 任务复杂;其余混合。
Q:Depthwise Separable Convolution 为什么适合移动端? 标准卷积计算量 H·W·Cin·Cout·K²;深度可分离拆成 depthwise(每通道独立卷积 H·W·Cin·K²)+ pointwise(1×1 组合通道 H·W·Cin·Cout),计算量降为约 1/K² + 1/Cout(K=3 时约省 8~9 倍)。参数量同步锐减。MobileNet 全家桶的核心。代价:表达能力略降,靠加宽或堆层补。
Q:量化在 NPU 上为什么特别重要? NPU/DSP 的 INT8 MAC 阵列密度远高于 FP32 单元——同面积 INT8 算力是 FP32 的 4~8 倍;且整数运算功耗低、内存带宽需求降 4 倍。浮点模型在 NPU 上要么跑不动要么回退 CPU/GPU。端侧量化不是优化,是入场券。
Q:模型 OTA 热更新要注意什么? 版本管理(模型版本与应用版本兼容矩阵)、完整性校验(签名/哈希防篡改)、灰度下发(先 5% 设备观察指标)、回滚机制、包体积(差分更新)。本质是把 MLOps 的发版纪律搬到端上。
Q:MCU(单片机)上跑模型呢? TinyML 领域:模型压到几百 KB 级(CMSIS-NN / TFLite Micro),关键词唤醒、手势识别是典型场景。约束再极端一档:内存 KB 级、无操作系统、全整数。MCUNet 这类工作用神经架构搜索直接搜「这个芯片跑得动的最优模型」。思路同源,数字更狠。
相关阅读
- 模型压缩与部署实战——量化蒸馏的服务器侧对照
- CNN 详解——MobileNet 改造的对象
- OpenCV 入门——端侧图像预处理工具
- vLLM 部署 Qwen 调优实录——云端推理的优化
- 人脸识别实战——端侧 AI 最经典的应用
端侧 AI 教会我的最重要一课:约束不是敌人,是最好的架构师。算力有限逼出了 MobileNet,功耗有限逼出了 NPU,隐私有限逼出了整个端侧生态——技术史往往是被约束推着走的。
💬 评论
这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。