6.2.2 · D4GPU Architecture

Exercises — Streaming multiprocessors (SM)

2,907 words13 min readBack to topic

This page is a self-test ladder for Streaming Multiprocessors (SM). Each problem states cleanly, then hides a full worked solution in a collapsible callout — try first, then reveal. Levels climb from recognising the parts of an SM to synthesising an occupancy strategy from scratch.

Every number here is machine-checked (see the verify block). Before we start, one shared vocabulary reminder, built from zero so no symbol arrives unearned.

Figure — Streaming multiprocessors (SM)

Read the picture above like a nesting doll: 32 threads make one warp (blue lane), several warps make a block (the outlined box), and the SM holds several blocks at once. Every exercise is just counting how many of these dolls fit under some limit.


Level 1 — Recognition

L1.1 — Warps in a block

Problem. A block contains threads. How many warps is that?

Recall Solution

A warp is always exactly threads (never a setting you change). So we divide: What we did: grouped threads into the hardware's fixed 32-wide bundles. Why: the warp scheduler never sees individual threads — it only ever issues to whole warps, so warps are the real unit of scheduling.

L1.2 — Name the memory tiers by speed

Problem. Put these four in order from fastest to slowest access: L2 cache, registers, shared memory, global DRAM.

Recall Solution

Using the SM latency ladder from the parent note: Why this order: each step moves the data physically farther from the CUDA cores. Registers sit inside the core; shared memory is on-chip SRAM with no tag lookup; L2 is shared across the chip; DRAM is off-chip entirely. Farther = more cycles. See GPU-memory-hierarchy.

L1.3 — Which unit does sqrt?

Problem. Your kernel calls sqrtf(x). Which SM component handles it: a CUDA core, an SFU, or a Load/Store unit?

Recall Solution

The Special Function Unit (SFU). CUDA cores do plain integer/float add and multiply; Load/Store units move memory; SFUs are the dedicated hardware for transcendentals (sin, cos, exp, sqrt). Why it exists: these functions cost 20–100 cycles if emulated with basic arithmetic, but ~1–2 cycles on the SFU.


Level 2 — Application

L2.1 — Register-limited block count

Problem. A kernel uses registers per thread. The SM has registers. Block size is threads. How many whole blocks fit on one SM, limited by registers?

Recall Solution

Step 1 — registers per block. Every thread needs its own copy: Step 2 — how many fit. Divide the SM's supply, then floor (you can't have a partial block resident): Why floor: a block is all-or-nothing on an SM. If only enough registers remain for half a block, that block simply waits. See Occupancy-optimization.

L2.2 — Occupancy from that block count

Problem. Continuing L2.1: with resident blocks of threads, and a max of warps per SM, what is the occupancy?

Recall Solution

Warps resident: warps. Occupancy: What this means: only a quarter of the scheduler's warp slots hold work, so latency-hiding is weak — see L3.

L2.3 — Threads a register file can hold

Problem. A kernel uses registers per thread on an SM with registers. If registers were the only limit, how many threads could be resident?

Recall Solution

But watch the second cap: if the SM also allows at most warps, that is threads. Here , so registers are the binding limit. Rule: actual occupancy is set by whichever resource runs out first.


Level 3 — Analysis

L3.1 — How many warps to hide a stall?

Problem. A global-memory load stalls a warp for cycles. A single scheduler issues instruction per cycle. Roughly how many independent in-flight warps do you need to fully hide that stall? Do resident warps (from L2.1) suffice?

Recall Solution

The model: while the stalled warp waits, the scheduler must find a different ready warp to issue every cycle for cycles. Why one instruction per warp, then it stalls again: in a memory-bound kernel the next thing most warps need to do is itself another dependent global load (or an op that consumes the value still in flight). So after the scheduler issues one instruction to a warp, that warp is immediately waiting again on its own long-latency access. It therefore contributes only about one useful cycle of cover before dropping out. That is why we need many distinct warps rather than one warp used repeatedly: Do 16 warps cover it? No — warps cover only about cycles of the -cycle hole per scheduler pass. The remaining cycles the scheduler sits idle. Lesson: low occupancy directly starves latency-hiding. This is why the parent note screams "maximise resident warps."

Figure — Streaming multiprocessors (SM)

The bar picture makes it visceral: the tall red bar is the -cycle stall; the short chalk-blue stack of warps barely dents it.

L3.2 — Occupancy is not linear with speed

Problem. You raise occupancy from ( warps) to ( warps). Does your compute-bound kernel — one that rarely touches global memory — get faster? Explain.

Recall Solution

No. Occupancy helps only by hiding latency. A compute-bound kernel is already keeping the CUDA cores busy; its bottleneck is arithmetic throughput, not waiting on memory. Extra warps just queue for the same busy ALUs. What changes vs. what doesn't: for a memory-bound kernel, going warps roughly doubles the latency you can hide and can noticeably help. For a compute-bound one, you may see near-zero improvement — you might even lose speed if the extra warps needed a register cut that spilled to memory. The analytical point: occupancy is a ceiling raiser for latency hiding, not a throughput multiplier.

