External Resource Interoperability (Vulkan/Direct3D/NVSCI)

重點總覽

項目 重點
目的 讓 CUDA 匯入其他 API(Direct3D 11/12、Vulkan、NVSCI)顯式匯出的資源,零複製共享
交換媒介 OS-native handle:Linux 為 file descriptor (fd)、Windows 為 NT handle / D3DKMT handle
兩類資源 Memory object(記憶體)與 Synchronization object(semaphore / fence)
匯入記憶體 cudaImportExternalMemory();映射為 device pointer 或 mipmapped array
映射記憶體 cudaExternalMemoryGetMappedBuffer() / cudaExternalMemoryGetMappedMipmappedArray()
釋放記憶體 cudaDestroyExternalMemory()(不釋放映射);映射另需 cudaFree() / cudaFreeMipmappedArray()
匯入同步物件 cudaImportExternalSemaphore();用 cudaSignal/WaitExternalSemaphoresAsync() signal/wait
釋放同步物件 cudaDestroyExternalSemaphore()(須先完成所有 outstanding signal/wait)
裝置配對 Vulkan 比對 device UUID;Direct3D12 比對 device LUID;NVSCI 用 GPU id (CUuuid)
handle 所有權 fd 匯入後 CUDA 取得所有權;NT handle 由應用程式負責 close;D3DKMT/NvSci 由應用程式維護

External Resource Interoperability 總論(4.19.2)

External resource interoperability 讓 CUDA 能匯入其他 API「顯式匯出」的資源。這些資源通常以 OS-native handle 匯出(Linux 的 file descriptor、Windows 的 NT handle),使 CUDA 與其他 API 之間能高效共享資源,不需中途複製或重製。支援的 API 為 Direct3D 11/12、Vulkan 與 NVSCI。

可匯入的資源分兩類:

其他 API (Vulkan / D3D12 / NVSCI)            CUDA
  create + export resource
        │  OS handle (fd / NT / D3DKMT / NvSciObj)
        ▼
  ┌─ memory ──► cudaImportExternalMemory ─► cudaExternalMemoryGetMappedBuffer
  │                                          / ...GetMappedMipmappedArray ─► kernel 可用
  └─ semaphore ► cudaImportExternalSemaphore ─► cudaSignal/WaitExternalSemaphoresAsync
Important

共享的是「同一塊底層資源」而非副本。映射參數(offset、size、dimensions、format、mip levels)必須完全對齊匯出端設定,任何不一致都導致 undefined behavior。

Vulkan Interoperability(4.19.2.1)

把 Vulkan graphics/compute 與 CUDA compute 部署在同一 GPU 上可最大化使用率並省去複製。整體流程:

  1. 初始化 Vulkan,建立並匯出 external buffer 與 / 或 synchronization object。
  2. 以「相符的 device UUID」設定 CUDA 要用的裝置。
  3. 取得 memory / synchronization handle。
  4. 用 handle 在 CUDA cudaImportExternalMemory() / cudaImportExternalSemaphore() 匯入。
  5. 把 device pointer 或 mipmapped array 映射到記憶體物件上。
  6. 透過對 semaphore 的 signal/wait 定義執行順序,CUDA 與 Vulkan 交替使用同一記憶體。

Setting up a Vulkan device

匯出資源需在建立 Vulkan instance / device 時啟用對應 extension:

extensions.push_back(VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME);
extensions.push_back(VK_KHR_EXTERNAL_SEMAPHORE_EXTENSION_NAME);
extensions.push_back(VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME);
#ifdef _WIN64   // Windows:win32 handle
extensions.push_back(VK_KHR_EXTERNAL_MEMORY_WIN32_EXTENSION_NAME);
#else           // Unix:file descriptor
extensions.push_back(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME);
#endif

重點:handle 型別 enum 為平台專屬,這些 extension 名稱要在 instance/device 建立資訊中啟用。

Matching device UUIDs

匯出的物件必須在「建立它的同一裝置」上匯入並映射。做法是把 CUDA device 的 UUID 與 Vulkan physical device 的 UUID(vkPhysicalDeviceIDProperties.deviceUUID)比對。

