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.
expEvery 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 said | What is underneath | When it changes a decision |
|---|---|---|
| An SM has 128 FP32 lanes, 32 per scheduler | Each 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” datapath | Kernels heavy in transcendental functions, integer address math or (accidentally) double |
| DRAM delivers 672 GB/s in 32-byte sectors | 24 channels of 8 bits, each with banks; a bank opens a row (a few KB) before reading it, and closes it before opening another | Random access with small units: gathers, embedding lookups, paged caches |
| The clock is 2.51 GHz | The rated boost clock; the real one moves with power, temperature and load, and the memory clock moves separately | Comparing 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.
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:
fmaheavy and fmalite: both do FP32 add, multiply and FMA, but only the heavy half does integer multiply (IMAD). That is why IMAD runs at half the FP32 rate.xu pipe) for MUFU, the multi-function instruction: reciprocal, reciprocal square root, base-2 log and exponential, sine, cosine. 16 per SM per cycle, so 4 lanes per quadrant.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 / SM | Cycles per warp instruction | Relative to FFMA |
|---|---|---|---|
FP32 add, multiply, FMA (FADD, FMUL, FFMA) | 128 | 1 | 1× |
| Integer add, integer min/max, warp vote | 128 | 1 | 1× |
FP16×2 (HFMA2, two halves per lane) | 64 (128 halves) | 2 | same FLOPs |
Integer multiply-add (IMAD), 3-input add | 64 | 2 | 1/2 |
Shifts, logic (LOP3), FP32 min/max | 64 | 2 | 1/2 |
| Conversions int↔float, FP16↔FP32 | 64 | 2 | 1/2 |
Warp shuffle (SHFL) | 32 | 4 | 1/4 |
MUFU: exp2, lg2, rcp, rsqrt, sin, cos | 16 | 8 | 1/8 |
| Population count, count leading zeros | 16 | 8 | 1/8 |
FP64 (DADD, DMUL, DFMA) and conversions to/from FP64 | 2 | 64 | 1/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.
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.
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.
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:
short_scoreboard: waiting on an MIO (memory input/output) operation that does not go to L1TEX: shared memory above all, but also MUFU and dynamic branches. Latency in the tens of cycles.long_scoreboard: waiting on L1TEX: global, local and texture memory. Hundreds of cycles if it goes to DRAM.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.
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 reason | The warp is waiting for… | Hardware behind it | Usual fix |
|---|---|---|---|
wait | a fixed number of cycles set by the compiler | FMA/ALU latency (4+ cycles) | more independent instructions (ILP) or more warps |
short_scoreboard | a shared-memory, MUFU or dynamic-branch result | MIO: shared memory, SFU | fewer bank conflicts, fewer/wider shared loads, fewer MUFU |
long_scoreboard | a global or local memory result | L1TEX, L2, DRAM | more bytes in flight (chapter 3), better locality |
math_pipe_throttle | a pipe that is still busy | an oversubscribed pipe: SFU, FP64, or FMA at peak | change the instruction mix; or nothing, if it is the FMA pipe at peak |
mio_throttle | room in the MIO queue | shared memory or SFU saturated | wider shared loads, fewer conflicts |
lg_throttle | room in the local/global queue | too many small global accesses | vectorize loads (float4), avoid local memory spills |
not_selected | its turn | another ready warp was picked | nothing: there is spare parallelism |
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.
expWhen 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:
| Function | Roughly compiles to | Maximum error |
|---|---|---|
expf(x) | a handful of FMAs and one MUFU.EX2 | 2 ulp (units in the last place) over the whole range |
__expf(x) (an intrinsic) | one FMUL and one MUFU.EX2 | 2 + 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 / SM | Tensor FLOPs per exp at balance | FLOPs per score, head dim d | |
|---|---|---|---|---|
| RTX 5070 | 512 | 16 | 32 | 256 at d = 64: the SFU needs 1/8 of the tensor time |
| B200 (datacenter Blackwell) | 8,192 | 16 | 512 | 512 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.
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.
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.
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:
uniform): a scalar unit for values that are the same in every thread of the warp: base pointers, loop counters, blockIdx-derived offsets. It has its own small register file (uniform registers, UR in SASS). Work on it doesn’t take an FP32/INT32 lane, though each instruction still uses the issue slot.lsu): 4 units per quadrant. Every global, local and shared memory instruction passes through here on its way to the L1, and so do shuffles, special-register reads (S2R, e.g. reading threadIdx) and block-level barrier operations. That is why shuffles show up as memory-like traffic in the profiler.cbu) handles warp-level convergence and branches, the divergence of chapter 2; the address divergence unit (adu) handles indirect jumps and also serves constant loads.tex): 4 per SM, for graphics texture sampling. CUDA uses them for some read-only loads.tc): chapter 8.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.
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:
lts__ metrics (L2 “tag stage”) count per slice.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.
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.
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:
| Traffic | Bursts per ACTIVATE | Bus busy (model) | What limits it |
|---|---|---|---|
| Streaming read | 64 | ~100% | nothing (refresh, on real hardware) |
| Random 32 B sectors | 1 | ~21% | the activation limit: 4 rows per 20 ns |
| Random 128 B chunks | 4 | ~60% | activations and bank timing |
| Random 2 KB chunks | 64 | ~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.
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.
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.
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:
nvidia-smi --query-gpu=clocks.sm,clocks.mem,power.draw,temperature.gpu --format=csv -lms 200
nvidia-smi -q -d PERFORMANCE # why the clock is what it is: power cap, thermal, idle...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.
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.
Outside the SMs and the memory system, a few more units take part in every CUDA program:
cudaMemcpyAsync overlap with running kernels (PMPP ch. 23).Where to go deeper, once Part II is done:
| Topic | Read |
|---|---|
| DRAM internals, bursts, banks and channels | PMPP, 5th ed., ch. 6 |
| How atomics are executed in the L2 | PMPP, 5th ed., section 9.3 |
| Virtual memory and TLBs on GPUs | PMPP, 5th ed., appendix C |
| Streams, copy engines, overlap | PMPP, 5th ed., ch. 23 |
| Microbenchmarking the pipes, caches and TLBs | Jia et al., Dissecting the NVIDIA Volta GPU Architecture via Microbenchmarking (2018) and the Turing follow-up (2019) |
| The per-instruction throughput table | CUDA C++ Best Practices Guide, “Arithmetic Instructions” |
| The pipes and stall reasons as the profiler sees them | Nsight Compute Profiling Guide, “Pipelines” and “Warp Scheduler States” |
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
| Experiment | What it measures | Prediction to write down first |
|---|---|---|
| A | cycles per step of one dependent chain: FFMA, IMAD, MUFU.EX2, MUFU.RSQ, DFMA, SHFL | FFMA = 4 (section 3); guess the others |
| B | results per clock per SM, every pipe | the table in section 2 |
| C | FFMA throughput for 1–12 warps per scheduler × 1, 2, 4 chains | full speed once warps × chains ≥ 4 (Fig. 1) |
| D | cycles per group of 1 MUFU + k FFMA | max(8, 1 + k) per warp (Fig. 1, acts 4–5) |
| E | read, write, copy GB/s at 1 GB | read near 88% of 672; copy a little lower |
| F | GB/s for working sets of 1 MB to 1 GB | a step where the data stops fitting in the 48 MB L2 |
| G | GB/s for random chunks of 32 B to 8 KB | the shape of section 7’s table (not its numbers) |
| H | ns per access, one access per 2 MB page, spans 16 MB to 4 GB | flat, 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.
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.
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.
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.
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.
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.
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.
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.
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.
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%.
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).
expf and __expf; the effect of -use_fast_math. NVCC documentation for --warn-on-double-precision-use.fmaheavy, fmalite, xu, lsu, uniform, cbu, adu, fp64), the 4-cycle FFMA dependency latency, and the warp stall reasons quoted in section 3.