jvonrad commited on
Commit
251ba47
·
verified ·
1 Parent(s): 5f73143

Upload src/xscript/train.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/xscript/train.py +326 -0
src/xscript/train.py ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pretraining loop: DDP + bf16, WSD schedule, deterministic resume.
2
+
3
+ One run = one (mixture, tokenizer) cell of the design. The mixture is driven
4
+ entirely by the run config's `langs`/`probs`; the tokenizer by `tok_name`. The
5
+ loader is globally deterministic and world-size-independent, so a run resumed on
6
+ a different node count sees the exact same token stream.
7
+
8
+ Cooldown branch: set `branch.from` to a `stable` checkpoint; the schedule then
9
+ has warmup=stable=0 and only decays, giving a cheap final model at a larger
10
+ token budget without retraining the trunk.
11
+ """
12
+ import json
13
+ import os
14
+ import time
15
+ from pathlib import Path
16
+
17
+ import numpy as np
18
+ import torch
19
+
20
+ from .model import ModelConfig, Transformer
21
+ from .data.loader import MixedStream
22
+ from .schedule import lr_at, ckpt_interval, stable_end_tokens, total_tokens
23
+ from .paths import run_dir, ensure
24
+
25
+
26
+ def _ddp():
27
+ if "RANK" in os.environ and int(os.environ.get("WORLD_SIZE", "1")) > 1:
28
+ import torch.distributed as dist
29
+ backend = "nccl" if torch.cuda.is_available() else "gloo"
30
+ dist.init_process_group(backend=backend)
31
+ rank = dist.get_rank()
32
+ world = dist.get_world_size()
33
+ local = int(os.environ.get("LOCAL_RANK", "0"))
34
+ if torch.cuda.is_available():
35
+ torch.cuda.set_device(local)
36
+ return dist.is_initialized(), rank, world, local
37
+ return False, 0, 1, 0
38
+
39
+
40
+ def _log(rank, path, rec):
41
+ if rank == 0:
42
+ with open(path, "a") as f:
43
+ f.write(json.dumps(rec) + "\n")
44
+
45
+
46
+ class Trainer:
47
+ def __init__(self, cfg: dict):
48
+ self.cfg = cfg
49
+ self.dist, self.rank, self.world, self.local = _ddp()
50
+ self.device = torch.device(f"cuda:{self.local}" if torch.cuda.is_available() else "cpu")
51
+ self.is_cuda = self.device.type == "cuda"
52
+ torch.manual_seed(cfg.get("seed", 0))
53
+ np.random.seed(cfg.get("seed", 0))
54
+
55
+ mc = cfg["model"]
56
+ self.mcfg = ModelConfig(**mc)
57
+ self.seq_len = self.mcfg.max_seq_len
58
+ raw_model = Transformer(self.mcfg).to(self.device)
59
+ if self.rank == 0:
60
+ print(f"[train] params: {raw_model.num_params(False)/1e6:.1f}M non-embedding, "
61
+ f"{raw_model.num_params(True)/1e6:.1f}M total")
62
+ # Keep the canonical module unwrapped for portable state_dict keys.
63
+ # Compilation is only the forward/backward execution path.
64
+ model = raw_model
65
+ if cfg.get("compile", False) and self.is_cuda:
66
+ model = torch.compile(model)
67
+ self.raw_model = raw_model
68
+ if self.dist:
69
+ from torch.nn.parallel import DistributedDataParallel as DDP
70
+ model = DDP(model, device_ids=[self.local] if self.is_cuda else None)
71
+ self.model = model
72
+
73
+ opt = cfg.get("optim", {})
74
+ self.optim = torch.optim.AdamW(
75
+ self.raw_model.parameters(),
76
+ lr=cfg["schedule"]["peak_lr"],
77
+ betas=tuple(opt.get("betas", (0.9, 0.95))),
78
+ weight_decay=opt.get("weight_decay", 0.1),
79
+ eps=opt.get("eps", 1e-8),
80
+ )
81
+ self.grad_clip = opt.get("grad_clip", 1.0)
82
+
83
+ # global batch bookkeeping
84
+ self.micro_bsz = cfg["train"]["micro_batch_size"]
85
+ gbt = cfg["train"]["global_batch_tokens"]
86
+ per_step_windows = max(1, round(gbt / self.seq_len))
87
+ # round up to a multiple of micro_bsz*world so each rank gets equal work
88
+ unit = self.micro_bsz * self.world
89
+ self.global_windows = max(unit, (per_step_windows // unit) * unit)
90
+ self.grad_accum = self.global_windows // unit
91
+ self.tokens_per_step = self.global_windows * self.seq_len
92
+ if self.rank == 0:
93
+ print(f"[train] global batch: {self.global_windows} windows "
94
+ f"({self.tokens_per_step/1e6:.3f}M tokens), grad_accum={self.grad_accum}")
95
+
96
+ # schedule (branch collapses warmup/stable)
97
+ self.sched = dict(cfg["schedule"])
98
+ self.branch = cfg.get("branch")
99
+ if self.branch:
100
+ self.sched["warmup_tokens"] = 0
101
+ self.sched["stable_tokens"] = 0
102
+ self.target_tokens = total_tokens(self.sched)
103
+ self.ckpt_table = cfg["train"].get("ckpt_schedule",
104
+ [[2e9, 250e6], [5e9, 500e6],
105
+ [15e9, 1e9], [1e15, 2e9]])
106
+ # token budgets at which a stable trunk saves a named branch point
107
+ self.stable_marks = sorted(cfg["train"].get("stable_marks", []))
108
+ self.marks_done = set()
109
+
110
+ # data mixer
111
+ self.mixer = MixedStream(cfg["langs"], cfg["tok_name"], self.seq_len,
112
+ seed=cfg.get("data_seed", 1234),
113
+ probs=cfg.get("probs"))
114
+
115
+ self.rdir = ensure(run_dir(cfg["name"]))
116
+ self.log_path = self.rdir / "train.jsonl"
117
+ self.tokens = 0
118
+ self.step = 0
119
+ self.last_ckpt_tokens = 0
120
+ self.saved_stable = False
121
+
122
+ self.wandb = None
123
+ if self.rank == 0:
124
+ try:
125
+ import wandb
126
+ self.wandb = wandb.init(
127
+ project="XScript-Pretraining", name=cfg["name"],
128
+ id=cfg.get("wandb_id", cfg["name"]),
129
+ resume="allow", config=cfg,
130
+ )
131
+ self.wandb.summary["params_total_M"] = self.raw_model.num_params(True) / 1e6
132
+ self.wandb.summary["params_non_embed_M"] = self.raw_model.num_params(False) / 1e6
133
+ except Exception as exc:
134
+ print(f"[train] wandb disabled ({exc})")
135
+
136
+ # ---- checkpoint io ----
137
+ def _ckpt_path(self, tag):
138
+ return ensure(self.rdir / "checkpoints") / f"{tag}.pt"
139
+
140
+ def save(self, tag, resumable=True):
141
+ if self.rank != 0:
142
+ return
143
+ payload = {
144
+ "model": self.raw_model.state_dict(),
145
+ "step": self.step, "tokens": self.tokens,
146
+ "cfg": self.cfg,
147
+ }
148
+ if resumable:
149
+ payload.update({
150
+ "optim": self.optim.state_dict(),
151
+ "mixer": self.mixer.state_dict(),
152
+ "last_ckpt_tokens": self.last_ckpt_tokens,
153
+ "saved_stable": self.saved_stable,
154
+ "torch_rng": torch.get_rng_state(),
155
+ })
156
+ torch.save(payload, self._ckpt_path(tag))
157
+ kind = "full" if resumable else "model-only"
158
+ print(f"[train] saved {tag} ({kind}) @ {self.tokens/1e9:.3f}B tokens")
159
+
160
+ def maybe_resume(self):
161
+ last = self._ckpt_path("last")
162
+ if self.branch and not last.exists():
163
+ ck = torch.load(self.branch["from"], map_location="cpu", weights_only=False)
164
+ self.raw_model.load_state_dict(ck["model"])
165
+ if self.branch.get("load_optim", True):
166
+ self.optim.load_state_dict(ck["optim"])
167
+ if self.rank == 0:
168
+ print(f"[train] branched from {self.branch['from']} "
169
+ f"@ {ck['tokens']/1e9:.3f}B (cooldown {self.target_tokens/1e9:.1f}B)")
170
+ return
171
+ if last.exists():
172
+ ck = torch.load(last, map_location="cpu", weights_only=False)
173
+ self.raw_model.load_state_dict(ck["model"])
174
+ self.optim.load_state_dict(ck["optim"])
175
+ self.mixer.load_state_dict(ck["mixer"])
176
+ self.step = ck["step"]; self.tokens = ck["tokens"]
177
+ self.last_ckpt_tokens = ck["last_ckpt_tokens"]
178
+ self.saved_stable = ck.get("saved_stable", False)
179
+ self.marks_done = {m for m in self.stable_marks if m <= self.tokens}
180
+ torch.set_rng_state(ck["torch_rng"])
181
+ if self.rank == 0:
182
+ print(f"[train] resumed @ step {self.step}, {self.tokens/1e9:.3f}B tokens")
183
+
184
+ # ---- data ----
185
+ def _next_micro_batches(self):
186
+ """Return grad_accum micro-batches of (x, y) on device for this rank."""
187
+ arr, counts = self.mixer.rank_batch(self.global_windows, self.rank, self.world)
188
+ # arr: (global_windows/world, seq_len+1)
189
+ t = torch.from_numpy(arr.astype(np.int64))
190
+ x = t[:, :-1].to(self.device, non_blocking=True)
191
+ y = t[:, 1:].to(self.device, non_blocking=True)
192
+ micros = [(x[i:i + self.micro_bsz], y[i:i + self.micro_bsz])
193
+ for i in range(0, x.size(0), self.micro_bsz)]
194
+ return micros, counts
195
+
196
+ # ---- eval ----
197
+ def _eval_sources(self):
198
+ from .langs import LANGS
199
+ srcs = {}
200
+ for l in self.cfg["langs"]:
201
+ try:
202
+ from .eval.bpb import load_holdout
203
+ h = load_holdout(l, self.cfg["train"].get("eval_docs", 500))
204
+ if h:
205
+ srcs[f"holdout_{l}"] = h
206
+ except Exception:
207
+ pass
208
+ try:
209
+ from . import flores
210
+ par = flores.load_parallel(list(self.cfg["langs"]), "dev")
211
+ for l, sents in par.items():
212
+ srcs[f"flores_{l}"] = sents
213
+ except Exception as e:
214
+ if self.rank == 0:
215
+ print(f"[train] flores eval skipped: {e}")
216
+ return srcs
217
+
218
+ def evaluate(self):
219
+ if self.rank != 0:
220
+ return {}
221
+ from .eval.bpb import eval_sources
222
+ from .tok.wrapper import Tok
223
+ from .paths import tokenizer_dir
224
+ tok = Tok(tokenizer_dir(self.cfg["tok_name"]))
225
+ res = eval_sources(self.raw_model, tok, self._eval_sources(),
226
+ self.device, self.seq_len)
227
+ self.model.train()
228
+ return res
229
+
230
+ # ---- loop ----
231
+ def train(self):
232
+ self.maybe_resume()
233
+ self.model.train()
234
+ t0 = time.time()
235
+ log_every = self.cfg["train"].get("log_every", 20)
236
+ while self.tokens < self.target_tokens:
237
+ lr = lr_at(self.tokens, self.sched)
238
+ for g in self.optim.param_groups:
239
+ g["lr"] = lr
240
+
241
+ micros, counts = self._next_micro_batches()
242
+ self.optim.zero_grad(set_to_none=True)
243
+ loss_acc = 0.0
244
+ for j, (x, y) in enumerate(micros):
245
+ sync = (not self.dist) or (j == len(micros) - 1)
246
+ ctx = self.model.no_sync() if (self.dist and not sync) else _null()
247
+ with ctx:
248
+ with torch.autocast("cuda", dtype=torch.bfloat16) if self.is_cuda else _null():
249
+ _, loss = self.model(x, y)
250
+ (loss / len(micros)).backward()
251
+ loss_acc += loss.detach().item() / len(micros)
252
+ torch.nn.utils.clip_grad_norm_(self.raw_model.parameters(), self.grad_clip)
253
+ self.optim.step()
254
+
255
+ self.tokens += self.tokens_per_step
256
+ self.step += 1
257
+
258
+ if self.step % log_every == 0:
259
+ dt = time.time() - t0
260
+ tps = self.tokens_per_step * log_every / dt if dt > 0 else 0
261
+ rec = {
262
+ "step": self.step, "tokens": self.tokens, "lr": lr,
263
+ "loss": loss_acc, "tok_per_s": round(tps),
264
+ "mix": self.mixer.stats(),
265
+ }
266
+ _log(self.rank, self.log_path, rec)
267
+ if self.wandb:
268
+ self.wandb.log({**rec, "tokens_b": self.tokens / 1e9}, step=self.step)
269
+ if self.rank == 0:
270
+ print(f"[train] step {self.step} | {self.tokens/1e9:.2f}B | "
271
+ f"loss {loss_acc:.4f} | lr {lr:.2e} | {tps/1e3:.0f}k tok/s")
272
+ t0 = time.time()
273
+
274
+ # stable checkpoint exactly once, at the trunk's decay boundary
275
+ if (not self.branch and not self.saved_stable
276
+ and self.tokens >= stable_end_tokens(self.sched)):
277
+ self.save("stable")
278
+ self.saved_stable = True
279
+
280
+ # named branch points for cooldown extensions (100B runs)
281
+ for mark in self.stable_marks:
282
+ if mark not in self.marks_done and self.tokens >= mark:
283
+ self.save(f"stable_{int(mark/1e6)}M")
284
+ self.marks_done.add(mark)
285
+
286
+ # log-spaced checkpoint + eval
287
+ if self.tokens - self.last_ckpt_tokens >= ckpt_interval(self.tokens, self.ckpt_table):
288
+ self.last_ckpt_tokens = self.tokens
289
+ self.save("last")
290
+ self.save(f"step{self.step}_{int(self.tokens/1e6)}M", resumable=False)
291
+ res = self.evaluate()
292
+ _log(self.rank, self.log_path,
293
+ {"step": self.step, "tokens": self.tokens, "eval": res})
294
+ if self.wandb and res:
295
+ self.wandb.log({f"eval/{k}_bpb": v["bpb"] for k, v in res.items()} |
296
+ {f"eval/{k}_ppl": v["ppl_token"] for k, v in res.items()},
297
+ step=self.step)
298
+ if self.rank == 0 and res:
299
+ brief = {k: round(v["bpb"], 4) for k, v in res.items()}
300
+ print(f"[eval] {self.tokens/1e9:.2f}B: {brief}")
301
+
302
+ self.save("last")
303
+ self.save("final", resumable=False)
304
+ res = self.evaluate()
305
+ _log(self.rank, self.log_path,
306
+ {"step": self.step, "tokens": self.tokens, "eval_final": res})
307
+ if self.wandb:
308
+ if res:
309
+ self.wandb.log({f"eval_final/{k}_bpb": v["bpb"] for k, v in res.items()} |
310
+ {f"eval_final/{k}_ppl": v["ppl_token"] for k, v in res.items()},
311
+ step=self.step)
312
+ self.wandb.finish()
313
+ if self.rank == 0:
314
+ print(f"[train] DONE {self.cfg['name']} @ {self.tokens/1e9:.2f}B tokens")
315
+ if self.dist:
316
+ import torch.distributed as dist
317
+ dist.destroy_process_group()
318
+
319
+
320
+ class _null:
321
+ def __enter__(self): return self
322
+ def __exit__(self, *a): return False
323
+
324
+
325
+ def run_from_config(cfg: dict):
326
+ Trainer(cfg).train()