iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
AI Engineering

30 天 GPU LeetCode 挑戰:從 CUDA 新手到 Kernel Leaderboard系列 第 2

Day 2|從 CPU 到 SM:一次看懂 CUDA 的硬體模型、Warp 與 GPU Memory

  • 分享至 

  • xImage
  •  

昨天先把第一個 CUDA 程式跑起來之後,今天想把「GPU 到底怎麼執行這些程式」補起來。

一開始看 CUDA Programming Guide,很容易被 kernelthreadblockgridSMwarpshared memory 這些名詞轟炸。

其實可以先拆成三件事:

  • 工作怎麼切:Thread → Block → Grid
  • 硬體怎麼跑:Block → SM → Warp 執行
  • 資料放哪裡:Register / Shared Memory / Cache / GPU DRAM

把這三條線串起來,後面學 CUDA optimization 會容易很多。


1. CUDA 是 CPU + GPU 一起工作的模型

CUDA 預設的是 heterogeneous system(異質運算系統)

  • Host:CPU 與 CPU 直接連接的記憶體
  • Device:GPU 與 GPU 直接連接的記憶體
  • CUDA application 會先從 CPU 開始執行
  • CPU 可以:
    • 搬資料到 GPU
    • 啟動 GPU 工作
    • 等待 GPU 工作完成

簡化來看:

CPU / Host
    │
    │ PCIe / NVLink
    ▼
GPU / Device

一般桌機中:

  • System DRAM:平常講的系統 RAM,例如 DDR5
  • GPU DRAM:GPU 使用的記憶體,也就是常說的 VRAM,例如 GDDR6X
  • PCIe / NVLink:CPU、GPU 或 GPU 彼此之間的高速連線

2. Device Code、Kernel 與 Kernel Launch

CUDA 中:

  • 跑在 CPU 的程式稱為 host code
  • 跑在 GPU 的程式稱為 device code
  • 被啟動、交給 GPU 執行的函式稱為 kernel
  • 啟動 kernel 的動作稱為 kernel launch

例如:

__global__ void add(float *a, float *b, float *c) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    c[i] = a[i] + b[i];
}

啟動:

add<<<gridSize, blockSize>>>(a, b, c);

它不是單純讓 GPU 執行一次 add(),而是:

Kernel Launch
     ↓
啟動大量 GPU Threads
     ↓
每個 Thread 執行同一份 Kernel Code

每個 thread 再透過自己的 index,決定自己要處理哪一筆資料。


3. GPU 裡最重要的硬體單位:SM

從 CUDA programming model 的角度,可以把 GPU 想成很多個:

SM(Streaming Multiprocessor)

簡化後:

GPU
├── GPC
│   ├── SM
│   ├── SM
│   └── ...
├── GPC
│   ├── SM
│   └── ...
└── GPU Memory

其中:

  • GPC(Graphics Processing Cluster)
    • 一組 SM 的集合
  • SM
    • 真正執行大量 CUDA threads 的主要硬體單位

每個 SM 裡會有:

  • Register File
  • Unified Data Cache
  • Shared Memory / L1 Cache 的實體資源
  • 各種運算單元
  • Warp scheduling 相關硬體

初學時可以先記:

CUDA 把工作切成很多 Thread Blocks,再把 Blocks 排到各個 SM 上執行。


4. Thread、Block、Grid 是什麼?

一次 kernel launch 通常會建立大量 threads。

CUDA 會把它們分組:

Grid
├── Thread Block
│   ├── Thread
│   ├── Thread
│   └── ...
├── Thread Block
└── ...

最基本的層級:

Grid
  ↓
Thread Block
  ↓
Thread

為什麼可以有 1D、2D、3D?

主要是讓 thread index 更容易對應資料的形狀。

例如:

  • 1D Array → 1D Threads
  • Image / Matrix → 2D Threads
  • 3D Volume → 3D Threads

處理圖片時:

int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;

就可以直接理解成:

Thread(x, y)
    ↓
Pixel(x, y)

這些維度是方便 programmer 使用的 programming abstraction,不代表 GPU 實體真的排成二維或三維。


5. Thread 怎麼知道自己要處理哪筆資料?

