6.2.13 · D1GPU Architecture

Foundations — CUDA programming model basics

2,012 words9 min readBack to topic

This is the D1 Foundations deep-dive for CUDA programming model basics. Before you touch a kernel, you must be fluent in every symbol the parent note throws at you. We build each one from nothing: plain words → a picture → why the topic can't live without it.


1. Host and Device — two separate worlds

The picture: imagine two islands. The Host island has a few clever workers. The Device island has thousands of simple workers. A single narrow bridge — the PCIe bus — connects them. Nothing on one island can reach across and grab a book from the other island's library directly; you must ship it across the bridge.

Why the topic needs it: every CUDA program is a story of moving data across that bridge, running work on the Device island, then shipping results back. The parent note's steps "Copy Host → Device" and "Copy Device → Host" only make sense once you see there are genuinely two memories that cannot see each other. See Memory Hierarchy for the full map of the Device's own internal libraries.


2. Thread — the smallest worker

The picture: a single dot on the warehouse floor, holding a slip of paper that says "I handle element 7." It does not know about element 6 or 8 — only its own.

Why the topic needs it: the whole point of a GPU is to stop writing loops. Instead of "do this N times, one after another," you say "here is the work for ONE element — now spawn N threads and let them all run at once." The thread is the atom of that idea.


3. Block — a team of threads

The picture: cluster the dots into square huddles of, say, 256 workers. Each huddle is a block. Workers inside one huddle can pass notes to each other cheaply; workers in different huddles cannot.

Why the topic needs it: cooperation is expensive at scale. Letting all 10,000 workers talk to all others is chaos. Grouping them into blocks means only the ~256 workers who need to collaborate share a table. A whole block is scheduled onto one physical Streaming Multiprocessor (SM).


4. Grid — all the teams together

The picture: zoom out. The entire warehouse floor, tiled with huddles, is the grid. One kernel launch = one grid.

Why the topic needs it: the grid is the scalability layer. You describe your problem as "this many blocks, this many threads each," and the GPU figures out how to pour those blocks onto however many SMs it happens to have — 20 SMs on a small card, 100+ on a big one. You never hardcode the hardware. This is the logical→physical mapping the parent note draws: Grid → GPU, Blocks → SMs, Threads → CUDA cores.


5. The dot-notation symbols: blockIdx, blockDim, threadIdx

These three built-in variables are how a thread discovers who it is. Each has an .x (and for 2D, .y). Read .x as "the horizontal component."

The picture: you are a worker. blockIdx.x = 2 means "I'm in the 3rd huddle (huddles counted from 0)." blockDim.x = 256 means "each huddle holds 256 people." threadIdx.x = 5 means "I'm the 6th person in my huddle."


6. The global index formula — earning every piece

Now we assemble the parent note's central equation. We build it, not just quote it.

WHAT it does: turns a thread's local seat number into a unique global number from to across the whole grid.

WHY, step by step (look at figure s03):

  • Each block holds blockDim.x threads. So before my block, there were blockIdx.x full blocks.
  • The number of threads that came before my block = blockIdx.x × blockDim.x. That is my huddle's starting offset — the red bracket in the figure.
  • Add my seat inside the huddle, threadIdx.x, and I land on my own unique slot.

A concrete check. Block , block size , seat : Thread handles C[517]. No two threads ever collide, because each (blockIdx, threadIdx) pair gives a different sum — exactly like (page × 256) + line uniquely locates a line in a book.

Why multiplication and not something fancier? We need each block to claim a contiguous, non-overlapping range of indices. Multiplying the block number by the block size is precisely "skip over all the earlier blocks' slots." No trigonometry, no exotic tool — just counting.

Recall Why does

blockIdx.x × blockDim.x come before adding threadIdx.x? Because it is the count of all threads in earlier blocks — the starting offset of my block. Adding my within-block seat then places me at my exact global slot.


7. ceil, and the ceiling-division trick

To cover elements with blocks of size , how many blocks do we launch?

The picture: you have eggs and cartons holding eggs each. Even if the last carton is only partly full, you still need that whole carton. Rounding down would leave eggs homeless.

WHY the + T - 1 trick? Computers do integer division by chopping off the remainder (rounding down). To force a round-up, you nudge the numerator up by first. If divides evenly, that nudge isn't enough to spill into the next block; if there's any remainder, it is. Example: , : Four blocks. But — which is why we need the next symbol.


8. The boundary check if (idx < N)

Four blocks give 1024 threads, but we only have 1000 elements. Threads through are extra — they have no valid egg to touch.

The picture: the last carton has 24 empty egg slots. If an extra worker reaches into slot of an array that stops at , it grabs memory that isn't ours — a crash or garbage.

This is the parent note's Mistake 1. It is not optional — because block sizes are fixed, numBlocks × T almost always overshoots .


9. Symbols in the memory table: cycle, KB/MB/GB

The parent's memory table uses these units — define them so the table is readable.

The picture — why closer = faster: registers live inside the compute core (millimetres of wire → 1 cycle). Global VRAM sits off-chip (centimetres of wire + a memory controller → hundreds of cycles). Host RAM is across the PCIe bridge (metres of signalling → thousands of cycles). Distance literally costs time because electrical signals take time to travel. The full tiered picture lives in Memory Hierarchy.


Prerequisite map

Host and Device two memories

CUDA execution model

Thread one worker

Block team of threads

Grid all blocks

blockIdx blockDim threadIdx

global index formula

ceiling division numBlocks

boundary check idx less than N

cycles and byte units

memory hierarchy

Each foundation on the left feeds the CUDA execution model. If any node is unclear, revisit its section above before reading the parent note. Related paths onward: Thread Warps and SIMT, Streaming Multiprocessors, GPU Architecture Overview.


Equipment checklist

Cover the right side and answer aloud. If you stumble, that section is your next stop.

What is the Host and what is the Device?
Host = CPU + system RAM; Device = GPU + VRAM; they are separate memories joined only by the PCIe bus.
What is a single thread responsible for?
One small piece of work — e.g. computing exactly one output element C[idx].
How do a block and a grid differ?
A block is a cooperating group of threads (mapped to one SM); a grid is the full collection of all blocks in one kernel launch.
What does blockIdx.x mean vs threadIdx.x?
blockIdx.x = which block I'm in; threadIdx.x = my seat number inside that block.
State the global index formula and explain each term.
globalIdx = blockIdx.x * blockDim.x + threadIdx.x; block index times block size = threads before my block, plus my seat = my unique global slot.
Compute the global index for block 3, block size 128, thread 10.
.
What does mean and why use (N + T - 1) / T?
Ceiling rounds up; the +T-1 nudge forces integer division (which rounds down) to round up so no elements are left uncovered.
Why is if (idx < N) necessary?
Because numBlocks × threadsPerBlock usually overshoots ; extra threads must not touch out-of-bounds memory.
Why are registers faster than global memory?
Registers are on-chip (millimetres of wire, ~1 cycle); global VRAM is off-chip (centimetres + controller, hundreds of cycles). Distance costs time.