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 |
- 降低自訂記憶體管理抽象層的需求;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 內「配置 → 使用 → 釋放」是最基本的正確用法。
- cudaMallocAsync 忽略 current device/context,改依「指定的 memory pool 或 supplied stream」決定 allocation 落在哪個 device。
時序(單一 stream):
stream: [ mallocAsync ]──[ kernel use ]──[ freeAsync ]
位址有效起點 ────────────────► 位址失效起點
從「非配置 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 釋放 cudaMallocAsync 的配置時,driver 不做任何進一步同步。使用者須自行用 cudaStreamQuery / cudaStreamSynchronize / cudaEventQuery / cudaEventSynchronize / cudaDeviceSynchronize 保證 GPU 不會再存取該 allocation。
Memory Pools
memory pool 封裝 virtual address 與 physical memory 資源,依 pool 屬性與性質配置管理。pool 最主要的面向是它管理的記憶體種類與位置。
- 所有 cudaMallocAsync 都從某個 pool 取用資源;未指定時用 supplied stream 之 device 的 current pool。
- current pool 以 cudaDeviceSetMempool 設定、cudaDeviceGetMempool 查詢;未呼叫 set 前 default pool 為作用中。
- cudaMallocFromPoolAsync(及 cudaMallocAsync 的 C++ overload)可在不設為 current pool 的情況下指定 pool。
- cudaDeviceGetDefaultMempool / cudaMemPoolCreate 回傳 pool handle;cudaMemPoolSetAttribute / cudaMemPoolGetAttribute 控制屬性。
device 的 current mempool 一律 local 於該 device,因此「不指定 pool 的配置」永遠落在 stream 所屬 device 上。
Default / Implicit Pools
- 以 cudaDeviceGetDefaultMempool 取得。
- 配置物為 non-migratable 的 device allocation,位於該 device,永遠可從該 device 存取。
- 存取性可用 cudaMemPoolSetAccess 修改、cudaMemPoolGetAccess 查詢。
- 因不需顯式建立,常稱為 implicit pool;default pool 不支援 IPC。
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
- pool 必須以「pool properties 中指定的 handle type」建立,cudaMemPoolExportToShareableHandle 才會成功。
- handleTypes 設為非零即讓 pool 可匯出(IPC capable),例如 CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR(FD 為整數型 handle)。
// 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 初始只能從其 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, ...);
釋放時 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 |
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);
- cudaDevAttrMemoryPoolsSupported:是否支援 stream-ordered allocator。
- cudaDevAttrMemoryPoolSupportedHandleTypes:IPC 支援的 handle type(CUDA 11.3 新增;舊 driver 查詢會回 cudaErrorInvalidValue)。
- 先做 driver version 檢查可避免該錯誤;或用 cudaGetLastError 清除錯誤。
Physical Page Caching Behavior
預設下 allocator 盡量縮小 pool 持有的 physical memory。要減少 OS 配置/釋放呼叫,應為每個 pool 設定記憶體足跡——即 release threshold。
- cudaMemPoolAttrReleaseThreshold:pool 在試圖把記憶體歸還 OS 前可持有的 bytes。
- 超過門檻時,allocator 會在下一次 stream / event / device synchronize 嘗試歸還。
- 設為 UINT64_MAX 可防止 driver 在每次同步後縮小 pool。
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) |
- *MemHigh watermark 可用 cudaMemPoolSetAttribute 把它設回 current 值(傳入 0 即重置為 current)。
- 可寫 helper 用 cudaMemPoolGetAttribute 一次抓 reserved/reservedHigh/used/usedHigh。
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:插入依賴後重用
- 三者皆透過 cudaMemPoolSetAttribute 啟用/停用;升級 driver 可能變更/增補/重排 reuse policy 列舉。
- FollowEventDependencies 不受 Opportunistic 停用影響,兩者獨立。
Opportunistic 重用會因 CPU/GPU 執行交錯而引入 run-to-run 的配置模式變異;Internal dependency 插入可能以非預期、非確定的方式序列化工作。若使用者偏好在配置失敗時自行顯式同步 event/stream,可停用這些策略。
Synchronization API Actions
allocator 身為 CUDA driver 的一部分,與 synchronize API 整合:
- 使用者請求 driver 同步時,driver 等非同步工作完成。
- 回傳前,driver 判定哪些 free 已「保證完成」,這些 allocation 即可供配置——不論指定 stream 或停用的配置策略。
- driver 同時檢查 cudaMemPoolAttrReleaseThreshold,釋放可釋放的多餘 physical memory。
附錄要點(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) |
Related Notes
- 04-CUDA-Features/05-CUDA-Graphs-Memory-Nodes-and-Device-Launch
- 04-CUDA-Features/19-Interprocess-Communication
- 04-CUDA-Features/20-Virtual-Memory-Management
- 04-CUDA-Features/21-Extended-GPU-Memory
- 04-CUDA-Features/01-Unified-Memory-Full-Support
- 04-CUDA-Features/02-Unified-Memory-Platforms-and-Hints
- 03-Advanced-CUDA/10-Tour-of-CUDA-Features
- 02-Programming-GPUs/16-Unified-and-System-Memory
- 04-CUDA-Features/Practice-CUDA-Features
- 00-Dashboard/Exam-Traps