ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

TensorFlow Lite端侧推理实战:模型转换、量化与委托优化指南

TensorFlow Lite端侧推理实战:模型转换、量化与委托优化指南

1. 从训练到部署:为什么端侧推理是移动AI的最后一公里

做过移动端AI功能的人都有一个共同感受:模型在服务器上跑得好好的,一放到手机上就各种问题。推理速度慢、内存占用高、包体积膨胀、不同芯片表现差异巨大——这些坑几乎每个团队都会踩一遍。而TensorFlow Lite(以下简称TFLite)就是Google给出的端侧推理答案,它专门为移动设备和嵌入式场景设计,核心目标是在有限的算力和内存条件下,让训练好的模型高效跑起来。

我接触TFLite是从一个图像分类的移动端Demo开始的。当时团队用Keras训练了一个轻量级分类网络,准确率达标,但直接往Android上搬的时候发现:原生TensorFlow的so库加上模型文件,光依赖就超过50MB,推理一帧要300多毫秒,低端机上直接卡死。换成TFLite之后,同样的模型经过转换和量化,依赖体积降到几MB,单帧推理压到40毫秒以内,体验完全不一样。这个对比让我意识到,端侧部署不是"把模型拷过去"这么简单,它涉及格式转换、量化压缩、硬件加速委托、内存管理等一系列工程问题。

这篇文章适合谁看?如果你正在做移动端或IoT设备的AI功能,手上有训练好的模型但不知道怎么高效落地,或者你已经用了TFLite但推理性能不理想、想搞清楚量化到底损失了多少精度、GPU委托为什么有时候反而更慢,那这篇内容应该能帮到你。我会从模型转换讲起,一路覆盖量化策略、委托机制、内存优化和实测调优,尽量把每个决策背后的"为什么"说清楚,而不是只丢一堆API让你自己猜。

需要说明的是,TFLite的版本迭代很快,不同版本之间API有差异。我下面提到的操作基于近两年的稳定版本,如果你用的是更早或更新的版本,部分接口名称可能有变化,但核心原理和思路是通用的。

2. 模型转换这一步,决定了后面80%的坑

2.1 从SavedModel到.tflite:转换器的选择逻辑

TFLite的模型格式是.tflite,它是一个FlatBuffer序列化文件,包含了模型结构和权重。你不能直接把Keras的.h5或者TensorFlow的SavedModel丢给移动端,必须先经过转换器处理。转换的核心API是TFLiteConverter,它提供了三种入口:

  • from_saved_model():从SavedModel目录转换,这是最推荐的方式,支持完整的图结构和签名。
  • from_keras_model():直接从Keras模型对象转换,适合快速验证。
  • from_concrete_functions():从具体函数转换,适合需要精细控制输入输出的场景。

我个人的经验是,生产环境一律走SavedModel路线。原因很简单:Keras模型对象在转换时对自定义层、自定义损失函数的支持不够稳定,而且转换过程中如果模型包含动态shape,from_keras_model经常报一些莫名其妙的错误。先用model.save()导出SavedModel,再用from_saved_model转换,路径清晰,出问题也容易定位。

转换的基本代码长这样:

import tensorflow as tf # 加载SavedModel converter = tf.lite.TFLiteConverter.from_saved_model("saved_model_dir") # 设置优化策略(后面详细讲) converter.optimizations = [tf.lite.Optimize.DEFAULT] # 执行转换 tflite_model = converter.convert() # 保存 with open("model.tflite", "wb") as f: f.write(tflite_model)

看起来很简单对吧?但这里有几个容易忽略的点。第一,from_saved_model默认使用模型的默认签名,如果你的SavedModel有多个签名(比如同时有serving和training签名),需要显式指定signature_keys。第二,转换后的模型输入输出名称可能和你训练时的不一样,尤其是经过量化之后,输入输出的dtype会变,这个后面会细说。第三,转换过程中如果遇到不支持的算子,转换器会直接报错,而不是静默降级,所以遇到报错不要慌,先看是哪个op不支持。

2.2 算子兼容性:那些转换器不认的层

TFLite支持的算子集是TensorFlow的一个子集,不是所有TF算子都能转换。常见的坑包括:

  • 自定义层里用了tf.py_function或tf.numpy_function,这类Python回调无法序列化到图中,转换必然失败。
  • 用了tf.raw_ops里的冷门算子,TFLite没有对应实现。
  • 动态控制流(如tf.while_loop配合复杂条件)在某些版本下支持不完整。

