Stream-Ordered Memory Allocator

重點總覽

項目 重點
動機 cudaMalloc/cudaFree 會讓 GPU 跨所有 stream 同步;stream-ordered allocator 把配置/釋放排入 stream,與 kernel、async copy 一起依序執行
核心 API cudaMallocAsync 配置、cudaFreeAsync 釋放,兩者皆吃 stream 參數定義「何時可用、何時停止可用」
記憶體重用 利用 stream-ordering 語意重用釋放的記憶體,並可控制 caching 行為以避免昂貴的 OS 呼叫
Memory pool 封裝 virtual address 與 physical memory 資源;分 default/implicit 與 explicit 兩類
Default/implicit pool 每個 device 預設即有、非 migratable、永遠可從該 device 存取、不支援 IPC
Explicit pool cudaMemPoolCreate 建立;可要求 IPC 能力、最大池大小、CPU NUMA node 位置等屬性
Multi-GPU 存取 cudaMemPoolSetAccess 控制;不跟隨 peer access,需 device 為 peer capable
IPC pool 先共享 pool(建立安全邊界),再共享個別 allocation(協調虛擬位址與映射時機)
釋放門檻 cudaMemPoolAttrReleaseThreshold 控制 pool 在歸還 OS 前最多持有多少 bytes
Reuse policy FollowEventDependencies、AllowOpportunistic、AllowInternalDependencies 三種可調策略
同步 API 整合 stream/event/device synchronize 時,driver 釋出已保證完成的 free 並檢查 release threshold
三大好處

  1. 降低自訂記憶體管理抽象層的需求;2) 讓多個 library 共享 driver 管理的同一個 pool,減少冗餘記憶體佔用;3) driver 因感知 allocator 與 stream 管理 API 而能做最佳化。Nsight Compute 與 Next-Gen CUDA debugger 自 CUDA 11.3 起感知此 allocator。

配置與釋放語意(Stream-Ordered Semantics)

cudaMallocAsync 觸發綁定到特定 stream 的非同步配置,cudaFreeAsync 則以 stream-ordered 方式釋放。兩者都不阻塞 host 或其他 stream,避免 cudaMalloc/cudaFree 的昂貴同步。

void *ptr;
size_t size = 512;
cudaMallocAsync(&ptr, size, cudaStreamPerThread);
// do work using the allocation
kernel<<<..., cudaStreamPerThread>>>(ptr, ...);
// 不需同步 CPU 與 GPU 即可指定非同步釋放
cudaFreeAsync(ptr, cudaStreamPerThread);
時序(單一 stream):
 stream: [ mallocAsync ]──[ kernel use ]──[ freeAsync ]
            位址有效起點 ────────────────► 位址失效起點
跨 stream 存取規則

從「非配置 stream」存取 allocation 時,使用者必須保證存取發生在配置動作之後,否則行為未定義。釋放動作開始後再使用 allocation 同樣為未定義行為。

跨 stream 釋放需用 event/stream 同步

當配置、使用、釋放分散在不同 stream,必須用 event 或 stream 同步保證順序。

cudaMallocAsync(&ptr, size, stream1);
cudaEventRecord(event1, stream1);
cudaStreamWaitEvent(stream2, event1);   // stream2 等配置就緒才存取
kernel<<<..., stream2>>>(ptr, ...);
cudaEventRecord(event2, stream2);
cudaStreamWaitEvent(stream3, event2);   // stream3 等存取完成才釋放
cudaFreeAsync(ptr, stream3);

重點:free 前所有對該記憶體的存取都必須完成;event 把「就緒」與「用完」這兩個時間點接力傳遞。

與 cudaMalloc/cudaFree 的互通

配置方式 釋放方式 行為
cudaMalloc cudaFreeAsync 可行,但需保證 free 開始前所有存取已完成
cudaMallocAsync cudaFree 可行,driver 假設所有存取已完成、不再額外同步
cudaMallocAsync(&ptr, size, stream);
kernel<<<..., stream>>>(ptr, ...);
cudaStreamSynchronize(stream);  // 必須同步,否則會過早釋放
cudaFree(ptr);
cudaFree 不替你同步

用 cudaFree 釋放 cudaMallocAsync 的配置時,driver 不做任何進一步同步。使用者須自行用 cudaStreamQuery / cudaStreamSynchronize / cudaEventQuery / cudaEventSynchronize / cudaDeviceSynchronize 保證 GPU 不會再存取該 allocation。

