GPUgpuarchitecturecompute unitoccupancyshared memory

What Is Actually Inside a GPU

A host CPU, a device with many compute units, an unusually large register file, a small block of programmer-managed on-chip memory per unit, and a large pool of high-bandwidth global memory. The register file being large — and being the thing that limits how many lanes stay resident — is the part that surprises people.

▶ Run the labFollow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
What are the parts of a GPU, and which of them ends up limiting how much parallelism I actually get?
What you wrote
A device you send a kernel and an array to, which returns a transformed array some time later.
What the hardware does
A hierarchy: the device holds many compute units; each compute unit holds lanes, a slice of a very large register file, and a small programmer-managed scratchpad; all units share a global memory pool and a modest cache in front of it.
Two GPU-specific limits fall directly out of this structure and explain most disappointing kernels: the register file bounds how many lane groups can be resident at once, and the scratchpad is the only fast memory you control explicitly. Neither has a CPU analogue that behaves the same way.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

The parts

SIMPLIFIEDOmits the memory controllers and interconnect, the texture and constant paths, tensor or matrix units present on recent parts, and multi-die packaging. It also flattens a cache hierarchy that has more levels than shown, and vendor terminology for every box differs.

The host CPU owns the program and the decisions; the device owns the lanes. Work reaches the device as a kernel launch, which the device scheduler distributes across compute units as many groups of lanes. Each compute unit runs several groups concurrently, switching between them whenever one stalls.

Inside a compute unit there are three resources that matter to a programmer. The lanes perform the arithmetic. The register file is unusually large by CPU standards, because every resident lane needs its own registers simultaneously — this is the resource that decides how many groups can be resident. The on-chip scratchpad is a small, fast, explicitly managed memory shared by a group, and unlike a CPU cache it is something you place data into deliberately.

Below all of it sits global memory — a large pool with high bandwidth and high latency, fronted by a cache that is small relative to the number of lanes competing for it. The whole design assumes that global memory latency will be covered by having other work available, which is why the register file is sized to keep many groups resident rather than to make any one group fast.

launch + datalane groupsHost CPUPCIe / host interconnectDevice schedulerCompute unit 1Compute unit 2Compute unit NRegister file (per unit)On-chip scratchpad (per unit)Shared device cacheGlobal device memory
UserLLMAgentToolDataDecisionHumanGuardrail

The memory hierarchy is explicit, and that is the point

On a CPU the hierarchy is automatic: you access memory, and hardware decides what to cache. You influence it only indirectly, through layout and access order — the subject of Spatial Locality and Memory Moves in Lines, Not Variables. On a GPU the fast on-chip memory is *addressable*. You copy a tile into it, the group works out of it, you write results back. The hierarchy is part of the programming model rather than an optimisation the hardware performs on your behalf.

That is a genuine trade. It means a well-written kernel can guarantee reuse instead of hoping for it, which is exactly what makes tiled matrix multiplication so effective — the same idea as Matrix Tiling: Same Arithmetic, Ten Times Faster, but with the tile placement made explicit rather than left to a cache. It also means the responsibility is yours: forget to stage data and every lane reads global memory independently, which is the most common reason a first kernel underperforms.

The relative costs below are the ones worth internalising. Register access is effectively free; scratchpad is close; global memory is far enough away that a kernel touching it on every operation will be limited by it regardless of how much arithmetic capacity the device has.

Relative access cost within a GPU, in units of one register read — 1 unit ≈ one register readGPU-SPECIFIC
Register×1
On-chip scratchpad×5
Shared device cache×40
Global device memory×300
Host memory over the bus×5000
Ratios, not times. Absolute latencies depend on the processor, its clock, the memory it is attached to and what else is running — publishing them would be wrong everywhere except one machine. The bars are log-scaled, so each step is larger than it looks.
RegisterPer lane; the register file is large precisely so many groups stay resident
On-chip scratchpadExplicitly managed and shared within a group; bank conflicts can make it worse
Shared device cacheSmall relative to the number of lanes contending for it
Global device memoryHigh bandwidth, high latency; hidden by switching groups, not by avoiding it
Host memory over the busA different device entirely — see The Transfer You Forgot to Count

Occupancy: why registers limit parallelism

The scheduler hides memory latency by switching to another resident group. That only works if there *are* other resident groups, and residency is bounded by the physical resources each group consumes: registers per lane and scratchpad per group. A kernel that uses many registers per lane allows fewer groups to be resident, which leaves the unit with less to switch to when one group stalls.

This produces a counter-intuitive tuning result that has no CPU equivalent. Adding local variables — or unrolling a loop so aggressively that more values must be held live — can *reduce* throughput, because it raises register pressure and drops occupancy. The kernel got better at instruction-level work and worse at latency hiding, and on this device latency hiding was the resource that mattered.

Occupancy is not a goal in itself, and maximising it is not automatically right: a kernel with low occupancy but excellent reuse from the scratchpad can beat a high-occupancy kernel that keeps going to global memory. The useful framing is that occupancy is the budget for hiding latency, and you only need enough of it to cover the latency you actually incur.

What limits residency, and what to do about it
LimiterSymptomLever
Registers per laneOccupancy drops as the kernel grows; adding variables makes it slowerSimplify the kernel, cap unrolling, split into two kernels, or cap registers via compiler flags
Scratchpad per groupFewer groups resident than the register budget alone would allowUse smaller tiles, or trade scratchpad for recomputation
Group sizeUnits are partly idle because groups do not divide evenlySize groups to the hardware lane-group granularity
Not enough total workOccupancy is fine but units sit idleBatch more work per launch — see The Transfer You Forgot to Count
Nothing — reuse is highLow occupancy, near-peak arithmeticLeave it alone; occupancy was never the constraint

