Skip to content

Q&A 問答集

關於 CUDA 程式設計

Q: 為什麼 llm.c 的 matmul 使用 cuBLASLt 而不是手寫 kernel?

A: 矩陣乘法是 GPU 上經過數十年優化的操作,cuBLAS 使用了大量手調的 tile size、registers 使用和排程策略,很難在 kernel 中手動復現。Karpathy 的設計哲學是:使用 cuBLAS 作為「專家上限」,手寫 kernel 則放在 dev/ 目錄供教學參考。

Q: 為什麼 LayerNorm 有 kernel3 和 kernel6 兩個版本?

A: Kernel3 (無 shared memory) 是簡單版本,kernel6 將 weight/bias 載入 shared memory 並使用 vectorized load/store 和 streaming store 提示。Kernel6 需要較大的 shared memory,如果硬體不支援則 fallback 到 kernel3。

Q: __ldcs__stcs 有什麼作用?

A: 這些是 CUDA 的 streaming 指令,提示編譯器該資料的存取模式是一次性的(不會很快重複使用),因此可以繞過 L1 快取直接與 L2/DRAM 通訊。這可以減少快取汙染,讓更需要快取的資料(如 weight/bias)有更高的 hit rate。

關於 GPU 優化

Q: 為什麼需要 warpReduceSum

A: 在 LayerNorm forward 中,需要對 C 通道維度求和來計算 mean 和 variance。由於 C=768,且 WARP_SIZE=32,每個 thread 處理 C/32=24 個元素。Warp 級別的 reduce 可以在一個 warp 內完成求和,不需 shared memory 或全域記憶體同步,非常高效。

Q: x128 vectorized load/store 怎麼加速?

A: 一次載入 4 個 float (128-bit),將 4 次記憶體存取合併為 1 次,減少指令數量並提高記憶體頻寬利用率。原本需要 4 個 32-bit load 指令,現在只需要 1 個 128-bit load 指令。

Q: 為什麼要填充 vocab size 到 50304?

A: 50304 = 50257 + 47,填充到 128 的倍數。CUDA kernel 常使用 WARP_SIZE (32) 的倍數來設計,讓每個 thread 處理對齊的資料量。填充後尾部不需要特殊處理,kernel 設計更簡潔。

關於訓練流程

Q: 為什麼 backward 使用 += 而不是 =

A: 如果一個變數在計算圖中被多次使用,梯度需要累加。例如 residual stream 的梯度會來自兩個分支(主路徑和短路徑),需要累加。儘管在 GPT-2 中沒有複雜的分支,但為了正確性,作者統一使用 +=

Q: recompute 選項的原理?

A: 以 --recompute 1 為例,forward 時不儲存 GeLU 的輸出,backward 時從 fch 重新計算 GeLU forward 再算 backward。雖然增加了計算,但節省了 L * B * T * 4*C 的記憶體空間。對於 L=12, B=4, T=1024, C=768,約省 150MB VRAM。

Q: Multi-GPU 訓練中 gradient accumulation 怎麼運作?

A: 每個 GPU 載入不同的 micro-batch → forward/backward 得到梯度 → NCCL AllReduce 平均梯度 → 每個 GPU 用平均梯度更新權重。Gradient accumulation 則是在單 GPU 上多次 forward/backward 後才 AllReduce,讓有效 batch size 可以更大。