錯誤檢查與函式/變數修飾符 (Error Checking and Specifiers)
重點總覽
| 項目 | 重點 |
|---|---|
| cudaError_t | 每個 CUDA API 都回傳此列舉型別;無錯誤時為 cudaSuccess,正式專案應每次都檢查 |
| CUDA_CHECK macro | 常見 utility macro,包住 API 呼叫,配 cudaGetErrorString 印出可讀錯誤訊息 |
| Error State | 每個 host thread 維護一份錯誤狀態;cudaGetLastError 取回並重設,cudaPeekAtLastError 取回但不重設 |
| Triple chevron 啟動 | <<<>>> 不回傳 cudaError_t,啟動後應立即查錯誤狀態 |
| Synchronous vs Asynchronous Errors | kernel/多數 API 為非同步;非同步錯誤要等下次檢查狀態才回報,且會持續回傳直到被清除 |
| cudaErrorNotReady | cudaStreamQuery/cudaEventQuery 可能回傳,不視為錯誤,不被 peek/getlast 回報 |
| CUDA_LOG_FILE | 環境變數,driver 把錯誤寫入指定檔;不需程式自行檢查即可除錯(driver r570+) |
__global__ / __device__ / __host__ |
函式修飾符;__host__ __device__ 可同時標註讓函式 CPU/GPU 兩端皆可用 |
| 變數修飾符 | __device__/__constant__/__managed__/__shared__ 控制靜態變數放置位置 |
__CUDA_ARCH__ |
偵測 device 編譯路徑,於 __host__ __device__ 函式內分流 GPU/CPU 程式碼 |
| Thread Block Clusters | CC 9.0+ 的可選階層;cluster 內 block 保證同排程於同一 GPC,可用 cluster.sync() |
2.1.7 Error Checking in CUDA
每個 CUDA API 都回傳一個列舉型別 cudaError_t。範例程式常省略檢查,但正式(production)應用應每次都檢查並管理回傳值。無錯誤時回傳 cudaSuccess。
許多應用會實作一個 utility macro 來統一檢查:
#define CUDA_CHECK(expr_to_check) do { \
cudaError_t result = expr_to_check; \
if (result != cudaSuccess) { \
fprintf(stderr, \
"CUDA Runtime Error: %s:%i:%d = %s\n", \
__FILE__, __LINE__, result, \
cudaGetErrorString(result)); \
} \
} while (0)
此 macro 使用 cudaGetErrorString API,把一個 cudaError_t 值轉成人類可讀字串。使用方式是把 API 呼叫包進 macro:
CUDA_CHECK(cudaMalloc(&devA, vectorLength * sizeof(float)));
CUDA_CHECK(cudaMalloc(&devB, vectorLength * sizeof(float)));
任一呼叫偵測到錯誤就會印到 stderr。小專案常用此 macro,大型應用可改接到 logging 系統。
任一 CUDA API 回傳的錯誤狀態,也可能代表先前發出的非同步操作所產生的錯誤,而非當前這個呼叫本身的錯誤。
- 關鍵事實
cudaError_t是所有 runtime API 的統一回傳型別;cudaSuccess代表成功。cudaGetErrorString(result)取得錯誤的文字描述。- macro 寫法可彈性改接 logging / 其他錯誤處理機制。
2.1.7.1 Error State(錯誤狀態)
CUDA runtime 為每個 host thread 維護一份 cudaError_t 狀態,預設 cudaSuccess,發生錯誤時被覆寫。
| API | 行為 |
|---|---|
cudaGetLastError |
回傳目前錯誤狀態,並重設為 cudaSuccess |
cudaPeekAtLastError |
回傳目前錯誤狀態,不重設 |
Triple chevron(<<<>>>)kernel 啟動不回傳 cudaError_t。應在 launch 後立即檢查錯誤狀態以偵測啟動錯誤或先前的非同步錯誤。
launch 後立即檢查得到 cudaSuccess,不代表 kernel 已成功執行甚至已開始執行。它只驗證 launch 參數與 execution configuration 沒觸發錯誤,且當下沒有殘留的先前/非同步錯誤。
2.1.7.2 Synchronous vs Asynchronous Errors(同步與非同步錯誤)
CUDA kernel 啟動與許多 runtime API 都是非同步的。錯誤狀態在錯誤發生時被設定/覆寫,這代表非同步操作中發生的錯誤,只有在下次檢查錯誤狀態時才會被回報(可能是 cudaGetLastError、cudaPeekAtLastError,或任何回傳 cudaError_t 的 API)。
當 runtime API 回傳錯誤時,錯誤狀態不會被清除。例如 kernel 的非法記憶體存取造成的非同步錯誤,會被之後每一個 CUDA runtime API 持續回傳,直到呼叫 cudaGetLastError 把狀態清除為止。
vecAdd<<<blocks, threads>>>(devA, devB, devC);
// 啟動後立即檢查錯誤狀態(偵測 launch 即時錯誤)
CUDA_CHECK(cudaGetLastError());
// 等待 kernel 執行完成;CUDA_CHECK 會回報執行期間發生的錯誤
CUDA_CHECK(cudaDeviceSynchronize());
兩段檢查的分工:cudaGetLastError() 抓 launch 當下的錯誤;cudaDeviceSynchronize() 等待執行並抓執行期間才發生的非同步錯誤。
非同步錯誤傳播時間軸
─────────────────────────────────────────────►
launch ── kernel 執行中 ──[非法存取!]── 之後的 API
│
└─ 錯誤狀態被設定,但 launch 當下檢查可能仍是 cudaSuccess
之後每個回傳 cudaError_t 的 API 都會回傳此錯誤
直到 cudaGetLastError() 清除狀態
例外:cudaErrorNotReady(由 cudaStreamQuery、cudaEventQuery 可能回傳)不視為錯誤,不會被 cudaPeekAtLastError 或 cudaGetLastError 回報。
2.1.7.3 CUDA_LOG_FILE
另一個辨識 CUDA 錯誤的好方法是 CUDA_LOG_FILE 環境變數。設定後,CUDA driver 會把遇到的錯誤訊息寫到該變數指定路徑的檔案。即使應用本身沒做回傳值檢查,也能擷取並辨識錯誤。
__global__ void k() { }
int main() {
k<<<8192, 4096>>>(); // 非法 block 大小(4096 超過上限)
CUDA_CHECK(cudaGetLastError());
return 0;
}
$ env CUDA_LOG_FILE=cudaLog.txt ./errlog
CUDA Runtime Error: .../errorLogIllustration.cu:24:1 = invalid argument
$ cat cudaLog.txt
[CUDA][E] One or more of block dimensions of (4096,1,1) exceeds
corresponding maximum value of (1024,1024,64)
[CUDA][E] Returning 1 (CUDA_ERROR_INVALID_VALUE) from cuLaunchKernel
- log 檔比 stderr 提供更多細節(哪個維度超過哪個上限、底層
cuLaunchKernel回傳值)。 - 設為
stdout或stderr會分別印到標準輸出/標準錯誤。
CUDA_LOG_FILE 對除錯極強大,但單靠環境變數無法讓應用在 runtime 處理與復原錯誤。CUDA 的 error log management 還可註冊 callback function,偵測到錯誤時被呼叫,用以 runtime 處理錯誤或整合進既有 logging 系統。Error log management 與 CUDA_LOG_FILE 需 NVIDIA Driver r570 以後。
2.1.8 Device and Host Functions(函式修飾符)
| 修飾符 | 意義 | 可被誰呼叫 |
|---|---|---|
__global__ |
kernel 進入點(在 GPU 上平行執行) | 通常由 host 啟動;亦可由 kernel 用 dynamic parallelism 啟動 |
__device__ |
編譯給 GPU 用的函式 | 只能被其他 __device__ 或 __global__ 函式呼叫 |
__host__ |
編譯給 CPU 用的函式(預設行為) | host 端 |
__host__ __device__ |
同時編譯出 GPU 與 CPU 兩份程式碼 | host 與 device 兩端皆可用 |
- 函式(含 class member function、functor、lambda)可同時標註
__device__與__host__,讓同一份原始碼在 CPU 與 GPU 兩端都能呼叫。
__host__ __device__ float square(float x) { return x * x; } // 兩端皆可用
2.1.9 Variable Specifiers(變數修飾符)
CUDA 修飾符可用於靜態變數宣告以控制其放置位置。
| 修飾符 | 放置位置 |
|---|---|
__device__ |
Global Memory |
__constant__ |
Constant Memory |
__managed__ |
Unified Memory |
__shared__ |
Shared Memory |
無修飾符時的規則:
- 在
__device__/__global__函式內部宣告 → 盡量配置到 registers,必要時配到 local memory。 - 在
__device__/__global__函式外部宣告 → 配置到 system memory。
2.1.9.1 Detecting Device Compilation(偵測 device 編譯)
當函式標 __host__ __device__,編譯器會同時產生 GPU 與 CPU 兩份程式碼。若想在其中只為 GPU 或只為 CPU 撰寫特定程式碼,最常見做法是用前處理器檢查是否定義了 __CUDA_ARCH__。
__host__ __device__ int f() {
#ifdef __CUDA_ARCH__
// 只會編進 GPU 版本(device 編譯路徑)
return device_path();
#else
// 只會編進 CPU 版本(host 編譯路徑)
return host_path();
#endif
}
__CUDA_ARCH__只在 device 編譯階段有定義,故可用它分流。
__host__ __device__ 函式編譯分流
┌─────────────────────────┐
原始碼 ───►│ nvcc │
├─────────────┬───────────┤
│ device pass │ host pass │
│ __CUDA_ARCH_│ 未定義 │
│ _ 有定義 │ │
└─────┬───────┴─────┬─────┘
▼ ▼
GPU 程式碼 CPU 程式碼
2.1.10 Thread Block Clusters(執行緒區塊叢集)
從 compute capability 9.0 起,CUDA programming model 加入一個可選的階層:thread block cluster,由多個 thread block 組成。
- 如同 thread block 內的 thread 保證同排程於同一個 SM,cluster 內的 thread block 也保證同排程於 GPU 內的同一個 GPC(GPU Processing Cluster)。
- cluster 同樣可組成 1D / 2D / 3D 的 cluster grid。
階層對應關係
thread ──(co-scheduled)──► SM (在 thread block 內)
block ──(co-scheduled)──► GPC (在 cluster 內)
grid ⊃ cluster ⊃ block ⊃ thread
- cluster 大小:可由使用者定義;portable 上限為 8 個 block。硬體或 MIG 配置太小(不足 8 個 multiprocessor)時上限會相應下降。較小/較大(>8)配置為 architecture-specific,可用
cudaOccupancyMaxPotentialClusterSizeAPI 查詢。 - 硬體同步:cluster 內所有 block 保證同時排程於單一 GPC,可用 cooperative groups API
cluster.sync()做硬體支援的同步。 - 查詢 API:cluster group 提供
num_threads()、num_blocks()查 cluster 大小;dim_threads()、dim_blocks()查 thread/block 在 cluster 內的 rank。 - Distributed Shared Memory:cluster 內 block 可存取 distributed shared memory(cluster 內所有 block shared memory 的合集),可對其中任意位址做讀、寫、atomics。
在使用 cluster 的 kernel 中,gridDim 變數仍以 thread block 數量計(為相容性)。block 在 cluster 內的 rank 要用 Cooperative Groups API 查,而非 gridDim。
2.1.10.1 在 Triple Chevron 啟動 Cluster
啟用 cluster 有兩種方式:
| 方式 | 設定時機 | 啟動方式 |
|---|---|---|
編譯期 kernel attribute __cluster_dims__(X,Y,Z) |
compile time 固定,無法在 launch 時改 | 用傳統 <<<>>> |
runtime API cudaLaunchKernelEx |
runtime 指定 | 透過該 API |
// 編譯期 cluster 大小:X 維 2、Y/Z 維各 1
__global__ void __cluster_dims__(2, 1, 1)
cluster_kernel(float *input, float *output) { }
int main() {
float *input, *output;
dim3 threadsPerBlock(16, 16);
dim3 numBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y);
// grid 維度不受 cluster 影響,仍以 block 數計
cluster_kernel<<<numBlocks, threadsPerBlock>>>(input, output);
}
若 kernel 用編譯期 cluster 大小,啟動時不能再修改 cluster 大小,且 grid 維度必須是 cluster 大小的整數倍。
考試/測驗重點
| 情境/關鍵字 | 答案 |
|---|---|
| 取回錯誤狀態並重設 | cudaGetLastError |
| 取回錯誤狀態但不重設 | cudaPeekAtLastError |
<<<>>> 啟動回傳什麼 |
不回傳 cudaError_t;要另外查錯誤狀態 |
launch 後查到 cudaSuccess 代表? |
只代表 launch 參數/config 無誤,不代表 kernel 已執行或成功 |
| 非同步錯誤如何清除 | 呼叫 cudaGetLastError(回傳錯誤的 API 不會清除狀態) |
| 抓 kernel 執行期間的非同步錯誤 | 用 cudaDeviceSynchronize()(等待後檢查) |
cudaErrorNotReady 算錯誤嗎 |
不算;不被 peek/getlast 回報(來自 stream/event query) |
| 不改程式碼也能記錄錯誤 | 設 CUDA_LOG_FILE 環境變數(driver r570+);設 stdout/stderr 印到標準輸出/錯誤 |
| 錯誤狀態是 per-? | per host thread |
| 函式兩端皆可用 | 同時標 __host__ __device__ |
__device__ 函式可被誰呼叫 |
只能被 __device__ 或 __global__ 函式呼叫 |
__constant__ / __managed__ 變數放哪 |
Constant Memory / Unified Memory |
| 函式內無修飾符變數放哪 | registers,必要時 local memory |
| 函式外無修飾符變數放哪 | system memory |
| 偵測 device 編譯路徑 | #ifdef __CUDA_ARCH__ |
| cluster 最低 compute capability | 9.0 |
| portable cluster 大小上限 | 8 個 block(小配置會降低) |
| cluster 內 block 排程於 | 同一個 GPC |
| cluster 硬體同步 API | cluster.sync()(cooperative groups) |
| 查 potential cluster 大小 | cudaOccupancyMaxPotentialClusterSize |
| 編譯期 cluster 大小語法 | __cluster_dims__(X,Y,Z) + <<<>>> |
| runtime 設定 cluster | cudaLaunchKernelEx |
用 cluster 時 gridDim 以什麼計 |
仍以 thread block 數計 |
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/05-CUDA-Python
- 02-Programming-GPUs/06-SIMT-Basics-and-Thread-Hierarchy
- 02-Programming-GPUs/07-SIMT-Device-Memory-Spaces
- 02-Programming-GPUs/09-SIMT-Atomics-Cooperative-Occupancy
- 02-Programming-GPUs/14-Async-Streams-and-Events
- 02-Programming-GPUs/16-Unified-and-System-Memory
- 02-Programming-GPUs/17-NVCC-Compiler
- 01-Introduction-to-CUDA/02-Execution-Model-and-SIMT
- 01-Introduction-to-CUDA/04-GPU-Memory-Hierarchy
- 02-Programming-GPUs/Practice-Programming-GPUs
- 00-Dashboard/Exam-Traps