6.2.6 · D2GPU Architecture

Visual walkthrough — Thread blocks and grids

2,028 words9 min readBack to topic

We are answering ONE question the whole way down:

"I have thousands of workers split into teams. How does each worker figure out which single job in one long to-do list is theirs — with no worker ever guessing the same job as another?"


Step 1 — The problem laid flat: one long to-do list

WHAT. Picture our data as one straight row of boxes, numbered . This is the array C in vector addition (C[i] = A[i] + B[i]). Each box is one job.

WHY start here. Before we talk about teams, we must see the thing being divided. Memory on a GPU is fundamentally a 1D line of addresses — even a 2D image is stored as one long strip. So the target of all our indexing is a single number, an index into this line.

PICTURE. The blue strip below is the to-do list. Each cell is one element.


Step 2 — Cutting the list into equal teams (blocks)

WHAT. We chop the long strip into equal chunks. Each chunk is a thread block — a team of workers. Call the team size (in code: blockDim.x). The teams are numbered and a team's number is (in code: blockIdx.x).

WHY chop at all? A GPU cannot pour all workers into one pile — hardware forces teams onto separate Streaming Multiprocessors. Chopping the list into equal blocks means every team gets an identical slice of work, and the teams can be handed out to whichever processor is free. This is also why teams must be equal size: equal-size chunks make the arithmetic (coming in Step 4) a clean multiplication.

PICTURE. Same strip, now sliced by yellow dividers into blocks of size . Notice block starts at a specific place on the strip — hold that thought.


Step 3 — Each worker's local number

WHAT. Inside a team, workers count themselves from : worker , worker , up to worker . This within-team number is (in code: threadIdx.x).

WHY a local number and not a global one? Because every team is identical, the hardware gives every worker the same little counter that resets to in each team. That is cheap and uniform. The cost: worker in team and worker in team share the same local number but must do different jobs. Steps 4–5 fix exactly this clash.

PICTURE. Zoom into two teams. Both have a worker labelled (red). They point at different boxes on the strip. The local number alone is ambiguous.


Step 4 — Counting the jobs that came before you

WHAT. Team is not the first team. Teams came before it — that is complete teams, each owning jobs. So before team even starts, exactly jobs are already spoken for. That product is the offset to the front of your team's slice.

WHY multiply? Multiplication is repeated addition of equal groups. We have equal groups of size . Adding ( times) is precisely . This clean product is only possible because Step 2 made the blocks equal — it is the payoff for that choice.

PICTURE. The strip with the green bracket spanning boxes up to — every job that belongs to an earlier team. Team 's slice begins right after the bracket, at box .


Step 5 — Adding your seat inside the team: the formula is born

WHAT. You are worker inside team . Your team's slice begins at box (Step 4). You sit seats into that slice. So your job is:

Written with the CUDA names:

WHY add ? The multiplication got you to the door of your team's slice. The addition walks you steps in to your own seat. Multiply-then-add is the whole story: jump to your team, then step to your seat.

WHY is this collision-free? Two different pairs can never land on the same . Since , the value lands strictly inside — that is exactly team 's block of boxes and no other team's. So every worker gets a distinct job, and every job in gets exactly one worker. (Here = gridDim.x is the number of teams.)

PICTURE. Worker with block size : the green arrow jumps to box , then the red arrow steps to box . That is his job.


Step 6 — The leftover-worker edge case (why the if guard exists)

WHAT. Real problems rarely divide evenly. Suppose jobs and . How many teams? We cannot use teams (that covers only wait — ? Let's use the honest small case): , needs teams covering seats. But there are only jobs. Two workers have no job.

WHY it happens. We launched a whole number of equal teams, and the last team spills past the end of the list. Those extra workers would compute and — boxes that do not exist. Reading/writing there is illegal memory access → crash or garbage.

THE FIX. Every worker checks if (i < N) before touching memory. Workers with do nothing. The CUDA launch computes team count with ceiling division:

The pushes any nonzero remainder up to the next whole team, so no job is left uncovered — we always round up, never down.

PICTURE. Three teams over . Green boxes are real jobs; the two red hatched boxes are phantom seats whose workers are silenced by the guard.


Step 7 — Two dimensions: the same idea, twice, then flattened

WHAT. For an image (or a matrix), the natural picture is a 2D field of pixels. We tile it with 2D blocks. Each thread finds its column and row using the exact same jump-then-step rule, once per axis:

WHY two copies of the formula? The 2D grid is just the 1D idea run independently along (horizontal) and (vertical). counts across columns, counts down rows — the convention matching how we draw matrices (rows go down).

Then flatten. Memory is still a 1D line (Step 1!). A pixel at in an image of width lives at

This is row-major order: to reach my box, skip all complete rows above me ( rows of boxes each — a multiply), then step across my own row (an add). It is Step 4 + Step 5 all over again, one level up.

PICTURE. A 2D pixel field tiled by blocks; one thread's found by two arrows, then the row-major flattening to a single index.

Recall Why is flattening also a multiply-then-add?

Because 2D memory is a lie — it is stored as one long line. ::: To reach row you must skip whole rows of elements (, the jump), then move col into your row (the step). Identical shape to the block formula.


The one-picture summary

Everything above is one sentence: jump over what came before, then step to your own seat — and do it once per dimension. The figure below stacks all three jump-then-step moves (1D global ID, the leftover guard, and 2D→flat) into a single diagram.

Recall The whole walkthrough in plain words (Feynman retelling)

Imagine a huge to-do list, one job per box, boxes numbered from zero. We can't dump all workers on it, so we cut the list into equal teams and number the teams. Inside each team the workers count themselves from zero — but that local number repeats in every team, so it can't be a job number by itself. Here's the trick: to find your job, first count all the jobs owned by teams ahead of you. Every earlier team owns the same number of jobs, so that count is just (your team number) times (team size) — a multiplication, because it's equal groups added up. That lands you at the door of your team's slice. Now step in by your own local seat number — an addition. Multiply to jump, add to step: that's the global ID, and it never collides because your seat stays inside your team's slice. If the list doesn't split evenly, the last team has empty seats pointing past the end, so each worker checks "is my job real?" before touching memory, and we round the team count up so nothing is missed. For images we just run the same jump-then-step twice — across for columns, down for rows — then flatten to the real 1D memory line by skipping whole rows (multiply) and stepping across our own row (add). Same idea, all the way down.

Related vault topics: Warp Execution · Shared Memory · Thread Synchronization · GPU Occupancy · Memory Coalescing · Parallel Algorithm Design.