錯誤檢查與函式/變數修飾符 (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 系統。

Important

任一 CUDA API 回傳的錯誤狀態,也可能代表先前發出的非同步操作所產生的錯誤,而非當前這個呼叫本身的錯誤。

2.1.7.1 Error State(錯誤狀態)

CUDA runtime 為每個 host thread 維護一份 cudaError_t 狀態,預設 cudaSuccess,發生錯誤時被覆寫。

API 行為
cudaGetLastError 回傳目前錯誤狀態,並重設cudaSuccess
cudaPeekAtLastError 回傳目前錯誤狀態,不重設
Warning

Triple chevron(<<<>>>)kernel 啟動不回傳 cudaError_t。應在 launch 後立即檢查錯誤狀態以偵測啟動錯誤或先前的非同步錯誤。

Tip

launch 後立即檢查得到 cudaSuccess不代表 kernel 已成功執行甚至已開始執行。它只驗證 launch 參數與 execution configuration 沒觸發錯誤,且當下沒有殘留的先前/非同步錯誤。

2.1.7.2 Synchronous vs Asynchronous Errors(同步與非同步錯誤)

CUDA kernel 啟動與許多 runtime API 都是非同步的。錯誤狀態在錯誤發生時被設定/覆寫,這代表非同步操作中發生的錯誤,只有在下次檢查錯誤狀態時才會被回報(可能是 cudaGetLastErrorcudaPeekAtLastError,或任何回傳 cudaError_t 的 API)。

Warning

當 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() 清除狀態
Warning

例外:cudaErrorNotReady(由 cudaStreamQuerycudaEventQuery 可能回傳)不視為錯誤,不會被 cudaPeekAtLastErrorcudaGetLastError 回報。

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
Tip

CUDA_LOG_FILE 對除錯極強大,但單靠環境變數無法讓應用在 runtime 處理與復原錯誤。CUDA 的 error log management 還可註冊 callback function,偵測到錯誤時被呼叫,用以 runtime 處理錯誤或整合進既有 logging 系統。Error log management 與 CUDA_LOG_FILENVIDIA 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 兩端皆可用
__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
Important

無修飾符時的規則:

  • __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
}
__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  ──(co-scheduled)──► SM   (在 thread block 內)
 block   ──(co-scheduled)──► GPC  (在 cluster 內)

 grid ⊃ cluster ⊃ block ⊃ thread
Warning

在使用 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);
}
Important

若 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 數