CUDA Driver API (The CUDA Driver API)

重點總覽

項目 重點
Driver API 概觀 runtime API 建構在低階 driver API 之上;入口點皆以 cu 為前綴;handle-based、imperative;某些新介面(如 Virtual Memory Management)只在 driver API 暴露
初始化 任何 driver API 呼叫前必須先呼叫 cuInit(),再建立並 current 一個 context
Handle 型別 CUdeviceCUcontextCUmoduleCUfunctionCUdeviceptrCUarrayCUstreamCUevent 等不透明 handle
Context (3.3.1) 類比 CPU process;擁有獨立 address space;每個 host thread 維護一個 context stack;cuCtxCreate/cuCtxPushCurrent/cuCtxPopCurrent
Primary context runtime 隱式建立的 context;可用 cuDevicePrimaryCtxRetain() 從 driver API 取得
Module (3.3.2) 可動態載入的 device code 套件(類比 DLL);cuModuleLoad/cuModuleLoadData(Ex) 載入 cubin/PTX/fatbin;cuModuleGetFunction/cuModuleGetGlobal 取符號
PTX vs binary 要跑在未來架構必須載入 PTX(載入時由 driver 編成 binary);binary 為架構特定、不相容未來架構
Kernel Execution (3.3.3) cuLaunchKernel() 啟動 kernel;參數以 pointer 陣列或 CU_LAUNCH_PARAM_BUFFER_POINTER 單一 buffer 傳遞,須符合 device 端對齊
Runtime/Driver 互通 (3.3.4) 兩者可混用;CUdeviceptr 與一般指標可互轉;driver API 程式可呼叫 runtime API 寫的函式庫(cuFFT、cuBLAS)
Important

大多數應用程式不需碰 driver API 即可達到全效能。會用 driver API 的理由:新介面常先在 driver API 出現、以及部分進階介面(如 Virtual Memory Management)只在 driver API 暴露。

Driver API 概觀

CUDA runtime(即 cuda 前綴的 runtime API)是建構在更低階的 CUDA driver API 之上。driver API 實作於裝置驅動安裝時複製到系統的動態函式庫(cuda.dllcuda.so),所有入口點皆以 cu 為前綴。

下表為 driver API 中常見物件與其 handle 型別:

物件 Handle 說明
Device CUdevice CUDA-enabled device
Context CUcontext 大致等同一個 CPU process
Module CUmodule 大致等同一個 dynamic library
Function CUfunction Kernel
Heap memory CUdeviceptr 指向 device memory 的指標
CUDA array CUarray 一維/二維資料的不透明容器,可經 texture/surface 讀取
Texture object CUtexref 描述如何解讀 texture memory
Surface reference CUsurfref 描述如何讀寫 CUDA array
Stream CUstream CUDA stream
Event CUevent CUDA event

整體使用流程:先 cuInit(),再建立並 current 一個 context;於 context 內以 host code 明確載入 PTX/binary module;再用 cuLaunchKernel() 等入口點啟動 kernel。

Warning

用 driver API 時,C++ kernel 必須被單獨編譯成 PTX 或 binary 物件(runtime API 由 nvcc 自動處理這步,driver API 要自己做)。

下面是 driver API 版 VecAdd 的 host code 骨架(注意 cuInitcuDeviceGetcuCtxCreatecuModuleLoadcuMemAlloccuMemcpyHtoDcuModuleGetFunctioncuLaunchKernel 的順序):

cuInit(0);                                   // 必須最先呼叫

int deviceCount = 0;
cuDeviceGetCount(&deviceCount);

CUdevice cuDevice;
cuDeviceGet(&cuDevice, 0);                    // device 0 的 handle

CUcontext cuContext;
cuCtxCreate(&cuContext, 0, cuDevice);         // 建立 context 並 current

CUmodule cuModule;
cuModuleLoad(&cuModule, "VecAdd.ptx");        // 載入 module

CUdeviceptr d_A, d_B, d_C;
cuMemAlloc(&d_A, size);
cuMemAlloc(&d_B, size);
cuMemAlloc(&d_C, size);
cuMemcpyHtoD(d_A, h_A, size);
cuMemcpyHtoD(d_B, h_B, size);

CUfunction vecAdd;
cuModuleGetFunction(&vecAdd, cuModule, "VecAdd");

void* args[] = { &d_A, &d_B, &d_C, &N };
cuLaunchKernel(vecAdd,
               blocksPerGrid, 1, 1,           // grid dims
               threadsPerBlock, 1, 1,         // block dims
               0, 0, args, 0);                // sharedMem, stream, params, extra

要點:runtime API 用一行 cudaLaunchKernel/<<<>>> 隱式完成的事,driver API 要逐步手動建立 context、載入 module、取得 function handle 再啟動。完整程式見 vectorAddDrv CUDA sample。

Tip

要在未來的 device 架構上執行,必須載入 PTX 而非 binary。binary code 是架構特定、與未來架構不相容;PTX 則在載入時由 device driver 編成 binary code(JIT)。

