換踩坑實(shí)錄:TFLite 轉(zhuǎn) .nb 的算子與目標(biāo)平臺(tái)排查)
分享一個(gè)我這周剛踩完的坑把一個(gè) OCR 檢測(cè)模型從 .tflite 轉(zhuǎn)成 Paddle Lite 的 .nb 格式命令里帶了 --target 參數(shù)指定目標(biāo)平臺(tái)結(jié)果各種報(bào)錯(cuò)來(lái)回折騰光日志就看了好幾輪。這個(gè)問題看起來(lái)很小但涉及到的知識(shí)點(diǎn)其實(shí)很雜opt 工具版本差異、算子與 target 的匹配關(guān)系、轉(zhuǎn)換環(huán)境是否完整、最終部署目標(biāo)是否一致。如果你也正在做邊緣端 AI 部署或者模型是從 TensorFlow 導(dǎo)出、推理框架卻用的是 Paddle Lite那這篇排查記錄應(yīng)該能幫你省下不少時(shí)間。這篇文章適合幾類人第一次把 TFLite 模型拿去做 .nb 轉(zhuǎn)換的初學(xué)者在轉(zhuǎn)換時(shí)對(duì) --target / --valid_targets 含義和區(qū)別不清不楚的工程師以及遇到“Unrecognized option”、“The model is not supported in arm”、“no target connected”這類報(bào)錯(cuò)不知道怎么下手的同學(xué)。我會(huì)把整個(gè)排查過程、錯(cuò)誤日志、最終解決方案以及常見的坑全部整理出來(lái)照著操作就能復(fù)現(xiàn)和避坑。1. 模型格式拆解.tflite 和 .nb 到底差在哪1.1 .tflite 和 .nb 的底層思路先別急著看報(bào)錯(cuò)得先搞清楚這兩個(gè)格式之間的差異。.tflite 是 TensorFlow 的移動(dòng)端推理格式本質(zhì)上是一個(gè)用 FlatBuffers 序列化之后的模型文件把計(jì)算圖、權(quán)重、算子元數(shù)據(jù)全部壓縮到一個(gè)二進(jìn)制里。它設(shè)計(jì)的目標(biāo)是“體積小、加載快、能在移動(dòng)端跑”所以結(jié)構(gòu)非常緊湊。.nb 則是 Paddle Lite 的私有模型格式全名常叫 Naive Buffer。它不只是把模型重新序列化了一次而是按照 Paddle Lite 運(yùn)行時(shí)所需要的算子排列順序和內(nèi)存布局把權(quán)重全部重新組織并寫入二進(jìn)制文件。這樣做的好處非常明顯加載 .nb 模型時(shí)運(yùn)行時(shí)幾乎不需要再做復(fù)雜的解析和權(quán)重預(yù)處理直接映射到內(nèi)存就能開始推理。所以這就解釋了一個(gè)常見困惑為什么不能直接把 .tflite 后綴改成 .nb或者讓 Paddle Lite 直接加載 .tflite因?yàn)?Paddle Lite 的運(yùn)行時(shí)不認(rèn)識(shí) TFLite 的算子描述和權(quán)重排列方式它只認(rèn)自己定義的 .nb 結(jié)構(gòu)。如果最終推理框架定的是 Paddle Lite那這一步轉(zhuǎn)換就繞不開。1.2 Paddle Lite opt 工具在轉(zhuǎn)換鏈路里的位置負(fù)責(zé)把 TFLite 轉(zhuǎn)成 .nb 的官方工具是 opt也就是 paddle_lite_opt。它做的事情可以拆成三步把外部模型包括 Paddle 模型、TFLite、ONNX 等解析成 Paddle Lite 內(nèi)部的模型表示在這個(gè)表示上做算子融合、計(jì)算圖優(yōu)化、權(quán)重預(yù)處理根據(jù)你指定的目標(biāo)平臺(tái)挑選對(duì)應(yīng)的 kernel 實(shí)現(xiàn)并輸出最終的 .nb 文件。注意最后一步“根據(jù)目標(biāo)平臺(tái)挑選 kernel”這個(gè)目標(biāo)平臺(tái)就是通過 --target 或者新版工具里的 --valid_targets 參數(shù)來(lái)指定的。不同目標(biāo)平臺(tái)對(duì)應(yīng)不同的算子實(shí)現(xiàn)集合如果一個(gè)模型里的某個(gè)算子在你指定的 target 下沒有對(duì)應(yīng)的 kernel 實(shí)現(xiàn)轉(zhuǎn)換工具就會(huì)明確告訴你這個(gè)模型在這個(gè) target 上不支持。這也是大量轉(zhuǎn)換報(bào)錯(cuò)的總源頭。2. --target 參數(shù)的三個(gè)經(jīng)典坑版本、算子、運(yùn)行時(shí)2.1 參數(shù)名本身就是一個(gè)版本陷阱我第一次轉(zhuǎn)換時(shí)命令是照著網(wǎng)上教程抄的paddle_lite_opt --model_fileocr_det.tflite --targetarm --optimize_outocr_det.nb結(jié)果工具直接回了一句ERROR: Unrecognized option: target我當(dāng)時(shí)的第一個(gè)反應(yīng)是工具沒裝好于是去查了paddle_lite_opt --help發(fā)現(xiàn)新版本里根本沒有 --target 這個(gè)參數(shù)官方參數(shù)已經(jīng)改成了--valid_targets。舊教程里常寫的--targetarm在舊版工具里能識(shí)別但新版工具會(huì)在參數(shù)解析階段直接拒絕。這個(gè)改動(dòng)坑了不少人因?yàn)榫W(wǎng)上大量博客、帖子都停留在舊版本時(shí)代。所以碰到類似的“Unrecognized option”第一件事就是確認(rèn)你安裝的 opt 版本支持哪些參數(shù)不要盲目相信手頭的教程。2.2 算子覆蓋差異為什么 arm 轉(zhuǎn)不過、x86 卻能過把參數(shù)名改成--valid_targetsarm之后工具總算開始跑了但換來(lái)了另一個(gè)報(bào)錯(cuò)[WARNING] Find 2 invalid ops: [p_placeholder, mirror_pad] [ERROR] The model is not supported in arm.這里的關(guān)鍵點(diǎn)在于Paddle Lite 在不同 target 上實(shí)現(xiàn)的算子集合是不同的。x86 平臺(tái)因?yàn)殚_發(fā)調(diào)試最常用算子覆蓋率往往最高arm 平臺(tái)的算子覆蓋會(huì)略少一些而 opencl、npu 這類異構(gòu)計(jì)算平臺(tái)支持的算子更集中。很多在 x86 上能順利轉(zhuǎn)換的模型切到 arm 后就會(huì)出現(xiàn)“某幾個(gè)算子找不到實(shí)現(xiàn)”的情況。我當(dāng)時(shí)這個(gè)模型里的問題算子就是 MirrorPad。這是一個(gè)在部分圖像前處理里會(huì)用到的算子但 Paddle Lite 的 arm kernel 列表里沒有實(shí)現(xiàn)它。這個(gè)只能從模型結(jié)構(gòu)層面解決比如在 TensorFlow 側(cè)用等價(jià)算子替換或者升級(jí) Paddle Lite 版本碰碰運(yùn)氣。2.3 運(yùn)行時(shí)缺失導(dǎo)致的“no target connected”類報(bào)錯(cuò)還有一類報(bào)錯(cuò)和算子無(wú)關(guān)純粹是環(huán)境問題。我在一個(gè)精簡(jiǎn)的 Docker 容器里試過指定--valid_targetsopencl結(jié)果工具報(bào)出no target connected這個(gè)錯(cuò)誤的意思是opt 在初始化階段需要加載對(duì)應(yīng) target 的運(yùn)行時(shí)但當(dāng)前環(huán)境里沒有 OpenCL 庫(kù)也沒有可用的 GPU 設(shè)備于是工具認(rèn)為這個(gè) target 不可用。類似的情況還有指定 NPU target 但沒裝 NPU SDK、指定 xpu 但驅(qū)動(dòng)未加載等。這類問題一般排查路徑比較清晰確認(rèn)對(duì)應(yīng)運(yùn)行庫(kù)是否安裝設(shè)備節(jié)點(diǎn)是否存在環(huán)境變量是否設(shè)置。3. 轉(zhuǎn)換日志逐行看我是怎么定位到 MirrorPad 的3.1 環(huán)境準(zhǔn)備與版本確認(rèn)先說我當(dāng)時(shí)的運(yùn)行環(huán)境這個(gè)很重要因?yàn)榄h(huán)境不同報(bào)錯(cuò)現(xiàn)象真的會(huì)差很多宿主機(jī)x86_64 Ubuntu 20.04Python 3.8通過 pip 安裝 paddlelite 2.12opt 工具為同版本自帶的 paddle_lite_opt我強(qiáng)烈建議把轉(zhuǎn)換工作放在 x86 宿主機(jī)上做而不是在 ARM 開發(fā)板上做。原因后面會(huì)在速查表里詳細(xì)說簡(jiǎn)單講就是板子上缺圖形庫(kù)、缺依賴的概率太高容易引出無(wú)關(guān)報(bào)錯(cuò)。環(huán)境準(zhǔn)備如果用 conda有一個(gè)小坑要提醒創(chuàng)建虛擬環(huán)境時(shí)目標(biāo)目錄必須是一個(gè)不存在的新目錄如果你把 conda 環(huán)境直接指定到一個(gè)已經(jīng)存在且不是 conda 環(huán)境的目錄會(huì)報(bào)DirectoryNotACondaEnvironmentError。我當(dāng)時(shí)第一次建環(huán)境就踩了后來(lái)?yè)Q了個(gè)全新路徑才順利裝上。3.2 從參數(shù)報(bào)錯(cuò)到算子報(bào)錯(cuò)的完整路徑最后的排查路徑其實(shí)是有邏輯的我按這個(gè)順序走了一遍先確認(rèn)參數(shù)名是否合法用--help查看當(dāng)前版本支持的選項(xiàng)把--target改成--valid_targets后工具進(jìn)入實(shí)際轉(zhuǎn)換再用--valid_targetsx86試轉(zhuǎn)同一個(gè)模型如果 x86 能成功說明模型本身結(jié)構(gòu)沒問題問題出在 arm 的算子覆蓋上最后定位到具體不支持的算子去 TensorFlow 側(cè)改模型。這個(gè)過程里x86 試轉(zhuǎn)是個(gè)關(guān)鍵動(dòng)作。它能把“模型的問題”和“平臺(tái)的問題”切分開。如果連 x86 都轉(zhuǎn)不過那說明模型結(jié)構(gòu)和 TFLite 導(dǎo)出過程可能就有問題得先回到上層解決如果 x86 能過、arm 過不了那就專注處理不支持的算子。3.3 替換 MirrorPad 與重新導(dǎo)出我最終選擇在 TensorFlow 側(cè)把 MirrorPad 替換掉。簡(jiǎn)單說MirrorPad 的作用是把張量按某種鏡像模式進(jìn)行邊緣填充這在圖像預(yù)處理里并不少見。我用 tf.pad 加 tf.concat 手動(dòng)實(shí)現(xiàn)了同樣的效果然后重新導(dǎo)出 TFLite 模型import tensorflow as tf # 自定義鏡像填充實(shí)現(xiàn)代替 MirrorPad def mirror_pad_replacement(x, paddings): # paddings 是 [[top, bottom], [left, right]] 結(jié)構(gòu) # 先用 tf.reverse 構(gòu)造鏡像部分再 concat top, bottom paddings[0][0], paddings[0][1] left, right paddings[1][0], paddings[1][1] x_top tf.reverse(x[:, 1:1 top, :, :], axis[1]) x_bottom tf.reverse(x[:, -1 - bottom:-1, :, :], axis[1]) x tf.concat([x_top, x, x_bottom], axis1) x_left tf.reverse(x[:, :, 1:1 left, :], axis[2]) x_right tf.reverse(x[:, :, -1 - right:-1, :], axis[2]) x tf.concat([x_left, x, x_right], axis2) return x這里代碼只是一個(gè)示例思路在實(shí)際項(xiàng)目里替換操作要放在模型導(dǎo)出之前再經(jīng)過 TFLiteConverter 轉(zhuǎn)換converter tf.lite.TFLiteConverter.from_keras_model(model) converter.target_spec.supported_ops [tf.lite.OpsSet.TFLITE_BUILTINS] tflite_model converter.convert()重新導(dǎo)出后再執(zhí)行轉(zhuǎn)換命令就順利通過了。整個(gè)過程花的時(shí)間不算長(zhǎng)但如果不理解“算子與 target 不匹配”這個(gè)原理很容易在錯(cuò)誤方向上繞圈。3.4 成功轉(zhuǎn)換命令與部署驗(yàn)證最終的轉(zhuǎn)換命令是這樣寫的paddle_lite_opt \ --model_fileocr_det.tflite \ --model_typetflite \ --valid_targetsarm \ --optimize_outocr_det \ --optimize_out_typenaive_buffer注意兩個(gè)容易被忽略的點(diǎn)一個(gè)是--model_typetflite如果不顯式指定工具默認(rèn)可能按 Paddle 模型處理結(jié)果完全對(duì)不上另一個(gè)是--optimize_out_typenaive_buffer這個(gè)參數(shù)決定了輸出的是 .nb 格式而不是默認(rèn)的 protobuf 格式模型。成功轉(zhuǎn)換后會(huì)生成ocr_det.nb文件。在開發(fā)板上用 Paddle Lite 的 C API 加載時(shí)標(biāo)準(zhǔn)的加載方式是#include paddle_api.h using namespace paddle::lite_api; MobileConfig config; config.set_model_from_file(/data/model/ocr_det.nb); auto predictor CreatePaddlePredictorMobileConfig(config);我在這一步也踩過一個(gè)坑一開始沒有指定 --model_type轉(zhuǎn)換命令跑完沒有報(bào)錯(cuò)但生成的文件根本不是可用的 .nb部署時(shí)加載直接崩潰。所以轉(zhuǎn)換完一定要檢查文件別急著拷到板子上。4. 高頻報(bào)錯(cuò)速查表一眼鎖定 .nb 轉(zhuǎn)換失敗原因4.1 常見錯(cuò)誤對(duì)照與處理辦法我把這次排查過程中遇到以及從其他工程師那里收集到的常見報(bào)錯(cuò)整理成了一張速查表遇到問題時(shí)直接對(duì)著找就行報(bào)錯(cuò)信息可能原因處理辦法Unrecognized option: target工具版本較新參數(shù)已改為 --valid_targets用 --help 查看當(dāng)前版本支持的參數(shù)The model is not supported in arm模型包含 arm 平臺(tái)上不支持的算子替換算子上游實(shí)現(xiàn)或升級(jí) Paddle Lite 版本Find N invalid ops: [xxx]日志中會(huì)具體列出不支持的算子逐個(gè)在 TensorFlow 側(cè)做等價(jià)替換no target connected目標(biāo)平臺(tái)運(yùn)行時(shí)缺失或設(shè)備不可用檢查 OpenCL、NPU SDK、驅(qū)動(dòng)是否安裝The target environment has been corrupted虛擬環(huán)境或工具安裝損壞重建 conda 環(huán)境重新安裝 paddleliteDirectoryNotACondaEnvironmentErrorconda 環(huán)境目標(biāo)路徑已被非 conda 目錄占用換一個(gè)全新的空目錄創(chuàng)建環(huán)境libGL error: failed to load driver: rockchip板卡上缺少圖形庫(kù)或 GPU 驅(qū)動(dòng)不要在板子上跑轉(zhuǎn)換改用 x86 宿主機(jī)加載 .nb 時(shí)程序崩潰轉(zhuǎn)換 target 與部署設(shè)備不一致讓 --valid_targets 包含真實(shí)部署設(shè)備生成的文件無(wú)法被 Paddle Lite 識(shí)別未設(shè)置 --model_type 或 --optimize_out_type 不對(duì)顯式設(shè)置 --model_typetflite --optimize_out_typenaive_buffer這張表里前三條和最后一條出現(xiàn)的頻率最高建議把命令模板固定下來(lái)不要每次臨時(shí)寫參數(shù)。4.2 轉(zhuǎn)換與部署的幾條實(shí)用經(jīng)驗(yàn)清單下面這些都是我在實(shí)際項(xiàng)目里實(shí)驗(yàn)過、驗(yàn)證過有效的方法按執(zhí)行順序整理轉(zhuǎn)換工具不要在目標(biāo)開發(fā)板上運(yùn)行尤其不要在有圖形依賴的環(huán)境里運(yùn)行。板卡上經(jīng)常缺 OpenGL 庫(kù)運(yùn)行過程中容易爆出 libGL error 之類的無(wú)關(guān)錯(cuò)誤干擾排查。轉(zhuǎn)換命令里強(qiáng)制寫明 --model_type。針對(duì) TFLite 文件不寫的話工具可能按默認(rèn) Paddle 模型解析結(jié)果五花八門。--optimize_out_typenaive_buffer 才會(huì)生成真正可部署的 .nb。如果漏掉輸出格式不對(duì)部署時(shí)肯定加載失敗。先用 x86 target 試轉(zhuǎn)一遍。x86 能過、arm 不能過那基本是算子覆蓋問題x86 都不能過大概率是模型導(dǎo)出或結(jié)構(gòu)問題。模型算子復(fù)雜時(shí)用 Netron 打開 TFLite 文件人眼掃一遍算子列表遇到冷門算子提前在模型側(cè)替換能省一大輪轉(zhuǎn)換調(diào)試時(shí)間。--valid_targets 支持逗號(hào)分隔比如 --valid_targetsarm,opencl。在 GPU 設(shè)備上部署時(shí)這種寫法能讓算子盡量落到 GPU同時(shí)保留 CPU 后備提升整體成功率。轉(zhuǎn)換完成后用 file 命令檢查一下生成的 .nb確認(rèn)目標(biāo)文件確實(shí)是 Paddle Lite 的 naive buffer 格式。不要等到部署階段才知道轉(zhuǎn)換其實(shí)已經(jīng)失敗了。Paddle Lite 版本升級(jí)后舊的 .nb 最好重新轉(zhuǎn)換。因?yàn)樾掳姹究赡苷{(diào)整算子實(shí)現(xiàn)和模型格式舊文件不一定還能用。4.3 關(guān)于環(huán)境損壞和依賴缺失的補(bǔ)充有些報(bào)錯(cuò)看起來(lái)很像模型問題實(shí)際是環(huán)境問題。比如我在排查過程中看到過這類信息corrupted environment: the target environment has been corrupted這種大概率是 conda 環(huán)境或者 pip 安裝的依賴文件損壞。不用去改模型直接把環(huán)境刪掉重建重新安裝 paddlelite 和相關(guān)庫(kù)問題就消失了。還有一種常見的是跑轉(zhuǎn)換工具時(shí)提示缺少某個(gè)動(dòng)態(tài)庫(kù)比如 libOpenCL.so 找不到說明 opencl target 需要的運(yùn)行庫(kù)沒有安裝。這時(shí)候裝對(duì)應(yīng)庫(kù)或者干脆不用那個(gè) target都能解決。5. 最后分享幾點(diǎn)部署相關(guān)的經(jīng)驗(yàn)這次踩坑之后我在團(tuán)隊(duì)里做了一個(gè)小改進(jìn)把轉(zhuǎn)換命令固化成腳本模型一更新就直接跑。腳本里把 --model_type、--valid_targets、--optimize_out_type 這些容易出錯(cuò)的參數(shù)全部寫死只留模型路徑和 target 兩個(gè)變量。這樣無(wú)論是誰(shuí)來(lái)做轉(zhuǎn)換都不會(huì)因?yàn)閰?shù)名寫錯(cuò)再走一遍彎路。另外一個(gè)很重要的體會(huì)是不要迷信網(wǎng)上舊教程里的參數(shù)。工具版本迭代太快不同版本之間的參數(shù)和算子支持差異真的很大。遇到問題先確認(rèn)版本再對(duì)癥下藥比硬套教程要快得多。最后再分享一個(gè)小技巧如果模型很大轉(zhuǎn)換時(shí)間比較長(zhǎng)可以在命令前加一個(gè)time記錄耗時(shí)同時(shí)讓工具輸出詳細(xì)日志。這樣一旦某次轉(zhuǎn)換失敗你能很快判斷是卡在哪一步而不是對(duì)著屏幕干等。做邊緣端模型轉(zhuǎn)換這件事耐心和系統(tǒng)性排查缺一不可。