遇到不支持的算子,有三条路可走。一是改写模型结构,用TFLite支持的算子组合替代;二是通过converter.target_spec.supported_ops添加SELECT_TF_OPS,让转换器保留部分TF算子,但代价是运行时需要链接额外的TF Lite Flex库,包体积会增大;三是自己实现自定义算子并注册到TFLite运行时,这条路最灵活但工作量最大。

我一般优先选第一条路。举个例子,之前有个模型用了tf.image.extract_patches,TFLite早期版本不支持,我把它改写成用tf.nn.conv2d加特定kernel实现的等价操作,转换就通过了,推理结果也一致。改模型结构虽然麻烦,但换来的是最小的运行时依赖和最好的性能。

提示:转换时加上converter.experimental_new_converter = True(新版本默认开启)可以获得更好的算子覆盖和更优的图优化,如果遇到转换失败,先确认这个开关的状态。

3. 量化不是"压缩一下"那么简单:精度与体积的博弈

3.1 三种量化模式的本质区别

量化是TFLite最核心的优化手段,它把模型权重和激活值从32位浮点降到更低精度,从而减小模型体积、降低内存带宽、加速推理。但量化不是无损的,理解不同模式的原理才能做出正确取舍。

动态范围量化(Dynamic Range Quantization)是最简单的一种,只量化权重到8位整数,激活值在推理时动态量化。它的优点是转换时不需要代表性数据集,一行代码就能开:

converter.optimizations = [tf.lite.Optimize.DEFAULT]

体积能压到原来的1/4左右,推理速度通常有1.5到2倍提升。但它的局限是激活值仍然是浮点计算,在只支持整数运算的硬件(比如某些DSP)上无法发挥全部优势。

全整数量化(Full Integer Quantization)把权重和激活值都量化到8位整数,需要提供代表性数据集来校准激活值的动态范围:

def representative_dataset(): for _ in range(100): yield [np.random.rand(1, 224, 224, 3).astype(np.float32)] converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.representative_dataset = representative_dataset converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] converter.inference_input_type = tf.int8 converter.inference_output_type = tf.int8

全整数量化的模型体积同样是1/4,但在支持INT8加速的硬件上推理速度可以提升2到4倍。代价是精度损失更明显,尤其是对量化敏感的模型(比如检测小目标的网络)。

Float16量化只把权重降到16位浮点,激活值保持浮点:

converter.target_spec.supported_types = [tf.float16]

体积减半,精度损失极小,适合对精度要求高但能接受一定体积增长的场景。不过Float16在CPU上不一定有加速效果,主要收益在GPU委托上。

3.2 代表性数据集怎么选,直接影响量化精度

全整数量化最关键的一步是代表性数据集。它的作用是让转换器观察激活值的实际分布,从而计算出合适的量化参数(scale和zero_point)。如果代表性数据集和真实推理数据分布差异大,量化后的精度会崩得很厉害。

我踩过的一个坑:做一个文本分类模型时,代表性数据集随手用了训练集的前100条,结果量化后准确率掉了8个百分点。后来分析发现,训练集前100条恰好都是某个类别的样本,激活值分布严重偏斜。改成从每个类别均匀采样、覆盖不同长度和内容的样本后,精度损失降到1个百分点以内。

选代表性数据的几个原则:

  • 数量:100到500个样本通常够用,太少校准不准,太多转换时间线性增长。
  • 分布:必须覆盖真实推理时可能遇到的所有输入类型和范围,分类任务要每类都有,检测任务要包含不同尺度和场景。
  • 预处理:代表性数据的预处理必须和推理时完全一致,归一化参数、resize方式都不能差。

注意:代表性数据集不需要标签,只需要输入数据。但输入数据的shape必须和模型输入签名一致,否则转换器会报shape不匹配。

3.3 量化精度损失的实测评估方法

量化后怎么知道精度掉了多少?不能只看转换成功就完事。我的做法是准备一个验证集,分别用原始浮点模型和量化模型跑一遍,对比关键指标。