CUDA 提供幾個 built-in variables:

變數 功能
threadIdx Thread 在 Block 裡的位置
blockIdx Block 在 Grid 裡的位置
blockDim 一個 Block 的大小
gridDim 整個 Grid 的大小

最常見的一維 global index:

int i = blockIdx.x * blockDim.x + threadIdx.x;

如果一個 block 有 256 threads:

Block 0, Thread 0   → i = 0
Block 0, Thread 255 → i = 255

Block 1, Thread 0   → i = 256
Block 1, Thread 255 → i = 511

因此每個 thread 都能取得自己的 global identity,再決定自己負責哪筆資料。


6. Thread Block 與 SM 的關係

這是 CUDA 很重要的一條規則:

同一個 Thread Block 的所有 Threads 都會在同一個 SM 上執行。

原因是同一個 block 裡的 threads 需要能快速:

  • 共用 Shared Memory
  • 交換資料
  • 做 synchronization

概念上:

SM 0
├── Block A
│   ├── Thread
│   ├── Thread
│   └── ...
├── Block B
└── ...

注意兩件事:

  • 一個 Block 不會被拆到多個 SM
  • 一個 SM 可以同時容納多個 Blocks

實際可以同時放多少 blocks,會受到:

  • Thread 數量
  • Register 使用量
  • Shared Memory 使用量
  • GPU 架構限制

影響。


7. Grid 可以很大,因為 Blocks 可以分批執行

Grid 可能有幾十萬甚至幾百萬個 blocks,但 GPU 可能只有幾十或幾百個 SM。

例如:

1,000,000 Blocks
       ↓
   Scheduler
       ↓
   100 個 SM

GPU 會:

第一批 Blocks
      ↓
在 SM 上執行
      ↓
完成、釋放資源
      ↓
下一批 Blocks

而且 CUDA 不保證不同 Blocks 的執行順序

所以:

Block 0
Block 1
Block 2
Block 3

實際上可能:

Block 2 → Block 0 → Block 3 → Block 1

也可能同時平行執行。

因此 CUDA 的基本原則是:

不同 Thread Blocks 應該盡量彼此獨立,不依賴其他 Block 的執行順序。

這也是為什麼同一份 CUDA kernel,可以從少量 SM 的 GPU 一路 scale 到大量 SM 的 GPU。


8. Thread Block Cluster:Block 之上的額外分組

一般情況下,不同 blocks 應該彼此獨立。

但在 Compute Capability 9.0 以上的 GPU,CUDA 還提供一個額外層級:

Thread Block Cluster

可以把它想成:

Grid
├── Cluster
│   ├── Block
│   ├── Block
│   └── Block
└── Cluster

同一個 Cluster 裡的 Blocks:

  • 會被安排在同一個 GPC
  • 可以使用 Cooperative Groups 做跨 Block synchronization
  • 可以存取 Cluster 中其他 Block 的 Shared Memory
    • 稱為 Distributed Shared Memory

這算是前面「Block 彼此獨立」的一個進階例外。

初學 CUDA 先知道有這個概念即可。


9. Warp 與 SIMT:GPU 真正怎麼執行 Threads?

雖然我們寫程式時看到的是一個個 thread,但 GPU 不會單獨排程每一個 thread。

在一個 Thread Block 中,threads 會被分成:

Warp = 32 Threads

例如:

Block:256 Threads

Warp 0 → Thread 0  ~ 31
Warp 1 → Thread 32 ~ 63
Warp 2 → Thread 64 ~ 95
...

每個 thread 在 warp 裡的位置稱為 warp lane

lane 0 ~ lane 31

CUDA 採用的是:

SIMT(Single Instruction, Multiple Threads)

可以先口語理解成:

一組 32 個 threads,大部分時間一起執行同一條 instruction,但每個 thread 還是保有自己的資料與控制流程。


10. Warp Divergence 是什麼?

假設:

if (threadIdx.x % 2 == 0) {
    doSomething();
}

一個 warp 裡:

Thread 0  → 執行
Thread 1  → 不執行
Thread 2  → 執行
Thread 3  → 不執行
...

