Part I — The GPU · Interlude (optional, before Part II)

The hardware underneath: pipes, DRAM and clocks

~50 min read2 animated figures1 platelab 4b · ~45 min on your GPU

The interlude in one paragraph

Chapters 2–5 gave you a model of the GPU that is accurate enough to predict and profile real kernels, and they rounded three things off to get there. “128 FP32 lanes per SM” is one of several execution pipes, and the others run at very different speeds: an exp goes through a unit an eighth as wide, a double through one sixty-four times slower. “672 GB/s of DRAM” is 24 independent channels, each made of banks that must open a row before they can deliver a byte, which is why the access pattern matters below the 32-byte sector. And “2.51 GHz” is a rated number; the real clock moves with power and temperature, and moves compute-bound kernels with it. This interlude opens those three boxes. Nothing in Part II depends on it, but it explains several things Part II will simply state: why softmax is never limited by its exp on your card but attention almost is on a B200, why a stray 0.5 can make a kernel ten times slower, why the practical DRAM ceiling sits near 88%, and why random access is fine as long as the unit of randomness is large.

In this interlude

  1. What chapters 2–5 rounded off
  2. Issue and execute: the pipes of a quadrant
  3. Latency: waiting on the compiler, waiting on a scoreboard
  4. The SFU, and the price of exp
  5. FP64 and the accidental double
  6. The rest of the quadrant
  7. Below the L2
  8. Clocks and power
  9. The rest of the chip, and where to read more
  10. Lab 4b: measure the hardware
  11. Check your understanding

1. What chapters 2–5 rounded off

Every model of a machine leaves things out; the useful question is whether what it leaves out ever changes a decision. Part I’s model left out three things that occasionally do:

Part I saidWhat is underneathWhen it changes a decision
An SM has 128 FP32 lanes, 32 per schedulerEach quadrant has several execution pipes of different widths: FP32/INT32, a special function unit (SFU) for exp, sin, rsqrt, a tiny FP64 unit, load/store, a tensor core, a scalar “uniform” datapathKernels heavy in transcendental functions, integer address math or (accidentally) double
DRAM delivers 672 GB/s in 32-byte sectors24 channels of 8 bits, each with banks; a bank opens a row (a few KB) before reading it, and closes it before opening anotherRandom access with small units: gathers, embedding lookups, paged caches
The clock is 2.51 GHzThe rated boost clock; the real one moves with power, temperature and load, and the memory clock moves separatelyComparing measurements, and kernels near the ridge point

Each section below follows the same pattern as chapter 4: a number from NVIDIA’s documentation, what it predicts, and an experiment in lab 4b that checks it on your card. Two animated figures show the two places where timing is the whole story: one quadrant’s pipes, cycle by cycle, and one memory channel, nanosecond by nanosecond.

2. Issue and execute: the pipes of a quadrant

Chapter 2’s scheduler picks one ready warp per cycle and issues its next instruction. Here is the part it skipped: where the instruction goes. NVIDIA’s diagram of the RTX Blackwell SM shows each of the four quadrants (Nsight calls them SMSPs) with its own L0 instruction cache, warp scheduler, dispatch unit and 64 KB register file, feeding these units:

Two numbers describe a pipe. Its throughput is how often it can accept a new warp instruction: a 32-lane pipe takes one every cycle, a 16-lane pipe one every 2 cycles, the 4-lane SFU one every 8. Its latency is how long until the result can be used, about 4 cycles for an FMA. A pipe is like the conveyor of chapter 3’s Little’s law: it can hold several instructions at once, at different stages.

NVIDIA publishes the throughputs as results per clock per SM in the CUDA C++ Best Practices Guide (the column for compute capability 12.0, which is your card). Dividing by 4 quadrants and then into 32 threads gives the cycles one warp instruction occupies its pipe:

