核優(yōu)化實(shí)踐)
很多搞深度學(xué)習(xí)的朋友都有過(guò)這種體驗(yàn)GPU 利用率監(jiān)控看著沒(méi)毛病可訓(xùn)練吞吐就是上不去排查鏈路轉(zhuǎn)了一圈才發(fā)現(xiàn)時(shí)間幾乎全耗在同一類(lèi)運(yùn)算上——矩陣乘法。我之前做模型性能剖析時(shí)統(tǒng)計(jì)過(guò)從 embedding 到輸出層GEMM 類(lèi)算子能占到整體耗時(shí)的七成以上。這也是我后來(lái)動(dòng)手寫(xiě) DeepGEMM 這套 FP8 矩陣乘法內(nèi)核的根本原因與其在框架層反復(fù)做圖優(yōu)化不如直接吃透最底層這塊硬骨頭。DeepGEMM 的目標(biāo)很單純——利用 Hopper 架構(gòu) GPU 上 Tensor Core 與 TMA 的能力實(shí)現(xiàn)一個(gè)針對(duì) FP8 輸入、FP32 累加的矩陣乘法內(nèi)核。它不是一個(gè)要替代工業(yè)級(jí)內(nèi)核庫(kù)的通用方案更像是我對(duì)自己的一次摸底GEMM 到底怎么設(shè)計(jì)才能在特定 shape、特定精度下把硬件算力吃干凈。這篇文章會(huì)從為什么自研、底層原理、實(shí)現(xiàn)路徑、性能調(diào)優(yōu)到一次完整的排錯(cuò)過(guò)程都攤開(kāi)來(lái)講適合正在優(yōu)化自有算子、或者單純想搞清楚 Tensor Core 內(nèi)部邏輯的工程師和研究者。1. 為什么自己寫(xiě) GEMM模型跑得慢不一定怪 GPU1.1 先算一筆賬一次 Transformer 前向里的 GEMM 占比隨便拿出一個(gè)中等規(guī)模的 Transformer 模型前向計(jì)算里最重頭的部分就是四個(gè)映射矩陣乘法Q、K、V 的投影是一次大 GEMMattention score 的計(jì)算是一次 batched GEMMscore 對(duì) V 的加權(quán)又是一次 batched GEMMFFN 兩層則貢獻(xiàn)了剩余兩次超大矩陣乘。再加上 softmax、LayerNorm 這些逐元素算子和歸一化操作整個(gè) forward 的 GEMM 占比很容易超過(guò) 70%。訓(xùn)練場(chǎng)景還多一個(gè)反向傳播矩陣乘的數(shù)量直接翻倍這個(gè)比例只會(huì)更高。所以當(dāng)你在性能剖析工具里看到某個(gè) GEMM 算子耗時(shí)異常別急著懷疑數(shù)據(jù)加載或者并行通信先檢查算子本身的實(shí)現(xiàn)效率。這也是自研 GEMM 內(nèi)核最直接的收益點(diǎn)你可以精確控制每一字節(jié)的訪問(wèn)路徑和每條硬件指令的調(diào)度而不是被迫接受別人預(yù)設(shè)的策略。我見(jiàn)過(guò)好幾次換庫(kù)版本后推理提速的案例本質(zhì)就是新版本對(duì)某些 shape 選擇了更合適的 tile 切分而不是什么玄學(xué)優(yōu)化。1.2 cuBLAS 和 CUTLASS 都很好為什么還要有 DeepGEMM官方庫(kù)封裝得越徹底對(duì)調(diào)用者來(lái)說(shuō)黑盒程度就越高。cuBLAS 確實(shí)在絕大多數(shù) shape 上表現(xiàn)優(yōu)秀但它不會(huì)把內(nèi)核實(shí)現(xiàn)的細(xì)節(jié)暴露給你一旦遇到非標(biāo)準(zhǔn) shape 或特定精度需求你只能按它提供的預(yù)估接口去猜性能行為。CUTLASS 項(xiàng)目給了高度靈活的模板組合理論上可以拼出各種內(nèi)核策略但它概念繁多、模板深度很重想在里面改一條 TMA 加載路徑或調(diào)整 swizzle 策略學(xué)習(xí)成本相當(dāng)高。DeepGEMM 的定位就是輕量定制內(nèi)核。它不需要覆蓋所有硬件平臺(tái)和所有矩陣 shape只需要在自己的目標(biāo)機(jī)器上做到足夠快。這種取舍帶來(lái)兩個(gè)直接好處一是代碼量小、可讀性強(qiáng)每一條關(guān)鍵路徑都能很快定位二是遇到性能問(wèn)題時(shí)可以拿官方庫(kù)做基準(zhǔn)對(duì)照從 profiling 數(shù)據(jù)中反推差距在哪個(gè)環(huán)節(jié)真正把性能優(yōu)化的能力握在自己手里。如果你的業(yè)務(wù)只是定期跑幾類(lèi)固定矩陣規(guī)模自研內(nèi)核完全有理由比通用庫(kù)更貼合你的計(jì)算模式。1.3 DeepGEMM 的目標(biāo)邊界FP8、Hopper 與工程妥協(xié)做任何內(nèi)核項(xiàng)目都要先劃清邊界否則會(huì)因?yàn)槭裁炊枷胫С侄萑霟o(wú)盡復(fù)雜度。DeepGEMM 的初始范圍如下精度A、B 矩陣輸入為 FP8E4M3/E5M2 可配累加器用 FP32硬件僅針對(duì) Hopper 架構(gòu) GPU 做優(yōu)化重點(diǎn)使用其 TMA、wgmma 和分布式共享內(nèi)存能力Shape面向超大矩陣M、N 在 2048 以上做滿波優(yōu)化小矩陣則退回到簡(jiǎn)單路徑不做過(guò)深調(diào)優(yōu)擴(kuò)展性后端預(yù)留了 BF16 路徑主要用于精度對(duì)照實(shí)驗(yàn)不作為默認(rèn)主線。這個(gè)邊界意味著我不需要處理 Ampere 等舊架構(gòu)的兼容性可以把精力全部投入到大矩陣、批量加載、深流水線這一條主路上。工程上做減法和劃邊界是同樣的道理方案越單純?cè)饺菀追治龀鲂阅芷款i范圍一旦蔓延調(diào)試時(shí)會(huì)發(fā)現(xiàn)每個(gè)符號(hào)都可能是錯(cuò)的。2. GEMM 內(nèi)核的底層邏輯從最樸素循環(huán)到 Tensor Core 的關(guān)鍵躍遷2.1 最簡(jiǎn)單的 GEMM 內(nèi)核長(zhǎng)什么樣寫(xiě)一個(gè)在 CPU 上可以跑通的樸素版 GEMM只需要三重循環(huán)對(duì)每個(gè)輸出元素 C[i][j]累加 A 的第 i 行與 B 的第 j 列的內(nèi)積。拿到 GPU 上如果也照這個(gè)邏輯寫(xiě)結(jié)果就是災(zāi)難每個(gè)線程反復(fù)從全局內(nèi)存讀取同一段數(shù)據(jù)復(fù)用率趨近于零帶寬瞬間成為瓶頸。一個(gè) GPU 上有幾千個(gè)并發(fā)線程如果它們都在串行讀同一批 A 元素緩存會(huì)被完全沖垮。// 最樸素的 GEMM 思路每個(gè)線程算一個(gè)輸出元素 // 不考慮任何復(fù)用、布局、向量化僅作為反面教材 __global__ void naive_gemm_kernel(const float* A, const float* B, float* C, int M, int N, int K) { int row blockIdx.y * blockDim.y threadIdx.y; int col blockIdx.x * blockDim.x threadIdx.x; if (row M || col N) return; float sum 0.0f; for (int k 0; k K; k) { sum A[row * K k] * B[k * N col]; } C[row * N col] sum; }這段代碼每個(gè)線程要讀 K 個(gè) A 元素和 K 個(gè) B 元素如果一個(gè) warp 的 32 個(gè)線程各自負(fù)責(zé)相鄰 row 但相同 colB 的讀取在緩存層面還能有一定復(fù)用A 的讀取則完全重復(fù)。數(shù)據(jù)量稍微一漲DRAM 吞吐就直接封頂。真正高性能的 GEMM 內(nèi)核必須解決兩個(gè)核心問(wèn)題數(shù)據(jù)復(fù)用結(jié)構(gòu)怎么設(shè)計(jì)計(jì)算指令怎么選擇。2.2 計(jì)算強(qiáng)度FLOPs 再高也怕帶寬卡脖子判斷一個(gè)計(jì)算任務(wù)是否訪存密集還是計(jì)算密集關(guān)鍵看計(jì)算強(qiáng)度——單位字節(jié)的數(shù)據(jù)能支撐多少次浮點(diǎn)運(yùn)算。一個(gè) MNKn 的方陣乘法總計(jì)算量是 2n^3 FLOPs輸入數(shù)據(jù)量是 3n^2A、B、C 各一次讀寫(xiě)。把兩者相除計(jì)算強(qiáng)度等于 2n/3。n1024 時(shí)約 680 FLOPs/Byte遠(yuǎn)高于現(xiàn)代 GPU 的算訪比比如某 Hopper GPU 的 FP8 算力若按 1900 TFLOPS、HBM 帶寬按 3.35 TB/s算訪比約 570意味著理論上是計(jì)算密集型??涩F(xiàn)實(shí)中為什么很多 GEMM 跑不出峰值因?yàn)闆](méi)有有效的數(shù)據(jù)復(fù)用結(jié)構(gòu)實(shí)際從 DRAM 搬運(yùn)的字節(jié)數(shù)是理論值的幾十上百倍。所以優(yōu)化的本質(zhì)就是通過(guò)分塊讓數(shù)據(jù)盡可能留在片上。一個(gè)數(shù)據(jù)元素被讀到寄存器后如果能參與多次乘累加DRAM 的壓力就成倍下降。考慮 tiling 之后計(jì)算強(qiáng)度公式會(huì)變成一個(gè)關(guān)于 tile 尺寸的函數(shù)塊越大K 維循環(huán)中每個(gè)加載元素被使用的次數(shù)越多。2.3 三級(jí)存儲(chǔ)的 Tiling 協(xié)作寄存器、SMEM、GMEMGPU 芯片上的存儲(chǔ)層級(jí)可以類(lèi)比成廚房的操作臺(tái)全局內(nèi)存相當(dāng)于倉(cāng)庫(kù)共享內(nèi)存相當(dāng)于灶臺(tái)寄存器相當(dāng)于手邊已經(jīng)洗凈備用的配料。GEMM 內(nèi)核算法的核心就是設(shè)計(jì)一條從倉(cāng)庫(kù)批量取貨、放到灶臺(tái)上、再在手里快速加工的流水線。傳統(tǒng) CUDA 手動(dòng)調(diào)度路徑里一個(gè) CTA 通常負(fù)責(zé)一個(gè)輸出 tile先在共享內(nèi)存鋪好 A 和 B 的塊再由各線程從 SMEM 讀取數(shù)據(jù)做乘累加。Tile 尺寸的選擇直接決定復(fù)用率和硬件資源占用。常見(jiàn)策略是每個(gè) CTA 負(fù)責(zé) 128x128 或 128x256 的輸出 tileK 維每次處理 16 或 32。計(jì)算一下 SMEM 占用就知道A 和 B 兩個(gè) tile 若都是 128x32且 A、B 都是 1 字節(jié)的 FP8需要的共享內(nèi)存約 8KB如果上雙緩沖翻倍到 16KB。這個(gè)數(shù)字相對(duì)于一塊 Hopper GPU 上 228KB 的 SMEM 來(lái)說(shuō)非常充裕于是還可以進(jìn)一步加寬 N 方向的 tile用更多的片上數(shù)據(jù)換取更低的 DRAM 訪問(wèn)頻率。A tile 128 x 32 -- FP8: 128*32 4096 字節(jié) B tile 32 x 256 -- FP8: 32*256 8192 字節(jié) 雙緩沖總占用: 2 * (4096 8192) 24576 字節(jié) Hopper SMEM 典型容量約 228KB留白充足這個(gè)算術(shù)題看起來(lái)很樸素但它決定了內(nèi)核能否在寄存器、SMEM、GMEM 之間保持足夠深的流水線。數(shù)據(jù)一旦能在片上多待一輪全局內(nèi)存的帶寬壓力就小一輪。3. DeepGEMM 的實(shí)現(xiàn)路徑FP8 格式、Tensor Core 與 warp 分工3.1 FP8 的 E4M3 與 E5M2兩個(gè)尾數(shù)位的取舍FP8 不是一個(gè)單一的浮點(diǎn)格式它有 E4M3 和 E5M2 兩種子格式。E4M3 用 4 位指數(shù)加 3 位尾數(shù)取值范圍相對(duì)窄但精度更高E5M2 用 5 位指數(shù)加 2 位尾數(shù)動(dòng)態(tài)范圍大但尾數(shù)噪聲更明顯。做矩陣乘法時(shí)通常把激活值用 E4M3 存儲(chǔ)權(quán)重則根據(jù)是否需要大動(dòng)態(tài)范圍在 E4M3 和 E5M2 之間選擇。格式符號(hào)位指數(shù)位尾數(shù)位最大值范圍適用場(chǎng)景E4M3143約 448激活值、權(quán)重的主選E5M2152約 57344梯度、中間動(dòng)態(tài)范圍大的數(shù)據(jù)FP8 本身不能直接用于精度要求高的累加。實(shí)際計(jì)算必須以 FP32 作為累加器同時(shí)給輸入乘一個(gè)縮放因子scale。DeepGEMM 在加載數(shù)據(jù)后、送入 Tensor Core 之前會(huì)先檢查數(shù)據(jù)的整體范圍將 FP8 值反量化到實(shí)際浮點(diǎn)范圍。這個(gè)過(guò)程不是每元素做一次除法而是提前算好 scale最后在輸出端乘回來(lái)。這個(gè)設(shè)計(jì)在代碼層看起來(lái)只是多了兩個(gè)乘法但它決定了整個(gè)量化誤差的分布形態(tài)。3.2 Tensor Core 指令形態(tài)從 mma.sync 到 wgmma要真正把 FP8 算力燒起來(lái)光靠普通的 FMA 指令是不夠的。Tensor Core 本質(zhì)上是一組專用的矩陣乘累加硬件單元需要通過(guò)特定指令驅(qū)動(dòng)。在 Ampere 時(shí)代最直接的編程方式是 mma.sync 指令族比如 m16n8k16 這樣的形狀表示一次讓一個(gè) warp 協(xié)同完成 16x8x16 的矩陣乘片段。Hopper 時(shí)代則提供了更強(qiáng)的 wgmma 指令它不再以一個(gè) warp 為單位而是以一個(gè) warp group 為單位一次可以處理更大的矩陣塊并且支持異步流水線。DeepGEMM 的核心計(jì)算路徑選擇 wgmma 作為主用路徑。使用 wgmma 的好處顯而易見(jiàn)一次指令能覆蓋更大的輸出范圍硬件調(diào)度也允許與數(shù)據(jù)搬運(yùn)輸送完全重疊。但也帶來(lái)了一個(gè)副作用——warp 之間的同步和描述符管理變得復(fù)雜。一條 wgmma 指令需要引用 A、B 矩陣在共享內(nèi)存中的地址布局描述符這些描述符由 Tensor Core 的 TMA 硬件讀取若描述符內(nèi)部的 stride 或 swizzle 參數(shù)寫(xiě)錯(cuò)結(jié)果會(huì)非常離譜。3.3 生產(chǎn)者-消費(fèi)者分工TMA 搬運(yùn)與計(jì)算解耦Hopper 最值得利用的新特性之一就是 TMATensor Memory Accelerator。它允許你用一條指令把全局內(nèi)存中一個(gè) 2D 或 3D tile 直接搬到共享內(nèi)存還可以在搬運(yùn)過(guò)程中完成 swizzle 布局變換。這意味著傳統(tǒng)內(nèi)核里線程算地址、加載、再同步的模式被徹底改變了數(shù)據(jù)搬運(yùn)不再消耗計(jì)算線程的指令周期。DeepGEMM 把 warp 分成生產(chǎn)者和消費(fèi)者兩類(lèi)。生產(chǎn)者的職責(zé)只有兩個(gè)準(zhǔn)備 TMA 描述符、發(fā)布搬運(yùn)請(qǐng)求消費(fèi)者的職責(zé)是等待數(shù)據(jù)到達(dá)共享內(nèi)存然后啟動(dòng) wgmma 計(jì)算。這個(gè)模型很像工廠里的采購(gòu)員和裝配工采購(gòu)員只管把原料運(yùn)進(jìn)倉(cāng)庫(kù)裝配工不管原料怎么來(lái)的只等倉(cāng)庫(kù)里有貨就開(kāi)工。// 概念性偽代碼數(shù)據(jù)流水線主循環(huán) // 僅用于說(shuō)明生產(chǎn)者-消費(fèi)者結(jié)構(gòu)不表示完整內(nèi)核實(shí)現(xiàn) for (unsigned k_step 0; k_step K / K_BLOCK; k_step) { if (k_step 1 K / K_BLOCK) { // 生產(chǎn)者角色預(yù)取下一塊 A、B 到另一個(gè)共享內(nèi)存緩沖 tma_load_to_smem(A_smem[1 - cur], A_global_desc); tma_load_to_smem(B_smem[1 - cur], B_global_desc); } // 消費(fèi)者角色等待當(dāng)前緩沖數(shù)據(jù)就緒后啟動(dòng) wgmma wgmma_async(acc_smem, A_smem[cur], B_smem[cur], ...); }這個(gè)示例里的雙緩沖結(jié)構(gòu)把搬下一塊和算當(dāng)前塊完全重疊只要 TMA 的搬運(yùn)速度大于 wgmma 的消費(fèi)速度流水線就能一直保持滿載。實(shí)際工程里緩沖深度還可以加到三份甚至四份換取更大的容忍波動(dòng)能力。3.4 分布式共享內(nèi)存跨 CTA 協(xié)作的進(jìn)階玩法Hopper 還有一個(gè)容易被忽視的能力集群內(nèi)的多個(gè) CTA 可以組成一個(gè)線程塊集群通過(guò)分布式共享內(nèi)存機(jī)制互相讀取對(duì)方在共享內(nèi)存中的數(shù)據(jù)。這意味著一個(gè) CTA 計(jì)算輸出 tile 時(shí)不需要把完整的 A 或 B tile 都裝進(jìn)自己那一份 SMEM 里而是可以讓相鄰 CTA 把自己的 A tile 分享出來(lái)。DeepGEMM 在部分 shape 上啟用了這個(gè)功能。比如一個(gè)大矩陣的 N 方向被切成很多窄條相鄰 CTA 所需的 B tile 完全一樣此時(shí)讓其中一個(gè) CTA 加載并廣播給其他 CTA能省掉重復(fù)的 DRAM 讀取。這個(gè)特性讓 tile 切分策略有了新的維度不再單純?yōu)榱藴p少全局訪問(wèn)而把 SMEM 塞滿還可以通過(guò)集群通信實(shí)現(xiàn)數(shù)據(jù)共享。當(dāng)然分布式共享內(nèi)存也帶來(lái)同步復(fù)雜度???CTA 訪問(wèn)數(shù)據(jù)必須用集群級(jí)的 barrier 保證順序稍有不慎就會(huì)出現(xiàn)讀到了半成品數(shù)據(jù)的問(wèn)題。我的建議是第一版內(nèi)核先別碰這個(gè)特性等單 CTA 路徑跑穩(wěn)、性能基線確定后再作為進(jìn)階優(yōu)化加上去否則出現(xiàn)問(wèn)題時(shí)會(huì)很難分清是 TMA 描述符寫(xiě)錯(cuò)還是集群同步丟步。4. 性能調(diào)優(yōu)在 Nsight Compute 面前逐項(xiàng)逼出峰值4.1 第一輪先跑通基線版本記錄真實(shí)的短板任何優(yōu)化都離不開(kāi)可量化的起點(diǎn)。DeepGEMM 第一版是直接照搬常見(jiàn) CUTLASS 思路寫(xiě)出來(lái)的基礎(chǔ)雙緩沖版本沒(méi)有做 swizzle也沒(méi)有分布式共享內(nèi)存。在這種狀態(tài)下絕大多數(shù)矩陣 shape 是能跑對(duì)結(jié)果的但性能大概只有硬件峰值的 30% 左右。此時(shí)最該做的事不是繼續(xù)加特性而是用分析器看清瓶頸到底在哪個(gè)硬件單元。Nsight Compute 里有幾個(gè)關(guān)鍵指標(biāo)值得重點(diǎn)看sm__pipe_tensor_cycles_active反映張量核心忙碌程度l1tex__data_bank_conflicts反映共享內(nèi)存 bank 沖突嚴(yán)重性dram__throughput反映顯存壓力。第一版實(shí)測(cè)數(shù)據(jù)中tensor 核心利用率不高同時(shí) bank conflict 告警非常密集。這說(shuō)明當(dāng)前內(nèi)核的問(wèn)題在數(shù)據(jù)供給路徑而不是計(jì)算單元本身。這個(gè)判斷把優(yōu)化方向從改計(jì)算指令引導(dǎo)到了改數(shù)據(jù)布局。4.2 第二輪SMEM 地址布局與 bank conflict 的博弈共享內(nèi)存按 bank 組織同一地址周期只能讓每個(gè) bank 輸出一份數(shù)據(jù)。如果多個(gè)線程同時(shí)訪問(wèn)落在同一個(gè) bank 但不同地址的位置就會(huì)發(fā)生所謂的 bank conflict一次訪存被拆成多次。GEMM 這種高并發(fā)讀 SMEM 的負(fù)載對(duì) bank 布局極度敏感。最簡(jiǎn)單的布局下A tile 按行連續(xù)排列一行 128 字節(jié)。如果一行正好跨越了 32 個(gè) bank 的兩個(gè)周期而 warp 里的線程又在讀同一列的數(shù)據(jù)沖突就會(huì)出現(xiàn)。解決辦法是給地址加上一層 swizzle讓每個(gè)線程讀到的邏輯位置映射到不同的物理 bank。以常見(jiàn) 128 字節(jié)行寬為例可以按 8 行一組做異或擾動(dòng)// 常見(jiàn) swizzle 策略按行、列做 XOR 擾動(dòng) // 實(shí)際使用哪種模式取決于 tile 形狀和 SMEM 寬度 constexpr int SMEM_STRIDE 128; // 字節(jié) int swizzled_offset (row * SMEM_STRIDE) ^ ((col % 32) * 4);這行代碼看起來(lái)像是在亂改偏移但它把原本順序訪問(wèn)的模式打散成了跨 bank 的均勻分布。實(shí)測(cè)在加了 8 行 XOR swizzle 后bank conflict 基本消失張量核心利用率直接提升了一截。這個(gè)階段最值得留意的是不要憑直覺(jué)猜 swizzle 公式而是每次調(diào)整后都重新跑分析器看沖突計(jì)數(shù)是不是真的下降。4.3 第三輪雙緩沖升級(jí)為深度流水線延遲隱藏更進(jìn)一步雙緩沖能解決搬一陣、停一陣的交替問(wèn)題但還不能完全覆蓋 TMA 的啟動(dòng)延遲。TMA 搬運(yùn)一個(gè)大 tile 的延遲可能達(dá)幾百個(gè)周期如果流水線只有兩級(jí)消費(fèi)者等數(shù)據(jù)到達(dá)時(shí)仍會(huì)有明顯暫停。DeepGEMM 后續(xù)把緩沖數(shù)加到三份讓 TMA 始終在搬運(yùn)下一塊或下下一塊的數(shù)據(jù)消費(fèi)者計(jì)算當(dāng)前塊。從代碼結(jié)構(gòu)上講這需要維護(hù)一個(gè)下一塊指針隊(duì)列而不是簡(jiǎn)單地切換 cur 和 next。偽代碼上表現(xiàn)為// 三級(jí)緩沖的索引管理環(huán)形隊(duì)列 unsigned buf_idx (k_step % 3); unsigned next_idx ((k_step 1) % 3); unsigned after_next_idx ((k_step 2) % 3);三級(jí)緩沖換來(lái)的是更高的流水線余量讓 wgmma 和 TMA 的時(shí)序抖動(dòng)被緩沖吸收。實(shí)測(cè)三級(jí)緩沖相比二級(jí)在超大 K 的矩陣上提升了大約 8% 到 10%。這是因?yàn)?K 越大每個(gè)輸出 tile 需要連續(xù)搬運(yùn)的數(shù)據(jù)塊越多流水線越深越不容易把空缺暴露出來(lái)。至于要不要繼續(xù)加到四級(jí)、五級(jí)取決于 SMEM 容量預(yù)算畢竟還要給 wgmma 的累加結(jié)果留位置。4.4 實(shí)測(cè)數(shù)據(jù)不同 shape 下 DeepGEMM 與官方庫(kù)對(duì)比以下是某塊 Hopper 架構(gòu) GPU 上的實(shí)測(cè)對(duì)照數(shù)據(jù)。需要說(shuō)明的是官方庫(kù)的版本更新很快不同版本在同一 shape 上數(shù)據(jù)會(huì)浮動(dòng)這里只記錄當(dāng)時(shí)測(cè)試環(huán)境下的結(jié)果作為策略參考而非排名結(jié)論。矩陣規(guī)模 (M x N x K)實(shí)現(xiàn)方案耗時(shí) (ms)TFLOPS8192 x 8192 x 4096官方庫(kù) BF160.86約 6408192 x 8192 x 4096DeepGEMM FP8 雙緩沖0.54約 10198192 x 8192 x 4096DeepGEMM FP8 三緩沖swizzle0.49約 11231024 x 4096 x 2048官方庫(kù) FP80.041約 4191024 x 4096 x 2048DeepGEMM FP80.037約 464大矩陣規(guī)模下DeepGEMM 通過(guò)三緩沖和 swizzle 達(dá)到了硬件峰值的 50% 以上相比雙緩沖版本有明顯優(yōu)勢(shì)。但中小矩陣下差距就小很多了原因有兩個(gè)CTA 總數(shù)量不夠無(wú)法把所有 SM 都占滿TMA 的單次搬運(yùn)塊偏小延遲隱藏的空間有限。這也驗(yàn)證了之前定下的邊界主打超大矩陣小矩陣場(chǎng)景用簡(jiǎn)單路徑就夠了。5. FP8 運(yùn)算的精度陷阱兩個(gè)尾數(shù)位省下來(lái)的成本5.1 為什么 FP32 累加器并不能解決所有問(wèn)題FP8 的輸入只有 3 位尾數(shù)E4M3相乘結(jié)果的尾數(shù)位更是有限。即使 Tensor Core 支持 FP32 累加累加時(shí)的舍入順序依然會(huì)帶來(lái)浮點(diǎn)誤差。舉個(gè)例子連續(xù)累加幾千個(gè)數(shù)如果其中某個(gè)中間結(jié)果相對(duì)其他值大很多小值就會(huì)被大值完全淹沒(méi)這在浮點(diǎn)運(yùn)算中被稱為吸收消除。FP32 累加器可以緩解尾數(shù)截?cái)嗟珶o(wú)法修復(fù)輸入本身的量化噪聲。想進(jìn)一步提高數(shù)值穩(wěn)定性有兩條常用手段。其一是隨機(jī)舍入以數(shù)據(jù)低位為概率將結(jié)果向上下兩個(gè)方向隨機(jī)舍入這樣多次計(jì)算的平均誤差趨于零。其二是把累加順序做樹(shù)形規(guī)約在硬件允許范圍內(nèi)盡量讓相同量級(jí)的數(shù)先相加減少大數(shù)對(duì)小數(shù)的影響。DeepGEMM 的默認(rèn)路徑使用普通順序累加因?yàn)?Tensor Core 的硬件路徑已經(jīng)固定但在需要高精度的場(chǎng)景可以切換到隨機(jī)舍入模式。5.2 scale 設(shè)計(jì)per-tensor、per-channel 與在線量化scale 的選擇直接決定 FP8 的量化誤差。最簡(jiǎn)單的 per-tensor 方案是拿整個(gè)矩陣的最大絕對(duì)值去除得到一個(gè)統(tǒng)一的 scale。實(shí)現(xiàn)簡(jiǎn)單但如果激活值分布不均比如某些通道整體極小某些通道極大統(tǒng)一 scale 會(huì)讓小通道幾乎退化成零。策略開(kāi)銷(xiāo)誤差特點(diǎn)DeepGEMM 支持情況per-tensor最低分布不均時(shí)誤差大默認(rèn)per-channel中等對(duì)權(quán)重效果好激活仍需謹(jǐn)慎可配per-group (128元素一組)較高誤差最平穩(wěn)訓(xùn)練場(chǎng)景建議訓(xùn)練場(chǎng)景中在線量化需要在每次前向時(shí)動(dòng)態(tài)統(tǒng)計(jì) scale這個(gè)操作本身又增加了一輪歸約計(jì)算。DeepGEMM 在推理路徑上采用離線校準(zhǔn)的方式把 scale 在部署前算好在訓(xùn)練路徑上則按 128 元素分組做動(dòng)態(tài)統(tǒng)計(jì)配合隨機(jī)舍入讓 FP8 的精度損失接近 BF16 結(jié)果。5.3 實(shí)測(cè)失真的對(duì)照實(shí)驗(yàn)我在一個(gè)中等規(guī)模的 Transformer 訓(xùn)練任務(wù)上對(duì)比了三種方案。BF16 基線訓(xùn)練到固定步數(shù)后 loss 為 0.32FP8 per-tensor 方案為 0.35訓(xùn)練曲線尾部有抖動(dòng)FP8 per-group 加隨機(jī)舍入方案回到 0.33 附近收斂曲線與 BF16 基本貼合。這說(shuō)明 FP8 用在訓(xùn)練上完全可行前提是 scale 不能偷懶至少要做分組級(jí)的統(tǒng)計(jì)。推理階段相對(duì)寬松。固定權(quán)重的前提下per-tensor scale 帶來(lái)的精度損失可能只有幾個(gè)百分點(diǎn)對(duì)于大部分應(yīng)用可以接受。這里我給個(gè)實(shí)際建議如果你的部署模型對(duì)精度非常敏感先在驗(yàn)證集上同時(shí)跑 per-tensor 和 per-channel 兩組推理比較輸出分布的變化再?zèng)Q定是否需要更復(fù)雜的量化策略不要一上來(lái)就上最貴的方案。6. 排錯(cuò)實(shí)錄一次 K 維不對(duì)齊引發(fā)的 48 小時(shí)定位6.1 現(xiàn)象某些 shape 完全正常另一些隨機(jī)出錯(cuò)DeepGEMM 跑常規(guī) shape 時(shí)一直很穩(wěn)直到某次我測(cè)試 M6144、N6144、K1280 的矩陣乘法結(jié)果出現(xiàn)隨機(jī)性錯(cuò)誤。更奇怪的是M4096、N4096、K1024 時(shí)一切正常K1024 和 K1280 都是 32 的整數(shù)倍為什么一個(gè)對(duì)、一個(gè)錯(cuò)復(fù)現(xiàn)時(shí)還是概率性的有時(shí)連續(xù)跑五次錯(cuò)兩次有時(shí)全部正確非常折磨人。這種概率性出錯(cuò)通常指向兩種情況數(shù)據(jù)競(jìng)爭(zhēng)或者越界寫(xiě)入。如果只是算術(shù)邏輯錯(cuò)誤結(jié)果應(yīng)該是穩(wěn)定地錯(cuò)而不是隨機(jī)錯(cuò)。所以我第一時(shí)間把懷疑對(duì)象鎖定在 TMA 搬運(yùn)和共享內(nèi)存生命周期管理上而不是數(shù)學(xué)公式。6.2 排查鏈路從數(shù)據(jù)斷言到 TMA descriptor 邊界第一步是加數(shù)據(jù)斷言。我在每個(gè) k_step 之后用 CPU 復(fù)算同一塊輸出和 GPU 結(jié)果比對(duì)發(fā)現(xiàn)偏差出現(xiàn)在特定的 tile 組合上而且往往發(fā)生在后半段 K 循環(huán)。接著關(guān)閉雙緩沖把流水線退化成搬一塊、算一塊、再搬一塊錯(cuò)誤出現(xiàn)的概率降低但沒(méi)消失。這一步排除了純粹的雙緩沖競(jìng)爭(zhēng)——若是生產(chǎn)者消費(fèi)者同步問(wèn)題單緩沖下應(yīng)該完全消失。第二步是用內(nèi)存檢測(cè)工具跑同一 case。工具很快報(bào)出共享內(nèi)存越界寫(xiě)入的警告指向一個(gè) CTA 在某次循環(huán)里寫(xiě)到了自己 SMEM 范圍之外。順著這個(gè)線索我開(kāi)始逐行檢查 TMA 描述符的邊界計(jì)算尤其是 K 維循環(huán)接近末尾時(shí)的 tile 尺寸處理。最后發(fā)現(xiàn)問(wèn)題出在一個(gè)邊界分支里當(dāng) K 的剩余長(zhǎng)度不足一個(gè) K_BLOCK 時(shí)代碼仍然用了完整的 K_BLOCK 去構(gòu)造描述符導(dǎo)致 TMA 讀取越界數(shù)據(jù)同時(shí)把共享內(nèi)存寫(xiě)壞。6.3 根因tile 邊界的回卷寫(xiě)覆蓋與同步缺失為什么 K1024 正常而 K1280 就出問(wèn)題因?yàn)?1024 除以 32 得到 32 步循環(huán)干凈利落1280 除以 32 也是 40 步但某些 CTA 因?yàn)檩敵?tile 在 M、N 方向靠邊剩余的 A 或 B 數(shù)據(jù)不足 32走了退化路徑這時(shí)候計(jì)算 K 塊尺寸的偏移量出現(xiàn)了偏差。這個(gè)偏差在地址計(jì)算上表現(xiàn)為回卷某個(gè)索引在本應(yīng)結(jié)束的地方繼續(xù)向后延伸覆蓋了相鄰緩沖區(qū)的數(shù)據(jù)。修復(fù)方案有兩步。第一步是讓每個(gè) K 步都用實(shí)際剩余長(zhǎng)度裁剪 TMA 描述符而不是盲信 K_BLOCK 常量第二步是給退化路徑單獨(dú)加一道 barrier確保最后一個(gè)未滿 K 塊在寫(xiě)入 SMEM 后所有消費(fèi)者 warp 都能看到完整數(shù)據(jù)才啟動(dòng) wgmma。6.4 同類(lèi)隱患L2 hint、原子累加與分配器對(duì)齊這次排錯(cuò)讓我意外收獲了一串同族隱患。第一個(gè)是 L2 緩存策略TMA 搬運(yùn)時(shí)可以指定數(shù)據(jù)在 L2 中的偏好如果對(duì)輸出 tile 設(shè)置了錯(cuò)誤的淘汰優(yōu)先級(jí)某些數(shù)據(jù)可能被過(guò)早逐出在后續(xù)重算時(shí)引發(fā)不一致。第二個(gè)是多個(gè) CTA 對(duì)同一輸出區(qū)域的原子累加FP32 原子加不保證順序結(jié)果浮點(diǎn)誤差會(huì)在不同 run 間波動(dòng)。第三個(gè)是共享內(nèi)存分配的對(duì)齊問(wèn)題128 字節(jié)對(duì)齊和 4 字節(jié)對(duì)齊在 TMA 描述符眼里完全不同寫(xiě)錯(cuò)一個(gè)字段就可能導(dǎo)致跨邊界讀。疑似問(wèn)題排查手段結(jié)論雙緩沖競(jìng)爭(zhēng)切換單緩沖復(fù)測(cè)排除數(shù)據(jù)取值邏輯CPU 復(fù)算分組比對(duì)定位到特定 tileSMEM 越界內(nèi)存檢測(cè)工具掃描確認(rèn)越界TMA 描述符邊界逐行檢查 K 剩余長(zhǎng)度根因K 塊未裁剪如果你也遇到類(lèi)似小矩陣沒(méi)問(wèn)題、大矩陣偶發(fā)錯(cuò)的 case我的建議是直接運(yùn)行內(nèi)存檢測(cè)工具再配合單緩沖復(fù)測(cè)把手動(dòng)同步的變量排除干凈。這類(lèi)問(wèn)題最怕一上來(lái)就猜是浮點(diǎn)誤差把方向帶偏。拿到確鑿的越界報(bào)告后再回頭檢查所有描述符、指針、步長(zhǎng)參數(shù)往往比自己盯著代碼發(fā)呆更高效。最后再分享一個(gè)實(shí)際觀察GEMM 內(nèi)核的調(diào)試和調(diào)優(yōu)要分開(kāi)對(duì)待。調(diào)優(yōu)階段可以大膽試各種 swizzle 和緩沖深度因?yàn)殄e(cuò)誤是好觀測(cè)的但一旦進(jìn)入排錯(cuò)階段先復(fù)算簡(jiǎn)單路徑、再上檢測(cè)工具、最后才動(dòng)代碼這個(gè)順序不要亂。跑大量邊界 shape 的矩陣乘法作為回歸用例比單點(diǎn)性能數(shù)據(jù)更值得長(zhǎng)期維護(hù)——像 K 維不整除、N 方向剩余窄條這類(lèi)隱蔽問(wèn)題都是靠邊界用例抓出來(lái)的。