L3.3 — Divergence cost

Problem. In a -thread warp, threads take branch A and threads take branch B, each branch being instructions. How many instruction-issues does the warp cost, versus a non-diverging warp of instructions?

Recall Solution

Under SIMT, a warp issues one instruction for all lanes. When lanes split, the hardware runs both paths in sequence, masking the inactive half each time: Non-diverging cost is . So this is a slowdown, and half the lanes are idle during each path. Key subtlety: cost scales with the number of distinct paths taken, not the number of threads. If all 32 took branch A, cost stays — no divergence penalty at all.


Level 4 — Synthesis

L4.1 — Pick a block size under two limits

Problem. An SM has registers, allows at most warps ( threads) and at most blocks resident, and has KB shared memory. Your kernel uses registers/thread and KB shared memory per block. You may pick block size (all multiples of 32). Which gives the highest occupancy?

Recall Solution

For each block size, compute the resident-block cap from every limit, take the minimum, then convert to warps.

Registers. Per thread cost , so per SM the register file supports threads regardless of block size → not the binding limit here (it exactly matches the -thread warp cap).

Shared memory. Each block wants KB and the SM has KB, so blocks max (the KB units cancel, leaving a pure count).

Block/thread caps, per block size:

  • 128 threads (4 warps): thread cap blocks; block cap ; SMEM . Min blocks warps → 100%.
  • 256 threads (8 warps): thread cap blocks; SMEM ; block cap . Min warps → 100%.
  • 512 threads (16 warps): thread cap blocks; SMEM . Min warps → 100%.

Answer: all three reach warps = occupancy, because this kernel is light on registers and shared memory. Insight: when no resource is scarce, occupancy is decided by the raw warp cap, and block size becomes a secondary choice (favour a size divisible into the caps and good for your shared-memory tiling — here 256 is a safe default).

L4.2 — Now make shared memory the villain

Problem. Same SM as L4.1, but the kernel now uses KB shared memory per block (a big tiling buffer). Block size ( warps). What is occupancy, and what is the binding limit?

Recall Solution

Shared memory cap: blocks. Register cap: blocks. Block cap: . Min blocks. Warps: . Occupancy: . Binding limit: shared memory. What to do: shrink the tile (smaller SMEM per block) or split the buffer, trading a bit of data reuse for more resident blocks. This is the classic Occupancy-optimization tug-of-war between reuse and parallelism.


Level 5 — Mastery

L5.1 — End-to-end: diagnose and fix a slow kernel

Problem. A memory-bound kernel runs at occupancy ( warps of ). Profiling: it uses registers/thread, block size , on an SM with registers, -warp cap, negligible shared memory. Global loads stall cycles. (a) Why is it slow? (b) If you refactor down to registers/thread, what is the new occupancy? (c) Roughly how much more of the -cycle stall can now be hidden?

Recall Solution

(a) Diagnosis. From L2.1/L2.2: regs/thread → blocks → warps → . A -cycle stall needs warps to fully hide (L3.1 logic); warps cover cycles per scheduler pass, so the SM idles most of every stall. Root cause: register pressure caps resident warps far below what latency-hiding needs.

(b) After refactor to regs/thread. Registers per block: . Blocks: . Warps: . Occupancy: .

(c) Latency hidden. Warps doubled , so the hideable slice per scheduler pass roughly doubles ( cycles). Still far short of , so occupancy alone won't fully cure this — you'd also pursue memory-coalescing and reuse (shared-memory tiling) to reduce the number of stalls. Mastery point: occupancy raises the ceiling for how much latency you can hide, but the complementary — and often bigger — lever is to reduce the latency itself, by coalescing accesses and reusing data in shared memory so there are fewer long stalls to hide in the first place.

L5.2 — Design a launch that saturates warps and uses shared memory

Problem. Target: exactly resident warps () on an SM with registers, -warp cap, KB shared memory. You will use block size ( warps). What is the maximum registers/thread and maximum shared memory/block you can afford while keeping resident blocks (needed for warps)?

Recall Solution

To hold blocks resident: Registers. Total across all resident threads . Threads resident . So per-thread budget: Shared memory. blocks share KB: Check: blocks block cap () ✓; warps cap ✓. Answer: stay at or below regs/thread and KB SMEM/block to hit . Exceed either and resident blocks drop below , and occupancy falls. This is the full budget-balancing act of Occupancy-optimization — and why Tensor Cores and thread block sizing are tuned hand-in-hand.


Recall Self-test recap (cloze)

A warp is always 32 threads. Occupancy = active warps divided by ::: max warps per SM. Resident block count is the ::: minimum over all resource limits (registers, shared memory, warp cap, block cap). Extra warps hide ::: latency — they do not add ALU throughput. Divergence cost scales with the number of ::: distinct branch paths taken, not the number of threads.

Prerequisite links used above: CUDA-cores · Warp-scheduling · Shared-memory · Thread-blocks · Occupancy-optimization · GPU-memory-hierarchy · Tensor-cores · Hinglish version.