當執行 CUDA 核心時會發生什麼事:從原始碼到 SASS
當執行 CUDA 核心時會發生什麼事:從原始碼到 SASS
啟動 CUDA 核心涉及主機 CPU、NVIDIA 使用者模式驅動程式、核心模式驅動程式與 GPU 硬體之間的複雜協調。此過程會將高階 C++ 程式碼轉換為硬體特定指令與 SASS 指令的串流,並利用專門的記憶體映射門鈴系統在成千上萬的平行執行緒上觸發執行。
編譯管線:PTX 與 SASS
執行 CUDA 程式需要多個編譯階段,以彌合裝置無關程式碼與硬體特定機器指令之間的差距。
nvcc 充當協調多個編譯器的驅動程式。主機程式碼會送交一般的主機編譯器,而裝置程式碼則走一條特定路徑:cicc(基於 LLVM 的編譯器)產生 PTX(Parallel Thread Execution),接著由 ptxas 轉換成 SASS(Streaming Assembler)。
PTX 與 SASS
- PTX 是一種虛擬指令集架構(ISA)。它與裝置無關,使用無限數量的型別化暫存器。它作為向前相容的備援;如果二進位在未包含預編譯 SASS 的 GPU 架構上執行,驅動程式可以在載入時即時編譯 PTX 成新的 SASS。
- SASS 才是真正會在 GPU 上執行的機器碼。它將虛擬暫存器映射到有限的實體暫存器,並將複雜的 PTX 操作融合成單一硬體指令(例如
IMAD.WIDE)。
最終的二進位是一個 "fatbin",其中同時包含 SASS(以 ELF 容器形式)與壓縮的 PTX。這個 fatbin 會嵌入主機可執行檔的 .nv_fatbin 區段。
從主機觸發 GPU
由於 GPU 位於 PCIe 匯流排的另一端,CPU 無法直接 "呼叫" GPU 函式。相反地,它必須透過驅動程式管理的工作佇列進行通訊。
主機啟動 Stub
當 nvcc 編譯一個核心啟動(例如 vadd<<<4096, 256>>>)時,它會將此表達式取代為產生的主機啟動 stub。此 stub 會把核心參數打包到主機記憶體中的緩衝區,依特定的位元組偏移儲存。接著呼叫 __cudaLaunch,該函式使用主機端的函式指標作為查找鍵,以在 fatbin 中找到對應的裝置端符號。
驅動程式橋接
CUDA 執行時庫會與封閉原始碼的使用者模式驅動程式(libcuda.so)互動,後者再透過對 /dev/nvidiactl 等裝置檔的 ioctl 呼叫與核心模式驅動程式(nvidia.ko)通訊。
自 CUDA 12.2 起,模組載入預設為 lazy(延遲)模式。驅動程式會延後將 SASS cubin 上傳至 GPU 記憶體,直到第一次實際啟動該核心為止。
硬體啟動機制
GPU 執行是由一串從主機記憶體讀取的指令驅動,透過一個由三個主要結構組成的「通道」協調:
- Pushbuffer:主機記憶體中的一塊區域,驅動程式在此寫入「methods」(暫存器位址與值),定義 GPU 的動作。
- GPFIFO:指標環形緩衝區,告訴 GPU 要從 pushbuffer 的哪段區域讀取。
- USERD:裝置記憶體中的小結構,包含游標(
GP_GET與GP_PUT)以追蹤工作消耗情形。
門鈴
現代 GPU(Turing 及之後)不會偵測 GP_PUT 游標的變化。相反地,驅動程式會透過寫入工作提交代幣的方式 敲響門鈴——一個記憶體映射暫存器。這會通知 GPU 的主機引擎去抓取更新後的 GP_PUT,並透過 DMA 從 pushbuffer 拉取 methods。
佇列中繼資料 (QMD)
最關鍵的 methods 之一是 Queue Meta Data (QMD) 的串流。QMD 是啟動描述子,告訴 GPU:
- 網格與區塊的維度(例如 4096 個區塊、每個區塊 256 個執行緒)。
- 每個執行緒所需的暫存器與共享記憶體。
- SASS 程式碼的記憶體位址。
- 保存核心參數的常數區位址。
在串流多處理器 (SM) 上執行
當 QMD 交給計算工作分配器(GigaThread Engine)後,GPU 會將線性 SASS 指令映射到可用的串流多處理器 (SM) 上。
資源限制與佔用率
在 RTX 4090(AD102)上,SM 受執行緒容量與暫存器檔大小限制。若核心每個執行緒使用 16 個暫存器且每個區塊有 256 個執行緒:
- 暫存器容量:$65,536 / (256 \times 16) = 16$ 個區塊。
- 執行緒容量:$1,536 / 256 = 6$ 個區塊。
執行緒容量是較緊的瓶頸,意味著每個 SM 最多只能容納 6 個駐留區塊(48 個 warp)。
Warp 可執行性與延遲隱藏
與 CPU 不同,GPU 不使用複雜的亂序執行邏輯。它們透過在多個駐留 warp 之間切換來隱藏延遲。warp 是否 可執行 取決於 ptxas 在 SASS 指令中打包的控制碼負載:
- 靜態停頓計數:對於固定延遲的操作,編譯器會編碼 warp 必須停留的確切週期數。
- Yield Hint:一個提示位元,建議排程器優先執行其他 warp。
- 相依性障礙索引:對於可變延遲的操作(如全域記憶體載入
LDG),硬體使用六個實體 scoreboard 障礙。warp 會在其等待的特定障礙被清除前保持不可執行。
記憶體階層與資料傳輸
當 warp 發出載入指令(LDG.E)時,SM 的載入/儲存單元會執行 請求合併。若 warp 中的 32 個執行緒存取連續的 4 位元組浮點數,硬體會將這些請求合併成四個 32 位元組的區段請求,以最小化匯流排流量。
資料流向為 L1 資料快取 → L2 快取 → 記憶體控制器 → GDDR6X VRAM。對於算術密度低的核心(如向量加法),效能通常受 DRAM 匯流排頻寬限制,而非計算能力。
將結果返回給 CPU
最後一個區塊完成後,GPU 會發布一個完成信號量(在 QMD 中定義)。GPU 的拷貝引擎隨即執行 DMA,將結果從 L2 快取(或 VRAM)傳回主機記憶體。傳輸完成後,它會再發布自己的信號量,使主機端的 cudaMemcpy 呼叫得以返回,CPU 重新取得執行權(例如呼叫 printf)。
摘要: 深入探討 CUDA 核心的生命週期,從 nvcc 編譯、PTX/SASS 產生一路追溯到 RTX 4090 上的硬體層級執行。
標題: 當執行 CUDA 核心時會發生什麼事:從原始碼到 SASS