OpenSoftware-World commited on
Commit
a8b2e73
ยท
verified ยท
1 Parent(s): 30b1619

Model training code for the OpenSoftware-World-OSW1 AI model. (This code was written by Claude and edited by OpenSoftware-World.)

Browse files

Download one of the datasets we use to train our OpenSoftware-World-OSW1:5m, OpenSoftware-World-OSW1:10m, or OpenSoftware-World-OSW1:100m AI models and place it in the folder containing the model_training.py file. You can then begin training the OpenSoftware-World-OSW1 AI model.

Files changed (1) hide show
  1. model_training.py +560 -0
model_training.py ADDED
@@ -0,0 +1,560 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import math
5
+ import time
6
+ import glob
7
+ import random
8
+
9
+ from dataclasses import dataclass
10
+
11
+ import torch
12
+ import torch.nn as nn
13
+ import torch.nn.functional as F
14
+
15
+ torch.manual_seed(42)
16
+ random.seed(42)
17
+
18
+ NUM_THREADS = os.cpu_count() or 4
19
+ torch.set_num_threads(NUM_THREADS)
20
+ try:
21
+ torch.set_num_interop_threads(max(1, NUM_THREADS // 2))
22
+ except RuntimeError:
23
+ # The number of interop threads can only be set once at the start of the program
24
+ pass
25
+
26
+ try:
27
+ torch.backends.mkldnn.enabled = True # Intel MKL-DNN acceleration (if available)
28
+ except Exception:
29
+ pass
30
+
31
+ DEVICE = torch.device("cpu")
32
+
33
+ # Autocasting to bfloat16 on the CPU can speed up most matmul operations (if supported)
34
+ USE_BF16_AUTOCAST = True
35
+ try:
36
+ _ = torch.zeros(1, dtype=torch.bfloat16) + torch.zeros(1, dtype=torch.bfloat16)
37
+ except Exception:
38
+ USE_BF16_AUTOCAST = False
39
+
40
+ print(f"๐Ÿงต Number of CPU threads : {NUM_THREADS}")
41
+ print(f"โš™๏ธ bfloat16 autocast status : {'active' if USE_BF16_AUTOCAST else 'inactive'}")
42
+
43
+ @dataclass
44
+ class OSW1Config:
45
+ data_dir: str = "data"
46
+
47
+ block_size: int = 128
48
+ d_model: int = 256
49
+ n_layer: int = 4
50
+ n_head: int = 4
51
+ d_ff: int = 1024
52
+ dropout: float = 0.1
53
+
54
+ batch_size: int = 8
55
+ grad_accum_steps: int = 2
56
+ epochs: int = 20
57
+ max_lr: float = 3e-4
58
+ min_lr: float = 3e-5
59
+ warmup_ratio: float = 0.05
60
+ weight_decay: float = 0.1
61
+ grad_clip: float = 1.0
62
+ label_smoothing: float = 0.05
63
+
64
+ checkpoint_prefix: str = "opensoftware_world_osw1"
65
+
66
+ TOKEN_RE = re.compile(r"\w+|[^\w\s]", re.UNICODE)
67
+
68
+ def tokenize(text: str):
69
+ return TOKEN_RE.findall(text.lower())
70
+
71
+ class Vocab:
72
+ PAD, UNK, BOS, EOS = "<pad>", "<unk>", "<bos>", "<eos>"
73
+
74
+ def __init__(self):
75
+ self.stoi = {}
76
+ self.itos = []
77
+
78
+ def build(self, token_stream):
79
+ specials = [Vocab.PAD, Vocab.UNK, Vocab.BOS, Vocab.EOS]
80
+ counts = {}
81
+ for tok in token_stream:
82
+ counts[tok] = counts.get(tok, 0) + 1
83
+ sorted_toks = sorted(counts.items(), key=lambda x: (-x[1], x[0]))
84
+ self.itos = specials + [t for t, _ in sorted_toks]
85
+ self.stoi = {t: i for i, t in enumerate(self.itos)}
86
+
87
+ def encode(self, text, add_bos=False, add_eos=False):
88
+ ids = [self.stoi.get(t, self.stoi[Vocab.UNK]) for t in tokenize(text)]
89
+ if add_bos:
90
+ ids = [self.stoi[Vocab.BOS]] + ids
91
+ if add_eos:
92
+ ids = ids + [self.stoi[Vocab.EOS]]
93
+ return ids
94
+
95
+ def decode(self, ids):
96
+ toks = [self.itos[i] for i in ids if 0 <= i < len(self.itos)]
97
+ toks = [t for t in toks if t != Vocab.PAD and t != Vocab.BOS]
98
+ out = []
99
+ for t in toks:
100
+ if t == Vocab.EOS:
101
+ break
102
+ out.append(t)
103
+ text = " ".join(out)
104
+ text = re.sub(r"\s+([.,!?;:])", r"\1", text)
105
+ return text
106
+
107
+ def __len__(self):
108
+ return len(self.itos)
109
+
110
+ def load_json_pairs(json_dir):
111
+ pairs = []
112
+ if not os.path.isdir(json_dir):
113
+ return pairs
114
+ for path in glob.glob(os.path.join(json_dir, "*.json")):
115
+ try:
116
+ with open(path, "r", encoding="utf-8") as f:
117
+ data = json.load(f)
118
+ except Exception as e:
119
+ print(f"โš ๏ธ {path} could not be read: {e}")
120
+ continue
121
+ intents = data.get("intents", data if isinstance(data, list) else [])
122
+ for intent in intents:
123
+ patterns = intent.get("patterns", []) or []
124
+ responses = intent.get("responses", []) or []
125
+ if not patterns or not responses:
126
+ continue
127
+ for p in patterns:
128
+ for r in responses:
129
+ pairs.append((p, r))
130
+ return pairs
131
+
132
+
133
+ def load_txt_qa_pairs(qa_dir):
134
+ pairs = []
135
+ if not os.path.isdir(qa_dir):
136
+ return pairs
137
+ for path in glob.glob(os.path.join(qa_dir, "*.txt")):
138
+ with open(path, "r", encoding="utf-8") as f:
139
+ lines = [l.rstrip("\n") for l in f.readlines()]
140
+ q, a = None, None
141
+ for raw in lines:
142
+ line = raw.strip()
143
+ if line.startswith("Q:"):
144
+ q = line[2:].strip()
145
+ elif line.startswith("A:"):
146
+ a = line[2:].strip()
147
+ if q is not None and a:
148
+ pairs.append((q, a))
149
+ q, a = None, None
150
+ return pairs
151
+
152
+
153
+ def load_plain_texts(txt_dir):
154
+ texts = []
155
+ if not os.path.isdir(txt_dir):
156
+ return texts
157
+ for path in glob.glob(os.path.join(txt_dir, "*.txt")):
158
+ with open(path, "r", encoding="utf-8") as f:
159
+ content = f.read().strip()
160
+ if content:
161
+ texts.append(content)
162
+ return texts
163
+
164
+
165
+ def build_corpus(cfg: OSW1Config, vocab: Vocab):
166
+ json_dir = os.path.join(cfg.data_dir, "json")
167
+ qa_dir = os.path.join(cfg.data_dir, "txt_qa")
168
+ txt_dir = os.path.join(cfg.data_dir, "txt")
169
+
170
+ qa_pairs = load_json_pairs(json_dir) + load_txt_qa_pairs(qa_dir)
171
+ plain_texts = load_plain_texts(txt_dir)
172
+
173
+ print(f"๐Ÿ“š JSON + txt_qa pair count : {len(qa_pairs)}")
174
+ print(f"๐Ÿ“„ Plain text file count : {len(plain_texts)}")
175
+
176
+ if not qa_pairs and not plain_texts:
177
+ raise RuntimeError(
178
+ "No data found! Please populate the 'data/json', 'data/txt', 'data/txt_qa' "
179
+ "folders with data for the model to learn from."
180
+ )
181
+
182
+ all_tokens = []
183
+ for q, a in qa_pairs:
184
+ all_tokens.extend(tokenize(q))
185
+ all_tokens.extend(tokenize(a))
186
+ for t in plain_texts:
187
+ all_tokens.extend(tokenize(t))
188
+ vocab.build(all_tokens)
189
+
190
+ sequences = []
191
+
192
+ for q, a in qa_pairs:
193
+ ids = [vocab.stoi[Vocab.BOS]]
194
+ ids += vocab.encode(q)
195
+ ids += vocab.encode(a)
196
+ ids += [vocab.stoi[Vocab.EOS]]
197
+ if len(ids) >= 4:
198
+ sequences.append(ids)
199
+
200
+ for t in plain_texts:
201
+ ids = [vocab.stoi[Vocab.BOS]] + vocab.encode(t) + [vocab.stoi[Vocab.EOS]]
202
+ stride = max(1, cfg.block_size // 2)
203
+ for i in range(0, max(1, len(ids) - 1), stride):
204
+ chunk = ids[i:i + cfg.block_size + 1]
205
+ if len(chunk) >= 8:
206
+ sequences.append(chunk)
207
+
208
+ random.shuffle(sequences)
209
+ print(f"๐Ÿงฉ Total training sequences (sequence): {len(sequences)}")
210
+ print(f"๐Ÿ”ค Vocab size : {len(vocab)}")
211
+ return sequences
212
+
213
+ class SeqDataset(torch.utils.data.Dataset):
214
+ def __init__(self, sequences, block_size):
215
+ self.sequences = sequences
216
+ self.block_size = block_size
217
+
218
+ def __len__(self):
219
+ return len(self.sequences)
220
+
221
+ def __getitem__(self, idx):
222
+ ids = self.sequences[idx][: self.block_size + 1]
223
+ return torch.tensor(ids, dtype=torch.long)
224
+
225
+
226
+ def make_collate(pad_id):
227
+ def collate(batch):
228
+ max_len = max(len(x) for x in batch)
229
+ padded = torch.full((len(batch), max_len), pad_id, dtype=torch.long)
230
+ for i, seq in enumerate(batch):
231
+ padded[i, : len(seq)] = seq
232
+ x = padded[:, :-1].contiguous()
233
+ y = padded[:, 1:].contiguous()
234
+ return x, y
235
+ return collate
236
+
237
+ class CausalSelfAttention(nn.Module):
238
+ def __init__(self, d_model, n_head, dropout):
239
+ super().__init__()
240
+ assert d_model % n_head == 0, "d_model must be evenly divisible by n_head"
241
+ self.n_head = n_head
242
+ self.head_dim = d_model // n_head
243
+ self.qkv = nn.Linear(d_model, 3 * d_model)
244
+ self.proj = nn.Linear(d_model, d_model)
245
+ self.attn_drop = nn.Dropout(dropout)
246
+ self.resid_drop = nn.Dropout(dropout)
247
+
248
+ def forward(self, x, attn_mask):
249
+ B, T, C = x.shape
250
+ qkv = self.qkv(x)
251
+ q, k, v = qkv.split(C, dim=2)
252
+ q = q.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
253
+ k = k.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
254
+ v = v.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
255
+
256
+ att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
257
+ att = att.masked_fill(attn_mask, float("-inf"))
258
+ att = F.softmax(att, dim=-1)
259
+ att = self.attn_drop(att)
260
+ out = att @ v
261
+ out = out.transpose(1, 2).contiguous().view(B, T, C)
262
+ return self.resid_drop(self.proj(out))
263
+
264
+
265
+ class TransformerBlock(nn.Module):
266
+ def __init__(self, d_model, n_head, d_ff, dropout):
267
+ super().__init__()
268
+ self.ln1 = nn.LayerNorm(d_model)
269
+ self.attn = CausalSelfAttention(d_model, n_head, dropout)
270
+ self.ln2 = nn.LayerNorm(d_model)
271
+ self.mlp = nn.Sequential(
272
+ nn.Linear(d_model, d_ff),
273
+ nn.GELU(),
274
+ nn.Linear(d_ff, d_model),
275
+ nn.Dropout(dropout),
276
+ )
277
+
278
+ def forward(self, x, attn_mask):
279
+ x = x + self.attn(self.ln1(x), attn_mask)
280
+ x = x + self.mlp(self.ln2(x))
281
+ return x
282
+
283
+
284
+ class OSW1Model(nn.Module):
285
+ def __init__(self, vocab_size, cfg: OSW1Config, pad_id: int):
286
+ super().__init__()
287
+ self.cfg = cfg
288
+ self.pad_id = pad_id
289
+
290
+ self.tok_emb = nn.Embedding(vocab_size, cfg.d_model)
291
+ self.pos_emb = nn.Embedding(cfg.block_size, cfg.d_model)
292
+ self.drop = nn.Dropout(cfg.dropout)
293
+ self.blocks = nn.ModuleList([
294
+ TransformerBlock(cfg.d_model, cfg.n_head, cfg.d_ff, cfg.dropout)
295
+ for _ in range(cfg.n_layer)
296
+ ])
297
+ self.ln_f = nn.LayerNorm(cfg.d_model)
298
+ self.head = nn.Linear(cfg.d_model, vocab_size, bias=False)
299
+ self.head.weight = self.tok_emb.weight
300
+
301
+ self.apply(self._init_weights)
302
+
303
+ def _init_weights(self, module):
304
+ if isinstance(module, nn.Linear):
305
+ nn.init.normal_(module.weight, mean=0.0, std=0.02)
306
+ if module.bias is not None:
307
+ nn.init.zeros_(module.bias)
308
+ elif isinstance(module, nn.Embedding):
309
+ nn.init.normal_(module.weight, mean=0.0, std=0.02)
310
+
311
+ def forward(self, idx, targets=None):
312
+ B, T = idx.shape
313
+ pos = torch.arange(T, device=idx.device).unsqueeze(0)
314
+ x = self.drop(self.tok_emb(idx) + self.pos_emb(pos))
315
+
316
+ mask = torch.triu(torch.ones(T, T, dtype=torch.bool, device=idx.device), diagonal=1)
317
+ for block in self.blocks:
318
+ x = block(x, mask)
319
+ x = self.ln_f(x)
320
+ logits = self.head(x)
321
+
322
+ loss = None
323
+ if targets is not None:
324
+ loss = F.cross_entropy(
325
+ logits.reshape(-1, logits.size(-1)),
326
+ targets.reshape(-1),
327
+ ignore_index=self.pad_id,
328
+ label_smoothing=self.cfg.label_smoothing,
329
+ )
330
+ return logits, loss
331
+
332
+ @torch.no_grad()
333
+ def generate(self, idx, max_new_tokens, temperature=0.9, top_k=40, eos_id=None):
334
+ was_training = self.training
335
+ self.eval()
336
+ for _ in range(max_new_tokens):
337
+ idx_cond = idx[:, -self.cfg.block_size:]
338
+ logits, _ = self(idx_cond)
339
+ logits = logits[:, -1, :] / max(temperature, 1e-5)
340
+ if top_k is not None:
341
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
342
+ logits[logits < v[:, [-1]]] = float("-inf")
343
+ probs = F.softmax(logits, dim=-1)
344
+ next_id = torch.multinomial(probs, num_samples=1)
345
+ idx = torch.cat([idx, next_id], dim=1)
346
+ if eos_id is not None and next_id.item() == eos_id:
347
+ break
348
+ if was_training:
349
+ self.train()
350
+ return idx
351
+
352
+ def count_parameters(model: OSW1Model):
353
+ total = sum(p.numel() for p in model.parameters())
354
+ trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
355
+ breakdown = {
356
+ "Token + Position Embedding": model.tok_emb.weight.numel() + model.pos_emb.weight.numel(),
357
+ f"Transformer Blocks ({len(model.blocks)} pieces)": sum(p.numel() for p in model.blocks.parameters()),
358
+ "Final LayerNorm": sum(p.numel() for p in model.ln_f.parameters()),
359
+ "Output Layer (shared with embedding, no extra parameters)": 0,
360
+ }
361
+ return total, trainable, breakdown
362
+
363
+ def human_readable_param_count(n: int):
364
+ if n >= 1_000_000_000:
365
+ return f"{n/1_000_000_000:.2f}B", f"{max(1, round(n/1_000_000_000))}b"
366
+ elif n >= 1_000_000:
367
+ return f"{n/1_000_000:.2f}M", f"{max(1, round(n/1_000_000))}m"
368
+ elif n >= 1_000:
369
+ return f"{n/1_000:.2f}K", f"{max(1, round(n/1_000))}k"
370
+ else:
371
+ return str(n), str(n)
372
+
373
+ def print_model_report(model: OSW1Model, cfg: OSW1Config, vocab_size: int):
374
+ total, trainable, breakdown = count_parameters(model)
375
+ pretty, short = human_readable_param_count(total)
376
+ size_mb = total * 4 / (1024 ** 2)
377
+
378
+ print("\n" + "=" * 64)
379
+ print("๐Ÿง  OpenSoftware-World OSW1 โ€” MODEL REPORT")
380
+ print("=" * 64)
381
+ print(f" Vocab size : {vocab_size:,}")
382
+ print(f" Context window (block) : {cfg.block_size}")
383
+ print(f" Embedding size (d_model) : {cfg.d_model}")
384
+ print(f" Number of layers (n_layer) : {cfg.n_layer}")
385
+ print(f" Head count (n_head) : {cfg.n_head}")
386
+ print(f" Feed-forward size (d_ff) : {cfg.d_ff}")
387
+ print("-" * 64)
388
+ for name, count in breakdown.items():
389
+ print(f" {name:<50}: {count:,}")
390
+ print("-" * 64)
391
+ print(f" TOTAL PARAMETER COUNT : {total:,} (~{pretty})")
392
+ print(f" TRAINABLE PARAMETERS : {trainable:,}")
393
+ print(f" Estimated model size : {size_mb:.2f} MB (float32)")
394
+ print(f" Checkpoint file label : {short} -> {cfg.checkpoint_prefix}_{short}.pth")
395
+ print("=" * 64 + "\n")
396
+ return short
397
+
398
+ def lr_at_step(step, total_steps, warmup_steps, max_lr, min_lr):
399
+ if step < warmup_steps:
400
+ return max_lr * (step + 1) / max(1, warmup_steps)
401
+ progress = (step - warmup_steps) / max(1, total_steps - warmup_steps)
402
+ progress = min(max(progress, 0.0), 1.0)
403
+ return min_lr + 0.5 * (max_lr - min_lr) * (1 + math.cos(math.pi * progress))
404
+
405
+ def train(cfg: OSW1Config):
406
+ vocab = Vocab()
407
+ sequences = build_corpus(cfg, vocab)
408
+ pad_id = vocab.stoi[Vocab.PAD]
409
+
410
+ dataset = SeqDataset(sequences, cfg.block_size)
411
+ loader = torch.utils.data.DataLoader(
412
+ dataset,
413
+ batch_size=cfg.batch_size,
414
+ shuffle=True,
415
+ collate_fn=make_collate(pad_id),
416
+ num_workers=0,
417
+ drop_last=True,
418
+ )
419
+
420
+ if len(loader) == 0:
421
+ raise RuntimeError(
422
+ "The dataset is too small to even create a batch. "
423
+ "Try reducing 'batch_size' or adding more data."
424
+ )
425
+
426
+ model = OSW1Model(len(vocab), cfg, pad_id=pad_id).to(DEVICE)
427
+ compiled_model = model
428
+ try:
429
+ compiled_model = torch.compile(model, backend="inductor")
430
+ print("๐Ÿš€ torch.compile has been enabled (provides an extra speed boost if available).")
431
+ except Exception as e:
432
+ print(f"โ„น๏ธ torch.compile could not be used, continuing in normal mode: {e}")
433
+
434
+ size_tag = print_model_report(model, cfg, len(vocab))
435
+
436
+ optimizer = torch.optim.AdamW(
437
+ model.parameters(),
438
+ lr=cfg.max_lr,
439
+ betas=(0.9, 0.95),
440
+ weight_decay=cfg.weight_decay,
441
+ )
442
+
443
+ steps_per_epoch = max(1, len(loader) // cfg.grad_accum_steps)
444
+ total_steps = steps_per_epoch * cfg.epochs
445
+ warmup_steps = max(1, int(total_steps * cfg.warmup_ratio))
446
+
447
+ print(f"โฑ๏ธ Total optimization steps : {total_steps} | Warmup steps: {warmup_steps}")
448
+ print(f"๐Ÿ‹๏ธ Training starting... ({cfg.epochs} epoch, batch={cfg.batch_size}, "
449
+ f"grad_accum={cfg.grad_accum_steps})\n")
450
+
451
+ global_step = 0
452
+ train_start = time.time()
453
+
454
+ for epoch in range(1, cfg.epochs + 1):
455
+ epoch_start = time.time()
456
+ epoch_loss, n_batches = 0.0, 0
457
+ optimizer.zero_grad(set_to_none=True)
458
+
459
+ for i, (x, y) in enumerate(loader):
460
+ x, y = x.to(DEVICE), y.to(DEVICE)
461
+
462
+ if USE_BF16_AUTOCAST:
463
+ with torch.autocast(device_type="cpu", dtype=torch.bfloat16):
464
+ _, loss = compiled_model(x, y)
465
+ else:
466
+ _, loss = compiled_model(x, y)
467
+
468
+ loss_scaled = loss / cfg.grad_accum_steps
469
+ loss_scaled.backward()
470
+
471
+ if (i + 1) % cfg.grad_accum_steps == 0:
472
+ torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.grad_clip)
473
+ lr = lr_at_step(global_step, total_steps, warmup_steps, cfg.max_lr, cfg.min_lr)
474
+ for g in optimizer.param_groups:
475
+ g["lr"] = lr
476
+ optimizer.step()
477
+ optimizer.zero_grad(set_to_none=True)
478
+ global_step += 1
479
+
480
+ epoch_loss += loss.item()
481
+ n_batches += 1
482
+
483
+ avg_loss = epoch_loss / max(1, n_batches)
484
+ ppl = math.exp(min(avg_loss, 20))
485
+ epoch_time = time.time() - epoch_start
486
+ elapsed_total = time.time() - train_start
487
+ current_lr = optimizer.param_groups[0]["lr"]
488
+ print(
489
+ f"๐Ÿ“ˆ Epoch {epoch:>3}/{cfg.epochs} | "
490
+ f"loss={avg_loss:.4f} | ppl={ppl:.2f} | "
491
+ f"lr={current_lr:.2e} | "
492
+ f"time={epoch_time:.1f}s | total={elapsed_total/60:.1f}m"
493
+ )
494
+
495
+ total_time = time.time() - train_start
496
+ print(f"\nโœ… Training completed! Total time: "
497
+ f"{total_time/60:.2f} minutes ({total_time:.1f} seconds)\n")
498
+
499
+ ckpt_path = f"{cfg.checkpoint_prefix}_{size_tag}.pth"
500
+ torch.save({
501
+ "model_state_dict": model.state_dict(),
502
+ "config": cfg.__dict__,
503
+ "vocab_stoi": vocab.stoi,
504
+ "vocab_itos": vocab.itos,
505
+ "pad_id": pad_id,
506
+ "param_count": sum(p.numel() for p in model.parameters()),
507
+ "training_time_sec": total_time,
508
+ "final_loss": avg_loss,
509
+ }, ckpt_path)
510
+ print(f"๐Ÿ’พ Model saved: {ckpt_path}\n")
511
+
512
+ return model, vocab, cfg, ckpt_path
513
+
514
+ def chat_loop(model: OSW1Model, vocab: Vocab, cfg: OSW1Config):
515
+ print("=" * 64)
516
+ print("๐Ÿ’ฌ OSW1 with chat mode! Type 'exit' to quit.")
517
+ print("=" * 64)
518
+ model.eval()
519
+ eos_id = vocab.stoi[Vocab.EOS]
520
+ bos_id = vocab.stoi[Vocab.BOS]
521
+
522
+ while True:
523
+ try:
524
+ user_in = input("\You: ").strip()
525
+ except (EOFError, KeyboardInterrupt):
526
+ print("\n๐Ÿ‘‹ Goodbye!")
527
+ break
528
+
529
+ if user_in.lower() in ("exit", "quit"):
530
+ print("๐Ÿ‘‹ Goodbye!")
531
+ break
532
+ if not user_in:
533
+ continue
534
+
535
+ ids = [bos_id] + vocab.encode(user_in)
536
+ x = torch.tensor([ids], dtype=torch.long)
537
+ out = model.generate(x, max_new_tokens=60, temperature=0.85, top_k=40, eos_id=eos_id)
538
+ answer_ids = out[0, len(ids):].tolist()
539
+ answer = vocab.decode(answer_ids)
540
+ print(f"OSW1: {answer if answer else '(...silence...)'}")
541
+
542
+ def load_checkpoint(path: str):
543
+ ckpt = torch.load(path, map_location="cpu")
544
+ cfg = OSW1Config(**ckpt["config"])
545
+ vocab = Vocab()
546
+ vocab.stoi = ckpt["vocab_stoi"]
547
+ vocab.itos = ckpt["vocab_itos"]
548
+ model = OSW1Model(len(vocab), cfg, pad_id=ckpt["pad_id"])
549
+ model.load_state_dict(ckpt["model_state_dict"])
550
+ model.eval()
551
+ return model, vocab, cfg
552
+
553
+ def main():
554
+ cfg = OSW1Config()
555
+ model, vocab, cfg, ckpt_path = train(cfg)
556
+ chat_loop(model, vocab, cfg)
557
+
558
+
559
+ if __name__ == "__main__":
560
+ main()