Recipe: running a benchmark
A benchmark is a runnable measurement script that produces a number you can publish. This recipe walks through writing and running one for inference performance — the most common case.
What you need before starting
- A trained model in a known location (
runs/<id>/checkpoint.pt). - A representative input distribution (real samples, not random tensors).
- Hardware you can describe specifically (GPU model, CPU model, RAM).
If any are missing, fix that first.
The script template
# benchmarks/run_inference.py
import json, time, statistics, argparse, platform
from pathlib import Path
from datetime import datetime, timezone
import torch
from my_pkg.model import load_model
from my_pkg.data import load_inference_samples
def benchmark(
model,
inputs: list,
n_warmup: int = 3,
n_trials: int = 7,
) -> dict:
"""Best-of-N latency. Warm up before timing."""
model.eval()
with torch.inference_mode():
# warmup
for _ in range(n_warmup):
_ = model(inputs[0])
# measure
timings = []
for x in inputs[:n_trials]:
torch.cuda.synchronize() if torch.cuda.is_available() else None
t0 = time.perf_counter()
_ = model(x)
torch.cuda.synchronize() if torch.cuda.is_available() else None
timings.append(time.perf_counter() - t0)
return {
"n_warmup": n_warmup,
"n_trials": n_trials,
"median_ms": 1000 * statistics.median(timings),
"min_ms": 1000 * min(timings),
"max_ms": 1000 * max(timings),
"stddev_ms": 1000 * statistics.stdev(timings) if len(timings) > 1 else 0.0,
"raw_ms": [1000 * t for t in timings],
}
def gather_environment() -> dict:
env = {
"platform": platform.platform(),
"python": platform.python_version(),
"torch": torch.__version__,
"cuda_available": torch.cuda.is_available(),
"git_rev": _git_rev(),
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
}
if torch.cuda.is_available():
env["gpu"] = torch.cuda.get_device_name(0)
env["cuda"] = torch.version.cuda
return env
def _git_rev() -> str:
import subprocess
try:
return subprocess.check_output(
["git", "rev-parse", "HEAD"], text=True
).strip()
except Exception:
return "unknown"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--checkpoint", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--n-warmup", type=int, default=3)
ap.add_argument("--n-trials", type=int, default=7)
args = ap.parse_args()
model = load_model(args.checkpoint)
samples = load_inference_samples(n=max(args.n_warmup, args.n_trials))
result = {
"checkpoint": args.checkpoint,
"environment": gather_environment(),
"latency": benchmark(model, samples, args.n_warmup, args.n_trials),
}
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(result, indent=2))
print(f"wrote {out}")
print(f"median: {result['latency']['median_ms']:.2f} ms")
if __name__ == "__main__":
main()
Running it
uv run python benchmarks/run_inference.py \
--checkpoint runs/2026-05-04-final/checkpoint.pt \
--out benchmarks/results/2026-05-04-final-l4.json
Output:
{
"checkpoint": "runs/2026-05-04-final/checkpoint.pt",
"environment": {
"platform": "Linux-6.5.0-...",
"torch": "2.4.0",
"cuda_available": true,
"git_rev": "a1b2c3d",
"timestamp_utc": "2026-05-04T08:11:43+00:00",
"gpu": "NVIDIA L4",
"cuda": "12.4"
},
"latency": {
"n_warmup": 3,
"n_trials": 7,
"median_ms": 22.8,
"min_ms": 21.4,
"max_ms": 25.1,
"stddev_ms": 1.3,
"raw_ms": [22.8, 21.4, 23.5, 22.4, 25.1, 22.7, 22.9]
}
}
Commit benchmarks/results/2026-05-04-final-l4.json. This file is now the
source of truth for that number.
Citing the result in docs
| Metric | Value | Source |
| ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
| Inference latency (p50) | 22.8 ms | [`benchmarks/results/2026-05-04-final-l4.json`](../benchmarks/results/2026-05-04-final-l4.json), NVIDIA L4, batch=1, fp16 |
The link makes the claim verifiable. The source line states the conditions. The reader can re-run if they want.
Comparison benchmarks
When comparing against another tool/library:
| Tool | Version | Latency (p50) | Source |
| ------------ | ------- | ------------- | -------------------------------------------------- |
| ours | 0.4.1 | 22.8 ms | benchmarks/results/2026-05-04-final-l4.json |
| competitor-X | 9.4.0 | 31.2 ms | benchmarks/results/2026-05-04-competitor-x-l4.json |
Both rows must come from runnable scripts in your repo. Pin the competitor's version. Re-run when you bump it. Note the bench date so the reader knows the snapshot is from a specific moment, not "current."
Throughput vs. latency
Latency is per-call time; throughput is calls per unit time. They are different benchmarks:
- Latency: measure single calls with
n_warmup ≥ 3, n_trials ≥ 7, report median. - Throughput: measure batch of N calls back-to-back over a longer window (e.g., 60 seconds), report calls/second.
Throughput benefits from batching, async, and pipelining; latency does not. Don't conflate them.
Quality benchmarks
The same template applies, with quality metrics replacing latency. Add:
- Confidence interval (CV folds, bootstrap, or seed reruns).
- Per-class breakdown if applicable.
- A regression set with hand-curated examples (catches "improved benchmark, regressed on the use case").
When the number drifts
You re-run the benchmark and get a different number. Possible causes:
- Hardware difference. Same GPU type can perform differently across spot instances. Pin instance type if you can; report the variation.
- Driver / CUDA upgrade. The cuDNN algorithm chosen may have changed.
- Library upgrade. torch / transformers / etc. version bump shifted the perf curve.
- Warm cache. Earlier runs warmed kernels that the new run did not.
Increase
n_warmup. - Real regression. The model changed. Or the input distribution did.
Drift > 5% deserves an investigation, not a silent doc update. Find the cause, document it, then update the number with the explanation.
CLAUDE.md rules to enforce
## Benchmarks (hard rules)
- All benchmarks live in benchmarks/. All results in benchmarks/results/.
- Every benchmark warms up ≥3 calls before timing.
- Every benchmark reports best-of-N (N≥3), with N stated in the result.
- Every result includes git_rev, timestamp, hardware identifier.
- Comparison benchmarks pin the competitor version and bench date.
- Numbers in docs link to a result file in benchmarks/results/.