Kye Gomez commited on
Commit
97bc414
·
1 Parent(s): 5cfef74

use gpt-oss tokenizer because it's a great tokenizer

Browse files
open_mythos/__init__.py CHANGED
@@ -15,6 +15,7 @@ from open_mythos.main import (
15
  apply_rope,
16
  loop_index_embedding,
17
  )
 
18
  from open_mythos.variants import (
19
  mythos_1b,
20
  mythos_3b,
@@ -48,4 +49,7 @@ __all__ = [
48
  "mythos_100b",
49
  "mythos_500b",
50
  "mythos_1t",
 
 
 
51
  ]
 
15
  apply_rope,
16
  loop_index_embedding,
17
  )
18
+ from open_mythos.tokenizer import MythosTokenizer
19
  from open_mythos.variants import (
20
  mythos_1b,
21
  mythos_3b,
 
49
  "mythos_100b",
50
  "mythos_500b",
51
  "mythos_1t",
52
+ "load_tokenizer",
53
+ "get_vocab_size",
54
+ "MythosTokenizer",
55
  ]
open_mythos/main.py CHANGED
@@ -1,9 +1,3 @@
1
- """
2
- OpenMythos v1 — Recurrent-Depth Transformer
3
- Architecture: Prelude → [Looped Recurrent Block]×T → Coda
4
- MoE FFN (DeepSeek-style), GQA or MLA, RoPE, RMSNorm, KV cache, LTI-stable injection, ACT halting
5
- """
6
-
7
  from dataclasses import dataclass
8
  from typing import Optional
9
 
@@ -633,7 +627,9 @@ class TransformerBlock(nn.Module):
633
  Returns:
634
  Output tensor of shape (B, T, dim)
635
  """
636
- x = x + self.resid_drop(self.attn(self.attn_norm(x), freqs_cis, mask, kv_cache, cache_key))
 
 
637
  x = x + self.resid_drop(self.ffn(self.ffn_norm(x)))
638
  return x
639
 
 
 
 
 
 
 
 
1
  from dataclasses import dataclass
2
  from typing import Optional
3
 
 
627
  Returns:
628
  Output tensor of shape (B, T, dim)
629
  """
630
+ x = x + self.resid_drop(
631
+ self.attn(self.attn_norm(x), freqs_cis, mask, kv_cache, cache_key)
632
+ )
633
  x = x + self.resid_drop(self.ffn(self.ffn_norm(x)))
634
  return x
635
 
