OpenSoftware-World commited on
Commit
8edb682
·
verified ·
1 Parent(s): 5f309d0

The model training and model initialization code has been updated. (CPU/GPU auto-detection has been added.)

Browse files
Files changed (2) hide show
  1. model_init.py +23 -4
  2. model_training.py +134 -77
model_init.py CHANGED
@@ -8,6 +8,7 @@ import torch
8
  import torch.nn as nn
9
  import torch.nn.functional as F
10
  import sentencepiece as spm
 
11
 
12
  NUM_THREADS = os.cpu_count() or 4
13
  torch.set_num_threads(NUM_THREADS)
@@ -16,8 +17,17 @@ try:
16
  except RuntimeError:
17
  pass
18
 
19
- DEVICE = torch.device("cpu")
 
 
 
 
 
 
 
20
  print(f"🧵 Number of CPU threads : {NUM_THREADS}")
 
 
21
 
22
  class Vocab:
23
  PAD = "<pad>"
@@ -152,6 +162,10 @@ class OSW1Model(nn.Module):
152
  @torch.no_grad()
153
  def generate(self, idx, max_new_tokens, temperature=0.85, top_k=40, eos_id=None):
154
  self.eval()
 
 
 
 
155
  for _ in range(max_new_tokens):
156
  idx_cond = idx[:, -self.block_size:]
157
  logits = self(idx_cond)
@@ -175,6 +189,8 @@ def find_checkpoint():
175
 
176
  def load_checkpoint(path: str):
177
  print(f"📦 Loading: {path}")
 
 
178
  ckpt = torch.load(path, map_location="cpu")
179
 
180
  cfg = ckpt["config"]
@@ -188,6 +204,7 @@ def load_checkpoint(path: str):
188
  param_count = ckpt.get("param_count", sum(p.numel() for p in model.parameters()))
189
  training_time = ckpt.get("training_time_sec", None)
190
  final_loss = ckpt.get("final_loss", None)
 
191
 
192
  print("\n" + "=" * 64)
193
  print("🧠 OpenSoftware-World OSW1 — LOADED MODEL INFORMATION")
@@ -202,6 +219,7 @@ def load_checkpoint(path: str):
202
  print(f" Training time : {training_time/60:.2f} minutes")
203
  if final_loss is not None:
204
  print(f" Final training loss : {final_loss:.4f}")
 
205
  print("=" * 64 + "\n")
206
 
207
  return model, vocab, cfg
@@ -211,6 +229,7 @@ def chat_loop(model: OSW1Model, vocab: Vocab):
211
  print("💬 OpenSoftware-World-OSW1 ready! You can start chatting. Type 'exit' to quit.")
212
  print("=" * 64)
213
 
 
214
  eos_id = vocab.stoi[Vocab.EOS]
215
  bos_id = vocab.stoi[Vocab.BOS]
216
 
@@ -228,8 +247,8 @@ def chat_loop(model: OSW1Model, vocab: Vocab):
228
  continue
229
 
230
  ids = [bos_id] + vocab.encode(user_in)
231
- x = torch.tensor([ids], dtype=torch.long)
232
- out = model.generate(x, max_new_tokens=60, temperature=0.85, top_k=40, eos_id=eos_id)
233
  answer_ids = out[0, len(ids):].tolist()
234
  answer = vocab.decode(answer_ids)
235
  print(f"OpenSoftware-World-OSW1: {answer if answer else '(...silence...)'}")
@@ -255,4 +274,4 @@ def main():
255
 
256
 
257
  if __name__ == "__main__":
258
- main()
 
8
  import torch.nn as nn
9
  import torch.nn.functional as F
10
  import sentencepiece as spm
11
+ from config.model_config import *
12
 
13
  NUM_THREADS = os.cpu_count() or 4
14
  torch.set_num_threads(NUM_THREADS)
 
17
  except RuntimeError:
18
  pass
19
 
