CuPy 基础#

在本节中,您将了解以下内容:

  • 关于 cupy.ndarray 的基础知识

  • 当前设备 的概念

  • 主机-设备和设备-设备数组传输

cupy.ndarray 的基础#

CuPy 是一个实现 NumPy 接口子集的 GPU 数组后端。在以下代码中,cpcupy 的缩写,遵循将 numpy 缩写为 np 的标准约定:

>>> import numpy as np
>>> import cupy as cp

The cupy.ndarray class is at the core of CuPy and is a replacement class for NumPy’s numpy.ndarray.

>>> x_gpu = cp.array([1, 2, 3])

上面的 x_gpucupy.ndarray 的一个实例。正如我们所见,CuPy 的语法在这里与 NumPy 完全相同。cupy.ndarraynumpy.ndarray 之间的主要区别在于,CuPy 数组分配在 当前设备 上,我们将在后面讨论这一点。

大多数数组操作也以类似于 NumPy 的方式完成。以欧几里得范数(又称 L2 范数)为例。NumPy 有 numpy.linalg.norm() 函数,该函数在 CPU 上计算它。

>>> x_cpu = np.array([1, 2, 3])
>>> l2_cpu = np.linalg.norm(x_cpu)

使用 CuPy,我们可以在 GPU 上以类似的方式执行相同的计算:

>>> x_gpu = cp.array([1, 2, 3])
>>> l2_gpu = cp.linalg.norm(x_gpu)

CuPy 在 cupy.ndarray 对象上实现了许多函数。请参阅 参考 以了解支持的 NumPy API 子集。了解 NumPy 将帮助您利用大多数 CuPy 功能。因此,我们建议您熟悉 NumPy 文档

当前设备#

CuPy 有一个 当前设备 的概念,这是默认的 GPU 设备,数组的分配、操作、计算等都在该设备上进行。假设当前设备的 ID 是 0。在这种情况下,以下代码将在 GPU 0 上创建一个数组 x_on_gpu0

>>> x_on_gpu0 = cp.array([1, 2, 3, 4, 5])

要切换到另一个GPU设备,请使用 Device 上下文管理器:

>>> with cp.cuda.Device(1):
...    x_on_gpu1 = cp.array([1, 2, 3, 4, 5])
>>> x_on_gpu0 = cp.array([1, 2, 3, 4, 5])

所有 CuPy 操作(除了多GPU功能和设备间复制)都在当前活动的设备上执行。

通常情况下,CuPy 函数期望数组位于与当前设备相同的设备上。传递存储在非当前设备上的数组可能会根据硬件配置工作,但通常不鼓励这样做,因为它可能不会表现良好。

备注

如果数组的设备与当前设备不匹配,CuPy 函数会尝试在它们之间建立 点对点内存访问 (P2P),以便当前设备可以直接从另一个设备读取数组。请注意,只有当拓扑结构允许时,P2P 才可用。如果 P2P 不可用,这种尝试将失败并抛出 ValueError

cupy.ndarray.device 属性表示数组分配在哪个设备上。

>>> with cp.cuda.Device(1):
...    x = cp.array([1, 2, 3, 4, 5])
>>> x.device
<CUDA Device 1>

备注

当只有一个设备可用时,不需要显式地切换设备。

当前流#

与当前设备概念相关的是 当前流,它有助于避免在每次操作中显式传递流,从而保持 API 的 Pythonic 和用户友好性。在 CuPy 中,所有 CUDA 操作(如数据传输(参见 数据传输基础 部分)和内核启动)都会被排入当前流,并且同一流上的排队任务将按顺序执行(但与主机 异步)。

CuPy 中的默认当前流是 CUDA 的空流(即流 0)。它也被称为 遗留 默认流,每个设备都是唯一的。然而,可以使用 cupy.cuda.Stream API 更改当前流,请参阅 访问 CUDA 功能 以获取示例。可以使用 cupy.cuda.get_current_stream() 获取 CuPy 中的当前流。

值得注意的是,CuPy 的当前流是基于 每个线程、每个设备 管理的,这意味着在不同的 Python 线程或不同的设备上,当前流(如果不是空流)可以是不同的。

数据传输#

将数组移动到设备#

cupy.asarray() 可以用于将 numpy.ndarray、列表或任何可以传递给 numpy.array() 的对象移动到当前设备:

>>> x_cpu = np.array([1, 2, 3])
>>> x_gpu = cp.asarray(x_cpu)  # move the data to the current device.

cupy.asarray() 可以接受 cupy.ndarray,这意味着我们可以使用这个函数在设备之间传输数组。

>>> with cp.cuda.Device(0):
...     x_gpu_0 = cp.ndarray([1, 2, 3])  # create an array in GPU 0
>>> with cp.cuda.Device(1):
...     x_gpu_1 = cp.asarray(x_gpu_0)  # move the array to GPU 1

备注

cupy.asarray() 如果可能的话不会复制输入数组。因此,如果你放入当前设备的数组,它会返回输入对象本身。

如果在这种情况下我们复制数组,可以使用带有 copy=Truecupy.array()。实际上 cupy.asarray() 等同于 cupy.array(arr, dtype, copy=False)

将数组从设备移动到主机#

将设备数组移动到主机可以通过 cupy.asnumpy() 如下操作:

>>> x_gpu = cp.array([1, 2, 3])  # create an array in the current device
>>> x_cpu = cp.asnumpy(x_gpu)  # move the array to the host.

我们也可以使用 cupy.ndarray.get()

>>> x_cpu = x_gpu.get()

内存管理#

有关使用内存池在CuPy中如何管理内存的详细描述,请查看 内存管理

如何编写CPU/GPU不可知代码#

CuPy 与 NumPy 的兼容性使其能够编写 CPU/GPU 不可知代码。为此,CuPy 实现了 cupy.get_array_module() 函数,如果其任何参数位于 GPU 上,则返回对 cupy 的引用,否则返回对 numpy 的引用。以下是一个计算 log1p 的 CPU/GPU 不可知函数的示例:

>>> # Stable implementation of log(1 + exp(x))
>>> def softplus(x):
...     xp = cp.get_array_module(x)  # 'xp' is a standard usage in the community
...     print("Using:", xp.__name__)
...     return xp.maximum(0, x) + xp.log1p(xp.exp(-abs(x)))

当你需要操作 CPU 和 GPU 数组时,可能需要显式数据传输将它们移动到同一位置——无论是 CPU 还是 GPU。为此,CuPy 实现了两个姐妹方法,分别称为 cupy.asnumpy()cupy.asarray()。以下是一个演示这两种方法使用的示例:

>>> x_cpu = np.array([1, 2, 3])
>>> y_cpu = np.array([4, 5, 6])
>>> x_cpu + y_cpu
array([5, 7, 9])
>>> x_gpu = cp.asarray(x_cpu)
>>> x_gpu + y_cpu
Traceback (most recent call last):
...
TypeError: Unsupported type <class 'numpy.ndarray'>
>>> cp.asnumpy(x_gpu) + y_cpu
array([5, 7, 9])
>>> cp.asnumpy(x_gpu) + cp.asnumpy(y_cpu)
array([5, 7, 9])
>>> x_gpu + cp.asarray(y_cpu)
array([5, 7, 9])
>>> cp.asarray(x_gpu) + cp.asarray(y_cpu)
array([5, 7, 9])

方法 cupy.asnumpy() 返回一个 NumPy 数组(主机上的数组),而方法 cupy.asarray() 返回一个 CuPy 数组(当前设备上的数组)。这两种方法都可以接受任意输入,这意味着它们可以应用于位于主机或设备上的任何数据,并且可以转换为数组。