内存管理#

CuPy 默认使用内存池进行内存分配。内存池通过减轻内存分配和 CPU/GPU 同步的开销,显著提高了性能。

CuPy 中有两种不同的内存池

  • 设备内存池(GPU 设备内存),用于 GPU 内存分配。

  • 固定内存池(不可交换的 CPU 内存),用于 CPU 到 GPU 的数据传输。

注意

当您监控内存使用情况时(例如,使用 nvidia-smi 查看 GPU 内存或使用 ps 查看 CPU 内存),您可能会注意到即使数组实例超出作用域后,内存仍未释放。这是预期行为,因为默认的内存池会“缓存”已分配的内存块。

有关内存管理 API 的详细信息,请参阅低级 CUDA 支持

为了更方便地使用固定内存,我们还在 cupyx 命名空间中提供了一些高级 API,包括 cupyx.empty_pinned()cupyx.empty_like_pinned()cupyx.zeros_pinned()cupyx.zeros_like_pinned()。它们返回由固定内存支持的 NumPy 数组。如果使用了 CuPy 的固定内存池,则固定内存会从内存池中分配。

注意

CuPy v8 及更高版本提供了一个FFT 计划缓存,如果在使用了 FFT 及相关函数,它可能会占用部分设备内存。占用的内存可以通过缩小或禁用缓存来释放。

内存池操作#

内存池实例提供了内存分配的统计信息。要访问默认内存池实例,请使用 cupy.get_default_memory_pool()cupy.get_default_pinned_memory_pool()。您还可以释放内存池中持有的所有未使用内存块。详细信息请参阅下面的示例代码

import cupy
import numpy

mempool = cupy.get_default_memory_pool()
pinned_mempool = cupy.get_default_pinned_memory_pool()

# Create an array on CPU.
# NumPy allocates 400 bytes in CPU (not managed by CuPy memory pool).
a_cpu = numpy.ndarray(100, dtype=numpy.float32)
print(a_cpu.nbytes)                      # 400

# You can access statistics of these memory pools.
print(mempool.used_bytes())              # 0
print(mempool.total_bytes())             # 0
print(pinned_mempool.n_free_blocks())    # 0

# Transfer the array from CPU to GPU.
# This allocates 400 bytes from the device memory pool, and another 400
# bytes from the pinned memory pool.  The allocated pinned memory will be
# released just after the transfer is complete.  Note that the actual
# allocation size may be rounded to larger value than the requested size
# for performance.
a = cupy.array(a_cpu)
print(a.nbytes)                          # 400
print(mempool.used_bytes())              # 512
print(mempool.total_bytes())             # 512
print(pinned_mempool.n_free_blocks())    # 1

# When the array goes out of scope, the allocated device memory is released
# and kept in the pool for future reuse.
a = None  # (or `del a`)
print(mempool.used_bytes())              # 0
print(mempool.total_bytes())             # 512
print(pinned_mempool.n_free_blocks())    # 1

# You can clear the memory pool by calling `free_all_blocks`.
mempool.free_all_blocks()
pinned_mempool.free_all_blocks()
print(mempool.used_bytes())              # 0
print(mempool.total_bytes())             # 0
print(pinned_mempool.n_free_blocks())    # 0

详细信息请参阅 cupy.cuda.MemoryPoolcupy.cuda.PinnedMemoryPool

限制 GPU 内存使用#

您可以通过使用 CUPY_GPU_MEMORY_LIMIT 环境变量来硬性限制可分配的 GPU 内存量(详情请参阅环境变量)。

# Set the hard-limit to 1 GiB:
#   $ export CUPY_GPU_MEMORY_LIMIT="1073741824"

# You can also specify the limit in fraction of the total amount of memory
# on the GPU. If you have a GPU with 2 GiB memory, the following is
# equivalent to the above configuration.
#   $ export CUPY_GPU_MEMORY_LIMIT="50%"

import cupy
print(cupy.get_default_memory_pool().get_limit())  # 1073741824

您还可以使用 cupy.cuda.MemoryPool.set_limit() 来设置限制(或覆盖通过环境变量指定的值)。通过这种方式,您可以为每个 GPU 设备设置不同的限制。

import cupy

