the path · 0/15
start the path

the path · chapter 10 of 15 · part ii, the track

stage 00
title The machine
schedule week 1
gate PASSED

Given an op and its shapes, predict from first principles whether it is compute-bound or memory-bound on a given TPU generation, and estimate its ceiling latency.

mastery work · this chapter0/10
  1. go →auto
  2. go →auto
  3. go →auto
  4. go →auto
  5. go →auto
  6. go →auto
  7. go →auto
  8. open ↗auto
  9. auto
manual items are your word; auto items complete from your streaks, labs, and can-you ticks · stored in your browser only
the chapter

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 HBMThe chip’s main memory: large, far, and the resource memory-bound ops spend. 8.2e11 bytes per second on v5e, 1.6e12 on v6e.taught in /l/tpu → can only deliver 8.2e11 bytes per second. Divide the two and you get the ridgeThe FLOP-per-byte ratio where an op flips from memory-bound to compute-bound: about 240 on v5e, about 575 on v6e.taught in /l/tpu →: 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 chapter

The units and the scratchpad

The compute lives in two units. The MXUThe systolic matmul array: 128x128 on v5e, 256x256 on v6e. Matmuls only; everything else is the VPU’s job.taught in /l/tpu → is a systolic array (128x128 on most generations, 256x256 on v6e) built for matmuls; the VPUThe vector unit for elementwise work, organized as (8, 128) lanes; the origin of the tiling lattice every layer above obeys.taught in /l/tpu → is a vector unit that handles everything elementwise. Between them and HBMThe chip’s main memory: large, far, and the resource memory-bound ops spend. 8.2e11 bytes per second on v5e, 1.6e12 on v6e.taught in /l/tpu → sits VMEMThe TPU’s software-managed vector scratchpad, about 128 MiB. Blocks must be staged here before compute touches them; what is resident is what your schedule staged.taught in /l/tpu →, 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 chapter

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.

the chapter

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.
go deeper, in order

Lessons

  1. 01The die and the reticleEvery headline number on a chip starts inside one rectangle: the largest area a lithography scanner can expose in a single shot, 26 by 33 millimetres. ·
  2. 02The GPU chipA TPU v5p has at most two big compute units. An H100 has 132 small ones, and most of the other differences follow from that count. ·
  3. 03Inside the SM, multiplied outMultiply an H100's SM contents by a clock and NVIDIA's headline FLOPS come back out. NVIDIA never prints the clock, and the rates that would tell you one disagree with each other on the same page. ·
  4. 04Two machines, one jobBoth chips exist to multiply matrices. One does it with 132 small units the hardware schedules, the other with two big ones the compiler schedules. ·
  5. 05The ISA contractTwo words get used as though they named the same machine. One is a promise that has to survive a decade of silicon; the other is silicon that gets thrown away every two years. ·
  6. 06Who decides what runs at onceFour famous names get taught as though they were four rungs of one ladder. They are answers to two different questions, and each sorts in a sentence once you know which question it answers. ·
  7. 07The scale-up domainNVIDIA prints 900 GB/s for an H100's NVLink and the scaling book prints 450 for the same eighteen links. Both are correct. Sorting out why is what lets you read every other number in the rack. ·
hands on the machine

Instruments

EX·06 the memory hierarchy, to its real widths
HBM 16 GB large, slow, off-core 820 GB/s VMEM 128 MiB software-managed MXU 197 TFLOP/s bf16 VPU elementwise TPU v5e · every stage of this track is choreography across these three widths
constants from jax-ml.github.io/scaling-book (retrieved 2026-07-26) · nothing invented
EX·01 the roofline playground
0.11101001000100 GF/s1.00 TF/s10.0 TF/s100 TF/s1.00 PF/sARITHMETIC INTENSITY · FLOPs PER HBM BYTEridge 156 F/B
intensity
682.7 FLOPs/byte
verdict
compute-bound
attainable
140 TFLOP/s
floor latency
123 µs
chip constants from jax-ml.github.io/scaling-book (retrieved 2026-07-26) · all values computed, nothing invented
deliverables

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
sources

Readings

the plan

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
run it yourself

Labs

single source of truth

From the notebook

LAB·0.1Rooflines by hand · extracted 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  # us
results = []
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.")
source of truth: labs/stage-0/lab-0.1-rooflines.ipynb · cells tagged site:show render here; edit the notebook, rebuild, the page updates
LAB·0.2Spec sheet from parts · extracted from the notebook
# Peak FLOPs from parts: MAC cells x 2 ops per cell per cycle x clock. The
# two is one multiply and one add, and it is the only term here that is not
# looked up.
OPS_PER_MAC = 2


def peak_tflops(macs, clock_hz):
    return macs * OPS_PER_MAC * clock_hz / 1e12


# v1: one 256x256 systolic array.          ISCA 2017, section 2
# clock 700 MHz, published 92 TOPS.        ISCA 2017, Table 2
v1_macs = 256 * 256
reconcile("TPU v1 peak (int8)", peak_tflops(v1_macs, 700e6), 92.0, "TOPS")

