anlp-assignment2 / src /part2 /benchmark.py
sahoosk's picture
Upload folder using huggingface_hub
5407bdc verified
Raw History Blame Contribute Delete
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()