cudaGetDeviceProperties(&deviceProp, current_device);
if (deviceProp.computeMode != cudaComputeModeProhibited) {
    // 比對 CUDA 與 Vulkan 的 UUID
    if (memcmp(&deviceProp.uuid, vkDeviceUUID, UUID_SIZE) == 0)
        cudaSetDevice(current_device);   // 找到對應 GPU
}
Warning

該 Vulkan physical device 不可屬於「含一個以上 physical device 的 device group」。即 vkEnumeratePhysicalDeviceGroups 回傳、含此 device 的 group,physical device count 必須為 1。

Exporting Vulkan memory / synchronization objects

Importing memory objects

dedicated 與 non-dedicated 物件皆可匯入;匯入 dedicated 物件須設 cudaExternalMemoryDedicated flag。handle 型別決定 cudaExternalMemoryHandleDesc.type

cudaExternalMemoryHandleDesc desc = {};
// Win32:OpaqueWin32 / OpaqueWin32Kmt;Linux:OpaqueFd
desc.type = cudaExternalMemoryHandleTypeOpaqueFd;
desc.size = size;
#ifdef _WIN64
desc.handle.win32.handle = (HANDLE)getMemHandle(vkMem, handleType);
#else
desc.handle.fd = (int)(uintptr_t)getMemHandle(vkMem, handleType);
#endif
cudaImportExternalMemory(&cudaMem, &desc);
Warning

handle 所有權差異(重要考點):

  • Linux fd:匯入後 CUDA 取得所有權,匯入成功後再用該 fd 屬 undefined behavior。
  • Windows NT handle(OPAQUE_WIN32):CUDA 取得所有權,應用程式須在不需要時自行 CloseHandle();NT handle 持有資源 reference,須先釋放才能釋放底層記憶體。
  • D3DKMT (OPAQUE_WIN32_KMT) 全域共享 handle:不持有 reference,當其他 reference 全部釋放時自動銷毀。

也可用 named handle(desc.handle.win32.name)匯入 OPAQUE_WIN32 記憶體。

Mapping buffers / mipmapped arrays

cudaExternalMemoryBufferDesc bufDesc = {};
bufDesc.offset = 0; bufDesc.size = size; bufDesc.flags = 0;
cudaExternalMemoryGetMappedBuffer(cudaPtr, cudaMem, &bufDesc);  // -> 之後 cudaFree()

實務上需把 Vulkan 的 VkFormatVkExtent3D/VkImageViewTypeVkImageUsageFlags 轉成 CUDA 的 cudaChannelFormatDesccudaExtent、array flags(cube/layered/colorAttachment/surfaceLoadStore)。

Importing synchronization objects

handle 型別映射到 cudaExternalSemaphoreHandleDesc.type

Signaling / Waiting

// timeline:以遞增 value 定序,CUDA 等 Vulkan 完成 -> 跑 kernel -> signal 讓 Vulkan 繼續
cudaExternalSemaphoreWaitParams   waitParams   = {}; waitParams.params.fence.value   = waitValue;
cudaExternalSemaphoreSignalParams signalParams = {}; signalParams.params.fence.value = signalValue;
cudaWaitExternalSemaphoresAsync(&m_cudaTimelineSemaphore, &waitParams, 1, m_stream);
m_sim.stepSimulation(time, m_stream);   // CUDA kernel
cudaSignalExternalSemaphoresAsync(&m_cudaTimelineSemaphore, &signalParams, 1, m_stream);
waitValue += 2; signalValue += 2;       // binary 版則 value 固定為 0
Vulkan render ──signal──►  CUDA wait ──► CUDA kernel(step) ──signal──► Vulkan wait ──► render(更新後 buffer)
   (timeline: 同一 semaphore,value 1,2,3,4...;binary: 兩個 semaphore,value 恆 0)
Tip

Vulkan 也能匯入 Vulkan 匯出的 memory/semaphore,因此 OpenGL-CUDA interop 可走「Vulkan 匯出 → OpenGL 與 CUDA 各自匯入」的替代路徑(見 GL_EXT_memory_object* / GL_EXT_semaphore* extension)。