Operation (SASS)Results / clock / SMCycles per warp instructionRelative to FFMA
FP32 add, multiply, FMA (FADD, FMUL, FFMA)12811×
Integer add, integer min/max, warp vote12811×
FP16×2 (HFMA2, two halves per lane)64 (128 halves)2same FLOPs
Integer multiply-add (IMAD), 3-input add6421/2
Shifts, logic (LOP3), FP32 min/max6421/2
Conversions int↔float, FP16↔FP326421/2
Warp shuffle (SHFL)3241/4
MUFU: exp2, lg2, rcp, rsqrt, sin, cos1681/8
Population count, count leading zeros1681/8
FP64 (DADD, DMUL, DFMA) and conversions to/from FP642641/64

The scheduler is a pipe too: one warp instruction per cycle per quadrant, whatever kind it is. That single fact has a consequence chapter 4 hinted at. FP32 peak needs an FFMA issued every cycle, so every other instruction in the loop, an address computation, a load, a loop counter, a comparison, takes an issue slot that an FFMA could have used. Peak FLOP/s is a statement about a loop made only of FMAs. Real loops get close only when the FMAs vastly outnumber everything else, which is what tiling buys in chapter 7: each loaded value is reused in many FMAs.

The FP32 and INT32 units are the same units

Turing had separate INT32 units, so the integer adds and logic of index math ran beside the float math. Ampere and Ada made half of each quadrant’s units switchable between the two. On RTX Blackwell all 32 are: in a given cycle a unit does float work or integer work, not both. Index math now competes with the FMAs for the units as well as for issue slots. That is one reason the compiler hoists index math out of loops, and one reason the uniform datapath (section 6) exists.

Your move: an inner loop contains 6 FFMA, 2 IMAD for addresses, 1 load from shared memory and 1 branch. Ignore latency. What fraction of FP32 peak can it reach at best, and what is the limit?

Issue. 10 instructions per iteration, one per cycle: at least 10 cycles. The two FMA pipes together accept one warp instruction per cycle, and they need only 8 (6 FFMAs and 2 IMADs, the IMADs on the heavy half), so they are not the limit. 6 useful FFMAs in 10 issue cycles: 60% of peak at best. Unrolling (fewer branches and address updates per FFMA) and register tiling (more FFMAs per load) are the two ways up.

3. Latency: waiting on the compiler, waiting on a scoreboard

An instruction that needs the result of an earlier one must wait for it. How the hardware knows how long to wait depends on the pipe.

Fixed latency. FP32, integer and logic instructions always take the same number of cycles; the Nsight Compute Profiling Guide states that “FFMA-to-FFMA dependency latency is 4 cycles”. Because it is fixed, the hardware does not track it at all. The compiler does: every SASS instruction carries a few control bits, set by the compiler, that tell the scheduler how many cycles to hold this warp before its next instruction. (NVIDIA doesn’t document their format; Jia et al. reverse-engineered it, see section 9.) A warp held this way shows up in Nsight Compute as the stall reason wait: “waiting on a fixed latency execution dependency”.

Variable latency. A shared-memory load might hit a bank conflict; a global load might hit L1, L2 or DRAM. The compiler cannot know, so the hardware keeps a small set of scoreboards per warp: the instruction that produces a value sets a scoreboard, the one that consumes it waits until the scoreboard clears. There are two kinds, and they are the two scoreboard stalls you met in chapter 5:

Latency is hidden the way chapter 3 hid DRAM latency, by Little’s law: work in flight = latency × throughput. For the FMA pipe that is 4 cycles × 1 warp instruction per cycle = 4 independent FMAs in flight per quadrant. They can come from 4 warps that each have one chain (thread-level parallelism, TLP) or from one warp with 4 independent chains (instruction-level parallelism, ILP), or any mix. Chapter 4’s compute benchmark used four chains per thread for exactly this reason.

What in Fig. 1 is real, and what is modelled?

Real (NVIDIA documentation): one issue per cycle per quadrant, 12 warp slots, a 64 KB register file, 32 FP32/INT32 lanes split into two pipes, a 4-lane SFU (16 per SM), FP64 at 2 per SM, 4-cycle FFMA latency, the meaning of each stall reason. Modelled: the SFU latency (18 cycles here) and the FP64 latency (72), a simple round-robin choice among ready warps, and every instruction going straight to its pipe. Real schedulers have more rules, and real code has loads, branches and register bank conflicts. Every frame comes from a small cycle-by-cycle model in assets/fig-pipes.js; the percentages in the readout are its output, not measurements. Lab 4b measures the real ones.

