4.3. 流有序内存分配器
4.3.1. 简介
使用cudaMalloc和cudaFree管理内存分配会导致 GPU 在所有正在执行的 CUDA 流之间进行同步。流有序内存分配器使应用程序能够确定内存分配和释放与启动到 CUDA 流中的其他工作之间的先后顺序,这些工作包括核函数启动和异步复制。它利用流有序语义复用已分配的内存,从而改善应用程序的内存使用。分配器还允许应用程序控制分配器的内存缓存行为。设置适当的释放阈值后,当应用程序表示愿意接受更大的内存占用量时,缓存行为使分配器能够避免开销高昂的操作系统调用。该分配器还支持在进程之间轻松、安全地共享已分配的内存。
流有序内存分配器:
减少了对自定义内存管理抽象的需求,并使有需要的应用程序能够更轻松地创建高性能的自定义内存管理。
使多个库能够共享由驱动程序管理的公共内存池。这可以减少多余的内存消耗。
允许驱动程序基于其对分配器和其他流管理 API 的了解来执行优化。
注意
自 CUDA 11.3 起,Nsight Compute 和 Next-Gen CUDA 调试器能够感知该分配器。
4.3.2. 内存管理
cudaMallocAsync和cudaFreeAsync是实现流有序内存管理的 API。cudaMallocAsync返回一块分配的内存,而cudaFreeAsync释放一块分配的内存。这两个 API 都接受流参数,以定义所分配的内存何时开始可用以及何时停止可用。这些函数允许将内存操作绑定到特定的 CUDA 流,使这些操作能够在不阻塞主机或其他流的情况下进行。通过避免cudaMalloc和cudaFree可能开销高昂的同步,可以提高应用程序性能。
这些 API 可通过内存池进一步优化性能,内存池管理和复用大块内存,以实现更高效的分配和释放。内存池有助于减少开销并防止碎片化,从而提高频繁进行内存分配操作的场景中的性能。
4.3.2.1. 分配内存
cudaMallocAsync函数在 GPU 上触发异步内存分配,并将其关联到特定的 CUDA 流。cudaMallocAsync允许在不妨碍主机或其他流的情况下进行内存分配,从而消除开销高昂的同步的必要性。
注意
cudaMallocAsync在确定所分配的内存将位于何处时,会忽略当前设备/上下文。相反,cudaMallocAsync会根据指定的内存池或提供的流来确定适当的设备。
以下代码清单展示了一种基本使用模式:内存被分配、使用,然后释放回同一流。
void *ptr;
size_t size = 512;
cudaMallocAsync(&ptr, size, cudaStreamPerThread);
// do work using the allocation
kernel<<<..., cudaStreamPerThread>>>(ptr, ...);
// An asynchronous free can be specified without synchronizing the cpu and GPU
cudaFreeAsync(ptr, cudaStreamPerThread);
注意
从执行分配的流以外的流访问所分配的内存时,用户必须保证访问发生在分配操作之后,否则行为未定义。
4.3.2.2. 释放内存
cudaFreeAsync()以流有序的方式异步释放设备内存,这意味着内存释放被分配给特定的 CUDA 流,并且不会阻塞主机或其他流。
用户必须保证释放操作发生在分配操作以及对所分配内存的任何使用之后。在释放操作开始后,对所分配内存的任何使用都会导致未定义的行为。
应使用事件和/或流同步操作,保证其他流对所分配内存的任何访问都在释放操作开始之前完成,如下例所示。
cudaMallocAsync(&ptr, size, stream1);
cudaEventRecord(event1, stream1);
//stream2 must wait for the allocation to be ready before accessing
cudaStreamWaitEvent(stream2, event1);
kernel<<<..., stream2>>>(ptr, ...);
cudaEventRecord(event2, stream2);
// stream3 must wait for stream2 to finish accessing the allocation before
// freeing the allocation
cudaStreamWaitEvent(stream3, event2);
cudaFreeAsync(ptr, stream3);
使用cudaMalloc()分配的内存可以使用cudaFreeAsync()释放。如上所述,对该内存的所有访问都必须在释放操作开始之前完成。
cudaMalloc(&ptr, size);
kernel<<<..., stream>>>(ptr, ...);
cudaFreeAsync(ptr, stream);
同样,使用cudaMallocAsync分配的内存可以使用cudaFree()释放。通过cudaFree()API 释放此类分配的内存时,驱动程序假定对所分配内存的所有访问都已完成,并且不再执行进一步的同步。用户可以使用cudaStreamQuery/cudaStreamSynchronize/cudaEventQuery/cudaEventSynchronize/cudaDeviceSynchronize来保证相应的异步工作已完成,且 GPU 不会尝试访问所分配的内存。
cudaMallocAsync(&ptr, size,stream);
kernel<<<..., stream>>>(ptr, ...);
// synchronize is needed to avoid prematurely freeing the memory
cudaStreamSynchronize(stream);
cudaFree(ptr);
4.3.3. 内存池
内存池封装了虚拟地址和物理内存资源,这些资源根据池的属性和特性进行分配和管理。内存池的主要方面是它所管理的内存的类型和位置。
对cudaMallocAsync的所有调用都使用内存池中的资源。如果未指定内存池,cudaMallocAsync将使用所提供流的设备的当前内存池。可以使用cudaDeviceSetMempool设置设备的当前内存池,并使用cudaDeviceGetMempool进行查询。每个设备都有一个默认内存池,如果尚未调用cudaDeviceSetMempool,则该默认内存池处于活动状态。
APIcudaMallocFromPoolAsync和cudaMallocAsync 的 c++ 重载允许用户指定一次分配所使用的池,而无需将其设为当前池。APIcudaDeviceGetDefaultMempool和cudaMemPoolCreate返回内存池的句柄。cudaMemPoolSetAttribute和cudaMemPoolGetAttribute控制内存池的属性。
注意
设备的当前内存池将位于该设备本地。因此,不指定内存池进行分配时,始终会得到位于流所属设备本地的内存分配。
4.3.3.1. 默认/隐式池
可以通过调用cudaDeviceGetDefaultMempool获取设备的默认内存池。从设备的默认内存池分配的内存是位于该设备上的不可迁移的设备内存分配。该设备始终可以访问这些内存分配。可以使用cudaMemPoolSetAccess修改默认内存池的可访问性,并使用cudaMemPoolGetAccess进行查询。由于默认池不需要显式创建,因此它们有时也称为隐式池。设备的默认内存池不支持 IPC。
4.3.3.2. 显式池
cudaMemPoolCreate创建一个显式池。这允许应用程序为其内存分配请求默认/隐式池所提供的特性之外的特性。这些特性包括 IPC 能力、最大池大小、在支持的平台上使所分配的内存驻留在特定 CPU NUMA 节点上等。
// create a pool similar to the implicit pool on device 0
int device = 0;
cudaMemPoolProps poolProps = { };
poolProps.allocType = cudaMemAllocationTypePinned;
poolProps.location.id = device;
poolProps.location.type = cudaMemLocationTypeDevice;
cudaMemPoolCreate(&memPool, &poolProps));
以下代码片段展示了在有效的 CPU NUMA 节点上创建支持 IPC 的内存池的示例。
// create a pool resident on a CPU NUMA node that is capable of IPC sharing (via a file descriptor).
int cpu_numa_id = 0;
cudaMemPoolProps poolProps = { };
poolProps.allocType = cudaMemAllocationTypePinned;
poolProps.location.id = cpu_numa_id;
poolProps.location.type = cudaMemLocationTypeHostNuma;
poolProps.handleType = cudaMemHandleTypePosixFileDescriptor;
cudaMemPoolCreate(&ipcMemPool, &poolProps));
4.3.3.3. 用于多 GPU 支持的设备可访问性
与通过虚拟内存管理 API 控制的内存分配可访问性一样,内存池分配的内存的可访问性不遵循cudaDeviceEnablePeerAccess或cuCtxEnablePeerAccess。对于内存池,APIcudaMemPoolSetAccess用于修改哪些设备可以访问从池中分配的内存。默认情况下,只有所分配内存所在的设备可以访问这些内存。此访问权限无法撤销。要启用其他设备的访问,进行访问的设备必须与内存池所在的设备具有对等访问能力。这可以通过cudaDeviceCanAccessPeer进行验证。如果未检查对等访问能力,设置访问权限的操作可能会失败并返回cudaErrorInvalidDevice。但是,如果尚未从该池进行过任何分配,则即使设备不具备对等访问能力,cudaMemPoolSetAccess调用也可能成功。在这种情况下,下一次从该池进行的分配将失败。
值得注意的是,cudaMemPoolSetAccess会影响从内存池进行的所有分配,而不仅仅是未来的分配。同样,cudaMemPoolGetAccess报告的可访问性适用于从池进行的所有分配,而不仅仅是未来的分配。不建议频繁更改池针对给定 GPU 的可访问性设置。也就是说,一旦某个池可由给定的 GPU 访问,在该池的整个生命周期内,该 GPU 都应能够访问它。
// snippet showing usage of cudaMemPoolSetAccess:
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;
}
// Make the address accessible
return cudaMemPoolSetAccess(memPool, &accessDesc, 1);
}
4.3.3.4. 为 IPC 启用内存池
可以为内存池启用进程间通信(IPC),以便在进程之间轻松、高效且安全地共享 GPU 内存。CUDA 的 IPC 内存池提供了与 CUDA 的虚拟内存管理 API相同的安全优势。
使用内存池在进程之间共享内存分为两个步骤:进程首先需要共享对内存池的访问权限,然后共享该池中的特定内存分配。第一步建立并强制实施安全机制。第二步协调每个进程使用哪些虚拟地址,以及导入进程中的映射何时需要有效。
4.3.3.4.1. 创建和共享 IPC 内存池
共享对内存池的访问权限涉及以下操作:使用cudaMemPoolExportToShareableHandle()获取内存池的操作系统原生句柄,使用操作系统原生 IPC 机制将句柄传递给导入进程,然后使用cudaMemPoolImportFromShareableHandle()API 创建导入内存池。要使cudaMemPoolExportToShareableHandle成功,必须在创建内存池时,在池属性结构体中指定所请求的句柄类型。
有关在进程之间传递操作系统原生句柄的适当 IPC 机制,请参阅示例。其余过程可在以下代码片段中找到。
// in exporting process
// create an exportable IPC capable pool on device 0
cudaMemPoolProps poolProps = { };
poolProps.allocType = cudaMemAllocationTypePinned;
poolProps.location.id = 0;
poolProps.location.type = cudaMemLocationTypeDevice;
// Setting handleTypes to a non zero value will make the pool exportable (IPC capable)
poolProps.handleTypes = CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR;
cudaMemPoolCreate(&memPool, &poolProps));
// FD based handles are integer types
int fdHandle = 0;
// Retrieve an OS native handle to the pool.
// Note that a pointer to the handle memory is passed in here.
cudaMemPoolExportToShareableHandle(&fdHandle,
memPool,
CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR,
0);
// The handle must be sent to the importing process with the appropriate
// OS-specific APIs.
// in importing process
int fdHandle;
// The handle needs to be retrieved from the exporting process with the
// appropriate OS-specific APIs.
// Create an imported pool from the shareable handle.
// Note that the handle is passed by value here.
cudaMemPoolImportFromShareableHandle(&importedMemPool,
(void*)fdHandle,
CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR,
0);
4.3.3.4.2. 在导入进程中设置访问权限
导入内存池最初只能从其驻留设备访问。导入内存池不会继承导出进程设置的任何访问权限。导入进程需要使用cudaMemPoolSetAccess,为其计划用来访问该内存的每个 GPU 启用访问权限。
如果导入内存池所属的设备对导入进程不可见,用户必须使用cudaMemPoolSetAccessAPI,为将使用这些内存分配的 GPU 启用访问权限。(参见支持多 GPU 的设备访问权限)
4.3.3.4.3. 从导出的池中创建和共享内存分配
池共享后,导出进程中使用cudaMallocAsync()从该池分配的内存就可以与已导入该池的进程共享。由于池的安全策略是在池级别建立和验证的,因此操作系统无需额外的簿记操作来为特定的池内存分配提供安全保障。换言之,导入池内存分配所需的不透明cudaMemPoolPtrExportData可以通过任何机制发送给导入进程。
虽然可以在不以任何方式与分配流同步的情况下导出和导入内存分配,但导入进程在访问该内存分配时必须遵循与导出进程相同的规则。具体而言,对内存分配的访问必须发生在分配流中的分配操作执行之后。下面的两个代码片段展示了cudaMemPoolExportPointer()和cudaMemPoolImportPointer()如何共享内存分配,其中使用 IPC 事件来保证导入进程不会在内存分配准备就绪之前访问它。
// preparing an allocation in the exporting process
cudaMemPoolPtrExportData exportData;
cudaEvent_t readyIpcEvent;
cudaIpcEventHandle_t readyIpcEventHandle;
// ipc event for coordinating between processes
// cudaEventInterprocess flag makes the event an ipc event
// cudaEventDisableTiming is set for performance reasons
cudaEventCreate(&readyIpcEvent, cudaEventDisableTiming | cudaEventInterprocess)
// allocate from the exporting mem pool
cudaMallocAsync(&ptr, size,exportMemPool, stream);
// event for sharing when the allocation is ready.
cudaEventRecord(readyIpcEvent, stream);
cudaMemPoolExportPointer(&exportData, ptr);
cudaIpcGetEventHandle(&readyIpcEventHandle, readyIpcEvent);
// Share IPC event and pointer export data with the importing process using
// any mechanism. Here we copy the data into shared memory
shmem->ptrData = exportData;
shmem->readyIpcEventHandle = readyIpcEventHandle;
// signal consumers data is ready
// Importing an allocation
cudaMemPoolPtrExportData *importData = &shmem->prtData;
cudaEvent_t readyIpcEvent;
cudaIpcEventHandle_t *readyIpcEventHandle = &shmem->readyIpcEventHandle;
// Need to retrieve the ipc event handle and the export data from the
// exporting process using any mechanism. Here we are using shmem and just
// need synchronization to make sure the shared memory is filled in.
cudaIpcOpenEventHandle(&readyIpcEvent, readyIpcEventHandle);
// import the allocation. The operation does not block on the allocation being ready.
cudaMemPoolImportPointer(&ptr, importedMemPool, importData);
// Wait for the prior stream operations in the allocating stream to complete before
// using the allocation in the importing process.
cudaStreamWaitEvent(stream, readyIpcEvent);
kernel<<<..., stream>>>(ptr, ...);
释放内存分配时,必须先在导入进程中释放该内存分配,然后才能在导出进程中释放。以下代码片段演示了如何使用 CUDA IPC 事件,在两个进程的cudaFreeAsync操作之间提供所需的同步。显然,从导入进程对内存分配的访问受到导入进程一侧的释放操作的限制。值得注意的是,可以使用cudaFree在两个进程中释放该内存分配,也可以使用其他流同步 API 来代替 CUDA IPC 事件。
// The free must happen in importing process before the exporting process
kernel<<<..., stream>>>(ptr, ...);
// Last access in importing process
cudaFreeAsync(ptr, stream);
// Access not allowed in the importing process after the free
cudaIpcEventRecord(finishedIpcEvent, stream);
// Exporting process
// The exporting process needs to coordinate its free with the stream order
// of the importing process’s free.
cudaStreamWaitEvent(stream, finishedIpcEvent);
kernel<<<..., stream>>>(ptrInExportingProcess, ...);
// The free in the importing process doesn’t stop the exporting process
// from using the allocation.
cudFreeAsync(ptrInExportingProcess,stream);
4.3.3.4.4. IPC 导出池的限制
IPC 池目前不支持将物理块释放回操作系统。因此,cudaMemPoolTrimToAPI 不起作用,cudaMemPoolAttrReleaseThreshold实际上也会被忽略。此行为由驱动程序而非运行时控制,并且可能会在未来的驱动程序更新中改变。
4.3.3.4.5. IPC 导入池的限制
不允许从导入池分配内存;具体而言,不能将导入池设为当前池,也不能在cudaMallocFromPoolAsyncAPI 中使用导入池。因此,分配重用策略属性对这些池没有意义。
IPC 导入池与 IPC 导出池一样,目前不支持将物理块释放回操作系统。
资源使用统计属性查询仅反映导入到该进程中的内存分配及其关联的物理内存。
4.3.4. 最佳实践与调优
4.3.4.1. 查询支持情况
应用程序可以通过使用设备属性cudaDevAttrMemoryPoolsSupported调用cudaDeviceGetAttribute()(参见开发者博客),来确定设备是否支持流有序内存分配器。
可以使用cudaDevAttrMemoryPoolSupportedHandleTypes设备属性查询 IPC 内存池的支持情况。此属性是在 CUDA 11.3 中添加的,旧版驱动程序在查询此属性时将返回cudaErrorInvalidValue。
int driverVersion = 0;
int deviceSupportsMemoryPools = 0;
int poolSupportedHandleTypes = 0;
cudaDriverGetVersion(&driverVersion);
if (driverVersion >= 11020) {
cudaDeviceGetAttribute(&deviceSupportsMemoryPools,
cudaDevAttrMemoryPoolsSupported, device);
}
if (deviceSupportsMemoryPools != 0) {
// `device` supports the Stream-Ordered Memory Allocator
}
if (driverVersion >= 11030) {
cudaDeviceGetAttribute(&poolSupportedHandleTypes,
cudaDevAttrMemoryPoolSupportedHandleTypes, device);
}
if (poolSupportedHandleTypes & cudaMemHandleTypePosixFileDescriptor) {
// Pools on the specified device can be created with posix file descriptor-based IPC
}
在查询之前执行驱动程序版本检查,可以避免在尚未定义该属性的驱动程序上遇到cudaErrorInvalidValue错误。也可以使用cudaGetLastError来清除此错误,而不是避免它。
4.3.4.2. 物理页缓存行为
默认情况下,分配器会尝试尽可能减少池所拥有的物理内存。为了尽可能减少分配和释放物理内存的操作系统调用,应用程序必须为每个池配置内存占用量。应用程序可以使用释放阈值属性(cudaMemPoolAttrReleaseThreshold)来完成此操作。
释放阈值是池在尝试将内存释放回操作系统之前应保留的内存量,以字节为单位。当内存池持有的内存字节数超过释放阈值时,分配器将在下一次调用流、事件或设备同步时,尝试将内存释放回操作系统。将释放阈值设置为 UINT64_MAX 将阻止驱动程序在每次同步后尝试缩小池。
Cuuint64_t setVal = UINT64_MAX;
cudaMemPoolSetAttribute(memPool, cudaMemPoolAttrReleaseThreshold, &setVal);
将cudaMemPoolAttrReleaseThreshold设置得足够高以有效禁用内存池缩小的应用程序,可能希望显式缩减内存池的内存占用量。cudaMemPoolTrimTo允许应用程序这样做。在削减内存池的占用量时,minBytesToKeep参数允许应用程序保留指定数量的内存,例如它预期在后续执行阶段所需的内存量。
Cuuint64_t setVal = UINT64_MAX;
cudaMemPoolSetAttribute(memPool, cudaMemPoolAttrReleaseThreshold, &setVal);
// application phase needing a lot of memory from the stream-ordered allocator
for (i=0; i<10; i++) {
for (j=0; j<10; j++) {
cudaMallocAsync(&ptrs[j],size[j], stream);
}
kernel<<<...,stream>>>(ptrs,...);
for (j=0; j<10; j++) {
cudaFreeAsync(ptrs[j], stream);
}
}
// Process does not need as much memory for the next phase.
// Synchronize so that the trim operation will know that the allocations are no
// longer in use.
cudaStreamSynchronize(stream);
cudaMemPoolTrimTo(mempool, 0);
// Some other process/allocation mechanism can now use the physical memory
// released by the trimming operation.
4.3.4.3. 资源使用统计
查询池的cudaMemPoolAttrReservedMemCurrent属性会报告该池当前消耗的物理 GPU 内存总量。查询池的cudaMemPoolAttrUsedMemCurrent会返回从该池分配且不可用于重用的所有内存的总大小。
cudaMemPoolAttr*MemHigh属性是水位标记,记录自上次重置以来相应cudaMemPoolAttr*MemCurrent属性达到的最大值。可以使用cudaMemPoolSetAttributeAPI 将其重置为当前值。
// sample helper functions for getting the usage statistics in bulk
struct usageStatistics {
cuuint64_t reserved;
cuuint64_t reservedHigh;
cuuint64_t used;
cuuint64_t usedHigh;
};
void getUsageStatistics(cudaMemoryPool_t memPool, struct usageStatistics *statistics)
{
cudaMemPoolGetAttribute(memPool, cudaMemPoolAttrReservedMemCurrent, statistics->reserved);
cudaMemPoolGetAttribute(memPool, cudaMemPoolAttrReservedMemHigh, statistics->reservedHigh);
cudaMemPoolGetAttribute(memPool, cudaMemPoolAttrUsedMemCurrent, statistics->used);
cudaMemPoolGetAttribute(memPool, cudaMemPoolAttrUsedMemHigh, statistics->usedHigh);
}
// resetting the watermarks will make them take on the current value.
void resetStatistics(cudaMemoryPool_t memPool)
{
cuuint64_t value = 0;
cudaMemPoolSetAttribute(memPool, cudaMemPoolAttrReservedMemHigh, &value);
cudaMemPoolSetAttribute(memPool, cudaMemPoolAttrUsedMemHigh, &value);
}
4.3.4.4. 内存重用策略
为了满足分配请求,驱动程序在尝试从操作系统分配更多内存之前,会先尝试重用此前通过cudaFreeAsync()释放的内存。例如,在某个流中释放的内存可以立即用于该流中的后续分配请求。当流与 CPU 同步时,此前在该流中释放的内存就可供任意流中的分配重用。重用策略既可应用于默认内存池,也可应用于显式内存池。
流有序分配器具有几种可控制的分配策略。池属性cudaMemPoolReuseFollowEventDependencies、cudaMemPoolReuseAllowOpportunistic和cudaMemPoolReuseAllowInternalDependencies控制这些策略,具体说明见下文。可以通过调用cudaMemPoolSetAttribute来启用或禁用这些策略。升级到更新的 CUDA 驱动程序可能会更改、增强、扩充和/或重新排序所枚举的重用策略。
4.3.4.4.1. cudaMemPoolReuseFollowEventDependencies
在分配更多物理 GPU 内存之前,分配器会检查由 CUDA 事件建立的依赖关系信息,并尝试从另一个流中释放的内存进行分配。
cudaMallocAsync(&ptr, size, originalStream);
kernel<<<..., originalStream>>>(ptr, ...);
cudaFreeAsync(ptr, originalStream);
cudaEventRecord(event,originalStream);
// waiting on the event that captures the free in another stream
// allows the allocator to reuse the memory to satisfy
// a new allocation request in the other stream when
// cudaMemPoolReuseFollowEventDependencies is enabled.
cudaStreamWaitEvent(otherStream, event);
cudaMallocAsync(&ptr2, size, otherStream);
4.3.4.4.2. cudaMemPoolReuseAllowOpportunistic
启用cudaMemPoolReuseAllowOpportunistic策略时,分配器会检查已释放的内存分配,以确定释放操作的流顺序语义是否已得到满足,例如流是否已越过释放操作所指示的执行点。禁用此策略时,分配器仍会重用流与 CPU 同步时变为可用的内存。禁用此策略不会阻止cudaMemPoolReuseFollowEventDependencies的应用。
cudaMallocAsync(&ptr, size, originalStream);
kernel<<<..., originalStream>>>(ptr, ...);
cudaFreeAsync(ptr, originalStream);
// after some time, the kernel finishes running
wait(10);
// When cudaMemPoolReuseAllowOpportunistic is enabled this allocation request
// can be fulfilled with the prior allocation based on the progress of originalStream.
cudaMallocAsync(&ptr2, size, otherStream);
4.3.4.4.3. cudaMemPoolReuseAllowInternalDependencies
如果无法从操作系统分配并映射更多物理内存,驱动程序将查找其可用性取决于另一个流尚待执行的进度的内存。如果找到此类内存,驱动程序会将所需的依赖关系插入分配流并重用该内存。
cudaMallocAsync(&ptr, size, originalStream);
kernel<<<..., originalStream>>>(ptr, ...);
cudaFreeAsync(ptr, originalStream);
// When cudaMemPoolReuseAllowInternalDependencies is enabled
// and the driver fails to allocate more physical memory, the driver may
// effectively perform a cudaStreamWaitEvent in the allocating stream
// to make sure that future work in ‘otherStream’ happens after the work
// in the original stream that would be allowed to access the original allocation.
cudaMallocAsync(&ptr2, size, otherStream);
4.3.4.4.4. 禁用重用策略
虽然可控制的重用策略可以改善内存重用,但用户可能希望禁用它们。允许机会性重用(例如cudaMemPoolReuseAllowOpportunistic)会使分配模式基于 CPU 和 GPU 执行的交错情况,在各次运行之间产生差异。当用户更愿意在分配失败时显式同步事件或流时,内部依赖关系插入(例如cudaMemPoolReuseAllowInternalDependencies)可能会以出乎意料且可能不确定的方式使工作串行化。
4.3.4.5. 同步 API 的操作
分配器作为 CUDA 驱动程序的一部分所带来的一项优化是与同步 API 的集成。当用户请求 CUDA 驱动程序执行同步时,驱动程序会等待异步工作完成。在返回之前,驱动程序将确定哪些释放操作已由此次同步保证完成。无论指定了哪个流或禁用了哪些分配策略,这些内存分配都会变为可供分配的内存。驱动程序还会在此处检查cudaMemPoolAttrReleaseThreshold,并尽可能释放所有多余的物理内存。
4.3.5. 补充说明
4.3.5.1. cudaMemcpyAsync 对当前上下文/设备的敏感性
在当前的 CUDA 驱动程序中,任何涉及来自cudaMallocAsync的内存的异步memcpy操作,都应使用指定流的上下文作为调用线程的当前上下文来执行。对于cudaMemcpyPeerAsync,则不必如此,因为它引用的是 API 中指定的设备主上下文,而不是当前上下文。
4.3.5.2. cudaPointerGetAttributes 查询
对内存分配调用cudaFreeAsync之后,再对它调用cudaPointerGetAttributes会导致未定义行为。具体而言,即使仍然可以从给定流访问该内存分配,也不影响这一点:其行为仍然是未定义的。
4.3.5.3. cudaGraphAddMemsetNode
cudaGraphAddMemsetNode不适用于通过流有序分配器分配的内存。不过,可以通过流捕获来捕获对这些内存分配执行的 memset 操作。
4.3.5.4. 指针属性
cudaPointerGetAttributes查询适用于流有序内存分配。由于流有序内存分配不与上下文关联,查询CU_POINTER_ATTRIBUTE_CONTEXT会成功,但会在*data中返回 NULL。属性CU_POINTER_ATTRIBUTE_DEVICE_ORDINAL可用于确定内存分配的位置:在选择上下文以使用cudaMemcpyPeerAsync进行 p2h2p 复制时,这可能很有用。属性CU_POINTER_ATTRIBUTE_MEMPOOL_HANDLE是在 CUDA 11.3 中添加的,可用于调试,也可用于在执行 IPC 之前确认内存分配来自哪个池。
4.3.5.5. CPU 虚拟内存
使用 CUDA 流有序内存分配器 API 时,请避免通过“ulimit -v”设置 VRAM 限制,因为这种做法不受支持。