Direct3D Interoperability(4.19.2.2)

支援 Direct3D 11 與 12 資源匯入 CUDA(本節聚焦 D3D12)。

Matching device LUIDs

匯出物件須在同一裝置匯入;以 CUDA device 的 LUID 與 Direct3D12 device 的 LUID 比對。

LUID d3d12Luid = d3d12Device->GetAdapterLuid();
// 逐 CUDA device 比對 LowPart 與 HighPart
if (!memcmp(&d3d12Luid.LowPart, cudaLuid, sizeof(d3d12Luid.LowPart)) &&
    !memcmp(&d3d12Luid.HighPart, cudaLuid + sizeof(d3d12Luid.LowPart), sizeof(d3d12Luid.HighPart)))
    return cudaDevice;
Warning

Direct3D12 device 不可建立在 linked node adapter 上,即 ID3D12Device::GetNodeCount 必須回傳 1。

Importing memory objects

D3D12 資源從 NT handle 匯入,一律須設 cudaExternalMemoryDedicated(committed resource 尤其)。NT handle 由應用程式負責 close,且須先釋放才能釋放底層記憶體。

來源 desc.type 建立時的 flag
Shared heap(CreateHeap cudaExternalMemoryHandleTypeD3D12Heap D3D12_HEAP_FLAG_SHARED
Committed resource(CreateCommittedResource cudaExternalMemoryHandleTypeD3D12Resource D3D12_HEAP_FLAG_SHARED + cudaExternalMemoryDedicated
cudaExternalMemoryHandleDesc desc = {};
desc.type = cudaExternalMemoryHandleTypeD3D12Resource;
desc.handle.win32.handle = (void *)handle;   // 或 desc.handle.win32.name = name
desc.size = size;
desc.flags |= cudaExternalMemoryDedicated;
cudaImportExternalMemory(&extMem, &desc);
CloseHandle(handle);   // NT handle 由應用程式負責關閉

Mapping & Importing sync

// signal:設定 fence value,對應 wait 須在 D3D12 端、且在此 signal 之後發出
params.params.fence.value = value;
cudaSignalExternalSemaphoresAsync(&extSem, &params, 1, stream);
// wait:等到 fence value >= 指定值,對應 signal 須在 D3D12 端、且在此 wait 之前發出
cudaWaitExternalSemaphoresAsync(&extSem, &params, 1, stream);

D3D12 fence 為單調遞增:wait 等到「value >= 指定值」即通過。

NVSCI Interoperability(4.19.2.3)

NVSCI 提供兩個介面:NvSciBuf(配置與交換記憶體 buffer)與 NvSciSync(在操作邊界管理同步物件),常見於 NVIDIA DRIVE 平台。

Importing memory objects(NvSciBuf)

配置與 CUDA device 相容的 NvSciBuf 物件時,須在 attribute list 用 NvSciBufGeneralAttrKey_GpuId 設定對應 GPU id(取自 cuDeviceGetUuidCUuuid)。可選屬性:

匯入 CUDA 時填 cudaExternalMemoryHandleDesc

cudaExternalMemoryHandleDesc memHandleDesc = {};
memHandleDesc.type = cudaExternalMemoryHandleTypeNvSciBuf;
memHandleDesc.handle.nvSciBufObject = bufferObjRo;  // 以縮減權限的物件匯入
memHandleDesc.size = ret_size;                       // 由 NvSciBuf 屬性查得
cudaImportExternalMemory(&extMemBuffer, &memHandleDesc);
Important

attribute list 與 NvSciBuf 物件須由「應用程式自行維護」。若該 NvSciBuf 也被其他 driver 映射,須依輸出屬性 NvSciBufGeneralAttrKey_GpuSwNeedCacheCoherency 用 NvSciSync 做適當 barrier 維持 CUDA 與其他 driver 間的一致性。cache/compression 為 per-GPU 屬性,須以 UUID 比對讀取對應 GPU 的值。

Mapping(NvSciBuf)

Warning

NvSciBuf 映射 mipmapped array 時,mip level 數必須為 1

Importing synchronization objects(NvSciSync)

cudaDeviceGetNvSciSyncAttributes() 取得與某 CUDA device 相容的 NvSciSync 屬性,分別生成 signaler / waiter attribute list(CUDA_NVSCISYNC_ATTR_SIGNAL / CUDA_NVSCISYNC_ATTR_WAIT),reconcile 後 NvSciSyncObjAlloc() 建立物件,再匯入 CUDA:

cudaDeviceGetNvSciSyncAttributes(signalerAttrList, cudaDev0, CUDA_NVSCISYNC_ATTR_SIGNAL);
cudaDeviceGetNvSciSyncAttributes(waiterAttrList,   cudaDev1, CUDA_NVSCISYNC_ATTR_WAIT);
// ... NvSciSyncAttrListReconcile -> NvSciSyncObjAlloc ...
cudaExternalSemaphoreHandleDesc desc = {};
desc.type = cudaExternalSemaphoreHandleTypeNvSciSync;
desc.handle.nvSciSyncObj = nvSciSyncObj;
cudaImportExternalSemaphore(&extSem, &desc);
Warning

NvSciSyncObj 的所有權匯入後仍屬應用程式;在匯入後刪除 / 釋放 nvSciSyncObj 會導致 CUDA undefined behavior

Signaling / Waiting(NvSciSync)

signalParams.params.nvSciSync.fence = (void*)fence;
signalParams.flags = 0;  // 或 cudaExternalSemaphoreSignalSkipNvSciBufMemSync
cudaSignalExternalSemaphoresAsync(&extSem, &signalParams, 1, stream);

三種 API 對照

面向 Vulkan Direct3D12 NVSCI
裝置配對 device UUID device LUID GPU id(CUuuid)
裝置限制 device group count = 1 GetNodeCount = 1 由 GpuId 屬性指定
memory handle fd / NT handle / D3DKMT NT handle(heap / resource) NvSciBufObj
dedicated dedicated 物件須設 flag 一律須設 cudaExternalMemoryDedicated 不適用
sync 物件 binary / timeline semaphore D3D12 fence(單調遞增) NvSciSync object
sync type OpaqueFd/Win32(/Kmt)、Timeline... D3D12Fence NvSciSync

考試/測驗重點

問題 答案
匯入外部記憶體 / 同步物件的函式 cudaImportExternalMemory() / cudaImportExternalSemaphore()
把外部記憶體映射成 device pointer / array 的函式 cudaExternalMemoryGetMappedBuffer() / cudaExternalMemoryGetMappedMipmappedArray()
釋放外部記憶體物件用什麼?它會釋放映射嗎 cudaDestroyExternalMemory();不會,映射另以 cudaFree() / cudaFreeMipmappedArray() 釋放
signal / wait 外部 semaphore 的函式 cudaSignalExternalSemaphoresAsync() / cudaWaitExternalSemaphoresAsync()
Vulkan / D3D12 / NVSCI 各用什麼配對裝置 UUID / LUID / GPU id (CUuuid)
Linux fd 與 Windows NT handle 的所有權差異 fd 匯入後 CUDA 接管(勿再用);NT handle 由應用程式自行 CloseHandle
D3DKMT (Win32_KMT) handle 的生命週期 不持有 reference,其他 reference 全釋放時自動銷毀
匯入 D3D12 資源必設的 flag cudaExternalMemoryDedicated
Vulkan binary vs timeline semaphore binary 1-bit、value 恆 0;timeline 64-bit、可用 value 定多步順序
D3D12 fence wait 的通過條件 fence value >= 指定值(單調遞增)
NVSCI 映射 mipmapped array 的 mip level 數限制 必須為 1
NvSciSyncObj 匯入後可否釋放 否,匯入後刪除 / 釋放會造成 CUDA undefined behavior
何時設 SkipNvSciBufMemSync flag 當 NvSciBufGeneralAttrKey_GpuSwNeedCacheCoherency 為 FALSE 時
binary semaphore signal/wait 的順序限制 wait 必須在對應 signal 已發出之後才能發出
Vulkan physical device / D3D12 device 的單裝置限制 device group count 須為 1 / GetNodeCount 須為 1