Memory Pools

memory pool 封裝 virtual address 與 physical memory 資源,依 pool 屬性與性質配置管理。pool 最主要的面向是它管理的記憶體種類與位置。

current pool 一定是本地的

device 的 current mempool 一律 local 於該 device,因此「不指定 pool 的配置」永遠落在 stream 所屬 device 上。

Default / Implicit Pools

Explicit Pools

cudaMemPoolCreate 建立 explicit pool,可要求 default/implicit pool 無法提供的屬性(IPC 能力、最大池大小、駐於特定 CPU NUMA node 等)。

// 類似 device 0 implicit pool 的 pool
int device = 0;
cudaMemPoolProps poolProps = { };
poolProps.allocType     = cudaMemAllocationTypePinned;
poolProps.location.id   = device;
poolProps.location.type = cudaMemLocationTypeDevice;
cudaMemPoolCreate(&memPool, &poolProps);

建立駐於 CPU NUMA node 且可經 file descriptor 做 IPC 的 pool,只需改 location.type 為 cudaMemLocationTypeHostNuma 並設 handleType 為 cudaMemHandleTypePosixFileDescriptor。

Device Accessibility for Multi-GPU Support

重點 說明
不跟隨 peer access pool 存取性遵循 cudaDeviceEnablePeerAccess / cuCtxEnablePeerAccess
預設只本地可存取 allocation 預設僅其所在 device 可存取,且此存取不可撤銷
開放他機存取 用 cudaMemPoolSetAccess;存取 device 須與 pool 之 device peer capable(用 cudaDeviceCanAccessPeer 驗證)
影響範圍 SetAccess/GetAccess 影響 pool 內所有 allocation,非僅未來的
例外與陷阱

若未先檢查 peer capability,SetAccess 可能回傳 cudaErrorInvalidDevice。但若此時 pool 尚無任何 allocation,SetAccess 可能成功——而下一次配置才會失敗。不建議頻繁變更某 GPU 的存取設定;pool 一旦對某 GPU 開放,應在其 lifetime 內維持開放。

cudaError_t setAccessOnDevice(cudaMemPool_t memPool, int residentDevice,
                              int accessingDevice) {
  cudaMemAccessDesc accessDesc = {};
  accessDesc.location.type = cudaMemLocationTypeDevice;
  accessDesc.location.id   = accessingDevice;
  accessDesc.flags         = cudaMemAccessFlagsProtReadWrite;
  int canAccess = 0;
  cudaError_t error = cudaDeviceCanAccessPeer(&canAccess, accessingDevice,
                                              residentDevice);
  if (error != cudaSuccess) return error;
  else if (canAccess == 0)  return cudaErrorPeerAccessUnsupported;
  return cudaMemPoolSetAccess(memPool, &accessDesc, 1);  // 開放位址可存取
}

Enabling Memory Pools for IPC

CUDA 的 IPC memory pool 提供與 virtual memory management API 相同的安全保障。跨 process 共享需兩步:

步驟一:共享 pool(建立並強制安全邊界)
  export proc:  cudaMemPoolExportToShareableHandle ──OS IPC──► import proc:
                                                              cudaMemPoolImportFromShareableHandle

步驟二:共享個別 allocation(協調虛擬位址與映射時機)
  export proc:  cudaMemPoolExportPointer ──任意機制──► import proc:
                                                       cudaMemPoolImportPointer
// exporting process:建立可匯出之 IPC pool
poolProps.handleTypes = CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR;
cudaMemPoolCreate(&memPool, &poolProps);
int fdHandle = 0;
cudaMemPoolExportToShareableHandle(&fdHandle, memPool,
        CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR, 0);  // 傳指標進去
// importing process:由 shareable handle 建立 imported pool(handle 以 value 傳入)
cudaMemPoolImportFromShareableHandle(&importedMemPool, (void*)fdHandle,
        CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR, 0);
Imported pool 須自行開放存取

imported pool 初始只能從其 resident device 存取,且不繼承 exporting process 設定的任何存取性。importing process 必須對它計畫存取的每個 GPU 呼叫 cudaMemPoolSetAccess。若 pool 所屬 device 對 importing process 不可見,更必須用此 API 開放會用到的 GPU。

共享與釋放 allocation(IPC event 同步)