Reading Fig. 1 against chapter 5 gives a map from stall reason to hardware:

Stall reasonThe warp is waiting for…Hardware behind itUsual fix
waita fixed number of cycles set by the compilerFMA/ALU latency (4+ cycles)more independent instructions (ILP) or more warps
short_scoreboarda shared-memory, MUFU or dynamic-branch resultMIO: shared memory, SFUfewer bank conflicts, fewer/wider shared loads, fewer MUFU
long_scoreboarda global or local memory resultL1TEX, L2, DRAMmore bytes in flight (chapter 3), better locality
math_pipe_throttlea pipe that is still busyan oversubscribed pipe: SFU, FP64, or FMA at peakchange the instruction mix; or nothing, if it is the FMA pipe at peak
mio_throttleroom in the MIO queueshared memory or SFU saturatedwider shared loads, fewer conflicts
lg_throttleroom in the local/global queuetoo many small global accessesvectorize loads (float4), avoid local memory spills
not_selectedits turnanother ready warp was pickednothing: there is spare parallelism
Your move: a kernel’s inner loop is one long dependent chain of FFMAs per thread. How many warps per scheduler does it need to keep the FMA pipe full? And if you rewrite it with two independent chains per thread?

Four warps per scheduler with one chain each (4 cycles of latency, one issue per cycle): 16 warps per SM, a third of the 48-warp limit. With two chains per thread, two warps per scheduler suffice. Below that, Nsight Compute shows wait as the top stall and the issue slot idles, exactly like the first act of Fig. 1.

4. The SFU, and the price of exp

When you write expf(x) in a kernel, the compiler does not emit one instruction. The SFU only computes 2x (MUFU.EX2), approximately. To build an accurate expf, the compiler multiplies by log2 e, splits the result into an integer and a fraction, fixes up the rounding with a few FMAs, calls the SFU, and handles overflow and underflow. CUDA offers two versions, and the Programming Guide lists their accuracy:

FunctionRoughly compiles toMaximum error
expf(x)a handful of FMAs and one MUFU.EX22 ulp (units in the last place) over the whole range
__expf(x) (an intrinsic)one FMUL and one MUFU.EX22 + floor(|1.173·x|) ulp: grows with |x|

The same split exists for __sinf, __logf, __fdividef and others. The compiler flag -use_fast_math replaces every call with its intrinsic in one go. It implies --ftz=true (denormals flushed to zero), --prec-div=false and --prec-sqrt=false (fast, approximate division and square root) and --fmad=true. That is a lot of semantics to change with one flag; in this course we write intrinsics by hand where we want them and leave the rest accurate.

How much does it matter? Two cases, one where it doesn’t and one where it does.

Softmax on your card: not at all. A softmax over FP32 values reads 4 bytes and writes 4 bytes per element and does one exp. At the practical 590 GB/s that is about 74 billion elements per second. The SFUs can do 16 × 48 SMs × 2.51 GHz ≈ 1.9 trillion MUFU per second, 26× more. Softmax is memory-bound by a wide margin; whether its exp takes one MUFU or ten instructions doesn’t show up in its time. Chapter 4’s rule holds: on the roofline, removing work you don’t wait for buys nothing.

Attention: it depends on the card. In FlashAttention (Part III) the scores never touch DRAM. Each score costs about 4d FLOPs on the tensor cores (a dot product of length d to make it, and its share of the product with V) and one exp on the SFU, and both units work on the same tile at the same time. Compare their speeds:

Tensor FLOPs / clock / SM (FP16 in, FP32 accumulate, dense)MUFU / clock / SMTensor FLOPs per exp at balanceFLOPs per score, head dim d
RTX 50705121632256 at d = 64: the SFU needs 1/8 of the tensor time
B200 (datacenter Blackwell)8,19216512512 at d = 128: the SFU needs as long as the tensor cores