20
+ # --- Automatic device selection: use a compatible GPU if available, otherwise fall back to CPU ---
21
+ if torch.cuda.is_available():
22
+ DEVICE = torch.device("cuda")
23
+ elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
24
+ DEVICE = torch.device("mps")
25
+ else:
26
+ DEVICE = torch.device("cpu")
27
+
28
  print(f"🧵 Number of CPU threads : {NUM_THREADS}")
29
+ print(f"🖥️ Selected device : {DEVICE.type.upper()}"
30
+ + (f" ({torch.cuda.get_device_name(0)})" if DEVICE.type == "cuda" else ""))
31
 
32
  class Vocab:
33
  PAD = "<pad>"
 
162
  @torch.no_grad()
163
  def generate(self, idx, max_new_tokens, temperature=0.85, top_k=40, eos_id=None):
164
  self.eval()
165
+ # Make sure the input tensor lives on the same device as the model itself,
166
+ # so generation works no matter which device the checkpoint was trained on.
167
+ model_device = next(self.parameters()).device
168
+ idx = idx.to(model_device)
169
  for _ in range(max_new_tokens):
170
  idx_cond = idx[:, -self.block_size:]
171
  logits = self(idx_cond)
 
189
 
190
  def load_checkpoint(path: str):
191
  print(f"📦 Loading: {path}")
192
+ # map_location="cpu" guarantees the checkpoint can always be read back,
193
+ # regardless of which device (GPU/MPS/CPU) it was trained on.
194
  ckpt = torch.load(path, map_location="cpu")
195
 
196
  cfg = ckpt["config"]
 
204
  param_count = ckpt.get("param_count", sum(p.numel() for p in model.parameters()))
205
  training_time = ckpt.get("training_time_sec", None)
206
  final_loss = ckpt.get("final_loss", None)
207
+ trained_on = ckpt.get("trained_on_device", "unknown")
208
 
209
  print("\n" + "=" * 64)
210
  print("🧠 OpenSoftware-World OSW1 — LOADED MODEL INFORMATION")
 
219
  print(f" Training time : {training_time/60:.2f} minutes")
220
  if final_loss is not None:
221
  print(f" Final training loss : {final_loss:.4f}")
222
+ print(f" Trained on device : {trained_on} -> Running on: {DEVICE.type}")
223
  print("=" * 64 + "\n")
224
 
225
  return model, vocab, cfg
 
229
  print("💬 OpenSoftware-World-OSW1 ready! You can start chatting. Type 'exit' to quit.")
230
  print("=" * 64)
231
 
232
+ model_device = next(model.parameters()).device
233
  eos_id = vocab.stoi[Vocab.EOS]
234
  bos_id = vocab.stoi[Vocab.BOS]
235
 
 
247
  continue
248
 
249
  ids = [bos_id] + vocab.encode(user_in)
250
+ x = torch.tensor([ids], dtype=torch.long, device=model_device)
251
+ out = model.generate(x, max_new_tokens=init_max_new_tokens, temperature=init_temperature, top_k=init_top_k, eos_id=eos_id)
252
  answer_ids = out[0, len(ids):].tolist()
253
  answer = vocab.decode(answer_ids)
254
  print(f"OpenSoftware-World-OSW1: {answer if answer else '(...silence...)'}")
 
274
 
275
 
276
  if __name__ == "__main__":
277
+ main()
model_training.py CHANGED
@@ -5,12 +5,15 @@ 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)
@@ -28,84 +31,111 @@ try:
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 = []
@@ -179,14 +209,6 @@ def build_corpus(cfg: OSW1Config, vocab: Vocab):
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:
@@ -333,6 +355,10 @@ class OSW1Model(nn.Module):
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)
@@ -402,6 +428,20 @@ def lr_at_step(step, total_steps, warmup_steps, max_lr, min_lr):
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)
@@ -440,6 +480,12 @@ def train(cfg: OSW1Config):
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))
@@ -459,21 +505,21 @@ def train(cfg: OSW1Config):
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
 
@@ -497,15 +543,18 @@ def train(cfg: OSW1Config):
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
 