open_mythos/tokenizer.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import AutoTokenizer
2
+
3
+ DEFAULT_MODEL_ID = "openai/gpt-oss-20b"
4
+
5
+
6
+ class MythosTokenizer:
7
+ """
8
+ HuggingFace tokenizer wrapper for OpenMythos.
9
+
10
+ Args:
11
+ model_id (str): The HuggingFace model ID or path to use with AutoTokenizer.
12
+ Defaults to "openai/gpt-oss-20b".
13
+
14
+ Attributes:
15
+ tokenizer: An instance of HuggingFace's AutoTokenizer.
16
+
17
+ Example:
18
+ >>> tok = MythosTokenizer()
19
+ >>> ids = tok.encode("Hello world")
20
+ >>> s = tok.decode(ids)
21
+ """
22
+
23
+ def __init__(self, model_id: str = DEFAULT_MODEL_ID):
24
+ """
25
+ Initialize the MythosTokenizer.
26
+
27
+ Args:
28
+ model_id (str): HuggingFace model identifier or path to tokenizer files.
29
+ """
30
+ self.tokenizer = AutoTokenizer.from_pretrained(model_id)
31
+
32
+ @property
33
+ def vocab_size(self) -> int:
34
+ """
35
+ Return the size of the tokenizer vocabulary.
36
+
37
+ Returns:
38
+ int: The number of unique tokens in the tokenizer vocabulary.
39
+ """
40
+ return self.tokenizer.vocab_size
41
+
42
+ def encode(self, text: str) -> list[int]:
43
+ """
44
+ Encode input text into a list of token IDs.
45
+
46
+ Args:
47
+ text (str): The input text string to tokenize.
48
+
49
+ Returns:
50
+ list[int]: List of integer token IDs representing the input text.
51
+ """
52
+ return self.tokenizer.encode(text, add_special_tokens=False)
53
+
54
+ def decode(self, token_ids: list[int]) -> str:
55
+ """
56
+ Decode a list of token IDs back into a text string.
57
+
58
+ Args:
59
+ token_ids (list[int]): A list of integer token IDs to decode.
60
+
61
+ Returns:
62
+ str: Decoded string representation of the token IDs.
63
+ """
64
+ return self.tokenizer.decode(token_ids, skip_special_tokens=True)
train.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ OpenMythos pretraining on FineWeb-Edu with Muon optimizer.
4
+
5
+ Single GPU:
6
+ python train.py
7
+
8
+ Multi-GPU (auto-detects GPU count):
9
+ torchrun --nproc_per_node=$(python -c "import torch; print(torch.cuda.device_count())") train.py
10
+ """
11
+
12
+ import os
13
+ import math
14
+ import time
15
+ import torch
16
+ import torch.nn as nn
17
+ import torch.distributed as dist
18
+ from torch.nn.parallel import DistributedDataParallel as DDP
19
+ from torch.utils.data import IterableDataset, DataLoader, get_worker_info
20
+ from contextlib import nullcontext
21
+
22
+ from datasets import load_dataset
23
+
24
+ from open_mythos import OpenMythos
25
+ from open_mythos.variants import mythos_3b
26
+ from open_mythos.tokenizer import MythosTokenizer
27
+ from torch.optim import Muon
28
+
29
+
30
+ def build_optimizers(model: nn.Module, muon_lr: float, adamw_lr: float, wd: float):
31
+ """Muon for 2D weight matrices; AdamW for embeddings, norms, biases."""
32
+ muon_params, adamw_params = [], []
33
+ for name, p in model.named_parameters():
34
+ if not p.requires_grad:
35
+ continue
36
+ if (
37
+ p.ndim >= 2
38
+ and "embed" not in name
39
+ and "norm" not in name
40
+ and "scale" not in name
41
+ ):
42
+ muon_params.append(p)
43
+ else:
44
+ adamw_params.append(p)
45
+ muon = Muon(muon_params, lr=muon_lr)
46
+ adamw = torch.optim.AdamW(
47
+ adamw_params, lr=adamw_lr, weight_decay=wd, betas=(0.9, 0.95), fused=True
48
+ )
49
+ return muon, adamw
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Dataset
54
+ # ---------------------------------------------------------------------------
55
+
56
+
57
+ class FineWebEduDataset(IterableDataset):
58
+ def __init__(self, encoding, seq_len: int, subset: str, rank: int, world_size: int):
59
+ self.encoding = encoding
60
+ self.seq_len = seq_len
61
+ self.subset = subset
62
+ self.rank = rank
63
+ self.world_size = world_size
64
+
65
+ def __iter__(self):
66
+ worker = get_worker_info()
67
+ num_workers = worker.num_workers if worker else 1
68
+ worker_id = worker.id if worker else 0
69
+
70
+ # shard first by DDP rank, then by dataloader worker
71
+ total_shards = self.world_size * num_workers
72
+ shard_index = self.rank * num_workers + worker_id
73
+
74
+ ds = load_dataset(
75
+ "HuggingFaceFW/fineweb-edu",
76
+ name=self.subset,
77
+ split="train",
78
+ streaming=True,
79
+ ).shard(num_shards=total_shards, index=shard_index)
80
+
81
+ buf = []
82
+ for sample in ds:
83
+ buf.extend(self.encoding.encode(sample["text"]))
84
+ while len(buf) >= self.seq_len + 1:
85
+ chunk = buf[: self.seq_len + 1]
86
+ buf = buf[self.seq_len + 1 :]
87
+ yield (
88
+ torch.tensor(chunk[:-1], dtype=torch.long),
89
+ torch.tensor(chunk[1:], dtype=torch.long),
90
+ )
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # LR schedule: linear warmup → cosine decay
95
+ # ---------------------------------------------------------------------------
96
+
97
+
98
+ def get_lr(step: int, warmup: int, total: int, max_lr: float, min_lr: float) -> float:
99
+ if step < warmup:
100
+ return max_lr * step / warmup
101
+ if step >= total:
102
+ return min_lr
103
+ decay = (step - warmup) / (total - warmup)
104
+ return min_lr + 0.5 * (max_lr - min_lr) * (1.0 + math.cos(math.pi * decay))
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Main
109
+ # ---------------------------------------------------------------------------
110
+
111
+
112
+ def main():
113
+ # ------------------------------------------------------------------
114
+ # Distributed init — works for single GPU (python train.py)
115
+ # and multi-GPU (torchrun --nproc_per_node=N train.py)
116
+ # ------------------------------------------------------------------
117
+ ddp = int(os.environ.get("RANK", -1)) != -1
118
+ if ddp:
119
+ dist.init_process_group("nccl")
120
+ rank = int(os.environ["RANK"])
121
+ local_rank = int(os.environ["LOCAL_RANK"])
122
+ world_size = int(os.environ["WORLD_SIZE"])
123
+ device = f"cuda:{local_rank}"
124
+ torch.cuda.set_device(device)
125
+ else:
126
+ rank = local_rank = 0
127
+ world_size = 1
128
+ device = "cuda" if torch.cuda.is_available() else "cpu"
129
+
130
+ master = rank == 0
131
+
132
+ if master:
133
+ n_gpu = torch.cuda.device_count()
134
+ print(
135
+ f"GPUs detected: {n_gpu} | World size: {world_size} | Device: {device}"
136
+ )
137
+
138
+ # ------------------------------------------------------------------
139
+ # Tokenizer
140
+ # ------------------------------------------------------------------
141
+ encoding = MythosTokenizer()
142
+ vocab_size = encoding.vocab_size
143
+
144
+ if master:
145
+ print(f"Tokenizer: gpt-oss-20b | Vocab size: {vocab_size:,}")
146
+
147
+ # ------------------------------------------------------------------
148
+ # Hyperparameters
149
+ # ------------------------------------------------------------------
150
+ seq_len = 2048
151
+ micro_batch = 4 # sequences per GPU per grad-accum step
152
+ target_tokens = 30_000_000_000 # 30B token run
153
+ grad_accum = max(1, 256 // (world_size * micro_batch))
154
+ global_batch_tok = world_size * micro_batch * grad_accum * seq_len
155
+ total_steps = target_tokens // global_batch_tok
156
+ warmup_steps = 2000
157
+ muon_lr = 0.02
158
+ adamw_lr = 3e-4
159
+ wd = 0.1
160
+ log_every = 10
161
+ ckpt_every = 1000
162
+ ckpt_dir = "checkpoints"
163
+ dataset_subset = "sample-10BT" # → sample-100BT or "default" for full run
164
+
165
+ if master:
166
+ print(
167
+ f"seq_len={seq_len} | micro_batch={micro_batch} | grad_accum={grad_accum}\n"
168
+ f"global_batch_tokens={global_batch_tok:,} | total_steps={total_steps:,}"
169
+ )
170
+
171
+ # ------------------------------------------------------------------
172
+ # Model — override vocab_size to match tokenizer
173
+ # ------------------------------------------------------------------
174
+ cfg = mythos_3b()
175
+ cfg.vocab_size = vocab_size
176
+ cfg.max_seq_len = seq_len
177
+
178
+ model = OpenMythos(cfg).to(device)
179
+
180
+ # Mixed precision
181
+ bf16_supported = torch.cuda.is_available() and torch.cuda.is_bf16_supported()
182
+ amp_dtype = torch.bfloat16 if bf16_supported else torch.float16
183
+ amp_ctx = (
184
+ torch.amp.autocast(device_type="cuda", dtype=amp_dtype)
185
+ if "cuda" in device
186
+ else nullcontext()
187
+ )
188
+ scaler = torch.cuda.amp.GradScaler(enabled=(amp_dtype == torch.float16))
189
+
190
+ if master:
191
+ n_params = sum(p.numel() for p in model.parameters())
192
+ print(f"Parameters: {n_params:,} | AMP dtype: {amp_dtype}")
193
+
194
+ if ddp:
195
+ model = DDP(model, device_ids=[local_rank])
196
+
197
+ raw_model = model.module if ddp else model
198
+
199
+ # ------------------------------------------------------------------
200
+ # Optimizers
201
+ # ------------------------------------------------------------------
202
+ muon, adamw = build_optimizers(raw_model, muon_lr, adamw_lr, wd)
203
+
204
+ # ------------------------------------------------------------------
205
+ # Dataset + DataLoader
206
+ # ------------------------------------------------------------------
207
+ dataset = FineWebEduDataset(encoding, seq_len, dataset_subset, rank, world_size)
208
+ loader = DataLoader(dataset, batch_size=micro_batch, num_workers=4, pin_memory=True)
209
+
210
+ # ------------------------------------------------------------------
211
+ # Training loop
212
+ # ------------------------------------------------------------------
213
+ if master:
214
+ os.makedirs(ckpt_dir, exist_ok=True)
215
+
216
+ model.train()
217
+ data_iter = iter(loader)
218
+ t0 = time.perf_counter()
219
+ step = 0
220
+
221
+ while step < total_steps:
222
+ cur_muon_lr = get_lr(step, warmup_steps, total_steps, muon_lr, muon_lr * 0.1)
223
+ cur_adamw_lr = get_lr(step, warmup_steps, total_steps, adamw_lr, adamw_lr * 0.1)
224
+ for g in muon.param_groups:
225
+ g["lr"] = cur_muon_lr
226
+ for g in adamw.param_groups:
227
+ g["lr"] = cur_adamw_lr
228
+
229
+ muon.zero_grad()
230
+ adamw.zero_grad()
231
+ loss_accum = 0.0
232
+
233
+ for micro_step in range(grad_accum):
234
+ try:
235
+ x, y = next(data_iter)
236
+ except StopIteration:
237
+ data_iter = iter(loader)
238
+ x, y = next(data_iter)
239
+
240
+ x, y = x.to(device, non_blocking=True), y.to(device, non_blocking=True)
241
+
242
+ # Defer DDP gradient sync until the last micro-step
243
+ sync = (
244
+ nullcontext()
245
+ if (not ddp or micro_step == grad_accum - 1)
246
+ else model.no_sync()
247
+ )
248
+ with sync, amp_ctx:
249
+ logits = model(x)
250
+ loss = nn.functional.cross_entropy(
251
+ logits.view(-1, vocab_size), y.view(-1)
252
+ )
253
+ loss = loss / grad_accum
254
+
255
+ scaler.scale(loss).backward()
256
+ loss_accum += loss.item()
257
+
258
+ # Unscale, clip, step both optimizers
259
+ scaler.unscale_(muon)
260
+ scaler.unscale_(adamw)
261
+ nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
262
+ scaler.step(muon)
263
+ scaler.step(adamw)
264
+ scaler.update()
265
+
266
+ step += 1
267
+
268
+ if master and step % log_every == 0:
269
+ dt = time.perf_counter() - t0
270
+ tok_per_sec = global_batch_tok * log_every / dt
271
+ tokens_seen = step * global_batch_tok
272
+ print(
273
+ f"step {step:6d}/{total_steps} | loss {loss_accum:.4f} "
274
+ f"| muon_lr {cur_muon_lr:.2e} | adamw_lr {cur_adamw_lr:.2e} "
275
+ f"| {tok_per_sec / 1e6:.2f}M tok/s | {tokens_seen / 1e9:.1f}B tokens seen"
276
+ )
277
+ t0 = time.perf_counter()
278
+
279
+ if master and step % ckpt_every == 0:
280
+ path = os.path.join(ckpt_dir, f"step_{step:07d}.pt")
281
+ torch.save(
282
+ {
283
+ "step": step,
284
+ "model": raw_model.state_dict(),
285
+ "muon": muon.state_dict(),
286
+ "adamw": adamw.state_dict(),
287
+ "cfg": cfg,
288
+ "vocab_size": vocab_size,
289
+ },
290
+ path,
291
+ )
292
+ print(f"Checkpoint saved → {path}")
293
+
294
+ if ddp:
295
+ dist.destroy_process_group()
296
+
297
+ if master:
298
+ print("Training complete.")
299
+
300
+
301
+ if __name__ == "__main__":
302
+ main()