化實(shí)踐)
1. 問題背景與排查思路拆解1.1 項(xiàng)目場(chǎng)景與精度下降現(xiàn)象地平線 J6m 這顆芯片在邊緣側(cè)推理場(chǎng)景里算是比較熱門的選擇算力夠用、功耗控制得也不錯(cuò)很多做智能座艙、輔助駕駛、工業(yè)質(zhì)檢的團(tuán)隊(duì)都在用它跑視覺模型。我這次接到的任務(wù)是把一個(gè)已經(jīng)訓(xùn)練好的 YOLOv8s 檢測(cè)模型部署到 J6m 上走的是 INT8 量化路線目標(biāo)是拿到比 FP16 更低的延遲和更高的吞吐。模型轉(zhuǎn)換鏈路是標(biāo)準(zhǔn)的PyTorch 權(quán)重導(dǎo)出 ONNX再用地平線工具鏈做量化編譯最后生成板端可執(zhí)行的 hbm 模型。整個(gè)流程跑下來沒有報(bào)錯(cuò)板端也能正常出框但問題出在精度上——mAP 從 FP32 的 0.72 左右掉到了 0.51掉了二十多個(gè)點(diǎn)這個(gè)幅度已經(jīng)不能用“量化正常損失”來解釋了。正常 INT8 量化在檢測(cè)模型上掉 1 到 3 個(gè)點(diǎn)是可以接受的掉 20 個(gè)點(diǎn)說明某個(gè)環(huán)節(jié)出了結(jié)構(gòu)性問題。我先做了幾組對(duì)照實(shí)驗(yàn)來定位問題范圍。第一組把同一份 ONNX 在 PC 端用 ONNX Runtime 跑 FP32 推理結(jié)果和 PyTorch 對(duì)齊說明導(dǎo)出環(huán)節(jié)沒問題。第二組用工具鏈的校準(zhǔn)集做量化后在 PC 端模擬器上跑 INT8 推理精度就已經(jīng)掉了說明問題出在量化階段而不是板端部署階段。第三組把量化配置里的某些層強(qiáng)制回退到 FP16精度有明顯回升這就把嫌疑范圍縮小到了特定算子或特定結(jié)構(gòu)上。排查這類問題我的習(xí)慣是先建立一條“基準(zhǔn)鏈路”也就是確保 FP32 全鏈路對(duì)齊然后再逐段替換成 INT8看哪一段引入的誤差最大。很多人一上來就盯著量化參數(shù)調(diào)其實(shí)方向錯(cuò)了得先確認(rèn)模型結(jié)構(gòu)本身在轉(zhuǎn)換后有沒有變形。1.2 為什么 INT8 量化對(duì)檢測(cè)模型特別敏感要理解這次精度下降得先搞清楚 INT8 量化到底在做什么。簡(jiǎn)單說量化就是把 FP32 的浮點(diǎn)數(shù)值映射到 8 位整數(shù)區(qū)間用一個(gè) scale 和一個(gè) zero_point 來描述這個(gè)映射關(guān)系。對(duì)于權(quán)重來說這個(gè)映射是靜態(tài)的校準(zhǔn)階段就能確定對(duì)于激活值來說映射依賴輸入數(shù)據(jù)的分布所以需要校準(zhǔn)集來統(tǒng)計(jì)。檢測(cè)模型和分類模型不一樣它的輸出包含分類分支和回歸分支。分類分支對(duì)量化相對(duì)魯棒因?yàn)樽罱K取的是 argmax數(shù)值上的小擾動(dòng)不太影響類別判定。但回歸分支就麻煩了尤其是 YOLOv8 用的 DFLDistribution Focal Loss結(jié)構(gòu)它把邊界框回歸建模成一個(gè)離散分布每個(gè)邊對(duì)應(yīng)一組 logits最后通過期望計(jì)算得到連續(xù)坐標(biāo)。這個(gè)期望計(jì)算對(duì) logits 的數(shù)值精度非常敏感一旦量化誤差讓分布形狀發(fā)生偏移回歸出來的框就會(huì)明顯跑偏。另外YOLOv8 的檢測(cè)頭里還有 Sigmoid、Concat、Reshape 這些操作它們本身不引入量化誤差但會(huì)影響量化工具對(duì)前后算子的融合策略。如果融合沒做好中間就會(huì)插入額外的量化/反量化節(jié)點(diǎn)每一次轉(zhuǎn)換都會(huì)帶來精度損失。J6m 的工具鏈在這方面有自己的融合規(guī)則不是所有 ONNX 算子組合都能被完美識(shí)別。還有一個(gè)容易被忽略的點(diǎn)是校準(zhǔn)集的選擇。很多人隨便拿幾十張訓(xùn)練集圖片就當(dāng)校準(zhǔn)集用了但校準(zhǔn)集的數(shù)據(jù)分布必須和實(shí)際推理場(chǎng)景接近否則統(tǒng)計(jì)出來的激活范圍會(huì)偏導(dǎo)致量化后的數(shù)值大量落在有效區(qū)間之外。我這次一開始用的校準(zhǔn)集就是從訓(xùn)練集里隨機(jī)抽的后來換成驗(yàn)證集里更有代表性的樣本精度就有改善雖然沒完全解決問題但說明校準(zhǔn)集確實(shí)是個(gè)變量。1.3 排查路線從 ONNX 到板端的逐段驗(yàn)證我的排查路線分四步走。第一步確認(rèn) ONNX 模型本身的結(jié)構(gòu)和數(shù)值正確性用 Netron 看計(jì)算圖用 ONNX Runtime 跑數(shù)值對(duì)齊。第二步檢查量化配置重點(diǎn)看哪些層被量化、哪些層被跳過、校準(zhǔn)算法用的是什么。第三步在 PC 模擬器上做逐層誤差分析找出誤差最大的那幾個(gè)算子。第四步針對(duì)問題算子做結(jié)構(gòu)調(diào)整或量化策略調(diào)整重新編譯驗(yàn)證。這條路線的好處是每一步都有明確的輸入輸出不會(huì)陷入“改了參數(shù)看結(jié)果”的盲目試錯(cuò)。下面我按這個(gè)順序把每個(gè)環(huán)節(jié)的細(xì)節(jié)展開講。2. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)2.1 ONNX 導(dǎo)出環(huán)節(jié)的隱藏陷阱ONNX 導(dǎo)出看起來是最簡(jiǎn)單的一步但其實(shí)坑不少。YOLOv8 的官方導(dǎo)出腳本用的是ultralytics庫默認(rèn)導(dǎo)出的 ONNX 是 opset 17輸入是1x3x640x640。這里第一個(gè)要注意的是動(dòng)態(tài)軸的問題。如果你導(dǎo)出時(shí)開了 dynamic batchONNX 里會(huì)有dynamic_axes標(biāo)記地平線工具鏈對(duì)動(dòng)態(tài) shape 的支持有限可能會(huì)導(dǎo)致某些算子無法融合。我建議部署到 J6m 時(shí)先把 batch 固定成 1等精度對(duì)齊后再考慮動(dòng)態(tài) batch。第二個(gè)坑是 DFL 結(jié)構(gòu)的導(dǎo)出方式。YOLOv8 的檢測(cè)頭里DFL 模塊在 PyTorch 里是一個(gè)softmax加conv再加sum的組合導(dǎo)出到 ONNX 后可能變成SoftmaxConvReduceSum或者被優(yōu)化成MatMul形式。不同導(dǎo)出方式對(duì)量化的友好程度不一樣。我實(shí)測(cè)下來如果 DFL 被導(dǎo)出成Softmax后面直接接Conv量化工具會(huì)把 Softmax 的輸出當(dāng)成激活值來統(tǒng)計(jì)而 Softmax 輸出的分布非常集中量化 scale 會(huì)偏小導(dǎo)致后續(xù) Conv 的輸入精度損失嚴(yán)重。第三個(gè)坑是后處理。有些導(dǎo)出腳本會(huì)把 NMS 也導(dǎo)進(jìn) ONNX這樣模型輸出就是最終框。但地平線工具鏈對(duì) NMS 的支持有自己的實(shí)現(xiàn)如果你在 ONNX 里帶了 NMS工具鏈可能會(huì)用自定義算子替換替換過程中量化策略可能不一致。我的做法是 ONNX 只導(dǎo)出到檢測(cè)頭輸出NMS 放在板端用 CPU 做這樣量化范圍可控。檢查 ONNX 結(jié)構(gòu)我一般用兩個(gè)工具Netron 看圖結(jié)構(gòu)ONNX Runtime 跑數(shù)值。Netron 里重點(diǎn)看有沒有多余的QuantizeLinear/DequantizeLinear節(jié)點(diǎn)如果有說明導(dǎo)出時(shí)已經(jīng)帶了量化信息需要清理掉重新導(dǎo)出。ONNX Runtime 那邊我會(huì)寫一個(gè)簡(jiǎn)單的 Python 腳本把 PyTorch 的輸出和 ONNX 的輸出做逐元素對(duì)比確保 max diff 在 1e-4 以內(nèi)。import onnxruntime as ort import torch import numpy as np # PyTorch 推理 model torch.load(yolov8s.pt) model.eval() dummy torch.randn(1, 3, 640, 640) with torch.no_grad(): pt_out model(dummy).numpy() # ONNX Runtime 推理 sess ort.InferenceSession(yolov8s.onnx) onnx_out sess.run(None, {images: dummy.numpy()})[0] # 數(shù)值對(duì)比 diff np.abs(pt_out - onnx_out) print(fmax diff: {diff.max():.6f}, mean diff: {diff.mean():.6f})這段腳本跑下來如果 max diff 超過 1e-3就說明導(dǎo)出有問題得回去檢查導(dǎo)出參數(shù)。我這次跑出來是 2e-5說明導(dǎo)出環(huán)節(jié)是干凈的。2.2 量化配置的關(guān)鍵參數(shù)解讀地平線工具鏈的量化配置一般是一個(gè) YAML 文件里面有幾個(gè)關(guān)鍵字段需要重點(diǎn)關(guān)注。第一個(gè)是calibration_type常見的有kl、max、percentile幾種。KL 散度校準(zhǔn)適合激活分布比較集中的模型max 校準(zhǔn)簡(jiǎn)單粗暴但對(duì)異常值敏感percentile 是折中方案。YOLOv8 的激活分布在不同層差異很大我建議先用percentile跑一版再對(duì)比kl的效果。第二個(gè)是per_channel開關(guān)。權(quán)重量化默認(rèn)是 per-channel 的也就是每個(gè)輸出通道有獨(dú)立的 scale這對(duì)精度有好處。但激活量化通常是 per-tensor 的因?yàn)榧せ钪翟谕评頃r(shí)是動(dòng)態(tài)的沒法提前知道每個(gè)通道的范圍。如果你發(fā)現(xiàn)某一層的激活量化誤差特別大可以考慮把這層加到skip_layers里讓它保持 FP16。第三個(gè)是skip_layers列表。這個(gè)列表里的層不會(huì)被量化保持浮點(diǎn)精度。我這次排查發(fā)現(xiàn)把 DFL 相關(guān)的幾個(gè) Conv 層加進(jìn)去后精度從 0.51 回升到了 0.63說明這些層確實(shí)是精度瓶頸。但全部跳過又會(huì)導(dǎo)致速度下降太多所以需要在精度和速度之間找平衡。第四個(gè)是calibration_dataset的配置。校準(zhǔn)集一般準(zhǔn)備 100 到 500 張圖片太少統(tǒng)計(jì)不準(zhǔn)太多沒必要。圖片要從實(shí)際推理場(chǎng)景里選覆蓋不同的光照、角度、目標(biāo)尺度。我這次用了 200 張驗(yàn)證集圖片比之前隨機(jī)抽的 50 張訓(xùn)練集圖片效果好很多。還有一個(gè)細(xì)節(jié)是input_type和input_shape的配置。J6m 工具鏈要求輸入是 NHWC 格式而 ONNX 默認(rèn)是 NCHW轉(zhuǎn)換過程中會(huì)插入 transpose 操作。如果 transpose 的位置不對(duì)可能會(huì)影響前后算子的融合。我建議在導(dǎo)出 ONNX 時(shí)就把輸入改成 NHWC或者在量化配置里明確指定 transpose 的位置。2.3 DFL 結(jié)構(gòu)對(duì)量化的影響機(jī)制DFL 是這次問題的核心值得單獨(dú)拿出來講。YOLOv8 的邊界框回歸不是直接預(yù)測(cè)四個(gè)坐標(biāo)值而是預(yù)測(cè)每個(gè)邊的離散分布。具體來說每個(gè)邊預(yù)測(cè) 16 個(gè) logits經(jīng)過 Softmax 變成概率分布然后用0, 1, 2, ..., 15做加權(quán)求和得到期望值也就是最終的坐標(biāo)偏移。這個(gè)結(jié)構(gòu)對(duì)量化的敏感性來自兩個(gè)方面。第一Softmax 的輸出是概率值范圍在 0 到 1 之間而且大部分值集中在很小的區(qū)間里。量化工具在校準(zhǔn)時(shí)會(huì)統(tǒng)計(jì) Softmax 輸出的范圍如果大部分值都在 0.01 到 0.1 之間scale 就會(huì)設(shè)得很小導(dǎo)致量化后的分辨率不夠概率分布的細(xì)節(jié)丟失。第二加權(quán)求和那一步涉及乘加運(yùn)算如果 logits 的量化誤差導(dǎo)致概率分布偏移期望值就會(huì)偏離真實(shí)值而且這個(gè)偏差會(huì)隨著分布寬度的增加而放大。我做過一個(gè)實(shí)驗(yàn)把 DFL 的 Softmax 輸出單獨(dú)拿出來看量化前后的差異。量化前某個(gè)邊的概率分布峰值在 index 7 附近量化后峰值跑到了 index 5期望值差了將近 2 個(gè)像素。對(duì)于小目標(biāo)來說2 個(gè)像素的偏差可能就導(dǎo)致框完全偏出目標(biāo)。解決思路有幾個(gè)方向。一是把 DFL 的 Softmax 和后續(xù)的加權(quán)求和整體作為一個(gè)自定義算子不讓量化工具拆開處理。地平線工具鏈支持自定義算子注冊(cè)但需要寫一些配置代碼。二是把 DFL 相關(guān)的層全部跳過量化保持 FP16代價(jià)是這部分計(jì)算會(huì)慢一些但 DFL 的計(jì)算量本身不大對(duì)整體延遲影響有限。三是在訓(xùn)練階段做量化感知訓(xùn)練QAT讓模型提前適應(yīng)量化誤差但這次項(xiàng)目時(shí)間緊沒來得及做 QAT。我最后采用的是第二種方案把 DFL 的四個(gè)輸出分支對(duì)應(yīng)的 Conv 層加到skip_layers里同時(shí)把 Softmax 也跳過。這樣精度回升到了 0.68雖然還沒到 FP32 的 0.72但已經(jīng)可以接受了。剩下的 4 個(gè)點(diǎn)損失主要來自 backbone 和 neck 部分的量化這部分用 percentile 校準(zhǔn)后還能再找回 1 到 2 個(gè)點(diǎn)。2.4 校準(zhǔn)集構(gòu)建的實(shí)操經(jīng)驗(yàn)校準(zhǔn)集這塊我踩過坑值得多說幾句。一開始我圖省事直接從訓(xùn)練集里隨機(jī)抽了 50 張圖片結(jié)果量化后精度只有 0.51。后來分析發(fā)現(xiàn)訓(xùn)練集里的圖片分布很不均勻有些場(chǎng)景的圖片特別多有些場(chǎng)景幾乎沒有導(dǎo)致校準(zhǔn)統(tǒng)計(jì)出來的激活范圍偏向某些特定場(chǎng)景。第二次我改用驗(yàn)證集因?yàn)轵?yàn)證集的分布更接近實(shí)際測(cè)試場(chǎng)景。但驗(yàn)證集只有 200 張我全用了。效果有改善精度到了 0.55但還是不夠。第三次我做了一個(gè)改進(jìn)把驗(yàn)證集按場(chǎng)景分類每個(gè)場(chǎng)景抽等量的圖片保證校準(zhǔn)集的分布均衡。同時(shí)我還在校準(zhǔn)前對(duì)圖片做了和推理時(shí)一樣的前處理包括 resize、歸一化、通道轉(zhuǎn)換確保輸入分布一致。這里有個(gè)細(xì)節(jié)容易被忽略校準(zhǔn)時(shí)的前處理必須和推理時(shí)完全一致。如果你在校準(zhǔn)時(shí)用了 letterbox 填充推理時(shí)也必須是 letterbox如果你在校準(zhǔn)時(shí)把圖片歸一化到 0 到 1推理時(shí)也得一樣。我見過有人校準(zhǔn)時(shí)用 0 到 255 的原始像素推理時(shí)用 0 到 1 的歸一化值量化 scale 直接偏了 255 倍精度不掉才怪。另外校準(zhǔn)集的數(shù)量也不是越多越好。我試過用 500 張和 200 張的效果差不多但校準(zhǔn)時(shí)間多了不少。一般來說100 到 300 張足夠覆蓋主要場(chǎng)景了。如果模型特別大或者場(chǎng)景特別復(fù)雜可以適當(dāng)增加但沒必要超過 500 張。還有一個(gè)技巧是校準(zhǔn)集的多樣性。除了不同場(chǎng)景還要覆蓋不同的目標(biāo)尺度、不同的光照條件、不同的遮擋程度。我這次在校準(zhǔn)集里特意加入了一些小目標(biāo)密集的圖片和一些低光照?qǐng)D片因?yàn)檫@些場(chǎng)景下激活值的分布和普通場(chǎng)景差異很大如果不覆蓋量化后這些場(chǎng)景的精度會(huì)掉得特別厲害。3. 實(shí)操過程與核心環(huán)節(jié)實(shí)現(xiàn)3.1 環(huán)境準(zhǔn)備與工具鏈版本確認(rèn)動(dòng)手之前先把環(huán)境理清楚這一步很多人會(huì)跳過但版本不匹配導(dǎo)致的玄學(xué)問題特別多。我這次用的工具鏈?zhǔn)堑仄骄€官方發(fā)布的版本具體版本號(hào)在hb_mapper的--version里能看到。ONNX 的版本是 1.14ONNX Runtime 是 1.15PyTorch 是 2.0。這幾個(gè)版本之間的兼容性我提前查過沒有已知的沖突。工具鏈安裝完之后先跑一個(gè)官方的示例模型確認(rèn)整個(gè)流程能走通。官方示例一般是一個(gè)簡(jiǎn)單的分類模型量化后精度損失很小。如果示例都跑不通那說明環(huán)境有問題先解決環(huán)境再搞自己的模型。我這次跑示例花了大概半小時(shí)確認(rèn)沒問題后才開始處理 YOLOv8s。環(huán)境準(zhǔn)備里還有一個(gè)容易忽略的點(diǎn)是 Python 環(huán)境。地平線工具鏈對(duì) Python 版本有要求一般是 3.8 到 3.10。我建議用 conda 建一個(gè)獨(dú)立環(huán)境避免和系統(tǒng)里的其他包沖突。依賴包也要按官方文檔的版本裝不要圖省事直接pip install最新版版本不匹配的報(bào)錯(cuò)往往很難排查。3.2 ONNX 模型導(dǎo)出與結(jié)構(gòu)檢查導(dǎo)出 ONNX 我用的是ultralytics庫自帶的導(dǎo)出腳本命令很簡(jiǎn)單yolo export modelyolov8s.pt formatonnx imgsz640 opset17 simplifyTrue這里simplifyTrue會(huì)調(diào)用onnx-simplifier做圖優(yōu)化把一些冗余算子合并掉。但要注意simplify有時(shí)候會(huì)把 DFL 的結(jié)構(gòu)改掉導(dǎo)致量化工具識(shí)別不了。我建議先導(dǎo)出一版不 simplify 的用 Netron 看清楚原始結(jié)構(gòu)再導(dǎo)出一版 simplify 的做對(duì)比。如果 simplify 后結(jié)構(gòu)變化太大就用手動(dòng)方式做必要的優(yōu)化。導(dǎo)出完成后用 Netron 打開 ONNX 文件重點(diǎn)看幾個(gè)地方。第一輸入節(jié)點(diǎn)的 shape 是不是1x3x640x640數(shù)據(jù)類型是不是 float32。第二檢測(cè)頭的輸出是不是三個(gè)分支分別對(duì)應(yīng) stride 8、16、32。第三DFL 部分的結(jié)構(gòu)是不是Softmax后面接Conv或者M(jìn)atMul。第四有沒有多余的Cast、Reshape、Transpose節(jié)點(diǎn)這些節(jié)點(diǎn)會(huì)影響融合。我這次導(dǎo)出的 ONNX 里DFL 部分是一個(gè)Softmax接一個(gè)ConvConv 的權(quán)重是1x16x1x1相當(dāng)于對(duì) 16 個(gè)通道做加權(quán)求和。這個(gè)結(jié)構(gòu)本身沒問題但量化工具會(huì)把 Softmax 的輸出當(dāng)成激活值來統(tǒng)計(jì)這就是精度損失的來源。檢查完結(jié)構(gòu)后跑一遍 ONNX Runtime 的數(shù)值對(duì)齊確認(rèn)和 PyTorch 的輸出一致。我前面給的腳本可以直接用注意輸入名稱要和 ONNX 里的輸入節(jié)點(diǎn)名稱一致。如果 max diff 在 1e-4 以內(nèi)就可以進(jìn)入下一步了。3.3 量化配置編寫與校準(zhǔn)執(zhí)行量化配置我寫了一個(gè) YAML 文件核心內(nèi)容如下model: input_type: nhwc input_shape: [1, 640, 640, 3] output_names: [output0, output1, output2] calibration: calibration_type: percentile percentile: 99.99 calibration_dataset: ./calib_images num_samples: 200 quantization: per_channel: true skip_layers: - /model.22/cv2.0/cv2.0.2/Conv - /model.22/cv2.1/cv2.1.2/Conv - /model.22/cv2.2/cv2.2.2/Conv - /model.22/cv3.0/cv3.0.2/Conv - /model.22/cv3.1/cv3.1.2/Conv - /model.22/cv3.2/cv3.2.2/Conv這里的skip_layers就是 DFL 相關(guān)的 Conv 層。層名稱可以從 Netron 里復(fù)制注意要寫完整的路徑。calibration_type我選了percentilepercentile值設(shè)成 99.99意思是把激活值范圍的第 99.99 百分位作為量化上限這樣可以過濾掉極端異常值。校準(zhǔn)集的準(zhǔn)備我寫了一個(gè)腳本把驗(yàn)證集圖片按場(chǎng)景分類每個(gè)場(chǎng)景抽等量圖片統(tǒng)一 resize 到 640x640做 letterbox 填充歸一化到 0 到 1最后保存成 numpy 數(shù)組或者圖片文件。工具鏈一般支持直接讀圖片文件夾但要求圖片格式和推理時(shí)一致。執(zhí)行量化的命令是hb_mapper makertbin --config yolov8s_config.yaml --model-type onnx這個(gè)過程會(huì)先做校準(zhǔn)統(tǒng)計(jì)每一層的激活范圍然后生成量化模型。校準(zhǔn)時(shí)間取決于模型大小和校準(zhǔn)集數(shù)量YOLOv8s 加 200 張圖片大概跑了 10 分鐘左右。跑完后會(huì)生成一個(gè)量化后的 ONNX 或者 hbm 文件還有一個(gè)精度報(bào)告。精度報(bào)告里會(huì)列出每一層的量化誤差重點(diǎn)看cosine_similarity和max_abs_error這兩個(gè)指標(biāo)。如果某一層的 cosine similarity 低于 0.99說明這層的量化誤差比較大可以考慮加到skip_layers里。我這次報(bào)告里 DFL 相關(guān)層的 cosine similarity 只有 0.95 左右其他層都在 0.99 以上這就驗(yàn)證了我的判斷。3.4 板端部署與精度驗(yàn)證量化完成后把生成的 hbm 模型推到板端用地平線的推理接口加載。板端推理的代碼框架官方有示例主要是初始化模型、準(zhǔn)備輸入、執(zhí)行推理、解析輸出這幾步。輸入數(shù)據(jù)要和校準(zhǔn)時(shí)一致NHWC 格式float32 類型歸一化到 0 到 1。精度驗(yàn)證我用的是 COCO 驗(yàn)證集的一個(gè)子集大概 500 張圖片覆蓋不同的目標(biāo)類別和尺度。評(píng)測(cè)指標(biāo)用 mAP0.5 和 mAP0.5:0.95。FP32 的基準(zhǔn)是 0.72 和 0.52量化后如果 mAP0.5 能到 0.68 以上mAP0.5:0.95 能到 0.48 以上就算達(dá)標(biāo)了。我這次最終跑出來是 mAP0.5 為 0.68mAP0.5:0.95 為 0.47比 FP32 分別掉了 4 個(gè)點(diǎn)和 5 個(gè)點(diǎn)。這個(gè)損失在 INT8 量化里算是正常范圍尤其是檢測(cè)模型。如果業(yè)務(wù)對(duì)精度要求特別高可以考慮混合量化也就是部分層 INT8、部分層 FP16但速度會(huì)有所下降。板端推理的延遲我也測(cè)了INT8 比 FP16 快了大概 40%單幀推理時(shí)間從 12ms 降到了 7ms 左右。這個(gè)提升對(duì)于實(shí)時(shí)性要求高的場(chǎng)景很有價(jià)值比如 30fps 的視頻流FP16 可能勉強(qiáng)夠用INT8 就有足夠的余量了。4. 常見問題與排查技巧實(shí)錄4.1 精度下降問題的速查表排查精度問題我整理了一個(gè)速查表按可能性從高到低排列遇到問題可以按這個(gè)順序查。問題現(xiàn)象可能原因排查方法解決思路精度掉 20 個(gè)點(diǎn)以上模型結(jié)構(gòu)在轉(zhuǎn)換后變形Netron 對(duì)比 ONNX 和原始模型結(jié)構(gòu)重新導(dǎo)出關(guān)閉 simplify精度掉 10 到 20 個(gè)點(diǎn)DFL 或 Softmax 層量化誤差大看精度報(bào)告里的 cosine similarity把這些層加入 skip_layers精度掉 5 到 10 個(gè)點(diǎn)校準(zhǔn)集分布不匹配檢查校準(zhǔn)集和推理輸入的前處理重新構(gòu)建校準(zhǔn)集精度掉 3 到 5 個(gè)點(diǎn)量化參數(shù)不合適對(duì)比不同 calibration_type換用 percentile 或 kl精度掉 1 到 3 個(gè)點(diǎn)正常量化損失無需排查可接受或做 QAT 進(jìn)一步優(yōu)化板端和模擬器精度不一致板端前處理或后處理有差異對(duì)比板端和 PC 端的輸入輸出統(tǒng)一前處理和后處理邏輯這個(gè)表是我踩了多次坑之后總結(jié)的基本上覆蓋了常見的精度問題。實(shí)際排查時(shí)先看精度掉了多少然后按表里的順序查能省不少時(shí)間。4.2 幾個(gè)容易忽略的細(xì)節(jié)問題第一個(gè)細(xì)節(jié)是輸入數(shù)據(jù)的 layout。ONNX 默認(rèn)是 NCHW但 J6m 工具鏈要求 NHWC。如果你在量化配置里寫了input_type: nhwc但 ONNX 模型本身是 NCHW工具鏈會(huì)自動(dòng)插入 transpose。這個(gè) transpose 的位置很關(guān)鍵如果插在了量化節(jié)點(diǎn)之后可能會(huì)導(dǎo)致量化 scale 不匹配。我建議在導(dǎo)出 ONNX 時(shí)就把輸入改成 NHWC或者在量化配置里明確指定 transpose 的位置。第二個(gè)細(xì)節(jié)是輸出節(jié)點(diǎn)的選擇。YOLOv8 的 ONNX 輸出可能有多個(gè)節(jié)點(diǎn)包括檢測(cè)頭的原始輸出和后處理后的輸出。如果你把后處理也導(dǎo)進(jìn)了 ONNX量化工具可能會(huì)對(duì) NMS 部分做量化而 NMS 涉及排序和閾值比較量化后行為可能不一致。我的做法是只導(dǎo)出檢測(cè)頭的原始輸出NMS 放在板端用 CPU 做。第三個(gè)細(xì)節(jié)是校準(zhǔn)集的圖片格式。工具鏈一般支持 JPG、PNG 等常見格式但要求圖片的通道順序和模型輸入一致。如果你用 OpenCV 讀圖片默認(rèn)是 BGR 順序而模型訓(xùn)練時(shí)用的是 RGB這就會(huì)導(dǎo)致顏色通道錯(cuò)位量化 scale 偏掉。我建議在校準(zhǔn)腳本里顯式做 BGR 到 RGB 的轉(zhuǎn)換確保和訓(xùn)練時(shí)一致。第四個(gè)細(xì)節(jié)是量化后的模型大小。INT8 量化后模型大小應(yīng)該比 FP32 小 4 倍左右如果發(fā)現(xiàn)模型大小沒怎么變說明量化沒生效可能配置寫錯(cuò)了或者工具鏈沒識(shí)別到。我這次量化后模型從 22MB 降到了 6MB 左右符合預(yù)期。4.3 混合量化的取舍經(jīng)驗(yàn)混合量化是精度和速度之間的折中方案。我的經(jīng)驗(yàn)是優(yōu)先把以下幾類層保持 FP16一是 DFL 相關(guān)的層前面已經(jīng)講過原因二是檢測(cè)頭的最后幾層因?yàn)檫@些層直接決定輸出精度三是模型里數(shù)值范圍特別大的層比如某些激活函數(shù)輸出范圍很寬的層。哪些層可以放心用 INT8 呢backbone 里的卷積層一般沒問題因?yàn)樘卣鲌D的數(shù)值分布比較穩(wěn)定neck 里的上采樣和 concat 層也可以量化這些操作本身不引入數(shù)值誤差還有一些 BN 層通常會(huì)被融合到卷積里不需要單獨(dú)考慮。混合量化的配置方法是在skip_layers里列出要保持 FP16 的層。但要注意跳過的層越多速度越慢。我這次跳過了 6 個(gè) DFL 相關(guān)的 Conv 層速度比全 INT8 慢了大概 15%但精度回升了 17 個(gè)點(diǎn)這個(gè) trade-off 是值得的。如果業(yè)務(wù)對(duì)速度要求特別高可以考慮只跳過最關(guān)鍵的 2 到 3 層其他層用 INT8。具體跳過哪幾層可以看精度報(bào)告里的 cosine similarity優(yōu)先跳過 similarity 最低的那幾層。我這次試過只跳過 3 層精度是 0.64速度比跳過 6 層快了 8% 左右也算是一個(gè)可選的方案。4.4 從這次排查中總結(jié)的實(shí)操心得這次排查花了大概三天時(shí)間踩了不少坑也積累了一些經(jīng)驗(yàn)。第一個(gè)心得是遇到精度問題不要急著調(diào)量化參數(shù)先確認(rèn)模型結(jié)構(gòu)在轉(zhuǎn)換后有沒有變形。我一開始就是直接調(diào)calibration_type從max換到kl再換到percentile精度只波動(dòng)了 1 到 2 個(gè)點(diǎn)根本問題沒解決。后來用 Netron 仔細(xì)看結(jié)構(gòu)才發(fā)現(xiàn) DFL 部分的量化策略有問題。第二個(gè)心得是校準(zhǔn)集的質(zhì)量比數(shù)量重要。我試過用 500 張隨機(jī)圖片效果不如 200 張精心挑選的圖片。校準(zhǔn)集要覆蓋實(shí)際推理場(chǎng)景的主要變化包括光照、角度、目標(biāo)尺度、遮擋程度等。如果場(chǎng)景特別復(fù)雜可以分場(chǎng)景構(gòu)建多個(gè)校準(zhǔn)集分別量化后對(duì)比效果。第三個(gè)心得是精度報(bào)告要認(rèn)真看。工具鏈生成的精度報(bào)告里有很多有用信息比如每一層的 cosine similarity、max abs error、量化前后的數(shù)值范圍對(duì)比。這些信息能幫你快速定位問題層而不是盲目試錯(cuò)。我這次就是通過精度報(bào)告發(fā)現(xiàn) DFL 層的 similarity 只有 0.95其他層都在 0.99 以上才確定了問題方向。第四個(gè)心得是板端驗(yàn)證不能省。PC 模擬器的結(jié)果和板端有時(shí)候會(huì)有差異尤其是涉及自定義算子或者特殊融合策略的時(shí)候。我這次在 PC 模擬器上精度是 0.68板端跑出來也是 0.68說明一致性沒問題。但如果模擬器和板端不一致就要檢查板端的前處理和后處理邏輯看看有沒有和 PC 端不一樣的地方。第五個(gè)心得是保留一份完整的排查記錄。我這次把每一步的配置、命令、輸出、精度結(jié)果都記下來了包括失敗的嘗試。這樣后面再遇到類似問題可以直接翻記錄不用重新踩坑。而且記錄本身也能幫你理清思路避免在多個(gè)變量之間來回切換。4.5 后續(xù)可優(yōu)化的方向這次排查解決了主要問題但還有一些優(yōu)化空間。第一個(gè)方向是 QAT也就是量化感知訓(xùn)練。在訓(xùn)練階段就模擬量化誤差讓模型提前適應(yīng)這樣量化后的精度損失會(huì)更小。QAT 一般能比 PTQ 多找回 2 到 3 個(gè)點(diǎn)但需要重新訓(xùn)練模型時(shí)間成本比較高。第二個(gè)方向是自定義算子。地平線工具鏈支持注冊(cè)自定義算子可以把 DFL 的 Softmax 和加權(quán)求和整體作為一個(gè)算子避免量化工具拆開處理。這樣既能保持 INT8 的速度又能避免精度損失。但自定義算子的開發(fā)需要熟悉工具鏈的算子接口有一定的學(xué)習(xí)成本。第三個(gè)方向是校準(zhǔn)算法的改進(jìn)。除了 percentile 和 kl還有一些更高級(jí)的校準(zhǔn)算法比如基于熵的校準(zhǔn)、基于梯度的校準(zhǔn)等。這些算法在某些模型上效果更好但需要額外的實(shí)現(xiàn)和驗(yàn)證。如果項(xiàng)目對(duì)精度要求特別高可以嘗試這些方法。第四個(gè)方向是模型結(jié)構(gòu)的調(diào)整。如果 DFL 結(jié)構(gòu)對(duì)量化太敏感可以考慮在訓(xùn)練階段換一種回歸方式比如直接回歸坐標(biāo)值而不是用分布。但這樣會(huì)改變模型結(jié)構(gòu)需要重新訓(xùn)練和驗(yàn)證適合新項(xiàng)目而不是已有項(xiàng)目的優(yōu)化。這次排查讓我對(duì)邊緣側(cè) INT8 量化有了更深的理解尤其是檢測(cè)模型和分類模型的差異。分類模型量化后精度損失通常很小但檢測(cè)模型因?yàn)榛貧w分支的存在對(duì)量化誤差更敏感。DFL 這種結(jié)構(gòu)雖然提升了檢測(cè)精度但也增加了量化的難度。后續(xù)再做類似項(xiàng)目我會(huì)在模型選型階段就考慮量化友好性盡量選擇對(duì)量化不敏感的結(jié)構(gòu)。