对于分类模型,看top-1和top-5准确率;对于检测模型,看mAP;对于回归模型,看MAE或MSE。如果量化后指标下降超过可接受阈值(一般分类任务2个百分点以内,检测任务3个百分点以内),就需要调整量化策略。

调整的方向有几个:一是换用Float16量化,精度损失小但体积收益也小;二是对敏感层跳过量化,TFLite支持通过converter.quantized_input_stats或自定义量化配置来精细控制;三是增加代表性数据的覆盖度和数量;四是检查模型本身是否有对量化特别敏感的结构(比如某些归一化层),必要时在训练时加入量化感知训练(QAT)。

QAT是终极方案:在训练阶段就模拟量化误差,让模型学会适应低精度表示。经过QAT的模型,全整数量化后精度损失通常能控制在0.5个百分点以内。代价是训练流程更复杂,需要引入tensorflow_model_optimization工具包,训练时间也会增加。

4. 委托机制:让模型跑在正确的硬件上

4.1 GPU委托的适用边界与常见误区

TFLite支持多种硬件委托(Delegate),包括GPU、NNAPI、Hexagon等。委托的本质是把部分或全部算子交给专用硬件执行,从而获得加速。但委托不是万能的,用错了反而更慢。

GPU委托是最常用的。它把适合并行计算的算子(如卷积、矩阵乘)放到GPU上执行,适合计算密集、数据并行度高的模型。但GPU委托有几个限制:一是数据传输开销,如果模型层数少、每层计算量小,数据在CPU和GPU之间来回拷贝的时间可能超过计算本身;二是GPU委托对算子支持有限,不支持的算子会回退到CPU,导致频繁的上下文切换;三是不同GPU驱动版本行为差异大,某些机型上可能直接崩溃。

我实测过一个轻量级模型(5层卷积),在高端机上GPU委托比CPU快30%,但在中低端机上反而慢20%。原因就是中低端GPU的带宽和驱动优化不足,数据传输成了瓶颈。所以GPU委托不是无脑开,要看模型复杂度和目标设备档次。

开启GPU委托的代码(Android端):

GpuDelegate delegate = new GpuDelegate(); Interpreter.Options options = new Interpreter.Options(); options.addDelegate(delegate); Interpreter interpreter = new Interpreter(modelBuffer, options);

注意GpuDelegate需要在使用完毕后调用close()释放资源,否则会内存泄漏。另外,GPU委托的初始化有一定耗时,如果只是偶尔推理一次,初始化的开销可能不划算,建议在应用启动时初始化并复用。

4.2 NNAPI委托:Android端的硬件加速入口

NNAPI是Android的神经网络API,它把推理请求转发给设备上的专用硬件(NPU、DSP或GPU)。相比GPU委托,NNAPI的优势是能利用更底层的硬件加速,功耗通常更低。但NNAPI的碎片化问题严重:不同芯片厂商的实现质量参差不齐,同一个模型在不同设备上的表现可能天差地别。

使用NNAPI委托:

NnApiDelegate delegate = new NnApiDelegate(); Interpreter.Options options = new Interpreter.Options(); options.addDelegate(delegate);

NNAPI委托有一个setAllowFp16选项,允许在支持FP16的硬件上使用半精度计算,能进一步提速。但要注意,NNAPI对模型结构有要求,动态shape、控制流等特性支持不完整,转换时需要确保模型是静态shape。

我的建议是:NNAPI适合作为可选加速路径,而不是唯一路径。在应用里做设备白名单或运行时探测,如果NNAPI初始化失败或推理结果异常,自动回退到CPU。这样既能利用高端设备的加速能力,又不会在兼容性差的设备上翻车。

4.3 委托选择的决策框架

面对GPU、NNAPI、CPU三条路,怎么选?我总结了一个简单的决策流程:

场景推荐方案理由
模型计算密集、层数多GPU委托并行度高,数据传输开销被摊薄
中低端设备为主CPU(多线程)兼容性最好,无委托初始化开销
有NPU的旗舰设备NNAPI委托功耗低,加速明显
模型含大量不支持算子CPU避免频繁回退导致的上下文切换
需要极低延迟GPU或NNAPI,实测对比不同设备差异大,必须实测

这个表不是绝对的,最终还是要以目标设备上的实测数据为准。我通常会在2到3款代表性设备上跑benchmark,记录推理延迟、内存占用和功耗,再决定默认走哪条路。

