換ONNX部署RDK-X5實(shí)戰(zhàn)避坑指南)
1. 從PyTorch到ONNX為什么模型轉(zhuǎn)換是RDK-X5部署的第一道坎地平線RDK-X5這塊板子最近在邊緣計(jì)算圈子里討論度很高128 TOPS的算力、支持多路攝像頭輸入、功耗控制得也不錯(cuò)做智能視覺項(xiàng)目的人很難繞開它。但真正拿到板子之后你會發(fā)現(xiàn)從訓(xùn)練好的模型到板子上跑起來中間隔著一道不小的鴻溝——模型轉(zhuǎn)換。YOLOv11作為Ultralytics最新一代檢測模型在精度和速度上都有明顯提升很多人想把它部署到RDK-X5上做實(shí)時(shí)檢測。問題在于RDK-X5的推理引擎并不直接吃PyTorch的.pth文件你需要先把模型轉(zhuǎn)成ONNX再經(jīng)過地平線的工具鏈量化編譯成.bin模型最后才能在板子上加載運(yùn)行。這個(gè)鏈條里ONNX轉(zhuǎn)換是第一步也是最容易出問題的一步。我前后折騰了大概一周時(shí)間踩了不少坑有些是YOLOv11本身結(jié)構(gòu)帶來的有些是Ultralytics導(dǎo)出邏輯和地平線工具鏈之間的兼容性問題。這篇文章會把整個(gè)轉(zhuǎn)換過程拆開講清楚包括每一步為什么要這么做、參數(shù)怎么選、遇到報(bào)錯(cuò)怎么排查。如果你手頭正好有RDK-X5或者準(zhǔn)備入手做YOLOv11部署這些經(jīng)驗(yàn)應(yīng)該能幫你省下不少時(shí)間。先說清楚適用對象這篇文章面向的是已經(jīng)訓(xùn)練好YOLOv11模型、準(zhǔn)備往RDK-X5上部署的開發(fā)者。如果你還沒訓(xùn)練好自己的模型建議先把訓(xùn)練流程跑通再來看轉(zhuǎn)換部分。另外文中涉及的地平線工具鏈操作需要你對Linux命令行有基本了解完全不熟悉終端操作的話可能會有些吃力。2. 轉(zhuǎn)換前的環(huán)境準(zhǔn)備與版本對齊2.1 為什么版本匹配比你想的重要模型轉(zhuǎn)換這件事最怕的就是版本不對齊。Ultralytics的YOLOv11在不同版本之間導(dǎo)出ONNX的代碼邏輯是有差異的。我一開始用的是ultralytics 8.3.0導(dǎo)出之后發(fā)現(xiàn)ONNX模型的輸出節(jié)點(diǎn)名字和地平線工具鏈預(yù)期的對不上導(dǎo)致后面量化編譯一直報(bào)錯(cuò)。后來換成8.3.40版本輸出結(jié)構(gòu)才穩(wěn)定下來。除了ultralytics本身還有幾個(gè)關(guān)鍵依賴需要注意PyTorch版本建議用2.0以上但不要用最新的2.5某些算子導(dǎo)出會有問題。我實(shí)測2.1.2和2.2.0都比較穩(wěn)。ONNX版本1.15.0到1.16.0之間比較合適太新的版本有時(shí)候反而會引入不必要的opset變化。onnxsim這個(gè)工具用來簡化ONNX計(jì)算圖去掉冗余節(jié)點(diǎn)對后續(xù)量化很有幫助。但不是必須的后面會詳細(xì)說。地平線工具鏈RDK-X5用的是OpenExplorerOE工具鏈版本要和板子上的系統(tǒng)鏡像匹配。我用的OE 3.0.0對應(yīng)RDK-X5的1.0.5系統(tǒng)。注意地平線工具鏈的版本和板子系統(tǒng)版本必須嚴(yán)格對應(yīng)否則編譯出來的.bin模型加載會失敗。建議先在板子上用cat /etc/version確認(rèn)系統(tǒng)版本再去下載對應(yīng)的OE包。2.2 環(huán)境搭建的實(shí)操步驟我習(xí)慣用conda建一個(gè)獨(dú)立環(huán)境來做轉(zhuǎn)換避免和訓(xùn)練環(huán)境混在一起。具體操作如下conda create -n rdk_convert python3.10 conda activate rdk_convert pip install torch2.1.2 torchvision0.16.2 --index-url https://download.pytorch.org/whl/cpu pip install ultralytics8.3.40 pip install onnx1.15.0 onnxruntime1.17.0 pip install onnxsim這里有個(gè)細(xì)節(jié)PyTorch裝CPU版本就夠了因?yàn)閷?dǎo)出ONNX不需要GPU。裝GPU版本反而會讓環(huán)境變大而且有時(shí)候CUDA版本和驅(qū)動不匹配還會引入額外問題。地平線工具鏈的安裝稍微麻煩一些需要從官方渠道獲取OE包然后按照文檔配置環(huán)境變量。核心是horizon_tc_ui這個(gè)命令行工具后面量化編譯都靠它。安裝完成后可以用hb_mapper --version檢查是否配置成功。2.3 模型導(dǎo)出前的檢查清單在正式導(dǎo)出之前建議先確認(rèn)幾件事模型能正常推理用訓(xùn)練好的.pth在PyTorch環(huán)境下跑一張測試圖確認(rèn)輸出正常。這一步看起來多余但我確實(shí)遇到過訓(xùn)練完模型權(quán)重?fù)p壞的情況導(dǎo)出ONNX之后才發(fā)現(xiàn)問題白白浪費(fèi)了半天排查時(shí)間。輸入尺寸確定RDK-X5支持的輸入尺寸有限制常見的640x640、416x416都可以。如果你訓(xùn)練時(shí)用的是非正方形輸入建議改成正方形再導(dǎo)出否則后面編譯可能會報(bào)錯(cuò)。類別數(shù)確認(rèn)導(dǎo)出前確認(rèn)模型的nc類別數(shù)和你的實(shí)際需求一致。YOLOv11默認(rèn)是80類COCO如果你訓(xùn)練的是自定義數(shù)據(jù)集確保加載的是正確的權(quán)重文件。3. YOLOv11導(dǎo)出ONNX的核心細(xì)節(jié)與踩坑實(shí)錄3.1 導(dǎo)出命令與參數(shù)解析Ultralytics提供了非常方便的導(dǎo)出接口一行命令就能搞定from ultralytics import YOLO model YOLO(best.pt) model.export(formatonnx, imgsz640, opset11, simplifyTrue, dynamicFalse)看起來很簡單對吧但這里面每個(gè)參數(shù)都有講究。imgsz640這個(gè)不用多說和訓(xùn)練時(shí)保持一致。如果你訓(xùn)練用的是1280導(dǎo)出時(shí)也要用1280否則精度會掉。opset11這是關(guān)鍵。地平線OE工具鏈對opset版本有要求太高了不支持太低了某些算子又導(dǎo)不出來。我實(shí)測opset 11是最穩(wěn)的opset 12在某些情況下也能用但opset 13以上就會報(bào)不支持的算子錯(cuò)誤。simplifyTrue這個(gè)參數(shù)會調(diào)用onnxsim對模型進(jìn)行簡化。大部分情況下是好事能去掉一些冗余的Shape、Gather節(jié)點(diǎn)。但YOLOv11的結(jié)構(gòu)比較特殊簡化之后有時(shí)候反而會引入問題。如果你導(dǎo)出后發(fā)現(xiàn)模型推理結(jié)果不對可以試試把simplify關(guān)掉。dynamicFalse固定輸入尺寸。RDK-X5不支持動態(tài)輸入所以這里必須設(shè)為False。如果你設(shè)了True后面編譯一定會報(bào)錯(cuò)。3.2 導(dǎo)出后的模型結(jié)構(gòu)檢查導(dǎo)出完成后別急著往下走先用netron看一下模型結(jié)構(gòu)。重點(diǎn)檢查幾個(gè)地方輸入節(jié)點(diǎn)應(yīng)該只有一個(gè)名字通常是imagesshape是[1, 3, 640, 640]。輸出節(jié)點(diǎn)YOLOv11默認(rèn)導(dǎo)出會有一個(gè)輸出shape是[1, 84, 8400]80類COCO。如果是自定義數(shù)據(jù)集84會變成41nc。但地平線工具鏈通常需要三個(gè)輸出頭分別對應(yīng)stride 8、16、32的特征圖。這就涉及到后面要講的輸出節(jié)點(diǎn)修改。算子類型檢查有沒有工具鏈不支持的算子。常見的有GridSample、NonMaxSuppression等。YOLOv11默認(rèn)導(dǎo)出是不帶NMS的所以這個(gè)問題不大但如果你在導(dǎo)出時(shí)加了nmsTrue那就需要去掉。我第一次導(dǎo)出的時(shí)候發(fā)現(xiàn)模型里有一個(gè)Resize算子用的是nearest模式但地平線工具鏈只支持bilinear。這個(gè)問題在YOLOv11的某些版本里會出現(xiàn)解決辦法是在導(dǎo)出前修改模型的上采樣層或者用onnxsim做算子替換。3.3 輸出節(jié)點(diǎn)修改從單輸出到三輸出這是YOLOv11轉(zhuǎn)ONNX最核心的一個(gè)坑。Ultralytics默認(rèn)導(dǎo)出的ONNX模型只有一個(gè)輸出節(jié)點(diǎn)是已經(jīng)解碼好的檢測結(jié)果。但地平線工具鏈需要的是未解碼的原始特征圖也就是三個(gè)不同尺度的輸出。為什么要這樣做因?yàn)榈仄骄€芯片上的后處理是固化在硬件里的它需要拿到原始的特征圖自己去做解碼和NMS。如果你給它一個(gè)已經(jīng)解碼好的輸出它反而不知道怎么處理。修改方法有兩種方法一修改導(dǎo)出代碼在ultralytics的export代碼里找到torch.onnx.export那部分把輸出節(jié)點(diǎn)改成模型的中間層輸出。具體來說需要修改head.py里的forward函數(shù)讓它返回三個(gè)特征圖而不是解碼后的結(jié)果。# 在ultralytics/nn/modules/head.py中修改Detect類的forward方法 def forward(self, x): # 原來的代碼是返回解碼后的結(jié)果 # 修改為返回原始特征圖 for i in range(self.nl): x[i] self.cv2[i](x[i]) x[i] self.cv3[i](x[i]) return x # 返回三個(gè)特征圖方法二導(dǎo)出后修改ONNX計(jì)算圖如果不想動源碼可以用onnx工具在導(dǎo)出后修改輸出節(jié)點(diǎn)。具體操作是找到三個(gè)特征圖對應(yīng)的節(jié)點(diǎn)把它們標(biāo)記為輸出然后刪掉后面的解碼部分。import onnx model onnx.load(yolov11.onnx) # 找到三個(gè)輸出節(jié)點(diǎn)通常是Concat之前的節(jié)點(diǎn) # 這里需要根據(jù)實(shí)際模型結(jié)構(gòu)來確定節(jié)點(diǎn)名字 output_names [output0, output1, output2] # 修改輸出節(jié)點(diǎn) # ...具體代碼略需要根據(jù)模型結(jié)構(gòu)手動調(diào)整方法一更徹底但需要改源碼方法二更靈活但需要對ONNX結(jié)構(gòu)比較熟悉。我建議用方法一因?yàn)楦囊淮沃蠛竺鎸?dǎo)出都方便。實(shí)操心得修改輸出節(jié)點(diǎn)后導(dǎo)出的ONNX模型用onnxruntime跑一下確認(rèn)三個(gè)輸出的shape分別是[1, 64, 80, 80]、[1, 64, 40, 40]、[1, 64, 20, 20]640輸入80類。如果是自定義數(shù)據(jù)集64會變成nc41再乘以某個(gè)系數(shù)具體取決于YOLOv11的head結(jié)構(gòu)。3.4 常見報(bào)錯(cuò)與排查方法導(dǎo)出過程中我遇到過幾個(gè)典型報(bào)錯(cuò)這里整理一下報(bào)錯(cuò)信息原因解決方法Unsupported operator: GridSample某些版本YOLOv11用了GridSample升級ultralytics到8.3.40或手動替換算子Output shape mismatch輸出節(jié)點(diǎn)不對檢查是否修改了輸出為三特征圖Opset version not supportedopset太高改為opset11Dynamic shape not supported輸入是動態(tài)的導(dǎo)出時(shí)設(shè)dynamicFalseResize mode not supported上采樣用了nearest改為bilinear或修改模型還有一個(gè)比較隱蔽的問題導(dǎo)出后的ONNX模型在onnxruntime上跑正常但在地平線工具鏈里編譯時(shí)報(bào)錯(cuò)。這種情況通常是模型里有工具鏈不支持的算子組合需要用hb_mapper的check功能先檢查一遍。4. 從ONNX到RDK-X5量化編譯與板端部署4.1 量化編譯的整體流程ONNX導(dǎo)出成功只是第一步接下來要用地平線的工具鏈把它編譯成板子能加載的.bin模型。整個(gè)流程分為三步準(zhǔn)備校準(zhǔn)數(shù)據(jù)量化需要一批代表性圖片通常100-200張就夠了。這些圖片要覆蓋你的實(shí)際應(yīng)用場景比如做車牌識別就用各種光照、角度的車牌圖。配置yaml文件定義模型輸入輸出、量化參數(shù)、編譯選項(xiàng)。執(zhí)行編譯用hb_mapper命令完成量化、優(yōu)化、編譯。校準(zhǔn)數(shù)據(jù)的準(zhǔn)備有個(gè)小技巧不要只用訓(xùn)練集里的圖片最好從驗(yàn)證集和實(shí)際場景里各取一些。我一開始只用訓(xùn)練集圖片結(jié)果量化后的模型在測試集上精度掉了5個(gè)點(diǎn)。后來混入了一些實(shí)際場景的圖片精度就恢復(fù)到了正常水平。4.2 yaml配置文件的關(guān)鍵參數(shù)配置文件是量化編譯的核心幾個(gè)關(guān)鍵參數(shù)需要特別注意model_parameters: onnx_model: yolov11.onnx output_model_file_prefix: yolov11_rdk march: bayes-e # RDK-X5的架構(gòu)代號 input_shape: 1x3x640x640 output_nodes: [output0, output1, output2] # 三個(gè)輸出節(jié)點(diǎn)名字 input_parameters: input_name: images input_type_train: rgb input_type_rt: nv12 # 板端輸入格式 mean_value: [0, 0, 0] scale_value: [0.003921568627, 0.003921568627, 0.003921568627] # 1/255 calibration_parameters: cal_data_dir: ./cal_data calibration_type: default max_percentile: 0.9999 compiler_parameters: compile_mode: latency optimize_level: O3 debug: Falsemarch參數(shù)RDK-X5用的是bayes-e這個(gè)不能寫錯(cuò)否則編譯出來的模型加載會失敗。input_type_rt板端輸入格式通常是nv12。如果你在板子上用opencv讀圖需要先轉(zhuǎn)成nv12格式再送進(jìn)模型。mean_value和scale_value這兩個(gè)要和訓(xùn)練時(shí)的預(yù)處理保持一致。YOLOv11默認(rèn)是0-1歸一化所以scale是1/255mean是0。calibration_type量化校準(zhǔn)方式default是默認(rèn)的KL散度校準(zhǔn)大部分情況夠用。如果精度不理想可以試試mix模式。4.3 編譯過程中的常見問題編譯階段最容易遇到的是算子不支持的問題。地平線工具鏈對算子的支持是有限的有些ONNX算子它不認(rèn)識。常見的解決辦法有算子替換把不支持的算子換成支持的等價(jià)算子。比如某些版本的HardSwish不支持可以換成ReLU或SiLU。算子融合把多個(gè)小算子融合成一個(gè)減少工具鏈的解析負(fù)擔(dān)。自定義算子如果實(shí)在找不到替代方案可以寫自定義算子但這個(gè)門檻比較高。我遇到過一個(gè)比較典型的問題YOLOv11的C2PSA模塊里有一個(gè)Split算子地平線工具鏈對Split的支持有問題導(dǎo)致編譯報(bào)錯(cuò)。解決辦法是在導(dǎo)出ONNX之前把C2PSA模塊替換成普通的C2f模塊。雖然會損失一點(diǎn)精度但至少能跑起來。注意替換模塊后需要重新訓(xùn)練或者微調(diào)否則精度會掉得比較厲害。如果時(shí)間緊可以先替換后直接量化看看精度是否可接受。4.4 板端部署與推理測試編譯完成后會得到一個(gè).bin文件把它拷貝到RDK-X5上用地平線的推理接口加載運(yùn)行。板端推理的代碼結(jié)構(gòu)大致如下from hobot_dnn import pyeasy_dnn as dnn import numpy as np import cv2 # 加載模型 models dnn.load(yolov11_rdk.bin) model models[0] # 讀取圖片并預(yù)處理 img cv2.imread(test.jpg) img cv2.resize(img, (640, 640)) img cv2.cvtColor(img, cv2.COLOR_BGR2NV12) # 推理 outputs model.forward(img) # 后處理 # 三個(gè)輸出分別對應(yīng)不同尺度的特征圖 # 需要自己實(shí)現(xiàn)解碼和NMS后處理部分需要自己寫因?yàn)榈仄骄€工具鏈不包含YOLO的解碼邏輯。這部分代碼比較長核心就是根據(jù)三個(gè)特征圖做sigmoid、解碼邊界框、然后NMS。網(wǎng)上有現(xiàn)成的RDK-X5 YOLO后處理代碼可以參考但要注意YOLOv11的輸出結(jié)構(gòu)和YOLOv5/v8略有不同不能直接套用。5. 實(shí)操中的經(jīng)驗(yàn)總結(jié)與避坑指南5.1 精度損失的排查思路量化后精度下降是常見問題排查思路如下先確認(rèn)ONNX模型本身精度正常用onnxruntime跑一遍和PyTorch的輸出對比。如果ONNX就已經(jīng)掉點(diǎn)了說明導(dǎo)出過程有問題。檢查校準(zhǔn)數(shù)據(jù)校準(zhǔn)數(shù)據(jù)的分布是否和實(shí)際場景匹配。如果校準(zhǔn)數(shù)據(jù)太單一量化后的模型泛化能力會很差。調(diào)整量化參數(shù)試試不同的calibration_type和max_percentile。max_percentile設(shè)得太小會導(dǎo)致截?cái)噙^多精度下降設(shè)得太大又會讓量化范圍過寬同樣影響精度?;旌狭炕绻承訉忍貏e敏感可以把這些層設(shè)為浮點(diǎn)計(jì)算其他層保持量化。地平線工具鏈支持這種混合量化配置。我實(shí)測下來YOLOv11在RDK-X5上量化后mAP大概會掉1-3個(gè)點(diǎn)。如果掉得更多就需要仔細(xì)排查了。5.2 性能優(yōu)化的幾個(gè)方向模型跑起來之后如果幀率不理想可以從這幾個(gè)方向優(yōu)化降低輸入分辨率從640降到416幀率能提升一倍左右但小目標(biāo)檢測精度會下降。減少類別數(shù)如果你只需要檢測少數(shù)幾類把類別數(shù)降下來能減少計(jì)算量。使用更小的模型YOLOv11n比YOLOv11s快很多如果精度要求不高直接用n版本。優(yōu)化后處理后處理代碼用C重寫或者用地平線提供的硬件加速接口。5.3 一些零散但重要的注意事項(xiàng)導(dǎo)出ONNX時(shí)關(guān)掉訓(xùn)練模式確保模型處于eval模式否則BN層和Dropout會影響導(dǎo)出結(jié)果。檢查輸入輸出名字地平線工具鏈對輸入輸出名字有要求最好在導(dǎo)出時(shí)就設(shè)好避免后面改來改去。保存好中間文件ONNX模型、校準(zhǔn)數(shù)據(jù)、yaml配置都留著后面調(diào)優(yōu)的時(shí)候還用得上。板端系統(tǒng)版本要匹配OE工具鏈版本和板子系統(tǒng)版本不對應(yīng)編譯出來的模型加載會失敗。這個(gè)坑我踩過排查了半天才發(fā)現(xiàn)是版本問題。5.4 完整代碼示例最后附上完整的導(dǎo)出腳本可以直接參考from ultralytics import YOLO import onnx import onnxsim # 加載模型 model YOLO(best.pt) # 導(dǎo)出ONNX model.export( formatonnx, imgsz640, opset11, simplifyFalse, # 先關(guān)掉后面手動簡化 dynamicFalse, halfFalse ) # 手動簡化 onnx_model onnx.load(best.onnx) onnx_model_sim, check onnxsim.simplify(onnx_model) assert check, 簡化失敗 onnx.save(onnx_model_sim, best_sim.onnx) print(導(dǎo)出完成)這段代碼導(dǎo)出的是單輸出模型還需要按照前面講的方法改成三輸出。完整的修改代碼比較長核心就是修改Detect類的forward方法這里就不全部貼出來了。6. 關(guān)于模型轉(zhuǎn)換的一些個(gè)人體會做RDK-X5的模型部署模型轉(zhuǎn)換這一步大概占了整個(gè)項(xiàng)目30%的時(shí)間。剩下的時(shí)間主要花在后處理調(diào)試和性能優(yōu)化上。如果你剛開始接觸地平線的工具鏈建議先用官方的YOLOv5示例跑通整個(gè)流程熟悉了之后再換成自己的模型。這樣遇到問題的時(shí)候至少能確定是模型本身的問題還是流程的問題。另外地平線的開發(fā)者社區(qū)比較活躍遇到報(bào)錯(cuò)可以先搜一下有沒有人遇到過類似問題。我遇到的幾個(gè)算子不支持的問題都是在社區(qū)里找到的解決方案。官方文檔雖然全面但有些細(xì)節(jié)寫得不夠清楚社區(qū)里的實(shí)戰(zhàn)經(jīng)驗(yàn)反而更有參考價(jià)值。YOLOv11相比YOLOv8在結(jié)構(gòu)上做了一些改動這些改動在GPU上跑沒什么問題但到了邊緣設(shè)備上就可能觸發(fā)各種兼容性問題。如果你對精度要求不是特別苛刻YOLOv8在RDK-X5上的部署方案更成熟踩的坑會少很多。但如果你就是想用YOLOv11那這篇文章里的經(jīng)驗(yàn)應(yīng)該能幫你少走一些彎路。