計(jì)實(shí)戰(zhàn):從算子映射到INT8量化落地全解析)
做硬件加速這幾年來(lái)被問(wèn)得最多的一個(gè)問(wèn)題就是既然 CPU 和 GPU 都能跑模型為什么還要自己折騰 AI 推理加速器一開(kāi)始我也覺(jué)得這是個(gè)偽需求直到真在邊緣側(cè)設(shè)備上部署過(guò)幾個(gè)模型才明白——通用處理器要么算力不夠要么功耗壓不住真正要對(duì)功耗、時(shí)延和成本同時(shí)敏感的批量部署場(chǎng)景一塊精心設(shè)計(jì)的推理加速器往往比什么“通用算力神話”都靠譜。這篇就圍繞 AI 推理加速器的完整設(shè)計(jì)與優(yōu)化過(guò)程把我踩過(guò)的坑、驗(yàn)證過(guò)的方案和具體實(shí)操步驟攤開(kāi)講一講。這篇文章適合誰(shuí)看如果你正要做 FPGA 或?qū)S眯酒系纳窠?jīng)網(wǎng)絡(luò)推理部署或者你只是想在嵌入式設(shè)備上把模型跑得更快不希望只是調(diào)庫(kù)對(duì)參數(shù)那這里面講的設(shè)計(jì)思路、算子映射方法、量化落地細(xì)節(jié)和調(diào)試經(jīng)驗(yàn)應(yīng)該都能直接用上。很多內(nèi)容不區(qū)分具體芯片平臺(tái)只需要理解基本的計(jì)算機(jī)組成結(jié)構(gòu)就能跟著推演一遍。1. 設(shè)計(jì)思路拆解先搞清楚到底要加速什么1.1 瓶頸在哪算力、訪存還是控制開(kāi)銷推理加速器的設(shè)計(jì)第一件事不是選芯片、不是寫(xiě) RTL而是先把模型的計(jì)算畫(huà)像算清楚。我說(shuō)的“計(jì)算畫(huà)像”不是看多少個(gè)浮點(diǎn)運(yùn)算而是靜態(tài)分析每個(gè)算子到底卡在“算”還是“搬”。以卷積層為例。假設(shè)輸入特征圖是 112x112通道數(shù) 32用 64 個(gè) 3x3 卷積核、stride 2 做卷積輸出就變成 56x56x64。這層的乘累加MAC次數(shù)大約是565664323*3也就是 1.8 億次 MAC。如果目標(biāo)吞吐率是 1G MAC/s那么光這一層的計(jì)算時(shí)間就是 180ms——這是純計(jì)算視角。但換個(gè)角度看內(nèi)存訪問(wèn)量輸入特征圖大小是 11211232每個(gè)數(shù)假設(shè)是 INT8也就是 401KB。64 個(gè) 3x3x32 的卷積核權(quán)重一共是 18KB。輸出特征圖是 565664共 200KB。單層的片上搬運(yùn)量大概 600KB 多。如果帶寬是 12.8GB/s比如常見(jiàn)的 DDR4 單通道搬運(yùn)時(shí)間也就 47us 左右。在這個(gè)例子里把數(shù)據(jù)反復(fù)地從 DDR 搬進(jìn)搬出反而遠(yuǎn)超計(jì)算時(shí)間。這就是典型的訪存密集型算子。我自己的習(xí)慣是拿到網(wǎng)絡(luò)結(jié)構(gòu)之后先把每一層的 MAC 總數(shù)、特征圖尺寸變化、權(quán)重體積、是否有點(diǎn)乘或 concat 這類高訪存算子列一張表。然后粗略算一個(gè)“算術(shù)強(qiáng)度”計(jì)算量除以訪存量。算術(shù)強(qiáng)度高的層是計(jì)算受限低的屬于訪存受限。加速器設(shè)計(jì)初期起碼要做一件事優(yōu)先優(yōu)化訪存受限的算子而不是盲目堆 MAC 陣列。提示很多人一開(kāi)始就興致勃勃地把卷積算子在硬件上改成最復(fù)雜的脈動(dòng)陣列結(jié)果發(fā)現(xiàn)整個(gè)網(wǎng)絡(luò)里只有三成時(shí)間花在卷積上另外一半時(shí)間卡在 Pooling、Resize 和 concat 的數(shù)據(jù)搬運(yùn)上這時(shí)候陣列做得再快也換不來(lái)整體性能。先分析再動(dòng)手這一條原則在任何硬件項(xiàng)目里都適用。1.2 計(jì)算與數(shù)據(jù)流架構(gòu)選型脈動(dòng)陣列、SIMD 還是混合方式明確了瓶頸下一步就是選架構(gòu)。業(yè)內(nèi)常見(jiàn)的推理加速器數(shù)據(jù)流方案大概可以分三類。第一類是 CPU 式的 SIMD也就是把乘加單元排成一排配合向量寄存器做單指令多數(shù)據(jù)。這種方案控制靈活可以處理各種形狀不規(guī)整的算子但需要復(fù)雜的指令調(diào)度和編譯器支持對(duì)算力利用率來(lái)說(shuō)不是最極致的選擇。第二類是脈動(dòng)陣列Systolic Array讓數(shù)據(jù)在陣列里規(guī)則流動(dòng)一個(gè)數(shù)據(jù)能被多次復(fù)用因此訪存壓力小功耗控制得也好。缺點(diǎn)是控制邏輯相對(duì)固定一旦遇到不規(guī)整的結(jié)構(gòu)比如 1x1 卷積和全連接其實(shí)很規(guī)整但各種 depthwise 卷積、稀疏化的模型就很容易出現(xiàn)空閑單元利用率大打折扣。第三類就是干脆做異構(gòu)組合主體用 SIMD 或者脈動(dòng)陣列跑主計(jì)算再配一塊通用的控制邏輯和 DMA 引擎專門(mén)處理數(shù)據(jù)搬運(yùn)和特殊算子。這屬于工程化比較務(wù)實(shí)的方案適用范圍廣但設(shè)計(jì)復(fù)雜度也最高。我在實(shí)際項(xiàng)目里做過(guò)一個(gè)偏邊緣的加速器原型我們內(nèi)部叫它“模擬項(xiàng)目 X”最開(kāi)始采用了一組 16x16 的脈動(dòng)陣列跑標(biāo)準(zhǔn)卷積效果不錯(cuò)但模型里一旦出現(xiàn) depthwise 卷積比如移動(dòng)端網(wǎng)絡(luò)里的 DSCNN 結(jié)構(gòu)利用率和算力優(yōu)勢(shì)立刻垮掉。后來(lái)改成 8 個(gè) SIMD 計(jì)算核配一個(gè)小規(guī)模陣列的組合方式配合專門(mén)的數(shù)據(jù)搬運(yùn)指令整體吞吐反而高出將近 40%。所以架構(gòu)選型不要只看“峰值算力多高”而是要看“實(shí)際模型的結(jié)構(gòu)到底適配哪種數(shù)據(jù)流”。如果是網(wǎng)絡(luò)結(jié)構(gòu)固定的產(chǎn)品算力很密集脈動(dòng)陣列可以做得很穩(wěn)如果是多模型多形狀的平臺(tái)化需求異構(gòu)組合往往更現(xiàn)實(shí)。2. 核心計(jì)算模塊細(xì)節(jié)算子映射與量化方案如何落地2.1 把卷積拆成硬件能吃的循環(huán)結(jié)構(gòu)神經(jīng)網(wǎng)絡(luò)算子映射到硬件本質(zhì)上是對(duì)循環(huán)做變換。卷積的計(jì)算其實(shí)是一個(gè)五層循環(huán)遍歷輸出通道 k、輸入通道 c、輸出高度 h、輸出寬度 w、還有卷積分量 kh 和 kw。硬件上不可能一次性把所有這些循環(huán)都無(wú)限制展開(kāi)所以必須做分塊tiling。舉個(gè)例子設(shè)計(jì)一個(gè)加速器時(shí)我常先把卷積抽象成三個(gè)循環(huán)塊外層是輸出通道循環(huán)把它分成若干 block中間層是輸入通道循環(huán)做輸入特征圖和權(quán)重的分批加載內(nèi)層是輸出圖上的空間循環(huán)決定 MAC 單元怎么并行、數(shù)據(jù)怎么復(fù)用。為什么這么切因?yàn)檩敵鐾ǖ罌Q定了權(quán)重能不能駐留在片上。假設(shè)計(jì)算陣列寬度是 16輸出通道按 16 一個(gè) block 切那么 3x3x32 的卷積核一次只需要加載 16323*3 4.6KB 的權(quán)重只要 BRAM 放得下整個(gè) block 就只用從 DDR 讀一次權(quán)重。這在外設(shè)帶寬有限的系統(tǒng)里能省下非常多時(shí)間。數(shù)據(jù)流方面也都已經(jīng)總結(jié)出一個(gè)“stationary”的說(shuō)法。數(shù)據(jù)的駐留方式?jīng)Q定了你復(fù)用哪個(gè)數(shù)據(jù)Weight Stationary把權(quán)重固定在一組計(jì)算單元里反復(fù)使用適合權(quán)重小、特征圖大的卷積Output Stationary把部分和固定在寄存器里適合減少寫(xiě)回訪存Row Stationary輸入按行流動(dòng)對(duì)于不規(guī)則窗口和 MISD多指令多數(shù)據(jù)類運(yùn)算更友好。從我自己的設(shè)計(jì)經(jīng)驗(yàn)看輸出駐留往往對(duì)邊緣側(cè)推理加速器最直觀因?yàn)榫矸e的中間累加結(jié)果一旦搬回 DDR 就是無(wú)謂的帶寬浪費(fèi)。把部分和直接留在片上等整個(gè)輸出通道 block 累加完再一次性寫(xiě)回是最容易實(shí)現(xiàn)也最穩(wěn)的方案。2.2 INT8 量化的工程實(shí)現(xiàn)細(xì)節(jié)模型量化是推理加速器里繞不開(kāi)的核心環(huán)節(jié)。FP32 權(quán)重和激活值需要 32 位存儲(chǔ)和處理硬件面積和帶寬成本都太大FP16 對(duì)于芯片工藝來(lái)說(shuō)也不算省多數(shù)邊緣/嵌入式推理場(chǎng)景真正實(shí)用的是 INT8 甚至更低比特。量化落地有兩個(gè)關(guān)鍵點(diǎn)要講清楚。第一個(gè)是量化參數(shù)怎么確定。最常用的方案是非對(duì)稱線性量化即q round(r / scale) zero_point其中 scale 由量化前后數(shù)值范圍決定zero_point 把實(shí)數(shù) 0 映射到整數(shù)。這個(gè)映射關(guān)系需要在硬件里做一個(gè)減法再做一個(gè)乘法通常由加速器的預(yù)處理單元來(lái)做。scale 的確定有兩種主流手段直接根據(jù)權(quán)重張量的 min/max 計(jì)算這個(gè)簡(jiǎn)單但對(duì)激活值誤差較大用一套校準(zhǔn)數(shù)據(jù)集跑一遍網(wǎng)絡(luò)統(tǒng)計(jì)激活值的分布再用 KL 散度或者百分位數(shù)找閾值。我實(shí)際用過(guò)第二種方式效果比 min/max 穩(wěn)定得多。拿一個(gè)目標(biāo)檢測(cè)模型舉例校準(zhǔn)集用 200 張代表性輸入圖激活值量化的精度損失從 2.3% 降到 0.6% 左右。第二點(diǎn)是硬件上必須處理飽和與溢出。INT8 算完的乘積需要更高位的累加器至少是 INT32。假設(shè)一次累加 64 個(gè) INT8 乘積每個(gè)乘積最大就是 128*128 1638464 個(gè)加起來(lái)最大是 1,048,576明顯超出 INT16 的范圍所以累加器必須上 INT32。這部分在寫(xiě) RTL 時(shí)很容易被忽略導(dǎo)致精度災(zāi)難。注意量化感知訓(xùn)練QAT和訓(xùn)練后量化PTQ在項(xiàng)目里一定要提前區(qū)分。QAT 需要修改訓(xùn)練代碼成本大但精度高PTQ 只需要推理框架導(dǎo)出時(shí)校準(zhǔn)適合快速驗(yàn)證。實(shí)際項(xiàng)目里如果 PTQ 精度損失超過(guò) 1%再把敏感層挑出來(lái)用 QAT 或者混合精度處理一般能控制在可接受范圍。INT4 和更低比特的量化目前在部分加速器上也逐漸有人嘗試但除非你對(duì)模型的敏感層做了非常細(xì)致的誤差分析否則我還是建議謹(jǐn)慎。INT4 能省一半存取帶寬但精度損失和硬件實(shí)現(xiàn)復(fù)雜度是雙倍增加不是一個(gè)零成本的買(mǎi)賣(mài)。2.3 被忽視的那些算子Pooling、Concat 和殘差連接真正的實(shí)戰(zhàn)里卷積和全連接反而不是最讓人頭疼的。量化、單算子延遲、硬件時(shí)序大部分問(wèn)題都集中在“看起來(lái)很簡(jiǎn)單”的操作上。Pooling 的問(wèn)題在于它往往和卷積不在同一層里做數(shù)據(jù)流可能已經(jīng)走回了 DDR。如果加速器只加速卷積而池化交給 CPU 或其他核來(lái)處理那就是一次完整的數(shù)據(jù)發(fā)給 CPU 再傳回來(lái)晚了不少。比較有效的做法是把 max pooling 融合進(jìn)卷積的寫(xiě)回階段在最后輸出一個(gè)像素時(shí)直接比大小。Concat 和殘差連接更考驗(yàn)加速器的地址生成邏輯。這兩個(gè)操作不涉及任何乘加只是把多塊片上的內(nèi)存按順序搬到輸出區(qū)域。如果地址生成模塊寫(xiě)得不夠靈活最常見(jiàn)的現(xiàn)象就是硬件跑某個(gè)網(wǎng)絡(luò)時(shí)甚至比跑當(dāng)年模型還不穩(wěn)定因?yàn)榈教幎际菍?duì)齊錯(cuò)誤。這三個(gè)算子讓我學(xué)到最重要的一課加速器不只是算乘法更是一個(gè)數(shù)據(jù)搬運(yùn)和調(diào)度系統(tǒng)。設(shè)計(jì)時(shí)候給這些“白干活的模塊”留好帶寬和地址映射能力往往比給 MAC 陣列加面積更劃算。3. 實(shí)操過(guò)程從模型到板卡的四步走完整流程3.1 工具鏈選型和工程搭建HLS 還是 RTL很多剛?cè)腴T(mén)的人喜歡糾結(jié)一個(gè)問(wèn)題用 HLS高層次綜合還是寫(xiě) RTL我的答案是看階段。如果是做架構(gòu)原型驗(yàn)證HLS 的效率高很多。你可以在 C 里直接描述卷積循環(huán)的分塊邏輯讓工具綜合出流水線。雖然最終頻率可能不如手寫(xiě) RTL但對(duì)于驗(yàn)證“數(shù)據(jù)流對(duì)不對(duì)”“帶寬夠不夠”這些問(wèn)題已經(jīng)足夠了。下面給一個(gè)實(shí)際的 HLS 卷積核心示意偽代碼形式// 以 AXI 接口讀取輸入按 block 方式處理輸出通道 void conv_kernel( hls::streamap_int8 in_stream, hls::streamap_int8 weight_stream, hls::streamap_int32 out_stream, int input_h, int input_w, int num_blocks) { #pragma HLS INTERFACE axis portin_stream #pragma HLS INTERFACE axis portweight_stream #pragma HLS INTERFACE axis portout_stream ap_int32 acc[OUT_TILE_W]; #pragma HLS ARRAY_PARTITION variableacc complete dim1 // 按輸出通道 block 循環(huán) for (int ob 0; ob num_blocks; ob) { // 加載權(quán)重 block // 對(duì)空間維度做流水線 for (int h 0; h input_h; h) { for (int w 0; w input_w; w) { #pragma HLS PIPELINE II1 // 讀取輸入并做 MAC 累加 for (int i 0; i OUT_TILE_W; i) { acc[i] in_val * weight_val[i]; } } } } }這種代碼在綜合以后能自動(dòng)把中間循環(huán)轉(zhuǎn)成流水線。迭代間隔 II1 的意思是每個(gè)時(shí)鐘周期都要產(chǎn)生一個(gè)輸出點(diǎn)這是檢查性能的關(guān)鍵約束。但如果你做的是對(duì)面積和頻率極度敏感的正式流片版本那還是要回到 RTL 手寫(xiě)。HLS 在控制細(xì)粒度時(shí)序和跨時(shí)鐘域時(shí)相對(duì)吃力調(diào)試波形也不如 RTL 直觀。我的經(jīng)驗(yàn)是先用 HLS 驗(yàn)證架構(gòu)可行性再針對(duì)關(guān)鍵模塊重寫(xiě) RTL。因?yàn)閮?nèi)容安全問(wèn)題很多結(jié)構(gòu)我也講不了太細(xì)但有一點(diǎn)是確定的工程里結(jié)合兩條路線的結(jié)果比單純選一條路要快很多。3.2 數(shù)據(jù)搬運(yùn)與片上存儲(chǔ)分配雙緩沖和帶寬計(jì)算存儲(chǔ)系統(tǒng)設(shè)計(jì)是整個(gè)加速器里“看不見(jiàn)但最重要”的部分。片上存儲(chǔ)SRAM/BRAM/URAM的大小直接決定了數(shù)據(jù)復(fù)用長(zhǎng)度而外部存儲(chǔ)帶寬決定了整個(gè)系統(tǒng)能不能跑滿算力。以一個(gè)具體參數(shù)計(jì)算為例。假設(shè)我們要設(shè)計(jì)一個(gè)能跑到 100 GOPS 的 INT8 加速器也就是每秒需要 100G 個(gè)乘法累加。工作頻率 250MHz 的話每個(gè)周期需要完成 400 次 MAC。如果乘法陣列采用 16x16 的形狀每個(gè)周期剛好 256 次 MAC接近目標(biāo)。此時(shí)數(shù)據(jù)帶寬要求是多少每個(gè)周期從外部讀入兩個(gè) INT8 操作數(shù)輸入和權(quán)重就是 256*2 512B/周期。250MHz 下外部帶寬需要 512 * 250M 128GB/s。這個(gè)帶寬遠(yuǎn)超過(guò)普通 DDR 的能力。所以真正的設(shè)計(jì)思路是內(nèi)部數(shù)據(jù)復(fù)用。以 3x3 卷積為例同一輸入像素會(huì)被多個(gè)輸出通道重復(fù)使用 16 次權(quán)重則被多個(gè)空間位置重復(fù)使用多次。合理的數(shù)據(jù)流能把相對(duì)最大帶寬降低一個(gè)數(shù)量級(jí)引入多級(jí)緩沖。輸入通道先整批搬到片上片上的 buffer 可以是幾十到幾百 KB做完全部輸出通道的 MAC再丟棄。雙緩沖ping-pong buffer幾乎是標(biāo)配方法。原理很簡(jiǎn)單外部 DMA 正在往 buffer A 搬運(yùn)下一塊數(shù)據(jù)時(shí)計(jì)算單元正在從 buffer B 讀取當(dāng)前塊做計(jì)算。這樣搬運(yùn)和計(jì)算可以完全重疊系統(tǒng)吞吐率理論上能做到“搬運(yùn)時(shí)間和計(jì)算時(shí)間最大值”而不是兩者之和。我在“模擬項(xiàng)目 X”里測(cè)過(guò)一個(gè)極端的 case不做雙緩沖時(shí)計(jì)算單元有超過(guò) 60% 的時(shí)間在空轉(zhuǎn)加上雙緩沖后利用率直接拉到 92% 以上。這個(gè)提升純粹是存儲(chǔ)調(diào)度帶來(lái)的不需要增加任何 MAC 單元。注意片上 SRAM 的容量規(guī)劃要留足余量因?yàn)榈刂穼?duì)齊、padding 等問(wèn)題經(jīng)常會(huì)讓實(shí)際占用比理論值高出 20% 左右。我習(xí)慣在規(guī)劃階段就按理論值的 1.3 倍來(lái)估算省得后期挪布局。3.3 系統(tǒng)集成與板級(jí)驗(yàn)證從裸循環(huán)到完整鏈路模塊仿真做完了并不代表板子就能跑。真正進(jìn)入系統(tǒng)集成階段后你會(huì)遇到一系列“局部正確但整體掛了”的問(wèn)題。系統(tǒng)集成要考慮的組件包括DMA 引擎、片間互聯(lián)AXI 總線、中斷控制器、驅(qū)動(dòng)程序和應(yīng)用層接口。我通常把集成過(guò)程拆成三個(gè)階段。第一個(gè)階段是裸機(jī)驗(yàn)證。先用片上 SRAM 作為輸入直接通過(guò) JTAG 寫(xiě)入一些已知的特征圖數(shù)據(jù)跑完計(jì)算以后把結(jié)果直接讀到主機(jī)端做比對(duì)。這一步速度快任何問(wèn)題都容易定位因?yàn)樗@過(guò)了一切總線協(xié)議問(wèn)題。第二個(gè)階段是加入 DMA。把輸入輸出都放到 DDR 內(nèi)存里由主機(jī)端安排內(nèi)存地址然后觸發(fā)卸載offload任務(wù)。這一步最常踩的坑是指針地址沒(méi)有按 64B 對(duì)齊或者 burst 長(zhǎng)度設(shè)置超過(guò)了總線一次傳輸?shù)淖畲笾怠MA 傳輸一旦出現(xiàn) burst 邊界錯(cuò)誤數(shù)據(jù)就會(huì)靜默錯(cuò)位最后表現(xiàn)出來(lái)就是模型輸出全亂。第三個(gè)階段才是跑真實(shí)模型。這時(shí)要接上運(yùn)行時(shí)runtime軟件框架把權(quán)重文件按照加速器需要的 padding 和重排規(guī)則預(yù)處理好。很多模型跑不起來(lái)的真正原因不是硬件算力不夠而是權(quán)重格式和運(yùn)行時(shí)接口不匹配。板級(jí)驗(yàn)證時(shí)的經(jīng)典順序是先看中斷有沒(méi)有來(lái)再看 DMA 傳輸長(zhǎng)度對(duì)不對(duì)再看輸出緩沖區(qū)數(shù)據(jù)是不是帶上了垃圾數(shù)據(jù)。按這個(gè)順序排查能解決大部分集成階段的問(wèn)題。4. 調(diào)試與優(yōu)化實(shí)錄三個(gè)必踩的坑和一個(gè)速查表4.1 時(shí)序收斂為什么頻率上不去任何硬件設(shè)計(jì)做到最后時(shí)序收斂都是最熬人的環(huán)節(jié)。最常見(jiàn)的現(xiàn)象是綜合報(bào)告一跑最高頻率只有目標(biāo) 60%一大片關(guān)鍵路徑布滿在乘法陣列的加法樹(shù)上。關(guān)鍵字有三類組合邏輯太深通常是因?yàn)樵谝粋€(gè)周期內(nèi)做了 16 個(gè) 8 位乘法然后立刻加 64 個(gè)數(shù)。解決辦法是手工插入流水寄存器把加法樹(shù)切成多級(jí)。比如 64 個(gè)數(shù)的累加拆成兩級(jí)加法每級(jí) 32 個(gè)輸入這樣單周期組合路徑能縮短一半。扇出過(guò)高一個(gè)控制信號(hào)同時(shí)驅(qū)動(dòng)幾百個(gè)加法單元導(dǎo)致布線延遲過(guò)大。解決辦法是邏輯復(fù)制把同一個(gè)控制信號(hào)做成多份分區(qū)驅(qū)動(dòng)??鐣r(shí)鐘域如果加速器核心跑 250MHz而接口控制器跑 125MHz就必須用異步 FIFO 過(guò)渡不能直接采數(shù)。這里最容易踩坑的是 FIFO 的空滿信號(hào)判斷多打了幾拍導(dǎo)致數(shù)據(jù)多讀了一次或者漏讀了一次。我在調(diào)一個(gè)卷積陣列時(shí)花了兩周才把 200MHz 提到 250MHz最后改的僅僅是部分和更新各級(jí)的流水寄存器的插入位置。運(yùn)行頻率提不上去指令再怎么優(yōu)化都沒(méi)用所以時(shí)序這事值得花時(shí)間認(rèn)真做。心得時(shí)序優(yōu)化的正確順序是“架構(gòu)性的優(yōu)化 - 流水線插入 - RTL 微調(diào) - 布局約束”。不要一上來(lái)就改幾千行 RTL而是先畫(huà)出關(guān)鍵路徑的數(shù)據(jù)流圖看加法樹(shù)是否能折疊看是否可以把某個(gè)乘法器拆分到多個(gè)周期里做。大部分問(wèn)題出在架構(gòu)而不是語(yǔ)法。4.2 數(shù)據(jù)位寬與溢出問(wèn)題INT8 乘積的危險(xiǎn)區(qū)域只要你做了 INT8 定點(diǎn)化就必然要與溢出問(wèn)題正面遭遇。我前面已經(jīng)提到累加器要用 INT32但實(shí)際中還有幾個(gè)更隱蔽的地方。一是權(quán)重量化后的特殊值。如果某個(gè)權(quán)重用了對(duì)稱量化但 0 和 -128 都被映射成不可表達(dá)的值那么計(jì)算部分和時(shí)會(huì)出現(xiàn)極大值。特別是在殘差結(jié)構(gòu)的短路徑中結(jié)果直接被送給下一層時(shí)一旦值域超出下一層的量化范圍就會(huì)發(fā)生飽和截?cái)?。這類誤差不是單層能看出來(lái)的它會(huì)順著殘差逐層累積到最后的輸出就是幾個(gè)關(guān)鍵類別分?jǐn)?shù)全亂了。二是乘法陣列內(nèi)部如果用了截?cái)鄑runcation而不是四舍五入round部分和的誤差也會(huì)被放大。我做過(guò)一個(gè)對(duì)比實(shí)驗(yàn)累加結(jié)果不采四舍五入直接截?cái)嗄P偷?Top-1 精度下降 0.4%改成四舍五入后完全恢復(fù)。這個(gè)細(xì)節(jié)很多人會(huì)在一開(kāi)始忽略。三是非線性激活函數(shù)的實(shí)現(xiàn)。ReLU 的硬件實(shí)現(xiàn)既可以是 clamp 到 0 再飽和也可以直接做符號(hào)位判斷。如果檢測(cè)到負(fù)數(shù)時(shí)沒(méi)有正確飽和到 0累積誤差也會(huì)越來(lái)越大。排查這類問(wèn)題的方法也很簡(jiǎn)單把硬件輸出保存成文本在主機(jī)端用 Python 模擬同一位寬的定點(diǎn)數(shù)運(yùn)算逐層對(duì)比部分和的差異。一旦定位到誤差開(kāi)始出現(xiàn)的那一層就檢查該層的近似實(shí)現(xiàn)。4.3 模型精度下降量化后掉三個(gè)點(diǎn)的攻堅(jiān)記錄有一次我們把一個(gè)檢測(cè)模型從 FP32 切到 INT8測(cè)試精度掉了 3.1 個(gè)點(diǎn)。按經(jīng)驗(yàn)這個(gè)幅度已經(jīng)算比較大了于是開(kāi)始逐層排查。首先檢查的是校準(zhǔn)集。原來(lái)的校準(zhǔn)集里都是室內(nèi)圖像數(shù)據(jù)但實(shí)際部署場(chǎng)景有一半是室外逆光的圖像。激活值分布在暗光區(qū)域沒(méi)有校準(zhǔn)到于是量化 scale 整體偏差。換了一套混合場(chǎng)景的校準(zhǔn)集后精度損失降到 1.8%。接下來(lái)看 BNBatchNorm融合問(wèn)題。很多框架導(dǎo)出的模型里還帶 BN 層BN 在推理階段可以完全融合到卷積權(quán)重里省掉一整層的計(jì)算。但如果融合操作做晚了量化后的 BN 計(jì)算會(huì)引入一個(gè)額外的大數(shù)量級(jí)乘法嚴(yán)重拉大數(shù)值誤差。把 BN 融入到卷積再做量化精度損失又降了 0.5%。最后發(fā)現(xiàn)某一層殘差分支的輸出值域特別大用 INT8 表述后精度損失的方差都集中在這里。把那層提升為 INT16再跑一遍最終精度損失降到 0.6% 以內(nèi)。這個(gè)問(wèn)題給我一個(gè)很深的感觸逐層掃描配合統(tǒng)計(jì)信息比“全局調(diào)一調(diào) scale 再試一次”要高效得多。硬件加速器的精度問(wèn)題通常都不是一個(gè)玄學(xué)而是某幾個(gè)敏感層的具體問(wèn)題。4.4 常見(jiàn)問(wèn)題速查表先把調(diào)試現(xiàn)場(chǎng)整理成表格方便以后遇到問(wèn)題時(shí)直接查現(xiàn)象可能原因排查順序解決辦法板卡輸出全為 0DMA 沒(méi)觸發(fā)或地址對(duì)齊錯(cuò)先查中斷再查 DMA 描述符檢查地址對(duì)齊和 burst 長(zhǎng)度配置模型輸出隨機(jī)錯(cuò)亂數(shù)據(jù)位寬截?cái)嗷蛄炕绯鰧?duì)比單層定制結(jié)果用四舍五入累加查 INT32 累加器寬度推理速度不達(dá)預(yù)期計(jì)算和搬運(yùn)沒(méi)有重疊看仿真波形中計(jì)算單元的 busy 信號(hào)增加雙緩沖和 DMA 下發(fā)預(yù)取板級(jí)偶發(fā)抖動(dòng)跨時(shí)鐘域沒(méi)做同步抓不定態(tài)波形加異步 FIFO多拍同步后采樣頻率跑不上去組合邏輯過(guò)深或扇出過(guò)高分析關(guān)鍵路徑插入流水寄存器、復(fù)制控制信號(hào)量化后精度驟降校準(zhǔn)集分布不符統(tǒng)計(jì)每層激活值范圍換校準(zhǔn)集、對(duì)敏感層做混合精度這張表基本覆蓋了常見(jiàn)的軟硬件協(xié)同問(wèn)題。每次做完一塊加速器我都會(huì)把它更新一次項(xiàng)目做得越多排查速度就越快。最后再分享一點(diǎn)個(gè)人體會(huì)如果說(shuō)整個(gè)設(shè)計(jì)流程里有什么最反直覺(jué)的經(jīng)驗(yàn)?zāi)蔷褪钦嬲龥Q定一個(gè)推理加速器能否落地的往往不是那套 16x16 的乘法陣列而是外圍的存儲(chǔ)調(diào)度、DMA 設(shè)計(jì)和時(shí)序收斂處理。我見(jiàn)過(guò)有人在陣列面積上摳了一個(gè)月結(jié)果因?yàn)榫彺娣峙洳缓侠韺?dǎo)致利用率只有一半。反過(guò)來(lái)把數(shù)據(jù)流設(shè)計(jì)得足夠穩(wěn)系統(tǒng)級(jí)的收益經(jīng)常比單點(diǎn)算力翻倍還高。另外調(diào)試過(guò)程里最實(shí)在的小技巧是保存一份“軟件浮點(diǎn)中間結(jié)果綁定到硬件內(nèi)部節(jié)點(diǎn)”的測(cè)試環(huán)境。就是說(shuō)在硬件仿真階段就把每一層的期望值提前算好然后設(shè)計(jì) RTL 測(cè)試時(shí)直接比對(duì)中間信號(hào)。這樣任何一層出了問(wèn)題能立刻定位是計(jì)算問(wèn)題、數(shù)據(jù)搬運(yùn)問(wèn)題還是量化問(wèn)題。很多項(xiàng)目因?yàn)槿鄙龠@種閉環(huán)調(diào)試手段最后都陷入“調(diào)一個(gè)參數(shù)、看一個(gè)現(xiàn)象、再猜下一個(gè)原因”的泥潭。這塊內(nèi)容的擴(kuò)展方向其實(shí)也很多比如低比特量化、稀疏化加速、存內(nèi)計(jì)算甚至多卡互聯(lián)上的推理調(diào)度。每個(gè)方向都是一個(gè)不小的坑但基礎(chǔ)的數(shù)據(jù)流和硬件設(shè)計(jì)思路完全一致。希望這篇文章能幫你在起步階段少幾次重寫(xiě) RTL 的沖動(dòng)多跑通幾個(gè)真正能用的板卡。