CuPy基础#

在本节中,您将学习以下内容

  • cupy.ndarray基础

  • 当前设备的概念

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

cupy.ndarray基础#

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

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

cupy.ndarray 类是 CuPy 的核心,是 NumPynumpy.ndarray 的替代类。

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

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

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

>>> 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 中的当前流可以使用 cupy.cuda.get_current_stream() 检索。

值得注意的是,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() 如果可能,不会复制输入数组。因此,如果您将当前设备上的数组放入,它将返回输入对象本身。

如果在这种情况需要复制数组,您可以使用 cupy.array() 并设置 copy=True。实际上,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 数组(当前设备上的数组)。这两种方法都可以接受任意输入,这意味着它们可以应用于位于主机或设备上的任何数据,并且可以转换为数组。