iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
Software Development

GPU 效能優化實戰:30 天從 Kernel 到 Profiling (重賽版)系列 第 19

Day 19:比較標的、profile,還有為什麼不要只看速度表 )重賽版(

  • 分享至 

  • xImage
  •  

今天還是AI slob 我如果之後有寫好 改完 會附上更正是寫作版本的連結ㄉ
希望這不是ㄍflag


前面三天把三條路拆開了。Day 16 是一個 block 裡的 butterfly,Day 17 是 large-N 的兩次 pass,Day 18 是把 Hadamard 改寫成 Tensor Core GEMM。

今天要比。但 GPU kernel 很容易寫出一個看起來很快、其實沒有做同一件事的版本。Day 1 講過:沒有比較對象的 100 ms,不是快也不是慢。今天補一句:比較對象如果合約不同,那張速度表一樣不能用。


先把合約釘死

Hadamard / FWT 至少要對齊:

同一個 N、同一個 batch、同一個 dtype、同一個 input layout,以及 同一個 normalization

有人算 H^{⊗N} x,有人算 (1 / sqrt(2^N)) * H^{⊗N} x。差一個 scale。沒對齊的話,速度和誤差都沒有意義。

output 也要講清楚比的是哪一種:

bitwise 完全相同          最強
max diff 在 tolerance 內  合理
norm 有沒有守住           只能當 sanity check

FWT 全是加減,看起來應該 bitwise 對得死死的。可是 FP32 加法不是實數加法。butterfly 順序不同,rounding 路徑就可能不同。所以下面表裡有些 diff 不是 0,不一定是算錯。

baseline 也不只一種。naive / PyTorch 用來對答案,不拿來比速度。Dao-AILab 的 fast-hadamard-transform 是外面很強的 GPU 標的,尤其 N <= 15,精神跟 FlashAttention 很像:資料留在 register,必要時才用 shared memory 換位置。我們自己的 native、TF32、Ozaki,要在同一個合約下互相比。

下面兩張表不是同一個 harness 一次跑完。第一張是跟 Dao 對齊的 FP32;第二張是 Tensor Core 路徑的對打。分開寫,就是為了不要把不同合約的數字塞進同一格。


表一:跟 Dao 比(FP32, batch = 128)

這張表的合約是:FP32、batch=128、同一個 normalized transform。Dao 在 N >= 16 撐不住,那格就留空。

N native (us) Dao (us) 相對 Dao max diff
10 14.37 21.70 1.51× 7.4e+00
12 11.34 19.65 1.73× 7.2e+00
14 52.91 37.89 0.72× 8.6e+00
15 82.98 98.35 1.19× 0.0e+00
16 584.56 Dao 不支援
20 14710 Dao 不支援

先看 N=15。82.98 對 98.35,而且 diff 是 0。這不是「差不多對」。這是 butterfly 順序也對齊了。明天會看到,這顆是後來才切出來的 single-pass;對齊 Dao 不是巧合。

再看 N=10 / 12 / 14。我們有的比 Dao 快,N=14 卻輸了,diff 也不是 0。輸的原因和 diff 非零,很可能是同一件事:這些 N 的 kernel 還沒改成跟 Dao 同一套 ordering / 同一套 register 切法。數學上等價,bitwise 不等價,速度也不在同一條曲線上。

N=16 從 82.98 us 跳到 584.56 us。Day 17 算過:batch=128 時 state 大約 33.5 MB,兩次 pass 大約 134 MB traffic,有效頻寬大約 229 GB/s。這格比的不是加減法,是「一個 block 放不下之後,多付一次 global memory」。Dao 在這裡直接不支援,所以沒有對手,不代表我們已經最快,只代表比較對象先離開了。


表二:Tensor Core / Ozaki 對 native(約 N=14)

這張表回答另一個問題:同一題 Hadamard,改寫成 GEMM 之後會怎樣。dtype 和 harness 跟表一不同,不要拿 40 us 去跟上面的 52.91 us 比快慢。看的是同一張表裡的倍數。

寫法 時間 在比什麼
native FP64 ~600 us ALU butterfly
dense Ozaki GEMM ~314,000 us 把 H 當真的 dense 矩陣乘
radix-128 Ozaki ~900 us H_7 ⊗ H_7 + INT8/CRT
native FP32 ~40 us ALU butterfly
TF32 + transpose ~380 us 兩次小 GEMM + 轉置

dense Ozaki 慢五百倍,方向錯了。radix-128 把結構用上,從「慢五百倍」拉回「慢 1.5 倍」,還是沒贏。TF32 拿掉 CRT,仍然慢大約九倍。

三行一起看,結論就收得很窄:不是 INT8 轉換獨有的問題,也不是「我們 MMA 寫得不夠好」就能翻盤。只要為了 Tensor Core 多做 layout / transpose,FWT 這種加減法就付不起。


N=14 的 NCU:不要把 62% 讀成還能再快 1.6 倍

表一 N=14 的 52.91 us,kernel 本身大約 50.37 us。NCU 看到的是:

SM throughput     ~62%
Memory throughput ~62%
L2 hit            ~87%
Branch efficiency 100%
理論 occupancy    ~16.7%(每個 SM 幾乎只能放 1 個 block)

NCU 自己的判定是 compute 和 memory 已經接近平衡。FWT 算術這麼便宜,能推到這種平衡已經不容易。那 38% 不是「再 unroll 一下就能拿回來」。

occupancy 低也要分開看。N=14 的 chunk 大約 64 KB,shared memory 加上 register 之後,一個 SM 塞不下很多 block。它用 occupancy 換的是:資料留在 chip 上,少一次 global memory 來回。如果為了 occupancy 把每個 thread 拿的資料切少,結果多出一個 pass,表一的 N=16 就是那種帳單。

還有一件事一定要查:local memory load/store。N=15 每個 thread 拿 32 個 float4,也就是 128 個 float。compiler 一旦 spill,register 設計就偷偷變回比較慢的 memory path。速度表看不出這件事,profile 才看得出。


Tensor Core 那條路,profile 要看整條 pipeline

表二的 TF32 慢九倍,不該只打開 Tensor Core utilization。MMA 可能很忙,時間卻花在 MMA 外面:

input 排成 tile
Hadamard block 準備好
第一次小 GEMM 寫出
transpose
第二次小 GEMM
再轉回 output layout
Ozaki 的話,還要 conversion + CRT

end-to-end 要從原始 input 走到最後 output。有多少時間花在 conversion 而不是 MMA,有多少 access 不連續,這才是表二那幾個倍數的來源。Day 18 已經把這條資料流拆過;今天只是把它放到比較桌上,避免只截 MMA 那一小段來說「Tensor Core 很快」。


這兩張表合在一起在說什麼

表一說:跟外面最強的 FWT baseline 比,小 N 有贏有輸;N=15 對齊之後才穩定贏;N=16 開始付 2-pass 的 memory 錢,Dao 也不再是對手。

表二說:把同一題改寫成 Tensor Core GEMM,不管有沒有 CRT,都還沒贏過直接做 butterfly。

所以 FWT 的主戰場不是 Tensor Core TFLOPS。是資料流。速度表要先問「算的是不是同一件事」,profile 要再問「時間花在算還是花在搬」。

下一篇做最後一件事:表一裡 N=15 為什麼後來才贏。切之前是 10+5 兩個 pass,大約 278 us;切之後是一個 block 做完,82.98 us。把 before / after 攤開,重切這件事才看得到。


上一篇
Day 18:把 Hadamard 塞進 Tensor Core,問題在哪裡 {重賽版}
下一篇
Day 20:再做一次比較激進的版本]重賽版[
系列文
GPU 效能優化實戰:30 天從 Kernel 到 Profiling (重賽版)26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言