Upload train/preflight.py with huggingface_hub
Browse files- train/preflight.py +112 -3
train/preflight.py
CHANGED
|
@@ -29,8 +29,16 @@ REV_DEFAULT = "" # filled from the launcher; only used for reporting
|
|
| 29 |
def sh(argv, timeout=None, env=None, label=""):
|
| 30 |
print(f"=== {label or ' '.join(argv[:3])}", flush=True)
|
| 31 |
t0 = time.time()
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
for line in (p.stdout or "").splitlines():
|
| 35 |
print(" |", line[:240], flush=True)
|
| 36 |
if p.returncode != 0:
|
|
@@ -93,6 +101,10 @@ kw = dict(output_dir="/kaggle/working/argcheck", per_device_train_batch_size=2,
|
|
| 93 |
adam_beta2=0.95, adam_epsilon=1e-8, max_grad_norm=1.0, lr_scheduler_type="constant",
|
| 94 |
warmup_ratio=0.0, num_train_epochs=1, max_steps=10, fp16=True, bf16=False,
|
| 95 |
gradient_checkpointing=False, ddp_find_unused_parameters=False, dataloader_num_workers=2,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
dataloader_pin_memory=False, remove_unused_columns=False, ignore_data_skip=True,
|
| 97 |
save_strategy="steps", save_steps=5, save_total_limit=1, logging_steps=5, report_to=[],
|
| 98 |
seed=1, data_seed=1, accelerator_config={"dispatch_batches": False},
|
|
@@ -372,6 +384,99 @@ def p2_throughput(args, R):
|
|
| 372 |
print("VERDICT P2_pass=", R["P2_pass"], "eta_hours:", json.dumps(R["P2_eta_hours_at_1B"]), flush=True)
|
| 373 |
|
| 374 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
def p3_cold_resume(args, R):
|
| 376 |
"""Three legs, each ending in one checkpoint, each started from the Hub with nothing local but the mix.
|
| 377 |
|
|
@@ -458,7 +563,9 @@ def p3_cold_resume(args, R):
|
|
| 458 |
def main():
|
| 459 |
ap = argparse.ArgumentParser()
|
| 460 |
ap.add_argument("--stage", required=True,
|
| 461 |
-
choices=["pargs", "p0", "p1", "p2", "p3", "summary", "all"]
|
|
|
|
|
|
|
| 462 |
ap.add_argument("--seq-len", type=int, default=1024) # D-011: the config the main run will use
|
| 463 |
ap.add_argument("--attn", default="eager", choices=["eager", "sdpa"],
|
| 464 |
help="passed through to the trainer by P3; named so a default change cannot silently"
|
|
@@ -489,6 +596,8 @@ def main():
|
|
| 489 |
p2_throughput(args, R)
|
| 490 |
if "p3" in todo:
|
| 491 |
p3_cold_resume(args, R)
|
|
|
|
|
|
|
| 492 |
R["seconds_this_invocation"] = round(time.time() - t0, 1)
|
| 493 |
R["gpu_hours_this_invocation"] = round(R["seconds_this_invocation"] / 3600.0, 3)
|
| 494 |
R["PASSES"] = {k: R.get(k + "_pass") for k in ("PARGS", "P0", "P1", "P2", "P3")}
|
|
|
|
| 29 |
def sh(argv, timeout=None, env=None, label=""):
|
| 30 |
print(f"=== {label or ' '.join(argv[:3])}", flush=True)
|
| 31 |
t0 = time.time()
|
| 32 |
+
try:
|
| 33 |
+
p = subprocess.run(argv, cwd=WORK, capture_output=True, text=True, timeout=timeout,
|
| 34 |
+
env=dict(os.environ, **(env or {})))
|
| 35 |
+
except subprocess.TimeoutExpired as ex:
|
| 36 |
+
# A timeout is a result, not an exception to propagate: letting it raise discarded every cell
|
| 37 |
+
# that had already completed and orphaned both T4s (review finding).
|
| 38 |
+
print(f" TIMEOUT after {time.time() - t0:.0f}s", flush=True)
|
| 39 |
+
return {"rc": -9, "out": (ex.stdout or b"").decode()[-200000:],
|
| 40 |
+
"err": (ex.stderr or b"").decode()[-3000:],
|
| 41 |
+
"seconds": round(time.time() - t0, 1), "timeout": True}
|
| 42 |
for line in (p.stdout or "").splitlines():
|
| 43 |
print(" |", line[:240], flush=True)
|
| 44 |
if p.returncode != 0:
|
|
|
|
| 101 |
adam_beta2=0.95, adam_epsilon=1e-8, max_grad_norm=1.0, lr_scheduler_type="constant",
|
| 102 |
warmup_ratio=0.0, num_train_epochs=1, max_steps=10, fp16=True, bf16=False,
|
| 103 |
gradient_checkpointing=False, ddp_find_unused_parameters=False, dataloader_num_workers=2,
|
| 104 |
+
# the three knobs the tokens/sec sweep turns, checked here on CPU for free before any GPU cell
|
| 105 |
+
# spends billed seconds discovering transformers 5 renamed or rejected one
|
| 106 |
+
gradient_checkpointing_kwargs={"use_reentrant": False}, optim="adamw_torch_fused",
|
| 107 |
+
torch_compile=False, per_device_eval_batch_size=4,
|
| 108 |
dataloader_pin_memory=False, remove_unused_columns=False, ignore_data_skip=True,
|
| 109 |
save_strategy="steps", save_steps=5, save_total_limit=1, logging_steps=5, report_to=[],
|
| 110 |
seed=1, data_seed=1, accelerator_config={"dispatch_batches": False},
|
|
|
|
| 384 |
print("VERDICT P2_pass=", R["P2_pass"], "eta_hours:", json.dumps(R["P2_eta_hours_at_1B"]), flush=True)
|
| 385 |
|
| 386 |
|
| 387 |
+
def p_opts(args, R):
|
| 388 |
+
"""Tokens/sec, again -- because P3 came in at 5.99 GB peak on a 15.4 GB card and every number used to
|
| 389 |
+
freeze D-011 was measured at accum 1, not the run's accum 32.
|
| 390 |
+
|
| 391 |
+
Gradient checkpointing exists to buy memory, and it costs a full extra forward pass per layer. At
|
| 392 |
+
5.99 GB of ~15.4 GB it is buying headroom nobody is spending, so the honest question is whether the
|
| 393 |
+
frozen config is over-paying for it. Cells below change only *how* the same maths is executed --
|
| 394 |
+
micro-batch/accum multiply to the same 262,144 tokens/step in every row, and the optimiser, LR,
|
| 395 |
+
schedule, seed and data are identical -- so a winner can be adopted at freeze time without touching
|
| 396 |
+
what the run is a test of.
|
| 397 |
+
|
| 398 |
+
A baseline cell runs FIRST and the driver reports gains against it, because comparing across sessions
|
| 399 |
+
on shared T4s is how the 4,071-vs-11,062 confusion happened in the first place.
|
| 400 |
+
"""
|
| 401 |
+
cells = [
|
| 402 |
+
("base_m4_gc", 20, ["--micro-batch", "4", "--accum", "32", "--grad-ckpt"],
|
| 403 |
+
{"micro_batch": 4, "accum": 32, "grad_ckpt": True}),
|
| 404 |
+
("m4_nogc", 20, ["--micro-batch", "4", "--accum", "32", "--no-grad-ckpt"],
|
| 405 |
+
{"micro_batch": 4, "accum": 32, "grad_ckpt": False}),
|
| 406 |
+
("m8_gc_acc16", 20, ["--micro-batch", "8", "--accum", "16", "--grad-ckpt"],
|
| 407 |
+
{"micro_batch": 8, "accum": 16, "grad_ckpt": True}),
|
| 408 |
+
("m8_nogc_acc16", 20, ["--micro-batch", "8", "--accum", "16", "--no-grad-ckpt"],
|
| 409 |
+
{"micro_batch": 8, "accum": 16, "grad_ckpt": False}),
|
| 410 |
+
("m16_gc_acc8", 20, ["--micro-batch", "16", "--accum", "8", "--grad-ckpt"],
|
| 411 |
+
{"micro_batch": 16, "accum": 8, "grad_ckpt": True}),
|
| 412 |
+
("m4_gc_nonreentrant", 20, ["--micro-batch", "4", "--accum", "32", "--grad-ckpt",
|
| 413 |
+
"--gc-nonreentrant"],
|
| 414 |
+
{"micro_batch": 4, "accum": 32, "grad_ckpt": True, "gc_nonreentrant": True}),
|
| 415 |
+
("m4_gc_fusedoptim", 20, ["--micro-batch", "4", "--accum", "32", "--grad-ckpt",
|
| 416 |
+
"--optim", "adamw_torch_fused"],
|
| 417 |
+
{"micro_batch": 4, "accum": 32, "grad_ckpt": True, "optim": "adamw_torch_fused"}),
|
| 418 |
+
]
|
| 419 |
+
common = ["--seq-len", str(args.seq_len), "--tokens", str(args.tokens), "--attn", args.attn,
|
| 420 |
+
"--hub-repo", "", "--log-every", "10", "--val-tokens", "200000"]
|
| 421 |
+
out = {}
|
| 422 |
+
base_rate = None
|
| 423 |
+
for label, steps, extra, expect in cells:
|
| 424 |
+
# Every row keeps the frozen 262,144 tokens/step: micro_batch x accum x 2 cards is constant, so a
|
| 425 |
+
# winner changes how the same maths is executed, never what is being trained.
|
| 426 |
+
if expect["micro_batch"] * expect["accum"] * 2 * args.seq_len != 262144:
|
| 427 |
+
raise SystemExit(f"cell {label} does not preserve the frozen batch size in tokens")
|
| 428 |
+
# An OOM in one row is a result, not a systematic defect: unlike P2, keep going and let the table
|
| 429 |
+
# show which configurations this hardware can hold. --push-every-steps = the whole cell means one
|
| 430 |
+
# checkpoint write per cell instead of ten, which would otherwise dominate elapsed and fill the
|
| 431 |
+
# 19.5 GB working directory (review finding).
|
| 432 |
+
r = torchrun(args, common + extra + ["--max-steps", str(steps),
|
| 433 |
+
"--push-every-steps", str(steps),
|
| 434 |
+
"--out", WORK + "/opt_" + label], timeout=1500)
|
| 435 |
+
cell = {"rc": r["rc"], "seconds": r["seconds"], "argv": " ".join(extra),
|
| 436 |
+
"expect": expect, "tokens_per_step": expect["micro_batch"] * expect["accum"] * 2
|
| 437 |
+
* args.seq_len}
|
| 438 |
+
for line in r["out"].splitlines():
|
| 439 |
+
if line.startswith("RUN_JSON "):
|
| 440 |
+
try:
|
| 441 |
+
cell.update(json.loads(line[len("RUN_JSON "):]))
|
| 442 |
+
except Exception as e:
|
| 443 |
+
cell["run_json_unparseable"] = str(e)[:120]
|
| 444 |
+
elif line.startswith("precision:"):
|
| 445 |
+
cell["precision"] = line[:240]
|
| 446 |
+
elif "OutOfMemory" in line or "CUDA out of memory" in line:
|
| 447 |
+
cell["oom"] = True
|
| 448 |
+
# Trust but verify: the cell is only the configuration it claims to be if the trainer says so.
|
| 449 |
+
cell["expect_mismatch"] = {k: [v, cell.get(k)] for k, v in expect.items()
|
| 450 |
+
if cell.get(k) is not None and cell.get(k) != v}
|
| 451 |
+
if "tok_per_s" not in cell:
|
| 452 |
+
cell["tail"] = (r["out"] or "")[-1200:]
|
| 453 |
+
cell["err_first_error"] = ((r["err"] or "")[-1500:])
|
| 454 |
+
if label == "base_m4_gc":
|
| 455 |
+
base_rate = cell.get("tok_per_s")
|
| 456 |
+
if base_rate and cell.get("tok_per_s"):
|
| 457 |
+
cell["gain_vs_base_pct"] = round(100.0 * (cell["tok_per_s"] / base_rate - 1.0), 1)
|
| 458 |
+
out[label] = cell
|
| 459 |
+
print(f"VERDICT opt {label} rc={cell['rc']} tok_per_s={cell.get('tok_per_s')} "
|
| 460 |
+
f"peak_gb={cell.get('peak_gpu_gb')} gain={cell.get('gain_vs_base_pct')} "
|
| 461 |
+
f"mismatch={cell['expect_mismatch'] or 'none'}", flush=True)
|
| 462 |
+
# Nothing here needs to survive the cell, and 8 cells x ~3 GB of export would fill the disk and
|
| 463 |
+
# take the whole session down with it (§3.13's free-space discipline).
|
| 464 |
+
sh(["bash", "-c", "rm -rf " + WORK + "/opt_" + label + " && df -h " + WORK + " | tail -1"],
|
| 465 |
+
label="cleanup " + label)
|
| 466 |
+
R["OPT"] = out
|
| 467 |
+
got = {k: v["tok_per_s"] for k, v in out.items() if v.get("tok_per_s")}
|
| 468 |
+
best = max(got, key=got.get) if got else None
|
| 469 |
+
R["OPT_best"] = best
|
| 470 |
+
R["OPT_best_tok_per_s"] = got.get(best)
|
| 471 |
+
R["OPT_best_eta_hours_at_1B"] = (round(1e9 / got[best] / 3600, 2) if best else None)
|
| 472 |
+
# "Passing" here means the table is trustworthy, not that anything improved: every cell must have
|
| 473 |
+
# either run or OOMed (a real answer), and none may have died in a way that hides a defect.
|
| 474 |
+
R["OPT_pass"] = bool(len(out) == len(cells) and base_rate and
|
| 475 |
+
all(("tok_per_s" in v) or v.get("oom") for v in out.values())
|
| 476 |
+
and not any(v["expect_mismatch"] for v in out.values()))
|
| 477 |
+
print("VERDICT OPT_pass=", R["OPT_pass"], "best=", best, got, flush=True)
|
| 478 |
+
|
| 479 |
+
|
| 480 |
def p3_cold_resume(args, R):
|
| 481 |
"""Three legs, each ending in one checkpoint, each started from the Hub with nothing local but the mix.
|
| 482 |
|
|
|
|
| 563 |
def main():
|
| 564 |
ap = argparse.ArgumentParser()
|
| 565 |
ap.add_argument("--stage", required=True,
|
| 566 |
+
choices=["pargs", "p0", "p1", "p2", "p3", "opts", "summary", "all"],
|
| 567 |
+
help="opts is the tokens/sec re-audit at the run's real accumulation; it is not a "
|
| 568 |
+
"Gate 3 stage and does not enter GATE_3_READY")
|
| 569 |
ap.add_argument("--seq-len", type=int, default=1024) # D-011: the config the main run will use
|
| 570 |
ap.add_argument("--attn", default="eager", choices=["eager", "sdpa"],
|
| 571 |
help="passed through to the trainer by P3; named so a default change cannot silently"
|
|
|
|
| 596 |
p2_throughput(args, R)
|
| 597 |
if "p3" in todo:
|
| 598 |
p3_cold_resume(args, R)
|
| 599 |
+
if "opts" in todo:
|
| 600 |
+
p_opts(args, R)
|
| 601 |
R["seconds_this_invocation"] = round(time.time() - t0, 1)
|
| 602 |
R["gpu_hours_this_invocation"] = round(R["seconds_this_invocation"] / 3600.0, 3)
|
| 603 |
R["PASSES"] = {k: R.get(k + "_pass") for k in ("PARGS", "P0", "P1", "P2", "P3")}
|