CUDA Python 入門 (Intro to CUDA Python)
重點總覽
| 項目 | 重點 |
|---|---|
| CUDA Python Ecosystem | 一群 Python 工具/函式庫;cuda.core/cuda.compute/CuPy 控制 GPU 與跑現成 code,cuda.lang/cuda.tile 寫 kernel |
| 優先用 libraries | 現代 CUDA 幾乎總是優先用 GPU-accelerated libraries(專家調校);不足時才自己寫 kernel,Python 與 C++ 皆可 |
| Getting Setup | 多數元件在 PyPi,用 pip 安裝;只需最新 NVIDIA Driver,通常不需要 CUDA Toolkit |
| 執行方式 | 與一般 Python 程式相同:python3 app.py |
| 指定 kernel | from numba import cuda + 在函式上方加 @cuda.jit 裝飾器;第一次 launch 時對當前 GPU JIT 編譯 |
| launch kernel | 方括號 execution configuration:kernel[blocks, threads](args);等同 C++ 的 <<<...>>> 三角括號 |
| 多維 grid/block | 1 維用整數,2/3 維用 tuple:function[(gx,gy), (bx,by)](...);每 block 上限 1024 threads |
| index intrinsics | cuda.threadIdx/blockDim/blockIdx/gridDim,皆有 .x/.y/.z;shorthand cuda.grid(n) |
| GPU 上建立 array | CuPy 以類 Numpy 介面在 GPU DRAM 建 ndarray;無指定型別預設 float32 |
| host↔GPU 複製 | H→D:cp.array(host);D→H:cp.asnumpy(device);不會隱式複製,需程式設計師明確控制 |
| ndarray 型別 | array 只存在 host 或 GPU 其一,不能兩邊;攜帶 dimension extents,kernel 內自動邊界檢查 |
| 同步 | kernel launch 對 host 非同步;cp.synchronize() / cuda.synchronize() / device.synchronize() 做 device-wide 同步 |
| Error Checking | CUDA 錯誤在 Python 拋 exception,用 try/except 捕捉;如 block_size=2048 → CUDA_ERROR_INVALID_VALUE |
本筆記是 C++ 版(2.1 節)的 Python 對應。CUDA programming model 的概念兩種語言相同,差別主要在語法與工具鏈。以下每節都會對照 C++ 寫法凸顯 Python 的等價作法。
CUDA Python Ecosystem
CUDA Python 是一整套讓 Python 進行 GPU 運算的工具與函式庫生態系,並非每個元件都是本章所需。它可分為三類:
主要元件(GPU 控制 / 執行 library 提供的 GPU code)
cuda.core— CUDA 控制(記憶體、device 管理)的 Pythonic 介面;為 Python 提供 CUDA Runtime 為 CUDA C++ 提供的功能。cuda.compute— 提供 CCCL (CUDA Core Compute Library) 的 GPU-accelerated 函式。- CuPy — 提供 Numpy routine 的 GPU 加速版本,以及 GPU 加速版的
ndarray資料容器(本章記憶體操作主力)。
Kernel 撰寫元件
cuda.lang— 用 Python 子集、以 SIMT 模型撰寫 kernel 與 device function 的 Python DSL (Domain-specific language)。cuda.coop— 提供 CCCL 的 device-callable primitive(以cuda.lang實作)。cuda.tile— 以 Tile programming model 撰寫 kernel/device function 的 Python DSL。
其他元件
cuda.pathfinder— 定位 Python 環境中已安裝 CUDA 元件的工具。cuda.bindings— CUDA 函式庫的低階 Python bindings(CUDA Driver API、Runtime API、NVRTC、NVVM 等);功能與cuda.core相同,但它是C 語言 API 的 Python wrapper,不是 native 的 Pythonic 介面。
本章實際示範時,使用的是 numba.cuda(寫/launch SIMT kernel)搭配 CuPy(GPU 記憶體與 ndarray)。上面的官方生態系清單是讓你認識完整版圖,並非全部都會用到。
CUDA Python Ecosystem
├─ 控制 / 跑現成 GPU code ─ cuda.core | cuda.compute | CuPy
├─ 寫 kernel / device fn ── cuda.lang(SIMT) | cuda.coop | cuda.tile(Tile)
└─ 其他工具 ────────────── cuda.pathfinder | cuda.bindings(低階 C wrapper)
在 Python 使用 CUDA libraries
- CUDA C++ 自 2006 年問世時函式庫很少,開發者大多得自己寫 kernel;之後累積了大量 library,讓 C++ 開發者幾乎不用寫 GPU code 就能用 GPU。
- CUDA Python 則是反方向演進:CuPy 等 Python library 先提供了 GPU 加速的演算法實作(許多是 CUDA C++ code 的 Python binding),之後才有用 Python 語法/語義直接寫 custom kernel 的能力。
現代 CUDA 的最佳實務:只要 GPU-accelerated library 的表達力足夠,幾乎總是優先用 library(多由 GPU 運算專家調校過)。只有在 library 不存在或不足時,才自己寫 kernel 與 device function——這在 Python 與 C++ 中都辦得到。本章接下來講的就是「需要自己寫 GPU code」時的作法。
Getting Setup 與執行
- 多數 CUDA Python 元件發佈在 PyPi,可用
pip或任何主流套件管理器安裝。 - 所有套件都需要系統安裝最新的 NVIDIA Driver。
- CUDA Toolkit 通常不需要就能寫/跑 CUDA Python 應用程式(與 C++ 需要 nvcc 工具鏈不同)。
- 執行方式與一般 Python 程式完全相同:
python3 cuda-python-app.py
SIMT Kernels in Python
在 GPU 上執行、可從 host 呼叫的函式稱為 kernel。CUDA 提供兩種模型:SIMT 與 CUDA tile。SIMT kernel 由大量平行 thread 同時執行——這個概念在 CUDA Python 與 CUDA C++ 完全一致。本章以 SIMT kernel 介紹 CUDA Python。
指定 kernel(Specifying Kernels)
先 import numba.cuda,再用 @cuda.jit 裝飾器把函式標記成 kernel:
from numba import cuda
@cuda.jit
def function(input_array, output_array):
...
@cuda.jit會讓 kernel 在第一次 launch 時對當前 GPU 進行 JIT 編譯。- 未指定其他 GPU 時,使用 default CUDA device。
對照 C++:@cuda.jit def f(...) 等同於 __global__ void f(...)。C++ 由 nvcc 提前(或 JIT)編譯成 PTX/cubin;Python 則由 @cuda.jit 在首次 launch 時 JIT。
Launching Kernels
執行 kernel 的 thread 數量在 launch 時指定,稱為 execution configuration;每次呼叫可有不同的 block size 或 block 數量。execution configuration 放在 kernel 名稱後、引數前的方括號 [ ] 內,引數順序與 C++ 三角括號相同:
from numba import cuda
@cuda.jit
def my_kernel(input, output):
...
## launch the kernel
my_kernel[num_thread_blocks, threads_per_block](in_array, out_array)
每個 thread block 有 thread 數上限:因為一個 block 的所有 thread 都駐在同一個 SM 上、共用該 SM 資源。目前 GPU 每個 block 最多 1024 threads。若資源允許,多個 block 可同時排程到同一 SM。
C++/Python launch 語法對照:
| 動作 | C++ | Python (numba.cuda) |
|---|---|---|
| launch(1 維) | f<<<blocks, threads>>>(a) |
f[blocks, threads](a) |
| 包住 config 的符號 | 三角括號 <<< >>> |
方括號 [ ] |
| 引數順序 | blocks, threads | blocks, threads(相同) |
多維 grid 與 thread block
grid 與 thread block 可為 1、2 或 3 維。1 維時用整數;2/3 維時用 tuple:
@cuda.jit
def function(input, output):
...
## 2D grid 與 2D block:(gridX, gridY) 與 (blockX, blockY)
function[(gridX, gridY), (blockX, blockY)](in_array, out_array)
Thread/Grid Index Intrinsics
kernel 內可用下列變數判斷自己的身分(皆為 3 分量向量,具 .x / .y / .z):
| Intrinsic | 意義 | C++ 對應 |
|---|---|---|
cuda.threadIdx.[xyz] |
thread 在其 block 內的 index | threadIdx |
cuda.blockDim.[xyz] |
thread block 的維度(來自 exec config) | blockDim |
cuda.blockIdx.[xyz] |
block 在 grid 內的 index | blockIdx |
cuda.gridDim.[xyz] |
grid 的維度(來自 exec config) | gridDim |
- 未在 launch 指定的維度:維度 (Dim) 預設 1,index (Idx) 預設 0。
cuda.threadIdx與cuda.blockIdxzero-indexed:threadIdx.x取值0 ~ blockDim.x - 1,.y/.z同理。
一個 element-wise 向量加法 kernel(C = A + B):
@cuda.jit
def vecadd(A, B, C):
idx = cuda.threadIdx.x + cuda.blockIdx.x * cuda.blockDim.x
C[idx] = A[idx] + B[idx]
idx是 thread 在 grid 中的唯一 index,範圍0 ~ N-1,其中N = cuda.gridDim.x * cuda.blockDim.x。此 kernel 假設 1 維 block + 1 維 grid。- 這個「global index」計算太常見,Numba 提供 shorthand
cuda.grid(n)(n為維度數):
## 以下兩行等價
idx = cuda.threadIdx.x + cuda.blockIdx.x * cuda.blockDim.x
idx = cuda.grid(1)
上面的 kernel 沒有做 out-of-bounds 檢查。本章假設 A/B/C 是 CuPy 建立的 ndarray,其陣列型別會隱式做邊界檢查,因此當 thread 總數略大於資料長度時不需手動檢查越界。(對照 C++ 通常需自己寫 if (idx < N) 守門。)
grid (1D) thread global index
┌──────────┬──────────┬──────────┐
│ block 0 │ block 1 │ block 2 │ idx = threadIdx.x
│ t0..t255 │ t0..t255 │ t0..t255 │ + blockIdx.x * blockDim.x
└──────────┴──────────┴──────────┘ = cuda.grid(1)
blockDim.x = 256, gridDim.x = 3, N = 3 * 256 = 768
Memory in GPU computing
GPU 有自己連接的 DRAM。kernel 要用的資料陣列一般必須先位於 GPU DRAM 才能被 kernel 存取。在 Python 中,控制資料位置(在 CPU↔GPU 間搬移)是程式設計師的責任——這與 C++ 的 explicit memory management 情況相同。
CuPy 等 Python 套件其實是直接呼叫 CUDA C++ API(如 2.1.3.2 節)來做 GPU 記憶體管理。多個套件提供各自的 wrapper,本指南只涵蓋 CuPy;多數套件行為與其 C++ 對應相似,除非另有說明。
在 GPU 上 instantiate arrays
CuPy 提供在 GPU 上建立指定型別/維度 ndarray 的函式,函式簽章與 Numpy 相似:
import cupy as cp
import numpy as np
## 在 GPU 上建立全零矩陣;未指定 datatype 時預設 float32
A_device = cp.zeros((1024, 1024))
## 在 GPU 上建立 2^20 個隨機 double
B_device = cp.rand.random((2**20), dtype=np.double)
## 建立與既有 array 同 shape、同 datatype 的全零 array
C_device = cp.zeros_like(A)
重點記憶:CuPy 未指定 datatype 時預設 float32。cp.zeros / cp.zeros_like / cp.random.* 等對應 Numpy 同名函式,但結果落在 GPU 記憶體。
host↔GPU 複製
import cupy as cp
import numpy as np
A_host = np.zeros((1024, 1024)) # host memory (Numpy)
A_device = cp.array(A_host) # H→D:複製到 GPU
B_device = cp.rand.random((1024, 1024)) # GPU memory
B_host = cp.asnumpy(B_device) # D→H:複製回 host
C++/Python 記憶體操作對照:
| 動作 | C++ | Python (CuPy) |
|---|---|---|
| 配置 device 記憶體 | cudaMalloc(...) |
cp.zeros(...) / cp.random.* 等 |
| H→D 複製 | cudaMemcpy(..., H2D) |
cp.array(host_arr) |
| D→H 複製 | cudaMemcpy(..., D2H) |
cp.asnumpy(dev_arr) |
ndarray 物件型別
- ndarray 只存在於 host 記憶體或 GPU 記憶體其中之一,不會同時兩邊都有。
- 把位於 host 的 array 傳給 kernel → error。
- 把位於 GPU 的 array 傳給一般 Python 函式(非 kernel)→ error。
- CuPy 不會隱式在 CPU↔GPU 間複製(複製昂貴、過度複製傷效能),要求程式設計師明確決定何時複製。
- 在 kernel 內使用 ndarray 的好處:array 攜帶各維度的 extent,因此可自動做邊界檢查;當所需 thread 總數略小於 block/grid 總大小時,kernel code 不需手動檢查越界。
Numpy ndarray CuPy ndarray
(host memory) (GPU/device memory)
│ │
cp.array(host) ─────────────────────▶ (H→D)
◀───────────────────── cp.asnumpy(dev) (D→H)
傳給 kernel:只接受 GPU 上的 ndarray(host array → error)
傳給一般 Python fn:只接受 host array(GPU array → error)
Synchronizing the CPU and the GPU
與 C++ 相同,CUDA Python 的 kernel launch 對 host thread 是非同步的:host code 在 launch 後即繼續往下執行,不保證 kernel 已完成、甚至已開始。要確保 GPU kernel 完成,host thread 必須做某種同步。
最簡單的形式是同步整個 GPU(device-wide synchronization),由 CUDA driver 提供,CuPy 與 numba.cuda 都以 synchronize() 方法暴露:
import cupy as cp
from numba import cuda
## 等待所有先前發出的 GPU 工作完成(CuPy 介面)
cp.synchronize()
## 等待所有先前發出的 GPU 工作完成(numba.cuda 介面)
cuda.synchronize()
- device-wide 同步會阻塞 host thread,直到所有先前發出的 GPU 工作完成。
- 對照 C++:等同
cudaDeviceSynchronize()。
需要更細粒度的同步時,使用 CUDA streams(見 2.5 節)。在 Python 中建議用 cuda.core 建立 stream,並只對特定 stream 做必要同步。
host thread: launch kernel ──▶ (繼續執行 host code,不等 GPU)
│ synchronize()
GPU: [====== kernel 執行中 ======]
host thread: └──▶ 阻塞直到 GPU 全部完成,再往下走
Putting it All Together
完整的平行向量加法(Python 版),刻意讓 vector_size 不是 2 的次方、也不是 block_size 的倍數,以示範自動邊界檢查:
import numpy as np
from numba import cuda
import cupy as cp
@cuda.jit
def vecadd(A, B, C):
work_index = cuda.grid(1)
C[work_index] = A[work_index] + B[work_index]
vector_size = 2**24 + 11 # 非 2 次方、非 block_size 倍數
device = cp.cuda.Device()
## 直接在 GPU 上建立輸入(隨機 float32)與輸出(全零)
a = cp.random.uniform(-1, 1, vector_size)
b = cp.random.uniform(-1, 1, vector_size)
c = cp.zeros_like(a)
block_size = 256
grid_size = int(np.ceil(vector_size / block_size))
vecadd[grid_size, block_size](a, b, c)
## 顯式同步示範好習慣;其實下面的 asnumpy 複製也會隱式等待 kernel 完成
device.synchronize()
## 把 3 個 array 複製回 CPU 做驗證
a_np = cp.asnumpy(a)
b_np = cp.asnumpy(b)
c_np = cp.asnumpy(c)
expected = a_np + b_np
np.testing.assert_array_almost_equal(c_np, expected)
print("Test succeeded")
grid_size = ceil(vector_size / block_size)保證 thread 數 ≥ 元素數;多出的 thread 因 ndarray 自動邊界檢查而安全。device.synchronize()此處純為示範好習慣;cp.asnumpy()的 D→H 複製本身就會隱式等待 kernel 完成。- A、B 在 GPU 上產生與初始化,最後才複製回 CPU,只是為了讓 CPU 也算一次以驗證 GPU 答案。
注意 cp.cuda.Device() 物件也提供 synchronize()(即 device.synchronize()),與 cp.synchronize() / cuda.synchronize() 同為 device-wide 同步。
Error Checking in CUDA Python
任何影響 GPU 的操作(記憶體配置、複製、kernel launch)都可能引發錯誤;如同 C++(2.1.7 節),確認過程中未發生錯誤是 best practice。
在 Python,CUDA 錯誤會拋出 exception,若未捕捉會終止程式;可用一般 Python 的 try/except 捕捉。下例把 block_size 設成 2048(超過任何現行 GPU 的 1024 上限),使 kernel launch 失敗並拋 exception:
try:
vector_size = 2**24 + 11
device = cp.cuda.Device()
a = cp.random.uniform(-1, 1, vector_size)
b = cp.random.uniform(-1, 1, vector_size)
c = cp.zeros_like(a)
block_size = 2048 # 對任何現行 GPU 都過大
grid_size = int(np.ceil(vector_size / block_size))
vecadd[grid_size, block_size](a, b, c) # Error: 無效的 block size
device.synchronize()
print("Test did not encounter any errors")
except Exception as e:
print(f"Exception occurred: {e}")
執行輸出:
$ python3 vecadd_error.py
Exception occurred: CUDA_ERROR_INVALID_VALUE: This indicates that one or more
of the parameters passed to the API call is not within an acceptable range of values.
- 程式捕捉到 exception 後正常結束。
- 若沒有
try/except,程式會異常結束並 dump traceback,顯示相同錯誤。
C++/Python 錯誤處理對照:C++ 多數 runtime 呼叫回傳 cudaError_t,需主動檢查;Python 則把錯誤轉成 exception,用 try/except 攔截,更貼近 Python 慣用法。block_size > 1024 這類「參數超出範圍」會得到 CUDA_ERROR_INVALID_VALUE。
考試/測驗重點
| 情境/關鍵字 | 答案 |
|---|---|
| 本章用哪兩個套件示範 SIMT? | numba.cuda(寫/launch kernel)+ CuPy(GPU 記憶體與 ndarray) |
| 把函式標記為 kernel 的語法? | 在函式上方加 @cuda.jit 裝飾器 |
@cuda.jit 何時編譯? |
第一次 launch 時對當前 GPU JIT 編譯 |
| Python launch 語法?對應 C++ 什麼? | kernel[blocks, threads](args);對應 C++ 三角括號 <<<blocks, threads>>> |
| execution configuration 放哪?引數順序? | kernel 名稱後的方括號 [ ];順序 blocks, threads(與 C++ 相同) |
| 每個 thread block 最多幾 threads?為何受限? | 1024;因一個 block 所有 thread 駐在同一 SM、共用 SM 資源 |
| 多維 grid/block 怎麼指定? | 1 維用整數;2/3 維用 tuple [(gx,gy),(bx,by)] |
| 四個 index intrinsic? | cuda.threadIdx / cuda.blockDim / cuda.blockIdx / cuda.gridDim(皆 .xyz) |
| 未指定維度時 intrinsic 預設值? | Dim 預設 1,Idx 預設 0 |
| 計算 1D global index 的 shorthand? | cuda.grid(1) = threadIdx.x + blockIdx.x * blockDim.x |
N(grid 內總 thread 數,1D)= ? |
cuda.gridDim.x * cuda.blockDim.x |
| 為何 vecadd kernel 不寫越界檢查? | 用 CuPy ndarray,array 攜帶 extent,自動邊界檢查 |
| CuPy 未指定 datatype 的預設型別? | float32 |
| H→D / D→H 複製函式? | H→D:cp.array(host);D→H:cp.asnumpy(device) |
| H→D / D→H 對應的 C++ API? | cudaMemcpy(... H2D/D2H);配置對應 cudaMalloc → cp.zeros 等 |
| 把 host array 傳給 kernel 會怎樣? | error(kernel 只接受 GPU 上的 ndarray) |
| 把 GPU array 傳給一般 Python 函式? | error(非 kernel 只接受 host array) |
| CuPy 會自動在 CPU↔GPU 複製嗎? | 不會;複製昂貴,須程式設計師明確控制 |
| kernel launch 對 host 同步還是非同步? | 非同步;不保證已完成甚至已開始 |
| device-wide 同步的三種寫法? | cp.synchronize() / cuda.synchronize() / device.synchronize() |
| device-wide 同步對應 C++? | cudaDeviceSynchronize() |
| 更細粒度同步用什麼?Python 建議用哪個建 stream? | CUDA streams;建議用 cuda.core 建立 stream |
為何範例仍呼叫 device.synchronize()? |
示範好習慣;其實後面 cp.asnumpy 複製已隱式等待 kernel 完成 |
| CUDA Python 怎麼回報錯誤?怎麼接? | 拋 exception;用 try/except 捕捉 |
| block_size=2048 會得到什麼錯誤? | CUDA_ERROR_INVALID_VALUE(參數超出可接受範圍) |
| 安裝需要什麼?需要 CUDA Toolkit 嗎? | 需最新 NVIDIA Driver;通常不需要 CUDA Toolkit |
| 多數 CUDA Python 元件從哪安裝? | PyPi(pip) |
cuda.core 之於 Python 等同什麼之於 C++? |
等同 CUDA Runtime 之於 CUDA C++ |
| 現代 CUDA 寫 GPU code 的優先順序? | 先用 GPU-accelerated library,不足時才自己寫 kernel |
Related Notes
- 02-Programming-GPUs/01-CUDA-Cpp-Kernels-and-Launch
- 02-Programming-GPUs/02-CUDA-Cpp-Memory-Management
- 02-Programming-GPUs/03-CUDA-Cpp-Sync-and-Workflow
- 02-Programming-GPUs/04-CUDA-Cpp-Errors-and-Specifiers
- 02-Programming-GPUs/06-SIMT-Basics-and-Thread-Hierarchy
- 02-Programming-GPUs/14-Async-Streams-and-Events
- 01-Introduction-to-CUDA/02-Execution-Model-and-SIMT
- 01-Introduction-to-CUDA/05-CUDA-Platform
- 02-Programming-GPUs/Practice-Programming-GPUs
- 00-Dashboard/Exam-Traps