eac123 commited on
Commit
6e5e0d0
·
verified ·
1 Parent(s): 3b27a95

single-run trainer

Browse files
Files changed (1) hide show
  1. train.py +197 -0
train.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Train one pointer-chasing model (fixed-depth or recurrent).
2
+
3
+ Example:
4
+ python train.py --arch rec --depth 1 --iters 4 --seed 0 \
5
+ --run-name rec1x4-seed0 --hub-repo eac123/ptr-recurrent-depth \
6
+ --space-id <trackio space id>
7
+
8
+ Trains on fresh random mappings every batch (no fixed train set), k ~ U{1..8},
9
+ and logs to trackio. Saves a checkpoint + training history JSON locally and
10
+ uploads both to the Hub repo.
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+ import time
17
+
18
+ import numpy as np
19
+ import torch
20
+ import torch.nn.functional as F
21
+
22
+ from ptr.common import sample_train_batch, sample_eval_batch
23
+ from ptr.flops import count_params, flops_budget_summary, step_flops
24
+ from ptr.models import PtrTransformer
25
+
26
+ PROBE_KS = (16, 32, 64)
27
+
28
+
29
+ def make_parser():
30
+ p = argparse.ArgumentParser()
31
+ p.add_argument("--arch", choices=["fixed", "rec"], required=True)
32
+ p.add_argument("--depth", type=int, default=4)
33
+ p.add_argument("--iters", default="1",
34
+ help="int, or 'dynamic' for R ~ U{2..8} per batch")
35
+ p.add_argument("--d-model", type=int, default=128)
36
+ p.add_argument("--S", type=int, default=64)
37
+ p.add_argument("--k-max", type=int, default=8)
38
+ p.add_argument("--steps", type=int, default=25000)
39
+ p.add_argument("--batch", type=int, default=256)
40
+ p.add_argument("--lr", type=float, default=3e-4)
41
+ p.add_argument("--warmup", type=int, default=1000)
42
+ p.add_argument("--seed", type=int, default=0)
43
+ p.add_argument("--run-name", required=True)
44
+ p.add_argument("--out-dir", default="out")
45
+ p.add_argument("--hub-repo", default=None)
46
+ p.add_argument("--space-id", default=None,
47
+ help="trackio space id for the metrics dashboard")
48
+ p.add_argument("--project", default="ptr-recurrent-depth")
49
+ p.add_argument("--eval-every", type=int, default=2500)
50
+ p.add_argument("--eval-n", type=int, default=400)
51
+ return p
52
+
53
+
54
+ def resolve_iters(args):
55
+ if args.iters == "dynamic":
56
+ return ("dynamic", 2, 8)
57
+ return int(args.iters)
58
+
59
+
60
+ def sample_iters(spec, rng):
61
+ if isinstance(spec, tuple):
62
+ return int(rng.integers(spec[1], spec[2] + 1))
63
+ return spec
64
+
65
+
66
+ def lr_at(step, args):
67
+ if step < args.warmup:
68
+ return args.lr * (step + 1) / args.warmup
69
+ t = (step - args.warmup) / max(1, args.steps - args.warmup)
70
+ return args.lr * 0.5 * (1 + np.cos(np.pi * t))
71
+
72
+
73
+ @torch.no_grad()
74
+ def probe(model, rng, device, S, ks, n, n_iters):
75
+ accs = {}
76
+ for k in ks:
77
+ b = sample_eval_batch(rng, n, S, k=k)
78
+ f = torch.as_tensor(b["f"], device=device)
79
+ x = torch.as_tensor(b["x"], device=device)
80
+ kt = torch.as_tensor(b["k"], device=device)
81
+ y = torch.as_tensor(b["answer"], device=device)
82
+ logits = model(f, x, kt, n_iters=n_iters)
83
+ accs[k] = float((logits.argmax(-1) == y).float().mean())
84
+ return accs
85
+
86
+
87
+ def main():
88
+ args = make_parser().parse_args()
89
+ os.makedirs(args.out_dir, exist_ok=True)
90
+ torch.manual_seed(args.seed)
91
+ np.random.seed(args.seed)
92
+ rng = np.random.default_rng(args.seed)
93
+ device = "cuda" if torch.cuda.is_available() else "cpu"
94
+ iters_spec = resolve_iters(args)
95
+
96
+ model = PtrTransformer(args.S, args.d_model, 4, 4 * args.d_model,
97
+ depth=args.depth, zero_init=True).to(device)
98
+ nparams = count_params(model)
99
+ budget = flops_budget_summary(model, args.S, args.batch, iters_spec,
100
+ args.steps)
101
+ print(f"[budget] {json.dumps(budget)}")
102
+ # cheap assertions up front
103
+ assert budget["params_total"] > 0 and budget["train_flops_total"] > 0
104
+
105
+ opt = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=0.01)
106
+
107
+ trackio = None
108
+ if args.space_id:
109
+ import trackio
110
+ trackio.init(project=args.project, space_id=args.space_id,
111
+ config={**vars(args), **budget})
112
+
113
+ history = {"loss": [], "id_acc": [], "ood": [], "iters": [],
114
+ "flops_cum": 0.0}
115
+ flops_cum = 0.0
116
+ t0 = time.time()
117
+ bytes_per_batch = 0
118
+ for step in range(args.steps):
119
+ lr = lr_at(step, args)
120
+ for g in opt.param_groups:
121
+ g["lr"] = lr
122
+ n_iters = sample_iters(iters_spec, rng)
123
+ b = sample_train_batch(rng, args.batch, args.S, k_max=args.k_max)
124
+ f = torch.as_tensor(b["f"], device=device)
125
+ x = torch.as_tensor(b["x"], device=device)
126
+ k = torch.as_tensor(b["k"], device=device)
127
+ y = torch.as_tensor(b["answer"], device=device)
128
+ logits = model(f, x, k, n_iters=n_iters)
129
+ loss = F.cross_entropy(logits, y)
130
+ opt.zero_grad(set_to_none=True)
131
+ loss.backward()
132
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
133
+ opt.step()
134
+ flops_cum += step_flops(model, args.S, args.batch, n_iters, True)
135
+
136
+ if step % 100 == 0 or step == args.steps - 1:
137
+ ips = 100 / max(1e-9, time.time() - t0) if step > 0 else 0.0
138
+ t0 = time.time()
139
+ rec = {"step": step, "loss": round(float(loss), 4), "lr": lr,
140
+ "iters": n_iters, "steps_per_s": round(ips, 2),
141
+ "flops_cum": flops_cum}
142
+ history["loss"].append(rec)
143
+ if device == "cuda" and step == 100:
144
+ bytes_per_batch = torch.cuda.max_memory_allocated()
145
+ print(f"[mem] peak GPU memory {bytes_per_batch/1e6:.1f} MB")
146
+ if trackio:
147
+ trackio.log({"train/loss": float(loss), "train/lr": lr,
148
+ "train/steps_per_s": ips,
149
+ "train/flops_cum": flops_cum,
150
+ "train/iters": n_iters}, step=step)
151
+ if (step + 1) % args.eval_every == 0 or step == args.steps - 1:
152
+ n_eval = sample_iters(iters_spec, rng) if args.arch == "rec" \
153
+ else 1
154
+ id_accs = probe(model, rng, device, args.S,
155
+ list(range(1, args.k_max + 1)), args.eval_n,
156
+ n_eval)
157
+ ood = probe(model, rng, device, args.S, PROBE_KS, args.eval_n,
158
+ n_eval)
159
+ id_mean = float(np.mean(list(id_accs.values())))
160
+ print(f"[eval] step {step+1} id_acc(k=1..8) {id_mean:.4f} "
161
+ f"ood {ood}")
162
+ history["id_acc"].append({"step": step + 1, "mean": id_mean,
163
+ "per_k": id_accs})
164
+ history["ood"].append({"step": step + 1, "acc": ood})
165
+ if trackio:
166
+ trackio.log({"eval/id_acc_mean": id_mean,
167
+ "eval/ood_k16": ood[16], "eval/ood_k32": ood[32],
168
+ "eval/ood_k64": ood[64]}, step=step + 1)
169
+ history["flops_cum"] = flops_cum
170
+
171
+ ckpt_path = os.path.join(args.out_dir, f"{args.run_name}.pt")
172
+ torch.save({"state_dict": model.state_dict(),
173
+ "config": vars(args), "budget": budget,
174
+ "n_iters_spec": iters_spec}, ckpt_path)
175
+ hist_path = os.path.join(args.out_dir, f"{args.run_name}_history.json")
176
+ with open(hist_path, "w") as fh:
177
+ json.dump(history, fh, indent=1)
178
+
179
+ if args.hub_repo:
180
+ from huggingface_hub import HfApi
181
+ api = HfApi()
182
+ api.upload_file(path_or_fileobj=ckpt_path,
183
+ path_in_repo=f"ckpts/{args.run_name}.pt",
184
+ repo_id=args.hub_repo, repo_type="model")
185
+ api.upload_file(path_or_fileobj=hist_path,
186
+ path_in_repo=f"results/{args.run_name}_history.json",
187
+ repo_id=args.hub_repo, repo_type="model")
188
+ print(f"[hub] uploaded ckpt + history to {args.hub_repo}")
189
+ if trackio:
190
+ trackio.finish()
191
+ print(f"[done] {args.run_name}: final loss "
192
+ f"{history['loss'][-1]['loss']}, "
193
+ f"id_acc {history['id_acc'][-1]['mean']:.4f}")
194
+
195
+
196
+ if __name__ == "__main__":
197
+ main()