Skill Tracer
Skills › Scientific Agent Skills › optimize-for-gpu › cupy.md

cupy.md

674 lines · skills/optimize-for-gpu/references/cupy.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# CuPy Reference
2
3> Review: 2026-10-01. Code below is illustrative unless explicitly described as CPU-tested.
4> GPU execution, performance, GDS, and multi-GPU behavior require validation on target hardware.
5
6CuPy is a NumPy/SciPy-compatible array library for GPU-accelerated computing. It wraps NVIDIA's optimized libraries (cuBLAS, cuFFT, cuSOLVER, cuSPARSE, cuRAND) so standard array operations are already highly tuned. Most NumPy code works by simply changing the import.
7
8> **Full documentation:** https://docs.cupy.dev/en/stable/
9

CuPy Reference

Review: 2026-10-01. Code below is illustrative unless explicitly described as CPU-tested. GPU execution, performance, GDS, and multi-GPU behavior require validation on target hardware.

CuPy is a NumPy/SciPy-compatible array library for GPU-accelerated computing. It wraps NVIDIA's optimized libraries (cuBLAS, cuFFT, cuSOLVER, cuSPARSE, cuRAND) so standard array operations are already highly tuned. Most NumPy code works by simply changing the import.

Full documentation: https://docs.cupy.dev/en/stable/
Raw lines 1 to 9
10## Table of Contents
121. [Installation and Setup](#installation-and-setup)
132. [The Drop-In Replacement Pattern](#the-drop-in-replacement-pattern)
143. [Core API: cupy.ndarray](#core-api)
154. [Supported Operations](#supported-operations)
165. [Custom Kernels](#custom-kernels)
176. [Kernel Fusion](#kernel-fusion)
187. [Memory Management](#memory-management)
198. [Streams and Async Operations](#streams-and-async-operations)
209. [Multi-GPU](#multi-gpu)
2110. [Performance Optimization](#performance-optimization)
2211. [Interoperability](#interoperability)
2312. [Key Differences from NumPy](#key-differences-from-numpy)
2413. [Common Pitfalls](#common-pitfalls)
2514. [Environment Variables](#environment-variables)
27---

Table of Contents

  1. Installation and Setup
  2. The Drop-In Replacement Pattern
  3. Core API: cupy.ndarray
  4. Supported Operations
  5. Custom Kernels
  6. Kernel Fusion
  7. Memory Management
  8. Streams and Async Operations
  9. Multi-GPU
  10. Performance Optimization
  11. Interoperability
  12. Key Differences from NumPy
  13. Common Pitfalls
  14. Environment Variables

Raw lines 10 to 28
29## Installation and Setup
31Use `uv add` in standalone examples; follow the user's existing project package manager when one
32is already configured.
34```bash
35uv add "cupy-cuda12x==14.2.*" # For CUDA 12.x
36uv add "cupy-cuda13x==14.2.*" # For CUDA 13.x
37```
39CuPy 14.2 requires CUDA >= 12.0, Python >= 3.10, and NumPy >= 2.0 (it follows NumPy 2 type-promotion rules, NEP 50), and supports free-threaded Python. The `[ctk]` extra (e.g. `cupy-cuda13x[ctk]`) pulls the required CUDA runtime components from PyPI, so only the NVIDIA driver needs to be pre-installed.
41Verify:
42```python
43import cupy as cp
44print(cp.cuda.runtime.getDeviceCount()) # >= 1 means GPU is available
45print(cp.show_config()) # Full environment info
46```
48---

Installation and Setup

Use uv add in standalone examples; follow the user's existing project package manager when one is already configured.

uv add "cupy-cuda12x==14.2.*"    # For CUDA 12.x
uv add "cupy-cuda13x==14.2.*"    # For CUDA 13.x

CuPy 14.2 requires CUDA >= 12.0, Python >= 3.10, and NumPy >= 2.0 (it follows NumPy 2 type-promotion rules, NEP 50), and supports free-threaded Python. The [ctk] extra (e.g. cupy-cuda13x[ctk]) pulls the required CUDA runtime components from PyPI, so only the NVIDIA driver needs to be pre-installed.

Verify:

import cupy as cp
print(cp.cuda.runtime.getDeviceCount())  # >= 1 means GPU is available
print(cp.show_config())                  # Full environment info

Raw lines 29 to 49
50## The Drop-In Replacement Pattern
52The fastest way to GPU-accelerate NumPy code: change the import.
54```python
55# Before (CPU)
56import numpy as np
57a = np.random.rand(10_000_000)
58b = np.fft.fft(a)
59c = np.sort(b.real)
61# After (GPU)
62import cupy as cp
63a = cp.random.rand(10_000_000)
64b = cp.fft.fft(a)
65c = cp.sort(b.real)
66```

The Drop-In Replacement Pattern

The fastest way to GPU-accelerate NumPy code: change the import.

# Before (CPU)
import numpy as np
a = np.random.rand(10_000_000)
b = np.fft.fft(a)
c = np.sort(b.real)

# After (GPU)
import cupy as cp
a = cp.random.rand(10_000_000)
b = cp.fft.fft(a)
c = cp.sort(b.real)
Raw lines 50 to 67
68### Data Transfer Between CPU and GPU
70```python
71# NumPy → CuPy (CPU → GPU)
72gpu_array = cp.asarray(numpy_array) # Zero-copy if already on current device
73gpu_array = cp.array(numpy_array) # Always copies
75# CuPy → NumPy (GPU → CPU)
76cpu_array = cp.asnumpy(gpu_array) # Copy to CPU
77cpu_array = gpu_array.get() # Same thing
78```

Data Transfer Between CPU and GPU

# NumPy → CuPy (CPU → GPU)
gpu_array = cp.asarray(numpy_array)     # Zero-copy if already on current device
gpu_array = cp.array(numpy_array)       # Always copies

# CuPy → NumPy (GPU → CPU)
cpu_array = cp.asnumpy(gpu_array)       # Copy to CPU
cpu_array = gpu_array.get()             # Same thing
Raw lines 68 to 79
80### Writing CPU/GPU Agnostic Code
82```python
83def normalize(x):
84 xp = cp.get_array_module(x) # Returns cupy or numpy depending on input
85 return x / xp.linalg.norm(x)
87# Works with both NumPy and CuPy arrays
88normalize(numpy_array) # Runs on CPU
89normalize(cupy_array) # Runs on GPU
90```
92CuPy arrays implement `__array_ufunc__` and `__array_function__`, so NumPy functions can dispatch to CuPy automatically when given CuPy arrays (NumPy >= 1.17).
94---

Writing CPU/GPU Agnostic Code

def normalize(x):
    xp = cp.get_array_module(x)  # Returns cupy or numpy depending on input
    return x / xp.linalg.norm(x)

# Works with both NumPy and CuPy arrays
normalize(numpy_array)   # Runs on CPU
normalize(cupy_array)    # Runs on GPU

CuPy arrays implement __array_ufunc__ and __array_function__, so NumPy functions can dispatch to CuPy automatically when given CuPy arrays (NumPy >= 1.17).


Raw lines 80 to 95
Starts an agent dispatch :92
96## Core API
98`cupy.ndarray` mirrors `numpy.ndarray` — same attributes (`shape`, `dtype`, `ndim`, `size`, `strides`, `T`), plus `device` (which GPU the array lives on).
100**Important:** `cupy.ndarray` and `numpy.ndarray` are NOT implicitly convertible. Every conversion incurs a host-device data transfer.

Core API

cupy.ndarray mirrors numpy.ndarray — same attributes (shape, dtype, ndim, size, strides, T), plus device (which GPU the array lives on).

Important: cupy.ndarray and numpy.ndarray are NOT implicitly convertible. Every conversion incurs a host-device data transfer.

Raw lines 96 to 101
102### Array Creation
104```python
105cp.empty((1000, 1000), dtype=cp.float32)
106cp.zeros((1000,), dtype=cp.float64)
107cp.ones((512, 512), dtype=cp.float32)
108cp.full((100,), fill_value=3.14, dtype=cp.float32)
109cp.arange(0, 100, 0.1)
110cp.linspace(0, 1, 1000)
111cp.eye(100)
112cp.random.rand(1000, 1000) # Uniform [0, 1)
113cp.random.randn(1000, 1000) # Standard normal
114cp.random.default_rng(42).normal(0, 1, 1000) # Generator API
115```
117Many CuPy random methods accept `dtype`; check each method. NumPy Generator.random also supports float32/float64. Choose precision from the numerical contract.
119---

Array Creation

cp.empty((1000, 1000), dtype=cp.float32)
cp.zeros((1000,), dtype=cp.float64)
cp.ones((512, 512), dtype=cp.float32)
cp.full((100,), fill_value=3.14, dtype=cp.float32)
cp.arange(0, 100, 0.1)
cp.linspace(0, 1, 1000)
cp.eye(100)
cp.random.rand(1000, 1000)                    # Uniform [0, 1)
cp.random.randn(1000, 1000)                   # Standard normal
cp.random.default_rng(42).normal(0, 1, 1000)  # Generator API

Many CuPy random methods accept dtype; check each method. NumPy Generator.random also supports float32/float64. Choose precision from the numerical contract.


Raw lines 102 to 120
121## Supported Operations
123CuPy implements most of NumPy and large parts of SciPy. All are GPU-accelerated.

Supported Operations

CuPy implements most of NumPy and large parts of SciPy. All are GPU-accelerated.

Raw lines 121 to 124
125### Array Math and Element-wise Operations
126`sin`, `cos`, `tan`, `exp`, `log`, `log2`, `log10`, `sqrt`, `square`, `abs`, `power`, `add`, `subtract`, `multiply`, `divide`, `mod`, `clip`, `sign`, `ceil`, `floor`, `round`, `maximum`, `minimum`

Array Math and Element-wise Operations

sin, cos, tan, exp, log, log2, log10, sqrt, square, abs, power, add, subtract, multiply, divide, mod, clip, sign, ceil, floor, round, maximum, minimum

Raw lines 125 to 127
128### Reductions
129`sum`, `prod`, `mean`, `std`, `var`, `min`, `max`, `argmin`, `argmax`, `cumsum`, `cumprod`, `any`, `all`, `nansum`, `nanmean`, `nanstd`, `nanvar`

Reductions

sum, prod, mean, std, var, min, max, argmin, argmax, cumsum, cumprod, any, all, nansum, nanmean, nanstd, nanvar

Raw lines 128 to 130
131### Linear Algebra (`cupy.linalg` — powered by cuBLAS/cuSOLVER)
132`dot`, `matmul`, `@` operator, `tensordot`, `einsum`, `inner`, `outer`, `cholesky`, `qr`, `svd`, `eig`, `eigh`, `eigvalsh`, `norm`, `solve`, `inv`, `pinv`, `lstsq`, `det`, `slogdet`, `matrix_rank`, `matrix_power`

Linear Algebra (cupy.linalg — powered by cuBLAS/cuSOLVER)

dot, matmul, @ operator, tensordot, einsum, inner, outer, cholesky, qr, svd, eig, eigh, eigvalsh, norm, solve, inv, pinv, lstsq, det, slogdet, matrix_rank, matrix_power

Raw lines 131 to 133
134### FFT (`cupy.fft` — powered by cuFFT)
135`fft`, `ifft`, `fft2`, `ifft2`, `fftn`, `ifftn`, `rfft`, `irfft`, `rfft2`, `irfft2`, `rfftn`, `irfftn`, `fftfreq`, `rfftfreq`, `fftshift`, `ifftshift`

FFT (cupy.fft — powered by cuFFT)

fft, ifft, fft2, ifft2, fftn, ifftn, rfft, irfft, rfft2, irfft2, rfftn, irfftn, fftfreq, rfftfreq, fftshift, ifftshift

Raw lines 134 to 136
137### Sorting and Searching
138`sort`, `argsort`, `partition`, `argpartition`, `argmin`, `argmax`, `where`, `nonzero`, `unique`, `searchsorted`

Sorting and Searching

sort, argsort, partition, argpartition, argmin, argmax, where, nonzero, unique, searchsorted

Raw lines 137 to 139
140### Array Manipulation
141`reshape`, `ravel`, `flatten`, `transpose`, `swapaxes`, `concatenate`, `stack`, `vstack`, `hstack`, `dstack`, `split`, `hsplit`, `vsplit`, `tile`, `repeat`, `pad`, `flip`, `fliplr`, `flipud`, `roll`, `rot90`, `broadcast_to`, `expand_dims`, `squeeze`

Array Manipulation

reshape, ravel, flatten, transpose, swapaxes, concatenate, stack, vstack, hstack, dstack, split, hsplit, vsplit, tile, repeat, pad, flip, fliplr, flipud, roll, rot90, broadcast_to, expand_dims, squeeze

Raw lines 140 to 142
143### Sparse Matrices (`cupyx.scipy.sparse`)
144CSR, CSC, COO formats. Matrix-vector multiply, matrix-matrix multiply, conversions between formats. Powered by cuSPARSE. CuPy v14 adds support for large sparse matrices with 64-bit dimensions and nonzero counts.

Sparse Matrices (cupyx.scipy.sparse)

CSR, CSC, COO formats. Matrix-vector multiply, matrix-matrix multiply, conversions between formats. Powered by cuSPARSE. CuPy v14 adds support for large sparse matrices with 64-bit dimensions and nonzero counts.

Raw lines 143 to 145
146### Signal Processing (`cupyx.scipy.signal`)
147Convolution, correlation, filtering, window functions.

Signal Processing (cupyx.scipy.signal)

Convolution, correlation, filtering, window functions.

Raw lines 146 to 148
149### Special Functions (`cupyx.scipy.special`)
150Bessel functions, error functions, gamma functions, and more.

Special Functions (cupyx.scipy.special)

Bessel functions, error functions, gamma functions, and more.

Raw lines 149 to 151
152### Statistics
153`mean`, `median`, `std`, `var`, `percentile`, `quantile`, `corrcoef`, `cov`, `histogram`, `bincount`, `digitize`
155---

Statistics

mean, median, std, var, percentile, quantile, corrcoef, cov, histogram, bincount, digitize


Raw lines 152 to 156
157## Custom Kernels
159When built-in operations aren't enough, CuPy offers several ways to write custom GPU code, ordered from simplest to most powerful.

Custom Kernels

When built-in operations aren't enough, CuPy offers several ways to write custom GPU code, ordered from simplest to most powerful.

Raw lines 157 to 160
161### ElementwiseKernel — Custom Element-wise Operations
163CuPy handles indexing and broadcasting automatically. You just write the per-element logic in C++.
165```python
166squared_diff = cp.ElementwiseKernel(
167 'float32 x, float32 y', # Input params
168 'float32 z', # Output params
169 'z = (x - y) * (x - y)', # Per-element operation (C++ code)
170 'squared_diff' # Kernel name
173result = squared_diff(a, b) # Broadcasting works automatically
174```
176**Type-generic kernels:** Use single-letter type placeholders. Same letter = same type, resolved from arguments at call time.
178```python
179generic_squared_diff = cp.ElementwiseKernel(
180 'T x, T y', 'T z',
181 'z = (x - y) * (x - y)',
182 'generic_squared_diff'
184# Works with float32, float64, etc. — type inferred from inputs
185```
187**Raw indexing:** Prefix with `raw` to disable automatic indexing. Use `i` for loop index.
189```python
190# Access neighbors — raw disables auto-indexing so you can index manually
191stencil = cp.ElementwiseKernel(
192 'raw T x', 'T y',
193 'y = (x[i > 0 ? i-1 : 0] + x[i] + x[i < _ind.size()-1 ? i+1 : _ind.size()-1]) / 3',
194 'stencil_1d'
196```

ElementwiseKernel — Custom Element-wise Operations

CuPy handles indexing and broadcasting automatically. You just write the per-element logic in C++.

squared_diff = cp.ElementwiseKernel(
    'float32 x, float32 y',   # Input params
    'float32 z',               # Output params
    'z = (x - y) * (x - y)',  # Per-element operation (C++ code)
    'squared_diff'             # Kernel name
)

result = squared_diff(a, b)  # Broadcasting works automatically

Type-generic kernels: Use single-letter type placeholders. Same letter = same type, resolved from arguments at call time.

generic_squared_diff = cp.ElementwiseKernel(
    'T x, T y', 'T z',
    'z = (x - y) * (x - y)',
    'generic_squared_diff'
)
# Works with float32, float64, etc. — type inferred from inputs

Raw indexing: Prefix with raw to disable automatic indexing. Use i for loop index.

# Access neighbors — raw disables auto-indexing so you can index manually
stencil = cp.ElementwiseKernel(
    'raw T x', 'T y',
    'y = (x[i > 0 ? i-1 : 0] + x[i] + x[i < _ind.size()-1 ? i+1 : _ind.size()-1]) / 3',
    'stencil_1d'
)
Raw lines 161 to 197
198### ReductionKernel — Custom Reductions
200Four-part reduction: map each element, reduce pairs, post-process the result.
202```python
203l2norm = cp.ReductionKernel(
204 'T x', # Input
205 'T y', # Output
206 'x * x', # Map: square each element
207 'a + b', # Reduce: sum pairs (a, b are the binary operands)
208 'y = sqrt(a)', # Post-map: sqrt of final sum
209 '0', # Identity element
210 'l2norm' # Kernel name
213norm = l2norm(array) # Full reduction → scalar
214norms = l2norm(matrix, axis=1) # Reduce along axis → vector
215```

ReductionKernel — Custom Reductions

Four-part reduction: map each element, reduce pairs, post-process the result.

l2norm = cp.ReductionKernel(
    'T x',           # Input
    'T y',           # Output
    'x * x',         # Map: square each element
    'a + b',         # Reduce: sum pairs (a, b are the binary operands)
    'y = sqrt(a)',   # Post-map: sqrt of final sum
    '0',             # Identity element
    'l2norm'         # Kernel name
)

norm = l2norm(array)        # Full reduction → scalar
norms = l2norm(matrix, axis=1)  # Reduce along axis → vector
Raw lines 198 to 216
217### RawKernel — Full CUDA C/C++
219For complete control over grid, blocks, shared memory — write raw CUDA.
221```python
222import numpy as np
224kernel_code = r'''
225extern "C" __global__
226void vector_add(const float* a, const float* b, float* c, int n) {
227 int tid = blockDim.x * blockIdx.x + threadIdx.x;
228 if (tid < n) {
229 c[tid] = a[tid] + b[tid];
230 }
232'''
233vector_add = cp.RawKernel(kernel_code, 'vector_add')
235n = 1_000_000
236a = cp.random.rand(n, dtype=cp.float32)
237b = cp.random.rand(n, dtype=cp.float32)
238c = cp.zeros(n, dtype=cp.float32)
240threads = 256
241blocks = (n + threads - 1) // threads
242vector_add((blocks,), (threads,), (a, b, c, np.int32(n))) # CUDA int is int32
243```
245**Important RawKernel caveats:**
246- Ignores array views/strides — `matrix.T` is treated as `matrix`. Handle strides yourself.
247- Use `extern "C"` to prevent C++ name mangling.
248- For complex numbers, include `<cupy/complex.cuh>`.
249- Compiled binaries cached in `~/.cupy/kernel_cache`.
251**CuPy dtype to CUDA type mapping:**
253| CuPy dtype | CUDA type |
254|-----------|-----------|
255| `float16` | `half` |
256| `float32` | `float` |
257| `float64` | `double` |
258| `int32` | `int` |
259| `int64` | `long long` |
260| `complex64` | `complex<float>` |
261| `complex128` | `complex<double>` |

RawKernel — Full CUDA C/C++

For complete control over grid, blocks, shared memory — write raw CUDA.

import numpy as np

kernel_code = r'''
extern "C" __global__
void vector_add(const float* a, const float* b, float* c, int n) {
    int tid = blockDim.x * blockIdx.x + threadIdx.x;
    if (tid < n) {
        c[tid] = a[tid] + b[tid];
    }
}
'''
vector_add = cp.RawKernel(kernel_code, 'vector_add')

n = 1_000_000
a = cp.random.rand(n, dtype=cp.float32)
b = cp.random.rand(n, dtype=cp.float32)
c = cp.zeros(n, dtype=cp.float32)

threads = 256
blocks = (n + threads - 1) // threads
vector_add((blocks,), (threads,), (a, b, c, np.int32(n)))  # CUDA int is int32

Important RawKernel caveats:

  • Ignores array views/strides — matrix.T is treated as matrix. Handle strides yourself.
  • Use extern "C" to prevent C++ name mangling.
  • For complex numbers, include <cupy/complex.cuh>.
  • Compiled binaries cached in ~/.cupy/kernel_cache.

CuPy dtype to CUDA type mapping:

CuPy dtypeCUDA type
float16half
float32float
float64double
int32int
int64long long
complex64complex<float>
complex128complex<double>
Raw lines 217 to 262
263### RawModule — Large CUDA Codebases
265For multi-kernel CUDA files or precompiled binaries:
267```python
268module = cp.RawModule(code=cuda_source) # From source string
269from pathlib import Path
270module = cp.RawModule(code=Path('kernels.cu').read_text()) # CUDA source file
271module = cp.RawModule(path='kernels.cubin') # From precompiled
273kernel = module.get_function('my_kernel')
274kernel((blocks,), (threads,), args) # args is the kernel argument tuple
275```

RawModule — Large CUDA Codebases

For multi-kernel CUDA files or precompiled binaries:

module = cp.RawModule(code=cuda_source)       # From source string
from pathlib import Path
module = cp.RawModule(code=Path('kernels.cu').read_text())  # CUDA source file
module = cp.RawModule(path='kernels.cubin')   # From precompiled

kernel = module.get_function('my_kernel')
kernel((blocks,), (threads,), args)  # args is the kernel argument tuple
Raw lines 263 to 276
277### JIT Kernel (cupyx.jit.rawkernel) — CUDA Kernels in Python Syntax
279Write CUDA-style kernels using Python syntax instead of C++.
281```python
282@cupyx.jit.rawkernel()
283def my_kernel(x, y, size):
284 tid = cupyx.jit.grid(1)
285 if tid < size:
286 y[tid] = x[tid] * 2.0
288my_kernel[blocks, threads](x, y, n)
289```
291Available JIT primitives:
292- `cupyx.jit.threadIdx`, `blockIdx`, `blockDim`, `gridDim`
293- `cupyx.jit.grid(ndim)`, `gridsize(ndim)`
294- `cupyx.jit.syncthreads()`, `syncwarp()`
295- `cupyx.jit.shared_memory(dtype, size)`
296- `cupyx.jit.atomic_add/min/max/and/or/xor(array, index, value)`
297- Warp shuffles: `shfl_sync`, `shfl_up_sync`, `shfl_down_sync`, `shfl_xor_sync`
299**Limitation:** Does not work in Python REPL (needs source code access). Use from .py files.
301---

JIT Kernel (cupyx.jit.rawkernel) — CUDA Kernels in Python Syntax

Write CUDA-style kernels using Python syntax instead of C++.

@cupyx.jit.rawkernel()
def my_kernel(x, y, size):
    tid = cupyx.jit.grid(1)
    if tid < size:
        y[tid] = x[tid] * 2.0

my_kernel[blocks, threads](x, y, n)

Available JIT primitives:

  • cupyx.jit.threadIdx, blockIdx, blockDim, gridDim
  • cupyx.jit.grid(ndim), gridsize(ndim)
  • cupyx.jit.syncthreads(), syncwarp()
  • cupyx.jit.shared_memory(dtype, size)
  • cupyx.jit.atomic_add/min/max/and/or/xor(array, index, value)
  • Warp shuffles: shfl_sync, shfl_up_sync, shfl_down_sync, shfl_xor_sync

Limitation: Does not work in Python REPL (needs source code access). Use from .py files.


Raw lines 277 to 302
303## Kernel Fusion
305Combine multiple element-wise operations into a single kernel launch — eliminates intermediate arrays and reduces kernel launch overhead.
307```python
308@cp.fuse()
309def fused_op(x, y):
310 return cp.sqrt((x - y) ** 2 + 1.0)
312# This compiles into ONE kernel instead of multiple
313result = fused_op(a, b)
314```
316**Limitation:** Only fuses elementwise and simple reduction operations. Does not support `matmul`, `reshape`, indexing, etc.
318---

Kernel Fusion

Combine multiple element-wise operations into a single kernel launch — eliminates intermediate arrays and reduces kernel launch overhead.

@cp.fuse()
def fused_op(x, y):
    return cp.sqrt((x - y) ** 2 + 1.0)

# This compiles into ONE kernel instead of multiple
result = fused_op(a, b)

Limitation: Only fuses elementwise and simple reduction operations. Does not support matmul, reshape, indexing, etc.


Raw lines 303 to 319
320## Memory Management

Memory Management

Raw lines 320 to 321
322### Memory Pools (Default Behavior)
324CuPy uses memory pools by default — this is critical for performance. The pool caches freed GPU memory for reuse, avoiding expensive `cudaMalloc`/`cudaFree` calls and implicit synchronization.
326**Key insight:** Memory is NOT freed to the OS when arrays go out of scope — it's returned to the pool. This is expected behavior (shows up in `nvidia-smi` as still-allocated).
328```python
329mempool = cp.get_default_memory_pool()
330mempool.used_bytes() # Currently allocated by CuPy arrays
331mempool.total_bytes() # Total held by pool (including free blocks)
332mempool.free_all_blocks() # Release all unused memory back to OS
334pinned_mempool = cp.get_default_pinned_memory_pool()
335pinned_mempool.free_all_blocks()
336```

Memory Pools (Default Behavior)

CuPy uses memory pools by default — this is critical for performance. The pool caches freed GPU memory for reuse, avoiding expensive cudaMalloc/cudaFree calls and implicit synchronization.

Key insight: Memory is NOT freed to the OS when arrays go out of scope — it's returned to the pool. This is expected behavior (shows up in nvidia-smi as still-allocated).

mempool = cp.get_default_memory_pool()
mempool.used_bytes()        # Currently allocated by CuPy arrays
mempool.total_bytes()       # Total held by pool (including free blocks)
mempool.free_all_blocks()   # Release all unused memory back to OS

pinned_mempool = cp.get_default_pinned_memory_pool()
pinned_mempool.free_all_blocks()
Raw lines 322 to 337
338### Limiting GPU Memory
340```python
341mempool = cp.get_default_memory_pool()
342with cp.cuda.Device(0):
343 mempool.set_limit(size=4 * 1024**3) # 4 GiB limit for GPU 0
344```
346Or via environment variable (set before `import cupy`):
347```bash
348export CUPY_GPU_MEMORY_LIMIT="50%" # Percentage of total GPU memory
349export CUPY_GPU_MEMORY_LIMIT="4294967296" # Bytes
350```

Limiting GPU Memory

mempool = cp.get_default_memory_pool()
with cp.cuda.Device(0):
    mempool.set_limit(size=4 * 1024**3)  # 4 GiB limit for GPU 0

Or via environment variable (set before import cupy):

export CUPY_GPU_MEMORY_LIMIT="50%"     # Percentage of total GPU memory
export CUPY_GPU_MEMORY_LIMIT="4294967296"  # Bytes
Raw lines 338 to 351
352### Managed (Unified) Memory
354Data auto-migrates between CPU and GPU. Useful when data doesn't fit in GPU memory.
356```python
357cp.cuda.set_allocator(cp.cuda.MemoryPool(cp.cuda.malloc_managed).malloc)
358```

Managed (Unified) Memory

Data auto-migrates between CPU and GPU. Useful when data doesn't fit in GPU memory.

cp.cuda.set_allocator(cp.cuda.MemoryPool(cp.cuda.malloc_managed).malloc)
Raw lines 352 to 359
360### Pinned Memory for Fast Transfers
362```python
363# High-level API
364pinned_array = cupyx.empty_pinned((1000,), dtype=np.float32)
365pinned_array = cupyx.zeros_pinned((1000,), dtype=np.float32)
367# These are NumPy arrays backed by page-locked memory — transfers to GPU are faster
368```

Pinned Memory for Fast Transfers

# High-level API
pinned_array = cupyx.empty_pinned((1000,), dtype=np.float32)
pinned_array = cupyx.zeros_pinned((1000,), dtype=np.float32)

# These are NumPy arrays backed by page-locked memory — transfers to GPU are faster
Raw lines 360 to 369
370### Disabling Pools
372```python
373cp.cuda.set_allocator(None) # Disable device pool
374cp.cuda.set_pinned_memory_allocator(None) # Disable pinned pool
375```
377Must be done before any CuPy operations.

Disabling Pools

cp.cuda.set_allocator(None)                    # Disable device pool
cp.cuda.set_pinned_memory_allocator(None)      # Disable pinned pool

Must be done before any CuPy operations.

Raw lines 370 to 378
379### Using RMM (RAPIDS Memory Manager)
381When using CuPy alongside cuDF/RAPIDS, align on a single allocator:
383```python
384import rmm
385from rmm.allocators.cupy import rmm_cupy_allocator
387rmm.reinitialize(pool_allocator=True) # Configure before allocating any live device arrays
388cp.cuda.set_allocator(rmm_cupy_allocator)
389```
391---

Using RMM (RAPIDS Memory Manager)

When using CuPy alongside cuDF/RAPIDS, align on a single allocator:

import rmm
from rmm.allocators.cupy import rmm_cupy_allocator

rmm.reinitialize(pool_allocator=True)  # Configure before allocating any live device arrays
cp.cuda.set_allocator(rmm_cupy_allocator)

Raw lines 379 to 392
393## Streams and Async Operations
395Streams enable overlapping computation with data transfer and running multiple operations concurrently.
397```python
398stream = cp.cuda.Stream()
400# Context manager style
401with stream:
402 d_data = cp.asarray(host_data) # H→D transfer on this stream
403 result = cp.sum(d_data) # Kernel on this stream
404# Operations enqueued but may not be complete here
406stream.synchronize() # Wait for all operations on this stream
407```

Streams and Async Operations

Streams enable overlapping computation with data transfer and running multiple operations concurrently.

stream = cp.cuda.Stream()

# Context manager style
with stream:
    d_data = cp.asarray(host_data)     # H→D transfer on this stream
    result = cp.sum(d_data)            # Kernel on this stream
# Operations enqueued but may not be complete here

stream.synchronize()  # Wait for all operations on this stream
Raw lines 393 to 408
409### Multiple Streams for Overlap
411```python
412s1 = cp.cuda.Stream()
413s2 = cp.cuda.Stream()
415with s1:
416 d_a = cp.asarray(data_a)
417 result_a = cp.fft.fft(d_a)
419with s2:
420 d_b = cp.asarray(data_b) # Overlaps with s1's FFT
421 result_b = cp.fft.fft(d_b)
423cp.cuda.Device().synchronize() # Wait for all streams
424```

Multiple Streams for Overlap

s1 = cp.cuda.Stream()
s2 = cp.cuda.Stream()

with s1:
    d_a = cp.asarray(data_a)
    result_a = cp.fft.fft(d_a)

with s2:
    d_b = cp.asarray(data_b)  # Overlaps with s1's FFT
    result_b = cp.fft.fft(d_b)

cp.cuda.Device().synchronize()  # Wait for all streams
Raw lines 409 to 425
426### Events for Timing
428```python
429start = cp.cuda.Event()
430end = cp.cuda.Event()
432start.record()
433# ... GPU operations ...
434end.record()
435end.synchronize()
437elapsed_ms = cp.cuda.get_elapsed_time(start, end)
438```

Events for Timing

start = cp.cuda.Event()
end = cp.cuda.Event()

start.record()
# ... GPU operations ...
end.record()
end.synchronize()

elapsed_ms = cp.cuda.get_elapsed_time(start, end)
Raw lines 426 to 439
440### Per-Thread Default Stream
442```bash
443export CUPY_CUDA_PER_THREAD_DEFAULT_STREAM=1
444```
446Enables per-thread default streams for better concurrency in multi-threaded applications.
448---

Per-Thread Default Stream

export CUPY_CUDA_PER_THREAD_DEFAULT_STREAM=1

Enables per-thread default streams for better concurrency in multi-threaded applications.


Raw lines 440 to 449
450## Multi-GPU
452```python
453# Set current device
454cp.cuda.Device(0).use()
456# Context manager
457with cp.cuda.Device(1):
458 x = cp.array([1, 2, 3]) # Allocated on GPU 1
460# Check which device an array is on
461print(x.device) # Device 1
462```
464Cross-device operations may work via P2P (peer-to-peer) memory access if the GPU topology supports it. Use `cp.asarray()` to explicitly transfer arrays between devices.

Multi-GPU

# Set current device
cp.cuda.Device(0).use()

# Context manager
with cp.cuda.Device(1):
    x = cp.array([1, 2, 3])  # Allocated on GPU 1

# Check which device an array is on
print(x.device)  # Device 1

Cross-device operations may work via P2P (peer-to-peer) memory access if the GPU topology supports it. Use cp.asarray() to explicitly transfer arrays between devices.

Raw lines 450 to 465
466### Per-Device Memory Limits
468```python
469mempool = cp.get_default_memory_pool()
470with cp.cuda.Device(0):
471 mempool.set_limit(size=4 * 1024**3)
472with cp.cuda.Device(1):
473 mempool.set_limit(size=4 * 1024**3)
474```
476---

Per-Device Memory Limits

mempool = cp.get_default_memory_pool()
with cp.cuda.Device(0):
    mempool.set_limit(size=4 * 1024**3)
with cp.cuda.Device(1):
    mempool.set_limit(size=4 * 1024**3)

Raw lines 466 to 477
478## Performance Optimization

Performance Optimization

Raw lines 478 to 479
480### Benchmarking (Critical First Step)
482An unsynchronized `time.perf_counter()` or `%timeit` measures enqueue latency. For end-to-end wall time, synchronize the relevant device/streams before starting and before stopping the timer. For GPU region timing use CUDA events or the benchmark helper below.
484```python
485from cupyx.profiler import benchmark
487result = benchmark(my_function, (arg1, arg2), n_repeat=100, n_warmup=10)
488print(result) # Shows CPU and GPU elapsed times with statistics
489```
491In IPython/Jupyter:
492```python
493%load_ext cupyx.profiler
494%gpu_timeit my_function(args)
495```

Benchmarking (Critical First Step)

An unsynchronized time.perf_counter() or %timeit measures enqueue latency. For end-to-end wall time, synchronize the relevant device/streams before starting and before stopping the timer. For GPU region timing use CUDA events or the benchmark helper below.

from cupyx.profiler import benchmark

result = benchmark(my_function, (arg1, arg2), n_repeat=100, n_warmup=10)
print(result)  # Shows CPU and GPU elapsed times with statistics

In IPython/Jupyter:

%load_ext cupyx.profiler
%gpu_timeit my_function(args)
Raw lines 480 to 496
497### One-Time Overheads
499- **Context initialization:** First CuPy call may take 1-5 seconds (CUDA context creation). This is one-time.
500- **Kernel JIT compilation:** First call to any operation triggers on-the-fly kernel compilation. Cached in `~/.cupy/kernel_cache`. Persist this directory across CI/CD runs.

One-Time Overheads

  • Context initialization: First CuPy call may take 1-5 seconds (CUDA context creation). This is one-time.
  • Kernel JIT compilation: First call to any operation triggers on-the-fly kernel compilation. Cached in ~/.cupy/kernel_cache. Persist this directory across CI/CD runs.
Raw lines 497 to 501
502### CUB and cuTENSOR Acceleration
504```bash
505# CuPy v11+ uses CUB by default
506export CUPY_ACCELERATORS=cub # CUB only (default)
507export CUPY_ACCELERATORS=cub,cutensor # Both (requires cuTENSOR installed)
508```
510CUB accelerates reductions (`sum`, `prod`, `amin`, `amax`, `argmin`, `argmax`),
511inclusive scans (`cumsum`), histograms, sparse matrix-vector multiply, and `ReductionKernel`.
512The benefit depends on dtype, shape, axis, and hardware; benchmark the target operation.
514cuTENSOR accelerates: binary elementwise ufuncs, reduction, tensor contraction.

CUB and cuTENSOR Acceleration

# CuPy v11+ uses CUB by default
export CUPY_ACCELERATORS=cub          # CUB only (default)
export CUPY_ACCELERATORS=cub,cutensor # Both (requires cuTENSOR installed)

CUB accelerates reductions (sum, prod, amin, amax, argmin, argmax), inclusive scans (cumsum), histograms, sparse matrix-vector multiply, and ReductionKernel. The benefit depends on dtype, shape, axis, and hardware; benchmark the target operation.

cuTENSOR accelerates: binary elementwise ufuncs, reduction, tensor contraction.

Raw lines 502 to 515
516### Key Optimization Strategies
5181. **Prefer float32 over float64 when the numerical contract allows it.** Throughput differences
519depend on GPU architecture; validate accuracy and benchmark the target device.
5212. **Minimize CPU-GPU transfers.** Every `cp.asnumpy()` / `.get()` triggers synchronization and PCI-e transfer. Keep data on GPU as long as possible.
5233. **Use kernel fusion.** `@cp.fuse()` combines multiple elementwise operations into one kernel, eliminating intermediate arrays.
5254. **Batch operations.** Fewer large operations usually beat many small ones. Measure launch
526overhead on the target system rather than relying on a fixed latency.
5285. **Pre-allocate output arrays.** Use `out=` parameter in ufuncs to avoid repeated allocation:
529 ```python
530 cp.add(a, b, out=result) # Writes into existing array
531 ```
5336. **Use in-place operations.** `a += b` avoids allocating a new array.
5357. **Use streams** to overlap computation and data transfer.
5378. **Profile with NVTX markers** for Nsight Systems analysis:
538 ```python
539 with cupyx.profiler.time_range('my_operation', color_id=0):
540 result = heavy_computation()
541 ```

Key Optimization Strategies

  1. Prefer float32 over float64 when the numerical contract allows it. Throughput differences

depend on GPU architecture; validate accuracy and benchmark the target device.

  1. Minimize CPU-GPU transfers. Every cp.asnumpy() / .get() triggers synchronization and PCI-e transfer. Keep data on GPU as long as possible.
  1. Use kernel fusion. @cp.fuse() combines multiple elementwise operations into one kernel, eliminating intermediate arrays.
  1. Batch operations. Fewer large operations usually beat many small ones. Measure launch

overhead on the target system rather than relying on a fixed latency.

  1. Pre-allocate output arrays. Use out= parameter in ufuncs to avoid repeated allocation: ``python cp.add(a, b, out=result) # Writes into existing array
  1. Use in-place operations. a += b avoids allocating a new array.
  1. Use streams to overlap computation and data transfer.
  1. Profile with NVTX markers for Nsight Systems analysis: ``python with cupyx.profiler.time_range('my_operation', color_id=0): result = heavy_computation()
Raw lines 516 to 542
543### Decision Tree: Which Kernel Approach?
5451. **Can be expressed as NumPy ops?** → Use built-in CuPy functions (fastest development, often best performance)
5462. **Multiple chained elementwise ops?** → Use `@cp.fuse()`
5473. **Custom elementwise with broadcasting?** → Use `ElementwiseKernel`
5484. **Custom reduction?** → Use `ReductionKernel`
5495. **Need full grid/block/shared memory control?** → Use `RawKernel` or `cupyx.jit.rawkernel`
5506. **Large CUDA codebase?** → Use `RawModule`
552---

Decision Tree: Which Kernel Approach?

  1. Can be expressed as NumPy ops? → Use built-in CuPy functions (fastest development, often best performance)
  2. Multiple chained elementwise ops? → Use @cp.fuse()
  3. Custom elementwise with broadcasting? → Use ElementwiseKernel
  4. Custom reduction? → Use ReductionKernel
  5. Need full grid/block/shared memory control? → Use RawKernel or cupyx.jit.rawkernel
  6. Large CUDA codebase? → Use RawModule

Raw lines 543 to 553
554## Interoperability
556CuPy interoperates with other GPU libraries via the CUDA Array Interface and DLPack protocol — both enable zero-copy data sharing.

Interoperability

CuPy interoperates with other GPU libraries via the CUDA Array Interface and DLPack protocol — both enable zero-copy data sharing.

Raw lines 554 to 557
558### NumPy
560```python
561# NumPy functions auto-dispatch to CuPy (NumPy >= 1.17)
562import numpy as np
563result = np.sum(cupy_array) # Dispatches to CuPy, returns CuPy array
564```

NumPy

# NumPy functions auto-dispatch to CuPy (NumPy >= 1.17)
import numpy as np
result = np.sum(cupy_array)  # Dispatches to CuPy, returns CuPy array
Raw lines 558 to 565
Starts an agent dispatch :561
Starts an agent Dispatches :563
566### Numba
568```python
569from numba import cuda
571@cuda.jit
572def numba_kernel(x, y):
573 i = cuda.grid(1)
574 if i < x.shape[0]:
575 y[i] = x[i] * 2
577# CuPy arrays pass directly to Numba kernels — zero copy
578a = cp.arange(1000, dtype=cp.float32)
579b = cp.zeros_like(a)
580numba_kernel[4, 256](a, b)
581```

Numba

from numba import cuda

@cuda.jit
def numba_kernel(x, y):
    i = cuda.grid(1)
    if i < x.shape[0]:
        y[i] = x[i] * 2

# CuPy arrays pass directly to Numba kernels — zero copy
a = cp.arange(1000, dtype=cp.float32)
b = cp.zeros_like(a)
numba_kernel[4, 256](a, b)
Raw lines 566 to 582
583### PyTorch
585```python
586import torch
588# CuPy → PyTorch (zero copy via CUDA Array Interface)
589cupy_array = cp.array([1.0, 2.0, 3.0], dtype=cp.float32)
590torch_tensor = torch.as_tensor(cupy_array, device='cuda')
592# PyTorch → CuPy (zero copy)
593cupy_array = cp.asarray(torch_tensor)
595# Via DLPack (also zero copy)
596cupy_array = cp.from_dlpack(torch_tensor)
597torch_tensor = torch.from_dlpack(cupy_array)
598```

PyTorch

import torch

# CuPy → PyTorch (zero copy via CUDA Array Interface)
cupy_array = cp.array([1.0, 2.0, 3.0], dtype=cp.float32)
torch_tensor = torch.as_tensor(cupy_array, device='cuda')

# PyTorch → CuPy (zero copy)
cupy_array = cp.asarray(torch_tensor)

# Via DLPack (also zero copy)
cupy_array = cp.from_dlpack(torch_tensor)
torch_tensor = torch.from_dlpack(cupy_array)
Raw lines 583 to 599
600### cuDF
602```python
603import cudf
605# cuDF → CuPy
606arr = df.to_cupy()
607arr = cp.asarray(df['column'])
609# CuPy → cuDF
610df = cudf.DataFrame(cupy_array)
611s = cudf.Series(cupy_array)
612```

cuDF

import cudf

# cuDF → CuPy
arr = df.to_cupy()
arr = cp.asarray(df['column'])

# CuPy → cuDF
df = cudf.DataFrame(cupy_array)
s = cudf.Series(cupy_array)
Raw lines 600 to 613
614### Raw Pointer Interop
616```python
617# Export pointer
618ptr = cupy_array.data.ptr # Raw device pointer as int
620# Import foreign pointer
621mem = cp.cuda.UnownedMemory(ptr, size_bytes, owner=owner_obj)
622memptr = cp.cuda.MemoryPointer(mem, offset=0)
623arr = cp.ndarray(shape, dtype, memptr=memptr)
624```
626---

Raw Pointer Interop

# Export pointer
ptr = cupy_array.data.ptr  # Raw device pointer as int

# Import foreign pointer
mem = cp.cuda.UnownedMemory(ptr, size_bytes, owner=owner_obj)
memptr = cp.cuda.MemoryPointer(mem, offset=0)
arr = cp.ndarray(shape, dtype, memptr=memptr)

Raw lines 614 to 627
628## Key Differences from NumPy
630These are the behavioral differences that can cause bugs if you're not aware of them.
6321. **Reductions return 0-d arrays, not scalars.** `cp.sum(a)` returns a 0-d `cupy.ndarray`, not a Python float. This avoids implicit GPU-CPU synchronization. Use `.item()` if you need a scalar.
6342. **Out-of-bounds integer-array indexing can wrap silently.** Validate indices rather than relying on NumPy error behavior.
6363. **Duplicate indices in assignment are undefined.** `a[[0, 0]] = [1, 2]` — NumPy stores the last value; CuPy stores an undefined value (GPU race condition).
6384. **Float-to-integer casts differ at edges.** Casting negative float to unsigned int or infinity to int gives different results than NumPy.
6405. **No string/object dtypes.** CuPy only supports numeric types. No structured arrays with string fields.
6426. **CuPy ufuncs require CuPy arrays.** Unlike NumPy ufuncs, CuPy ufuncs don't accept lists or NumPy arrays — convert first.
6447. **Random streams differ.** The same seed does not produce the same sequence across NumPy and CuPy or guarantee identical streams across versions. Use shared input fixtures for numerical comparisons.
646---

Key Differences from NumPy

These are the behavioral differences that can cause bugs if you're not aware of them.

  1. Reductions return 0-d arrays, not scalars. cp.sum(a) returns a 0-d cupy.ndarray, not a Python float. This avoids implicit GPU-CPU synchronization. Use .item() if you need a scalar.
  1. Out-of-bounds integer-array indexing can wrap silently. Validate indices rather than relying on NumPy error behavior.
  1. Duplicate indices in assignment are undefined. a[[0, 0]] = [1, 2] — NumPy stores the last value; CuPy stores an undefined value (GPU race condition).
  1. Float-to-integer casts differ at edges. Casting negative float to unsigned int or infinity to int gives different results than NumPy.
  1. No string/object dtypes. CuPy only supports numeric types. No structured arrays with string fields.
  1. CuPy ufuncs require CuPy arrays. Unlike NumPy ufuncs, CuPy ufuncs don't accept lists or NumPy arrays — convert first.
  1. Random streams differ. The same seed does not produce the same sequence across NumPy and CuPy or guarantee identical streams across versions. Use shared input fixtures for numerical comparisons.

Raw lines 628 to 647
648## Common Pitfalls
6501. **Unsynchronized timing.** Use CUDA events/`cupyx.profiler.benchmark()` for device regions, or explicitly synchronize before and after wall-clock timing.
6522. **Unnecessary round-trips.** Every `cp.asnumpy()` / `.get()` syncs the GPU and copies data across PCI-e. Restructure code to keep data on GPU.
6543. **"Memory leak" from pools.** The memory pool caches freed blocks. `nvidia-smi` shows them as allocated. Use `mempool.free_all_blocks()` to release.
6564. **First-call latency.** CUDA context init + kernel JIT compilation. Warm up before benchmarking.
6585. **Mixing devices.** Using an array from GPU 0 on GPU 1 without explicit transfer can fail or be slow.
6606. **RawKernel ignoring views.** Transposed or sliced arrays passed to RawKernel are treated as the original contiguous layout. You must handle strides manually.
6627. **Forgetting `synchronize()` before reading results.** If you pass data back to CPU or use it in non-CuPy code, ensure the GPU is done first.
664---

Common Pitfalls

  1. Unsynchronized timing. Use CUDA events/cupyx.profiler.benchmark() for device regions, or explicitly synchronize before and after wall-clock timing.
  1. Unnecessary round-trips. Every cp.asnumpy() / .get() syncs the GPU and copies data across PCI-e. Restructure code to keep data on GPU.
  1. "Memory leak" from pools. The memory pool caches freed blocks. nvidia-smi shows them as allocated. Use mempool.free_all_blocks() to release.
  1. First-call latency. CUDA context init + kernel JIT compilation. Warm up before benchmarking.
  1. Mixing devices. Using an array from GPU 0 on GPU 1 without explicit transfer can fail or be slow.
  1. RawKernel ignoring views. Transposed or sliced arrays passed to RawKernel are treated as the original contiguous layout. You must handle strides manually.
  1. **Forgetting synchronize() before reading results.** If you pass data back to CPU or use it in non-CuPy code, ensure the GPU is done first.

Raw lines 648 to 665
666## Environment Variables
668| Variable | Purpose |
669|----------|---------|
670| `CUPY_ACCELERATORS` | Backend list: `cub`, `cutensor` (default: `cub` for v11+) |
671| `CUPY_CACHE_DIR` | Kernel cache directory (default: `~/.cupy/kernel_cache`) |
672| `CUPY_GPU_MEMORY_LIMIT` | GPU memory limit (bytes or `"50%"`) |
673| `CUPY_CACHE_SAVE_CUDA_SOURCE` | Set `1` to dump kernel source for profiling |
674| `CUPY_CUDA_PER_THREAD_DEFAULT_STREAM` | Set `1` for per-thread default streams |

Environment Variables

VariablePurpose
CUPY_ACCELERATORSBackend list: cub, cutensor (default: cub for v11+)
CUPY_CACHE_DIRKernel cache directory (default: ~/.cupy/kernel_cache)
CUPY_GPU_MEMORY_LIMITGPU memory limit (bytes or "50%")
CUPY_CACHE_SAVE_CUDA_SOURCESet 1 to dump kernel source for profiling
CUPY_CUDA_PER_THREAD_DEFAULT_STREAMSet 1 for per-thread default streams
Raw lines 666 to 674