v9.1.1
Menu

Chapter 2: Moving Data: GPU Memory with Numba

Week 2

The GPU has its own memory, separate from your program's. Learn how to move arrays to the device and back, allocate and mutate device arrays, index 2D grids for matrices, and use fast per-block shared memory with a barrier to let threads cooperate.

Topics
4
Demo
1
Videos
3
Min read
11

Chapter Overview

A kernel can only touch memory that lives on the GPU. Your NumPy arrays live in host (CPU) memory, in a completely separate address space connected to the GPU by the PCIe bus. Before a kernel can run, its inputs must be copied to the device, and after it finishes, the results must be copied back.

Managing that movement deliberately is most of what separates a fast GPU program from a slow one — transfers are expensive, so you want to move data up once, do as much work as possible on the device, and move only the results back.

This chapter covers the memory model and the tools Numba gives you to work with it:

  • Host vs Device Memory: two separate worlds, bridged by explicit copies
  • to_device & copy_to_host: staging inputs and retrieving results
  • 2D Grids for Matrices: indexing rows and columns with cuda.grid(2)
  • Shared Memory & __syncthreads(): fast on-chip memory that lets a block's threads cooperate

Chapter Roadmap

Click any topic to jump in

Explainer videoInteractive demo
1
Host vs Device Memory

CPU and GPU have separate address spaces bridged by PCIe; transfers are explicit and costly.

Two Address Spaces, One Bus
How to move data deliberately
2
to_device & copy_to_host

Stage inputs with to_device, allocate outputs with device_array, retrieve with copy_to_host.

Staging Inputs and Retrieving Results
Indexing higher-dimensional data
3
2D Grids for Matrices

cuda.grid(2) gives each thread a (row, col); 2D blocks tile the matrix; guard both dimensions.

cuda.grid(2) and 2D Launches
Letting a block's threads cooperate
4
Shared Memory & __syncthreads()

Fast per-block on-chip memory; a barrier coordinates cooperative load-then-read across threads.

On-Chip Memory and the Barrier

Topics

4 topics in this chapter

A GPU is not simply a faster CPU bolted onto your program; it is a separate processor with its own physical memory, sitting across the PCIe bus from the host. That physical separation is the single fact that shapes every GPU program, and this topic makes it concrete before you move a single byte. Data your Python code creates lives in host memory (system RAM); a kernel can only read and write device memory (the GPU's onboard VRAM). Nothing on the GPU can dereference a host pointer, and nothing on the CPU can dereference a device pointer — the two address spaces are genuinely distinct. So the shape of every CUDA workflow is the same three-step rhythm: copy inputs from host to device, launch a kernel that computes on the device, then copy results back from device to host. Those transfers cross the PCIe bus and are often the slowest part of a naive program, which is why understanding the two address spaces up front pays off immediately. The next topics turn this model into concrete Numba calls — to_device, copy_to_host, and the device arrays that live entirely in VRAM.

Definition

The host is the CPU and its system RAM; the device is the GPU and its onboard memory. The two form separate address spaces connected by the PCIe bus: kernels access only device memory, host code accesses only host memory, and data must be explicitly copied across the bus in each direction.

1 of 1
Two Address Spaces, One Bus

Your Python process and its NumPy arrays live in host memory (system RAM). The GPU has its own device memory (VRAM) with far higher bandwidth but no direct access to host RAM. The two are connected by the PCIe bus, which is fast in absolute terms but slow compared to either chip's local memory.

The practical consequence: a kernel's arguments must already be on the device. If you pass a NumPy array, Numba will helpfully copy it up and the result back automatically — convenient, but it hides a transfer on every call. For anything beyond a one-off, you manage the device arrays yourself so you can keep data resident on the GPU across multiple kernels and pay the transfer cost only when you truly must.

Mathematical Intuition

If a kernel does WW FLOPs on BB bytes and the device runs at FF FLOP/s while PCIe moves GG bytes/s, then compute takes W/FW/F and the round-trip transfer takes 2B/G2B/G. The GPU only wins when W/F≪W/F \ll the time saved elsewhere — for low arithmetic intensity (W/BW/B small) the 2B/G2B/G transfer term dominates, which is why minimizing transfers matters more than tuning the kernel.

Example:

You run a pipeline of five kernels on the same array. Why is it much faster to transfer the array to the device once at the start than to pass a NumPy array to each kernel?

Related Problems on PixelBank