# v4: 2 TensorCores x 4 MXUs, each 128x128. ISCA 2023, section 2
# clock 1050 MHz, published 275 TFLOPS.     ISCA 2023, Table 4
v4_macs = 2 * 4 * 128 * 128
v4_peak = peak_tflops(v4_macs, 1050e6)
reconcile("TPU v4 peak (bf16)", v4_peak, 275.0, "TFLOPS")

# One more zoom, same multiplication: a full v4 pod is 4096 chips, and
# Google advertises 1.1 exaflops for it. Two significant figures on the
# claim, so the tolerance has to be loose enough to read them.
# Google Cloud TPU v4 page, via /l/tpu/tensorcore-complex
reconcile("TPU v4 pod, 4096 chips", v4_peak * 4096 / 1e6, 1.1, "EFLOPS",
          tol=0.05, note="advertised to two figures")

print()
print(f"v1 and v4 hold the same cells twice over: {v1_macs} then {v4_macs}")
# v6e: 1 TensorCore x 2 MXUs (Google v6e page), each 256x256 (Cloud TPU
# architecture page). Published peak 918 TFLOPS bf16 (Google v6e page).
v6e_macs = 1 * 2 * 256 * 256
V6E_CLAIM = 918e12
implied_v6e = V6E_CLAIM / (v6e_macs * OPS_PER_MAC)

# yardsticks: the fastest clock Google itself has printed is v4 at 1050 MHz,
# and the fastest figure in the public record of any kind is 1.75 GHz for
# v5p (scaling book, secondary).
print(f"v6e cells from the published shape: {v6e_macs}")
print(f"implied clock: {implied_v6e / 1e9:.2f} GHz")
print(f"  against v4, printed:      {1050e6 / 1e9:.2f} GHz "
      f"({implied_v6e / 1050e6:.1f}x)")
print(f"  against v5p, secondary:   {1.75:.2f} GHz "
      f"({implied_v6e / 1.75e9:.1f}x)")
print()

# Two ways to make it close, neither of them evidence.
rescue_macs = 4 * 256 * 256          # assume four MXUs; Google's page says two
print(f"with 4 MXUs instead of 2:  {V6E_CLAIM / (rescue_macs * OPS_PER_MAC) / 1e9:.2f}"
      " GHz, which matches v5p exactly and contradicts the page")

# The same architecture page puts an MXU at 16K multiply-accumulate ops per
# cycle. 16K is 16,384, which is 128x128, not 256x256.
alt_macs = 2 * 16 * 1024
print(f"with the page's own 16K MACs per MXU: "
      f"{V6E_CLAIM / (alt_macs * OPS_PER_MAC) / 1e9:.1f} GHz, worse")
print(f"16K = {16 * 1024} = {int((16 * 1024) ** 0.5)}x{int((16 * 1024) ** 0.5)}, "
      "so the page disagrees with itself about the MXU shape")
print()

# Run the derivation the only honest way left: published shape, and the
# fastest clock anywhere in the public record. It comes up half short.
reconcile("TPU v6e peak at the fastest public clock",
          peak_tflops(v6e_macs, 1.75e9), 918.0, "TFLOPS",
          note="1.75 GHz is v5p's, and no v6e clock exists")

finding("tpu-v6e-open",
        f"The published v6e shape (2 MXUs at 256x256) and the published 918 "
        f"TFLOPS need {implied_v6e / 1e9:.2f} GHz, which no TPU approaches. "
        "Google publishes no v6e clock, and its architecture page gives an "
        "MXU MAC count that fits 128x128 rather than 256x256, so one input "
        "is wrong and nothing public says which.")
# A100 is the chip whose clock is printed. Whitepaper Table 3, p.39:
# 108 SMs, 1410 MHz boost, FP16 Tensor 312/624 (dense/sparse).
A100_SMS, A100_CLOCK, A100_FP16_DENSE = 108, 1.41e9, 312e12
a100_const = A100_FP16_DENSE / (A100_SMS * A100_CLOCK)
print(f"solve for the constant: {a100_const:.1f} FLOP/SM/clk -> call it 2048")
reconcile("A100 FP16 Tensor, dense", A100_SMS * 2048 * A100_CLOCK / 1e12,
          312.0, "TFLOPS")

# One sentence carries it to Hopper: "2x the MMA computational rates of the
# A100 SM on equivalent data types" (whitepaper p.22), per SM and per clock.
H100_PER_SM = 2048 * 2
H100_SMS = SM_COUNTS["H100 SXM5"]
h100_flops_per_clk = H100_SMS * H100_PER_SM
print(f"\nH100 SXM5: {H100_SMS} SMs x {H100_PER_SM} FLOP/SM/clk "
      f"= {h100_flops_per_clk:,} FLOP/clk")

