Cion-lab commited on
Commit
f256711
·
verified ·
1 Parent(s): 6ed0dbb

Upload train/preflight.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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
- p = subprocess.run(argv, cwd=WORK, capture_output=True, text=True, timeout=timeout,
33
- env=dict(os.environ, **(env or {})))
 
 
 
 
 
 
 
 
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")}