因為同一個 warp 原本希望一起執行相同 instruction,所以 GPU 會把目前不走這條 branch 的 lanes 暫時 mask off

概念上:

Warp 32 Threads
│
├── 16 Threads 執行 if
└── 16 Threads 暫時 inactive

這種情況稱為:

Warp Divergence

如果同一個 warp 裡大家走不同 branch,GPU 的硬體利用率通常會下降。

因此:

同一個 Warp 的 Threads 越常走相同的 control flow,通常越有效率。

另外,一個 Thread Block 的 thread 數量通常建議是 32 的倍數

例如:

128
256
512

因為如果 block 有 100 threads:

Warp 0 → 32
Warp 1 → 32
Warp 2 → 32
Warp 3 → 只剩 4 Threads

最後一個 warp 還是存在,只是很多 lanes 沒有工作。


11. SIMT 與 SIMD 有什麼差?

兩者名字很像:

  • SIMD:Single Instruction Multiple Data
  • SIMT:Single Instruction Multiple Threads

差異可以先簡單理解成:

  • SIMD 比較像「一條 instruction 直接處理一組固定寬度的 data」
  • SIMT 則是「很多獨立 threads 一起執行,但每個 thread 還是有自己的狀態與 control flow」

CUDA programmer 平常主要用 Thread 的角度寫程式,而不是手動操作一個固定寬度的 SIMD vector。


12. Tile Programming:比 Thread 更高階的寫法

新版 CUDA Programming Guide 還介紹了另一種模式:

Tile Programming

傳統 SIMT:

Programmer
↓
自己寫每個 Thread 要做什麼
↓
Thread 操作資料

Tile Programming:

Programmer
↓
描述整個 Block 要對一塊 Tile 做什麼
↓
Compiler
↓
自動分配給 Block 裡的 Threads

例如 programmer 可以描述:

  • Tile element-wise operation
  • Matrix multiplication
  • Reduction
  • Reshape / Transpose

比較重要的是:

Block 是執行工作的單位,Tile 是資料的單位,兩者不要混在一起。

Tile Programming 不會取代 SIMT。

  • 想要細粒度控制 thread → SIMT
  • 想要用更高階方式描述整塊資料 → Tile Programming

對 CUDA 初學者來說,目前還是先把 SIMT / Thread / Block / Warp 搞懂比較重要。


13. GPU Memory:資料到底放在哪裡?

GPU performance 不只看算力,memory 怎麼用通常一樣重要

先把 CUDA 裡最常見的 memory hierarchy 畫出來:

Register
   ↓
L1 Cache / Shared Memory
   ↓
L2 Cache
   ↓
GPU DRAM / Global Memory

通常越上面:

  • 越靠近運算單元
  • 容量越小
  • 存取越快

14. System Memory 與 Global Memory

CPU 與 GPU 通常都有自己直接連接的 DRAM。

CPU
 ↓
System DRAM
(System / Host Memory)

GPU
 ↓
GPU DRAM
(Global Memory)

從 CUDA device code 的角度:

GPU 直接連接的 DRAM 稱為 Global Memory

因為 GPU 裡所有 SM 都可以存取它。

而 CPU 直接連接的 DRAM,通常叫:

  • System Memory
  • Host Memory

如果是 multi-GPU:

GPU 0 → 自己的 GPU Memory
GPU 1 → 自己的 GPU Memory
GPU 2 → 自己的 GPU Memory

每張 GPU 都可能有自己的 memory。


15. Register 與 Shared Memory

除了 GPU DRAM 之外,SM 上還有速度很快的 on-chip memory。

Register

  • 位於 SM
  • 通常存放 thread 的 local variables
  • 每個 Thread 使用自己的 Register
  • 大多由 compiler 自動配置

概念上:

Thread 0 → Registers
Thread 1 → Registers
Thread 2 → Registers

Shared Memory

  • 位於 SM
  • Thread Block 為單位配置
  • 同一 Block 的 Threads 可以共同存取
  • 很適合交換資料與重複使用資料

例如:

__shared__ float data[256];

概念:

Block
├── Thread 0 ─┐
├── Thread 1 ─┼→ Shared Memory
├── Thread 2 ─┤
└── ...      ─┘

