Download src/part2/benchmark.py from sahoosk/anlp-assignment2: direct link, hf CLI and curl.
- Browser
- Download file 4.85 kB
-
https://huggingface.co/sahoosk/anlp-assignment2/resolve/main/src/part2/benchmark.py
- Command line
-
hf download hf://sahoosk/anlp-assignment2/src/part2/benchmark.py
-
curl -L -o benchmark.py https://huggingface.co/sahoosk/anlp-assignment2/resolve/main/src/part2/benchmark.py
4.85 kB
| """Fair per-step cost comparison of the five optimizers. | |
| The wall-clock totals from the training runs are not comparable: they were | |
| produced on a shared GPU, so a run that happened to start while the device was | |
| busy looks slower than one that had it to itself, regardless of the update | |
| rule. This benchmark instead times every optimizer back-to-back on the same | |
| model, same batch shape and same device, reporting the cost of the optimizer | |
| *step* separately from the forward/backward pass that all of them share. | |
| python -m src.part2.benchmark | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import time | |
| import torch | |
| import torch.nn.functional as F | |
| from ..part1.model import Transformer | |
| from ..utils import RUNS, save_json | |
| from .optimizers import CATEGORY, DEFAULT_HPARAMS, OPTIMIZERS | |
| from .optimizers.sophia import SophiaG | |
| from .train import build_optimizer, model_config | |
| def time_optimizer(name, args, device, warmup=5): | |
| torch.manual_seed(0) | |
| cfg = model_config(args.vocab_size, args.n_ctx) | |
| model = Transformer(cfg).to(device) | |
| opt = build_optimizer(name, model, dict(DEFAULT_HPARAMS[name])) | |
| is_sophia = isinstance(opt, SophiaG) | |
| tok_per_step = args.batch_size * args.n_ctx | |
| x = torch.randint(0, args.vocab_size, (args.batch_size, args.n_ctx), | |
| device=device) | |
| y = torch.randint(0, args.vocab_size, (args.batch_size, args.n_ctx), | |
| device=device) | |
| fwd_bwd, step_t = 0.0, 0.0 | |
| for i in range(warmup + args.steps): | |
| if device.type == "cuda": | |
| torch.cuda.synchronize() | |
| t0 = time.perf_counter() | |
| with torch.autocast("cuda", dtype=torch.bfloat16, | |
| enabled=device.type == "cuda"): | |
| logits, _, _ = model(x) | |
| loss = F.cross_entropy(logits.view(-1, logits.size(-1)).float(), | |
| y.reshape(-1)) | |
| opt.zero_grad(set_to_none=True) | |
| loss.backward() | |
| if device.type == "cuda": | |
| torch.cuda.synchronize() | |
| t1 = time.perf_counter() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| if is_sophia: | |
| opt.step(bs_scale=tok_per_step) | |
| else: | |
| opt.step() | |
| if device.type == "cuda": | |
| torch.cuda.synchronize() | |
| t2 = time.perf_counter() | |
| if i >= warmup: # discard warmup iterations | |
| fwd_bwd += t1 - t0 | |
| step_t += t2 - t1 | |
| n_params = sum(p.numel() for p in model.parameters()) | |
| state_mb = opt.memory_bytes() / 1e6 | |
| del model, opt | |
| torch.cuda.empty_cache() | |
| fwd_bwd_ms = 1000 * fwd_bwd / args.steps | |
| step_ms = 1000 * step_t / args.steps | |
| # Sophia additionally pays for one extra forward+backward every k steps to | |
| # refresh its curvature estimate. That cost is real but falls outside | |
| # opt.step(), so timing the step alone would understate it. | |
| extra = fwd_bwd_ms / args.sophia_k if is_sophia else 0.0 | |
| return { | |
| "optimizer": name, | |
| "category": CATEGORY[name], | |
| "fwd_bwd_ms": fwd_bwd_ms, | |
| "step_ms": step_ms, | |
| "hessian_amortised_ms": extra, | |
| "total_ms": fwd_bwd_ms + step_ms + extra, | |
| "state_mb": state_mb, | |
| "state_multiple": state_mb * 1e6 / (n_params * 4), | |
| } | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--steps", type=int, default=30) | |
| ap.add_argument("--batch_size", type=int, default=24) | |
| ap.add_argument("--n_ctx", type=int, default=512) | |
| ap.add_argument("--vocab_size", type=int, default=16000) | |
| ap.add_argument("--sophia_k", type=int, default=10, | |
| help="curvature refresh interval, to amortise its cost") | |
| args = ap.parse_args() | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| print(f"[benchmark] {args.steps} timed steps each, batch " | |
| f"{args.batch_size}x{args.n_ctx}, on {device}", flush=True) | |
| print(f"\n{'optimizer':10s} {'fwd+bwd':>9s} {'opt.step':>9s} " | |
| f"{'hess':>7s} {'total':>9s} {'overhead':>9s} {'state':>8s}") | |
| out = [] | |
| for name in OPTIMIZERS: | |
| r = time_optimizer(name, args, device) | |
| out.append(r) | |
| over = 100 * (r["step_ms"] + r["hessian_amortised_ms"]) / r["total_ms"] | |
| print(f"{name:10s} {r['fwd_bwd_ms']:8.1f}ms {r['step_ms']:8.1f}ms " | |
| f"{r['hessian_amortised_ms']:6.1f}ms {r['total_ms']:8.1f}ms " | |
| f"{over:8.1f}% {r['state_multiple']:7.1f}x", flush=True) | |
| base = next(r for r in out if r["optimizer"] == "adamw") | |
| print(f"\nrelative to AdamW (total ms/step):") | |
| for r in out: | |
| print(f" {r['optimizer']:10s} {r['total_ms']/base['total_ms']:.3f}x") | |
| save_json(out, RUNS / "part2" / "benchmark.json") | |
| print(f"\nwrote {RUNS/'part2'/'benchmark.json'}") | |
| if __name__ == "__main__": | |
| main() | |