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

numba.md

825 lines · skills/optimize-for-gpu/references/numba.md at 5f2bc03

Open on GitHub

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

1# Numba CUDA 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
6The established Numba-CUDA target compiles Python into CUDA kernels with explicit control over
7threads, blocks, shared memory, and synchronization. It is now in maintenance mode. For new kernel
8projects, evaluate Numba-CUDA-MLIR first; use this reference for existing `numba.cuda` code,
9compatibility work, or features not yet available in the MLIR implementation.
11> **Full documentation:** https://nvidia.github.io/numba-cuda/
12> **New-development path:** https://nvidia.github.io/numba-cuda-mlir/

Numba CUDA 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.

The established Numba-CUDA target compiles Python into CUDA kernels with explicit control over threads, blocks, shared memory, and synchronization. It is now in maintenance mode. For new kernel projects, evaluate Numba-CUDA-MLIR first; use this reference for existing numba.cuda code, compatibility work, or features not yet available in the MLIR implementation.

Full documentation: https://nvidia.github.io/numba-cuda/ New-development path: https://nvidia.github.io/numba-cuda-mlir/
Raw lines 1 to 13
14## Table of Contents
161. [Installation and Setup](#installation-and-setup)
172. [Core Concepts: Kernels, Threads, Blocks, Grids](#core-concepts)
183. [Writing CUDA Kernels](#writing-cuda-kernels)
194. [Thread Positioning](#thread-positioning)
205. [Memory Management](#memory-management)
216. [Shared Memory](#shared-memory)
227. [Device Functions](#device-functions)
238. [Atomic Operations](#atomic-operations)
249. [GPU Ufuncs: @vectorize and @guvectorize](#gpu-ufuncs)
2510. [GPU Reductions](#gpu-reductions)
2611. [Streams and Async Operations](#streams)
2712. [Random Number Generation](#random-number-generation)
2813. [Cooperative Groups](#cooperative-groups)
2914. [Common Patterns for Scientific Computing](#common-patterns)
3015. [Performance Optimization](#performance-optimization)
3116. [Debugging](#debugging)
3217. [Interoperability](#interoperability)
3318. [Common Pitfalls](#common-pitfalls)
35---

Table of Contents

  1. Installation and Setup
  2. Core Concepts: Kernels, Threads, Blocks, Grids
  3. Writing CUDA Kernels
  4. Thread Positioning
  5. Memory Management
  6. Shared Memory
  7. Device Functions
  8. Atomic Operations
  9. GPU Ufuncs: @vectorize and @guvectorize
  10. GPU Reductions
  11. Streams and Async Operations
  12. Random Number Generation
  13. Cooperative Groups
  14. Common Patterns for Scientific Computing
  15. Performance Optimization
  16. Debugging
  17. Interoperability
  18. Common Pitfalls

Raw lines 14 to 36
37## Installation and Setup
39Use `uv add` in standalone examples; follow the user's existing project package manager when one
40is already configured.
42```bash
43uv add "numba-cuda[cu12]==0.30.*" # For CUDA 12.x (pulls in numba and CUDA components)
44uv add "numba-cuda[cu13]==0.30.*" # For CUDA 13.x
45```
47The NVIDIA `numba-cuda` package is the out-of-tree implementation of the established
48`numba.cuda` target (Numba's built-in target is deprecated). It keeps the `numba.cuda` namespace
49and depends on `numba`, so a single install command suffices. NVIDIA limits this implementation to
50security and critical fixes through the CUDA 13 lifetime; new feature development targets the
51separate `numba-cuda-mlir` package. Follow its migration guide before choosing a compiler for new
52code because feature coverage continues to evolve.
54**Requirements:** CUDA Toolkit 12 or 13. GPU with Compute Capability >= 5.0 (Maxwell or newer) on CUDA 12, or >= 7.5 (Turing or newer) on CUDA 13.
56```python
57from numba import cuda
59# Verify GPU is available
60print(cuda.is_available()) # True if CUDA works
61cuda.detect() # Prints GPU details
62```
64---

Installation and Setup

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

uv add "numba-cuda[cu12]==0.30.*"    # For CUDA 12.x (pulls in numba and CUDA components)
uv add "numba-cuda[cu13]==0.30.*"    # For CUDA 13.x

The NVIDIA numba-cuda package is the out-of-tree implementation of the established numba.cuda target (Numba's built-in target is deprecated). It keeps the numba.cuda namespace and depends on numba, so a single install command suffices. NVIDIA limits this implementation to security and critical fixes through the CUDA 13 lifetime; new feature development targets the separate numba-cuda-mlir package. Follow its migration guide before choosing a compiler for new code because feature coverage continues to evolve.

Requirements: CUDA Toolkit 12 or 13. GPU with Compute Capability >= 5.0 (Maxwell or newer) on CUDA 12, or >= 7.5 (Turing or newer) on CUDA 13.

from numba import cuda

# Verify GPU is available
print(cuda.is_available())   # True if CUDA works
cuda.detect()                # Prints GPU details

Raw lines 37 to 65
66## Core Concepts
68CUDA organizes parallel execution in a hierarchy:
70```
71Grid (of blocks) → Blocks (of threads) → Threads
72```
74- **Thread**: The smallest unit of execution. Each runs your kernel function.
75- **Block**: A group of threads that can share fast on-chip memory and synchronize with each other. Max 1024 threads per block.
76- **Grid**: The collection of all blocks for a kernel launch.
78A **kernel** is a function that runs on the GPU, launched from the CPU. A **device function** runs on the GPU but is called from other GPU code (not from CPU).
80---

Core Concepts

CUDA organizes parallel execution in a hierarchy:

Grid (of blocks) → Blocks (of threads) → Threads
  • Thread: The smallest unit of execution. Each runs your kernel function.
  • Block: A group of threads that can share fast on-chip memory and synchronize with each other. Max 1024 threads per block.
  • Grid: The collection of all blocks for a kernel launch.

A kernel is a function that runs on the GPU, launched from the CPU. A device function runs on the GPU but is called from other GPU code (not from CPU).


Raw lines 66 to 81
82## Writing CUDA Kernels

Writing CUDA Kernels

Raw lines 82 to 83
84### The @cuda.jit Decorator
86```python
87from numba import cuda
89@cuda.jit
90def my_kernel(input_array, output_array):
91 i = cuda.grid(1) # Get this thread's global index
92 if i < input_array.size: # Bounds check — ALWAYS do this
93 output_array[i] = input_array[i] * 2.0
94```
96**Key parameters for @cuda.jit:**
98| Parameter | Purpose |
99|-----------|---------|
100| `device=True` | Makes this a device function (callable from GPU only, can return values) |
101| `fastmath=True` | Enables fast math (fast sqrt, division, FMA, trig/exp/log approximations on float32). Use when IEEE-754 strictness isn't required |
102| `max_registers=N` | Limits registers per thread to increase occupancy |
103| `cache=True` | Caches compiled kernel to disk |
104| `debug=True` | Enables exception checking (slow — for debugging only, pair with `opt=False`) |
105| `lineinfo=True` | Source line info for profiling without full debug overhead |

The @cuda.jit Decorator

from numba import cuda

@cuda.jit
def my_kernel(input_array, output_array):
    i = cuda.grid(1)                    # Get this thread's global index
    if i < input_array.size:            # Bounds check — ALWAYS do this
        output_array[i] = input_array[i] * 2.0

Key parameters for @cuda.jit:

ParameterPurpose
device=TrueMakes this a device function (callable from GPU only, can return values)
fastmath=TrueEnables fast math (fast sqrt, division, FMA, trig/exp/log approximations on float32). Use when IEEE-754 strictness isn't required
max_registers=NLimits registers per thread to increase occupancy
cache=TrueCaches compiled kernel to disk
debug=TrueEnables exception checking (slow — for debugging only, pair with opt=False)
lineinfo=TrueSource line info for profiling without full debug overhead
Raw lines 84 to 106
107### Launching Kernels
109```python
110import numpy as np
111from numba import cuda
113data = np.random.rand(1_000_000).astype(np.float32)
114out = np.zeros_like(data)
116# Transfer to GPU
117d_data = cuda.to_device(data)
118d_out = cuda.device_array_like(out)
120# Calculate launch configuration
121threads_per_block = 256
122blocks_per_grid = (data.size + threads_per_block - 1) // threads_per_block
124# Launch
125my_kernel[blocks_per_grid, threads_per_block](d_data, d_out)
127# Get results back
128result = d_out.copy_to_host()
129```
131**Launch syntax:** `kernel[grid_dim, block_dim, stream, dynamic_shared_mem_bytes](...args)`
133The 3rd and 4th parameters are optional (stream and dynamic shared memory size in bytes).

Launching Kernels

import numpy as np
from numba import cuda

data = np.random.rand(1_000_000).astype(np.float32)
out = np.zeros_like(data)

# Transfer to GPU
d_data = cuda.to_device(data)
d_out = cuda.device_array_like(out)

# Calculate launch configuration
threads_per_block = 256
blocks_per_grid = (data.size + threads_per_block - 1) // threads_per_block

# Launch
my_kernel[blocks_per_grid, threads_per_block](d_data, d_out)

# Get results back
result = d_out.copy_to_host()

Launch syntax: kernel[grid_dim, block_dim, stream, dynamic_shared_mem_bytes](...args)

The 3rd and 4th parameters are optional (stream and dynamic shared memory size in bytes).

Raw lines 107 to 134
135### 2D Launch Configuration
137```python
138@cuda.jit
139def kernel_2d(matrix, output):
140 x, y = cuda.grid(2)
141 if x < matrix.shape[0] and y < matrix.shape[1]:
142 output[x, y] = matrix[x, y] * 2.0
144threads = (16, 16)
145blocks = (
146 (matrix.shape[0] + threads[0] - 1) // threads[0],
147 (matrix.shape[1] + threads[1] - 1) // threads[1],
149kernel_2d[blocks, threads](d_matrix, d_output)
150```

2D Launch Configuration

@cuda.jit
def kernel_2d(matrix, output):
    x, y = cuda.grid(2)
    if x < matrix.shape[0] and y < matrix.shape[1]:
        output[x, y] = matrix[x, y] * 2.0

threads = (16, 16)
blocks = (
    (matrix.shape[0] + threads[0] - 1) // threads[0],
    (matrix.shape[1] + threads[1] - 1) // threads[1],
)
kernel_2d[blocks, threads](d_matrix, d_output)
Raw lines 135 to 151
152### Convenience: .forall() for 1D
154```python
155# Automatically computes grid dimensions for 1D
156my_kernel.forall(len(data))(d_data, d_out)
157```

Convenience: .forall() for 1D

# Automatically computes grid dimensions for 1D
my_kernel.forall(len(data))(d_data, d_out)
Raw lines 152 to 158
159### Critical Rules for Kernels
1611. **Kernels CANNOT return values.** All output must be written to arrays passed as arguments.
1622. **Always check array bounds.** If grid_size > array_size, out-of-bounds threads corrupt memory silently.
1633. **Kernel launches are asynchronous.** Use `cuda.synchronize()` before reading results on the CPU.
165---

Critical Rules for Kernels

  1. Kernels CANNOT return values. All output must be written to arrays passed as arguments.
  2. Always check array bounds. If grid_size > array_size, out-of-bounds threads corrupt memory silently.
  3. Kernel launches are asynchronous. Use cuda.synchronize() before reading results on the CPU.

Raw lines 159 to 166
167## Thread Positioning

Thread Positioning

Raw lines 167 to 168
169### Intrinsics
171| Intrinsic | Description |
172|-----------|-------------|
173| `cuda.threadIdx.x/y/z` | Thread index within its block |
174| `cuda.blockIdx.x/y/z` | Block index within the grid |
175| `cuda.blockDim.x/y/z` | Threads per block |
176| `cuda.gridDim.x/y/z` | Blocks in the grid |
177| `cuda.grid(ndim)` | Absolute position in entire grid (1D → int, 2D/3D → tuple) |
178| `cuda.gridsize(ndim)` | Total number of threads in entire grid |

Intrinsics

IntrinsicDescription
cuda.threadIdx.x/y/zThread index within its block
cuda.blockIdx.x/y/zBlock index within the grid
cuda.blockDim.x/y/zThreads per block
cuda.gridDim.x/y/zBlocks in the grid
cuda.grid(ndim)Absolute position in entire grid (1D → int, 2D/3D → tuple)
cuda.gridsize(ndim)Total number of threads in entire grid
Raw lines 169 to 179
180### Grid-Stride Loop Pattern
182For processing data larger than the grid, use a grid-stride loop. This decouples grid size from problem size and is essential for reusing RNG states.
184```python
185@cuda.jit
186def process_large(data, out):
187 start = cuda.grid(1)
188 stride = cuda.gridsize(1)
189 for i in range(start, data.shape[0], stride):
190 out[i] = data[i] * 2.0
191```
193---

Grid-Stride Loop Pattern

For processing data larger than the grid, use a grid-stride loop. This decouples grid size from problem size and is essential for reusing RNG states.

@cuda.jit
def process_large(data, out):
    start = cuda.grid(1)
    stride = cuda.gridsize(1)
    for i in range(start, data.shape[0], stride):
        out[i] = data[i] * 2.0

Raw lines 180 to 194
195## Memory Management

Memory Management

Raw lines 195 to 196
197### Data Transfer
199```python
200# Host → Device
201d_array = cuda.to_device(numpy_array) # Synchronous copy
202d_array = cuda.to_device(numpy_array, stream=stream) # Async copy
204# Allocate on device (no copy)
205d_array = cuda.device_array(shape=(1000,), dtype=np.float32)
206d_array = cuda.device_array_like(numpy_array)
208# Device → Host
209host_array = d_array.copy_to_host() # New array
210d_array.copy_to_host(existing_array) # Into pre-allocated
211d_array.copy_to_host(stream=stream) # Async
212```

Data Transfer

# Host → Device
d_array = cuda.to_device(numpy_array)                    # Synchronous copy
d_array = cuda.to_device(numpy_array, stream=stream)     # Async copy

# Allocate on device (no copy)
d_array = cuda.device_array(shape=(1000,), dtype=np.float32)
d_array = cuda.device_array_like(numpy_array)

# Device → Host
host_array = d_array.copy_to_host()                      # New array
d_array.copy_to_host(existing_array)                     # Into pre-allocated
d_array.copy_to_host(stream=stream)                      # Async
Raw lines 197 to 213
214### Memory Types
216| Type | API | Use Case |
217|------|-----|----------|
218| **Device** | `cuda.device_array()`, `cuda.to_device()` | Standard GPU memory |
219| **Pinned** | `cuda.pinned_array()`, `cuda.pinned()` context manager | Page-locked host memory — faster transfers |
220| **Mapped** | `cuda.mapped_array()` | Accessible from both host and device |
221| **Managed** | `cuda.managed_array()` | Unified memory — auto-migrates between host/device (Linux/x86 recommended) |
222| **Constant** | `cuda.const.array_like(arr)` | Read-only, cached, set from host |

Memory Types

TypeAPIUse Case
Devicecuda.device_array(), cuda.to_device()Standard GPU memory
Pinnedcuda.pinned_array(), cuda.pinned() context managerPage-locked host memory — faster transfers
Mappedcuda.mapped_array()Accessible from both host and device
Managedcuda.managed_array()Unified memory — auto-migrates between host/device (Linux/x86 recommended)
Constantcuda.const.array_like(arr)Read-only, cached, set from host
Raw lines 214 to 223
224### Pinned Memory for Fast Transfers
226```python
227# Allocate pinned host memory (page-locked — faster PCI-e transfers)
228with cuda.pinned(host_array):
229 d_array = cuda.to_device(host_array, stream=stream)
230 # Transfer is faster because the OS can't page this memory out
232# Or allocate directly
233pinned = cuda.pinned_array(shape=(1000,), dtype=np.float32)
234```

Pinned Memory for Fast Transfers

# Allocate pinned host memory (page-locked — faster PCI-e transfers)
with cuda.pinned(host_array):
    d_array = cuda.to_device(host_array, stream=stream)
    # Transfer is faster because the OS can't page this memory out

# Or allocate directly
pinned = cuda.pinned_array(shape=(1000,), dtype=np.float32)
Raw lines 224 to 235
236### Deallocation Control
238```python
239with cuda.defer_cleanup():
240 # All GPU deallocation deferred here — avoids implicit synchronization
241 # Use this in performance-critical sections
242 run_many_kernels()
243# Cleanup happens here
244```
246---

Deallocation Control

with cuda.defer_cleanup():
    # All GPU deallocation deferred here — avoids implicit synchronization
    # Use this in performance-critical sections
    run_many_kernels()
# Cleanup happens here

Raw lines 236 to 247
248## Shared Memory
250Shared memory is fast on-chip memory (tens of TB/s bandwidth) shared within a block. It's the key to high-performance kernels — use it to cache data that multiple threads in a block will access.

Shared Memory

Shared memory is fast on-chip memory (tens of TB/s bandwidth) shared within a block. It's the key to high-performance kernels — use it to cache data that multiple threads in a block will access.

Raw lines 248 to 251
252### Static Shared Memory (size known at compile time)
254```python
255from numba import cuda, float32
257@cuda.jit
258def kernel_with_shared(data, output):
259 # Allocate shared memory — visible to all threads in this block
260 shared = cuda.shared.array(256, dtype=float32)
262 tid = cuda.threadIdx.x
263 i = cuda.grid(1)
265 # Each thread loads one element into shared memory
266 if i < data.size:
267 shared[tid] = data[i]
268 else:
269 shared[tid] = 0.0
271 # BARRIER: wait for ALL threads in block to finish loading
272 cuda.syncthreads()
274 # Now safe to read any element in shared[]
275 if i < data.size:
276 output[i] = shared[tid] + (shared[tid - 1] if tid > 0 else 0.0)
277```

Static Shared Memory (size known at compile time)

from numba import cuda, float32

@cuda.jit
def kernel_with_shared(data, output):
    # Allocate shared memory — visible to all threads in this block
    shared = cuda.shared.array(256, dtype=float32)

    tid = cuda.threadIdx.x
    i = cuda.grid(1)

    # Each thread loads one element into shared memory
    if i < data.size:
        shared[tid] = data[i]
    else:
        shared[tid] = 0.0

    # BARRIER: wait for ALL threads in block to finish loading
    cuda.syncthreads()

    # Now safe to read any element in shared[]
    if i < data.size:
        output[i] = shared[tid] + (shared[tid - 1] if tid > 0 else 0.0)
Raw lines 252 to 278
279### Dynamic Shared Memory (size set at launch)
281```python
282@cuda.jit
283def kernel_dynamic_shared(data):
284 # size=0 means "use dynamic shared memory"
285 dyn = cuda.shared.array(0, dtype=float32)
286 tid = cuda.threadIdx.x
287 i = cuda.grid(1)
288 dyn[tid] = data[i] if i < data.size else 0.0
289 cuda.syncthreads()
290 # ...
292# Specify size at launch (4th parameter = bytes)
293kernel_dynamic_shared[blocks, threads, stream, 1024](data) # 1024 bytes of shared mem
294```
296**Important:** All `cuda.shared.array(0, ...)` calls in the same kernel alias the same memory region. To use multiple dynamic shared arrays, take disjoint slices manually.

Dynamic Shared Memory (size set at launch)

@cuda.jit
def kernel_dynamic_shared(data):
    # size=0 means "use dynamic shared memory"
    dyn = cuda.shared.array(0, dtype=float32)
    tid = cuda.threadIdx.x
    i = cuda.grid(1)
    dyn[tid] = data[i] if i < data.size else 0.0
    cuda.syncthreads()
    # ...

# Specify size at launch (4th parameter = bytes)
kernel_dynamic_shared[blocks, threads, stream, 1024](data)  # 1024 bytes of shared mem

Important: All cuda.shared.array(0, ...) calls in the same kernel alias the same memory region. To use multiple dynamic shared arrays, take disjoint slices manually.

Raw lines 279 to 297
298### Local Memory (per-thread scratchpad)
300```python
301@cuda.jit
302def kernel_with_local(data):
303 # Each thread gets its own private array
304 local_buf = cuda.local.array(10, dtype=float32)
305 i = cuda.grid(1)
306 for j in range(10):
307 idx = i * 10 + j
308 local_buf[j] = data[idx] if idx < data.size else 0.0
309 # Process local_buf...
310```
312---

Local Memory (per-thread scratchpad)

@cuda.jit
def kernel_with_local(data):
    # Each thread gets its own private array
    local_buf = cuda.local.array(10, dtype=float32)
    i = cuda.grid(1)
    for j in range(10):
        idx = i * 10 + j
        local_buf[j] = data[idx] if idx < data.size else 0.0
    # Process local_buf...

Raw lines 298 to 313
314## Device Functions
316Device functions run on the GPU and are called from kernels or other device functions. Unlike kernels, they **can return values**.
318```python
319@cuda.jit(device=True)
320def compute_distance(x1, y1, x2, y2):
321 return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
323@cuda.jit
324def kernel(points, distances):
325 i = cuda.grid(1)
326 if i < points.shape[0] - 1:
327 distances[i] = compute_distance(
328 points[i, 0], points[i, 1],
329 points[i+1, 0], points[i+1, 1]
330 )
331```
333CPU `@numba.jit` dispatchers are not CUDA device functions. If CPU and GPU paths need the same
334formula, keep a small undecorated source function and create explicit CPU (`@njit`) and GPU
335(`@cuda.jit(device=True)`) implementations or wrappers; test them against the same fixtures.
337---

Device Functions

Device functions run on the GPU and are called from kernels or other device functions. Unlike kernels, they can return values.

@cuda.jit(device=True)
def compute_distance(x1, y1, x2, y2):
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

@cuda.jit
def kernel(points, distances):
    i = cuda.grid(1)
    if i < points.shape[0] - 1:
        distances[i] = compute_distance(
            points[i, 0], points[i, 1],
            points[i+1, 0], points[i+1, 1]
        )

CPU @numba.jit dispatchers are not CUDA device functions. If CPU and GPU paths need the same formula, keep a small undecorated source function and create explicit CPU (@njit) and GPU (@cuda.jit(device=True)) implementations or wrappers; test them against the same fixtures.


Raw lines 314 to 338
339## Atomic Operations
341Atomics ensure thread-safe updates to shared data. All return the **old** value.
343```python
344cuda.atomic.add(array, index, value) # += (int32, float32, float64)
345cuda.atomic.sub(array, index, value) # -= (int32, float32, float64)
346cuda.atomic.max(array, index, value) # max (int/uint 32/64, float 32/64)
347cuda.atomic.min(array, index, value) # min (same types)
348cuda.atomic.nanmax(array, index, value) # max ignoring NaN
349cuda.atomic.nanmin(array, index, value) # min ignoring NaN
350cuda.atomic.and_(array, index, value) # &= (int/uint 32/64)
351cuda.atomic.or_(array, index, value) # |= (int/uint 32/64)
352cuda.atomic.xor(array, index, value) # ^= (int/uint 32/64)
353cuda.atomic.exch(array, index, value) # exchange
354cuda.atomic.cas(array, index, old, value) # compare-and-swap
355```
357Multi-dimensional indexing works via tuples: `cuda.atomic.add(result, (row, col), value)`

Atomic Operations

Atomics ensure thread-safe updates to shared data. All return the old value.

cuda.atomic.add(array, index, value)       # +=   (int32, float32, float64)
cuda.atomic.sub(array, index, value)       # -=   (int32, float32, float64)
cuda.atomic.max(array, index, value)       # max  (int/uint 32/64, float 32/64)
cuda.atomic.min(array, index, value)       # min  (same types)
cuda.atomic.nanmax(array, index, value)    # max ignoring NaN
cuda.atomic.nanmin(array, index, value)    # min ignoring NaN
cuda.atomic.and_(array, index, value)      # &=   (int/uint 32/64)
cuda.atomic.or_(array, index, value)       # |=   (int/uint 32/64)
cuda.atomic.xor(array, index, value)       # ^=   (int/uint 32/64)
cuda.atomic.exch(array, index, value)      # exchange
cuda.atomic.cas(array, index, old, value)  # compare-and-swap

Multi-dimensional indexing works via tuples: cuda.atomic.add(result, (row, col), value)

Raw lines 339 to 358
359### Example: Histogram
361```python
362@cuda.jit
363def histogram(data, bins, min_value, max_value):
364 i = cuda.grid(1)
365 if i < data.size:
366 value = data[i]
367 n_bins = bins.size
368 if min_value <= value <= max_value:
369 bin_idx = int((value - min_value) * n_bins / (max_value - min_value))
370 if bin_idx == n_bins: # Include the rightmost edge.
371 bin_idx = n_bins - 1
372 cuda.atomic.add(bins, bin_idx, 1)
373```
375Validate `max_value > min_value` on the host before launch. For production histograms, prefer
376CuPy/CUB unless a custom binning rule is required.
378---

Example: Histogram

@cuda.jit
def histogram(data, bins, min_value, max_value):
    i = cuda.grid(1)
    if i < data.size:
        value = data[i]
        n_bins = bins.size
        if min_value <= value <= max_value:
            bin_idx = int((value - min_value) * n_bins / (max_value - min_value))
            if bin_idx == n_bins:  # Include the rightmost edge.
                bin_idx = n_bins - 1
            cuda.atomic.add(bins, bin_idx, 1)

Validate max_value > min_value on the host before launch. For production histograms, prefer CuPy/CUB unless a custom binning rule is required.


Raw lines 359 to 379
380## GPU Ufuncs
382### @vectorize — Element-wise Operations on GPU
384The simplest way to run element-wise operations on GPU. Write a scalar function, Numba broadcasts it over arrays automatically.
386```python
387from numba import vectorize, float32, float64
388import math
390@vectorize([float32(float32, float32),
391 float64(float64, float64)],
392 target='cuda')
393def gpu_hypot(x, y):
394 return math.sqrt(x**2 + y**2)
396# Usage — just call it like a NumPy ufunc
397result = gpu_hypot(array_x, array_y)
399# Pass device arrays to avoid transfers
400d_x = cuda.to_device(x)
401d_y = cuda.to_device(y)
402d_result = gpu_hypot(d_x, d_y)
403```

@vectorize — Element-wise Operations on GPU

The simplest way to run element-wise operations on GPU. Write a scalar function, Numba broadcasts it over arrays automatically.

from numba import vectorize, float32, float64
import math

@vectorize([float32(float32, float32),
            float64(float64, float64)],
           target='cuda')
def gpu_hypot(x, y):
    return math.sqrt(x**2 + y**2)

# Usage — just call it like a NumPy ufunc
result = gpu_hypot(array_x, array_y)

# Pass device arrays to avoid transfers
d_x = cuda.to_device(x)
d_y = cuda.to_device(y)
d_result = gpu_hypot(d_x, d_y)
Raw lines 382 to 404
405### @guvectorize — Generalized Ufuncs
407For operations on sub-arrays (not just scalars). Uses NumPy's generalized ufunc signature.
409```python
410from numba import guvectorize, float32
412@guvectorize([(float32[:,:], float32[:,:], float32[:,:])],
413 '(m,n),(n,p)->(m,p)', target='cuda')
414def gpu_matmul(A, B, C):
415 for i in range(A.shape[0]):
416 for j in range(B.shape[1]):
417 total = 0.0
418 for k in range(A.shape[1]):
419 total += A[i, k] * B[k, j]
420 C[i, j] = total
421```
423---

@guvectorize — Generalized Ufuncs

For operations on sub-arrays (not just scalars). Uses NumPy's generalized ufunc signature.

from numba import guvectorize, float32

@guvectorize([(float32[:,:], float32[:,:], float32[:,:])],
             '(m,n),(n,p)->(m,p)', target='cuda')
def gpu_matmul(A, B, C):
    for i in range(A.shape[0]):
        for j in range(B.shape[1]):
            total = 0.0
            for k in range(A.shape[1]):
                total += A[i, k] * B[k, j]
            C[i, j] = total

Raw lines 405 to 424
425## GPU Reductions
427```python
428from numba import cuda
430# Define reduction operation
431sum_reduce = cuda.reduce(lambda a, b: a + b)
433# Use it
434result = sum_reduce(array) # Full reduction
435result = sum_reduce(array, init=0) # With initial value
436sum_reduce(array, res=device_result) # Write to device array (no D→H copy)
437sum_reduce(array, stream=stream) # Async
438```
440Custom reduction:
442```python
443@cuda.reduce
444def max_reduce(a, b):
445 return a if a > b else b
447maximum = max_reduce(data_array)
448```
450---

GPU Reductions

from numba import cuda

# Define reduction operation
sum_reduce = cuda.reduce(lambda a, b: a + b)

# Use it
result = sum_reduce(array)                    # Full reduction
result = sum_reduce(array, init=0)            # With initial value
sum_reduce(array, res=device_result)          # Write to device array (no D→H copy)
sum_reduce(array, stream=stream)              # Async

Custom reduction:

@cuda.reduce
def max_reduce(a, b):
    return a if a > b else b

maximum = max_reduce(data_array)

Raw lines 425 to 451
452## Streams
454Streams enable overlapping computation with data transfer and running multiple kernels concurrently.
456```python
457stream = cuda.stream()
459# Async transfer → kernel → transfer back
460d_data = cuda.to_device(host_data, stream=stream)
461my_kernel[blocks, threads, stream](d_data, d_out)
462result = d_out.copy_to_host(stream=stream)
463stream.synchronize() # Wait for everything on this stream
465# Context manager that auto-synchronizes
466with stream.auto_synchronize():
467 d_data = cuda.to_device(host_data, stream=stream)
468 my_kernel[blocks, threads, stream](d_data, d_out)
469 result = d_out.copy_to_host(stream=stream)
470# Synchronizes here automatically
471```

Streams

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

stream = cuda.stream()

# Async transfer → kernel → transfer back
d_data = cuda.to_device(host_data, stream=stream)
my_kernel[blocks, threads, stream](d_data, d_out)
result = d_out.copy_to_host(stream=stream)
stream.synchronize()  # Wait for everything on this stream

# Context manager that auto-synchronizes
with stream.auto_synchronize():
    d_data = cuda.to_device(host_data, stream=stream)
    my_kernel[blocks, threads, stream](d_data, d_out)
    result = d_out.copy_to_host(stream=stream)
# Synchronizes here automatically
Raw lines 452 to 472
473### Pipeline Pattern (overlap transfer and compute)
475```python
476stream1 = cuda.stream()
477stream2 = cuda.stream()
479# Chunk 1: transfer on stream1
480d_chunk1 = cuda.to_device(data[:half], stream=stream1)
481# Chunk 2: transfer on stream2 (overlaps with stream1 transfer)
482d_chunk2 = cuda.to_device(data[half:], stream=stream2)
484# Process chunk1 on stream1
485kernel[blocks, threads, stream1](d_chunk1, d_out1)
486# Process chunk2 on stream2 (overlaps with stream1 compute)
487kernel[blocks, threads, stream2](d_chunk2, d_out2)
489cuda.synchronize() # Wait for all streams
490```
492---

Pipeline Pattern (overlap transfer and compute)

stream1 = cuda.stream()
stream2 = cuda.stream()

# Chunk 1: transfer on stream1
d_chunk1 = cuda.to_device(data[:half], stream=stream1)
# Chunk 2: transfer on stream2 (overlaps with stream1 transfer)
d_chunk2 = cuda.to_device(data[half:], stream=stream2)

# Process chunk1 on stream1
kernel[blocks, threads, stream1](d_chunk1, d_out1)
# Process chunk2 on stream2 (overlaps with stream1 compute)
kernel[blocks, threads, stream2](d_chunk2, d_out2)

cuda.synchronize()  # Wait for all streams

Raw lines 473 to 493
494## Random Number Generation
496Numba provides GPU-native random number generation using the xoroshiro128+ algorithm.
498```python
499from numba import cuda
500from numba.cuda.random import (
501 create_xoroshiro128p_states,
502 xoroshiro128p_uniform_float32,
503 xoroshiro128p_uniform_float64,
504 xoroshiro128p_normal_float32,
505 xoroshiro128p_normal_float64,
508# Create RNG states — one per thread
509n_threads = 256 * 128
510rng_states = create_xoroshiro128p_states(n_threads, seed=42)
512@cuda.jit
513def monte_carlo_pi(rng_states, iterations, out):
514 gid = cuda.grid(1)
515 if gid < out.size:
516 inside = 0
517 for _ in range(iterations):
518 x = xoroshiro128p_uniform_float32(rng_states, gid)
519 y = xoroshiro128p_uniform_float32(rng_states, gid)
520 if x**2 + y**2 <= 1.0:
521 inside += 1
522 out[gid] = inside / iterations * 4.0
524monte_carlo_pi[128, 256](rng_states, 10000, d_out)
525```
527**Tip:** RNG states consume memory proportional to thread count. Use grid-stride loops to limit the number of states needed for large problems.
529---

Random Number Generation

Numba provides GPU-native random number generation using the xoroshiro128+ algorithm.

from numba import cuda
from numba.cuda.random import (
    create_xoroshiro128p_states,
    xoroshiro128p_uniform_float32,
    xoroshiro128p_uniform_float64,
    xoroshiro128p_normal_float32,
    xoroshiro128p_normal_float64,
)

# Create RNG states — one per thread
n_threads = 256 * 128
rng_states = create_xoroshiro128p_states(n_threads, seed=42)

@cuda.jit
def monte_carlo_pi(rng_states, iterations, out):
    gid = cuda.grid(1)
    if gid < out.size:
        inside = 0
        for _ in range(iterations):
            x = xoroshiro128p_uniform_float32(rng_states, gid)
            y = xoroshiro128p_uniform_float32(rng_states, gid)
            if x**2 + y**2 <= 1.0:
                inside += 1
        out[gid] = inside / iterations * 4.0

monte_carlo_pi[128, 256](rng_states, 10000, d_out)

Tip: RNG states consume memory proportional to thread count. Use grid-stride loops to limit the number of states needed for large problems.


Raw lines 494 to 530
531## Cooperative Groups
533For algorithms requiring synchronization across ALL blocks in a grid (not just within a single block).
535```python
536@cuda.jit
537def iterative_kernel(M):
538 col = cuda.grid(1)
539 g = cuda.cg.this_grid() # Get grid group
541 for row in range(1, M.shape[0]):
542 if col < M.shape[1]:
543 M[row, col] = M[row - 1, col] + 1
544 g.sync() # Global barrier — all blocks wait here
546# Query max grid size for cooperative launch
547overload = iterative_kernel.overloads[signature]
548max_blocks = overload.max_cooperative_grid_blocks(block_dim)
549```
551Cooperative launches are triggered automatically when `g.sync()` is detected. The grid must not exceed `max_cooperative_grid_blocks()`.
553---

Cooperative Groups

For algorithms requiring synchronization across ALL blocks in a grid (not just within a single block).

@cuda.jit
def iterative_kernel(M):
    col = cuda.grid(1)
    g = cuda.cg.this_grid()  # Get grid group

    for row in range(1, M.shape[0]):
        if col < M.shape[1]:
            M[row, col] = M[row - 1, col] + 1
        g.sync()  # Global barrier — all blocks wait here

# Query max grid size for cooperative launch
overload = iterative_kernel.overloads[signature]
max_blocks = overload.max_cooperative_grid_blocks(block_dim)

Cooperative launches are triggered automatically when g.sync() is detected. The grid must not exceed max_cooperative_grid_blocks().


Raw lines 531 to 554
555## Common Patterns

Common Patterns

Raw lines 555 to 556
557### Tiled Matrix Multiplication with Shared Memory
559This is the canonical example of shared memory optimization — tiles of A and B are loaded into fast shared memory to reduce slow global memory accesses.
561```python
562from numba import cuda, float32
563import numpy as np
565TPB = 16 # Tile/block size
567@cuda.jit
568def matmul_shared(A, B, C):
569 sA = cuda.shared.array((TPB, TPB), dtype=float32)
570 sB = cuda.shared.array((TPB, TPB), dtype=float32)
572 x, y = cuda.grid(2)
573 tx, ty = cuda.threadIdx.x, cuda.threadIdx.y
575 tmp = float32(0.0)
576 n_tiles = (A.shape[1] + TPB - 1) // TPB
577 for tile in range(n_tiles):
578 # Load tile into shared memory (with bounds check)
579 col = tx + tile * TPB
580 row = ty + tile * TPB
581 sA[ty, tx] = A[y, col] if (y < A.shape[0] and col < A.shape[1]) else 0
582 sB[ty, tx] = B[row, x] if (x < B.shape[1] and row < B.shape[0]) else 0
583 cuda.syncthreads()
585 # Compute partial dot product from this tile
586 for k in range(TPB):
587 tmp += sA[ty, k] * sB[k, tx]
588 cuda.syncthreads()
590 if y < C.shape[0] and x < C.shape[1]:
591 C[y, x] = tmp
592```

Tiled Matrix Multiplication with Shared Memory

This is the canonical example of shared memory optimization — tiles of A and B are loaded into fast shared memory to reduce slow global memory accesses.

from numba import cuda, float32
import numpy as np

TPB = 16  # Tile/block size

@cuda.jit
def matmul_shared(A, B, C):
    sA = cuda.shared.array((TPB, TPB), dtype=float32)
    sB = cuda.shared.array((TPB, TPB), dtype=float32)

    x, y = cuda.grid(2)
    tx, ty = cuda.threadIdx.x, cuda.threadIdx.y

    tmp = float32(0.0)
    n_tiles = (A.shape[1] + TPB - 1) // TPB
    for tile in range(n_tiles):
        # Load tile into shared memory (with bounds check)
        col = tx + tile * TPB
        row = ty + tile * TPB
        sA[ty, tx] = A[y, col] if (y < A.shape[0] and col < A.shape[1]) else 0
        sB[ty, tx] = B[row, x] if (x < B.shape[1] and row < B.shape[0]) else 0
        cuda.syncthreads()

        # Compute partial dot product from this tile
        for k in range(TPB):
            tmp += sA[ty, k] * sB[k, tx]
        cuda.syncthreads()

    if y < C.shape[0] and x < C.shape[1]:
        C[y, x] = tmp
Raw lines 557 to 593
594### Block-Local Inclusive Prefix Sum
596```python
597@cuda.jit
598def block_inclusive_scan(data, output):
599 shared = cuda.shared.array(256, dtype=float32)
600 tid = cuda.threadIdx.x
601 i = cuda.grid(1)
603 shared[tid] = data[i] if i < data.size else 0
604 cuda.syncthreads()
606 # Hillis-Steele scan within one block. Two barriers prevent read/write races.
607 offset = 1
608 while offset < cuda.blockDim.x:
609 addend = float32(0.0)
610 if tid >= offset:
611 addend = shared[tid - offset]
612 cuda.syncthreads()
613 if tid >= offset:
614 shared[tid] += addend
615 cuda.syncthreads()
616 offset *= 2
618 if i < data.size:
619 output[i] = shared[tid]
620```
622This computes an independent scan per block, not a whole-array scan. A complete multi-block scan
623also scans block totals and adds block offsets. Prefer `cupy.cumsum()`/CUB unless a custom scan
624operator is required.
626Launch `block_inclusive_scan` with at most 256 threads per block.

Block-Local Inclusive Prefix Sum

@cuda.jit
def block_inclusive_scan(data, output):
    shared = cuda.shared.array(256, dtype=float32)
    tid = cuda.threadIdx.x
    i = cuda.grid(1)

    shared[tid] = data[i] if i < data.size else 0
    cuda.syncthreads()

    # Hillis-Steele scan within one block. Two barriers prevent read/write races.
    offset = 1
    while offset < cuda.blockDim.x:
        addend = float32(0.0)
        if tid >= offset:
            addend = shared[tid - offset]
        cuda.syncthreads()
        if tid >= offset:
            shared[tid] += addend
        cuda.syncthreads()
        offset *= 2

    if i < data.size:
        output[i] = shared[tid]

This computes an independent scan per block, not a whole-array scan. A complete multi-block scan also scans block totals and adds block offsets. Prefer cupy.cumsum()/CUB unless a custom scan operator is required.

Launch block_inclusive_scan with at most 256 threads per block.

Raw lines 594 to 627
628### Shared Memory Reduction
630```python
631@cuda.jit
632def block_reduce_sum(data, partial_sums):
633 shared = cuda.shared.array(256, dtype=float32)
634 tid = cuda.threadIdx.x
635 i = cuda.grid(1)
637 shared[tid] = data[i] if i < data.size else 0.0
638 cuda.syncthreads()
640 # Tree reduction in shared memory
641 s = cuda.blockDim.x // 2
642 while s > 0:
643 if tid < s:
644 shared[tid] += shared[tid + s]
645 s //= 2
646 cuda.syncthreads()
648 # Thread 0 of each block writes the block's sum
649 if tid == 0:
650 partial_sums[cuda.blockIdx.x] = shared[0]
651```
653Launch `block_reduce_sum` with a power-of-two block size no greater than 256; sum the
654partial results in a second stage. Other block sizes do not satisfy this reduction algorithm.

Shared Memory Reduction

@cuda.jit
def block_reduce_sum(data, partial_sums):
    shared = cuda.shared.array(256, dtype=float32)
    tid = cuda.threadIdx.x
    i = cuda.grid(1)

    shared[tid] = data[i] if i < data.size else 0.0
    cuda.syncthreads()

    # Tree reduction in shared memory
    s = cuda.blockDim.x // 2
    while s > 0:
        if tid < s:
            shared[tid] += shared[tid + s]
        s //= 2
        cuda.syncthreads()

    # Thread 0 of each block writes the block's sum
    if tid == 0:
        partial_sums[cuda.blockIdx.x] = shared[0]

Launch block_reduce_sum with a power-of-two block size no greater than 256; sum the partial results in a second stage. Other block sizes do not satisfy this reduction algorithm.

Raw lines 628 to 655
656### Stencil / Neighbor Access Pattern
658```python
659@cuda.jit
660def stencil_1d(data, output, radius):
661 shared = cuda.shared.array(288, dtype=float32) # blockDim + 2*radius
662 tid = cuda.threadIdx.x
663 i = cuda.grid(1)
665 # Load center + halo into shared memory
666 shared[tid + radius] = data[i] if i < data.size else 0
667 if tid < radius:
668 shared[tid] = data[i - radius] if i >= radius else 0
669 shared[tid + cuda.blockDim.x + radius] = (
670 data[i + cuda.blockDim.x] if i + cuda.blockDim.x < data.size else 0
671 )
672 cuda.syncthreads()
674 if i < data.size:
675 total = float32(0.0)
676 for j in range(-radius, radius + 1):
677 total += shared[tid + radius + j]
678 output[i] = total / (2 * radius + 1)
679```
681---
683The stencil uses zero padding (a fixed denominator at boundaries). Launch with 256 threads
684and require `0 <= radius <= 16`; larger radii exceed its static shared allocation.
685Compare that exact boundary convention with the CPU reference.

Stencil / Neighbor Access Pattern

@cuda.jit
def stencil_1d(data, output, radius):
    shared = cuda.shared.array(288, dtype=float32)  # blockDim + 2*radius
    tid = cuda.threadIdx.x
    i = cuda.grid(1)

    # Load center + halo into shared memory
    shared[tid + radius] = data[i] if i < data.size else 0
    if tid < radius:
        shared[tid] = data[i - radius] if i >= radius else 0
        shared[tid + cuda.blockDim.x + radius] = (
            data[i + cuda.blockDim.x] if i + cuda.blockDim.x < data.size else 0
        )
    cuda.syncthreads()

    if i < data.size:
        total = float32(0.0)
        for j in range(-radius, radius + 1):
            total += shared[tid + radius + j]
        output[i] = total / (2 * radius + 1)

The stencil uses zero padding (a fixed denominator at boundaries). Launch with 256 threads and require 0 <= radius <= 16; larger radii exceed its static shared allocation. Compare that exact boundary convention with the CPU reference.

Raw lines 656 to 686
687## Performance Optimization

Performance Optimization

Raw lines 687 to 688
689### GPU-Specific Tips
6911. **Minimize host-device transfers.** Use `cuda.to_device()` and keep data on GPU across multiple
692kernel calls. Measure on the target system; interconnect and device-memory bandwidth vary widely.
6942. **Use shared memory** for data reused across threads in a block when profiling shows global
695memory traffic is limiting performance. Shared memory is finite and can reduce occupancy.
6973. **Coalesce memory accesses.** Adjacent threads (consecutive `threadIdx.x`) should access adjacent memory locations. This lets the hardware combine accesses into fewer wide transactions.
6994. **Choose block size for occupancy.** 128-256 threads/block for 1D, (16,16) or (32,32) for 2D. Too few threads underutilizes the GPU; too many may limit registers/shared memory per thread.
7015. **Use `fastmath=True`** when IEEE-754 strictness isn't required. Enables FMA, fast sqrt/division, and faster trig/exp/log for float32.
7036. **Prefer float32 over float64** when precision and stability requirements allow. The throughput
704ratio is architecture-specific, so benchmark the target GPU.
7067. **Use streams** to overlap data transfer with computation.
7088. **Use `cuda.defer_cleanup()`** in performance-critical sections to prevent implicit synchronization from memory deallocation.
7109. **Limit register usage** with `max_registers` parameter when occupancy is the bottleneck.
71210. **Use grid-stride loops** to decouple grid size from problem size and improve flexibility.

GPU-Specific Tips

  1. Minimize host-device transfers. Use cuda.to_device() and keep data on GPU across multiple

kernel calls. Measure on the target system; interconnect and device-memory bandwidth vary widely.

  1. Use shared memory for data reused across threads in a block when profiling shows global

memory traffic is limiting performance. Shared memory is finite and can reduce occupancy.

  1. Coalesce memory accesses. Adjacent threads (consecutive threadIdx.x) should access adjacent memory locations. This lets the hardware combine accesses into fewer wide transactions.
  1. Choose block size for occupancy. 128-256 threads/block for 1D, (16,16) or (32,32) for 2D. Too few threads underutilizes the GPU; too many may limit registers/shared memory per thread.
  1. **Use fastmath=True** when IEEE-754 strictness isn't required. Enables FMA, fast sqrt/division, and faster trig/exp/log for float32.
  1. Prefer float32 over float64 when precision and stability requirements allow. The throughput

ratio is architecture-specific, so benchmark the target GPU.

  1. Use streams to overlap data transfer with computation.
  1. **Use cuda.defer_cleanup()** in performance-critical sections to prevent implicit synchronization from memory deallocation.
  1. Limit register usage with max_registers parameter when occupancy is the bottleneck.
  1. Use grid-stride loops to decouple grid size from problem size and improve flexibility.
Raw lines 689 to 713
714### What Not To Do
716- Don't use Python objects, strings, or dynamic memory allocation inside kernels — Numba CUDA supports a restricted Python subset.
717- Don't put `syncthreads()` inside divergent branches — if threads in a block take different paths through a barrier, behavior is undefined (deadlock or corruption).
718- Don't forget `cuda.synchronize()` before reading results on CPU — kernel launches are async.
719- Don't assume a custom kernel helps small arrays. Measure launch and transfer overhead on the
720 target system and batch or fuse work when overhead dominates.
722---

What Not To Do

  • Don't use Python objects, strings, or dynamic memory allocation inside kernels — Numba CUDA supports a restricted Python subset.
  • Don't put syncthreads() inside divergent branches — if threads in a block take different paths through a barrier, behavior is undefined (deadlock or corruption).
  • Don't forget cuda.synchronize() before reading results on CPU — kernel launches are async.
  • Don't assume a custom kernel helps small arrays. Measure launch and transfer overhead on the target system and batch or fuse work when overhead dominates.

Raw lines 714 to 723
724## Debugging
726### CUDA Simulator
728Run CUDA code on CPU for debugging — supports `print()` and `pdb` inside kernels.
730```bash
731export NUMBA_ENABLE_CUDASIM=1
732python your_script.py
733```
735The simulator runs kernels one block at a time, spawning one thread per CUDA thread. Supports shared/local/constant memory, atomics, and `syncthreads()`.

CUDA Simulator

Run CUDA code on CPU for debugging — supports print() and pdb inside kernels.

export NUMBA_ENABLE_CUDASIM=1
python your_script.py

The simulator runs kernels one block at a time, spawning one thread per CUDA thread. Supports shared/local/constant memory, atomics, and syncthreads().

Raw lines 726 to 736
Runs code python your_script.py :732
Starts an agent spawning :735
737### Debug a Specific Thread
739```python
740@cuda.jit
741def debug_kernel(data, out):
742 i = cuda.grid(1)
743 if cuda.threadIdx.x == 0 and cuda.blockIdx.x == 0:
744 # Only thread (0,0) hits the debugger
745 from pdb import set_trace; set_trace()
746 if i < data.size:
747 out[i] = data[i] * 2
748```

Debug a Specific Thread

@cuda.jit
def debug_kernel(data, out):
    i = cuda.grid(1)
    if cuda.threadIdx.x == 0 and cuda.blockIdx.x == 0:
        # Only thread (0,0) hits the debugger
        from pdb import set_trace; set_trace()
    if i < data.size:
        out[i] = data[i] * 2
Raw lines 737 to 749
750### On-Device Debug Mode
752```python
753@cuda.jit(debug=True, opt=False)
754def kernel_debug(data):
755 # Enables CUDA exception checking — much slower but catches errors
756 ...
757```
759---

On-Device Debug Mode

@cuda.jit(debug=True, opt=False)
def kernel_debug(data):
    # Enables CUDA exception checking — much slower but catches errors
    ...

Raw lines 750 to 760
761## Interoperability
763Numba supports the **CUDA Array Interface** (version 3) — any object exposing `__cuda_array_interface__` can be passed directly to Numba kernels with zero copy.

Interoperability

Numba supports the CUDA Array Interface (version 3) — any object exposing __cuda_array_interface__ can be passed directly to Numba kernels with zero copy.

Raw lines 761 to 764
765### With CuPy
767```python
768import cupy as cp
769from numba import cuda
771@cuda.jit
772def add_kernel(x, y, out):
773 i = cuda.grid(1)
774 if i < x.shape[0]:
775 out[i] = x[i] + y[i]
777# CuPy arrays work directly — zero copy
778a = cp.arange(1000, dtype=cp.float32)
779b = cp.ones(1000, dtype=cp.float32)
780out = cp.zeros(1000, dtype=cp.float32)
781add_kernel[4, 256](a, b, out)
782```

With CuPy

import cupy as cp
from numba import cuda

@cuda.jit
def add_kernel(x, y, out):
    i = cuda.grid(1)
    if i < x.shape[0]:
        out[i] = x[i] + y[i]

# CuPy arrays work directly — zero copy
a = cp.arange(1000, dtype=cp.float32)
b = cp.ones(1000, dtype=cp.float32)
out = cp.zeros(1000, dtype=cp.float32)
add_kernel[4, 256](a, b, out)
Raw lines 765 to 783
784### With PyTorch
786```python
787import torch
788from numba import cuda
790t = torch.tensor([1, 2, 3], dtype=torch.float32, device="cuda")
791d_array = cuda.as_cuda_array(t) # Zero-copy Numba view of PyTorch tensor
792```

With PyTorch

import torch
from numba import cuda

t = torch.tensor([1, 2, 3], dtype=torch.float32, device="cuda")
d_array = cuda.as_cuda_array(t)  # Zero-copy Numba view of PyTorch tensor
Raw lines 784 to 793
794### Checking GPU Arrays
796```python
797cuda.is_cuda_array(obj) # True if obj has __cuda_array_interface__
798cuda.as_cuda_array(obj) # Wrap as Numba device array (zero copy)
799```
801Compatibility is object-specific: check `cuda.is_cuda_array(obj)`. Convert JAX or TensorFlow through a supported DLPack consumer such as CuPy instead of assuming every tensor exposes this interface.
803---

Checking GPU Arrays

cuda.is_cuda_array(obj)       # True if obj has __cuda_array_interface__
cuda.as_cuda_array(obj)       # Wrap as Numba device array (zero copy)

Compatibility is object-specific: check cuda.is_cuda_array(obj). Convert JAX or TensorFlow through a supported DLPack consumer such as CuPy instead of assuming every tensor exposes this interface.


Raw lines 794 to 804
805## Common Pitfalls
8071. **Forgetting bounds checks.** If `blocks * threads > array_size`, out-of-bounds threads corrupt memory silently. Always: `if i < array.size`.
8092. **Trying to return values from kernels.** Kernels cannot return — write to output arrays instead. Return values are silently discarded.
8113. **Implicit synchronous transfers.** Passing host (NumPy) arrays directly to kernels triggers synchronous copy-back. Use explicit `cuda.to_device()` / `copy_to_host()`.
8134. **Shared memory size must be a compile-time constant** for static allocation. Use dynamic shared memory (size=0) for runtime-determined sizes.
8155. **Dynamic shared memory aliasing.** All `cuda.shared.array(0, ...)` in the same kernel share the same memory. Slice manually for multiple arrays.
8176. **`syncthreads()` in divergent branches.** All threads in a block must reach the same `syncthreads()` call. Divergent paths → undefined behavior.
8197. **Atomic operation type restrictions.** `atomic.add` supports int32, float32, float64. Bitwise atomics only work on integer types.
8218. **Forgetting `cuda.synchronize()`.** Kernel launches are async. Reading host-side results before sync gives stale/incomplete data.
8239. **Unsupported Python features in kernels.** No dynamic allocation, no Python objects, no string operations, no exceptions (unless debug mode). Stick to numeric types and math.
82510. **Using float64 on consumer GPUs.** Consumer NVIDIA GPUs (GeForce) have heavily throttled float64 throughput (often 1/32 of float32). Use float32 unless you need the precision.

Common Pitfalls

  1. Forgetting bounds checks. If blocks * threads > array_size, out-of-bounds threads corrupt memory silently. Always: if i < array.size.
  1. Trying to return values from kernels. Kernels cannot return — write to output arrays instead. Return values are silently discarded.
  1. Implicit synchronous transfers. Passing host (NumPy) arrays directly to kernels triggers synchronous copy-back. Use explicit cuda.to_device() / copy_to_host().
  1. Shared memory size must be a compile-time constant for static allocation. Use dynamic shared memory (size=0) for runtime-determined sizes.
  1. Dynamic shared memory aliasing. All cuda.shared.array(0, ...) in the same kernel share the same memory. Slice manually for multiple arrays.
  1. **syncthreads() in divergent branches.** All threads in a block must reach the same syncthreads() call. Divergent paths → undefined behavior.
  1. Atomic operation type restrictions. atomic.add supports int32, float32, float64. Bitwise atomics only work on integer types.
  1. **Forgetting cuda.synchronize().** Kernel launches are async. Reading host-side results before sync gives stale/incomplete data.
  1. Unsupported Python features in kernels. No dynamic allocation, no Python objects, no string operations, no exceptions (unless debug mode). Stick to numeric types and math.
  1. Using float64 on consumer GPUs. Consumer NVIDIA GPUs (GeForce) have heavily throttled float64 throughput (often 1/32 of float32). Use float32 unless you need the precision.
Raw lines 805 to 825