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 型別 CUdevice、CUcontext、CUmodule、CUfunction、CUdeviceptr、CUarray、CUstream、CUevent 等不透明 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.dll 或 cuda.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 骨架(注意 cuInit → cuDeviceGet → cuCtxCreate → cuModuleLoad → cuMemAlloc → cuMemcpyHtoD → cuModuleGetFunction → cuLaunchKernel 的順序):

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 的編譯 + 連結:用 cuLinkCreate → cuLinkAddData(每段 PTX 一次,input type CU_JIT_INPUT_PTX)→ cuLinkComplete 產生 cubin → cuModuleLoadData 載入,最後 cuLinkDestroy。可透過 CU_JIT_WALL_TIME、CU_JIT_INFO_LOG_BUFFER、CU_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_BUFFER 等 CUjit_option
多 PTX 連結流程 cuLinkCreate→cuLinkAddData→cuLinkComplete→cuModuleLoadData→cuLinkDestroy
加速 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 可(兩者互通)