16. Unified Data Cache、L1 與 L2

每個 SM 都有自己的 Unified Data Cache

它提供:

  • L1 Cache
  • Shared Memory

所需的實體資源。

概念:

SM
└── Unified Data Cache
    ├── L1 Cache
    └── Shared Memory

在部分 GPU architecture 中,可以調整 L1 與 Shared Memory 的配置比例。

另外:

  • L1 Cache:每個 SM 自己有
  • L2 Cache:整張 GPU 的 SM 共同使用

可以先記成:

Thread
 ↓
Register
 ↓
L1 / Shared Memory
 ↓
L2
 ↓
GPU Global Memory

17. Register / Shared Memory 也會限制一個 SM 能放多少 Blocks

SM 的資源不是無限的。

假設一個 kernel:

  • 每個 Thread 使用很多 Registers
  • 每個 Block 使用很多 Shared Memory

那同一個 SM 能同時放的 Blocks 就可能變少。

簡單想:

SM 的資源
├── Registers:有限
└── Shared Memory:有限

一個 Block 用越多:

每個 SM 可以同時 Resident 的 Blocks
                ↓
              可能變少

這也是之後學:

Occupancy

時非常重要的基礎。


18. Unified Memory 是什麼?

傳統做法可能需要 programmer 明確管理:

CPU Memory
    │
    │ cudaMemcpy
    ▼
GPU Memory

CUDA 也提供:

Unified Memory

讓同一個 allocation 可以被 CPU 或 GPU 存取。

概念上:

        Unified Memory
        ↙           ↘
      CPU           GPU

CUDA runtime / hardware 會幫忙處理資料需要放在哪裡、何時 migration。

但:

Unified Memory 不代表資料搬移完全沒有成本。

要有效能,還是要盡量減少不必要的 CPU ↔ GPU migration。


19. Locality Domain:大型 GPU 的進階 Memory 概念

新版 CUDA 還有 Locality Domain 的概念。

可以簡單理解成:

在大型 GPU 中,把部分 Memory 與部分 SM 視為一個比較靠近彼此的區域。

如果資料和執行它的 SM 都放在同一個 locality domain,有些 workload 可以有更好的效能。

不過它屬於:

Performance optimization,而不是 correctness requirement。

初學時先知道有這個概念即可。


20. Key Takeaway

把今天的內容全部串起來:

CPU
 │
 │ Kernel Launch
 ▼
Grid
 │
 ├── Thread Block
 │      │
 │      ├── Warp
 │      │    └── 32 Threads
 │      │
 │      └── Warp
 │
 └── Thread Block
        │
        ▼
      GPU SM

Memory:

Thread
 ↓
Register
 ↓
L1 Cache / Shared Memory
 ↓
L2 Cache
 ↓
GPU Global Memory
  • Kernel:在 GPU 上執行的函式
  • Grid:一次 Kernel Launch 的所有 Blocks
  • Thread Block:一群可以合作的 Threads
  • Warp:32 個 Threads,是理解 GPU 執行效率的重要單位
  • SM:真正執行 Blocks / Warps 的 GPU 硬體
  • 不同 Blocks 原則上不能依賴執行順序
  • Shared Memory:同一個 Block 內 Threads 共用的高速記憶體
  • Global Memory:GPU DRAM,所有 SM 都能存取
  • Unified Memory:讓 CPU / GPU 可以存取同一個 allocation,由 runtime / hardware 協助管理資料位置

我們用 Grid / Block / Thread 描述「工作怎麼切」,GPU 再把 Block 排到 SM 上,並以 Warp 為重要的執行群組;而效能高不高,很大一部分取決於 Threads 怎麼走 control flow,以及資料放在哪一層 Memory。

下一步再往下學 memory coalescing、shared memory optimization、occupancy 時,就可以直接把概念接回今天這張圖。


References


上一篇
Day 1|GPU 為什麼這麼快?從 CPU 到第一個 CUDA Kernel
下一篇
Day 3|GPU Memory Access:Coalescing、Shared Memory 與 Bank Conflict
系列文
30 天 GPU LeetCode 挑戰:從 CUDA 新手到 Kernel Leaderboard3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言