Kye Gomez commited on
Commit
eae0f04
·
1 Parent(s): 289981b

fix training

Browse files
Files changed (1) hide show
  1. training/3b_fine_web_edu.py +278 -25
training/3b_fine_web_edu.py CHANGED
@@ -41,7 +41,30 @@ from open_mythos.tokenizer import MythosTokenizer
41
 
42
 
43
  class FineWebEduDataset(IterableDataset):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  def __init__(self, encoding, seq_len: int, subset: str, rank: int, world_size: int):
 
 
 
 
 
 
 
 
45
  self.encoding = encoding
46
  self.seq_len = seq_len
47
  self.subset = subset
@@ -49,6 +72,16 @@ class FineWebEduDataset(IterableDataset):
49
  self.world_size = world_size
50
 
51
  def __iter__(self):
 
 
 
 
 
 
 
 
 
 
52
  worker = get_worker_info()
53
  num_workers = worker.num_workers if worker else 1
54
  worker_id = worker.id if worker else 0
@@ -81,6 +114,32 @@ class FineWebEduDataset(IterableDataset):
81
 
82
 
83
  def get_lr(step: int, warmup: int, total: int, max_lr: float, min_lr: float) -> float:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  if step < warmup:
85
  return max_lr * step / warmup
86
  if step >= total:
@@ -89,12 +148,194 @@ def get_lr(step: int, warmup: int, total: int, max_lr: float, min_lr: float) ->
89
  return min_lr + 0.5 * (max_lr - min_lr) * (1.0 + math.cos(math.pi * decay))
90
 
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  # ---------------------------------------------------------------------------
93
  # Main
94
  # ---------------------------------------------------------------------------
95
 
96
 
97
  def main():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  # ------------------------------------------------------------------
99
  # Distributed init
100
  # ------------------------------------------------------------------
@@ -198,6 +439,22 @@ def main():
198
  model.parameters(), lr=lr, weight_decay=wd, betas=(0.9, 0.95), fused=True
199
  )
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  # ------------------------------------------------------------------
202
  # Dataset + DataLoader
203
  # ------------------------------------------------------------------
@@ -213,7 +470,7 @@ def main():
213
  model.train()
214
  data_iter = iter(loader)
215
  t0 = time.perf_counter()
216
- step = 0
217
 
218
  while step < total_steps:
219
  cur_lr = get_lr(step, warmup_steps, total_steps, lr, lr * 0.1)
@@ -248,7 +505,13 @@ def main():
248
  loss.backward()
249
  loss_accum += loss.item()
250
 
251
- nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
 
 
 
 
 
 
252
  optimizer.step()
253
  step += 1
254
 
@@ -258,36 +521,26 @@ def main():
258
  tokens_seen = step * global_batch_tok
259
  logger.info(
260
  f"step {step:6d}/{total_steps} | loss {loss_accum:.4f} "
261
- f"| lr {cur_lr:.2e} | {tok_per_sec / 1e6:.2f}M tok/s "
 
262
  f"| {tokens_seen / 1e9:.1f}B tokens seen"
263
  )
264
  t0 = time.perf_counter()
265
 
266
- if master and step % ckpt_every == 0:
267
- path = os.path.join(ckpt_dir, f"step_{step:07d}.pt")
268
- logger.info(f"Saving checkpoint at step {step} → {path}")
269
- if ddp:
270
- with FSDP.state_dict_type(
271
- model,
272
- StateDictType.FULL_STATE_DICT,
273
- FullStateDictConfig(offload_to_cpu=True, rank0_only=True),
274
- ):
275
- model_state = model.state_dict()
276
- else:
277
- model_state = model.state_dict()
278
- torch.save(
279
- {
280
- "step": step,
281
- "model": model_state,
282
- "optimizer": optimizer.state_dict(),
283
- "cfg": cfg,
284
- "vocab_size": vocab_size,
285
- },
286
- path,
287
  )
288
- logger.success(f"Checkpoint saved → {path}")
 
 
 
 
289
 
290
  if ddp:
 
 
 
291
  dist.destroy_process_group()
292
 
293
  if master:
 
41
 
42
 
43
  class FineWebEduDataset(IterableDataset):
44
+ """
45
+ Streaming FineWeb-Edu loader yielding fixed-length (input, target) pairs.
46
+
47
+ FineWeb-Edu is trillions of tokens, so `streaming=True` pulls shards on
48
+ demand instead of materializing to disk. Sharding is two-dimensional —
49
+ `world_size` ranks × `num_workers` DataLoader workers per rank — and each
50
+ `(rank, worker_id)` deterministically owns one shard of the global stream.
51
+ That gives disjoint coverage without any cross-process coordination.
52
+
53
+ Streaming datasets are not seekable, so a resumed run re-enters its shard
54
+ from the beginning. Acceptable at pretraining scale: the chance of
55
+ re-playing the same tokens before the run ends is negligible versus the
56
+ cost of a true resumable loader.
57
+ """
58
+
59
  def __init__(self, encoding, seq_len: int, subset: str, rank: int, world_size: int):
