Honest GPU Timer
Build a tiny benchmarking harness that warms up, uses CUDA events, repeats, reports medians, and refuses to benchmark CPU fallbacks by accident.
- Module
- PyTorch and the memory wall
- Objective
- Write a benchmark function that measures GPU work instead of queueing time.
Prompt
Write a function:
def bench_cuda(fn, *, warmup=20, repeat=100):
...
It should benchmark a zero-argument callable fn that launches CUDA work.
Requirements
- Run warmup iterations before measuring.
- Use
torch.cuda.Event(enable_timing=True)for timing. - Synchronize before the measurement loop begins and after each measured event pair.
- Return a dictionary with
median_ms,p10_ms,p90_ms, andsamples. - Raise a useful error if CUDA is unavailable.
- Do not include tensor allocation in the timed function unless the caller put it inside
fnintentionally.
Test workload
Use the harness on:
x = torch.randn(4096, 4096, device="cuda", dtype=torch.float16)
w = torch.randn(4096, 4096, device="cuda", dtype=torch.float16)
def matmul():
return x @ w
Then test a deliberately bad version:
def alloc_and_matmul():
x = torch.randn(4096, 4096, device="cuda", dtype=torch.float16)
w = torch.randn(4096, 4096, device="cuda", dtype=torch.float16)
return x @ w
Deliverable
Submit:
- The benchmark function.
- The two result dictionaries.
- A paragraph explaining why the second benchmark answers a different question.
- A sentence describing when including allocation would be the correct thing to do.
Acceptance criteria
The solution is correct if:
- Removing synchronization makes the reported time suspiciously tiny.
- The function produces stable-ish medians across repeated calls.
- The bad workload is called bad because its timed region changed, not because allocation is morally wrong.
Stretch
Add an optional bytes_moved argument and report effective bandwidth in GB/s when it is provided.
Debrief
The harness is a ritual. Warm up, measure on the device clock, repeat, summarize. Everything later in the track assumes this reflex.
Part B — Catch three lies
Extend the harness (or wrap it) so each of these fails loudly or is documented as a different question. For each, write the median you would trust, and one sentence on what the naive timer is actually measuring.
Lie 1. time.perf_counter() around y = x @ w with no synchronize.
Lie 2. Include the first call after torch.compile(fn) in the timed loop.
Lie 3. repeat=1 on a noisy Colab GPU.
Then run a size sweep for n in [256, 512, 1024, 2048, 4096] on the honest timer. Plot or table: median ms, TFLOP/s. Write three sentences:
- Where is the kernel overhead-bound?
- Where does TFLOP/s flatten?
- Why "the matmul takes X ms" is not a sentence you are allowed to say without
n.
Part C — Bandwidth from the same harness
Time y = x + x for x of shape (4096, 4096) fp16. Traffic is 3 × n² × 2 bytes (two reads, one write). Report GB/s against a spec-sheet peak for the card you used (or L4 300 GB/s if you are on paper). Classify: memory, compute, or overhead.
Check
Lie 1 measures launch. Lie 2 measures compilation. Lie 3 measures noise. Small n is overhead. Large n GEMM approaches a TFLOP/s plateau. The add should land near a large fraction of HBM, not near tensor peak.