端側(cè)AI:為什么我回頭重學(xué)CNN,反而少走了三年彎路)
1. 為什么在端側(cè) AI 時(shí)代回頭啃 CNN 不是走彎路很多人聽說(shuō)我在嵌入式崗位上干了幾年之后反而掉頭去把卷積神經(jīng)網(wǎng)絡(luò)從最底層重新學(xué)一遍第一反應(yīng)都是你是不是搞反了方向。畢竟現(xiàn)在打開任何一個(gè)技術(shù)社區(qū)滿屏都是大模型、Transformer、端側(cè)部署量化推理這些詞CNN 看起來(lái)像是上個(gè)時(shí)代的產(chǎn)物。但我自己踩過(guò)一圈坑之后越來(lái)越確信在嵌入式到端側(cè) AI 的這條路徑上CNN 不是過(guò)時(shí)的東西而是繞不開的地基。先說(shuō)清楚這個(gè)判斷的背景。嵌入式開發(fā)和端側(cè) AI 部署本質(zhì)上都受同一組約束支配算力有限、內(nèi)存有限、功耗有限、實(shí)時(shí)性要求高。你在服務(wù)器上可以隨手堆幾百億參數(shù)但在一個(gè)只有幾百 KB RAM、主頻幾百 MHz 的 MCU 上或者在一個(gè)算力被嚴(yán)格限制的邊緣設(shè)備上每一個(gè)乘加運(yùn)算、每一次內(nèi)存搬運(yùn)都要精打細(xì)算。CNN 恰好是理解計(jì)算量從哪來(lái)、內(nèi)存帶寬怎么被吃掉、精度和速度怎么權(quán)衡這件事最直觀的載體。它的結(jié)構(gòu)足夠簡(jiǎn)單簡(jiǎn)單到你可以在紙上把一層卷積的乘加次數(shù)算清楚又足夠典型典型到幾乎所有端側(cè)推理框架的優(yōu)化手段——算子融合、權(quán)重量化、內(nèi)存復(fù)用、Winograd 變換——都是圍繞它發(fā)展出來(lái)的。我決定重新從 CNN 開始還有一個(gè)很現(xiàn)實(shí)的原因端側(cè)部署里 80% 的坑根子都在對(duì)模型計(jì)算過(guò)程的理解不夠。你如果只是會(huì)調(diào)庫(kù)、會(huì)跑現(xiàn)成的推理腳本一旦遇到精度掉點(diǎn)、推理變慢、內(nèi)存溢出就完全不知道從哪下手。而當(dāng)你親手把一層卷積的前向傳播、反向傳播、量化誤差都推導(dǎo)過(guò)一遍之后再看那些部署工具報(bào)出來(lái)的日志你會(huì)突然發(fā)現(xiàn)每一條都在說(shuō)人話。這篇文章我想聊的不是CNN 是什么這種教科書內(nèi)容而是站在一個(gè)從嵌入式轉(zhuǎn)過(guò)來(lái)的人的角度把 CNN 和端側(cè) AI 之間那條真實(shí)的連接線講清楚為什么嵌入式背景的人學(xué) CNN 有獨(dú)特優(yōu)勢(shì)、CNN 的核心計(jì)算到底怎么落到硬件上、量化為什么是端側(cè)的生命線、以及從零搭一個(gè)能在端側(cè)跑起來(lái)的小模型需要經(jīng)歷哪些環(huán)節(jié)。適合正在做嵌入式、想往端側(cè) AI 轉(zhuǎn)或者已經(jīng)在做端側(cè)部署但總覺得根基不牢的朋友。2. 嵌入式背景學(xué) CNN優(yōu)勢(shì)比你想的大得多2.1 你早就懂資源約束這門課做嵌入式的人有一個(gè)天然優(yōu)勢(shì)就是對(duì)資源約束有肌肉記憶。你寫單片機(jī)代碼的時(shí)候會(huì)下意識(shí)地關(guān)心這個(gè)變量占幾個(gè)字節(jié)、這個(gè)循環(huán)會(huì)不會(huì)被編譯器優(yōu)化掉、這段代碼放在 Flash 還是 RAM 里。這種思維習(xí)慣遷移到 CNN 上簡(jiǎn)直是降維打擊。舉個(gè)具體的例子。一個(gè)普通的卷積層輸入特征圖是 32×32×16卷積核 3×3輸出通道 32。很多人第一反應(yīng)是這不就是個(gè)矩陣乘法嘛但如果你有嵌入式背景你會(huì)立刻開始算這一層有多少次乘加輸出特征圖每個(gè)點(diǎn)要做 3×3×16 144 次乘加輸出一共 32×32×32 32768 個(gè)點(diǎn)所以總共約 471 萬(wàn)次乘加。如果每個(gè)乘加按一次 MAC 算在只有幾十 MHz 主頻、沒有硬件加速的 MCU 上這一層就要跑幾十毫秒。這個(gè)數(shù)字一出來(lái)你就知道為什么端側(cè)模型必須做通道剪枝、必須做量化了。這種先算賬再動(dòng)手的習(xí)慣是很多從純算法轉(zhuǎn)過(guò)來(lái)的人欠缺的。他們習(xí)慣性地認(rèn)為算力是無(wú)限的直到部署的時(shí)候才發(fā)現(xiàn)模型根本跑不動(dòng)。而你從一開始就會(huì)把算力預(yù)算當(dāng)成設(shè)計(jì)約束這讓你在設(shè)計(jì)模型結(jié)構(gòu)時(shí)就有天然的克制。2.2 內(nèi)存布局和訪存模式嵌入式人一看就懂CNN 在硬件上跑得快不快很多時(shí)候不取決于計(jì)算量而取決于訪存模式。這一點(diǎn)嵌入式開發(fā)者理解起來(lái)特別快因?yàn)槟阏{(diào)過(guò) DMA、寫過(guò)緩存友好的代碼、知道什么叫內(nèi)存對(duì)齊。卷積的計(jì)算有個(gè)特點(diǎn)計(jì)算密度其實(shí)不高但訪存很頻繁。以 NHWC 和 NCHW 兩種常見的內(nèi)存布局為例同一個(gè)卷積在不同布局下的訪存效率可能差好幾倍。NCHW 布局下通道維是連續(xù)的做逐通道卷積時(shí)訪存很友好但 NHWC 布局下空間維連續(xù)做 3×3 卷積時(shí)相鄰像素的讀取更連續(xù)。端側(cè)推理框架之所以要針對(duì)不同硬件選擇不同布局本質(zhì)就是在優(yōu)化訪存局部性。我在實(shí)際調(diào)試一個(gè)端側(cè)模型時(shí)遇到過(guò)這樣的情況同一個(gè)模型只是把輸入張量的內(nèi)存布局從 NCHW 換成 NHWC在某個(gè)邊緣設(shè)備上的推理時(shí)間就從 45ms 降到了 28ms。原因就是這個(gè)設(shè)備的緩存行是 64 字節(jié)NHWC 布局下連續(xù)讀取的像素能更好地填滿緩存行減少了緩存未命中。這種優(yōu)化思路做過(guò)嵌入式內(nèi)存優(yōu)化的人幾乎是本能反應(yīng)。2.3 定點(diǎn)數(shù)思維讓你對(duì)量化不再恐懼端側(cè) AI 繞不開量化而量化本質(zhì)上就是把浮點(diǎn)運(yùn)算換成定點(diǎn)運(yùn)算。這件事對(duì)嵌入式開發(fā)者來(lái)說(shuō)太熟悉了——你在 MCU 上做傳感器數(shù)據(jù)處理的時(shí)候早就習(xí)慣了用 Q 格式定點(diǎn)數(shù)來(lái)避免浮點(diǎn)運(yùn)算的開銷。量化的核心是把 FP32 的權(quán)重和激活值映射到 INT8 的整數(shù)域。聽起來(lái)復(fù)雜但如果你理解定點(diǎn)數(shù)的定標(biāo)原理就會(huì)覺得這不過(guò)是換了個(gè) scale 而已。一個(gè) FP32 數(shù)值 x 量化成 INT8 的過(guò)程是先確定一個(gè)縮放因子 s然后 q round(x / s)反量化時(shí) x q × s。這個(gè) s 怎么選、怎么保證量化后精度損失最小就是量化算法的核心。而嵌入式開發(fā)者對(duì)精度和范圍的權(quán)衡這件事早就在 ADC 采樣、PWM 占空比這些場(chǎng)景里練過(guò)無(wú)數(shù)遍了。提示如果你之前沒接觸過(guò)定點(diǎn)數(shù)建議先花半天時(shí)間把 Q 格式比如 Q15、Q7的加減乘除和溢出處理搞清楚再回頭看量化會(huì)發(fā)現(xiàn)概念是通的。3. 把 CNN 的計(jì)算拆到硬件層面才算真正理解它3.1 一層卷積到底在算什么很多人學(xué) CNN 的時(shí)候腦子里是一張張漂亮的示意圖但真到了部署階段示意圖幫不上忙。你需要把卷積拆成硬件能執(zhí)行的原子操作。一個(gè)標(biāo)準(zhǔn)卷積層輸入張量形狀是 [H, W, C_in]卷積核是 [K, K, C_in, C_out]輸出是 [H_out, W_out, C_out]。它的計(jì)算可以拆成四層循環(huán)遍歷輸出通道、遍歷輸出空間位置、遍歷卷積核空間位置、遍歷輸入通道。最內(nèi)層的操作就是一次乘加acc input[...] * weight[...]。這個(gè)四層循環(huán)的循環(huán)順序直接決定了訪存效率和能不能做并行。比如把輸出通道放在最外層那么每個(gè)輸出通道的計(jì)算可以獨(dú)立進(jìn)行適合多核并行把輸入通道放在最內(nèi)層那么內(nèi)層循環(huán)可以連續(xù)讀取輸入和權(quán)重適合向量化指令。端側(cè)推理框架里的im2col GEMM策略本質(zhì)就是把卷積展開成矩陣乘法然后調(diào)用高度優(yōu)化的矩陣乘法庫(kù)這樣就能復(fù)用成熟的 BLAS 優(yōu)化。但 im2col 有個(gè)代價(jià)它會(huì)復(fù)制輸入數(shù)據(jù)把內(nèi)存占用放大 K×K 倍。在內(nèi)存緊張的端側(cè)設(shè)備上這個(gè)代價(jià)有時(shí)候不能接受。所以很多輕量級(jí)框架會(huì)選擇直接卷積direct convolution雖然計(jì)算效率低一點(diǎn)但省內(nèi)存。這就是一個(gè)典型的端側(cè)權(quán)衡用計(jì)算換內(nèi)存還是用內(nèi)存換計(jì)算。3.2 感受野、步長(zhǎng)、填充這些參數(shù)怎么影響端側(cè)性能感受野、步長(zhǎng)、填充這些概念在算法層面是理解模型表達(dá)能力的關(guān)鍵但在端側(cè)部署層面它們直接決定了計(jì)算量和內(nèi)存占用。步長(zhǎng)stride是最直接的影響因素。步長(zhǎng)為 1 的卷積輸出空間尺寸基本不變步長(zhǎng)為 2 的卷積輸出尺寸減半計(jì)算量直接降到四分之一。所以在端側(cè)模型里早期層經(jīng)常用步長(zhǎng) 2 來(lái)快速降分辨率減少后續(xù)層的計(jì)算量。但步長(zhǎng)太大會(huì)丟失空間信息影響精度這個(gè)平衡點(diǎn)需要根據(jù)具體任務(wù)調(diào)。填充padding影響的是邊界處理。SAME padding 會(huì)讓輸出尺寸和輸入一致VALID padding 則會(huì)讓輸出縮小。在端側(cè)padding 的處理方式會(huì)影響內(nèi)存分配——SAME padding 需要在輸入邊界補(bǔ)零如果實(shí)現(xiàn)得不好會(huì)產(chǎn)生額外的內(nèi)存拷貝。有些框架會(huì)通過(guò)隱式 padding來(lái)避免實(shí)際拷貝只在計(jì)算時(shí)判斷邊界這樣能省下不少內(nèi)存。感受野則是理解模型看到多大范圍的關(guān)鍵。在端側(cè)做目標(biāo)檢測(cè)或者語(yǔ)義分割時(shí)感受野不夠大會(huì)導(dǎo)致模型無(wú)法理解全局上下文。但擴(kuò)大感受野的手段——堆更多層、用空洞卷積、用大卷積核——都會(huì)增加計(jì)算量。所以端側(cè)模型設(shè)計(jì)的一個(gè)核心矛盾就是在有限算力下怎么用最少的計(jì)算獲得足夠大的感受野。MobileNet 用深度可分離卷積、ShuffleNet 用通道混洗本質(zhì)上都是在解這個(gè)矛盾。3.3 從乘加次數(shù)到實(shí)際延遲中間隔著什么理論上算出來(lái)的乘加次數(shù)MACs和實(shí)際推理延遲之間往往差著好幾倍。這個(gè)差距來(lái)自哪里是端側(cè)部署必須搞清楚的。第一是內(nèi)存帶寬。很多端側(cè)設(shè)備的算力其實(shí)不弱但內(nèi)存帶寬是瓶頸。卷積層的計(jì)算密度每讀一個(gè)字節(jié)能做多少次運(yùn)算如果太低那實(shí)際性能就被內(nèi)存帶寬卡死而不是被算力卡死。這就是為什么深度可分離卷積雖然 MACs 大幅降低但在某些設(shè)備上加速比并沒有理論值那么高——因?yàn)樗挠?jì)算密度更低更容易受內(nèi)存帶寬限制。第二是算子啟動(dòng)開銷。每個(gè)算子卷積、激活、池化在硬件上執(zhí)行都有啟動(dòng)開銷算子越多開銷累積越明顯。所以端側(cè)推理框架會(huì)做算子融合把卷積 BN 激活融合成一個(gè)算子減少中間結(jié)果的寫回和讀取。這個(gè)優(yōu)化在理論上不減少 MACs但實(shí)際延遲能降 20% 到 40%。第三是數(shù)據(jù)搬運(yùn)。模型權(quán)重從 Flash 加載到 RAM、中間特征圖在不同內(nèi)存層級(jí)之間搬運(yùn)這些都有開銷。端側(cè)設(shè)備的內(nèi)存層級(jí)往往比服務(wù)器簡(jiǎn)單但 Flash 讀取速度慢這個(gè)問(wèn)題很突出。所以權(quán)重量化不僅是為了減少計(jì)算也是為了減少權(quán)重體積降低加載開銷。影響因素對(duì)延遲的影響典型優(yōu)化手段內(nèi)存帶寬計(jì)算密度低時(shí)成為主瓶頸提高計(jì)算密度、算子融合算子啟動(dòng)開銷算子數(shù)量多時(shí)累積明顯算子融合、圖優(yōu)化權(quán)重加載Flash 讀取慢時(shí)突出權(quán)重量化、權(quán)重常駐 RAM緩存未命中訪存模式差時(shí)嚴(yán)重調(diào)整內(nèi)存布局、分塊計(jì)算4. 量化端側(cè) AI 的生命線也是理解 CNN 的最佳切口4.1 為什么端側(cè)幾乎必然要量化在端側(cè)設(shè)備上量化不是可選優(yōu)化而是能不能跑起來(lái)的問(wèn)題。原因有三層。最直接的是內(nèi)存。一個(gè) FP32 的權(quán)重占 4 字節(jié)量化成 INT8 只占 1 字節(jié)模型體積直接降到四分之一。對(duì)于一個(gè)幾 MB 的模型這意味著能不能塞進(jìn)設(shè)備的 Flash 里。很多低端 MCU 的 Flash 只有 512KB 到 1MB不量化根本放不下。其次是計(jì)算速度。INT8 乘加在大多數(shù)端側(cè)硬件上比 FP32 快得多。有些 DSP 或者 NPU 有專門的 INT8 加速單元吞吐量是 FP32 的好幾倍。即使沒有專用硬件INT8 運(yùn)算的功耗也更低這對(duì)電池供電的設(shè)備至關(guān)重要。第三是功耗。浮點(diǎn)運(yùn)算的能耗遠(yuǎn)高于整數(shù)運(yùn)算。有研究數(shù)據(jù)表明一次 FP32 乘加的能耗大約是一次 INT8 乘加的十幾倍。在功耗預(yù)算嚴(yán)格的端側(cè)場(chǎng)景量化帶來(lái)的功耗下降是決定性的。4.2 訓(xùn)練后量化與量化感知訓(xùn)練怎么選量化有兩條主要路線訓(xùn)練后量化PTQ和量化感知訓(xùn)練QAT。PTQ 是在模型訓(xùn)練完成之后用一批校準(zhǔn)數(shù)據(jù)統(tǒng)計(jì)激活值的分布然后確定量化參數(shù)。它的優(yōu)點(diǎn)是簡(jiǎn)單快捷不需要重新訓(xùn)練適合快速驗(yàn)證。缺點(diǎn)是精度損失可能比較大尤其是對(duì)那些激活值分布不均勻的模型。PTQ 又分動(dòng)態(tài)量化和靜態(tài)量化動(dòng)態(tài)量化在推理時(shí)實(shí)時(shí)計(jì)算激活的量化參數(shù)靈活但開銷大靜態(tài)量化提前算好參數(shù)推理時(shí)直接用效率高但需要校準(zhǔn)數(shù)據(jù)。QAT 是在訓(xùn)練過(guò)程中就模擬量化誤差讓模型學(xué)會(huì)適應(yīng)量化。它的精度通常比 PTQ 好尤其是量化到 INT8 甚至更低比特時(shí)。代價(jià)是需要重新訓(xùn)練訓(xùn)練過(guò)程也更復(fù)雜要在前向傳播里插入偽量化節(jié)點(diǎn)。我的經(jīng)驗(yàn)是如果模型本身比較小、結(jié)構(gòu)比較規(guī)整PTQ 通常夠用如果模型對(duì)精度敏感或者要量化到 8 比特以下QAT 更穩(wěn)。在端側(cè)實(shí)際項(xiàng)目里我一般先用 PTQ 快速跑通流程看精度掉多少如果掉點(diǎn)在可接受范圍內(nèi)就直接用如果掉太多再上 QAT。4.3 量化誤差從哪來(lái)怎么定位量化誤差主要來(lái)自三個(gè)地方權(quán)重量化誤差、激活量化誤差、以及兩者疊加后的累積誤差。權(quán)重量化誤差相對(duì)好控制因?yàn)闄?quán)重是靜態(tài)的可以提前分析分布選擇合適的量化范圍。激活量化誤差更麻煩因?yàn)榧せ钪凳莿?dòng)態(tài)的不同輸入的激活分布可能差別很大。如果校準(zhǔn)數(shù)據(jù)的分布和實(shí)際推理數(shù)據(jù)不一致激活量化誤差就會(huì)很大。定位量化誤差我常用的方法是逐層對(duì)比把 FP32 模型和量化模型在同一個(gè)輸入上的中間層輸出都 dump 出來(lái)逐層算余弦相似度或者相對(duì)誤差。哪一層的誤差突然變大問(wèn)題就出在哪一層。常見的原因是那一層的激活值有長(zhǎng)尾分布或者有極端值把量化范圍撐大了導(dǎo)致大部分值被壓縮到很小的量化區(qū)間里。解決手段也有幾種一是對(duì)那一層單獨(dú)用更高的比特混合精度量化二是對(duì)激活值做截?cái)喟褬O端值裁掉犧牲一點(diǎn)精度換整體量化效果三是調(diào)整那一層的量化粒度從 per-tensor 改成 per-channel。注意per-channel 量化對(duì)權(quán)重的效果通常比 per-tensor 好很多因?yàn)椴煌敵鐾ǖ赖臋?quán)重分布差異可能很大。但 per-channel 量化會(huì)增加推理時(shí)的開銷因?yàn)槊總€(gè)通道要用不同的 scale。在端側(cè)要權(quán)衡。5. 從零搭一個(gè)能上端側(cè)的小 CNN完整實(shí)操鏈路5.1 先定約束再設(shè)計(jì)結(jié)構(gòu)端側(cè)模型設(shè)計(jì)的第一步不是畫網(wǎng)絡(luò)結(jié)構(gòu)而是把約束列清楚。我通常會(huì)問(wèn)自己幾個(gè)問(wèn)題目標(biāo)設(shè)備的 RAM 有多大、Flash 有多大、有沒有硬件加速單元、推理延遲要求是多少、精度底線是多少。這些約束定下來(lái)之后模型結(jié)構(gòu)其實(shí)就八九不離十了。舉個(gè)例子假設(shè)目標(biāo)設(shè)備有 256KB RAM、1MB Flash、沒有 NPU、要求單幀推理在 50ms 以內(nèi)。那么模型參數(shù)量大概要控制在 100K 到 300K 之間中間特征圖峰值內(nèi)存不能超過(guò) 200KB?;谶@些約束我會(huì)選擇深度可分離卷積作為基本模塊早期層用步長(zhǎng) 2 快速降分辨率通道數(shù)從 16 開始逐步增加到 128 左右最后用全局平均池化代替全連接層來(lái)減少參數(shù)。這個(gè)設(shè)計(jì)過(guò)程不是拍腦袋而是每一步都有約束在背后撐著。通道數(shù)為什么從 16 開始因?yàn)榈谝粚虞斎胪ǖ劳ǔJ?1 或 3輸出通道 16 已經(jīng)能提取足夠的初級(jí)特征再大就浪費(fèi)。為什么用全局平均池化因?yàn)槿B接層的參數(shù)量通常是整個(gè)模型的大頭去掉它能省下大量 Flash。5.2 訓(xùn)練階段的幾個(gè)關(guān)鍵設(shè)置端側(cè)模型的訓(xùn)練和普通模型訓(xùn)練有些不一樣的地方這些細(xì)節(jié)直接影響后面能不能順利量化。首先是權(quán)重衰減。端側(cè)模型通常比較小容易過(guò)擬合所以權(quán)重衰減要設(shè)得合理。但權(quán)重衰減太強(qiáng)會(huì)讓權(quán)重分布過(guò)于集中反而不利于量化。我的經(jīng)驗(yàn)是權(quán)重衰減設(shè)在 1e-4 到 1e-5 之間比較穩(wěn)。其次是BN 層的處理。BN 層在訓(xùn)練時(shí)很有用但推理時(shí)會(huì)被融合進(jìn)卷積。融合的前提是 BN 的參數(shù)已經(jīng)穩(wěn)定所以訓(xùn)練時(shí)要確保 BN 的移動(dòng)平均更新充分。如果訓(xùn)練輪數(shù)太少BN 統(tǒng)計(jì)量不準(zhǔn)融合后精度會(huì)掉。我一般會(huì)在訓(xùn)練后期凍結(jié) BN 的統(tǒng)計(jì)量再跑幾輪微調(diào)。第三是激活函數(shù)的選擇。ReLU 是最省算力的但它的輸出沒有上界量化時(shí)不好確定范圍。ReLU6 把輸出限制在 0 到 6 之間量化友好得多這也是 MobileNet 系列用 ReLU6 的原因。如果對(duì)精度要求高可以用帶參數(shù)的激活函數(shù)但要注意它們對(duì)量化的友好程度。5.3 量化實(shí)操?gòu)?FP32 到 INT8 的完整流程下面是我常用的量化流程以靜態(tài) PTQ 為例。第一步準(zhǔn)備校準(zhǔn)數(shù)據(jù)。校準(zhǔn)數(shù)據(jù)不需要標(biāo)注但分布要盡量接近實(shí)際推理數(shù)據(jù)。一般準(zhǔn)備 100 到 500 個(gè)樣本就夠了。數(shù)據(jù)太少校準(zhǔn)不準(zhǔn)太多浪費(fèi)時(shí)間。第二步插入觀測(cè)節(jié)點(diǎn)。在模型里插入統(tǒng)計(jì)激活值分布的節(jié)點(diǎn)跑一遍校準(zhǔn)數(shù)據(jù)收集每一層激活的最小值、最大值或者直方圖。第三步計(jì)算量化參數(shù)。根據(jù)收集到的分布用 KL 散度或者最小化均方誤差的方法確定每一層的 scale 和 zero_point。KL 散度方法會(huì)找一個(gè)截?cái)帱c(diǎn)把長(zhǎng)尾部分裁掉讓量化范圍更緊湊。第四步轉(zhuǎn)換模型。把 FP32 的權(quán)重和激活都轉(zhuǎn)成 INT8同時(shí)把卷積、BN、激活融合成量化算子。第五步驗(yàn)證精度。在測(cè)試集上跑量化模型對(duì)比 FP32 模型的精度。如果掉點(diǎn)超過(guò)閾值回到第三步調(diào)整量化策略。# 量化流程的偽代碼示意 def quantize_model(fp32_model, calib_loader): # 1. 插入觀測(cè)節(jié)點(diǎn) model prepare_for_quantization(fp32_model) # 2. 跑校準(zhǔn)數(shù)據(jù)收集分布 for batch in calib_loader: model(batch) # 3. 計(jì)算量化參數(shù)并轉(zhuǎn)換 quantized_model convert_to_int8(model) # 4. 驗(yàn)證 acc_fp32 evaluate(fp32_model, test_loader) acc_int8 evaluate(quantized_model, test_loader) print(fFP32 acc: {acc_fp32}, INT8 acc: {acc_int8}) return quantized_model5.4 部署到端側(cè)后的性能調(diào)優(yōu)模型量化完、轉(zhuǎn)換完只是走完了一半。真正部署到設(shè)備上之后還有一輪性能調(diào)優(yōu)。首先要確認(rèn)推理框架用的是不是針對(duì)目標(biāo)硬件優(yōu)化過(guò)的版本。同一個(gè)模型用通用框架和用廠商優(yōu)化過(guò)的框架性能可能差好幾倍。廠商的框架通常會(huì)針對(duì)自家硬件的緩存大小、DMA 特性、加速單元做專門優(yōu)化。其次是內(nèi)存規(guī)劃。端側(cè)推理框架一般會(huì)做內(nèi)存復(fù)用把不同層的中間特征圖分配到同一塊內(nèi)存上因?yàn)樗鼈兊纳芷诓恢丿B。但內(nèi)存復(fù)用算法有好有壞好的算法能把峰值內(nèi)存壓到理論下限附近。如果發(fā)現(xiàn)內(nèi)存占用比預(yù)期高可以檢查一下框架的內(nèi)存規(guī)劃策略。第三是線程和并行。如果設(shè)備是多核的要確認(rèn)推理框架有沒有用上多核。有些框架默認(rèn)單線程需要手動(dòng)開啟多線程。但多線程也不是越多越好線程太多會(huì)有調(diào)度開銷在核數(shù)少的設(shè)備上反而變慢。最后是輸入預(yù)處理和后處理。這兩部分經(jīng)常被忽略但在端側(cè)它們可能占總延遲的很大比例。比如圖像 resize、歸一化、NMS 這些操作如果實(shí)現(xiàn)得不好可能比模型推理本身還慢。我遇到過(guò)預(yù)處理占了總延遲 40% 的情況后來(lái)把 resize 換成硬件加速才解決。6. 那些只有踩過(guò)才知道的端側(cè) CNN 坑6.1 精度掉點(diǎn)不一定是量化的鍋有一次我量化完一個(gè)模型精度掉了 5 個(gè)百分點(diǎn)第一反應(yīng)是量化策略有問(wèn)題調(diào)了半天量化參數(shù)都沒用。后來(lái)逐層對(duì)比才發(fā)現(xiàn)問(wèn)題出在 BN 融合上——訓(xùn)練時(shí) BN 的移動(dòng)平均沒更新充分融合后統(tǒng)計(jì)量偏移導(dǎo)致精度掉。重新訓(xùn)練并凍結(jié) BN 統(tǒng)計(jì)量之后量化精度只掉了 0.5 個(gè)點(diǎn)。這個(gè)坑讓我養(yǎng)成了一個(gè)習(xí)慣精度掉點(diǎn)先查 BN 和算子融合再查量化本身。因?yàn)?BN 融合是確定性的數(shù)學(xué)變換如果實(shí)現(xiàn)正確不應(yīng)該掉精度一旦掉了說(shuō)明 BN 參數(shù)本身有問(wèn)題。6.2 校準(zhǔn)數(shù)據(jù)的分布比數(shù)量重要另一個(gè)常見的坑是校準(zhǔn)數(shù)據(jù)選得不對(duì)。我見過(guò)有人用訓(xùn)練集的前 100 張圖做校準(zhǔn)結(jié)果這 100 張圖恰好都是某個(gè)類別的激活分布嚴(yán)重偏斜量化后其他類別的精度慘不忍睹。校準(zhǔn)數(shù)據(jù)的核心要求是分布覆蓋不是數(shù)量。100 張覆蓋各類別、各場(chǎng)景的圖比 1000 張同質(zhì)化的圖效果好得多。如果實(shí)在不確定可以用聚類的方法從訓(xùn)練集里挑代表性樣本。6.3 端側(cè)設(shè)備的標(biāo)稱算力要打折扣設(shè)備規(guī)格書上的算力數(shù)字和實(shí)際能跑出來(lái)的性能往往差很遠(yuǎn)。標(biāo)稱 1 TOPS 的設(shè)備實(shí)際跑 CNN 可能只能發(fā)揮 20% 到 30%。原因很多內(nèi)存帶寬跟不上、算子支持不全、調(diào)度開銷大。所以選型的時(shí)候不要只看標(biāo)稱算力要看實(shí)際跑目標(biāo)模型的 benchmark。如果廠商提供了 benchmark 數(shù)據(jù)要看它跑的是什么模型、什么精度、什么 batch size。條件允許的話最好自己拿目標(biāo)模型實(shí)測(cè)一下。6.4 別忽略數(shù)值穩(wěn)定性端側(cè)量化模型在極端輸入下可能出現(xiàn)數(shù)值溢出。比如某個(gè)激活值在訓(xùn)練集里最大是 10量化范圍定到 10但實(shí)際推理時(shí)來(lái)了一個(gè)激活值 15 的輸入量化后就溢出了輸出完全錯(cuò)誤。解決辦法是在確定量化范圍時(shí)留一點(diǎn)余量或者用飽和處理。但余量留太多會(huì)浪費(fèi)量化精度這個(gè)平衡要根據(jù)實(shí)際場(chǎng)景調(diào)。如果應(yīng)用場(chǎng)景輸入可控余量可以留小一點(diǎn)如果輸入不可控余量要留足。7. 回頭看這條路該怎么走從嵌入式轉(zhuǎn)到端側(cè) AI再回頭把 CNN 重新啃一遍我最大的體會(huì)是這兩件事本來(lái)就是一件事。嵌入式的資源約束思維、定點(diǎn)數(shù)思維、訪存優(yōu)化思維和端側(cè) AI 的量化、算子融合、內(nèi)存規(guī)劃底層邏輯是通的。CNN 只是把這些思維應(yīng)用到了一個(gè)具體的計(jì)算模式上。如果你也在走類似的路我的建議是不要急著去追最新的模型結(jié)構(gòu)先把一個(gè)最簡(jiǎn)單的 CNN 從訓(xùn)練到量化到部署完整走一遍。走完之后你對(duì)端側(cè) AI 的理解會(huì)比看十篇綜述都扎實(shí)。具體來(lái)說(shuō)可以拿一個(gè)經(jīng)典的圖像分類任務(wù)自己搭一個(gè)幾層的小 CNN訓(xùn)練到能用的精度然后量化、轉(zhuǎn)換、部署到一個(gè)真實(shí)的端側(cè)設(shè)備上測(cè)延遲、測(cè)內(nèi)存、測(cè)精度。這個(gè)過(guò)程中遇到的每一個(gè)問(wèn)題都是真正長(zhǎng)本事的時(shí)刻。至于后續(xù)往哪個(gè)方向深入我的看法是先把 CNN 這條線走透再去看 Transformer 在端側(cè)的部署。因?yàn)槎藗?cè) Transformer 的很多優(yōu)化手段——量化、算子融合、內(nèi)存復(fù)用——都是從 CNN 時(shí)代積累下來(lái)的。地基打牢了上面蓋什么樓都穩(wěn)。