Tensor cores got 16× faster per SM between the two chips; the SFU did not. On a B200, attention with head dimension 128 spends as long on exp as on matrix multiplication, and the two can overlap at best. FlashAttention-4 (2026) responds by computing part of the exponentials without the SFU: a cubic polynomial evaluated with FMAs on the FP32 pipe, which has 8× the SFU’s throughput and is otherwise idle. On your card that trick isn’t needed. The lesson generalizes: a unit that didn’t scale becomes the bottleneck when everything around it did, and the fix is often to move its work to a unit that did.

The fourth and fifth acts of Fig. 1 show the same thing at the scale of one quadrant: one MUFU per instruction group saturates the SFU at one warp instruction per 8 cycles, but up to seven other instructions can issue in its shadow for free.

Your move: GELU with the tanh approximation, 0.5x(1 + tanh(√(2/π)(x + 0.044715x³))), runs as its own elementwise kernel over FP16 activations. Is it SFU-bound? What if it were fused into the epilogue of a matmul instead?

As its own kernel: no. It reads 2 bytes and writes 2 bytes per element, about 150 billion elements per second at 590 GB/s, and even an accurate tanhf (a few MUFU and a dozen FMAs) stays far below the SFU and FMA limits. Fused into a matmul epilogue, the bytes disappear and the kernel is limited by the tensor cores; the GELU then costs a few instructions per output element against roughly 2×K tensor FLOPs per output (K in the thousands). Still negligible. GELU only matters when K is small.

5. FP64 and the accidental double

GeForce cards are built for graphics and AI, which don’t need 64-bit floats, so NVIDIA gives them just enough FP64 to run code that uses it: 2 operations per clock per SM, 1/64 of FP32. (Datacenter chips like the B200 have far more.) That is fine until a kernel uses double without meaning to. The classic way:

__global__ void scale(float* y, const float* x, int n) {
  int i = blockIdx.x * blockDim.x + threadIdx.x;
  if (i < n) y[i] = x[i] * 0.5;    // 0.5 is a double literal
}

C++ promotes x[i] to double, multiplies, and converts back. In SASS that is F2F.F64.F32, DMUL, F2F.F32.F64: three instructions at the FP64 rate, 64 cycles each per warp. The fix is one character, 0.5f. Other ways in: pow(x, 2.0), sqrt(2.0) used as a constant, a double constant in a header, an unsuffixed literal like 1e-6 in a comparison. The sixth act of Fig. 1 shows what it looks like: the issue slot idles 98% of the time while the FP64 unit chews through one warp instruction every 64 cycles.

Will this slow a memory-bound kernel like scale? Each element needs three FP64 operations, and the GPU does 2 × 48 × 2.51 GHz ≈ 240 billion per second: 80 billion elements per second, about the same as the 74 billion that DRAM allows. So the accident turns a cleanly memory-bound kernel into one limited by both, and any kernel with more than one stray double per element becomes FP64-bound. Two ways to catch it, both cheap:

cuobjdump -sass ./prog | grep -E "DMUL|DADD|DFMA|F2F\.F64"      # any FP64 instruction in the binary
nvcc -Xptxas --warn-on-double-precision-use ...                   # ptxas warns at compile time

The second is worth adding to every Makefile in this course that has no business using double. In Nsight Compute, the Compute Workload Analysis section shows the fp64 pipe’s utilization; anything above zero in a float kernel is an accident.

6. The rest of the quadrant

A few more units appear in Nsight Compute’s pipe list. You don’t need to tune for them, but you will see their names:

In the Compute Workload Analysis section, Nsight Compute shows the utilization of every one of these pipes. When the FMA pipe is not the busiest, that chart tells you which one is.

7. Below the L2

