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- 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=
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 603 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|