60
+ """
61
+ Args:
62
+ encoding -- tokenizer exposing `.encode(str) -> list[int]`
63
+ seq_len -- context length; every yielded pair has this many tokens
64
+ subset -- FineWeb-Edu config name (e.g. "sample-10BT", "default")
65
+ rank -- global rank of this process within the distributed job
66
+ world_size -- total number of distributed processes
67
+ """
68
  self.encoding = encoding
69
  self.seq_len = seq_len
70
  self.subset = subset
 
72
  self.world_size = world_size
73
 
74
  def __iter__(self):
75
+ """
76
+ Yield `(input_ids, target_ids)` tensors of length `seq_len` forever.
77
+
78
+ Inputs and targets are shifted by one for next-token prediction —
79
+ `target[i] == input[i + 1]`. Documents are concatenated into a rolling
80
+ buffer and sliced into fixed-length chunks, packing short docs together
81
+ and splitting long ones. This keeps every step at the same shape,
82
+ which under FSDP avoids recompute from variable-length inputs and
83
+ removes the need for a pad-aware attention mask.
84
+ """
85
  worker = get_worker_info()
86
  num_workers = worker.num_workers if worker else 1
87
  worker_id = worker.id if worker else 0
 
114
 
115
 
116
  def get_lr(step: int, warmup: int, total: int, max_lr: float, min_lr: float) -> float:
117
+ """
118
+ Linear warmup → half-cosine decay to `min_lr`.
119
+
120
+ Standard language-model pretraining schedule. The warmup phase prevents
121
+ Adam's second-moment estimate from collapsing to a huge LR in the first
122
+ few steps when gradients are noisy. The cosine tail lets the model make
123
+ small, increasingly conservative updates near the end of training rather
124
+ than crashing to `min_lr` at a fixed step.
125
+
126
+ Behavior by region:
127
+ step < warmup → linear ramp 0 → max_lr
128
+ warmup ≤ step < total → cosine decay max_lr → min_lr
129
+ step ≥ total → clamped at min_lr (safety for
130
+ off-by-one step counters at the end
131
+ of training)
132
+
133
+ Args:
134
+ step -- current global optimizer step (0-indexed)
135
+ warmup -- number of warmup steps before cosine decay begins
136
+ total -- step at which the cosine reaches `min_lr`
137
+ max_lr -- peak learning rate reached at the end of warmup
138
+ min_lr -- floor learning rate at and after `total` steps
139
+
140
+ Returns:
141
+ Scalar learning rate for this step.
142
+ """
143
  if step < warmup:
144
  return max_lr * step / warmup
145
  if step >= total:
 
148
  return min_lr + 0.5 * (max_lr - min_lr) * (1.0 + math.cos(math.pi * decay))
149
 
150
 