Chapter 3 stopped at the L2: on a miss, a 32-byte sector comes from “DRAM”. Here is the path it takes, from top to bottom:

  1. L2 slices. The 48 MB L2 is not one cache but many slices; each physical address belongs to exactly one slice, picked by a hash of its bits. NVIDIA doesn’t publish the slice count for your chip; Nsight Compute’s lts__ metrics (L2 “tag stage”) count per slice.
  2. A crossbar connects every SM to every slice, so any SM can reach any address at the same cost.
  3. Six memory controllers, each 32 bits wide, each attached to one 2 GB GDDR7 chip: 192 bits, 12 GB.
  4. Four channels per chip. A GDDR7 chip is split into four independent 8-bit channels, so the card has 24. Each pin runs at 28 Gbit/s, so each channel moves 28 GB/s, and 24 × 28 = 672 GB/s, the number from chapter 1. GDDR7 sends 1.5 bits per symbol by using three voltage levels (PAM3) instead of two; that is how it reaches 28 Gbit/s per pin.
  5. Bursts. One access moves 32 bits per pin over 8 pins: 32 bytes, exactly one L2 sector, in about 1.14 ns.

The address hash matters more than it looks. Early GPUs picked the channel from a few plain address bits, so a strided access pattern, like walking down a matrix column, could land on one channel while the others sat idle (“partition camping”). Today’s hash scatters any regular pattern over all channels and slices; you no longer need to think about it.

Banks and rows

Inside each channel the memory is divided into banks, and each bank into rows of a few KB. A bank can only read from one row at a time, through its row buffer:

On top of that, DRAM limits how many rows may be activated in any short window, to bound the current the chip draws (DDR’s rule is four; the model below uses the same). A stream of reads that keeps hitting open rows runs at full speed. A stream where every burst needs its own row pays an ACTIVATE and a PRECHARGE per 32 bytes and hits the activation limit. Fig. 2 runs one channel through five traffic patterns.

What in Fig. 2 is real, and what is modelled?

Real: six 32-bit controllers and six GDDR7 chips, four 8-bit channels per chip, 28 Gbit/s per pin, one 32-byte burst per 1.14 ns per channel, 672 GB/s in total, and the ACTIVATE → READ → PRECHARGE protocol. Modelled, illustrative and not measured: 16 banks per channel (as in GDDR6; GDDR7’s exact organisation is in the JEDEC standard, which we have not checked), 2 KB rows, the timings (about 11 ns to activate, 11 ns to precharge, at most 4 activates per 20 ns, a few ns to turn the bus around), the number of L2 slices drawn, and the controller’s policy (oldest request first, reads before writes, writes sent in batches). Refresh is not modelled, so streaming reaches 100% in the figure and about 88–92% on the real card. Each frame comes from the model in assets/fig-dram.js; the readout’s GB/s is the model’s bus utilization times 672.

The model’s numbers over the 110 ns window:

TrafficBursts per ACTIVATEBus busy (model)What limits it
Streaming read64~100%nothing (refresh, on real hardware)
Random 32 B sectors1~21%the activation limit: 4 rows per 20 ns
Random 128 B chunks4~60%activations and bank timing
Random 2 KB chunks64~100%nothing: as good as a stream
Copy (read + write)64~86%turning the bus around between read and write batches

This is where the practical ceiling of chapter 4 comes from. Even a perfect stream loses a few percent to refresh (every row must be rewritten every few tens of milliseconds, and the chip pauses for it) and to read/write turnaround (a kernel that both reads and writes makes the bus switch direction, and each switch costs idle nanoseconds). Hence chapter 4’s 590 GB/s, 88% of 672.

And this is the hardware reason behind a rule you will meet again in Part III: random access is fine when the unit of randomness is large. Chapter 3 already showed that a warp should use all 32 bytes of each sector it touches. Below the L2 the rule extends: scattered 32-byte sectors waste most of DRAM’s time on opening rows, while scattered chunks of a few KB stream almost as well as one contiguous array. An embedding table of 4 KB rows, or a KV cache stored in pages of several KB (Part III’s paged attention), can be read in random order at nearly full bandwidth.

One more rung: the TLB

Every address a kernel uses is virtual. Translating it to a physical address goes through a page table, cached in small TLBs (translation lookaside buffers) at each level. GPU memory is mapped in large pages, typically 2 MB, so a TLB with a few thousand entries covers gigabytes. But not unlimited gigabytes: chapter 3’s latency staircase can creep up past a few hundred MB, and accesses spread randomly over the whole 12 GB miss the TLB and pay a page-table walk on top of DRAM latency. NVIDIA doesn’t publish the TLB sizes; Jia et al. measured them on earlier GPUs by pointer-chasing with one access per page, and experiment H in lab 4b does the same on yours.