mempool = cupy.get_default_memory_pool()

with cupy.cuda.Device(0):
    mempool.set_limit(size=1024**3)  # 1 GiB

with cupy.cuda.Device(1):
    mempool.set_limit(size=2*1024**3)  # 2 GiB

注意

CUDA 会在内存池之外分配一些 GPU 内存(例如 CUDA 上下文、库句柄等)。根据使用情况,这些内存可能占用一到几百 MiB。这部分内存将不计入限制。

更改内存池#

您可以通过将内存分配函数传递给 cupy.cuda.set_allocator() / cupy.cuda.set_pinned_memory_allocator(),来使用您自己的内存分配器代替默认内存池。内存分配函数应接受 1 个参数(请求的大小,以字节为单位)并返回 cupy.cuda.MemoryPointer / cupy.cuda.PinnedMemoryPointer

CuPy 提供了两个这样的分配器,用于在 GPU 上使用托管内存(managed memory)和流有序内存(stream ordered memory),详情请分别参阅 cupy.cuda.malloc_managed()cupy.cuda.malloc_async()。要启用由托管内存支持的内存池,您可以构建一个新的 MemoryPool 实例,并将其分配器设置为 malloc_managed(),如下所示

import cupy

# Use managed memory
cupy.cuda.set_allocator(cupy.cuda.MemoryPool(cupy.cuda.malloc_managed).malloc)

请注意,如果您直接将 malloc_managed() 传递给 set_allocator() 而不构建 MemoryPool 实例,则内存释放时会立即返回给系统,这可能不是您期望的行为。

流有序内存分配器(Stream Ordered Memory Allocator)是 CUDA 11.2 之后添加的新功能。CuPy 提供了一个实验性的接口。与 CuPy 的内存池类似,流有序内存分配器也会以流有序的方式从内存池中异步地分配/释放内存。主要区别在于它是 NVIDIA 在 CUDA 驱动中实现的内置功能,因此同一进程中的其他 CUDA 应用程序可以轻松地从同一内存池分配内存。

要启用管理流有序内存的内存池,您可以构建一个新的 MemoryAsyncPool 实例

import cupy

# Use asynchronous stream ordered memory
cupy.cuda.set_allocator(cupy.cuda.MemoryAsyncPool().malloc)

# Create a custom stream
s = cupy.cuda.Stream()

# This would allocate memory asynchronously on stream s
with s:
    a = cupy.empty((100,), dtype=cupy.float64)

请注意,在这种情况下,我们不使用 MemoryPool 类。MemoryAsyncPool 接受与 MemoryPool 不同的输入参数,用于指示使用哪个内存池。更多详细信息请参阅 MemoryAsyncPool 的文档。

请注意,如果您直接将 malloc_async() 传递给 set_allocator() 而不构建 MemoryAsyncPool 实例,则将使用设备的当前内存池。

使用流有序内存时,重要的是您自己维护正确的流语义,例如使用 StreamEvent API(详情请参阅 流和事件);CuPy 不会试图为您智能地处理。在释放时,内存会异步释放,优先在分配该内存的流上释放(第一次尝试),或在任何当前的 CuPy 流上释放(第二次尝试)。允许在分配内存的流上的所有内存释放之前销毁该流。

此外,内部使用 cudaMalloc(CUDA 默认的同步分配器)的应用程序/库可能与流有序内存分配器产生意想不到的交互。具体来说,释放到内存池的内存可能无法立即被 cudaMalloc 看到,从而可能导致内存不足错误。在这种情况下,您可以调用 free_all_blocks(),或者手动执行(事件/流/设备)同步,然后重试。

目前,MemoryAsyncPool 接口是实验性的。特别是,虽然其 API 与 MemoryPool 基本相同,但由于 CUDA 的限制,该内存池的几个方法需要足够新的驱动(当然,还需要支持的硬件、CUDA 版本和平台)。

您甚至可以使用以下代码禁用默认内存池。请确保在进行任何其他 CuPy 操作之前执行此操作。

import cupy

# Disable memory pool for device memory (GPU)
cupy.cuda.set_allocator(None)

# Disable memory pool for pinned memory (CPU).
cupy.cuda.set_pinned_memory_allocator(None)