CuPy
lastmod 2024-08-13created 2024-07-1142 edits
CuPy is a NumPy/SciPy-compatible array library for GPU-accelerated computing with Python.
CuPy acts as a drop-in replacement to run existing NumPy/SciPy code on NVIDIA CUDA or AMD ROCm platforms.
Categories
- CuPy:IPC - Includes a sample of doing IPC with cupy.
- CuPy:Stream
- CuPy:Event
- CuPy:Kernels - How to write user-defined kernels in cupy.
Install
Requirements
NVIDIA CUDA GPU with the Compute Capability 3.0 or larger.
- CUDA Toolkit: v11.2 / v11.3 / v11.4 / v11.5 / v11.6 / v11.7 / v11.8 / v12.0 / v12.1 / v12.2 / v12.3 / v12.4
- If you have multiple versions of CUDA Toolkit installed, CuPy will automatically choose one of the CUDA installations. See Working with Custom CUDA Installation for details.
- This requirement is optional if you install CuPy from conda-forge. However, you still need to have a compatible driver installed for your GPU. See Installing CuPy from Conda-Forge for details.
| NOTE |
| Currently, CuPy is tested against Ubuntu 20.04 LTS / 22.04 LTS (x86_64), CentOS 7 / 8 (x86_64) and Windows Server 2016 (x86_64). |
Python Dependencies
NumPy/SciPy-compatible API in CuPy v13 is based on NumPy 1.26 and SciPy 1.11, and has been tested against the following versions:
- NumPy: v1.22 / v1.23 / v1.24 / v1.25 / v1.26 / v2.0
- SciPy (optional): v1.7 / v1.8 / v1.9 / v1.10 / v1.11
- Required only when coping sparse matrices from GPU to CPU (see Sparse matrices (cupyx.scipy.sparse).)
- Required only when using Automatic Kernel Parameters Optimizations (cupyx.optimizing).
| NOTE |
| SciPy and Optuna are optional dependencies and will not be installed automatically. |
| NOTE |
| Before installing CuPy, we recommend you to upgrade setuptools and pip: |
Additional CUDA Libraries
Part of the CUDA features in CuPy will be activated only when the corresponding libraries are installed.
- cuTENSOR: v2.0
- The library to accelerate tensor operations. See Environment variables for the details.
- The library to perform collective multi-GPU / multi-node computations.
- The library to accelerate deep neural network computations.
- The library to accelerate sparse matrix-matrix multiplication.
Installing CuPy from PyPI
Wheels (precompiled binary packages) are available for Linux and Windows. Package names are different depending on your CUDA Toolkit version.
| CUDA | Command |
| v11.2 ~ 11.8 (x86_64 / aarch64) | |
| v12.x (x86_64 / aarch64) | |
To enable features provided by additional CUDA libraries (cuTENSOR / NCCL / cuDNN), you need to install them manually. If you installed CuPy via wheels, you can use the installer command below to setup these libraries in case you don’t have a previous installation:
| NOTE |
| Append |
Building CuPy for ROCm From Source
To build CuPy from source, set the CUPY_INSTALL_USE_HIP, ROCM_HOME, and HCC_AMDGPU_TARGET environment variables. (HCC_AMDGPU_TARGET is the ISA name supported by your GPU. Run rocminfo and use the value displayed in Name: line (e.g., gfx900). You can specify a comma-separated list of ISAs if you have multiple GPUs of different architectures.)
$ export CUPY_INSTALL_USE_HIP=1
$ export ROCM_HOME=/opt/rocm
$ export HCC_AMDGPU_TARGET=gfx906
$ pip install cupy
| NOTE |
| If you don’t specify the |
Device Pointer
import cupy as cp
# GPU 배열 생성
array = cp.arange(1000000).reshape(1000, 1000)
# Device pointer 얻기
dev_ptr = array.data.ptr
# Device pointer 출력
print(f'Device pointer: {dev_ptr}')
Checking Device Overlap
How to check the Device Overlap property to see whether CUDA:Stream is supported:
Asynchronous operations
- class cupy.cuda.MemoryAsync(size_t size, stream)
- Asynchronous memory allocation on a CUDA device.
- This class provides an RAII interface of the CUDA memory allocation.
- Parameters:
-
size(int) – Size of the memory allocation in bytes. -
stream(Stream) – The stream on which the memory is allocated and freed.
-
In any API that has a blocking parameter, that parameter makes it use an asynchronous function.
- cupy.ndarray
- cupy.array
- cupy.asarray
- cupy.asnumpy
* blocking (bool) - False로 설정하면 복사본이 지정된 (제공된 경우) 또는 현재 스트림에서 비동기적으로 실행되며 사용자는 스트림 순서를 확인할 책임이 있습니다.
cudaGetDeviceCount
Gets the number of CUDA devices.
Selecting a Device
If the array's device and the current device do not match, CuPy functions try to set up Peer-to-Peer Memory Access (P2P) between the two devices so that the current device can read the array directly from the other device. P2P is available only when the topology permits it. If P2P is unavailable, such an attempt fails with a ValueError exception.
The N-dimensional array (ndarray)
cupy.ndarray
cupy.array
cupy.asarray
cupy.asnumpy
cupy.get_array_module
cupyx.scipy.get_array_module
Data transfer
Moving arrays to a device
cupy.asarray() can be used to move a numpy.ndarray, a list, or any object that can be passed to numpy.array() to the current device:
cupy.asarray() can accept cupy.ndarray, which means we can transfer the array between devices with this function.
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
| Note |
| If you need to copy the array in this situation, set |
Moving arrays from a device to the host
Moving a device array to the host can be done by cupy.asnumpy() as follows:
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.
We can also use cupy.ndarray.get():
| Note |
| If you are using Chainer, you can use Translator's note: Chainer is a framework that supports dynamic computational graphs, and CuPy was split off from Chainer. |
Writing CPU/GPU-agnostic code
Thanks to the compatibility between CuPy and NumPy, you can write code that works on both CPU and GPU. The cupy.get_array_module() function makes this easy. It returns the numpy or cupy module based on its argument. A CPU/GPU-agnostic function is defined and used as follows.
# log(1 + exp(x))의 안정적인 구현
def softplus(x):
xp = cp.get_array_module(x)
return xp.maximum(0, x) + xp.log1p(xp.exp(-abs(x)))
Sometimes you may need to convert explicitly to a host or device array.
cupy.asarray() and cupy.asnumpy() can be used in generic implementations to convert between CuPY and NumPy arrays.
x_cpu = np.array([1, 2, 3])
y_cpu = np.array([4, 5, 6])
x_cpu + y_cpu # array([5, 7, 9])
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])
How to allocate a CUDA MemoryPointer
import cupy as cp
import numpy as np
size = 1024 # 배열의 크기
memory = cp.cuda.alloc(size * cp.float32().itemsize)
array = cp.ndarray((size,), dtype=cp.float32, memptr=memory)
How to directly assign a CUDA low-level device pointer (GPU)
mem = cp.cuda.UnownedMemory(ptr, size, owning_obj)
mem_ptr = cp.cuda.MemoryPointer(mem, 0)
arr = cp.ndarray(..., memptr= mem_ptr, ...) # make sure you interpret the array shape/dtype/strides correctly
Pinned Memory
How to synchronize pinned memory
- python - Asynchronous GPU memory transfer with cupy - Stack Overflow
- Copy Memory-mapped file into CuPy array · Issue #3431 · cupy/cupy
def pinned_array(array):
# first constructing pinned memory
mem = cupy.cuda.alloc_pinned_memory(array.nbytes)
src = numpy.frombuffer(
mem, array.dtype, array.size).reshape(array.shape)
src[...] = array
return src
a_cpu = np.ones((10000, 10000), dtype=np.float32)
b_cpu = np.ones((10000, 10000), dtype=np.float32)
# np.ndarray with pinned memory
a_cpu = pinned_array(a_cpu)
b_cpu = pinned_array(b_cpu)
a_stream = cp.cuda.Stream(non_blocking=True)
b_stream = cp.cuda.Stream(non_blocking=True)
a_gpu = cp.empty_like(a_cpu)
b_gpu = cp.empty_like(b_cpu)
a_gpu.set(a_cpu, stream=a_stream)
b_gpu.set(b_cpu, stream=b_stream)
# wait until a_cpu is copied in a_gpu
a_stream.synchronize()
# This line runs parallel to b_gpu.set()
a_gpu *= 2
Why the allocated memory size does not exactly match
Because memory is allocated to fit the memory alignment:
# Round up the memory size to fit memory alignment of cudaHostAlloc
unit = self._allocation_unit_size
size = internal.clp2(((size + unit - 1) // unit) * unit)
Memory alignment code
- Allocating aligned memory · Issue #2647 · cupy/cupy
- cupy/cupy/cuda/memory.pyx at 59e6c2b2e0c722b09c7a7af13f908942ef7806cc · cupy/cupy
# cudaMalloc() is aligned to at least 512 bytes
# cf. https://gist.github.com/sonots/41daaa6432b1c8b27ef782cd14064269
DEF ALLOCATION_UNIT_SIZE = 512
# for test
_allocation_unit_size = ALLOCATION_UNIT_SIZE
cpdef size_t _round_size(size_t size):
"""Rounds up the memory size to fit memory alignment of cudaMalloc."""
# avoid 0 div checking
size = (size + ALLOCATION_UNIT_SIZE - 1) // ALLOCATION_UNIT_SIZE
return size * ALLOCATION_UNIT_SIZE
cpdef size_t _bin_index_from_size(size_t size):
"""Returns appropriate bins index from the memory size."""
# avoid 0 div checking
return (size - 1) // ALLOCATION_UNIT_SIZE
Benchmarking
Use cupyx.profiler.benchmark. This function runs a few warm-up runs to reduce timing fluctuation and to exclude the overhead of the first call.
Synopsis:
cupyx.profiler.benchmark(func, args=(), kwargs={}, n_repeat=10000, *, name=None, n_warmup=10, max_duration=inf, devices=None)
Usage example:
from cupyx.profiler import benchmark
def my_func(a):
return cp.sqrt(cp.sum(a**2, axis=-1))
a = cp.random.random((256, 1024))
print(benchmark(my_func, (a,), n_repeat=20))
The output looks like this:
my_func : CPU: 44.407 us +/- 2.428 (min: 42.516 / max: 53.098) us GPU-0: 181.565 us +/- 1.853 (min: 180.288 / max: 188.608) us
Example of a manual implementation:
import time
start_gpu = cp.cuda.Event()
end_gpu = cp.cuda.Event()
start_gpu.record()
start_cpu = time.perf_counter()
out = my_func(a)
end_cpu = time.perf_counter()
end_gpu.record()
end_gpu.synchronize()
t_gpu = cp.cuda.get_elapsed_time(start_gpu, end_gpu)
t_cpu = end_cpu - start_cpu
Basics
- [Recommended] CuPy guide : Naver Blog [^0]
Low-level CUDA support
cupy.cuda.Event
A CUDA event, which is a synchronization point of CUDA streams.
This class handles the CUDA event handle in RAII way, i.e., when an Event instance is destroyed by the GC, its handle is also destroyed.
-
class cupy.cuda.Event(block=False, disable_timing=False, interprocess=False)- Parameters:
-
block(bool) – If True, the event blocks on the.synchronize()method. -
disable_timing(bool) – If True, the event does not prepare the timing data. -
interprocess(bool) – If True, the event can be passed to other processes.
-
- Parameters:
.synchronize() Method- Synchronizes all device work to the event.
- If the event is created as a blocking event, it also blocks the CPU thread until the event is done.
IPC
-
cupy.cuda.runtime.ipcGetMemHandle(intptr_t devPtr) -
cupy.cuda.runtime.ipcOpenMemHandle(bytes handle, unsigned int flags=cudaIpcMemLazyEnablePeerAccess) -
cupy.cuda.runtime.ipcCloseMemHandle(intptr_t devPtr) -
cupy.cuda.runtime.ipcGetEventHandle(intptr_t event) -
cupy.cuda.runtime.ipcOpenEventHandle(bytes handle)
See CuPy:IPC for an implementation example.
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
Interoperability
NumPy
cupy.ndarray implements array_ufunc interface.
Numba
Numba is a Python JIT compiler with NumPy support.
cupy.ndarray implements cuda_array_interface, the CUDA array interchange interface compatible with Numba v0.39.0 or later (see CUDA Array Interface for details).
This means you can pass CuPy arrays to kernels JITed with Numba. The following is a simple example code borrowed from numba/numba#2860.
import cupy
from numba import cuda
@cuda.jit
def add(x, y, out):
start = cuda.grid(1)
stride = cuda.gridsize(1)
for i in range(start, x.shape[0], stride):
out[i] = x[i] + y[i]
a = cupy.arange(10)
b = a * 2
out = cupy.zeros_like(a)
print(out) # => [0 0 0 0 0 0 0 0 0 0]
add[1, 32](a, b, out)
print(out) # => [ 0 3 6 9 12 15 18 21 24 27]
In addition, cupy.asarray() supports zero-copy conversion from a Numba CUDA array to a CuPy array.
import numpy
import numba
import cupy
x = numpy.arange(10) # type: numpy.ndarray
x_numba = numba.cuda.to_device(x) # type: numba.cuda.cudadrv.devicearray.DeviceNDArray
x_cupy = cupy.asarray(x_numba) # type: cupy.ndarray
mpi4py
PyTorch
PyTorch is a machine learning framework that provides high-performance, differentiable tensor operations.
PyTorch also supports cuda_array_interface, so zero-copy data exchange between CuPy and PyTorch can be done at no cost.
The only caveat is that PyTorch by default creates CPU tensors, which do not have the cuda_array_interface property defined, so users need to make sure the tensor is already on the GPU before exchanging.
import cupy as cp
import torch
import cupy as cp
import torch
# convert a torch tensor to a cupy array
a = torch.rand((4, 4), device='cuda')
b = cp.asarray(a)
b *= b
print(b)
"""
array([[0.8215962 , 0.82399917, 0.65607935, 0.30354425],
[0.422695 , 0.8367199 , 0.00208597, 0.18545236],
[0.00226746, 0.46201342, 0.6833052 , 0.47549972],
[0.5208748 , 0.6059282 , 0.1909013 , 0.5148635 ]], dtype=float32)
"""
print(a)
"""
tensor([[0.8216, 0.8240, 0.6561, 0.3035],
[0.4227, 0.8367, 0.0021, 0.1855],
[0.0023, 0.4620, 0.6833, 0.4755],
[0.5209, 0.6059, 0.1909, 0.5149]], device='cuda:0')
"""
# check the underlying memory pointer is the same
assert a.__cuda_array_interface__['data'][0] == b.__cuda_array_interface__['data'][0]
# convert a cupy array to a torch tensor
a = cp.arange(10)
b = torch.as_tensor(a, device='cuda')
b += 3
print(b)
"""tensor([ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], device='cuda:0')"""
print(a)
"""array([ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])"""
assert a.__cuda_array_interface__['data'][0] == b.__cuda_array_interface__['data'][0]
PyTorch also supports zero-copy data exchange through DLPack (see #DLPack below).
import cupy
import torch
# Create a PyTorch tensor.
tx1 = torch.randn(1, 2, 3, 4).cuda()
# Convert it into a CuPy array.
cx = cupy.from_dlpack(tx1)
# Convert it back to a PyTorch tensor.
tx2 = torch.from_dlpack(cx)
Feeding a torch.Tensor directly to cupy.from_dlpack() is only supported in the (new) DLPack data exchange protocol added in CuPy v10+ and PyTorch 1.10+.
For earlier versions, you need to wrap the Tensor with torch.utils.dlpack.to_dlpack() as shown in the example above.
RMM
DLPack
DLPack is a specification of tensor structure for sharing tensors among frameworks.
CuPy supports importing from and exporting to the DLPack data structure (cupy.from_dlpack() and cupy.ndarray.toDlpack()).
import cupy
# Create a CuPy array.
cx1 = cupy.random.randn(1, 2, 3, 4).astype(cupy.float32)
# Convert it into a DLPack tensor.
dx = cx1.toDlpack()
# Convert it back to a CuPy array.
cx2 = cupy.from_dlpack(dx)