Download train.py from eac123/ptr-recurrent-depth: direct link, hf CLI and curl.
- Browser
- Download file 8.55 kB
-
https://huggingface.co/eac123/ptr-recurrent-depth/resolve/main/train.py
- Command line
-
hf download hf://eac123/ptr-recurrent-depth/train.py
-
curl -L -o train.py https://huggingface.co/eac123/ptr-recurrent-depth/resolve/main/train.py
8.55 kB
| """Train one pointer-chasing model (fixed-depth or recurrent). | |
| Example: | |
| python train.py --arch rec --depth 1 --iters 4 --seed 0 \ | |
| --run-name rec1x4-seed0 --hub-repo eac123/ptr-recurrent-depth \ | |
| --space-id <trackio space id> | |
| Trains on fresh random mappings every batch (no fixed train set), k ~ U{1..8}, | |
| and logs to trackio. Saves a checkpoint + training history JSON locally and | |
| uploads both to the Hub repo. | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import time | |
| import numpy as np | |
| import torch | |
| import torch.nn.functional as F | |
| from ptr.common import sample_train_batch, sample_eval_batch | |
| from ptr.flops import count_params, flops_budget_summary, step_flops | |
| from ptr.models import PtrTransformer | |
| PROBE_KS = (16, 32, 64) | |
| def make_parser(): | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--arch", choices=["fixed", "rec"], required=True) | |
| p.add_argument("--depth", type=int, default=4) | |
| p.add_argument("--iters", default="1", | |
| help="int, or 'dynamic' for R ~ U{2..8} per batch") | |
| p.add_argument("--d-model", type=int, default=128) | |
| p.add_argument("--S", type=int, default=64) | |
| p.add_argument("--k-max", type=int, default=8) | |
| p.add_argument("--steps", type=int, default=25000) | |
| p.add_argument("--batch", type=int, default=256) | |
| p.add_argument("--lr", type=float, default=3e-4) | |
| p.add_argument("--warmup", type=int, default=1000) | |
| p.add_argument("--seed", type=int, default=0) | |
| p.add_argument("--run-name", required=True) | |
| p.add_argument("--out-dir", default="out") | |
| p.add_argument("--hub-repo", default=None) | |
| p.add_argument("--space-id", default=None, | |
| help="trackio space id for the metrics dashboard") | |
| p.add_argument("--project", default="ptr-recurrent-depth") | |
| p.add_argument("--eval-every", type=int, default=2500) | |
| p.add_argument("--eval-n", type=int, default=400) | |
| return p | |
| def resolve_iters(args): | |
| if args.iters == "dynamic": | |
| return ("dynamic", 2, 8) | |
| if args.iters == "aligned": | |
| # one unroll per hop: R = k per example (all examples in a batch | |
| # share the same k so a single unroll count serves the batch) | |
| return ("aligned", 1, args.k_max) | |
| return int(args.iters) | |
| def sample_iters(spec, rng): | |
| if isinstance(spec, tuple): | |
| return int(rng.integers(spec[1], spec[2] + 1)) | |
| return spec | |
| def lr_at(step, args): | |
| if step < args.warmup: | |
| return args.lr * (step + 1) / args.warmup | |
| t = (step - args.warmup) / max(1, args.steps - args.warmup) | |
| return args.lr * 0.5 * (1 + np.cos(np.pi * t)) | |
| def probe(model, rng, device, S, ks, n, n_iters, aligned=False): | |
| accs = {} | |
| for k in ks: | |
| b = sample_eval_batch(rng, n, S, k=k) | |
| f = torch.as_tensor(b["f"], device=device) | |
| x = torch.as_tensor(b["x"], device=device) | |
| kt = torch.as_tensor(b["k"], device=device) | |
| y = torch.as_tensor(b["answer"], device=device) | |
| r = k if aligned else n_iters | |
| logits = model(f, x, kt, n_iters=r) | |
| accs[k] = float((logits.argmax(-1) == y).float().mean()) | |
| return accs | |
| def main(): | |
| args = make_parser().parse_args() | |
| os.makedirs(args.out_dir, exist_ok=True) | |
| torch.manual_seed(args.seed) | |
| np.random.seed(args.seed) | |
| rng = np.random.default_rng(args.seed) | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| iters_spec = resolve_iters(args) | |
| model = PtrTransformer(args.S, args.d_model, 4, 4 * args.d_model, | |
| depth=args.depth, zero_init=True).to(device) | |
| nparams = count_params(model) | |
| budget = flops_budget_summary(model, args.S, args.batch, iters_spec, | |
| args.steps) | |
| print(f"[budget] {json.dumps(budget)}") | |
| # cheap assertions up front | |
| assert budget["params_total"] > 0 and budget["train_flops_total"] > 0 | |
| opt = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=0.01) | |
| trackio = None | |
| if args.space_id: | |
| import trackio | |
| trackio.init(project=args.project, space_id=args.space_id, | |
| config={**vars(args), **budget}) | |
| history = {"loss": [], "id_acc": [], "ood": [], "iters": [], | |
| "flops_cum": 0.0} | |
| flops_cum = 0.0 | |
| t0 = time.time() | |
| bytes_per_batch = 0 | |
| for step in range(args.steps): | |
| lr = lr_at(step, args) | |
| for g in opt.param_groups: | |
| g["lr"] = lr | |
| if isinstance(iters_spec, tuple) and iters_spec[0] == "aligned": | |
| kb = int(rng.integers(iters_spec[1], iters_spec[2] + 1)) | |
| n_iters = kb | |
| else: | |
| n_iters = sample_iters(iters_spec, rng) | |
| kb = None | |
| b = sample_train_batch(rng, args.batch, args.S, k_max=args.k_max) | |
| if kb is not None: | |
| b["k"][:] = kb | |
| f = torch.as_tensor(b["f"], device=device) | |
| x = torch.as_tensor(b["x"], device=device) | |
| k = torch.as_tensor(b["k"], device=device) | |
| y = torch.as_tensor(b["answer"], device=device) | |
| logits = model(f, x, k, n_iters=n_iters) | |
| loss = F.cross_entropy(logits, y) | |
| opt.zero_grad(set_to_none=True) | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| opt.step() | |
| flops_cum += step_flops(model, args.S, args.batch, n_iters, True) | |
| if step % 100 == 0 or step == args.steps - 1: | |
| ips = 100 / max(1e-9, time.time() - t0) if step > 0 else 0.0 | |
| t0 = time.time() | |
| rec = {"step": step, "loss": round(float(loss), 4), "lr": lr, | |
| "iters": n_iters, "steps_per_s": round(ips, 2), | |
| "flops_cum": flops_cum} | |
| history["loss"].append(rec) | |
| if device == "cuda" and step == 100: | |
| bytes_per_batch = torch.cuda.max_memory_allocated() | |
| print(f"[mem] peak GPU memory {bytes_per_batch/1e6:.1f} MB") | |
| if trackio: | |
| trackio.log({"train/loss": float(loss), "train/lr": lr, | |
| "train/steps_per_s": ips, | |
| "train/flops_cum": flops_cum, | |
| "train/iters": n_iters}, step=step) | |
| if (step + 1) % args.eval_every == 0 or step == args.steps - 1: | |
| aligned = isinstance(iters_spec, tuple) \ | |
| and iters_spec[0] == "aligned" | |
| n_eval = sample_iters(iters_spec, rng) if args.arch == "rec" \ | |
| and not aligned else 1 | |
| id_accs = probe(model, rng, device, args.S, | |
| list(range(1, args.k_max + 1)), args.eval_n, | |
| n_eval, aligned=aligned) | |
| ood = probe(model, rng, device, args.S, PROBE_KS, args.eval_n, | |
| n_eval, aligned=aligned) | |
| id_mean = float(np.mean(list(id_accs.values()))) | |
| print(f"[eval] step {step+1} id_acc(k=1..8) {id_mean:.4f} " | |
| f"ood {ood}") | |
| history["id_acc"].append({"step": step + 1, "mean": id_mean, | |
| "per_k": id_accs}) | |
| history["ood"].append({"step": step + 1, "acc": ood}) | |
| if trackio: | |
| trackio.log({"eval/id_acc_mean": id_mean, | |
| "eval/ood_k16": ood[16], "eval/ood_k32": ood[32], | |
| "eval/ood_k64": ood[64]}, step=step + 1) | |
| history["flops_cum"] = flops_cum | |
| ckpt_path = os.path.join(args.out_dir, f"{args.run_name}.pt") | |
| torch.save({"state_dict": model.state_dict(), | |
| "config": vars(args), "budget": budget, | |
| "n_iters_spec": iters_spec}, ckpt_path) | |
| hist_path = os.path.join(args.out_dir, f"{args.run_name}_history.json") | |
| with open(hist_path, "w") as fh: | |
| json.dump(history, fh, indent=1) | |
| if args.hub_repo: | |
| from huggingface_hub import HfApi | |
| api = HfApi() | |
| api.upload_file(path_or_fileobj=ckpt_path, | |
| path_in_repo=f"ckpts/{args.run_name}.pt", | |
| repo_id=args.hub_repo, repo_type="model") | |
| api.upload_file(path_or_fileobj=hist_path, | |
| path_in_repo=f"results/{args.run_name}_history.json", | |
| repo_id=args.hub_repo, repo_type="model") | |
| print(f"[hub] uploaded ckpt + history to {args.hub_repo}") | |
| if trackio: | |
| trackio.finish() | |
| print(f"[done] {args.run_name}: final loss " | |
| f"{history['loss'][-1]['loss']}, " | |
| f"id_acc {history['id_acc'][-1]['mean']:.4f}") | |
| if __name__ == "__main__": | |
| main() |