5. 内存与线程:那些文档不会告诉你的调优细节

5.1 内存复用与Arena分配策略

TFLite在推理时会为中间张量分配内存。默认情况下,每次推理都会重新分配,这在频繁推理的场景下会造成大量内存碎片和分配开销。TFLite提供了内存复用机制,通过Interpreter.Options的setUseXNNPACK和arena分配策略来优化。

在C++ API中,可以通过tflite::InterpreterBuilder的SetNumThreads和SetMemoryArena来控制。在Java API中,Interpreter构造时可以传入Options,其中setUseXNNPACK(true)会启用XNNPACK后端,它在CPU上对浮点模型有很好的加速效果,尤其是卷积网络。

XNNPACK是Google开发的高性能CPU推理库,TFLite从2.3版本开始集成。实测下来,开启XNNPACK后,浮点模型在CPU上的推理速度通常有1.5到2倍提升,而且精度无损。如果你的模型没有量化,强烈建议开启。

Interpreter.Options options = new Interpreter.Options(); options.setUseXNNPACK(true); options.setNumThreads(4);

线程数的设置也有讲究。setNumThreads不是越大越好,一般设置为设备大核数量(通常是4或6)比较合适。设太多会导致线程调度开销增加,反而变慢。我实测过一个模型,2线程比4线程快,因为模型本身计算量不大,多线程的同步开销占了主导。

5.2 输入输出张量的预处理开销

很多人只关注推理本身的时间,忽略了输入输出的预处理。实际上,图像模型的预处理(resize、归一化、格式转换)可能占到总耗时的30%以上。

TFLite的Interpreter提供了run方法,输入输出都是ByteBuffer或数组。如果你在Java层做图像预处理,再把数据拷进ByteBuffer,这个拷贝过程是有开销的。优化方法有几个:一是用TensorImage和ImageProcessor(来自TFLite Support库),它们内部做了零拷贝优化;二是直接在Native层做预处理,避免Java和Native之间的数据传递;三是如果模型支持,把预处理算子(如resize、归一化)直接编进模型图里,让TFLite一起执行。

把预处理编进模型图是个好思路,但要注意:编进去的算子也必须被TFLite支持,而且量化时这些算子的行为要和推理时一致。我一般只在预处理逻辑简单且固定的场景下这么做。

5.3 多模型共存时的资源竞争

一个应用里同时跑多个TFLite模型时,资源竞争问题会凸显。每个Interpreter实例都会占用内存和线程资源,如果同时初始化多个,可能导致内存峰值过高被系统杀掉。

我的做法是:按需初始化,用完释放。对于不常驻的模型,在需要时创建Interpreter,推理完调用close()。对于常驻模型,控制同时活跃的实例数量,一般不超过2个。另外,多个Interpreter共享线程池时,要注意线程数的总和不超设备核心数,否则会互相抢占。

还有一个容易忽略的点:Interpreter不是线程安全的。如果多个线程共用一个实例,必须加锁,但加锁会串行化推理,失去并发意义。正确做法是每个线程持有独立的Interpreter实例,或者用对象池管理。

6. 实测调优:一个图像分类模型的完整优化链路

6.1 基线测量:先搞清楚瓶颈在哪

优化之前必须先测量。我拿一个MobileNetV2结构的分类模型做例子,输入224x224x3,输出1000类。基线是未量化的浮点模型,在CPU上单线程推理。

测量工具用TFLite的benchmark_model,它可以输出详细的耗时分解:

benchmark_model --graph=model.tflite --num_threads=1 --num_runs=50

基线数据:模型体积14MB,单次推理85ms,内存峰值约30MB。这个数据在中端机上只能算勉强可用,离流畅还有距离。

6.2 逐步优化与每步收益

第一步,开启XNNPACK和4线程:

benchmark_model --graph=model.tflite --num_threads=4 --use_xnnpack=true

推理时间降到28ms,提升约3倍。这一步没有精度损失,是性价比最高的优化。

第二步,动态范围量化:

converter.optimizations = [tf.lite.Optimize.DEFAULT]

模型体积降到3.6MB,推理时间降到22ms。精度在验证集上掉了0.8个百分点,可以接受。