151
+ # ---------------------------------------------------------------------------
152
+ # Checkpointing
153
+ # ---------------------------------------------------------------------------
154
+
155
+
156
+ def _list_ckpts(ckpt_dir: str) -> list[str]:
157
+ """
158
+ Return checkpoint paths in `ckpt_dir` sorted oldest → newest.
159
+
160
+ Relies on the zero-padded `step_{0000000}.pt` filename convention so
161
+ lexicographic sort matches chronological order. Changing the filename
162
+ format elsewhere without updating the pad width would silently break
163
+ both `keep_last` pruning and resume-latest on startup, since both pick
164
+ the last element of this list.
165
+
166
+ Args:
167
+ ckpt_dir -- directory to scan; missing directory returns []
168
+
169
+ Returns:
170
+ Sorted list of absolute paths to matching checkpoint files.
171
+ """
172
+ if not os.path.isdir(ckpt_dir):
173
+ return []
174
+ return sorted(
175
+ os.path.join(ckpt_dir, f)
176
+ for f in os.listdir(ckpt_dir)
177
+ if f.startswith("step_") and f.endswith(".pt")
178
+ )
179
+
180
+
181
+ def save_checkpoint(
182
+ model,
183
+ optimizer,
184
+ step: int,
185
+ cfg,
186
+ vocab_size: int,
187
+ ckpt_dir: str,
188
+ ddp: bool,
189
+ master: bool,
190
+ keep_last: int = 3,
191
+ ) -> None:
192
+ """
193
+ Gather full model + optimizer state, write atomically, prune old files.
194
+
195
+ Under FSDP both states are collected inside a single FULL_STATE_DICT
196
+ context so the optim-state tensors bind to fully-unsharded parameters;
197
+ mixing contexts between model and optimizer has caused silent divergence
198
+ on resume in past torch versions. The temp-file + os.replace write means
199
+ a kill mid-save leaves the previous checkpoint intact instead of a
200
+ truncated .pt file. Non-master ranks participate in the FSDP gather
201
+ (otherwise the collective would hang) but exit before touching disk.
202
+
203
+ Args:
204
+ model -- FSDP-wrapped (ddp=True) or raw (ddp=False) model
205
+ optimizer -- the optimizer whose state should round-trip with the model
206
+ step -- global step number; encoded zero-padded into the filename
207
+ cfg -- model config object; saved so downstream eval can
208
+ reconstruct the model without re-importing the variant
209
+ vocab_size -- tokenizer vocab size at train time; saved for sanity-check
210
+ on load against a (possibly updated) tokenizer
211
+ ckpt_dir -- directory to write into; created if missing
212
+ ddp -- True if FSDP path; False for single-GPU / CPU
213
+ master -- whether this rank writes to disk (rank 0 only)
214
+ keep_last -- number of most-recent checkpoints to retain; older ones
215
+ are unlinked after a successful write
216
+
217
+ Returns:
218
+ None. Writes to disk as a side effect on master rank.
219
+ """
220
+ if ddp:
221
+ with FSDP.state_dict_type(
222
+ model,
223
+ StateDictType.FULL_STATE_DICT,
224
+ FullStateDictConfig(offload_to_cpu=True, rank0_only=True),
225
+ ):
226
+ model_state = model.state_dict()
227
+ optim_state = FSDP.optim_state_dict(model, optimizer)
228
+ else:
229
+ model_state = model.state_dict()
230
+ optim_state = optimizer.state_dict()
231
+
232
+ if not master:
233
+ return
234
+
235
+ os.makedirs(ckpt_dir, exist_ok=True)
236
+ final_path = os.path.join(ckpt_dir, f"step_{step:07d}.pt")
237
+ tmp_path = final_path + ".tmp"
238
+ torch.save(
239
+ {
240
+ "step": step,
241
+ "model": model_state,
242
+ "optimizer": optim_state,
243
+ "cfg": cfg,
244
+ "vocab_size": vocab_size,
245
+ },
246
+ tmp_path,
247
+ )
248
+ os.replace(tmp_path, final_path)
249
+
250
+ for old in _list_ckpts(ckpt_dir)[:-keep_last]:
251
+ try:
252
+ os.remove(old)
253
+ except OSError as exc:
254
+ logger.warning(f"Failed to prune old checkpoint {old}: {exc}")
255
+
256
+ logger.success(f"Checkpoint saved → {final_path}")
257
+
258
+
259
+ def load_checkpoint(model, optimizer, path: str, ddp: bool) -> int:
260
+ """
261
+ Restore model + optimizer from disk, returning the step to resume at.
262
+
263
+ Every rank reads the file (`rank0_only=False` on load) so FSDP has access
264
+ to the full state on each rank — the complement to the `rank0_only=True`
265
+ save path. Must mirror save's single-context pattern; splitting the model
266
+ and optimizer loads across two `state_dict_type` blocks has historically
267
+ produced optimizer state bound to the wrong shard shapes.
268
+
269
+ `weights_only=False` is required because the checkpoint contains the
270
+ pickled `cfg` dataclass — flip to `weights_only=True` only if you
271
+ separate config out.
272
+
273
+ Args:
274
+ model -- same FSDP-wrapped or raw model used during save
275
+ optimizer -- freshly constructed optimizer to be filled in-place
276
+ path -- absolute path to a `step_{N:07d}.pt` file produced by
277
+ `save_checkpoint`
278
+ ddp -- whether the model is FSDP-wrapped; must match the save run
279
+
280
+ Returns:
281
+ The step number the checkpoint was taken at; the caller advances the
282
+ training loop from this value.
283
+ """
284
+ ckpt = torch.load(path, map_location="cpu", weights_only=False)
285
+
286
+ if ddp:
287
+ with FSDP.state_dict_type(
288
+ model,
289
+ StateDictType.FULL_STATE_DICT,
290
+ FullStateDictConfig(offload_to_cpu=True, rank0_only=False),
291
+ ):
292
+ model.load_state_dict(ckpt["model"])
293
+ optim_state = FSDP.optim_state_dict_to_load(
294
+ model=model,
295
+ optim=optimizer,
296
+ optim_state_dict=ckpt["optimizer"],
297
+ )
298
+ optimizer.load_state_dict(optim_state)
299
+ else:
300
+ model.load_state_dict(ckpt["model"])
301
+ optimizer.load_state_dict(ckpt["optimizer"])
302
+
303
+ return int(ckpt["step"])
304
+
305
+
306
  # ---------------------------------------------------------------------------
307
  # Main
308
  # ---------------------------------------------------------------------------
309
 
310
 
311
  def main():
