C 標準函式庫、Lambda 與多型包裝器 (C Standard Library, Lambdas and Polymorphic Wrappers)
重點總覽
device code 可用一小組 C standard library 函式(clock()/printf()/memcpy()/malloc()/alloca() 等),各有 CUDA 專屬語意與限制。Lambda 的執行空間由最內層 enclosing function 的執行空間決定;要把 lambda 傳進 kernel 必須是 __device__ 或 __host__ __device__,且常需 extended lambda(--extended-lambda 旗標)。Extended lambda 有大量限制(this 捕捉、__CUDA_ARCH__、捕捉方式、return type 內省等)。nvstd::function 提供 host/device 皆可用的多型函式包裝器,但不可跨 host/device 邊界傳遞。
| 項目 | 重點 |
|---|---|
clock() / clock64() |
回傳 per-multiprocessor 計數器;因 thread 被 time-sliced,量到的週期數大於實際執行週期 |
device printf() |
每個 thread 各自執行;回傳解析的引數數(非字元數),上限 32 引數;輸出在 host 端格式化 |
| printf buffer | 預設 1 MB 環狀緩衝,僅在特定同步/啟動事件時 flush;程式結束不會自動 flush |
device malloc()/free() |
從 device heap 配置,回傳 16-byte 對齊;預設 heap 8 MB,須在任何配置前用 cudaLimitMallocHeapSize 設定 |
| heap 互通性 | device-side malloc/new 配置的記憶體不可用 cudaFree/cudaMemcpy 操作,反之亦然 |
alloca() |
在 caller stack frame 配置,回傳時自動釋放;device 端 16-byte 對齊 |
| Lambda 執行空間 | 由最內層 enclosing function 決定;無 enclosing scope 則為 __host__ |
傳給 __global__ |
lambda 須為 __device__ 或 __host__ __device__;global/namespace scope lambda 不可當引數 |
| Extended lambda | 定義在 __host__/__host__ __device__ 函式內、帶執行空間標註的 lambda;需 --extended-lambda |
| 捕捉限制 | extended lambda 只能 by value 捕捉;init-capture 僅 device-only 支援;陣列維度 ≤ 7 |
*this 捕捉 |
C++17 *this by-value 複製物件本身,解決 device 上存取 host this->member 的 runtime error |
__CUDA_ARCH__ |
extended lambda 的存在/數量/捕捉,不可依賴 __CUDA_ARCH__ 是否定義或其值 |
nvstd::function |
<nvfunctional> 提供,host/device 皆可用;不可跨 host/device 邊界傳遞或初始化 |
device 可用的 C standard library functions
device code 只能用一小組 C 標準函式,且多數有 CUDA 專屬語意。建議優先用 <cuda/std/...> 標頭內的 cuda::std:: 對應版本(較安全、可攜)。
| 函式 | 執行空間 | 語意 / 限制 |
|---|---|---|
clock() / clock64() |
clock() 為 __host__ __device__;clock64() 為 __device__ |
回傳 per-multiprocessor、每週期遞增的計數器;對應 cuda::std::clock() 在 <cuda/std/ctime> |
printf() |
__host__ __device__ __tile__ |
host 端格式化輸出;見下節 |
memcpy() / memset() |
__host__ __device__ __tile__ |
複製/填值 size bytes;memset 的 value 視為 unsigned char;建議改用 <cuda/std/cstring> 的 cuda::std:: 版 |
malloc() / free() |
__host__ __device__ |
從 device heap 配置;見下節 |
__nv_aligned_device_malloc() |
__device__ |
對齊配置,align 須為非零 2 的次方 |
alloca() |
__host__ __device__ |
在 caller stack frame 配置,回傳時自動釋放 |
計時除了 clock() 外,亦提供可攜的 <cuda/std/chrono> 實作。
device printf()
__host__ __device__ __tile__ int printf(const char* format[, arg, ...]);
- 行為類似標準 C
printf(),但每個 thread 各自執行:multi-threaded kernel 中每個遇到printf()的 thread 都會在 host stream 印出一份輸出。 - 回傳值與標準 C 不同:回傳解析的引數數(非印出的字元數)。無引數回傳
0;format為NULL回傳-1;內部錯誤回傳-2。 - 內部用共享資料結構,可能改變 thread 執行順序;但 CUDA 本就不保證執行順序(除
__syncthreads()屏障外)。
Format specifiers:%[flags][width][.precision][size]type
| 欄位 | 支援 |
|---|---|
| Flags | #、空白、0、+、- |
| Width | *、0-9 |
| Precision | 0-9 |
| Size | h、l、ll |
| Type | %cdiouxXpeEfgGaAs |
- 最終格式化在 host 端進行,故 format string 必須被 host 編譯器與 C library 理解;無效 flag/type 組合的輸出為 undefined。
- 上限 32 個引數(不含 format string);多出的引數被忽略,format specifier 原樣輸出。
- Windows 的
long是 32-bit、Linux 是 64-bit:在 Linux 編譯、Windows 執行時含%ld的輸出會損毀,建議編譯與執行平台一致。
tile code 中的差異:引數可為 tile(每元素依對應 specifier 印出);回傳值永遠是提供的引數數(即使 NULL 或內部錯誤);format string 必須是 literal;引數數與 specifier 數不符會報錯(SIMT device code 中只是 warning)。
Host-side buffer:固定大小、環狀緩衝,預設 1 MB(cudaLimitPrintfFifoSize 可調)。滿了會覆蓋舊輸出。只在下列事件 flush:
flush 時機:
├─ kernel launch(<<<>>> 或 cuLaunchKernel)啟動時
│ └─ 若 CUDA_LAUNCH_BLOCKING=1,啟動結束時也 flush
├─ 同步:cudaDeviceSynchronize / cuCtxSynchronize / cudaStreamSynchronize
│ / cuStreamSynchronize / cudaEventSynchronize / cuEventSynchronize
├─ blocking memcpy:cudaMemcpy* / cuMemcpy*
├─ module load/unload:cuModuleLoad / cuModuleUnload
├─ context 銷毀:cudaDeviceReset / cuCtxDestroy
└─ 執行 cudaLaunchHostFunc / cuLaunchHostFunc 加入的 stream callback 之前
程式結束時 ⇒ 不會自動 flush
device malloc() / free() 與 heap
malloc()(device 端)、cuda::std::malloccalloc(從 device heap 配置至少 size bytes,回傳對齊 16-byte 的指標;不足回傳NULL。__nv_aligned_device_malloc()/cuda::std::aligned_alloc()回傳align倍數位址,align須為非零 2 的次方。- 配置的記憶體在 CUDA context 生命週期內持續存在(除非被
free()釋放),可被其他 thread、甚至後續 kernel launch 使用;任何 thread 都可釋放他人配置的記憶體,但同一指標不可重複 free(UB)。
device heap 大小必須在任何用到 device heap 的程式(含 new/delete)之前指定,否則預設配置 8 MB。
- 用
cudaDeviceGetLimit / cudaDeviceSetLimit搭配cudaLimitMallocHeapSize取得/設定。 - heap 實際配置發生在 module 載入 context 時;失敗則 module load 回
CUDA_ERROR_SHARED_OBJECT_INIT_FAILED。 - module 載入後 heap 大小不可變、也不會依需求動態調整。
互通性:device-side malloc/calloc/aligned_alloc/new 配置的記憶體不可用 runtime/driver API(cudaMalloc/cudaMemcpy/cudaMemset/cudaFree)操作或釋放;反之,host runtime API 配置的記憶體也不可用 device-side free()/delete 釋放。device heap 的記憶體獨立於 host 端 cudaMalloc 之外。
常見配置模式:per-thread(每 thread 各自 malloc/free)、per-block(thread 0 配置、經 shared memory 分享指標、僅一 thread free)、跨 kernel 持續(__device__ 全域指標陣列保存配置,多次 kernel 重用後再 free)。
alloca()
__host__ __device__ void* alloca(size_t size);
- 在 caller 的 stack frame 配置 size bytes,回傳指標;device 端起始位址 16-byte 對齊。caller 回傳時自動釋放。
Windows 上使用前須先 #include <malloc.h>;alloca() 可能造成 stack overflow,須自行調整 stack 大小。
Lambda 的執行空間與傳給 kernel
compiler 依 lambda 所在最內層 enclosing function scope 的執行空間決定 lambda(closure type, C++11)的執行空間;若無 enclosing function scope,則為 __host__。也可用 extended lambda 語法明確標註。
| lambda 所在處 | 推導執行空間 |
|---|---|
| global / namespace scope | __host__ |
host_function() 內 |
__host__ |
__device__ 函式內 |
__device__ |
__global__ 函式內 |
__device__ |
__host__ __device__ 函式內 |
__host__ __device__ |
__tile__ 函式內 |
__tile__ |
傳給 __global__ 函式參數
- lambda/closure 只能在執行空間為
__device__或__host__ __device__時當作__global__函式的引數。 - global 或 namespace scope 的 lambda 不可當
__global__引數(即使是 extended lambda)。
template <typename T> __global__ void kernel(T input) {}
void host_function() {
kernel<<<1, 1>>>([] __device__() {}); // 正確,extended lambda
kernel<<<1, 1>>>([] __host__ __device__() {}); // 正確,extended lambda
// kernel<<<1, 1>>>([](){}); // 錯誤,host 執行空間的 closure
// kernel<<<1, 1>>>(global_lambda); // 錯誤,extended lambda 但在 global scope
}
在 __device__ 函式內啟動 device kernel(device-side launch)需要 separate compilation(-rdc=true)。
Extended Lambdas 與其限制
--extended-lambda 旗標允許在 lambda 明確標註執行空間(標註置於 lambda introducer 之後、optional declarator 之前)。指定後 nvcc 定義巨集 __CUDACC_EXTENDED_LAMBDA__。
- extended lambda 必須定義在
__host__或__host__ __device__函式的(巢狀)block scope 內。 - extended device lambda:標
__device__;extended host-device lambda:標__host__ __device__。 - 與一般 lambda 不同,extended lambda 可當
__global__函式的 type 引數。
在 __device__ 函式內的 lambda(即使標 __device__/__host__ __device__)都不是 extended lambda,因為 enclosing function 不是 __host__/__host__ __device__。同理,global scope 的 lambda 也不是 extended lambda。
型別 trait
| Trait | 回傳 true 的條件 |
|---|---|
__nv_is_extended_device_lambda_closure_type(T) |
T 是 extended __device__ lambda 的 closure type |
__nv_is_extended_device_lambda_with_preserved_return_type(T) |
上者且定義有 trailing return type(且 return type 未引用任何 lambda 參數名) |
__nv_is_extended_host_device_lambda_closure_type(T) |
T 是 extended __host__ __device__ lambda 的 closure type |
這些 trait 在任何編譯模式皆可用;extended lambda 模式未啟用時一律回傳 false。
主要限制(精選)
CUDA compiler 在送給 host 編譯器前,會把 extended lambda 換成 namespace scope 的placeholder type,其 template 引數需取得 enclosing function 的位址。由此衍生大量限制:
- extended lambda 不可定義在另一個 extended lambda 內。
- 不可定義在 generic lambda(帶
auto參數)內。 - 若巢狀在多層 lambda 內,最外層 lambda 必須定義在某(非 lambda)函式的 block scope 內。
- enclosing function 必須有名稱且位址可取得;若為 class member:所有 enclosing class 須有名稱,且 member function 與 enclosing class 不可為 private/protected。
- 取 enclosing routine 位址須能無歧義(例如 alias 宣告遮蔽同名 template 型別引數時會出錯)。
- 不可定義在函式內 local 的 class 中。
- enclosing function 不可有 deduced return type(
auto回傳)。 - host-device extended lambda 不可是 generic lambda(不可有
auto/auto...參數)。 - enclosing function 為 template 實例時:最多一個 variadic 參數且須在最後、template 參數須具名、實例化引數型別不可為 function-local 或 private/protected member。
- MSVC host 編譯器:enclosing function 須有 external linkage。
- MSVC:extended lambda 不可定義在
if constexprblock body 內。 - 捕捉限制(重要):
- 只能 by value 捕捉,不可 by reference。
- 陣列型別變數維度 > 7 不可捕捉;陣列元素型別須 default-constructible 且 copy-assignable。
- variadic argument pack 的元素不可捕捉。
- 捕捉變數型別不可 function-local(extended lambda closure 除外)或 private/protected member。
- init-capture:host-device extended lambda 不支援;device extended lambda 支援,但 initializer 不可為陣列或
std::initializer_list。 - extended lambda 的
operator()非 constexpr,closure type 非 literal type;不可用constexpr/consteval。 - 巢狀於 extended lambda 內的
if constexprblock 中,變數不可首次隱式捕捉(除非已在外部隱式捕捉或在明確 capture list)。
void host_function() {
auto l1 = [x = 1] __device__ () { return x; }; // 正確:device-only 可 init-capture
// auto l2 = [x = 1] __host__ __device__ () { return x; }; // 錯誤:host-device 不可 init-capture
int a = 1;
// auto l3 = [&a] __device__ () { return a; }; // 錯誤:不可 by-reference 捕捉
}
__CUDA_ARCH__ 與 return type 內省限制
- 13. compiler 對函式內每個 extended lambda 指派 counter;故 extended lambda 的存在與相對宣告順序不可依賴
__CUDA_ARCH__是否定義或其值(不可放在#if defined(__CUDA_ARCH__)內)。 - 16. 傳給
__global__的 lambda,其 body 中捕捉變數的表達式必須與__CUDA_ARCH__無關;否則 device/host 兩次編譯的 closure layout 不同,程式可能執行錯誤。 - 14/15/17. placeholder type 在 host code 中通常不定義等價的
operator()、也不提供 pointer-to-function 轉換運算子;除非..._with_preserved_return_type()為 true,否則在 host code 內省 device-only lambda 的 return/parameter type 會出錯(device code 內省則可)。 - 18. placeholder type 可能定義特殊成員函式,使
std::is_trivially_copyable等 trait 在 CUDA front-end 與 host 編譯器結果不一致;勿用其結果去實例化__global__/__device__/__constant__/__managed__template。
compiler 只會對情況 1-12 的部分產生診斷;情況 13-17 不產生診斷,但 host 編譯器可能編譯失敗。
Host-Device Lambda 優化注意
- 與 device-only lambda 不同,extended host-device lambda 可從 host code 呼叫。CUDA compiler 會把 host code 中的 extended lambda 換成具名的 placeholder type,而 extended host-device lambda 的 placeholder type 是以**間接函式呼叫(indirect function call)**去呼叫原 lambda 的
operator()。 - 正因這個間接呼叫,host 編譯器對 extended host-device lambda 的優化會比隱式或明確的純
__host__lambda 差:純 host lambda 的 body 可被輕易 inline 進呼叫點,但遇到 extended host-device lambda 時,host 編譯器難以將原 lambda body inline。 - device-only lambda 不可從 host 呼叫,故沒有這個 host 端 inline 的問題。
*this 捕捉 by-value(C++17)
依 C++11/14 規則,lambda 引用 class member 時捕捉的是 this 指標(by value),而非 member 本身。若 extended device/host-device lambda 在 host 函式定義、於 GPU 執行,且 this 指向 host memory,存取該 member 會造成 runtime error。
C++17 引入 *this 捕捉模式:複製 *this 指向的物件本身而非指標。
struct MyStruct {
int var;
__host__ __device__ MyStruct() : var(10) {}
void run() {
auto lambda1 = [=, *this] __device__ { // 複製 *this,GPU 存取 copy_of_star_this->var
return var + 1;
};
foo<<<1, 1>>>(lambda1); // kernel launch 成功
cudaDeviceSynchronize();
}
};
- CUDA 支援
*this捕捉用於:__device__/__global__函式內的 lambda,以及 host code 內的 extended device-only lambda(需--extended-lambda)。 - host code 內的非標註 lambda 或 extended host-device lambda,除非語言方言已啟用
*this捕捉,否則不允許。
ADL 副作用:placeholder type 的 template 引數含 enclosing function 位址,可能讓額外 namespace 參與 Argument-Dependent Lookup,導致 host 編譯器選到錯誤的 overload(產生歧義/編譯失敗)。
Polymorphic Function Wrappers function
<nvfunctional> 提供多型函式包裝器 class template nvstd::function,可儲存、複製、呼叫任意 callable target(如 lambda),host 與 device code 皆可用。
#include <nvfunctional>
__global__ void kernel(int* result) {
nvstd::function<int()> fn1 = device_function; // __device__
nvstd::function<int()> fn2 = host_device_function; // __host__ __device__
nvstd::function<int()> fn3 = [](){ return 10; };
*result = fn1() + fn2() + fn3();
}
無效用法:
- host code 的
nvstd::function不可用__device__函式位址(或operator()為__device__的 functor)初始化。 - device code 的
nvstd::function不可用__host__函式(或__host__operator()functor)初始化。 nvstd::function實例不可在 runtime 從 host 傳到 device(或反向)。- 若
__global__函式從 host 啟動,其參數型別不可用nvstd::function。
nvstd::function 的成員(建構/解構/assignment/swap/operator bool/比較)多為 __device__ __host__,但函式呼叫運算子 operator()(ArgTypes...) 僅 __device__。
考試/測驗重點
| 題型 | 關鍵答案 |
|---|---|
device printf() 回傳什麼? |
解析的引數數(非字元數);無引數→0、format 為 NULL→-1、內部錯誤→-2 |
device printf() 引數上限? |
32 個(不含 format string),多出的被忽略 |
| printf buffer 預設大小與性質? | 1 MB、環狀緩衝;滿了覆蓋舊輸出;cudaLimitPrintfFifoSize 調整 |
| printf 何時 flush? | kernel launch、同步、blocking memcpy、module load/unload、context 銷毀、host callback 前;程式結束不會 flush |
%ld 跨平台陷阱? |
Windows long 32-bit、Linux 64-bit;Linux 編譯 + Windows 執行會損毀輸出 |
| device heap 預設大小?如何改? | 預設 8 MB;用 cudaDeviceSetLimit(cudaLimitMallocHeapSize, ...),須在任何配置前 |
| device heap 大小何時不可變? | module 載入 context 後不可變、也不動態調整 |
device malloc() 對齊? |
16-byte;__nv_aligned_device_malloc 的 align 須非零 2 的次方 |
device malloc 配置可用 cudaFree 釋放嗎? |
不可;device heap 與 host runtime API 記憶體互不相通 |
clock() 量到的週期數準嗎? |
偏大;因 thread 被 time-sliced,量到值 > 實際執行指令週期 |
| lambda 執行空間如何決定? | 由最內層 enclosing function scope 決定;無 scope → __host__ |
哪種 lambda 可傳給 __global__? |
執行空間為 __device__ 或 __host__ __device__;global/namespace scope 不可 |
| 什麼是 extended lambda? | 定義在 __host__/__host__ __device__ 函式內、帶執行空間標註的 lambda;需 --extended-lambda |
標了 __device__ 就一定是 extended lambda 嗎? |
否;enclosing function 須為 __host__ 或 __host__ __device__;__device__/__global__ 函式內、或 global/namespace scope 的標註 lambda 都不是 extended lambda |
| extended lambda 啟用的巨集? | __CUDACC_EXTENDED_LAMBDA__ |
| extended lambda 捕捉限制? | 只能 by value;陣列維度 ≤ 7;host-device 不可 init-capture;不可捕 variadic pack 元素 |
為何不可依 __CUDA_ARCH__ 改變 extended lambda? |
compiler 用 counter 識別 lambda;device/host 兩次編譯需一致的 closure layout |
| 為何 extended host-device lambda 在 host 端優化較差? | placeholder type 以間接函式呼叫呼叫 operator(),host 編譯器難以將 lambda body inline;純 __host__ lambda 可輕易 inline,device-only lambda 不可從 host 呼叫故無此問題 |
*this 捕捉解決什麼問題? |
C++17 複製物件本身而非 this 指標,避免 GPU 上存取 host this->member 的 runtime error |
nvstd::function 哪裡定義?跨邊界可傳嗎? |
<nvfunctional>;host/device 皆可用,但不可跨 host/device 邊界傳遞或初始化 |
nvstd::function::operator() 執行空間? |
僅 __device__(其餘成員多為 __device__ __host__) |
| RTTI 與例外是否能在 device code 用? | 不支援(typeid、dynamic_cast 等)— 見 C/C++ Language Restrictions |