第三步,全整数量化加代表性数据集:

converter.representative_dataset = representative_dataset converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]

模型体积3.5MB(和动态范围差不多),推理时间降到15ms。精度掉了1.5个百分点。这时候如果精度不可接受,可以考虑QAT。

第四步,GPU委托:

options.addDelegate(new GpuDelegate());

在高端机上推理时间降到8ms,但在中端机上反而升到18ms。所以最终策略是:高端机走GPU,中低端机走CPU加INT8。

整个优化链路下来,推理时间从85ms降到15ms(中端机)或8ms(高端机),模型体积从14MB降到3.5MB。这个收益是实实在在的。

6.3 优化过程中的三个反直觉发现

第一个反直觉:量化后模型不一定更快。在某些设备上,INT8推理因为需要额外的反量化操作,反而比浮点慢。这取决于硬件是否原生支持INT8运算。所以量化后一定要在目标设备上实测,不能想当然。

第二个反直觉:线程数不是越多越好。前面提过,2线程可能比4线程快。另外,大核和小核的调度也会影响,有些设备上线程被调度到小核,性能反而下降。可以通过setNumThreads配合CPU亲和性设置来优化,但Android层做这个比较麻烦,一般交给系统调度。

第三个反直觉:模型体积小不等于内存占用低。全整数量化模型体积小,但推理时的中间激活值可能仍然很大,尤其是输入分辨率高的模型。内存峰值主要取决于最大中间张量的大小,而不是权重体积。优化内存要从模型结构入手,比如减少通道数、降低分辨率、使用深度可分离卷积。

7. 跨平台部署的差异与应对

7.1 Android与iOS的API差异

TFLite在Android和iOS上都有官方支持,但API风格不同。Android用Java/Kotlin API,iOS用Objective-C/Swift API。核心概念一致(Interpreter、Delegate、Options),但方法名和参数有差异。

iOS上开启Metal委托(GPU加速):

let options = Interpreter.Options() options.addDelegate(MetalDelegate()) let interpreter = try Interpreter(modelPath: modelPath, options: options)

Android和iOS的委托支持情况也不同。Android有NNAPI,iOS没有对应物;iOS有Core ML委托,可以把TFLite模型转成Core ML格式跑在Neural Engine上,Android没有。所以跨平台应用需要针对每个平台单独做委托策略。

7.2 模型文件的打包与加载

模型文件怎么打进应用包?Android一般放在assets目录,通过AssetManager读取;iOS放在bundle里,通过路径加载。要注意的是,模型文件如果超过一定大小,某些应用商店会有额外要求(比如Android App Bundle的分包机制)。

加载模型时,Android推荐用MappedByteBuffer直接映射文件,避免把整个模型读进堆内存:

AssetFileDescriptor fd = assets.openFd("model.tflite"); FileInputStream is = fd.createInputStream(); FileChannel channel = is.getChannel(); MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, fd.getStartOffset(), fd.getDeclaredLength()); Interpreter interpreter = new Interpreter(buffer);

这种方式加载大模型时内存占用更低,启动也更快。

7.3 版本兼容与回退策略

TFLite的运行时版本和模型转换版本需要匹配。用新版本转换器生成的模型,在旧版本运行时上可能加载失败。所以应用里要处理好版本检测和回退。

我的做法是:在应用启动时尝试加载模型,如果失败,捕获异常并降级到备用方案(比如用更早版本转换的模型,或者关闭某些优化)。同时,在服务端保留模型的多版本,根据客户端上报的运行时版本下发对应模型。这样即使部分用户设备上的TFLite版本较旧,也能正常使用。

8. 我踩过的那些坑与对应的解法

8.1 转换时的shape陷阱

最常遇到的转换错误是shape不匹配。SavedModel的输入签名可能是[None, 224, 224, 3],其中batch维度是动态的。TFLite转换时如果保留动态batch,推理时每次都要重新分配内存,性能受影响。我的做法是在转换前把batch维度固定为1:

converter = tf.lite.TFLiteConverter.from_saved_model("saved_model_dir") converter._experimental_fix_input_shape = True # 部分版本支持

或者更稳妥的方式是在导出SavedModel时就固定shape:

@tf.function(input_signature=[tf.TensorSpec([1, 224, 224, 3], tf.float32)]) def serving_fn(x): return model(x)

