<<<CUDA C++ Coursegrid · block · warp · lane
Stage 1 · Getting started02 / 15about 22 mincuda/02_vector_add.cu

Thread hierarchy: index arithmetic for grid / block / thread

Map 1D and 2D data onto threads, and why every kernel needs a bounds check

After this lesson you can
  • Derive the global thread index fluently, in both 1D and 2D
  • Know why grid size rounds up, and the out-of-bounds risk that comes with it
  • Use a grid-stride loop, a more reliable pattern than a bare index
  • Build an intuition for the mapping from thread to data element

CUDA's parallel model is a two-level hierarchy: a grid is made of blocks, a block is made of threads. The split is not cosmetic. It maps onto hardware: a block is assigned as a whole to one SM. Threads in a block can share shared memory and wait for each other with __syncthreads(). Threads in different blocks share nothing.

                        grid
  ┌───────────────┬───────────────┬───────────────┐
  │   block 0     │   block 1     │   block 2     │
  │ t0 t1 t2 t3   │ t0 t1 t2 t3   │ t0 t1 t2 t3   │
  └───────────────┴───────────────┴───────────────┘
    0  1  2  3      4  5  6  7      8  9 10 11     ← global index

  global = blockIdx.x * blockDim.x + threadIdx.x
             ↑             ↑              ↑
        which block    threads/block   thread in block
Thread numbers for grid(3) × block(4)
Open the indexing visualization labDo not memorize the formula. Drag gridDim / blockDim yourself, click any thread, and see which data element it maps to. For 2D mapping especially, click around here before you keep reading.

1D: vector add

The classic kernel
1__global__ void vecAdd(const float* a, const float* b, float* c, int n) {2    int i = blockIdx.x * blockDim.x + threadIdx.x;3    if (i < n) {              // bounds check: do not skip this4        c[i] = a[i] + b[i];5    }6}

Why must you write if (i < n)? Block thread counts are usually a power of two such as 128 or 256, while n is an arbitrary value. To cover every element, grid size must round up, so some threads in the last block have an index past the array. Skip that check and you get an out-of-bounds device-memory write: wrong results in the mild case, a smashed neighboring data structure in the bad case.

Grid size rounded up
1int n = 1'000'000;2int threads = 256;3int blocks = (n + threads - 1) / threads;   // = ceil(n / threads) = 39074 5vecAdd<<<blocks, threads>>>(d_a, d_b, d_c, n);6// 3907 * 256 = 1'000'192 threads. The last 192 are stopped by the if

grid-stride loop: a more reliable pattern

One thread per element is a poor fit when the data is very large or very small. A grid-stride loop decouples grid size from data size: no matter how many threads you launch, each thread walks the whole array with a fixed stride. The same kernel can then run any size of input with one grid size tuned to the hardware.

grid-stride loop
1__global__ void vecAddStride(const float* a, const float* b, float* c, int n) {2    int stride = gridDim.x * blockDim.x;         // total threads in the grid3    for (int i = blockIdx.x * blockDim.x + threadIdx.x;4         i < n;5         i += stride) {6        c[i] = a[i] + b[i];7    }8}
  • Size independent: the same grid configuration finishes correctly whether n is 1000 or a billion.
  • Accesses stay coalesced: neighboring threads still touch neighboring addresses in each iteration, so you do not break memory coalescing (covered in the next stage).
  • Easier to debug: set the grid to <<<1, 1>>> and it collapses to serial execution, so you can check results against a known reference.
  • Reuse registers: compute constants once outside the loop and spread that cost across many elements.

2D: images and matrices

A 2D grid is the natural fit for matrices. One convention matters: threadIdx.x should map to the contiguous direction inside a row (the column index). Neighboring threads in a warp are numbered .x first, so coalescing only happens when .x walks contiguous memory. Reverse that mapping and performance can drop by 5x or more.

2D indexing: x maps to column
1__global__ void scaleMatrix(float* m, int rows, int cols, float k) {2    // x → column (contiguous in memory), y → row3    int col = blockIdx.x * blockDim.x + threadIdx.x;4    int row = blockIdx.y * blockDim.y + threadIdx.y;5 6    if (row < rows && col < cols) {7        m[row * cols + col] *= k;     // row-major: one warp covers contiguous cols8    }9}10 11// host side12dim3 block(32, 8);                                    // 256 threads, x dimension is exactly one warp13dim3 grid((cols + block.x - 1) / block.x,14          (rows + block.y - 1) / block.y);15scaleMatrix<<<grid, block>>>(d_m, rows, cols, 2.0f);

How to pick a block size

block sizeNotes
32Too small. Each SM has a cap on resident blocks (usually 16 to 32), so total threads stall and occupancy is stuck
128 / 256Default starting point. Most kernels start here. Occupancy and scheduling flexibility stay in a good range
512Fits kernels with few registers per thread that cooperate through a large shared-memory region
1024Hardware maximum. Register pressure is huge, so this is usually slower unless the algorithm truly needs it
not a multiple of 32Never. 100, for example, leaves 28 threads idle in the last warp of every block

Self-check

Answer in your head first, then open the solution. If you cannot, reread that section.