Key points

  • A GPU is a hierarchy: device → compute units → lanes, over a large register file, a small explicit scratchpad and a big high-latency global pool.
  • The register file is large because many lane groups must be resident at once; residency is how latency gets hidden.
  • Fast on-chip memory is programmer-managed, so reuse is something you guarantee rather than hope for.
  • Register pressure limits occupancy, which is why adding local variables or unrolling harder can make a kernel slower.
  • Occupancy is a budget for hiding latency, not a score — high reuse can beat high occupancy.

Where the Data Is

Change an input and watch which number moves — and which one refuses to.

Where a value can be, and roughly what each costs relative to a register — 1 unit ≈ one register accessSIMPLIFIED
Register×1
L1 cache×4
L2 cache×14
L3 cache×45
DRAM×200
NVMe storage×100000
Network round trip×10000000
Ratios, not times. Absolute latencies depend on the processor, its clock, the memory it is attached to and what else is running — publishing them would be wrong everywhere except one machine. The bars are log-scaled, so each step is larger than it looks.
RegisterAlready in the core. Effectively free.
L3 cacheUsually shared between cores, so other work affects your hit rate.
DRAMTwo orders of magnitude past L1. This is the cliff.
NVMe storageAnother three orders of magnitude, and the OS gets involved.
Network round tripDifferent universe. Included to keep the earlier rows in perspective.

The exact ratios vary by machine and the absolute times vary far more, which is why none are shown. What is stable enough to build intuition on is the shape: each level is several times the one above, and the gap between the last cache level and memory is the one that decides most program performance.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Host → device: a kernel launch specifies the work and the group shape; the fixed cost is per launch, not per element.
  2. 2
    Scheduler → compute units: groups are assigned to units up to the limit set by register and scratchpad consumption.
  3. 3
    Compute unit → lanes: one instruction is issued across the lanes of a group; a stalled group is switched out for a ready one.
  4. 4
    Lanes → scratchpad: the kernel explicitly stages a tile so the group can reuse it without returning to global memory.
  5. 5
    Compute unit → global memory: whatever was not staged goes to the high-latency pool, covered only if other groups are runnable.
What people conclude from this — wrongly
  • "Maximise occupancy" — occupancy only needs to be high enough to cover the latency you actually incur; a high-reuse kernel can be fastest at low occupancy.
  • "The scratchpad is just a faster cache" — it is not automatic. Nothing arrives in it unless the kernel puts it there.
  • "More unrolling is better, as it is on a CPU" — on a GPU it can raise register pressure and reduce the residency that hides latency.
  • "Global memory is fast because bandwidth is high" — bandwidth is high and latency is also high; the design covers latency with concurrency, not with speed.

Consequences, controls and cost

What it causes
  • • Kernels that stage data into the scratchpad can achieve reuse that a CPU cache would only give you by luck of layout.
  • • Kernels that use too many registers lose throughput even though their instruction stream got better.
  • • Small launches waste the device: the fixed launch cost is amortised over too little work.
  • • Two kernels with identical arithmetic can differ by an order of magnitude based on where their data lived.
What you can do
  • • Stage reused data into the on-chip scratchpad explicitly instead of relying on the cache to notice the reuse.
  • • Watch register pressure when unrolling or adding locals; check achieved occupancy after any such change rather than assuming.
  • • Size lane groups to the hardware granularity so units are not left partly idle.
  • • Give the device enough work per launch that the fixed launch cost disappears into the total.
How to see it
  • • Read achieved occupancy from the vendor profiler and compare it against the theoretical limit implied by register and scratchpad use.
  • • Check the register count the compiler assigned per lane; most toolchains report it and most allow a cap.
  • • Compare achieved global memory throughput against the device peak — near peak with low arithmetic means memory-bound.
  • • Vary group size and tile size and measure; the interaction is not reliably predictable from first principles.
What it costs
  • • Explicit memory management is more code and more opportunity for error than relying on a cache.
  • • Tile sizes tuned for one device are frequently wrong on the next generation, so the tuning is perishable.
  • • Optimising for occupancy can conflict with optimising for reuse; the two must be balanced by measurement.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • SIMPLIFIEDA schematic. Real devices add texture and constant paths, matrix units, more cache levels and multi-die interconnects; the box names differ per vendor.
  • GPU-SPECIFICDiscrete GPUs. Integrated parts share memory with the CPU, so the global-memory and host-transfer rows collapse together.

Misconceptions

Claim
“GPU cores are like CPU cores.”
Reality
A GPU "core" in marketing terms is a lane, closer to one SIMD lane of a CPU vector unit than to a CPU core. A compute unit — with its own scheduler and register file — is the nearer analogue to a core, and there are far fewer of those.
Claim
“The on-chip memory is a cache, so the hardware will use it.”
Reality
The scratchpad is addressable and manually managed. If the kernel does not copy data into it, it stays empty while every lane reads global memory independently.
Claim
“Higher occupancy always means better performance.”
Reality
Occupancy is a means of hiding latency. Past the point where latency is covered, more occupancy buys nothing, and pursuing it by shrinking tiles can destroy the reuse that mattered more.