固定shape后,转换和推理都更稳定。

8.2 量化后的输出解析错误

全整数量化后,模型的输入输出dtype变成int8,但很多人在推理时仍然按float解析,导致结果完全错误。正确的做法是:先获取输入输出张量的量化参数(scale和zero_point),然后做反量化:

Tensor inputTensor = interpreter.getInputTensor(0); Tensor outputTensor = interpreter.getOutputTensor(0); float inputScale = inputTensor.quantizationParams().getScale(); int inputZeroPoint = inputTensor.quantizationParams().getZeroPoint(); // 浮点输入转int8 int8Input = (int8)(floatInput / inputScale + inputZeroPoint); // int8输出转浮点 floatOutput = (int8Output - outputZeroPoint) * outputScale;

这个转换过程容易出错,建议封装成工具函数,并写单元测试验证。

8.3 委托初始化失败的静默崩溃

GPU委托和NNAPI委托在某些设备上初始化会失败,但失败方式可能是抛异常,也可能是静默返回一个不可用的委托,推理时直接崩溃。我的处理方式是:委托初始化放在try-catch里,并且推理前先用一个小的测试输入跑一遍,确认输出正常后再正式使用。如果测试失败,立即回退到CPU。

try { GpuDelegate delegate = new GpuDelegate(); options.addDelegate(delegate); Interpreter testInterpreter = new Interpreter(modelBuffer, options); // 用测试输入跑一遍 testInterpreter.run(testInput, testOutput); // 验证输出合理性 if (!isOutputValid(testOutput)) { throw new RuntimeException("Delegate output invalid"); } } catch (Exception e) { // 回退到CPU options = new Interpreter.Options(); options.setNumThreads(4); }

这个"先测试再使用"的模式帮我避免了很多线上崩溃。

8.4 模型更新后的缓存问题

应用更新模型文件后,如果用户设备上缓存了旧模型,可能导致新旧模型混用。Android的assets目录在应用更新时会重新解压,但如果你把模型拷贝到了内部存储,就需要手动清理。我的做法是给模型文件加版本号,加载时检查版本,不一致就重新拷贝。

另外,TFLite的Interpreter实例在模型文件更新后不会自动重新加载,必须销毁旧实例重新创建。如果应用支持热更新模型,要确保旧实例被正确释放,否则会内存泄漏。

9. 从工程化角度看TFLite的长期维护

9.1 模型版本管理与灰度发布

生产环境里,模型不是一成不变的。每次模型更新都涉及转换、量化、测试、发布一整套流程。我建议把模型文件当作代码一样管理:用版本号标记,每次更新记录变更内容、精度指标、性能数据。发布时走灰度,先小比例用户验证,确认无异常后再全量。

灰度发布的关键是能回退。服务端保留历史版本模型,客户端上报当前模型版本和推理指标(延迟、失败率),如果某版本异常率超标,自动切回上一版本。

9.2 性能监控与异常上报

线上推理的性能和稳定性需要持续监控。我一般会采集这几个指标:推理延迟的P50和P99、内存峰值、委托使用情况、推理失败率。这些数据上报到服务端,用于分析不同设备、不同模型版本的表现差异。

异常上报要包含足够的信息:设备型号、系统版本、TFLite版本、模型版本、委托类型、异常堆栈。有了这些信息,排查线上问题会快很多。

9.3 持续集成中的模型测试

模型转换和量化应该纳入CI流程。每次模型代码变更后,自动执行转换、量化、精度验证和benchmark,确保新模型在目标指标上不退化。我通常会在CI里跑一个精简的验证集,对比量化前后的精度差异,如果超过阈值就阻断合并。

benchmark也可以在CI里跑,但要注意CI机器的硬件和真实设备差异大,benchmark结果只能作为参考,不能替代真机测试。真机测试还是需要人工或设备农场来完成。


最后分享一个我在实际项目中总结的小技巧:建立自己的模型优化checklist。每次拿到一个新模型,按顺序过一遍:先测基线,再开XNNPACK,再试动态量化,再试全整数,再试委托,每一步都记录数据和精度。这样不会漏掉任何优化点,也方便对比不同模型的优化空间。踩过的坑多了,这个checklist会越来越长,但你的部署效率也会越来越高。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进