3.3.1 Context

一個 CUDA context 類比於一個 CPU process。driver API 內所有資源與動作都封裝在某個 context 內,當 context 被銷毀時系統自動清理這些資源。

Context stack:每個 host thread 維護一個 current contexts 的堆疊。

host thread 的 context stack(堆疊頂 = current)

  cuCtxCreate / cuCtxPushCurrent        cuCtxPopCurrent
        push ↓                              pop ↑
   ┌──────────────┐  <- current(top)   ┌──────────────┐  <- floating(detach 後)
   │  ctxB (new)  │                    │  ctxB        │ ──→ 可 push 給其他 thread
   ├──────────────┤                    └──────────────┘
   │  ctxA        │                    ┌──────────────┐
   └──────────────┘                    │  ctxA        │  <- 還原成 current
                                       └──────────────┘

Usage count(使用計數):每個 context 維護一個使用計數。

Tip

慣例:應用程式通常先建立 context 再載入/初始化函式庫,函式庫只操作被交給它的 context handle。想自行建立 context(且不讓 client 知道)的函式庫,應使用 cuCtxPushCurrent()/cuCtxPopCurrent() 包夾,用完還原呼叫者原本的 current context(Library Context Management)。

Important

driver API 與 runtime 互通:可用 cuDevicePrimaryCtxRetain() 從 driver API 存取由 runtime 管理的 primary context。primary context 與自建 context(cuCtxCreate)不同,是 runtime 在初始化時隱式建立、per-device 共享的那一個。

3.3.2 Module

Module 是可動態載入的 device code 與資料套件,類比 Windows 的 DLL,由 nvcc 輸出(見 Compilation with NVCC)。

CUmodule cuModule;
cuModuleLoad(&cuModule, "myModule.ptx");      // 從檔案載入 cubin/PTX/fatbin
CUfunction myKernel;
cuModuleGetFunction(&myKernel, cuModule, "MyKernel");   // 依名稱取 kernel
Tip

對應 API:cuModuleLoad 從檔案載入;cuModuleLoadData/cuModuleLoadDataEx 從記憶體中的影像載入;cuModuleGetFunction 取 kernel;cuModuleGetGlobal 取 module 內的 global 變數(位址與大小)。

從 PTX 即時編譯並解析錯誤:用 cuModuleLoadDataEx 搭配 CUjit_option 選項陣列傳入 error log buffer,可在 JIT 失敗時取得編譯錯誤訊息。

CUjit_option options[3];
void* values[3];
char error_log[BUFFER_SIZE];
options[0] = CU_JIT_ERROR_LOG_BUFFER;            values[0] = (void*)error_log;
options[1] = CU_JIT_ERROR_LOG_BUFFER_SIZE_BYTES; values[1] = (void*)BUFFER_SIZE;
options[2] = CU_JIT_TARGET_FROM_CUCONTEXT;       values[2] = 0;
err = cuModuleLoadDataEx(&cuModule, PTXCode, 3, options, values);
if (err != CUDA_SUCCESS) printf("Link error:\n%s\n", error_log);

多個 PTX 的編譯 + 連結:用 cuLinkCreatecuLinkAddData(每段 PTX 一次,input type CU_JIT_INPUT_PTX)→ cuLinkComplete 產生 cubin → cuModuleLoadData 載入,最後 cuLinkDestroy。可透過 CU_JIT_WALL_TIMECU_JIT_INFO_LOG_BUFFERCU_JIT_LOG_VERBOSE 取得連結時間與資訊。

cuLinkCreate(6, options, values, &linkState);
cuLinkAddData(linkState, CU_JIT_INPUT_PTX, (void*)PTXCode0, strlen(PTXCode0)+1, 0,0,0,0);
cuLinkAddData(linkState, CU_JIT_INPUT_PTX, (void*)PTXCode1, strlen(PTXCode1)+1, 0,0,0,0);
cuLinkComplete(linkState, &cubin, &cubinSize);   // 產生連結後的 cubin
cuModuleLoadData(cuModule, cubin);
cuLinkDestroy(linkState);

加速載入:載入 cubin 等過程可用多執行緒加速,傳 CU_JIT_BINARY_LOADER_THREAD_COUNT(值設 0 表示使用機器上 CPU 數量的執行緒)。完整程式見 ptxjit CUDA sample。

3.3.3 Kernel Execution

cuLaunchKernel() 以給定的 execution configuration 啟動一個 kernel。參數有兩種傳遞方式:

