6.2.15 · D2GPU Architecture

Visual walkthrough — ROCm - OpenCL alternatives

2,687 words12 min readBack to topic

This page derives one central result of the parent note in pictures: a formula that gives every single worker on a GPU its own unique home address. We will not write that formula down yet — every symbol in it has to be earned first. By the end you will see why the whole thing is just "counting boxes inside boxes", and you will know what happens at every edge — the last box, the leftover box, the empty case.

Read the parent first if you haven't: ROCm / OpenCL alternatives.


Step 1 — The problem: a huge pile of identical jobs

WHAT. Imagine we have a giant list of numbers and we want to do the same tiny operation to each one — say, add two arrays element by element: C[i] = A[i] + B[i]. Suppose the list has elements (we use a small number so it fits in a picture; the real note uses over a million).

WHY. A GPU is not one fast worker. It is thousands of slow-ish workers. To use it, we must hand each worker exactly one job and make sure no two workers grab the same job and no job is skipped. So the very first question is: how do we hand out job numbers so every worker knows which one is theirs?

PICTURE. Here is the raw pile of jobs — a single row of numbered boxes.

Figure — ROCm - OpenCL alternatives

Step 2 — The first symbol: the global ID

WHAT. You might say: "Just number them to in one long line and we're done." That flat position — the worker's address in the whole problem — is important enough to earn a name and a symbol right now.

WHY. We give it a symbol now because it is the quantity we are ultimately hunting for. Everything else on this page exists only to reconstruct from the smaller pieces the hardware actually gives us. Hold onto this box-number picture; every later symbol will be measured against it.

PICTURE. The same row again, now with the name written under it.

Figure — ROCm - OpenCL alternatives

Step 3 — Why we can't just number them all at once

WHAT. So why not stop here and use directly? Because the hardware won't let us hand out those flat numbers. It insists on chopping the line into equal chunks first. Each chunk is called a work-group.

WHY. Real silicon executes workers in fixed-size batches — NVIDIA runs them at a time (a warp), AMD runs them at a time (a wavefront). So we are forced to slice the flat line into equal groups that match the hardware's batch size.

PICTURE. Same boxes, but now sliced into groups of . (We pick instead of only so it fits on screen — the logic is identical.)

Figure — ROCm - OpenCL alternatives


Step 4 — Two rulers instead of one

WHAT. The hardware never tells a worker its directly. Instead it gives each worker two smaller addresses:

  • Its group IDwhich box it lives in ().
  • Its local IDits seat inside that box ().

WHY. The hardware only ever knows "you are worker #2 inside group #1". That is a natural consequence of the batching from Step 3. So we are handed the two small rulers and must reconstruct the one big ruler () ourselves.

PICTURE. The same strip, labelled twice: the bottom ruler counts groups, the top ruler restarts at inside every group.

Figure — ROCm - OpenCL alternatives

Notice the top ruler resets to 0 at the start of every group. That reset is the whole reason we need a formula: local ID appears four different times, so it cannot be an address by itself.

group_id
which group a worker belongs to — an index over whole boxes
local_id
a worker's seat number inside its box, which restarts at 0 in every box

Step 5 — Building the formula: skip past full boxes, then step in

WHAT. Now we finally assemble from the two rulers. Take worker local ID 2 in group 1. To find its global number we do two things:

  1. Skip over every box before ours. There is full box in front of us, each holding workers, so we skip addresses.
  2. Walk in to our seat. Add our local seat number, .

That gives . Check against Step 4: yes, the box labelled global is exactly local-2-of-group-1.

WHY. This is nothing more than reading a number written in a mixed base. "Group , seat , with seats per group" is the same idea as " ten and ones ", except each place is worth instead of .

PICTURE. The red arrow first jumps over the full groups (that's the multiply), then steps to the seat (that's the add).

Figure — ROCm - OpenCL alternatives


Step 6 — Going backwards: recovering box and seat from a global number

WHAT. Sometimes we have the global number and want the other direction: which box, which seat? That is just the formula solved backwards using integer division and remainder:

WHY. Integer division answers "how many whole boxes fit below me?" and the remainder answers "how far past the last full box am I?" — the exact inverse of skip-then-step.