@@ -516,12 +565,13 @@ def chat_loop(model: OSW1Model, vocab: Vocab, cfg: OSW1Config):
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
@@ -533,21 +583,28 @@ def chat_loop(model: OSW1Model, vocab: Vocab, cfg: OSW1Config):
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():
@@ -557,4 +614,4 @@ def main():
557
 
558
 
559
  if __name__ == "__main__":
560
- main()
 
5
  import time
6
  import glob
7
  import random
8
+ import contextlib
9
 
10
  from dataclasses import dataclass
11
 
12
  import torch
13
  import torch.nn as nn
14
  import torch.nn.functional as F
15
+ import sentencepiece as spm
16
+ from config.model_config import *
17
 
18
  torch.manual_seed(42)
19
  random.seed(42)
 
31
  except Exception:
32
  pass
33
 
34
+ # --- Automatic device selection: use a compatible GPU if available, otherwise fall back to CPU ---
35
+ if torch.cuda.is_available():
36
+ DEVICE = torch.device("cuda")
37
+ elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
38
+ DEVICE = torch.device("mps")
39
+ else:
40
+ DEVICE = torch.device("cpu")
41
 
42
  # Autocasting to bfloat16 on the CPU can speed up most matmul operations (if supported)
43
+ # This optimization only matters when we are actually training on CPU.
44
+ USE_BF16_AUTOCAST = False
45
+ if DEVICE.type == "cpu":
46
+ try:
47
+ _ = torch.zeros(1, dtype=torch.bfloat16) + torch.zeros(1, dtype=torch.bfloat16)
48
+ USE_BF16_AUTOCAST = True
49
+ except Exception:
50
+ USE_BF16_AUTOCAST = False
51
+
52
+ # Whether bfloat16 autocast is available on the current CUDA GPU (Ampere+ generally supports this)
53
+ USE_CUDA_BF16_AUTOCAST = DEVICE.type == "cuda" and torch.cuda.is_bf16_supported()
54
 
55
  print(f"🧵 Number of CPU threads : {NUM_THREADS}")
56
+ print(f"🖥Selected training device : {DEVICE.type.upper()}"
57
+ + (f" ({torch.cuda.get_device_name(0)})" if DEVICE.type == "cuda" else ""))
58
+ print(f"⚙️ bfloat16 autocast status : "
59
+ f"{'active (CPU)' if USE_BF16_AUTOCAST else ('active (CUDA)' if USE_CUDA_BF16_AUTOCAST else 'inactive')}")
60
 
61
  @dataclass
62
  class OSW1Config:
63
  data_dir: str = "data"
64
 
65
+ block_size: int = training_block_size
66
+ d_model: int = training_d_model
67
+ n_layer: int = training_n_layer
68
+ n_head: int = training_n_head
69
+ d_ff: int = training_d_ff
70
+ dropout: float = training_dropout
71
+
72
+ batch_size: int = training_batch_size
73
+ grad_accum_steps: int = training_grad_accum_steps
74
+ epochs: int = training_epochs
75
+ max_lr: float = training_max_lr
76
+ min_lr: float = training_min_lr
77
+ warmup_ratio: float = training_warmup_ratio
78
+ weight_decay: float = training_weight_decay
79
+ grad_clip: float = training_grad_clip
80
+ label_smoothing: float = training_label_smoothing
81
 
82
  checkpoint_prefix: str = "opensoftware_world_osw1"
83
 
 
 
 
 
 
84
  class Vocab:
85
+ PAD = "<pad>"
86
+ UNK = "<unk>"
87
+ BOS = "<bos>"
88
+ EOS = "<eos>"
89
 
90
+ def __init__(self, model_path="opensoftware_world_osw1_tokenizer.model"):
91
+ self.sp = spm.SentencePieceProcessor()
92
+ self.sp.load(model_path)
93
 
94
+ self.pad_id = self.sp.pad_id()
95
+ self.unk_id = self.sp.unk_id()
96
+ self.bos_id = self.sp.bos_id()
97
+ self.eos_id = self.sp.eos_id()
 
 
 
 
98
 