clock_dense = BF16_DENSE * 1e12 / h100_flops_per_clk
clock_sparse = BF16_SPARSE * 1e12 / h100_flops_per_clk
print(f"clock implied by the dense rate:   {clock_dense / 1e9:.3f} GHz")
print(f"clock implied by the starred rate: {clock_sparse / 1e9:.3f} GHz  "
      "<- what the sparsity trap costs you")
print("third-party databases list 1830 MHz boost for H100 SXM5, which agrees;"
      "\nread that as a check on the arithmetic, not as the missing line")
# The datasheet rows that do not go through a Tensor Core, and the whitepaper
# preliminary column for the same part. FP32 is 128 lanes x 2 per SM, FP64 is
# 64 x 2, both from whitepaper Table 3.
fp32_per_clk = H100_SMS * FP32_PER_SM * 2
fp64_per_clk = H100_SMS * FP64_PER_SM * 2


def clock_band(printed, per_clk):
    "A printed rate is rounded, so it covers every clock in a band."
    lo = (printed - 0.5) * 1e12 / per_clk
    hi = (printed + 0.5) * 1e12 / per_clk
    return lo / 1e9, hi / 1e9


rows = [
    # rate on the page, TFLOPS, FLOPs per clock, source
    ("datasheet BF16 Tensor, dense", BF16_DENSE, h100_flops_per_clk, "nvidia.com H100"),
    ("datasheet FP32", 67.0, fp32_per_clk, "nvidia.com H100"),
    ("datasheet FP64", 34.0, fp64_per_clk, "nvidia.com H100"),
    ("preliminary FP32", 60.0, fp32_per_clk, "whitepaper Table 3"),
    ("preliminary FP64", 30.0, fp64_per_clk, "whitepaper Table 3"),
    ("preliminary FP16 Tensor, dense", 1000.0, h100_flops_per_clk, "whitepaper Table 3"),
]
for name, rate, per_clk, src in rows:
    lo, hi = clock_band(rate, per_clk)
    print(f"{name:<32} {rate:>7.1f} TF / {per_clk:>7,} = "
          f"{rate * 1e12 / per_clk / 1e9:.3f} GHz   band {lo:.3f}-{hi:.3f}  [{src}]")

# One row cannot be run through this arithmetic at all. FP64 Tensor Core
# prints 67 TFLOPS, the FP32 number over again, and NVIDIA never prints an
# FP64 rate per clock through the Tensor Cores.
print(f"\n{'datasheet FP64 Tensor Core':<32} {67.0:>7.1f} TF / "
      f"{'not published':>13} = cannot be derived")

# Do the two non-Tensor bands overlap, and does the Tensor clock sit in them?
lo32, hi32 = clock_band(67.0, fp32_per_clk)
lo64, hi64 = clock_band(34.0, fp64_per_clk)
overlap = (max(lo32, lo64), min(hi32, hi64))
print(f"\nFP32 and FP64 agree on a band: {overlap[0]:.3f}-{overlap[1]:.3f} GHz")
print(f"the Tensor rows demand {clock_dense / 1e9:.3f} GHz, which is "
      f"{'inside' if overlap[0] <= clock_dense / 1e9 <= overlap[1] else 'outside'}"
      " that band")
print("printed clock, either document: none. The whitepaper says 'Not "
      "Finalized' and the datasheet page lists no clock at all.")

# Assume the whole page is quoted at one clock, take the Tensor rows' clock,
# and predict the FP32 row from it. The miss is the size of the problem.
print()
reconcile("H100 FP32 at the Tensor rows' clock",
          fp32_per_clk * clock_dense / 1e12, 67.0, "TFLOPS",
          note="one page, one clock, assumed")

finding("h100-three-clocks",
        f"One page implies three clocks. The Tensor rows give "
        f"{clock_dense / 1e9:.3f} GHz, FP32 and FP64 agree on a band of "
        f"{overlap[0]:.3f} to {overlap[1]:.3f} GHz once rounding is allowed, "
        "and NVIDIA prints no clock in either document. A peak-FLOPS table "
        "is not necessarily quoted at one clock.")
finding("h100-fp64-tensor",
        "The FP64 Tensor Core row prints 67 TFLOPS and no FLOPs-per-clock "
        "figure exists for it, so its divisor can only be inferred from the "
        "doubling against plain FP64. Recorded as unavailable, not filled in.")
source of truth: labs/stage-0/lab-0.2-spec-sheet.ipynb · cells tagged site:show render here; edit the notebook, rebuild, the page updates
the self-test, before the gate

Can you

0/7
checked state lives in your browser only · the gate below is the public half
pass or fail, in public

The gate

GATE 00 PASSED
criterionmeasured
Latency predictions for five ops within 2x of measured, every miss explained measured on v6e-1 (jax 0.11): big matmul 362.5 µs vs 149.4 µs floor (2.4x); the four µs-scale ops sit 3.6x to 7.7x over floor. Every miss has the same explanation, a per-launch overhead the roofline does not model, and all five rows live on the bench.
gates pass only on real hardware, published pass or fail · est./computed rows are predictions holding the slot until a run replaces them · records in bench/