This chapter exists to make the rest of the book readable: enough GPU programming to follow the architecture discussions — and essential if your research ever changes the hardware/software interface (researchers adding transactional memory to GPUs, for instance, had to write their own benchmarks because no GPU supports TM — see §5.3).
The central sleight of hand of GPU programming: the hardware is wide
SIMD, but CUDA and OpenCL present a MIMD-like model. You launch a large
array of scalar threads, each free to take its own control-flow path and
touch arbitrary memory. At runtime, the hardware silently gangs these threads
into
From a loop to a grid of threads
Start with the book’s running example: SAXPY (
void saxpy_serial(int n, float a, float *x, float *y)
{
for (int i = 0; i < n; ++i)
y[i] = a*x[i] + y[i];
}
main() {
float *x, *y;
int n;
// omitted: allocate CPU memory for x and y and initialize contents
saxpy_serial(n, 2.0, x, y); // Invoke serial SAXPY kernel
// omitted: use y on CPU, free memory pointed to by x and y
}
And the CUDA version — the for loop is gone, dissolved into threads:
__global__ void saxpy(int n, float a, float *x, float *y)
{
int i = blockIdx.x*blockDim.x + threadIdx.x;
if(i<n)
y[i] = a*x[i] + y[i];
}
int main() {
float *h_x, *h_y;
int n;
// omitted: allocate CPU memory for h_x and h_y and initialize contents
float *d_x, *d_y;
int nblocks = (n + 255) / 256;
cudaMalloc( &d_x, n * sizeof(float) );
cudaMalloc( &d_y, n * sizeof(float) );
cudaMemcpy( d_x, h_x, n * sizeof(float), cudaMemcpyHostToDevice );
cudaMemcpy( d_y, h_y, n * sizeof(float), cudaMemcpyHostToDevice );
saxpy<<<nblocks, 256>>>(n, 2.0, d_x, d_y);
cudaMemcpy( h_x, d_x, n * sizeof(float), cudaMemcpyDeviceToHost );
// omitted: use h_y on CPU, free memory pointed to by h_x, h_y, d_x, and d_y
}
The __global__ keyword marks saxpy as a
Step 1: main() starts on the CPU
Like any C/C++ program, execution begins on the CPU. The host arrays h_x and h_y are allocated in system memory and initialized (omitted in the book's listing).
float *h_x, *h_y;
int n;
// allocate + initialize h_x, h_yOne tick = one step of the CUDA SAXPY program's host/device choreography (Fig 2.2 of the book).
The thread hierarchy: grid → blocks → warps → threads
The launch syntax saxpy<<<nblocks, 256>>>(…) — CUDA’s nblocks (n, 2.0, d_x, d_y) are delivered to every thread.
Each thread finds its share of the data by computing a global index from
three built-ins: blockIdx.x (which block am I in?), blockDim.x (how big
is a block? — 256 here), and threadIdx.x (which thread am I within my
block?). Watch blocks map onto the array one tick at a time (toy sizes:
n = 20, blockDim = 8, warps of 4), and click any thread to compute its
index — including the tail threads whose if (i<n) guard fails:
y[0 … 19] — one element per thread
grid = 3 thread blocks × 8 threads (warps of 4)
block 0 (not yet mapped)
block 1 (not yet mapped)
block 2 (not yet mapped)
Click any thread to compute its global index.
Note what the compiler and hardware are doing for you here: each thread appears to execute independently — the lockstep warps underneath are invisible at this level. (Chapter 3 is about what it costs to maintain that illusion.)
Memory: h_ and d_, and what the SAXPY example doesn’t show
Following NVIDIA convention, h_ prefixes pointers into CPU (host) memory
and d_ into GPU (device) memory. cudaMalloc asks the cudaMemcpy asks it to
copy — explicitly, in the general separate-memories case the book (and this
site) focuses on. NVIDIA’s
Beyond SAXPY, three programming-model facts matter for later chapters:
- Fast intra-CTA communication. Threads of a CTA share a per-core
: NVIDIA’sscratchpad scratchpad memoryFast per-core, software-managed memory through which threads on a core communicate.Glossary → (one per SM, 16–64 KB, split among the CTAs resident there; declared withshared memory shared memoryNVIDIA's name for the per-SM scratchpad (16–64 KB), declared with __shared__; a software-controlled cache.Glossary →__shared__), AMD’s (LDS) — plus, uniquely on AMD, a GPU-widelocal data store local data store (LDS)AMD GCN's per-CU scratchpad memory, also used to pass parameters between graphics shaders.Glossary → (GDS). A scratchpad is a software-controlled cache: it wins when you can identify data that’s reused often and predictably (in graphics, e.g., passing parameters between vertex and pixel shaders). Hardware caches exist too, but miss unpredictably.global data store global data store (GDS)AMD GCN's GPU-wide scratchpad shared by all compute units.Glossary → - Fast intra-CTA synchronization. Threads in the same CTA synchronize
with hardware
instructions. Threads in different CTAs can only communicate throughbarrier barrierHardware-supported instruction letting all threads of a CTA synchronize cheaply.Glossary → — more expensive in both time and energy.global memory global memoryThe address space accessible to all threads; slower and more energy-costly than shared memory; the only way threads in different CTAs communicate.Glossary → - Nested parallelism.
(Kepler) lets a kernel launch further work from the GPU itself, because irregular, data-dependent applications otherwise leave the hardware underutilized through load imbalance — the same disease Dynamic Warp Formation research attacks in §3.4.CUDA Dynamic Parallelism CUDA Dynamic Parallelism (CDP)Kepler-era feature letting kernels launch kernels, targeting load imbalance in irregular applications.Glossary →
Check yourself
1. CUDA exposes a MIMD-like model of independent scalar threads. What actually happens at runtime?
2. Predict the index (use the mapping animation): blockDim.x = 256. Which element does the thread with blockIdx.x = 2, threadIdx.x = 5 compute?
3. n = 20 elements, blockDim = 8 → nblocks = ⌈20/8⌉ = 3, i.e. 24 threads launched. How many execute the y[i] update?
4. Which statement about scratchpad ('shared') memory is correct?
5. What motivated CUDA Dynamic Parallelism (CDP)?