同設計與編譯器調(diào)優(yōu)實戰(zhàn))
做過幾個AI加速芯片相關的項目之后我最大的感受是芯片設計里最難的不是RTL邏輯而是讓軟件棧跟上硬件架構的脾氣。最典型的例子是我們某次流片回來仿真階段各項指標都很漂亮結果板子上一跑真實網(wǎng)絡模型吞吐量直接打了六折定位了快兩周才發(fā)現(xiàn)問題既不在某個模塊的功能bug也不在DMA搬數(shù)邏輯而是編譯器生成的調(diào)度順序和硬件片上緩存的分配策略根本不在一個頻道上。這類問題恰恰是AI芯片的軟硬件設計里最考驗團隊功底的部分也是很多團隊最容易踩的坑。這篇內(nèi)容適合兩類人看一類是做AI芯片架構、RTL設計想了解軟件棧到底怎么消費硬件能力的工程師另一類是寫編譯器和運行時想弄明白硬件架構背后隱藏了哪些約束的開發(fā)同學。無論你從哪一頭切入這篇文章的核心觀點是一致的AI芯片的算力是硬件給的但真正的性能是軟硬件一起談出來的。1. 為什么AI芯片容易算得快但跑不快——先從數(shù)據(jù)流理解架構選型的底層邏輯很多芯片團隊立項時會先定一個目標算力比如要做多少TOPS然后反推需要多少MAC陣列、跑多高的主頻。這個推導過程本身沒錯但往往一上來就把重點放在算得夠不夠快忽視了另一個關鍵指標數(shù)據(jù)能不能供得上。1.1 帶寬墻算力數(shù)字好聽的代價我習慣用一個簡單的公式來估算芯片能跑出的真實算力上限。假設MC陣列有N個乘累加單元主頻為f理論峰值就是 2 × N × f TOPS。但實際上每個MAC操作都要消耗權重和輸入數(shù)據(jù)而這部分數(shù)據(jù)得從片外的DDR或者片上SRAM里搬進來。如果搬不進來MAC陣列就只能空轉。舉一個具體計算256個MAC、1GHz主頻的陣列理論峰值大概是512GOPS。假設只算權重搬運FP16權重一個元素占2字節(jié)那么為了保證MAC滿負荷運轉每秒需要提供 512 × 10^9 × 2 字節(jié)的權重也就是1024GB/s前提還是每個權重只用一次。當然權重肯定會被復用但復用什么程度完全取決于數(shù)據(jù)流和數(shù)據(jù)切分策略。如果平均每份權重能復用16次帶寬需求就降到64GB/s。如果DDR實際可用帶寬只有32GB/s那峰值算力可能只剩一半。這就是帶寬墻它比功耗墻更隱蔽也更容易在設計早期被忽略。所以我建議在架構選型階段先不要只盯著TOPS數(shù)字而是把目標Workload拆開統(tǒng)計每一類算子、每個網(wǎng)絡層的實際訪存量然后問自己一個問題我的片上緩存和片外帶寬能不能支撐目標主頻下的數(shù)據(jù)供給如果不能要么提高復用率要么降主頻要么加帶寬沒有第四條路。1.2 數(shù)據(jù)流模式硬件架構的性格底色AI計算尤其卷積和矩陣乘天然存在三種經(jīng)典的數(shù)據(jù)復用模式權重固定Weight Stationary把權重提前全部加載到PE陣列里的寄存器或局部緩存輸入數(shù)據(jù)流過陣列。這種模式適合權重比較大、輸入可以被分片多次重用的場景比如卷積核參數(shù)較厚的情況。輸出固定Output Stationary每個PE里保存部分和累加值輸入和權重都不斷流入。因為輸出累加盡量留在片內(nèi)減少了中間結果寫回內(nèi)存的開銷很適合對輸出通道數(shù)較大的卷積層。行固定Row Stationary按行切分數(shù)據(jù)流讓相鄰PE之間做數(shù)據(jù)傳遞提高脈動陣列的空間利用率。選擇哪套數(shù)據(jù)流不是硬件團隊拍腦袋定的。比如選了Output Stationary編譯器在調(diào)度時就得盡量讓同一位置的輸出累加提前集中計算減少部分和反復寫回選了Weight Stationary軟件棧就得優(yōu)先保證權重分塊和常駐否則權重反復從DDR加載會讓性能直接崩。我見過一個團隊硬件架構師在文檔里寫支持三種數(shù)據(jù)流模式但軟件那邊只針對其中一種做了優(yōu)化映射另兩種模式在RTL里可能沒問題跑出來的性能卻慘不忍睹。這個矛盾在架構定稿那一刻就已經(jīng)埋下了。所以架構選型的真正參與者不只是硬件架構師還必須包括編譯器團隊和運行時團隊至少要讓他們在紙面上把關鍵算子的映射方案畫出來再定案。1.3 編譯器是硬件的第一用戶有一個說法我很認同編譯器的能力邊界就是芯片的性能上限。芯片設計完成之后所有理想化的架構優(yōu)勢都要通過編譯器落到真實程序里才能體現(xiàn)。硬件做的再好如果編譯器看不懂你的指令集設計意圖或者無法表達某種高效的調(diào)度方式這顆芯片就只能當一顆高功耗的普通處理器用。所以在設計指令集和DMA描述符的時候我會建議團隊先讓軟件工程師出一份編譯器期望的硬件抽象再對照硬件實現(xiàn)能力取交集。比如循環(huán)切塊的步長是否任意、非連續(xù)地址訪問是否支持、訪存對齊有幾種模式這些看起來不起眼的點往往是后期編譯器代碼生成能否高效的關鍵。2. 硬件設計里最容易埋雷的四個細節(jié)陣列、緩存、指令集、精度這一節(jié)想談的不是教材上的標準流程而是我們在項目中反復踩過、最終沉淀下來的硬件設計經(jīng)驗。這里不討論RTL編碼技巧重點放在架構決策為什么這么做、以及做錯了會有什么后果。2.1 計算陣列的粒度選擇為什么不是越大越好陣列規(guī)模每翻一倍算力數(shù)字就漂亮一倍但由此帶來的問題是每個PE的有效利用率可能直線下降。原因在于PE陣列要想跑滿每個PE都必須持續(xù)拿到對的輸入數(shù)據(jù)如果片上緩存和數(shù)據(jù)通道無法支撐出現(xiàn)等待氣泡陣列規(guī)模越大浪費越明顯。我用一個生活中的類比廚房里有十個爐灶理論上一小時能出120道菜但如果切配臺只有兩個人備菜速度跟不上一半的爐灶只能空燒。PE陣列就是這個爐灶數(shù)據(jù)供給系統(tǒng)就是切配臺。算力不是由爐灶數(shù)量決定而是由最短那塊木板決定。實際做硬件選型時除考慮目標峰值算力外還必須模擬目標網(wǎng)絡里卷積層出現(xiàn)的各種輸入尺寸。比如輸入是112×112、64通道一個8×8 PE陣列可以把輸入分塊進片上SRAM數(shù)據(jù)復用率很高但如果是1×1卷積、通道數(shù)很小數(shù)據(jù)復用率天然就低這時更大的陣列只會帶來更大的空轉損失。設計時最好對目標Workload做一次數(shù)據(jù)復用率敏感度分析畫出陣列尺寸和預期利用率的曲線再選拐點附近那個配置。2.2 片上SRAM分配權重、激活、輸出三大緩沖的博弈AI芯片的片上SRAM容量有限通常在幾百KB到幾MB之間怎么切分直接決定編譯器能切多大的tile。這里我把一次典型的分配問題拆開看假設總共有256KB方案權重緩沖激活緩沖輸出緩沖適合場景潛在問題方案A40% (約102KB)40%20%卷積層權重復用高大輸出通道時輸出緩沖吃緊方案B60%20%20%權重密集且層厚激活為主的可分離卷積性能差方案C30%30%40%輸出通道多、部分和復用權重tile太小需頻繁加載方案B看起來權重容納很多很爽但我們曾經(jīng)在某個輕量化網(wǎng)絡上實測過它大量使用深度可分離卷積這種層計算量不大但激活的數(shù)據(jù)量很大權重緩沖大、激活緩沖小的分配方式直接導致激活反復在DDR和SRAM之間搬運性能損失接近一半。從那以后我們在做buffer分配決策時都是把目標網(wǎng)絡層的訪存特征拉出來逐一過一遍。另外還有一個容易被忽略的點DMA描述符需要的SRAM空間。有些設計會預留一部分SRAM作為DMA描述符環(huán)(descriptor ring)區(qū)但編譯器團隊如果不知道這塊空間的具體大小做出的調(diào)度就會建立在錯誤假設上。軟硬件接口文檔里必須寫清楚哪塊空間歸誰以及各自的預留邊界對齊規(guī)則。2.3 指令集應該少而精還是寬而全AI芯片的指令集設計存在一個常見爭論。主張少而精的團隊傾向于只提供最基本的矩陣運算指令把靈活性交給編譯器主張寬而全的團隊則期望把復雜的數(shù)據(jù)調(diào)度也放進指令里方便手寫算子庫。我的觀點是指令集描述的應該是計算內(nèi)核和數(shù)據(jù)搬運動作不需要太多花哨的控制流指令但訪存描述能力必須給足。曾經(jīng)有個項目里DMA描述符的地址偏移字段只有12位結果一個大tile的尋址范圍超出表達上限編譯器只能把一個連續(xù)的大搬運拆成幾百個小搬運性能開銷暴漲。后來我們在指令集里增加了一個二維搬移描述符一次能描述任意行列跨步的搬運這個問題才解決。這個教訓告訴我設計指令集時不光要考慮能不能實現(xiàn)功能還要站在編譯器的視角問一句這條指令能不能描述清楚、開銷合不合理。寧可指令數(shù)少一點也要讓每一條指令都有足夠的表達能力。2.4 累加精度和數(shù)據(jù)類型硬件上必須明確選擇的位寬AI推理進入INT8時代以后大家都關注激活和權重的量化精度但累加器的位寬反而被很多團隊忽略。卷積層里N個輸入×權重的結果累加在一起如果不給足累加位寬溢出和截斷誤差會在深層網(wǎng)絡里被逐層放大。以INT8計算為例常見做法是MAC的乘法和加法用8位輸入但累加寄存器至少給到32位這樣才能覆蓋一個相對大的卷積核累加范圍FP16計算則往往累加器用FP32來保證精度。設計時要把累加器的位寬作為正式指標寫入架構文檔而不是讓RTL工程師順手拍一個位寬。同時還要在硬件上實現(xiàn)不同的舍入模式round to nearest、truncate等供軟件層按場景選擇。3. 軟件棧的翻譯過程從模型圖到硬件指令的層層決策有了硬件真正要讓模型跑起來軟件棧要做的事情比想象中多得多。我們不談圖優(yōu)化框架這種宏觀話題只剖析三個決定性能的微觀決策點算子融合的邊界、循環(huán)映射的tile策略、以及數(shù)據(jù)精度管理的軟硬分工。3.1 算子融合的邊界到底該融到什么程度算子融合是減少中間張量訪存的有效手段。比如ConvBNReLU融合后原先需要把卷積輸出完整寫到DDR、再讀回來做BN和ReLU現(xiàn)在可以在片內(nèi)一氣呵成。但融合并非越多越好。原因在于片上SRAM容量是有限的。兩個大算子融在一起中間結果很容易超出一塊buffer的容量編譯器只能選擇看起來融合、實際還是把中間結果寫出去的偽融合。我做過一次實驗某個網(wǎng)絡在融合掉Batchnorm和ReLU后吞吐提升約15%但嘗試把兩個相鄰的大卷積算子融合進去性能反而下降因為編譯器生成的調(diào)度把buffer切得特別碎尋址開銷和同步開銷都上去了。所以軟件棧里應當有一個融合收益估算器根據(jù)當前片上buffer空閑容量、算子計算密度、目標tile大小三個輸入來決定融合邊界。這個估算器和硬件設計密切相關如果硬件SRAM做得越大、分布式緩存層級越靈活融合決策空間就越大軟件棧能發(fā)揮的空間也越大。3.2 循環(huán)映射與tile策略一次典型卷積的調(diào)度推演矩陣乘法在編譯器層面最終都要被映射成多重循環(huán)。以卷積為例常見的循環(huán)維度有輸出通道、輸入通道、輸出H、輸出W、卷積核Kh、卷積核Kw。不同順序就會產(chǎn)生完全不同的數(shù)據(jù)流動軌跡。為了講清楚我用一個簡化的偽代碼展示兩種循環(huán)順序對數(shù)據(jù)復用的影響。假設輸出塊大小是(64, 64, 64)權重塊是(64, 64, 3, 3)# 循環(huán)順序A內(nèi)部循環(huán)先遍歷輸出通道OC for oh in range(0, H, TileH): for ow in range(0, W, TileW): for oc in range(0, C_out, TileOC): for ic in range(0, C_in, TileIC): load_weights(oc, ic, KernelSize) load_activation(oh, ow, ic) mac_compute(oc, ic, oh, ow) # 輸出部分和暫存片上 write_back_partial_sum(oc block)# 循環(huán)順序B內(nèi)部循環(huán)先遍歷輸入通道IC for oh in range(0, H, TileH): for ow in range(0, W, TileW): for ic in range(0, C_in, TileIC): for oc in range(0, C_out, TileOC): load_weights(oc, ic, KernelSize) load_activation(oh, ow, ic) mac_compute(oc, ic, oh, ow) write_back_full_sum(oc block)循環(huán)順序A里權重塊會按照輸出通道分組重復加載因為內(nèi)層遍歷輸入通道時每一個IC都要把整個OC塊再讀一遍權重循環(huán)順序B則是在固定輸入通道情況下把OC遍歷完權重讀取次數(shù)少得多。實際工程中真正復雜的tile策略還要同時考慮片上的三塊buffer空間誰先被占滿。比如weights buffer 102KBactivation buffer 100KBoutput buffer 44KB那么編譯器決策時會去動態(tài)計算每個tile的負載找到能同時滿足三塊buffer空間限制的最大化tile組合。這一步做得好不好直接影響最終MAC利用率能到30%還是70%。所以芯片團隊必須給軟件棧提供硬件資源描述表包括每塊buffer容量、訪問寬度、bank數(shù)量、仲裁規(guī)則否則編譯器就只能在很粗糙的假設上做決策。3.3 數(shù)據(jù)精度管理的軟硬分工模型訓練時用高精度是常規(guī)操作但推理部署往往要INT8甚至更低精度。在這個過程中量化策略如何選scale、zero point、是否做per-channel量化是軟件棧負責的事而硬件的職責是提供足夠寬容的數(shù)據(jù)通路。具體來說硬件必須明確回答這幾個問題MAC單元的輸入位寬是否可配置累加器位寬是否足夠大、是否會飽和截斷遇到非對齊訪問是否會產(chǎn)生異常舍入模式是固定還是可編程如果這些問題留給軟件去猜結果就是軟件棧為了規(guī)避不確定性普遍采用保守配置性能又掉一截。我在一個FP16推理項目中就吃過虧硬件手冊寫累加器是FP32但RTL實際實現(xiàn)成FP16累加后截斷由于量化誤差積累跑深一點的網(wǎng)絡精度明顯下降。這個問題的根因不是RTL寫錯而是規(guī)格定義里沒有把累加位寬和每次累加后的舍入行為寫清楚。從此我們把精度規(guī)格寫成一張硬性表格包含數(shù)據(jù)位寬、累加位寬、溢出策略、舍入模式、以及每類算子的默認配置軟件和硬件雙方都必須按這張表驗收。4. 一次真實性能調(diào)優(yōu)復盤為什么仿真的性能預期實測時對不上這部分記錄一次具體的性能排查經(jīng)歷。某次我們做一款面向邊緣場景的AI加速芯片目標是在某個主流分類網(wǎng)絡上跑到86FPS以上。流片回來后實測只有52FPS功能完全正確精度也達標就是跑不快。4.1 現(xiàn)象與第一步排查首先我們打開了片上的性能計數(shù)器發(fā)現(xiàn)MAC陣列利用率只有40%左右DDR讀帶寬接近峰值寫帶寬也不低。這個組合很可疑如果MAC利用率低說明數(shù)據(jù)供不上來如果讀帶寬和寫帶寬同時居高不下說明有大量中間數(shù)據(jù)在DDR往返。接著看編譯器生成的調(diào)度日志發(fā)現(xiàn)輸出通道維度的tile被切得很碎每算完16個輸出通道就要寫一次部分和回DDR然后下一塊輸入數(shù)據(jù)又要把權重重新加載一遍。權重的重復加載次數(shù)比基準建模時多了大概3倍。4.2 根因定位軟件假設與硬件約束對不上這個問題的直接原因是編譯器決策模型里output buffer的可分配容量被設置得太保守。軟件棧拿到的參數(shù)表顯示輸出緩沖只有20KB而實際上硬件RTL里這塊SRAM有44KB中間差的24KB被預留成了DMA臨時區(qū)。硬件團隊這樣做本來是出于安全考慮給DMA描述符預留空間防止溢出但預留空間的具體數(shù)值沒有同步給軟件棧。編譯器以為輸出緩沖很小只能選擇頻繁寫回部分和最終表現(xiàn)為DDR寫帶寬激增和權重重復加載。這類問題的本質不是硬件功能設計錯誤而是軟硬件接口契約不一致。排查鏈路走到這里已經(jīng)清楚不是RTL邏輯的問題而是參數(shù)配置和軟件調(diào)度策略共同造成的性能損失。4.3 修復方式與最終效果這個階段芯片已經(jīng)流片回來沒法改動SRAM容量所以修復完全在軟件側。我們做了兩步調(diào)整修改軟件棧里的硬件資源描述表把output buffer容量和DMA預留空間的真實邊界寫清楚優(yōu)化編譯器tile切分策略允許在滿足實際buffer容量約束的情況下采用更大的輸出通道tile減少權重重新加載次數(shù)。調(diào)整之后同一塊板子上實測FPS從52提升到79雖然還沒到86的預期值但性能分析顯示剩余差異來自DDR帶寬飽和屬于跑目標網(wǎng)絡本身的帶寬瓶頸。這個項目讓我意識到芯片回來后如果性能不達標第一反應不應該是懷疑硬件邏輯而是先驗證軟件棧手里的硬件參數(shù)和實際RTL實現(xiàn)是否一致。5. 軟硬件接口驗證最容易走的彎路差分測試、性能建模與逐層比對芯片設計流程中RTL功能驗證做得很充分但軟硬件聯(lián)合驗證往往被壓縮到極致。這里說三個我們實踐中走過彎路的方向。5.1 指令語義的隨機差分測試很多團隊在做指令集驗證時喜歡用寫好的定向測試用例去覆蓋指令功能感覺覆蓋率高就放心了。但真正的風險往往出現(xiàn)在編譯器生成的長序列指令流里多條DMA指令并發(fā)、訪存地址非對齊、部分和寫回和權重加載交錯等場景。我推薦的辦法是做指令集模擬器與RTL仿真器的差分對比。讓編譯器隨機生成大量合法指令序列分別在指令集模擬器和RTL里運行比對每條指令執(zhí)行后的寄存器狀態(tài)和內(nèi)存鏡像。這個方法能暴露大量語義死角。在一個項目里我們用隨機差分測試發(fā)現(xiàn)DMA描述符的地址累加邏輯在跨bank邊界時差了半拍這種bug用人工定向用例很難發(fā)現(xiàn)因為它只在特定的地址對齊組合下出現(xiàn)。5.2 性能模型的精度初期就納入總線仲裁與bank沖突性能模型如果做得太粗糙軟件?;谒龅恼{(diào)度決策就會產(chǎn)生偏差。最典型的偏差來源是完全沒有建模SRAM bank conflict兩個不同的宏單元同時訪問同一bank的不同地址導致一個周期內(nèi)只能服務一個請求實際訪存延遲翻倍。當初我們第一版性能模型只建模了一條理想總線和無沖突SRAM在RTL仿真和美發(fā)之后驗證發(fā)現(xiàn)模型預估的性能比真實硬件高25%~30%。軟件?;谶@個偏高預估做的調(diào)度自然在硬件上跑不出預期。這個問題的解法是在性能模型中把總線矩陣仲裁、SRAM bank劃分和DMA通道的并發(fā)約束逐步加進來。模型會變得更復雜開發(fā)周期可能多兩三個月但流片后省下來的是整個團隊的排障時間。如果從一開始就建模好這些細節(jié)就可以在芯片流片前把軟硬件性能聯(lián)調(diào)的大部分問題提前消化掉。5.3 端到端逐層數(shù)據(jù)比對抓shape推導和隱性問題端到端驗證階段很多團隊習慣只看最終輸出和參考模型對比的top-1精度。這個做法在絕大多數(shù)情況下夠用但有時候某些層的計算實際上被編譯成了一個正確的死循環(huán)——一個算子的循環(huán)順序被優(yōu)化成了錯誤的重心復制模式輸出結果看上去沒有變化但計算量和訪存模式完全偏離預期。解決這個問題的辦法是增加逐層輸出比對和逐層性能日志輸出。編譯器每生成一個算子的調(diào)度決策就把它對應的預期MAC利用率、訪存次數(shù)、執(zhí)行周期記錄下來和實際硬件計數(shù)器逐一比對。哪一層對不上問題就定位在哪一層的調(diào)度上。這需要軟件棧在生成代碼時保留足夠的debug信息硬件側的硬性能計數(shù)器也要做到每層可標記。早期我們沒做這層工作結果性能問題一出現(xiàn)就要好幾天定位后來補上這些機制同類問題基本半天就能鎖定。6. 給剛入行者和轉崗工程師的實踐建議從算子到全棧如果你正準備進入AI芯片的軟硬件設計領域或者正在帶領團隊做相關項目下面這幾條建議是我從多個項目的實戰(zhàn)中提煉出來的按優(yōu)先級排序。6.1 從小算子做全棧閉環(huán)比追逐大算力更有效不管你的目標是云端訓練芯片還是邊緣推理芯片都建議先用一個簡單的3×3卷積算子把完整的工具鏈走通一遍模型轉換、量化校準、圖優(yōu)化、算子映射、指令生成、RTL仿真、FPGA驗證、性能計數(shù)。小算子全流程跑通之后再逐步擴展其他算子。這個建議看似基礎但實際操作中很多團隊習慣先把大框架搭起來最后才接算子。結果大框架的空轉時間很長遇到的問題偏宏觀反而不容易沉淀實質性的軟硬件協(xié)同經(jīng)驗。從小算子入手每一層都能看到具體的數(shù)據(jù)流動和決策邏輯更容易建立整體直覺。6.2 軟硬件接口定義階段讓軟件團隊敢于提出約束芯片架構文檔往往由硬件團隊主筆軟件團隊只負責接收。我建議反過來在架構早期就讓編譯器團隊列出他們期望的硬件能力包括DMA描述符表達力、SRAM容量和bank劃分、指令流水線的嵌套限制。軟件提出的約束哪怕后來沒有全部實現(xiàn)也對架構設計有正面價值。因為軟件側的約束一旦被討論過即使最終被放棄每個被砍掉的能力都有明確的原因和后續(xù)替代方案不會再出現(xiàn)流片后才發(fā)現(xiàn)某類調(diào)度在ISA層面完全無法表達的情況。6.3 性能回歸要納入版本管理芯片項目周期長軟件棧和RTL版本都在快速迭代。很多團隊只在軟件棧側維護基準測試但很少把RTL版本、綜合參數(shù)、片上SRAM配置與軟件版本打包在一個全局版本里。我建議做軟硬件聯(lián)合回歸每周固定跑一次基準集合并記錄每個基準對應的RTL版本、編譯器版本、硬件配置任何一方改動后如果性能有明顯變化必須能追溯到是哪次版本變更導致。這套機制看起來重復麻煩但它是確保軟硬件設計持續(xù)向同一目標收斂的基礎設施。6.4 別把正確性通過誤認為性能達標最后一條建議其實是一個提醒功能驗證全綠只說明芯片能工作不代表它能以設計目標的速度工作。軟硬件協(xié)同性能驗證要有獨立的評審標準標記每個基準測試的預期throughput、期望MAC利用率、允許的訪存開銷水位任何只有正確性驗證、沒有性能驗證的報告都不應該被當作階段完成的標準。我在多個項目的復盤中發(fā)現(xiàn)性能問題的根因大多不是單一模塊的低級錯誤而是多個環(huán)節(jié)的錯誤假設疊加。軟硬件設計如果能從一開始就建立聯(lián)合驗證和統(tǒng)一性能預期很多問題在仿真階段就會提前暴露流片后的調(diào)試時間可以大幅壓縮。這是我做AI芯片軟硬件設計這些年最值得分享的一點體會。