部署實(shí)戰(zhàn):從模型轉(zhuǎn)換到性能優(yōu)化)
很多人都會(huì)在生產(chǎn)環(huán)境里碰到同一個(gè)問(wèn)題模型在服務(wù)器上跑得飛快一搬到手機(jī)或者嵌入式設(shè)備上要么體積大到裝上就閃退要么推理一次半天沒(méi)反應(yīng)要么干脆報(bào)錯(cuò)說(shuō)設(shè)備不認(rèn)這個(gè)模型格式。我最早做端側(cè)部署的時(shí)候也是這樣明明訓(xùn)練時(shí)各種指標(biāo)都很好但真正要把模型塞進(jìn)一臺(tái)只有幾百M(fèi)B可用內(nèi)存的設(shè)備里才發(fā)現(xiàn)模型壓根不是為這種環(huán)境設(shè)計(jì)的。TensorFlow Lite集成要做的事說(shuō)白了就是解決這個(gè)中間落差把訓(xùn)練好的模型壓縮、量化、轉(zhuǎn)換再塞進(jìn)手機(jī)、平板或者單片機(jī)的內(nèi)存里讓它在端側(cè)也能低延遲地跑起來(lái)。這篇東西適合正在做模型部署的人看也適合剛接觸端側(cè)推理、想知道TFLite到底怎么集成的新手。全文我會(huì)從部署痛點(diǎn)講起把轉(zhuǎn)換、量化、安卓端集成、性能優(yōu)化四條鏈路完整走一遍每個(gè)環(huán)節(jié)都帶上我在實(shí)際項(xiàng)目里踩過(guò)的坑和驗(yàn)證過(guò)的做法。內(nèi)容偏實(shí)操代碼可以直接抄你照著跑一遍基本能打通整個(gè)流程。1. 端側(cè)部署的真實(shí)痛點(diǎn)為什么服務(wù)端的模型不能直接搬到手機(jī)上1.1 模型不是程序打包復(fù)制就能跑很多剛?cè)腴T的開發(fā)者會(huì)有個(gè)直覺(jué)模型訓(xùn)練好了保存下來(lái)部署到手機(jī)里然后加載圖片、調(diào)用模型不就行了嗎這個(gè)直覺(jué)錯(cuò)得很離譜。訓(xùn)練好的模型在服務(wù)端依賴著一整套運(yùn)行環(huán)境顯卡驅(qū)動(dòng)、CUDA、cuDNN、Python運(yùn)行時(shí)、TensorFlow完整安裝包。而手機(jī)端沒(méi)有這些一臺(tái)常規(guī)安卓手機(jī)能分給單個(gè)應(yīng)用的內(nèi)存常常只有幾百M(fèi)B處理器和功耗更是被嚴(yán)格限制。舉個(gè)例子我在某次做圖像分類Demo時(shí)就吃過(guò)大虧。當(dāng)時(shí)用了一個(gè)比較重的骨干網(wǎng)絡(luò)模型文件足足有200多MB直接塞進(jìn)APK里不僅安裝包膨脹到離譜運(yùn)行時(shí)光是加載模型文件就把內(nèi)存占了接近500MB在某款入門級(jí)安卓機(jī)上直接閃退。后來(lái)我換成TFLite的壓縮加量化鏈路模型體積壓到了原來(lái)的四分之一不到推理延遲也從幾百毫秒降到幾十毫秒。這還只是常規(guī)優(yōu)化沒(méi)有上GPU委托這類加速手段。1.2 TFLite的三個(gè)組成部分各做了什么TFLite不是簡(jiǎn)單的格式轉(zhuǎn)換工具嚴(yán)格來(lái)說(shuō)它是一整套為端側(cè)推理設(shè)計(jì)的生態(tài)包含三塊東西轉(zhuǎn)換器把訓(xùn)練好的模型轉(zhuǎn)成.tflite格式轉(zhuǎn)換過(guò)程中可以套用量化、剪枝等優(yōu)化解釋器在目標(biāo)設(shè)備上運(yùn)行.tflite模型的運(yùn)行時(shí)引擎針對(duì)移動(dòng)端做了內(nèi)存和算子調(diào)度優(yōu)化委托機(jī)制把部分算子交給GPU、NPU或者專用DSP執(zhí)行加速效果非常明顯。另外.tflite文件本身采用FlatBuffers格式。FlatBuffers是一種零拷貝的序列化格式模型加載時(shí)不需要像Protocol Buffers那樣反序列化出一個(gè)完整對(duì)象樹而是直接在二進(jìn)制數(shù)據(jù)上讀取字段。這個(gè)特性對(duì)端側(cè)非常重要因?yàn)槟P臀募诩虞d時(shí)省掉了一整輪數(shù)據(jù)解析內(nèi)存峰值和啟動(dòng)時(shí)間都會(huì)明顯下降。我記得第一次跑通TFLite時(shí)還特意對(duì)比過(guò)同樣的模型直接用TensorFlow的SavedModel格式在手機(jī)上推理光是加載階段就花了接近3秒轉(zhuǎn)成.tflite之后加載時(shí)間約800毫秒。這不是玄學(xué)FlatBuffers零拷貝讀取就是其中一個(gè)關(guān)鍵原因。2. 從SavedModel到.tflite轉(zhuǎn)換流程里的關(guān)鍵實(shí)操2.1 轉(zhuǎn)換工具選型Python API和命令行怎么選TFLite轉(zhuǎn)換主要的入口有兩個(gè)。一是Python APITFLiteConverter適合在訓(xùn)練腳本里順手轉(zhuǎn)換可以方便地對(duì)模型做精度驗(yàn)證二是命令行工具tflite_converter適合在CI流水線里批量轉(zhuǎn)換。我自己的習(xí)慣是第一次試驗(yàn)、要對(duì)量化效果做迭代驗(yàn)證時(shí)用Python API模型穩(wěn)定后要把多個(gè)版本批量出包時(shí)寫個(gè)shell腳本調(diào)命令行。Python API的用法非常直接以Keras模型為例import tensorflow as tf # 從Keras模型轉(zhuǎn)換 model tf.keras.models.load_model(mobilenetv2_finetuned.h5) converter tf.lite.TFLiteConverter.from_keras_model(model) # 轉(zhuǎn)換為float32的TFLite模型只做格式轉(zhuǎn)換不做量化 tflite_model converter.convert() with open(mobilenetv2.tflite, wb) as f: f.write(tflite_model)如果訓(xùn)練時(shí)保存的是SavedModel格式就換成from_saved_model(導(dǎo)出目錄)。這兩個(gè)入口本質(zhì)上沒(méi)有區(qū)別區(qū)別只在于你手頭保存的模型格式是什么。2.2 轉(zhuǎn)換參數(shù)配置不是所有模型都能一鍵轉(zhuǎn)換真正容易出問(wèn)題的不是API調(diào)用而是轉(zhuǎn)換過(guò)程中算子的兼容性。TFLite支持一套固定算子集不是TensorFlow里所有算子都能直接轉(zhuǎn)換。比如某些訓(xùn)練腳本里常見(jiàn)的tf.image.resize_images在老版本里經(jīng)常需要打開實(shí)驗(yàn)性開關(guān)或者替換成TFLite支持的resize實(shí)現(xiàn)。轉(zhuǎn)換前最好把TFLite官方算子列表過(guò)一遍提前排查模型中用了哪些不在列表里的算子。排查方法也不復(fù)雜先試轉(zhuǎn)一次報(bào)錯(cuò)信息里會(huì)直接告訴你哪個(gè)算子不被支持。遇到不支持的算子優(yōu)先思路是用等價(jià)操作替換其次才是考慮拆解模型、把不兼容的部分留在服務(wù)端計(jì)算。另外要留意輸入輸出簽名。模型如果是用新版本tf_function導(dǎo)出的簽名可能包含命名參數(shù)轉(zhuǎn)換后解釋器讀取輸入輸出索引時(shí)就要按簽名名來(lái)拿而不是簡(jiǎn)單按0、1索引。這個(gè)差異在集成階段非??游液竺鏁?huì)在安卓集成部分詳細(xì)展開。2.3 轉(zhuǎn)換后的正確性驗(yàn)證這一步千萬(wàn)別省轉(zhuǎn)換完成不代表萬(wàn)事大吉。浮點(diǎn)模型轉(zhuǎn)換成TFLite后理論上精度幾乎無(wú)損失但實(shí)際中因?yàn)椴糠炙阕觾?nèi)部實(shí)現(xiàn)差異輸出值可能會(huì)有微小波動(dòng)。如果轉(zhuǎn)換時(shí)疊加了量化差異會(huì)更明顯。所以轉(zhuǎn)換后必須做一次等價(jià)性驗(yàn)證。我自己常用的驗(yàn)證流程是準(zhǔn)備一張固定輸入分別用原模型和TFLite解釋器推理對(duì)比輸出向量。對(duì)于分類模型重點(diǎn)看softmax后最大概率對(duì)應(yīng)的類別是否一致對(duì)于回歸模型看數(shù)值相對(duì)誤差是否在可接受范圍。import numpy as np import tensorflow as tf interpreter tf.lite.Interpreter(model_pathmobilenetv2.tflite) interpreter.allocate_tensors() input_detail interpreter.get_input_details()[0] output_detail interpreter.get_output_details()[0] # 輸入數(shù)據(jù)要和訓(xùn)練時(shí)保持完全一致的預(yù)處理 sample np.random.rand(1, 224, 224, 3).astype(np.float32) interpreter.set_tensor(input_detail[index], sample) interpreter.invoke() tflite_out interpreter.get_tensor(output_detail[index]) # 和原模型輸出對(duì)比 original_out model.predict(sample) print(TFLite最大類別:, np.argmax(tflite_out)) print(Original最大類別:, np.argmax(original_out)) print(最大輸出差異:, np.max(np.abs(tflite_out - original_out)))如果最大輸出差異在1e-4量級(jí)以上基本可以判斷轉(zhuǎn)換過(guò)程有問(wèn)題需要回頭檢查是否漏了量化參數(shù)或者選錯(cuò)了轉(zhuǎn)換入口。3. 量化不是可選項(xiàng)三種量化方案的取舍與實(shí)測(cè)數(shù)據(jù)3.1 動(dòng)態(tài)范圍量化性價(jià)比最高的起步方案TFLite的量化并不是一個(gè)單一開關(guān)而是三種方案并存。最容易上手的是動(dòng)態(tài)范圍量化。它的原理是把模型權(quán)重從float32壓縮到int8推理時(shí)輸入和中間激活仍是float32但權(quán)重會(huì)在運(yùn)行時(shí)做反量化再參與計(jì)算。這樣做的收益非常直接模型體積大約能壓到原來(lái)的四分之一推理速度平均能提升20%到30%而且精度損失通常小到可以忽略。不需要準(zhǔn)備任何校準(zhǔn)數(shù)據(jù)這是它最大的優(yōu)勢(shì)。如果項(xiàng)目著急上線先在轉(zhuǎn)換參數(shù)里加上optimizations[tf.lite.Optimize.DEFAULT]就能白拿這一波優(yōu)化。converter.optimizations [tf.lite.Optimize.DEFAULT] tflite_model_quantized converter.convert()動(dòng)態(tài)范圍量化適合預(yù)算緊張、精度要求高、模型不復(fù)雜的場(chǎng)景。損失方面我實(shí)測(cè)過(guò)多次絕大多數(shù)情況下Top-1準(zhǔn)確率的變化不超過(guò)0.2個(gè)百分點(diǎn)。3.2 FP16量化給GPU設(shè)備準(zhǔn)備的精度友好方案第二種方案是FP16量化權(quán)重量化為float16推理時(shí)如果是GPU委托硬件本身原生支持半精度計(jì)算速度提升顯著如果是CPU推理運(yùn)行時(shí)會(huì)反量化成float32再計(jì)算所以CPU場(chǎng)景收益不太明顯。FP16量化的最大優(yōu)點(diǎn)是精度損失極小理論上可以做到和float32幾乎無(wú)異。損失通常在0.1%以內(nèi)有時(shí)甚至沒(méi)有損失。模型體積縮水一半左右。配置方法是在converter上加target_specconverter.optimizations [tf.lite.Optimize.DEFAULT] converter.target_spec.supported_types [tf.float16] tflite_fp16_model converter.convert()這個(gè)方案特別適合兩類設(shè)備搭載支持半精度計(jì)算的手機(jī)GPU以及需要把模型壓在存儲(chǔ)空間限制內(nèi)的場(chǎng)景。我曾在某款中端機(jī)型上對(duì)比開啟GPU委托后FP16模型推理延遲比FP32版本低了大約30%。3.3 全整型量化端側(cè)部署的終極形態(tài)第三種方案是全整型量化權(quán)重和激活全部變成int8類型。這是TFLite在硬件友好度上的巔峰整數(shù)運(yùn)算在大多數(shù)CPU上占用更少功耗許多NPU和DSP只接受整數(shù)輸入可以說(shuō)全整型模型是打通底層硬件的鑰匙。但全整型量化有個(gè)硬性前置條件必須提供代表性數(shù)據(jù)集。轉(zhuǎn)換器需要從數(shù)據(jù)集中統(tǒng)計(jì)每一層激活值的范圍才能確定int8的縮放比例和零點(diǎn)。如果直接轉(zhuǎn)換不提供校準(zhǔn)數(shù)據(jù)TFLite會(huì)跳過(guò)量化的部分模型實(shí)際仍是半量化狀態(tài)。代表性數(shù)據(jù)集的選取是門學(xué)問(wèn)。我第一次做全整型量化時(shí)直接拿了訓(xùn)練集里隨機(jī)抽的100張圖做校準(zhǔn)結(jié)果模型精度掉了接近3個(gè)百分點(diǎn)。后來(lái)?yè)Q成按類別均勻抽樣、覆蓋各類光照和背景精度損失立刻收斂到1%以內(nèi)。核心原則是校準(zhǔn)集要能代表模型真實(shí)部署時(shí)會(huì)遇到的輸入分布數(shù)量上100到500張通常就夠不是越多越好。def representative_dataset(): for image in calibration_images: input_data preprocess(image) yield [input_data.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] tflite_int8_model converter.convert()三種量化方案放在一起看各自的定位就很清楚了動(dòng)態(tài)范圍量化是零成本起步FP16是精度敏感場(chǎng)景的穩(wěn)妥選擇全整型量化則是面向低功耗硬件和廣泛設(shè)備兼容性的最終形態(tài)。實(shí)際項(xiàng)目中完全可以根據(jù)目標(biāo)設(shè)備分層出多個(gè)模型版本。3.4 量化后精度退化的排查思路量化不是沒(méi)有代價(jià)的尤其是在小模型和敏感層面前。常見(jiàn)問(wèn)題包括校準(zhǔn)集分布偏差、模型里某些層對(duì)量化特別敏感、輸出層被強(qiáng)制int8導(dǎo)致精度丟失。排查方法我建議這樣先做動(dòng)態(tài)范圍量化如果精度正常就能確定問(wèn)題出在激活值量化上再做全整型量化如果精度異常就替換校準(zhǔn)集觀察變化還是不行就把模型里幾個(gè)高敏感層設(shè)置為float32保留其余層繼續(xù)int8。TFLite支持混合精度配置尤其在檢測(cè)類模型里這種混合模式很常見(jiàn)。這幾個(gè)排查步驟走下來(lái)大部分精度退化問(wèn)題都能定位在哪一層或者哪個(gè)環(huán)節(jié)。4. 安卓端集成實(shí)操解釋器API還是Task API先想清楚再動(dòng)手4.1 依賴與環(huán)境配置安卓端集成第一步是引入依賴。在build.gradle里加上TFLite相關(guān)的包我下面的配置是目前穩(wěn)定可用的組合dependencies { implementation org.tensorflow:tensorflow-lite:2.16.1 implementation org.tensorflow:tensorflow-lite-gpu:2.16.1 implementation org.tensorflow:tensorflow-lite-support:0.4.4 }tensorflow-lite是解釋器本體tensorflow-lite-gpu是GPU委托tensorflow-lite-support是圖像處理和數(shù)據(jù)轉(zhuǎn)換的輔助庫(kù)里面有Task API的基礎(chǔ)組件。新手最容易漏的是把模型文件放對(duì)位置.tflite文件必須放在src/main/assets目錄下否則AssetManager讀不到文件。4.2 用Interpreter API做自定義模型推理如果模型不是標(biāo)準(zhǔn)的圖像分類結(jié)構(gòu)或者你需要完全控制輸入輸出直接用Interpreter API是最靈活的方案。初始化時(shí)加載模型創(chuàng)建解釋器實(shí)例然后通過(guò)getInputDetails和getOutputDetails拿到張量信息。class Classifier( private val context: Context, private val modelPath: String ) { private var interpreter: Interpreter? null fun init() { val model FileUtil.loadMappedFile(context, modelPath) interpreter Interpreter(model, Interpreter.Options().apply { numThreads 4 setUseXNNPACK(true) }) } fun classify(input: FloatArray): FloatArray { val inputShape intArrayOf(1, 224, 224, 3) val outputShape intArrayOf(1, 1000) val inputBuffer ByteBuffer.allocateDirect(4 * inputShape.fold(1, Int::times)) inputBuffer.order(ByteOrder.nativeOrder()) inputBuffer.rewind() // 這里需要把input按CHW順序?qū)懭隻uffer具體寫法和預(yù)處理有關(guān) inputBuffer.asFloatBuffer().put(input) val outputBuffer ByteBuffer.allocateDirect(4 * outputShape.fold(1, Int::times)) outputBuffer.order(ByteOrder.nativeOrder()) interpreter?.run(inputBuffer, outputBuffer) val output FloatArray(outputShape[1]) outputBuffer.asFloatBuffer().get(output) return output } fun close() { interpreter?.close() interpreter null } }這里要特別強(qiáng)調(diào)預(yù)處理的一致性。訓(xùn)練時(shí)圖像分辨率、歸一化公式、RGB通道順序是什么推理時(shí)必須一模一樣。比如訓(xùn)練時(shí)用的是mean[0.485, 0.456, 0.406]、std[0.229, 0.224, 0.225]做歸一化安卓端就要復(fù)現(xiàn)同樣的過(guò)程否則模型的輸出會(huì)莫名其妙地偏離。很多人抱怨部署后效果變差八成問(wèn)題出在這里。4.3 Task API把預(yù)處理后處理一起打包的省心方案如果你做的是圖像分類、目標(biāo)檢測(cè)、圖片分割這類常見(jiàn)任務(wù)我更推薦先用Task API。它把圖像解碼、尺寸縮放、歸一化、輸出解析全部封裝好了你只需要提供圖片路徑或Bitmap就能直接拿到分類結(jié)果省掉大量可復(fù)現(xiàn)性Bug。val options ImageClassifier.ImageClassifierOptions.builder() .setNumThreads(4) .setMaxResults(5) .build() val classifier ImageClassifier.createFromFileAndOptions( context, mobilenetv2_finetuned.tflite, options ) val image Bitmap.createScaledBitmap(source, 224, 224, true) val results classifier.process(image) for (result in results) { Log.d(分類結(jié)果, ${result.categories.first().label}: ${result.categories.first().score}) }Task API底層會(huì)自動(dòng)完成色空間轉(zhuǎn)換、尺寸歸一化這些瑣碎操作對(duì)剛接觸端側(cè)部署的人非常友好。它唯一的限制是不能處理自定義輸出結(jié)構(gòu)比如模型輸出的是兩個(gè)分支的檢測(cè)框坐標(biāo)Task API就不能覆蓋。這種時(shí)候就需要回到Interpreter API做定制開發(fā)。4.4 線程與生命周期管理推理操作不應(yīng)該跑在主線程上否則界面會(huì)明顯卡頓。我通常用單線程的ExecutorService來(lái)跑推理避免多線程同時(shí)操作同一個(gè)Interpreter實(shí)例。Interpreter實(shí)例本身不是線程安全的多個(gè)線程共用同一個(gè)實(shí)例會(huì)出現(xiàn)不可預(yù)期的錯(cuò)誤。如果確實(shí)要并行推理就創(chuàng)建多個(gè)Interpreter實(shí)例各自獨(dú)立。還有一點(diǎn)容易忽略Interpreter用完要close。雖然系統(tǒng)會(huì)在內(nèi)存不足時(shí)回收但tflite模型占用的Native內(nèi)存不會(huì)立刻釋放長(zhǎng)期運(yùn)行的應(yīng)用如果頻繁創(chuàng)建和銷毀解釋器內(nèi)存會(huì)緩慢上漲。我見(jiàn)過(guò)一個(gè)應(yīng)用每次拍照都創(chuàng)建新解釋器但忘了關(guān)閉運(yùn)行半小時(shí)后內(nèi)存占用翻了一倍。養(yǎng)成用完即關(guān)的習(xí)慣能省掉很多線上問(wèn)題。5. 性能基準(zhǔn)測(cè)試與端側(cè)優(yōu)化從能跑到跑得快5.1 用官方基準(zhǔn)測(cè)試工具量化性能集成跑通只是第一步跑得快才是端側(cè)部署的終極目標(biāo)。優(yōu)化之前要先測(cè)量。TFLite官方提供了benchmark_model工具可以直接在設(shè)備上運(yùn)行模型并輸出延遲統(tǒng)計(jì)。# 在設(shè)備上執(zhí)行基準(zhǔn)測(cè)試 adb push mobilenetv2.tflite /data/local/tmp/ adb shell /data/local/tmp/benchmark_model \ --graph/data/local/tmp/mobilenetv2.tflite \ --num_threads4 \ --warmup_min_secs5 \ --run_interval100這個(gè)工具會(huì)輸出每輪推理的平均延遲、標(biāo)準(zhǔn)差等參數(shù)。我一般在優(yōu)化前后各測(cè)一次用數(shù)據(jù)來(lái)指導(dǎo)方向。要注意的是基準(zhǔn)測(cè)試受CPU頻率策略影響波動(dòng)很大最好測(cè)試時(shí)保持屏幕狀態(tài)一致或者多測(cè)幾輪取中位數(shù)。5.2 GPU委托與NNAPI委托怎么選委托機(jī)制是TFLite加速的核心。GPU委托把支持的算子放到GPU上執(zhí)行對(duì)圖像類卷積網(wǎng)絡(luò)提升非常明顯通常能獲得2到3倍的加速。NNAPI委托則依賴設(shè)備上由芯片廠商提供的NPU驅(qū)動(dòng)不同機(jī)型差異巨大有的快得驚人有的反而比CPU還慢。我的選型經(jīng)驗(yàn)是如果目標(biāo)機(jī)型主要是中高端手機(jī)優(yōu)先開啟GPU委托效果穩(wěn)定如果要做全機(jī)型覆蓋建議做分層策略在性能較強(qiáng)的設(shè)備上開啟NNAPI在其他設(shè)備上用CPU加X(jué)NNPACK兜底。原因在于NNAPI在不同設(shè)備上的兼容性和性能方差太大直接全量發(fā)布等到線上才暴露問(wèn)題會(huì)比較被動(dòng)。5.3 端側(cè)優(yōu)化三板斧線程數(shù)、輸入分辨率、內(nèi)存復(fù)用第一個(gè)可調(diào)的參數(shù)是線程數(shù)。Interpreter.Options里的numThreads通常設(shè)置為4比較合理繼續(xù)往上加不一定能提升速度因?yàn)槎藗?cè)芯片的物理核心數(shù)是有限的線程過(guò)多反而引入調(diào)度開銷。第二個(gè)思路是降低輸入分辨率。很多模型訓(xùn)練時(shí)用224x224甚至299x299但端側(cè)場(chǎng)景往往不需要這么高的輸入精度。直接把預(yù)處理階段的分辨率降到192x192推理延遲大約能再降20%到30%對(duì)最終精度影響很小。第三個(gè)是內(nèi)存復(fù)用。在實(shí)時(shí)推理場(chǎng)景比如相機(jī)預(yù)覽連續(xù)做幀分類反復(fù)分配和回收輸入輸出緩沖區(qū)是非常浪費(fèi)的。我一般會(huì)在初始化時(shí)就把固定大小的ByteBuffer分配好每次推理直接復(fù)用同一塊緩沖區(qū)垃圾回收次數(shù)肉眼可見(jiàn)地減少幀率穩(wěn)定性有明顯改善。5.4 常見(jiàn)部署問(wèn)題的排查清單這一年多做了不少TFLite部署項(xiàng)目把最常見(jiàn)的坑匯總成一張表直接對(duì)著排查效率很高現(xiàn)象可能原因排查方向輸出全是NaN或亂碼輸入預(yù)處理和訓(xùn)練不一致檢查歸一化、通道順序、數(shù)據(jù)類型推理速度遠(yuǎn)低于預(yù)期模型未量化、線程數(shù)過(guò)低、未用委托檢查量化配置、開啟GPU/XNNPACK轉(zhuǎn)換時(shí)報(bào)算子不支持模型用了TFLite算子集之外的算子替換算子、拆模型、檢查TFLite版本加載模型時(shí)內(nèi)存暴漲模型未量化或體積過(guò)大改用int8量化、裁剪模型結(jié)構(gòu)多線程推理結(jié)果錯(cuò)亂Interpreter實(shí)例被并發(fā)使用多實(shí)例隔離或加鎖串行化GPU委托后精度下降部分算子FP16精度不足只委托部分層、換回CPU推理敏感層這些坑我基本都踩過(guò)一遍。最離譜的一次是模型在服務(wù)端速度正常部署到某款機(jī)型后首次推理耗時(shí)接近5秒查到最后發(fā)現(xiàn)是模型沒(méi)有啟用XNNPACK加速CPU推理時(shí)走了老路徑。開啟setUseXNNPACK(true)后首次推理時(shí)間降到700毫秒左右問(wèn)題迎刃而解。6. 擴(kuò)展方向與個(gè)人經(jīng)驗(yàn)從TFLite Micro到部署心態(tài)6.1 TFLite Micro面向單片機(jī)的一小塊天地如果項(xiàng)目目標(biāo)設(shè)備是單片機(jī)這類極致資源受限的環(huán)境那TFLite Micro是后續(xù)值得關(guān)注的方向。它把解釋器裁剪到極小尺寸能在只有幾十KB內(nèi)存的設(shè)備上運(yùn)行模型。我還沒(méi)正式在量產(chǎn)項(xiàng)目里用過(guò)它但做技術(shù)調(diào)研時(shí)把官方Demo跑通過(guò)。感受是雖然可用算子范圍更窄、集成難度更高但端側(cè)推理的天花板確實(shí)被打開了。比如在STM32級(jí)別的開發(fā)板上做喚醒詞識(shí)別、傳感器異常檢測(cè)這類場(chǎng)景用TFLite Micro有不可替代的價(jià)值。對(duì)于搞硬件或者IoT方向的人這塊值得盯住。6.2 部署不是一次性的動(dòng)作我的幾個(gè)經(jīng)驗(yàn)維度我在實(shí)際項(xiàng)目里體會(huì)最深的一點(diǎn)是TFLite集成不是一次性的轉(zhuǎn)換動(dòng)作而是持續(xù)調(diào)優(yōu)的過(guò)程。轉(zhuǎn)換、量化、部署、測(cè)速、再調(diào)整這五個(gè)步驟需要反復(fù)循環(huán)。別指望第一次轉(zhuǎn)換出來(lái)的模型就能達(dá)到最佳效果先把鏈路跑通再一步步壓體積、提速度這才是最穩(wěn)妥的節(jié)奏。量化參數(shù)、線程數(shù)、委托方式每一個(gè)都要拿實(shí)測(cè)數(shù)據(jù)說(shuō)話而不是憑感覺(jué)配置。6.3 一個(gè)很實(shí)用的小習(xí)慣端側(cè)日志與線上監(jiān)控最后分享一個(gè)實(shí)操里的小技巧在端側(cè)推理時(shí)把每次推理的耗時(shí)、置信度最大值、類別標(biāo)簽寫進(jìn)日志里配合線上數(shù)據(jù)埋點(diǎn)能幫你盡早發(fā)現(xiàn)模型在真實(shí)設(shè)備上的表現(xiàn)異常。我就靠這招在某個(gè)項(xiàng)目上線后第三天抓到了輸入圖像方向旋轉(zhuǎn)導(dǎo)致的精度問(wèn)題。當(dāng)時(shí)模型本身完全正常純粹是預(yù)處理時(shí)EXIF方向信息沒(méi)處理。部署這件事錯(cuò)的地方往往不在模型而在模型周圍的環(huán)境。把日志做細(xì)排查問(wèn)題的速度會(huì)快上很多。