Lab 01 — Transformer FLOPs / Memory / Cost & Tradeoff Modeler
Phase: 00 — Foundations: Scaling Laws, FLOPs/Memory Math & Tradeoffs Difficulty: ⭐⭐☆☆☆ (the arithmetic is easy; the judgment is ⭐⭐⭐⭐⭐) Time: 3–4 hours
Round-1 of an AI engineering interview — and the first slide of every design review — is back-of-envelope arithmetic worn as architecture. This lab builds the calculator: training and inference FLOPs (
6ND,2N), weight memory across precisions, the KV-cache (with GQA), Chinchilla-optimal data, the roofline (compute- vs memory-bound), the memory-bandwidth ceiling on decode throughput, the serving cost per million tokens, and a weighted tradeoff resolver whose entire point is that the same two designs flip winners when the workload's priorities change — which is why "best model" is a meaningless phrase.
What you build
training_flops/forward_flops_per_token/inference_flops— the compute budget:6·N·Dto train,2·Nper token to run. The numbers that size a cluster and a bill.weight_memory_bytes—N × bytes_per_paramacross fp32/fp16/int8/int4 — the lever that decides how many GPUs you need.kv_cache_bytes—2 · L · T · d_model · bytes · batch, with the GQA branch (d_kv = d_model · n_kv/n_head) — the memory wall everyone forgets and the thing that actually OOMs a server.chinchilla_optimal_tokens/chinchilla_optimal_params— the compute-optimalD≈20Nallocation (and its inverse).arithmetic_intensity/roofline_ridge_intensity/roofline_attainable_flops/is_memory_bound— the roofline:min(peak, bw·I), ridge= peak/bw, and the check that proves decode is memory-bandwidth-bound.decode_tokens_per_sec/cost_per_million_tokens/mfu— throughput ceiling (and how batching amortizes the weight read),$/1Mtokens, and the honest efficiency metric.Candidate(five dials) +resolve_tradeoff— score two designs under the workload's weights and return the winner and the deciding dial (the one sentence for your decision record).
Key concepts
| Concept | What to understand |
|---|---|
6ND train, 2N run | forward = 2N/token (one MAC/param); backward ≈ 2× forward ⇒ 6N/token ⇒ 6ND total |
| Weights ≠ the memory bound | weights are fixed; the KV-cache grows with batch × context and usually dominates |
| GQA shrinks the cache | fewer K/V heads ⇒ d_kv scaled by n_kv/n_head ⇒ proportionally smaller KV memory |
| Decode is memory-bound | I≈1 FLOP/byte vs an A100 ridge of ~156 ⇒ batch & quantize, don't buy FLOPs |
| Batching amortizes the read | one weight read serves B sequences ⇒ throughput ~∝ batch |
Chinchilla D≈20N | training-optimal; deployed models over-train past it because inference cost is forever |
| The deciding dial | "best model" is a category error; best for these weights is engineering |
Files
| File | Purpose |
|---|---|
lab.py | skeleton with # TODO markers — your implementation |
solution.py | complete reference; python solution.py runs a worked example |
test_lab.py | the proof — run it red, make it green |
requirements.txt | pytest only (pure stdlib otherwise) |
Run
pip install -r requirements.txt
pytest test_lab.py -v # against your lab.py
LAB_MODULE=solution pytest test_lab.py -v # against the reference
python solution.py # the worked example
Success criteria
- All tests pass against your implementation (and
LAB_MODULE=solution). - You can explain why training is
6NDand inference is2N/token (MACs × passes). - You can compute the KV-cache for
(L=32, T=4096, d=4096, fp16)≈ 2 GB in your head and why GQA 8/32 makes it 0.54 GB. - You can explain why
test_decode_tokens_per_sec_and_batchingshows ~linear scaling with batch, and why that is the entire economic argument for batched serving. - You can explain why
is_memory_bound(1.0, …)isTruefor decode and why that one fact governs inference optimization. - You can explain why
test_weights_flip_the_winneris the whole point of the lab. - Given any "deploy model X" prompt you can, in two minutes, write FLOPs + weight/KV memory + a
$/1Mestimate + the deciding dial on paper.
How this maps to the real stack
| The miniature | The production mechanism | Where to verify it |
|---|---|---|
training_flops / inference_flops | the 6ND / 2N rules used to size every training run and serving fleet; DeepSpeed's FLOPs profiler and the Chinchilla paper use exactly this | deepspeed.profiling.flops_profiler; the model card's "training compute" |
weight_memory_bytes | the GPU-count decision; fp16→int8→int4 is the difference between 2 GPUs and 1 | model.get_memory_footprint() (HF); bitsandbytes 8/4-bit |
kv_cache_bytes (+ GQA) | the actual serving memory wall; vLLM reserves blocks for it and num_key_value_heads (GQA) is the real config | vLLM gpu_memory_utilization, max_model_len; the config's num_key_value_heads |
arithmetic_intensity / roofline | the model behind every "are we compute- or memory-bound" decision; NVIDIA Nsight and the roofline paper | NVIDIA Nsight Compute roofline; torch.profiler |
decode_tokens_per_sec | the bandwidth ceiling vLLM/TensorRT-LLM throughput is measured against; batching is the lever | vLLM throughput logs; the HBM bandwidth in the GPU datasheet |
cost_per_million_tokens | the build-vs-buy number you compare against API pricing | cloud GPU hourly price ÷ measured tok/s |
resolve_tradeoff | a scored model/serving decision with the deciding dial written into an ADR | your own design-review template |
Limits of the miniature (be honest in the interview): the FLOP rules ignore attention's
O(T²) term (matters at long context), embeddings, and the LM head; real KV-cache adds block
fragmentation and over-allocation (which is why PagedAttention exists); decode throughput is a
ceiling — real systems lose to kernel launch overhead, sampling, and Python; and a tradeoff score
is a structured conversation, not a number you can sum mechanically across every workload.
Extensions (build these on real hardware)
- Add the attention
O(T²)FLOPs term and find the context length where it overtakes the MLP. - Add
training_memory_bytes(N, optimizer="adamw", dtype="fp16", activation_checkpointing=bool)using the~16 bytes/paramrule, and show why a 7B model needs ZeRO to train on one GPU. - Add
paged_kv_waste(seq_lens, block_size)modeling fragmentation, and show how PagedAttention's block allocator reclaims it (preview of P09). - Plot a real roofline for your GPU (
peak_flops,bandwidthfrom the datasheet) and mark where prefill, decode, and a big GEMM land on it. - Pull params with
sum(p.numel() for p in model.parameters())on a real HF model and check yourweight_memory_bytesagainstmodel.get_memory_footprint().
Interview / resume
- Talking points: "How many GPUs to serve a 70B at 1k QPS, and what limits it?" "Is decode
compute- or memory-bound — prove it." "Bigger model or distill a smaller one — defend with
numbers." "Compose the
$/1Mfor self-hosting vs an API." - Resume bullet: Built a transformer cost-and-tradeoff model that computes training/inference
FLOPs, weight and KV-cache memory (with GQA), the compute-vs-bandwidth roofline, decode
throughput and
$/1M-token serving cost, and produces explainable, priority-weighted model-selection decisions for design reviews.