pool 共享後,exporting process 用 cudaMallocAsync 從 pool 配置的記憶體即可分享給已匯入的 process。安全策略在 pool 層級建立驗證,故 opaque 的 cudaMemPoolPtrExportData 可用任意機制傳遞。

// exporting:配置並記錄 ready event(cudaEventInterprocess + cudaEventDisableTiming)
cudaMallocAsync(&ptr, size, exportMemPool, stream);
cudaEventRecord(readyIpcEvent, stream);
cudaMemPoolExportPointer(&exportData, ptr);
cudaIpcGetEventHandle(&readyIpcEventHandle, readyIpcEvent);
// importing:匯入後等 ready event 才使用(import 不阻塞於 allocation 就緒)
cudaMemPoolImportPointer(&ptr, importedMemPool, importData);
cudaStreamWaitEvent(stream, readyIpcEvent);
kernel<<<..., stream>>>(ptr, ...);
釋放順序:importing 必須先於 exporting

釋放時 allocation 必須在 importing process 先被釋放,才能在 exporting process 釋放。可用 CUDA IPC event 在兩端的 cudaFreeAsync 之間提供同步;cudaFree 也可在兩端使用,且可改用其他 stream 同步 API。importing 端的 free 不會阻止 exporting 端繼續使用該 allocation。

importing: kernel ─ freeAsync ─ ipcEventRecord(finished) ─┐
                                                          ▼ (跨 process)
exporting:                      streamWaitEvent(finished) ─ kernel ─ freeAsync

IPC Export / Import Pool 限制

限制 Export pool Import pool
歸還 physical block 給 OS 不支援(cudaMemPoolTrimTo 無效、ReleaseThreshold 被忽略) 不支援
從 pool 配置 可(cudaMallocAsync) 不可(不能設為 current、不能用於 cudaMallocFromPoolAsync)
reuse policy 屬性 有意義 無意義
resource usage stat 反映本 process 配置 僅反映本 process 匯入的 allocation 及其關聯 physical memory
Export pool 不釋放 physical block

IPC pool 目前無法把 physical block 歸還 OS,故 cudaMemPoolTrimTo 無效、cudaMemPoolAttrReleaseThreshold 形同被忽略。此行為由 driver(非 runtime)控制,未來 driver 可能變更。

Best Practices and Tuning

Query for Support

int driverVersion = 0, deviceSupportsMemoryPools = 0, poolSupportedHandleTypes = 0;
cudaDriverGetVersion(&driverVersion);
if (driverVersion >= 11020)
  cudaDeviceGetAttribute(&deviceSupportsMemoryPools,
                         cudaDevAttrMemoryPoolsSupported, device);
if (driverVersion >= 11030)
  cudaDeviceGetAttribute(&poolSupportedHandleTypes,
                         cudaDevAttrMemoryPoolSupportedHandleTypes, device);

Physical Page Caching Behavior

預設下 allocator 盡量縮小 pool 持有的 physical memory。要減少 OS 配置/釋放呼叫,應為每個 pool 設定記憶體足跡——即 release threshold。

cuuint64_t setVal = UINT64_MAX;
cudaMemPoolSetAttribute(memPool, cudaMemPoolAttrReleaseThreshold, &setVal);
// 需要時顯式縮小:minBytesToKeep 保留下一階段預期需要量
cudaStreamSynchronize(stream);     // 讓 trim 知道 allocation 已不再使用
cudaMemPoolTrimTo(memPool, 0);

重點:把 threshold 設高等於停用自動縮小;此時若想釋放足跡須顯式呼叫 cudaMemPoolTrimTo,minBytesToKeep 讓你保留一定量供後續階段使用。

Resource Usage Statistics

屬性 意義
cudaMemPoolAttrReservedMemCurrent pool 目前消耗的 physical GPU 記憶體總量
cudaMemPoolAttrUsedMemCurrent 從 pool 配置且尚不可重用的記憶體總大小
cudaMemPoolAttr*MemHigh 自上次 reset 起對應 *MemCurrent 的最高水位(watermark)

Memory Reuse Policies

服務配置請求時,driver 會先嘗試重用先前 cudaFreeAsync 釋放的記憶體,再向 OS 配置新記憶體。同一 stream 釋放的記憶體可立即被該 stream 後續配置重用;當 stream 與 CPU 同步後,先前在該 stream 釋放的記憶體即可供任意 stream 配置重用。policy 對 default 與 explicit pool 皆適用。