312
+ """
313
+ End-to-end pretraining entry point.
314
+
315
+ Order matters: distributed init must run before any CUDA allocation, the
316
+ tokenizer must exist before the model is built (vocab_size flows into
317
+ cfg), and FSDP must wrap the model before the optimizer is constructed
318
+ (FSDP re-flattens parameters, so an optimizer built on the unwrapped
319
+ model would track stale param objects). Resume then loads state into the
320
+ already-constructed optimizer in-place.
321
+
322
+ Lifecycle:
323
+ 1. Initialize torch.distributed (NCCL) if launched under torchrun.
324
+ 2. Build tokenizer → derive vocab_size.
325
+ 3. Construct OpenMythos with the 3B variant config.
326
+ 4. Wrap in FSDP with FULL_SHARD + bf16/fp16 mixed precision (multi-GPU)
327
+ or move to device + autocast (single-GPU).
328
+ 5. Build fused AdamW on (possibly sharded) parameters.
329
+ 6. Resume from the latest checkpoint in `ckpt_dir` if one exists.
330
+ 7. Stream FineWeb-Edu through grad-accumulation microbatches with
331
+ cosine LR schedule, per-step logging, and periodic checkpoints.
332
+ 8. Write a final checkpoint if the last save wasn't aligned to
333
+ `ckpt_every`, then barrier + tear down the process group.
334
+
335
+ All hyperparameters are literal constants in this function by design —
336
+ pretraining runs are long-lived and each run pins exact settings; a
337
+ CLI/config layer is deliberately avoided to keep the file self-auditable.
338
+ """
339
  # ------------------------------------------------------------------
340
  # Distributed init
341
  # ------------------------------------------------------------------
 
439
  model.parameters(), lr=lr, weight_decay=wd, betas=(0.9, 0.95), fused=True
440
  )
441
 
442
+ # ------------------------------------------------------------------
443
+ # Resume from latest checkpoint (if any)
444
+ # ------------------------------------------------------------------
445
+ # Streaming datasets are not resumable by position, so re-iterating from
446
+ # the beginning is accepted — at pretraining scale the loss of dataset
447
+ # position is negligible vs. the cost of discarded training steps.
448
+ start_step = 0
449
+ existing_ckpts = _list_ckpts(ckpt_dir)
450
+ if existing_ckpts:
451
+ latest = existing_ckpts[-1]
452
+ if master:
453
+ logger.info(f"Resuming from checkpoint: {latest}")
454
+ start_step = load_checkpoint(model, optimizer, latest, ddp)
455
+ if master:
456
+ logger.success(f"Resumed at step {start_step}")
457
+
458
  # ------------------------------------------------------------------
459
  # Dataset + DataLoader
460
  # ------------------------------------------------------------------
 
470
  model.train()
471
  data_iter = iter(loader)
472
  t0 = time.perf_counter()
473
+ step = start_step
474
 
475
  while step < total_steps:
476
  cur_lr = get_lr(step, warmup_steps, total_steps, lr, lr * 0.1)
 
505
  loss.backward()
506
  loss_accum += loss.item()
507
 
508
+ # FSDP shards parameters, so `nn.utils.clip_grad_norm_` would clip
509
+ # against each rank's local norm and miss the cross-shard gather.
510
+ # FSDP.clip_grad_norm_ computes the true global norm and returns it.
511
+ if ddp:
512
+ grad_norm = model.clip_grad_norm_(1.0)
513
+ else:
514
+ grad_norm = nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
515
  optimizer.step()
516
  step += 1
517
 
 
521
  tokens_seen = step * global_batch_tok
522
  logger.info(
523
  f"step {step:6d}/{total_steps} | loss {loss_accum:.4f} "
524
+ f"| gnorm {float(grad_norm):.2f} | lr {cur_lr:.2e} "
525
+ f"| {tok_per_sec / 1e6:.2f}M tok/s "
526
  f"| {tokens_seen / 1e9:.1f}B tokens seen"
527
  )
528
  t0 = time.perf_counter()
529
 
530
+ if step % ckpt_every == 0:
531
+ save_checkpoint(
532
+ model, optimizer, step, cfg, vocab_size, ckpt_dir, ddp, master
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  )
534
+
535
+ # Final checkpoint — total_steps may not be divisible by ckpt_every, so
536
+ # without this the tail of the run is lost if the schedule doesn't align.
537
+ if step > start_step and step % ckpt_every != 0:
538
+ save_checkpoint(model, optimizer, step, cfg, vocab_size, ckpt_dir, ddp, master)
539
 
540
  if ddp:
541
+ # Barrier so no rank exits while another is still finishing its
542
+ # checkpoint gather — avoids NCCL "process group destroyed" noise.
543
+ dist.barrier()
544
  dist.destroy_process_group()
545
 
546
  if master: