Add exact training script (defines CompactLM class)

#3
by Compactbot - opened
Files changed (1) hide show
  1. train_compactlm5m.py +328 -0
train_compactlm5m.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ CompactLM-5M — ~6.2M-param LLaMA-style English LM, from scratch.
4
+ Requested by DedeProGames (model-requests #14): LLaMA-style, ~5M params,
5
+ fineweb-edu, budget raised to ~100M tokens on the 30-50 min GPU window.
6
+
7
+ Architecture (~6.16M params, tied embeddings):
8
+ - vocab 12288 (gollem_eval BPE, byte-level)
9
+ - d_model 256, n_layers 4, n_heads 4 (head_dim 64), SwiGLU ff 640
10
+ - RMSNorm pre-norm, RoPE, causal attention, ctx 512
11
+ - Standard LLaMA (no sliding window) — "LLaMA-style"
12
+
13
+ Data: stream fineweb-edu (train) only. dclm-baseline-1.0 was failing
14
+ (ConnectError) at build time on this host, so single-corpus — logged honestly.
15
+ """
16
+ import os, sys, math, time, json, random, argparse, fcntl
17
+ import numpy as np
18
+ import torch
19
+ import torch.nn as nn
20
+ import torch.nn.functional as F
21
+
22
+ TOK_PATH = "gollem_eval/tokenizer.json"
23
+ CTX = 512
24
+
25
+ def load_tok():
26
+ from tokenizers import Tokenizer
27
+ return Tokenizer.from_file(TOK_PATH)
28
+
29
+ def stream_tokens(dsname, split, target_chars, tok, log):
30
+ from datasets import load_dataset
31
+ ds = load_dataset(dsname, split=split, streaming=True)
32
+ ids = []
33
+ nchars = 0
34
+ for row in ds:
35
+ text = row.get("text") or row.get("content") or ""
36
+ if not text:
37
+ continue
38
+ nchars += len(text)
39
+ ids.extend(tok.encode(text, add_special_tokens=False).ids)
40
+ if nchars >= target_chars:
41
+ break
42
+ return ids
43
+
44
+ def precompute_rope(dim, max_pos, base=10000.0):
45
+ freqs = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
46
+ t = torch.arange(max_pos).float()
47
+ angles = torch.outer(t, freqs)
48
+ return torch.polar(torch.ones_like(angles), angles)
49
+
50
+ def apply_rope(x, freqs_cis, offset=0):
51
+ B, nh, S, hd = x.shape
52
+ x = x.view(B, nh, S, hd // 2, 2)
53
+ xr = x[..., 0].float()
54
+ xi = x[..., 1].float()
55
+ fc = freqs_cis[offset:offset + S].to(x.device)
56
+ xr2 = xr * fc.real - xi * fc.imag
57
+ xi2 = xr * fc.imag + xi * fc.real
58
+ out = torch.stack([xr2, xi2], dim=-1).reshape(B, nh, S, hd)
59
+ return out.to(x.dtype)
60
+
61
+ class RMSNorm(nn.Module):
62
+ def __init__(self, dim, eps=1e-5):
63
+ super().__init__()
64
+ self.eps = eps
65
+ self.weight = nn.Parameter(torch.ones(dim))
66
+ def forward(self, x):
67
+ norm = x.float().pow(2).mean(-1, keepdim=True).add(self.eps).rsqrt()
68
+ return (x.float() * norm).to(x.dtype) * self.weight
69
+
70
+ class Attention(nn.Module):
71
+ def __init__(self, d, n_heads):
72
+ super().__init__()
73
+ self.n_heads = n_heads
74
+ self.head_dim = d // n_heads
75
+ self.wq = nn.Linear(d, d, bias=False)
76
+ self.wk = nn.Linear(d, d, bias=False)
77
+ self.wv = nn.Linear(d, d, bias=False)
78
+ self.wo = nn.Linear(d, d, bias=False)
79
+ def forward(self, x, freqs_cis, offset=0):
80
+ B, S, _ = x.shape
81
+ q = self.wq(x).view(B, S, self.n_heads, self.head_dim).transpose(1, 2)
82
+ k = self.wk(x).view(B, S, self.n_heads, self.head_dim).transpose(1, 2)
83
+ v = self.wv(x).view(B, S, self.n_heads, self.head_dim).transpose(1, 2)
84
+ q = apply_rope(q, freqs_cis, offset)
85
+ k = apply_rope(k, freqs_cis, offset)
86
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
87
+ y = y.transpose(1, 2).reshape(B, S, -1)
88
+ return self.wo(y)
89
+
90
+ class MLP(nn.Module):
91
+ def __init__(self, d, ff):
92
+ super().__init__()
93
+ self.w1 = nn.Linear(d, ff, bias=False)
94
+ self.w2 = nn.Linear(d, ff, bias=False)
95
+ self.w3 = nn.Linear(ff, d, bias=False)
96
+ def forward(self, x):
97
+ return self.w3(F.silu(self.w1(x)) * self.w2(x))
98
+
99
+ class Block(nn.Module):
100
+ def __init__(self, d, n_heads, ff):
101
+ super().__init__()
102
+ self.ln1 = RMSNorm(d)
103
+ self.attn = Attention(d, n_heads)
104
+ self.ln2 = RMSNorm(d)
105
+ self.mlp = MLP(d, ff)
106
+ def forward(self, x, freqs_cis, offset=0):
107
+ x = x + self.attn(self.ln1(x), freqs_cis, offset)
108
+ x = x + self.mlp(self.ln2(x))
109
+ return x
110
+
111
+ class CompactLM(nn.Module):
112
+ def __init__(self, vocab, d=256, n_layers=4, n_heads=4, ff=640, ctx=512):
113
+ super().__init__()
114
+ self.vocab = vocab; self.ctx = ctx; self.d = d
115
+ self.tok = nn.Embedding(vocab, d)
116
+ self.blocks = nn.ModuleList([Block(d, n_heads, ff) for _ in range(n_layers)])
117
+ self.ln_f = RMSNorm(d)
118
+ self.head = nn.Linear(d, vocab, bias=False)
119
+ self.head.weight = self.tok.weight # tied
120
+ self.freqs_cis = precompute_rope(d // n_heads, ctx)
121
+ self.init_weights()
122
+ def init_weights(self):
123
+ for m in self.modules():
124
+ if isinstance(m, nn.Linear):
125
+ nn.init.normal_(m.weight, mean=0.0, std=0.02)
126
+ nn.init.normal_(self.tok.weight, mean=0.0, std=0.02)
127
+ def forward(self, idx, targets=None):
128
+ B, S = idx.shape
129
+ h = self.tok(idx)
130
+ for b in self.blocks:
131
+ h = b(h, self.freqs_cis)
132
+ h = self.ln_f(h)
133
+ logits = self.head(h)
134
+ if targets is not None:
135
+ loss = F.cross_entropy(logits[:, :-1].reshape(-1, logits.size(-1)),
136
+ targets[:, 1:].reshape(-1), ignore_index=-1)
137
+ return loss
138
+ return logits
139
+ @torch.no_grad()
140
+ def generate(self, idx, max_new_tokens=128, temperature=0.8, top_k=40, seed=0):
141
+ g = torch.Generator(device=idx.device).manual_seed(seed)
142
+ for _ in range(max_new_tokens):
143
+ ctx_in = idx[:, -self.ctx:]
144
+ logits = self(ctx_in)[:, -1]
145
+ if temperature and temperature > 0:
146
+ logits = logits / temperature
147
+ if top_k:
148
+ v, _ = torch.topk(logits, top_k, dim=-1)
149
+ logits[logits < v[:, -1, None]] = float("-inf")
150
+ p = torch.softmax(logits, dim=1)
151
+ nxt = torch.multinomial(p, 1, generator=g)
152
+ idx = torch.cat([idx, nxt], dim=1)
153
+ return idx
154
+
155
+ def count_params(m):
156
+ return sum(p.numel() for p in m.parameters())
157
+
158
+ def build_data(args, tok, log):
159
+ outdir = os.path.join(args.out, "data")
160
+ os.makedirs(outdir, exist_ok=True)
161
+ train_npy = os.path.join(outdir, "train.npy")
162
+ val_npy = os.path.join(outdir, "val.npy")
163
+ if os.path.exists(train_npy) and os.path.exists(val_npy):
164
+ log(f"[data] reusing {train_npy}")
165
+ return
166
+ log("[data] streaming fineweb-edu ...")
167
+ t0 = time.time()
168
+ fw = stream_tokens("HuggingFaceFW/fineweb-edu", "train", args.fw_chars, tok, log)
169
+ log(f"[data] fineweb-edu: {len(fw):,} tokens from {args.fw_chars:,} chars in {time.time()-t0:.0f}s")
170
+ all_ids = np.array(fw, dtype=np.int32)
171
+ # NOTE: no token-level shuffle; batch_iter shuffles at the WINDOW level.
172
+ n_val = args.val_tokens
173
+ val = all_ids[:n_val]
174
+ train = all_ids[n_val:]
175
+ def pad(x):
176
+ n = (len(x) // CTX) * CTX
177
+ return x[:n].reshape(-1, CTX)
178
+ train = pad(train)
179
+ val = pad(val)
180
+ np.save(train_npy, train)
181
+ np.save(val_npy, val)
182
+ log(f"[data] train {train.shape} ({train.shape[0]*CTX:,} tok), val {val.shape} ({val.shape[0]*CTX:,} tok)")
183
+
184
+ def main():
185
+ ap = argparse.ArgumentParser()
186
+ ap.add_argument("--out", default="models/compactlm-5m")
187
+ ap.add_argument("--steps", type=int, default=20000)
188
+ ap.add_argument("--batch", type=int, default=128)
189
+ ap.add_argument("--ctx", type=int, default=CTX)
190
+ ap.add_argument("--lr", type=float, default=3e-4)
191
+ ap.add_argument("--warmup", type=int, default=300)
192
+ ap.add_argument("--min-lr-frac", type=float, default=0.1)
193
+ ap.add_argument("--weight-decay", type=float, default=0.1)
194
+ ap.add_argument("--grad-clip", type=float, default=1.0)
195
+ ap.add_argument("--fw-chars", type=int, default=250_000_000)
196
+ ap.add_argument("--val-tokens", type=int, default=1_000_000)
197
+ ap.add_argument("--ckpt-every", type=int, default=500)
198
+ ap.add_argument("--val-every", type=int, default=1000)
199
+ ap.add_argument("--sample-every", type=int, default=2000)
200
+ ap.add_argument("--resume", action="store_true")
201
+ args = ap.parse_args()
202
+
203
+ os.makedirs(args.out, exist_ok=True)
204
+ logf = open(os.path.join(args.out, "train.log"), "a")
205
+ def log(s):
206
+ print(s, flush=True)
207
+ logf.write(s + "\n"); logf.flush()
208
+
209
+ device = "cuda" if torch.cuda.is_available() else "cpu"
210
+ log(f"[init] device={device}")
211
+ if device == "cuda":
212
+ free, total = torch.cuda.mem_get_info()
213
+ log(f"[init] gpu free {free/1e6:.1f} / {total/1e6:.1f} MB")
214
+
215
+ tok = load_tok()
216
+ vocab = tok.get_vocab_size()
217
+ log(f"[init] vocab={vocab}")
218
+
219
+ build_data(args, tok, log)
220
+
221
+ model = CompactLM(vocab, d=256, n_layers=4, n_heads=4, ff=640, ctx=args.ctx).to(device)
222
+ npar = count_params(model)
223
+ log(f"[init] params={npar:,} ({npar/1e6:.2f}M)")
224
+ assert 5_500_000 <= npar <= 7_000_000, f"param count {npar} not ~6M"
225
+
226
+ train_ids = np.load(os.path.join(args.out, "data", "train.npy"))
227
+ val_ids = np.load(os.path.join(args.out, "data", "val.npy"))
228
+
229
+ def batch_iter(ids, batch, shuffle=True, seed=0):
230
+ n = ids.shape[0]
231
+ rng = np.random.default_rng(seed)
232
+ idx = rng.permutation(n) if shuffle else np.arange(n)
233
+ for start in range(0, n, batch):
234
+ sel = idx[start:start + batch]
235
+ if len(sel) < batch:
236
+ continue
237
+ yield torch.from_numpy(ids[sel]).long().to(device)
238
+
239
+ step = 0
240
+ best_val = float("inf")
241
+ if args.resume and os.path.exists(os.path.join(args.out, "last.pt")):
242
+ ck = torch.load(os.path.join(args.out, "last.pt"), map_location="cpu")
243
+ model.load_state_dict(ck["model"])
244
+ step = ck["step"]; best_val = ck.get("best_val", float("inf"))
245
+ log(f"[resume] from step {step}, best_val {best_val:.4f}")
246
+
247
+ no_decay = [p for n, p in model.named_parameters() if p.ndim <= 1]
248
+ decay = [p for n, p in model.named_parameters() if p.ndim > 1]
249
+ groups = [{"params": decay, "weight_decay": args.weight_decay},
250
+ {"params": no_decay, "weight_decay": 0.0}]
251
+ opt = torch.optim.AdamW(groups, lr=args.lr, betas=(0.9, 0.95), eps=1e-8)
252
+
253
+ def lr_at(step):
254
+ if step < args.warmup:
255
+ return args.lr * (step + 1) / args.warmup
256
+ p = (step - args.warmup) / max(1, args.steps - args.warmup)
257
+ p = min(1.0, p)
258
+ return args.lr * (args.min_lr_frac + (1 - args.min_lr_frac) * 0.5 * (1 + math.cos(math.pi * p)))
259
+
260
+ model.train()
261
+ t0 = time.time()
262
+ train_iter = None
263
+ while step < args.steps:
264
+ if train_iter is None:
265
+ train_iter = batch_iter(train_ids, args.batch, shuffle=True, seed=step // 1000)
266
+ try:
267
+ b = next(train_iter)
268
+ except StopIteration:
269
+ train_iter = batch_iter(train_ids, args.batch, shuffle=True, seed=step // 1000)
270
+ b = next(train_iter)
271
+ for g in groups:
272
+ g["lr"] = lr_at(step)
273
+ opt.zero_grad()
274
+ loss = model(b, b)
275
+ loss.backward()
276
+ torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
277
+ opt.step()
278
+ step += 1
279
+ if step % 100 == 0 or step == 1:
280
+ tok_s = (step * args.batch * args.ctx) / max(1e-6, time.time() - t0)
281
+ log(f"[step {step}/{args.steps}] loss {loss.item():.4f} lr {lr_at(step):.2e} tok/s {tok_s:,.0f}")
282
+ if step % args.ckpt_every == 0:
283
+ torch.save({"model": model.state_dict(), "step": step, "best_val": best_val, "vocab": vocab},
284
+ os.path.join(args.out, "last.pt"))
285
+ if step % args.val_every == 0:
286
+ model.eval()
287
+ with torch.no_grad():
288
+ vloss = 0.0; n = 0
289
+ for b in batch_iter(val_ids, 32, shuffle=False, seed=0):
290
+ vloss += model(b, b).item(); n += 1
291
+ vloss /= max(1, n)
292
+ log(f"[val step {step}] val_loss {vloss:.4f} ppl {math.exp(min(vloss,20)):.2f}")
293
+ if vloss < best_val:
294
+ best_val = vloss
295
+ torch.save({"model": model.state_dict(), "step": step, "best_val": best_val, "vocab": vocab},
296
+ os.path.join(args.out, "best.pt"))
297
+ log(f"[val step {step}] NEW BEST -> best.pt")
298
+ model.train()
299
+ if step % args.sample_every == 0:
300
+ model.eval()
301
+ with torch.no_grad():
302
+ tok_prompts = ["Once upon a time", "The cat sat on the", "def hello():"]
303
+ for p in tok_prompts:
304
+ ids = torch.tensor([tok.encode(p, add_special_tokens=False).ids], device=device)
305
+ out = model.generate(ids, max_new_tokens=80, temperature=0.8, top_k=40, seed=step % 1000)
306
+ log(f"[sample step {step}] {tok.decode(out[0].tolist(), skip_special_tokens=True)[:400]!r}")
307
+ model.train()
308
+ torch.save({"model": model.state_dict(), "step": step, "best_val": best_val, "vocab": vocab},
309
+ os.path.join(args.out, "final.pt"))
310
+ model.eval()
311
+ with torch.no_grad():
312
+ vloss = 0.0; n = 0
313
+ for b in batch_iter(val_ids, 32, shuffle=False, seed=0):
314
+ vloss += model(b, b).item(); n += 1
315
+ vloss /= max(1, n)
316
+ log(f"[final] step {step} val_loss {vloss:.4f} ppl {math.exp(min(vloss,20)):.2f}")
317
+ logf.close()
318
+
319
+ if __name__ == "__main__":
320
+ _inherited_fd = os.environ.get("CLM_LOCK_FD")
321
+ if _inherited_fd is None:
322
+ _lock = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".train_clm5m.lock"), "w")
323
+ try:
324
+ fcntl.flock(_lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
325
+ except BlockingIOError:
326
+ print("[lock] another compactlm5m process already holds the lock — REFUSING to double-launch", flush=True)
327
+ sys.exit(3)
328
+ main()