Policy 屬性 行為
cudaMemPoolReuseFollowEventDependencies 配置更多 physical memory 前,檢查 CUDA event 建立的依賴,嘗試從「另一 stream 釋放」的記憶體配置
cudaMemPoolReuseAllowOpportunistic 檢查已釋放配置是否已過 free 的 stream-order 點(stream 執行已通過該點)即可重用;停用此項仍會重用「stream 與 CPU 同步後」釋出的記憶體
cudaMemPoolReuseAllowInternalDependencies 向 OS 配置失敗時,找「依賴另一 stream 待完成進度」的記憶體,driver 插入所需依賴後重用
配置請求 ─► 可重用已釋放記憶體?
              ├─ Follow event deps:循 event 依賴跨 stream 重用
              ├─ Opportunistic:free 之 stream-order 已達成即重用
              └─ 否則向 OS 配置;失敗時 Internal deps:插入依賴後重用
何時該停用 reuse policy

Opportunistic 重用會因 CPU/GPU 執行交錯而引入 run-to-run 的配置模式變異;Internal dependency 插入可能以非預期、非確定的方式序列化工作。若使用者偏好在配置失敗時自行顯式同步 event/stream,可停用這些策略。

Synchronization API Actions

allocator 身為 CUDA driver 的一部分,與 synchronize API 整合:

附錄要點(Addendums)

主題 重點
cudaMemcpyAsync context 敏感 涉及 cudaMallocAsync 記憶體的 async memcpy 應以指定 stream 的 context 為呼叫 thread 的 current context;cudaMemcpyPeerAsync 例外(用 API 指定的 primary context)
cudaPointerGetAttributes 對已 cudaFreeAsync 的 allocation 查詢為未定義行為(即使仍可從某 stream 存取也一樣)
cudaGraphAddMemsetNode 不支援 stream-ordered allocator 的記憶體;但 memset 可用 stream capture
Pointer Attributes 查詢可用:CU_POINTER_ATTRIBUTE_CONTEXT 成功但回 NULL(非 context 關聯);CU_POINTER_ATTRIBUTE_DEVICE_ORDINAL 判定位置;CU_POINTER_ATTRIBUTE_MEMPOOL_HANDLE(11.3 新增)確認來源 pool
CPU Virtual Memory 使用此 allocator 時避免用 ulimit -v 設 VRAM 限制(不支援)

考試/測驗重點

主題 常見考點
動機 cudaMalloc/cudaFree 會跨所有 stream 同步 / cudaMallocAsync/cudaFreeAsync 排入 stream 不阻塞
device 決定 cudaMallocAsync 忽略 current device/context,依「pool 或 stream」決定 device
跨 stream 規則 存取須在配置之後 / free 開始後再用為未定義行為,須用 event 或 stream synchronize
cudaFree 釋放 async 配置 driver 不再同步,使用者須先 cudaStreamSynchronize 等避免過早釋放
default pool non-migratable / 永遠該 device 可存取 / 不支援 IPC(implicit)
explicit pool cudaMemPoolCreate / 可設 IPC、最大池大小、CPU NUMA node
multi-GPU 存取 不跟隨 peer access / cudaMemPoolSetAccess + cudaDeviceCanAccessPeer / 影響 pool 內所有 allocation
SetAccess 陷阱 未檢 peer 可能 cudaErrorInvalidDevice;無 allocation 時可能成功但下次配置失敗
IPC 兩步 先共享 pool(安全)再共享 allocation(協調位址/時機)
imported pool 不繼承 exporter 存取性 / 須自行 SetAccess / 不能配置
IPC 釋放順序 importing 必須先於 exporting 釋放
IPC pool 限制 不歸還 physical block / TrimTo 無效 / ReleaseThreshold 被忽略
release threshold 控制歸還 OS 的門檻 / UINT64_MAX 停用自動縮小 / 超門檻於下次 synchronize 歸還
usage stats ReservedMemCurrent vs UsedMemCurrent / *MemHigh watermark 可 reset
reuse policy FollowEventDependencies / AllowOpportunistic / AllowInternalDependencies 各自觸發條件
同步整合 synchronize 時 driver 釋出保證完成的 free 並檢查 release threshold
查詢支援 cudaDevAttrMemoryPoolsSupported / cudaDevAttrMemoryPoolSupportedHandleTypes(先查 driver version)