CuPy基础#
在本节中,您将学习以下内容
当前设备的概念
主机-设备和设备-设备数组传输
cupy.ndarray基础#
CuPy 是一个 GPU 数组后端,实现了 NumPy 接口的子集。在以下代码中,cp
是 cupy
的缩写,遵循将 numpy
缩写为 np
的标准惯例。
>>> import numpy as np
>>> import cupy as cp
cupy.ndarray
类是 CuPy
的核心,是 NumPy
的 numpy.ndarray
的替代类。
>>> x_gpu = cp.array([1, 2, 3])
上面的 x_gpu
是 cupy.ndarray
的一个实例。正如所见,CuPy 的语法与 NumPy 完全相同。 cupy.ndarray
和 numpy.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 数组(当前设备上的数组)。这两种方法都可以接受任意输入,这意味着它们可以应用于位于主机或设备上的任何数据,并且可以转换为数组。