Streaming multiprocessors (SM)
Overview
The Streaming Multiprocessor (SM) is the fundamental execution unit inside a GPU. Think of it as a mini-processor optimized for parallel execution – while a CPU might have 4-16 cores, a modern GPU has dozens of SMs, and each SM can execute hundreds of threads simultaneously.
This is hardware-level parallelism: multiple SMs run completely independently, each jugling hundreds of threads. The GPU scheduler distributes thread blocks across SMs – one SM might render pixels0-1023 while another renders 1024-2047.
Architecture of a Single SM
What's Inside an SM?
Why this structure? Each component targets a specific bottleneck:
- CUDA cores → raw arithmetic throughput
- SFUs → offload expensive math (20-100 cycles on CPU, 1-2 cycles on SFU)
- Registers → eliminate memory latency (threads keep variables in registers)
- Shared memory → fast inter-thread communication (100x faster than global memory)
Hierarchical Execution Model
The SM executes threads in a strict hierarchy:
Why 32 threads per warp? This is SIMT (Single Instruction Multiple Thread) – the warp scheduler issues one instruction that all 32 threads execute simultaneously. It's like SIMD (vector processing) but with more flexibility: threads can take different branches, though it costs performance (divergence).
Derivation of thread scheduling:
Each cycle, the warp scheduler:
- Picks an eligible warp (threads not stalled on memory/dependencies)
- Issues an instruction to that warp's 32 threads
- The CUDA cores execute the instruction in parallel
If a warp stalls (waiting for memory), the scheduler immediately switches to another warp – this is why GPUs need thousands of threads: to hide latency.
Occupancy calculation:
Why occupancy matters: Higher occupancy → more warps available → better latency hiding. But occupancy is limited by:
- Registers per thread × threads per block ≤ Total registers
- Shared memory per block ≤ Total shared memory
- Blocks per SM ≤ Max blocks (usually 16-32)
Example: If your kernel uses 64 registers/thread and the SM has 65,536 registers, max threads = 65,536 / 64 = 1024. But if max warps = 64, you cap at 64 × 32 = 2048 threads anyway.
How Work Gets Distributed
Scenario: Launch a kernel with 10,000 threads organized as 40 blocks of 256 threads each. GPU has 80 SMs.
What happens:
-
Block assignment: The GPU's Grid Distribution Unit assigns blocks to SMs. Each block runs on exactly one SM from start to finish (no migration).
- With 80 SMs and 40 blocks, each SM gets ⌊40/80⌋ = 0 or 1 block initially, then receives more as blocks complete.
-
Warp creation: Each 256-thread block becomes 256/32 = 8 warps. The SM's warp scheduler now juggles these 8 warps.
-
Instruction issue: Each cycle, the scheduler picks one eligible warp and issues its next instruction to the CUDA cores.
- If Warp 0 is fetching from memory (300 cycles), the scheduler runs other eligible warps while Warp 0's data is in flight.
Why this step? (Latency-hiding math done correctly) Suppose a global-memory access stalls a warp for 300 cycles, and a scheduler issues 1 instruction per cycle. To fully hide that stall you need roughly 300 independent in-flight warps (one per cycle of latency) — one SM's 8 warps from a single block are not enough to cover a 300-cycle stall alone. In practice we approach this by having many blocks resident at once: with a real SM cap of ~48-64 warps, plus multiple schedulers each issuing per cycle, we get closer to hiding hundreds of cycles. The lesson: maximize resident warps, because 8 warps only cover ~8 cycles of latency per scheduler pass.
Problem: Your kernel uses 128 registers per thread. SM has 65,536 registers. Block size = 256 threads.
Calculation:
- Registers needed per block: 256 × 128 = 32,768
- Max blocks per SM: 65,536 / 32,768 = 2 blocks
- Warps per block: 256/32 = 8 → Total warps = 2 × 8 = 16 warps
- If max warps/SM = 64, occupancy = 16/64 = 25%
Why this step? High register usage starves the SM of parallelism. With only 16 warps, and one scheduler issuing ~1 instruction/warp/cycle, you cover only a small slice of a 400-800 cycle global-memory stall — so a memory-bound kernel will stall. To hide hundreds of cycles you want tens of resident warps, which register pressure here prevents.
Fix: Reduce registers (algorithmic change), increase resident blocks/warps, or accept lower occupancy if compute-bound.
Memory Hierarchy and SM
Each SM has multiple memory tiers, each with distinct latency characteristics:
Note: Shared memory and the L1 data cache often share the same physical SRAM but are not the same thing — shared memory is explicitly managed (you index into it directly, no tag lookup), while L1 is hardware-managed with tag checks and eviction. Their latencies and behavior therefore differ by architecture.
Why shared memory? Threads in a block can cooperate via shared memory:
__shared__ float cache[256]; // Visible to all threads in block
cache[threadIdx.x] = data[i]; // Each thread writes
__syncthreads(); // Wait for all
float sum = cache[0] + cache[1] + ..; // Read others' dataShared memory is ~100x faster than global memory and enables algorithms like tiled matrix multiply, reductions, and prefix sums.
Bank conflicts (shared memory only): Shared memory is divided into 32 banks. If multiple threads in a warp access different addresses in the same bank, the accesses serialize. Design access patterns so thread i accesses bank i % 32. (Registers are private per-thread and do not suffer bank conflicts in this way.)
Warp Divergence
Why it feels right: The GPU marketing says "thousands of threads in parallel!"
Reality: Threads in a warp execute the same instruction. If threads take different branches:
if (threadIdx.x < 16) {
A; // Threads 0-15
} else {
B; // Threads 16-31
}The warp must execute both paths serially:
- Execute A with threads 0-15 active (16-31 masked)
- Execute B with threads 16-31 active (0-15 masked)
Performance cost: 2× the instructions. For complex branches, even worse.
Steel-man: The GPU does allow divergence for flexibility (unlike pure SIMD). But it's not free – use it sparingly.
Fix: Reorganize data so threads in a warp take the same branch. Example: sort data by category before processing.
Recall Explain to a 12-year-old
Imagine you have a big homework assignment: color 10,000 squares in a grid. You hire 80 helpers (the SMs). Each helper has 32 crayons (CUDA cores) and can color 32 squares at once – but all 32 must use the same color at the same time.
You divide the 10,000 squares into groups of 256 (blocks) and give each helper a group. The helper further splits their 256 squares into 8 mini-groups of 32 (warps). Every second, they pick one mini-group and color all 32 squares with the same crayon stroke.
If a mini-group is waiting for a new crayon box to arrive (memory fetch), the helper switches to another mini-group immediately – no wasted time! But a crayon box can take a long time to arrive (like 300 seconds), so you actually need lots of mini-groups waiting so the helper always has something to color. That's why you need thousands of threads: to fully cover the long waits.
And here's a trick: if your instructions say "color squares 0-15 red and 16-31 blue," the helper has to do two strokes (red, then blue) instead of one. That's slower! So it's best when all 32 squares in a mini-group need the same color.
Or: Streaming Machine – streams of threads flow through it continuously.
Connections
- CUDA-cores – The arithmetic units inside each SM
- Warp-scheduling – How the SM picks which threads run each cycle
- Shared-memory – Fast inter-thread communication within an SM
- Thread-blocks – The unit of work assigned to an SM
- Occupancy-optimization – Tuning resource usage to maximize active warps
- GPU-memory-hierarchy – How SMs access L1/L2/global memory
- Tensor-cores – Specialized matrix units in modern SMs
#flashcards/hardware
What is a Streaming Multiprocessor (SM)?
How many threads are in a warp?
What limits occupancy on an SM?
Why does the SM have multiple warp schedulers?
Roughly how many in-flight warps are needed to hide a 300-cycle memory stall at 1 instruction/cycle?
What happens during warp divergence?
What is shared memory in an SM, and how does it relate to L1?
How are thread blocks assigned to SMs?
What is the role of registers in an SM?
Why can't you run 10,000 threads on one SM?
What is a bank conflict in shared memory?
Concept Map
Hinglish (regional understanding)
Intuition Hinglish mein samjho
Hinglish (regional understanding)
Intuition Hinglish mein samjho
Chalo is concept ko simple tarike se samajhte hain. Streaming Multiprocessor, yaani SM, ek GPU ke andar ka sabse basic execution unit hota hai — isko tum ek chota sa mini-processor samajh sakte ho jo parallel kaam ke liye bana hai. Jaise ek CPU mein 4-16 cores hote hain, waisa hi ek modern GPU mein dozens of SMs hote hain, aur har ek SM hundreds of threads ko ek saath chala sakta hai. Har SM ke apne CUDA cores (arithmetic ke liye), apna register file (fast thread-local memory), apni shared memory (threads ke beech cooperation ke liye), aur apne warp schedulers hote hain. Matlab har SM ek independent mini-factory ki tarah kaam karta hai — ek SM shayad pixels 0-1023 render kare, doosra 1024-2047. Yahi hai hardware-level parallelism.
Ab important baat — threads ek strict hierarchy mein chalte hain: Thread se Warp (32 threads ka group), Warp se Block, aur Block se Grid. Yeh 32 threads ka warp jo hai, yahi SIMT ka core idea hai — scheduler ek hi instruction issue karta hai aur saare 32 threads usko ek saath execute karte hain. Sabse clever cheez yeh hai ki jab koi warp memory ka wait kar raha ho (stall ho jaye), toh scheduler turant doosre warp par switch kar deta hai. Isiliye GPU ko hazaaron threads chahiye — taaki latency chhup jaaye aur cores kabhi khaali na baithein. Isko latency hiding kehte hain, aur yahi GPU ki asli power hai.
Yeh sab isliye matter karta hai kyunki jab tum GPU programming karoge (CUDA, deep learning, ya graphics), toh performance directly depend karti hai occupancy par — matlab kitne active warps ek SM par chal rahe hain compared to maximum possible. Higher occupancy means better latency hiding aur faster execution. Lekin yeh occupancy limited hoti hai registers per thread aur shared memory se — agar ek thread bahut zyada registers use kare toh kam threads fit honge. Toh agar tum ML models train karoge ya kernels likhoge, in resource limits ko samajhna hi tumhe fast vs slow code ka difference sikhaayega. Yahi foundation hai efficient GPU code likhne ka.