Your move: a recommendation model looks up rows of an embedding table at random: 64 values of FP16 per row. Using the model in Fig. 2, roughly what fraction of DRAM bandwidth do you expect? What changes at 1,024 values per row?

64 × 2 bytes = 128 bytes, four sectors per row looked up: the third act, about 60% in the model (lower on the real card, which also has refresh and TLB misses over a large table). At 1,024 values a row is 2 KB, a whole DRAM row’s worth: close to streaming. This is one reason embedding tables are often stored with wide rows, or with several lookups batched so that each request carries more bytes.

8. Clocks and power

Chapter 4 computed the peak as 6,144 lanes × 2 FLOPs × 2.51 GHz = 30.9 TFLOP/s. The 2.51 GHz is the rated boost clock: a promise that the card will reach it under typical load. The clock it actually runs at is chosen continuously by the card’s firmware (GPU Boost) as the highest frequency allowed by three limits: the power budget (250 W total graphics power on the 5070), the temperature, and the voltage. A cool card running light work usually goes above the rated boost; a card running heavy tensor-core work at its power limit may go below. RTX Blackwell changes its clock far faster than earlier GPUs (NVIDIA says about 1000×), so the clock can now move within a single frame, and so within a long kernel.

The memory clock is separate and much steadier: GDDR7 runs at its rated 28 Gbit/s whatever the SM clock does. That splits kernels into two kinds:

Three practical consequences:

Lab 4b measures the clock directly: a kernel reads the SM’s cycle counter (clock64()) at its start and end, and dividing cycles by nanoseconds gives the real frequency during that kernel.

Your move: you benchmark two kernels in the morning, then again after an hour of training on the same card. The compute-bound one is 9% slower; the memory-bound one is unchanged. What happened, and how would you confirm it?

The card got hot and the firmware lowered the SM clock, by about 9%. The memory clock didn’t change, so the memory-bound kernel didn’t notice. To confirm, log clocks.sm and temperature.gpu during both runs, and check the “clocks event reasons” in nvidia-smi -q -d PERFORMANCE, which should report a thermal or power limit.

9. The rest of the chip, and where to read more

Outside the SMs and the memory system, a few more units take part in every CUDA program:

Where to go deeper, once Part II is done:

TopicRead
DRAM internals, bursts, banks and channelsPMPP, 5th ed., ch. 6
How atomics are executed in the L2PMPP, 5th ed., section 9.3
Virtual memory and TLBs on GPUsPMPP, 5th ed., appendix C
Streams, copy engines, overlapPMPP, 5th ed., ch. 23
Microbenchmarking the pipes, caches and TLBsJia et al., Dissecting the NVIDIA Volta GPU Architecture via Microbenchmarking (2018) and the Turing follow-up (2019)
The per-instruction throughput tableCUDA C++ Best Practices Guide, “Arithmetic Instructions”
The pipes and stall reasons as the profiler sees themNsight Compute Profiling Guide, “Pipelines” and “Warp Scheduler States”

10. Lab 4b: measure the hardware

Lab 4bPipes, latencies, DRAM patterns and the real clock~45 min · mlsys/labs/lab04b-pipes-and-dram/

Two microbenchmark programs. pipes measures latency and throughput of single instructions; dram measures bandwidth for the access patterns of section 7. Close other GPU programs first and let the card warm up for a minute: section 8 explains why.