PICTURE. Worker global : dividing by gives quotient (the box) and remainder (the seat). The two operations point back to the two parts of Step 5's arrow.

Figure — ROCm - OpenCL alternatives


Step 7 — Edge case A: the very first and very last worker

WHAT. Check the extremes so nothing is ever undefined.

  • First worker: group , local . Correct: the front of the line.
  • Last worker: group , local . Correct: the final box, final seat, which is .

WHY. We must prove the formula covers the boundaries, because off-by-one errors here are the classic GPU bug: read one box too far and you touch memory you don't own.

PICTURE. Both ends lit up — the global range runs exactly through , no gap, no overshoot.

Figure — ROCm - OpenCL alternatives

Step 8 — Edge case B: when the total doesn't divide evenly (the "guard" clause)

WHAT. Suppose we have real jobs but the hardware forces groups of . Then , which is not a whole number. The runtime rounds up the launch to groups workers — but only jobs exist. Workers and have no real job.

WHY. The hardware can only launch whole groups. So we always spawn a few extra workers and must switch them off before they read past the end of the array. That is exactly the if (i < n) guard in every kernel:

int i = blockIdx.x * blockDim.x + threadIdx.x; // = global_id
if (i < n) y[i] = a * x[i] + y[i];             // <-- the guard

PICTURE. The two extra workers (global ) are drawn as ghosts; the red guard line drops them so they do nothing.

Figure — ROCm - OpenCL alternatives

Step 9 — Edge case C: degenerate launches, including the empty one

WHAT. Three limiting shapes to check.

  • One giant group (): then group_id is always , and . The two rulers collapse into one — the formula still holds.
  • Groups of one (): then local_id is always , and . Again one ruler; formula still holds.
  • No jobs at all (): the line is empty, , so zero workers are launched. The formula is never evaluated — there is no worker to hold a — and nothing goes wrong. An empty workload is a valid, quietly-does-nothing launch.

WHY. A formula you trust must survive its extremes. At and the "boxes inside boxes" picture flattens to a single ruler; at the picture is blank and the guard from Step 8 (if (i < n) with ) means no worker ever runs. In every case the arithmetic agrees.

PICTURE. Top: one box holding all . Middle: boxes holding one each. Bottom: the empty line — no boxes, no workers.

Figure — ROCm - OpenCL alternatives
Recall Why do the degenerate cases matter?

Because a mapping that breaks at , , or would be an unreliable abstraction. The fact that group_id * local_size + local_id reduces gracefully to a plain identity in the collapsing cases, and simply never fires on an empty launch, is proof it is the right, general formula — not a special-case hack.


The one-picture summary

Everything above is one idea: a flat line of jobs is secretly a grid of boxes, and the address formula is how you fold between the two views. Multiply to skip full boxes, add to step into your seat; divide and remainder to go back.

Figure — ROCm - OpenCL alternatives
Recall Feynman retelling — say it like you'd tell a friend

A GPU has thousands of tiny workers, and the only trouble is giving each one a job with no clashes. Its true seat number in the whole job list we call the global ID. But the chip won't line them all up in one row — it runs them in fixed-size gangs (32 for NVIDIA, 64 for AMD), and each gang holds workers. So each worker is told two small things: which gang it's in (group ID) and which seat within the gang (local ID). To rebuild its global ID it skips over all the earlier gangs — that's "gang number times " — then walks to its seat — "plus the seat number". To reverse it, divide by (which gang) and take the remainder (which seat). We always launch a few too many workers because gangs come in whole sizes, so we add one line, if (i < n), telling the leftovers to sit still. Check the ends: worker zero lands at , the last real worker at , never at . Squash a gang down to size one, or blow it up to hold everyone, and the formula still spits out the same flat line — and if there are zero jobs, zero workers launch and nothing happens at all. That's it — counting boxes inside boxes.

recall — the address formula
global_id = group_id × L + local_id (L = local work size)
recall — going backwards, which seat?
local_id = global_id mod L
recall — why extra workers exist
gangs come in whole sizes, so G is rounded up to a multiple of L; leftovers are killed by if (i < n)
recall — what happens when G = 0
Num Groups = 0, no workers launch, the formula never fires