99
  def encode(self, text, add_bos=False, add_eos=False):
100
+ ids = self.sp.encode(text, out_type=int)
101
+
102
  if add_bos:
103
+ ids = [self.bos_id] + ids
104
+
105
  if add_eos:
106
+ ids = ids + [self.eos_id]
107
+
108
  return ids
109
 
110
  def decode(self, ids):
111
+ ids = [
112
+ i for i in ids
113
+ if i not in (self.pad_id, self.bos_id)
114
+ ]
115
+
116
+ if self.eos_id in ids:
117
+ ids = ids[:ids.index(self.eos_id)]
118
+
119
+ return self.sp.decode(ids)
 
120
 
121
  def __len__(self):
122
+ return self.sp.get_piece_size()
123
+
124
+ @property
125
+ def stoi(self):
126
+ return {
127
+ self.PAD: self.pad_id,
128
+ self.UNK: self.unk_id,
129
+ self.BOS: self.bos_id,
130
+ self.EOS: self.eos_id,
131
+ }
132
+
133
+ @property
134
+ def itos(self):
135
+ return [
136
+ self.sp.id_to_piece(i)
137
+ for i in range(self.sp.get_piece_size())
138
+ ]
139
 
140
  def load_json_pairs(json_dir):
141
  pairs = []
 
209
  "folders with data for the model to learn from."
210
  )
211
 
 
 
 
 
 
 
 
 
212
  sequences = []
213
 
214
  for q, a in qa_pairs:
 
355
  def generate(self, idx, max_new_tokens, temperature=0.9, top_k=40, eos_id=None):
356
  was_training = self.training
357
  self.eval()
358
+ # Make sure the input tensor lives on the same device as the model itself,
359
+ # so generation works no matter which device the model was trained/loaded on.
360
+ model_device = next(self.parameters()).device
361
+ idx = idx.to(model_device)
362
  for _ in range(max_new_tokens):
363
  idx_cond = idx[:, -self.cfg.block_size:]
364
  logits, _ = self(idx_cond)
 
428
  progress = min(max(progress, 0.0), 1.0)
429
  return min_lr + 0.5 * (max_lr - min_lr) * (1 + math.cos(math.pi * progress))
430
 
431
+ def get_autocast_context():
432
+ """
433
+ Returns the correct autocast context manager for whichever device we ended up
434
+ training on (CUDA, CPU, or MPS/other). Falls back to a no-op context if the
435
+ current device doesn't support (or benefit from) autocasting here.
436
+ """
437
+ if DEVICE.type == "cuda":
438
+ dtype = torch.bfloat16 if USE_CUDA_BF16_AUTOCAST else torch.float16
439
+ return torch.autocast(device_type="cuda", dtype=dtype)
440
+ elif DEVICE.type == "cpu" and USE_BF16_AUTOCAST:
441
+ return torch.autocast(device_type="cpu", dtype=torch.bfloat16)
442
+ else:
443
+ return contextlib.nullcontext()
444
+
445
  def train(cfg: OSW1Config):
446
  vocab = Vocab()
447
  sequences = build_corpus(cfg, vocab)
 
480
  weight_decay=cfg.weight_decay,
481
  )
482
 
483
+ # Only needed for numerically-fragile float16 training on CUDA GPUs that lack
484
+ # native bfloat16 support. When bfloat16 is available (or we're on CPU/MPS),
485
+ # the scaler simply stays disabled and behaves as a no-op.
486
+ use_grad_scaler = DEVICE.type == "cuda" and not USE_CUDA_BF16_AUTOCAST
487
+ scaler = torch.amp.GradScaler(enabled=use_grad_scaler)
488
+
489
  steps_per_epoch = max(1, len(loader) // cfg.grad_accum_steps)
490
  total_steps = steps_per_epoch * cfg.epochs
491
  warmup_steps = max(1, int(total_steps * cfg.warmup_ratio))
 
505
  for i, (x, y) in enumerate(loader):
506
  x, y = x.to(DEVICE), y.to(DEVICE)
507
 
508
+ with get_autocast_context():
 
 
 
509
  _, loss = compiled_model(x, y)
510
 
511
  loss_scaled = loss / cfg.grad_accum_steps
512
+ scaler.scale(loss_scaled).backward()
513
 
514
  if (i + 1) % cfg.grad_accum_steps == 0:
515
+ if use_grad_scaler:
516
+ scaler.unscale_(optimizer)
517
  torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.grad_clip)
