Cion-lab commited on
Commit
492bf86
·
verified ·
1 Parent(s): e11175e

review round: pointer written+read-back before prune, verify retried, latest rolled to final, stale-pointer scan on resume, ddp_timeout 4h, chunked batched commits, manifest self-consistency gate, full content verification after upload

Browse files
Files changed (1) hide show
  1. train/train_ounce100m.py +69 -8
train/train_ounce100m.py CHANGED
@@ -133,6 +133,7 @@ class HubPush(TrainerCallback):
133
  self.stop_after = int(stop_after_steps or 0)
134
  self.stopped_at = None
135
  self.n = 0
 
136
  self.results = []
137
 
138
  def on_step_end(self, args, state, control, **kw):
@@ -156,18 +157,27 @@ class HubPush(TrainerCallback):
156
  SD.save_cursor(d, SD.cursor_dict(self.seq_len, samples, self.seed, self.fingerprint,
157
  step=step))
158
  t0 = time.time()
 
 
 
 
 
 
159
  res = hubckpt.push_and_prune(self.repo, d, f"ckpt/checkpoint-{step}", self.api,
160
- repo_type=self.repo_type, token=self.token, prune=self.prune,
161
- min_free_gb=self.min_free_gb)
162
- hubckpt.write_latest_pointer(self.repo, {
163
- "path_in_repo": f"ckpt/checkpoint-{step}", "step": step, "samples_consumed": samples,
164
- "tokens_consumed": samples * self.seq_len, "fingerprint": self.fingerprint,
165
- "seed": self.seed}, self.api, repo_type=self.repo_type, token=self.token)
 
 
166
  loss = next((h["loss"] for h in reversed(state.log_history) if "loss" in h), None)
167
  print(f"CKPT {step} samples={samples:,} push+verify={round(time.time()-t0,1)}s "
168
  f"ok={res['verify']['ok']} pruned={res.get('pruned')} free={res['free_after_gb']}GB "
169
  f"loss={loss}", flush=True)
170
  self.n += 1
 
171
  self.results.append(res)
172
  return control
173
 
@@ -351,6 +361,29 @@ def main():
351
  # exactly the §3.1 violation this whole branch exists to prevent (E-018's shape).
352
  raise SystemExit(f"--resume auto could not read the hub pointer: {ptr['error']}")
353
  else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  # Both ranks need the checkpoint on disk, and this block runs before TrainingArguments has
355
  # created a process group -- so there is no barrier to synchronise on. Downloading into a
356
  # per-rank directory instead removes the race entirely (two writers, one path), at the cost of
@@ -483,6 +516,11 @@ def main():
483
  optim=args.optim,
484
  torch_compile=args.torch_compile,
485
  ddp_find_unused_parameters=False,
 
 
 
 
 
486
  dataloader_num_workers=2,
487
  dataloader_pin_memory=False,
488
  remove_unused_columns=False, # our dict keys must reach the model untouched
@@ -543,6 +581,20 @@ def main():
543
 
544
  elapsed = time.time() - t0
545
  step = tr.state.global_step
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
  samples = samples_at(step, seqs_per_step)
547
  consumed = samples * args.seq_len
548
  consumed_this_run = max(0, step - start_step) * tokens_per_step
@@ -599,8 +651,17 @@ def main():
599
  # .get, not [k]: `pruned` and `free_after_gb` are only present on some paths through
600
  # push_and_prune, and a KeyError here would throw away a completed run (E-023).
601
  say("final push:", json.dumps({k: r.get(k) for k in
602
- ("verify", "pruned", "free_before_gb", "free_after_gb",
603
- "prune_blocked_reason")}, default=str)[:900])
 
 
 
 
 
 
 
 
 
604
  elif segment:
605
  # The session stopped on a checkpoint interval, and HubPush already pushed AND verified that
606
  # checkpoint and rolled latest.json to it. Re-pushing it as "final" would add a 1.7 GB LFS
 
133
  self.stop_after = int(stop_after_steps or 0)
134
  self.stopped_at = None
135
  self.n = 0
136
+ self.steps = []
137
  self.results = []
138
 
139
  def on_step_end(self, args, state, control, **kw):
 
157
  SD.save_cursor(d, SD.cursor_dict(self.seq_len, samples, self.seed, self.fingerprint,
158
  step=step))
159
  t0 = time.time()
160
+ # Upload and verify *without* pruning, roll the pointer and read it back, and only then delete the
161
+ # local copy. The old order (prune inside push_and_prune, pointer written afterwards) meant one
162
+ # failed pointer commit left a Hub-verified checkpoint that nothing pointed at and no local copy to
163
+ # recover it with -- and every guard a resume runs would still pass, because weights, cursor,
164
+ # fingerprint and seed are all self-consistent. Only the pointer would be wrong, and the next
165
+ # session would quietly re-read 100 M tokens.
166
  res = hubckpt.push_and_prune(self.repo, d, f"ckpt/checkpoint-{step}", self.api,
167
+ repo_type=self.repo_type, token=self.token, prune=False)
168
+ ptr_body = {"path_in_repo": f"ckpt/checkpoint-{step}", "step": step,
169
+ "samples_consumed": samples, "tokens_consumed": samples * self.seq_len,
170
+ "fingerprint": self.fingerprint, "seed": self.seed}
171
+ hubckpt.write_latest_pointer(self.repo, ptr_body, self.api, repo_type=self.repo_type,
172
+ token=self.token)
173
+ if self.prune:
174
+ hubckpt.prune_local(d, res, self.min_free_gb)
175
  loss = next((h["loss"] for h in reversed(state.log_history) if "loss" in h), None)
