边缘 AI 与移动端部署:把模型塞进手机——TFLite 与 Core ML 实战

· 约 6 分钟 ·
本文目录(13 节)

模型压缩那篇讲的是「服务器侧」的优化,这篇讲更极端的场景:手机、摄像头、车机、MCU——没有 GPU 服务器、电量有限、可能断网。端侧 AI 的典型需求:人脸解锁(数据不能上传)、实时美颜(云端延迟不可接受)、离线翻译(飞机上也要用)。

下面以拍照分类串起导出、转换、量化和移动端调用。多次格式转换会增加兼容风险;如果目标运行时能直接支持训练框架的导出格式,应优先比较更短的路径,而不是机械地经过每一个中间格式。

前置阅读:建议先读 模型压缩与部署(量化基础)、CNN 详解。

端侧的四个约束(和服务器完全不同)

约束服务器手机端
算力A100 随便用NPU/DSP 算子支持有限,CPU 兜底
内存几十 GB应用可用几百 MB,模型最好 <50MB
功耗不关心持续推理掉电发热,用户会卸载
网络稳定可能离线——端侧存在的根本理由

约束决定了端侧模型的风格:小模型 + 整数运算。MobileNet(depthwise separable conv)、EfficientNet-Lite、ShuffleNet 这些名字就是为端侧生的。

端侧推理引擎选型

引擎平台特点
TFLiteAndroid 为主,跨平台Google 官方,转换工具链最全,NPU 委托(GPU/NNAPI delegate)
Core MLiOSApple 官方,深度集成 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模型文件服务器下发 + 版本号校验

练习

  1. 把 MobileNetV3 转成 INT8 TFLite,在电脑上先用 tflite-runtime 验证输出与 PyTorch 一致(误差 <2%)。
  2. 写 Android Demo:相册选图 → 推理 → 显示 top-3 类别和耗时,对比 NNAPI 开/关的延迟。
  3. 做一次「量化敏感度」实验:分别量化全模型 vs 只量化卷积层(第一层和输出层保持 FP32),对比精度和延迟。
  4. 设计一个云端协同方案:为「拍照翻译」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 这类工作用神经架构搜索直接搜「这个芯片跑得动的最优模型」。思路同源,数字更狠。

相关阅读

端侧 AI 教会我的最重要一课:约束不是敌人,是最好的架构师。算力有限逼出了 MobileNet,功耗有限逼出了 NPU,隐私有限逼出了整个端侧生态——技术史往往是被约束推着走的。

💬 评论

这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。

正在连接评论服务,首次访问可能需要几秒…