518
  lr = lr_at_step(global_step, total_steps, warmup_steps, cfg.max_lr, cfg.min_lr)
519
  for g in optimizer.param_groups:
520
  g["lr"] = lr
521
+ scaler.step(optimizer)
522
+ scaler.update()
523
  optimizer.zero_grad(set_to_none=True)
524
  global_step += 1
525
 
 
543
  f"{total_time/60:.2f} minutes ({total_time:.1f} seconds)\n")
544
 
545
  ckpt_path = f"{cfg.checkpoint_prefix}_{size_tag}.pth"
546
+ # Move every tensor in the state dict to CPU before saving. This makes the
547
+ # checkpoint device-agnostic: a model trained on GPU can later be loaded
548
+ # and run correctly on a machine that only has a CPU (and vice versa).
549
+ cpu_state_dict = {k: v.detach().cpu() for k, v in model.state_dict().items()}
550
  torch.save({
551
+ "model_state_dict": cpu_state_dict,
552
  "config": cfg.__dict__,
 
 
553
  "pad_id": pad_id,
554
  "param_count": sum(p.numel() for p in model.parameters()),
555
  "training_time_sec": total_time,
556
  "final_loss": avg_loss,
557
+ "trained_on_device": DEVICE.type,
558
  }, ckpt_path)
559
  print(f"💾 Model saved: {ckpt_path}\n")
560
 
 
565
  print("💬 OSW1 with chat mode! Type 'exit' to quit.")
566
  print("=" * 64)
567
  model.eval()
568
+ model_device = next(model.parameters()).device
569
  eos_id = vocab.stoi[Vocab.EOS]
570
  bos_id = vocab.stoi[Vocab.BOS]
571
 
572
  while True:
573
  try:
574
+ user_in = input("\nYou: ").strip()
575
  except (EOFError, KeyboardInterrupt):
576
  print("\n👋 Goodbye!")
577
  break
 
583
  continue
584
 
585
  ids = [bos_id] + vocab.encode(user_in)
586
+ x = torch.tensor([ids], dtype=torch.long, device=model_device)
587
+ out = model.generate(x, max_new_tokens=training_max_new_tokens, temperature=training_temperature, top_k=training_top_k, eos_id=eos_id)
588
  answer_ids = out[0, len(ids):].tolist()
589
  answer = vocab.decode(answer_ids)
590
  print(f"OSW1: {answer if answer else '(...silence...)'}")
591
 
592
  def load_checkpoint(path: str):
593
+ # map_location="cpu" guarantees the checkpoint can always be read back,
594
+ # regardless of which device it was trained on or whether a GPU is present
595
+ # on the machine doing the loading.
596
  ckpt = torch.load(path, map_location="cpu")
597
  cfg = OSW1Config(**ckpt["config"])
598
  vocab = Vocab()
 
 
599
  model = OSW1Model(len(vocab), cfg, pad_id=ckpt["pad_id"])
600
  model.load_state_dict(ckpt["model_state_dict"])
601
+ # Now move the freshly-loaded model onto whichever device is available
602
+ # on *this* machine (GPU/MPS if present, otherwise CPU) so it runs correctly
603
+ # no matter what device it was originally trained on.
604
+ model.to(DEVICE)
605
  model.eval()
606
+ trained_on = ckpt.get("trained_on_device", "unknown")
607
+ print(f"📦 Checkpoint loaded (trained on: {trained_on}) -> running on: {DEVICE.type}")
608
  return model, vocab, cfg
609
 
610
  def main():
 
614
 
615
 
616
  if __name__ == "__main__":
617
+ main()