176
  print(f"CKPT {step} samples={samples:,} push+verify={round(time.time()-t0,1)}s "
177
  f"ok={res['verify']['ok']} pruned={res.get('pruned')} free={res['free_after_gb']}GB "
178
  f"loss={loss}", flush=True)
179
  self.n += 1
180
+ self.steps.append(step)
181
  self.results.append(res)
182
  return control
183
 
 
361
  # exactly the §3.1 violation this whole branch exists to prevent (E-018's shape).
362
  raise SystemExit(f"--resume auto could not read the hub pointer: {ptr['error']}")
363
  else:
364
+ # A pointer is a resume point only if nothing newer was ever pushed. This scan catches the one
365
+ # case write_latest_pointer's own read-back cannot see: a pointer left behind by a rollback, a
366
+ # deletion, or a session that died between its checkpoint push and its pointer roll. Resuming
367
+ # from the stale pointer would re-train an interval with weights and a cursor that agree with
368
+ # each other and disagree with the Hub.
369
+ import re as _re
370
+ steps_on_hub = set()
371
+ for attempt in (1, 2):
372
+ try:
373
+ steps_on_hub = {int(m.group(1)) for k in
374
+ hubckpt.hub_listing(args.hub_repo, args.hub_repo_type, token=tk)
375
+ if (m := _re.match(r"ckpt/checkpoint-(\d+)/", k))}
376
+ break
377
+ except Exception as e:
378
+ say(f" checkpoint scan attempt {attempt}: {type(e).__name__}: {str(e)[:140]}")
379
+ time.sleep(15)
380
+ else:
381
+ raise SystemExit("--resume auto could not enumerate the checkpoint repo, so it cannot "
382
+ f"prove latest.json (step {ptr.get('step')}) is the newest state")
383
+ if steps_on_hub and max(steps_on_hub) > int(ptr.get("step") or 0):
384
+ raise SystemExit(f"latest.json points at step {ptr.get('step')} but {args.hub_repo} also "
385
+ f"holds checkpoint-{max(steps_on_hub)}: the pointer is behind the run. "
386
+ "Resuming here would re-train that interval, so refusing.")
387
  # Both ranks need the checkpoint on disk, and this block runs before TrainingArguments has
388
  # created a process group -- so there is no barrier to synchronise on. Downloading into a
389
  # per-rank directory instead removes the race entirely (two writers, one path), at the cost of
 
516
  optim=args.optim,
517
  torch_compile=args.torch_compile,
518
  ddp_find_unused_parameters=False,
519
+ # Rank 0 spends minutes inside on_save uploading ~1.7 GB, listing the repo and downloading one file
520
+ # back to hash it, while rank 1 waits at the next collective. The library's 1,800 s default would
521
+ # abort the run on a slow-egress Kaggle box -- precisely during the checkpoint that the other rank
522
+ # was protecting. Four hours makes the collective patient without hiding a real deadlock.
523
+ ddp_timeout=14400,
524
  dataloader_num_workers=2,
525
  dataloader_pin_memory=False,
526
  remove_unused_columns=False, # our dict keys must reach the model untouched
 
581
 
582
  elapsed = time.time() - t0
583
  step = tr.state.global_step
584
+ # Did this segment really end on a pushed checkpoint? `stopped_at` is set when the step counter reaches
585
+ # the target, but performing the save at that step is the library's decision. If it ever ordered the
586
+ # stop ahead of the save, the session would exit 0 having pushed nothing, and every relaunch would
587
+ # redo the same interval while `latest` stayed put -- which looks like a stuck run, not a broken one.
588
+ if hub_cb is not None and args.stop_after_steps:
589
+ if args.stop_after_steps <= start_step:
590
+ raise SystemExit(f"--stop-after-steps {args.stop_after_steps} is not beyond the resume point "
591
+ f"step {start_step}: this session would train nothing")
592
+ if step < args.stop_after_steps:
593
+ raise SystemExit(f"asked to stop at step {args.stop_after_steps}, stopped at {step}")
594
+ if args.stop_after_steps < steps_planned and args.stop_after_steps not in hub_cb.steps:
595
+ raise SystemExit(f"segment ended at step {step} but no checkpoint was pushed and verified "
596
+ f"for it (pushed: {sorted(hub_cb.steps)}). The next session would resume "
597
+ "from the previous pointer and re-read this interval.")
598
  samples = samples_at(step, seqs_per_step)
599
  consumed = samples * args.seq_len
600
  consumed_this_run = max(0, step - start_step) * tokens_per_step
 
651
  # .get, not [k]: `pruned` and `free_after_gb` are only present on some paths through
652
  # push_and_prune, and a KeyError here would throw away a completed run (E-023).
653
  say("final push:", json.dumps({k: r.get(k) for k in
654
+ ("verify", "pruned", "free_before_gb", "free_after_gb",
655
+ "prune_blocked_reason")}, default=str)[:900])
656
+ # The run is over and `latest.json` has to say so. `on_save` only ever rolls it at a 381-step
657
+ # boundary, so without this the pointer stops at 3,810, the launcher's `start_step >= horizon`
658
+ # check never fires, and every relaunch after completion trains four more steps and pushes again.
659
+ if step >= steps_planned:
660
+ hubckpt.write_latest_pointer(args.hub_repo, {
661
+ "path_in_repo": "final", "step": step, "samples_consumed": samples,
662
+ "tokens_consumed": consumed, "fingerprint": fingerprint, "seed": args.data_seed},
663
+ api, repo_type=args.hub_repo_type, token=os.environ.get("HF_TOKEN"))
664
+ say(f"latest.json rolled to the completed run: final at step {step}")
665
  elif segment:
666
  # The session stopped on a checkpoint interval, and HubPush already pushed AND verified that
667
  # checkpoint and rolled latest.json to it. Re-pushing it as "final" would add a 1.7 GB LFS