cd ~/unibo-lessons/mlsys/labs/lab04b-pipes-and-dram
make test          # quick correctness checks (every kernel computes what it claims)
make sass          # count the instructions in each loop: is it the one it claims to be?
make bench-pipes   # A: latencies, B: throughput per instruction, C: warps x ILP, D: MUFU + k FFMA
make bench-dram    # E: read/write/copy, F: working-set sweep, G: random chunks, H: TLB reach
ExperimentWhat it measuresPrediction to write down first
Acycles per step of one dependent chain: FFMA, IMAD, MUFU.EX2, MUFU.RSQ, DFMA, SHFLFFMA = 4 (section 3); guess the others
Bresults per clock per SM, every pipethe table in section 2
CFFMA throughput for 1–12 warps per scheduler × 1, 2, 4 chainsfull speed once warps × chains ≥ 4 (Fig. 1)
Dcycles per group of 1 MUFU + k FFMAmax(8, 1 + k) per warp (Fig. 1, acts 4–5)
Eread, write, copy GB/s at 1 GBread near 88% of 672; copy a little lower
FGB/s for working sets of 1 MB to 1 GBa step where the data stops fitting in the 48 MB L2
GGB/s for random chunks of 32 B to 8 KBthe shape of section 7’s table (not its numbers)
Hns per access, one access per 2 MB page, spans 16 MB to 4 GBflat, then a step where the pages stop fitting in the TLB

Every experiment also prints the clock it ran at, measured as cycles per nanosecond. Paste back the PASTE: lines: we will compare your latencies and the TLB step with the numbers in this interlude.

Stretch

Run experiment B under ncu --section ComputeWorkloadAnalysis and find each loop’s pipe in the utilization chart: the MUFU loop should drive xu to the top, the DFMA loop fp64. Then add an op whose step is a plain expf(-x) (copy OpEX2), and measure its latency and throughput with and without -use_fast_math. make sass shows the instructions behind each: the price of accuracy from section 4, in cycles.

Check your understanding

Why can a loop that is pure FP32 math still not reach FP32 peak?

The scheduler issues one warp instruction per cycle per quadrant, and peak needs an FFMA in every one of those slots. Any other instruction in the loop (address math, loads, loop control, comparisons) takes a slot from an FFMA. Also, with fewer than 4 independent FFMAs in flight per quadrant, the 4-cycle latency leaves slots empty.

A kernel’s top stall is wait. What is the warp waiting for, and what do you try?

A fixed-latency result, typically an FMA or integer operation, whose wait the compiler encoded in the instruction. There is not enough independent work per scheduler: add instruction-level parallelism (independent accumulators, unrolling) or more warps.

How many independent FFMAs must be in flight per quadrant to keep the FMA pipe full, and where does the number come from?

About 4: Little’s law with a 4-cycle latency and one warp instruction accepted per cycle. They can come from 4 warps, from 4 independent chains in one warp, or any combination.

Why is softmax not limited by its exp on an RTX 5070?

It is memory-bound: 8 bytes per element at 590 GB/s is about 74 billion elements per second, while the SFUs do about 1.9 trillion MUFU per second. The exp is hidden behind the memory traffic.

Why did exp become a bottleneck for attention on the B200, and what did FlashAttention-4 do about it?

The tensor cores got 16× faster per SM than on the 5070 while the SFU stayed at 16 per clock per SM, so at head dimension 128 the exponentials take as long as the matrix multiplications. FlashAttention-4 computes part of the exponentials with a polynomial on the FMA pipe, which has 8× the SFU’s throughput.

What does y[i] = x[i] * 0.5; compile to when x is float*, and how do you catch it?

A conversion to double, a DMUL and a conversion back, each running at 1/64 of FP32 speed on a GeForce card. Catch it with cuobjdump -sass | grep DMUL, with -Xptxas --warn-on-double-precision-use, or with a non-zero fp64 pipe in Nsight Compute. Fix it with 0.5f.

Why is reading random 32-byte sectors from DRAM so much slower than reading random 2 KB chunks, even though both read whole sectors?

Each random sector needs its bank to ACTIVATE a new row (and PRECHARGE the old one), and the chip may only activate a few rows per short window. A 2 KB chunk pays one activation for 64 bursts, so the bus stays busy as in a stream.

Why is the practical DRAM ceiling around 88% of 672 GB/s and not 100%?

Refresh periodically pauses each chip, the bus loses time switching between reads and writes, and row openings are never entirely hidden. Together they cost roughly 8–12%.

A compute-bound kernel is 7% faster today than yesterday, with no code change. What do you check first?

The SM clock: temperature and power limits change it, and compute-bound time scales with 1/clock. Log clocks.sm during both runs (or time with ncu, which fixes the clock).

Sources and further reading