GPUFlight Knowledge Library

A thread is not an array index

An image is 11 pixels wide, yet CUDA launches 12 threads across it. Which threads own pixels, and why can a bad coordinate still give a valid-looking address?

A camera produces a grayscale image 11 pixels wide and 7 pixels high. A CUDA kernel brightens each pixel. Its author assigns each thread the pixel at its global x and y coordinates, provided that pixel exists. Two neighboring threads at the right edge show how that rule connects a launch, an image, and an address.

Where does a thread start?

The kernel launches blocks of 4 × 4 threads. Covering 11 columns takes three blocks across, and covering seven rows takes two blocks down. The grid therefore launches 12 × 8 thread positions for 11 × 7 pixels. Every block has the same shape, including those that extend beyond the image. Of the 96 launched threads, 77 own pixels and 19 skip the pixel operation.

CUDA gives each thread a block coordinate (blockIdx) and a coordinate inside that block (threadIdx). Multiplying the block coordinate by the block dimension gives the block’s origin along that axis. Adding the local coordinate gives the global coordinate. Block (2, 0) begins at (8, 0), since two blocks of four columns precede it. Its local thread (2, 2) reaches (10, 2), and its local thread (3, 2) reaches (11, 2). NVIDIA’s guide to thread hierarchy.

Which thread owns a pixel?

With zero-based coordinates, the last column is 10 and the last row is 6. The condition x < width && y < height checks both coordinates before the thread writes. If rows are packed together, the first pixel of row 2 follows two complete rows of 11 pixels. Column 10 advances another ten elements, so pixel (10, 2) occupies element 32.

Two adjacent threads in block (2, 0), with coordinates ordered (x, y)
Thread in blockGlobal positionImage access
(2, 2)(10, 2)Writes packed element 32
(3, 2)(11, 2)Skips the write because x equals the width

Flattening the second coordinate too early hides the error. The calculation 2 × 11 + 11 produces 33, which lies inside the 77-element buffer. Element 33 already belongs to pixel (0, 3). A check on the flat offset alone would allow two threads to write the same pixel.

How far apart are two rows?

Suppose each pixel occupies four bytes. A packed row occupies 44 bytes, so pixel (10, 2) starts at byte offset 2 × 44 + 10 × 4 = 128. Now suppose the same image has a row pitch of 64 bytes. The allocation leaves 20 bytes of padding after each row’s pixels, and the same pixel starts at byte offset 2 × 64 + 10 × 4 = 168. Both versions have the same 11 valid columns. CUDA’s pitched allocation API returns this physical row spacing in bytes. NVIDIA CUDA Runtime API, memory management.

Would switching to 8 × 4 blocks move pixel (10, 2) in memory?

Its owner would become local thread (2, 2) in block (1, 0). The image dimensions, row pitch, and pixel size still determine its address. With the 64-byte pitch, the pixel stays at byte offset 168.