- open ↗auto
- auto
Two speeds, one ridge
A chip has two speeds: how fast it computes and how fast it feeds. A TPU v5e can do 1.97e14 bf16 FLOPs per second, but its HBM can only deliver 8.2e11 bytes per second. Divide the two and you get the ridge: about 240 FLOPs per byte. Any op that does fewer FLOPs per byte it moves is memory-bound, and no amount of compute cleverness will speed it up; any op above the ridge is compute-bound, and no amount of bandwidth will. This one division explains most TPU performance mysteries you will ever meet.
The units and the scratchpad
The compute lives in two units. The MXU is a systolic array (128x128 on most generations, 256x256 on v6e) built for matmuls; the VPU is a vector unit that handles everything elementwise. Between them and HBM sits VMEM, roughly 128 MiB of software-managed scratchpad on v5e. Data must be staged in VMEM before compute can touch it, and almost all kernel engineering is choreography of that staging. EX·06 below shows the hierarchy with its real widths.
The lattice rule
One lattice rule to memorize now, because every kernel in this track obeys it: TPU tiles want their last two dimensions in multiples of (8, 128), and packing doubles the sublane count for narrower dtypes (bf16 packs (16, 128)). Blocks that violate the lattice compile badly or not at all.
Predict, then measure
The habit this stage installs: predict before you measure, every time. Work the arithmetic in EX·01, commit to a number, then run the lab on a real chip and explain every miss. An engineer who predicts within 2x and can name the reason for each miss understands the machine; one who only measures is collecting trivia.
Predict before you measure, every time.
Instruments
- intensity
- 682.7 FLOPs/byte
- verdict
- compute-bound
- attainable
- 197 TFLOP/s
- floor latency
- 87.2 µs
What gets built
- Roofline estimates for five ops, by hand, reconciled against XProf measurements
- A step-time floor estimate for a 7B forward pass from chip constants alone
- The working vocabulary: MXU, VPU, VMEM, HBM, the (8, 128) tiling lattice, ICI
Readings
- Scaling book · All about TPUs · the backbone text; take chip constants from its tables, not from memory
- Scaling book · Rooflines · the reasoning framework this whole stage drills
- Cloud TPU system architecture · skim for v5e/v6e specifics
The work, in order
week 1
- Read both scaling-book chapters in full before touching a notebook
- By hand: classify five ops (large matmul, skinny matmul, elementwise add, long-axis softmax, embedding lookup) as compute- or memory-bound per chip generation
- Estimate the step-time floor for a 7B forward pass at batch 8, seq 4096 on one v5e from constants alone
- Run LAB·0.1 on a Colab TPU and reconcile three estimates against XProf; explain every miss in a sentence
Labs
From the notebook
# scaling-book chip constants (bf16), retrieved 2026-07-26
CHIPS = {
"v5e": {"flops": 1.97e14, "hbm_bw": 8.2e11},
"v6e": {"flops": 9.20e14, "hbm_bw": 1.6e12},
}
for name, c in CHIPS.items():
print(f"{name}: ridge = {c['flops'] / c['hbm_bw']:.0f} FLOPs/byte")def predict(name, flops, bytes_moved, chip):
c = CHIPS[chip]
intensity = flops / bytes_moved
ridge = c["flops"] / c["hbm_bw"]
bound = "compute" if intensity >= ridge else "memory"
floor_s = max(flops / c["flops"], bytes_moved / c["hbm_bw"])
return {"op": name, "intensity": intensity, "bound": bound, "floor_us": floor_s * 1e6}
N = 4096
ops = [
predict("matmul NxNxN", 2 * N**3, 2 * 3 * N**2, "v5e"),
predict("matmul skinny 8xNxN", 2 * 8 * N**2, 2 * (8*N + N*N + 8*N), "v5e"),
predict("elementwise add", N**2, 2 * 3 * N**2, "v5e"),
predict("softmax rows", 5 * N**2, 2 * 2 * N**2, "v5e"),
predict("reduce_sum", N**2, 2 * (N**2 + N), "v5e"),
]
for o in ops:
print(f"{o['op']:>18}: {o['intensity']:8.1f} F/B {o['bound']:>7}-bound floor {o['floor_us']:9.1f} us")def measure(fn, *args, reps=20):
fn(*args).block_until_ready() # compile + warm
times = []
for _ in range(reps):
t0 = time.perf_counter()
fn(*args).block_until_ready()
times.append(time.perf_counter() - t0)
return float(np.median(times)) * 1e6 # usresults = []
if ON_TPU:
key = jax.random.key(0)
a = jax.random.normal(key, (N, N), jnp.bfloat16)
b = jax.random.normal(key, (N, N), jnp.bfloat16)
s = jax.random.normal(key, (8, N), jnp.bfloat16)
cases = {
"matmul NxNxN": (jax.jit(lambda x, y: x @ y), a, b),
"matmul skinny 8xNxN": (jax.jit(lambda x, y: x @ y), s, b),
"elementwise add": (jax.jit(lambda x, y: x + y), a, b),
"softmax rows": (jax.jit(lambda x: jax.nn.softmax(x, axis=-1)), a),
"reduce_sum": (jax.jit(lambda x: jnp.sum(x, axis=-1)), a),
}
for name, (fn, *args) in cases.items():
us = measure(fn, *args)
pred = next(o for o in ops if o["op"] == name)
results.append({"op": name, "measured_us": round(us, 1),
"predicted_us": round(pred["floor_us"], 1),
"ratio": round(us / pred["floor_us"], 2)})
print(f"{name:>18}: measured {us:9.1f} us predicted {pred['floor_us']:9.1f} us ratio {us / pred['floor_us']:5.2f}x")
else:
print("Skipped: no TPU in this runtime.")Can you
The gate
| criterion | measured |
|---|---|
| Latency predictions for five ops within 2x of measured, every miss explained | predictions computed (see bench); measured half pending a TPU run |
Public artifact for this stage: how a TPU actually spends its time.