傳法 機制
Pointer 陣列(倒數第二個參數) 第 n 個指標對應第 n 個參數,指向參數被複製的記憶體區
Extra 選項(最後一個參數,CU_LAUNCH_PARAM_BUFFER_POINTER 指向單一 buffer,參數彼此須依 device 端各型別的對齊需求正確 offset

對齊規則(用 buffer 傳參時關鍵):

用 macro 逐一對齊並填入 parameter buffer,再經 extra[] 傳入:

#define ALIGN_UP(offset, alignment) \
    (offset) = ((offset) + (alignment) - 1) & ~((alignment) - 1)

ADD_TO_PARAM_BUFFER(i,  __alignof(i));
ADD_TO_PARAM_BUFFER(f4, 16);            // float4 對齊為 16
ADD_TO_PARAM_BUFFER(devPtr, __alignof(devPtr));
ADD_TO_PARAM_BUFFER(f2, 8);             // float2 對齊為 8

void* extra[] = {
    CU_LAUNCH_PARAM_BUFFER_POINTER, paramBuffer,
    CU_LAUNCH_PARAM_BUFFER_SIZE,     &paramBufferSize,
    CU_LAUNCH_PARAM_END
};
cuLaunchKernel(cuFunction,
               blockWidth, blockHeight, blockDepth,
               gridWidth, gridHeight, gridDepth,
               0, 0, 0, extra);          // args 為 0,改用 extra 傳參
Warning

structure 的對齊需求 = 其各 field 對齊需求的最大值。含 built-in vector types、CUdeviceptr、或未對齊 double/long long 的 structure,device 與 host 端的對齊與 padding 可能不同。例如下面這個 struct 在 host code 完全不 padding,但在 device code 中因 f4 對齊需求為 16,會在 f 之後 padding 12 bytes:

typedef struct {
    float  f;
    float4 f4;
} myStruct;

3.3.4 Interoperability between Runtime and Driver APIs

應用程式可將 runtime API 與 driver API 程式碼混用。

CUdeviceptr devPtr;
float* d_data;
// 用 driver API 配置
cuMemAlloc(&devPtr, size);
d_data = (float*)devPtr;
// 用 runtime API 配置
cudaMalloc(&d_data, size);
devPtr = (CUdeviceptr)d_data;
context 來源與互通

 driver API ──cuCtxCreate──► 自建 context ──► 後續 runtime 呼叫沿用同一 context
 runtime   ──隱式初始化──► primary context ◄── cuCtxGetCurrent / cuDevicePrimaryCtxRetain
                                              (driver 端以 Primary Context Mgmt 管理)

 CUdeviceptr  ⇄  float* / void*   (可雙向 cast)

考試/測驗重點

情境/關鍵字 答案
driver API 入口點前綴 cu(runtime 是 cuda
driver API 任何呼叫前必做 cuInit()
context 類比 CPU process;各 context 有獨立 address space
不同 context 的 CUdeviceptr 指向不同記憶體位置(位址空間獨立)
host thread 同時 current 幾個 context 1 個;維護一個 context stack
push / pop / detach context cuCtxPushCurrent / cuCtxPopCurrent(pop 還原前一個 current)
無 current context 呼叫 context 函式 回傳 CUDA_ERROR_INVALID_CONTEXT
usage count:create / attach / detach cuCtxCreate=1、cuCtxAttach++、cuCtxDetach--;歸 0 即銷毀
銷毀 context cuCtxDestroy() 或 usage count 歸 0
primary context 從 driver 取得 cuDevicePrimaryCtxRetain()(runtime 隱式建立的那一個)
Module 類比 dynamic library / DLL;符號在 module scope
從檔案 / 從記憶體載入 module cuModuleLoad / cuModuleLoadData(Ex)
取 kernel / 取 global 變數 cuModuleGetFunction / cuModuleGetGlobal
要跑未來架構,載 PTX 還是 binary PTX(載入時 driver JIT 成 binary);binary 架構特定不相容未來
JIT 錯誤訊息 CU_JIT_ERROR_LOG_BUFFERCUjit_option
多 PTX 連結流程 cuLinkCreatecuLinkAddDatacuLinkCompletecuModuleLoadDatacuLinkDestroy
加速 cubin 載入 CU_JIT_BINARY_LOADER_THREAD_COUNT(0 = 用 CPU 數的執行緒)
啟動 kernel cuLaunchKernel()(grid/block 各 3 維 + sharedMem + stream + args + extra)
兩種傳參方式 pointer 陣列(args)/ CU_LAUNCH_PARAM_BUFFER_POINTER(單一對齊 buffer)
buffer 傳參的結尾標記 CU_LAUNCH_PARAM_END
float4 / float2 對齊 16 / 8
CUdeviceptr 對齊 __alignof(void*)(它是整數但代表指標)
device 端 double/long long 對齊 永遠 two-word 邊界(即使 host 用 -mno-align-double 對齊 one-word)
struct 對齊需求 等於各 field 對齊需求的最大值;device/host 的 padding 可能不同
driver 建立 context 後 runtime 行為 runtime 沿用該 current context,不另建
取得 runtime 初始化的 context cuCtxGetCurrent()
CUdeviceptr 與 float* 可互相 cast;driver/runtime 配置的記憶體可互用
driver API 程式能否呼叫 cuFFT/cuBLAS 可(兩者互通)