p4_stop_probe: exercise --stop-after-steps, the push/verify/pointer/prune order and a cold Hub resume on 2xT4 before session 1 depends on them
Browse files- kernels/p4_stop_probe.py +177 -0
kernels/p4_stop_probe.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# P4 probe: does --stop-after-steps really produce a resumable, Hub-verified segment boundary?
|
| 2 |
+
#
|
| 3 |
+
# Why this exists and what it buys with ~0.4 GPU-hours: `--stop-after-steps` is the mechanism every Phase 4
|
| 4 |
+
# session ends on, and Gate 3 never exercised it -- P3's legs used `--max-steps`, so each leg *was* a whole
|
| 5 |
+
# run. The launcher's plan, the trainer's stop, HubPush's push-verify-pointer-prune order, the resume scan
|
| 6 |
+
# that refuses a stale pointer, and the final-push-plus-terminal-pointer path are therefore untested
|
| 7 |
+
# together. E-035/4 is precisely a defect in that untested seam, and the rehearsal kernel cannot reach it
|
| 8 |
+
# because it needs two T4s. This does, against the published mix and the frozen 22L geometry, in one
|
| 9 |
+
# session: leg 1 trains to a mid-run stop step, leg 2 wipes the disk, resumes from the Hub and runs to the
|
| 10 |
+
# horizon.
|
| 11 |
+
#
|
| 12 |
+
# The probe writes to its own checkpoint repo, never to Cion-lab/ounce100m-ckpt: the first real session has
|
| 13 |
+
# to find that repo absent, which is the RepoMissing branch E-034 was about.
|
| 14 |
+
import hashlib, json, os, shutil, signal, subprocess, sys, threading, time
|
| 15 |
+
|
| 16 |
+
os.chdir("/kaggle/working")
|
| 17 |
+
sys.path.insert(0, "/kaggle/working")
|
| 18 |
+
REV = "f6f100763eec8782851f1b418fe869df6a4dcb92"
|
| 19 |
+
WANT = {
|
| 20 |
+
"ounce100m_credentials.py": ("ounce100m_credentials.py",
|
| 21 |
+
"6525f62f03f2d73650a1eb4f70fcb52d1194caad4ca88b2d8bd8fd54f88339b6"),
|
| 22 |
+
"shard_dataset.py": ("train/shard_dataset.py",
|
| 23 |
+
"adbcd96fbab9505a5e8dec4d2e83d00366a73f702786bffbde430195cf6f0471"),
|
| 24 |
+
"hubckpt.py": ("train/hubckpt.py",
|
| 25 |
+
"568c31b300906cb8d78a59ef890baa9731b18e064e3d69b0eaea4b45c546f23e"),
|
| 26 |
+
"train_ounce100m.py": ("train/train_ounce100m.py",
|
| 27 |
+
"c4c178c0ac17c68de1521d85612a0dc604e6d4748dfdc8bd3e374512bc90ce96"),
|
| 28 |
+
}
|
| 29 |
+
BASE = "https://huggingface.co/Cion-lab/ounce100m-code/resolve/" + REV
|
| 30 |
+
for p, (rp, want) in sorted(WANT.items()):
|
| 31 |
+
assert subprocess.run(["curl", "-sfL", f"{BASE}/{rp}", "-o", p]).returncode == 0, ("fetch", rp)
|
| 32 |
+
got = hashlib.sha256(open(p, "rb").read()).hexdigest()
|
| 33 |
+
assert got == want, ("SHA MISMATCH", rp, got[:16], want[:16])
|
| 34 |
+
print("OK", p, got[:12], flush=True)
|
| 35 |
+
|
| 36 |
+
import ounce100m_credentials as C
|
| 37 |
+
print("creds:", json.dumps(C.install(verify=True)), flush=True)
|
| 38 |
+
import hubckpt
|
| 39 |
+
from huggingface_hub import HfApi
|
| 40 |
+
|
| 41 |
+
MIX = "Cion-lab/ounce100m-mix-v1"
|
| 42 |
+
PROBE = "Cion-lab/ounce100m-ckpt-probe" # never the real run's repo, see the header
|
| 43 |
+
ROOT, RUN = "/kaggle/working/mixroot", "/kaggle/working/run"
|
| 44 |
+
# 60 steps at accum 2 (16 seqs/step, 16,384 tokens/step) = 983,040 tokens of the real mix. accum is the
|
| 45 |
+
# only thing deviating from the frozen recipe, and the probe tests checkpoint mechanics, not throughput.
|
| 46 |
+
TOKENS, PUSH_EVERY, STOP1 = 983040, 10, 30
|
| 47 |
+
T0 = time.time()
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def run(argv, label, timeout):
|
| 51 |
+
print("=== " + label, flush=True)
|
| 52 |
+
t0 = time.time()
|
| 53 |
+
e = dict(os.environ); e["PYTHONPATH"] = "/kaggle/working"
|
| 54 |
+
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
| 55 |
+
env=e, bufsize=1, start_new_session=True)
|
| 56 |
+
killed = []
|
| 57 |
+
|
| 58 |
+
def _kill():
|
| 59 |
+
killed.append(True)
|
| 60 |
+
try:
|
| 61 |
+
os.killpg(os.getpgid(p.pid), signal.SIGTERM)
|
| 62 |
+
except Exception:
|
| 63 |
+
p.kill()
|
| 64 |
+
|
| 65 |
+
timer = threading.Timer(timeout, _kill)
|
| 66 |
+
timer.daemon = True
|
| 67 |
+
timer.start()
|
| 68 |
+
keep, lines = [], []
|
| 69 |
+
try:
|
| 70 |
+
for line in p.stdout:
|
| 71 |
+
line = line.rstrip("\n")
|
| 72 |
+
lines.append(line)
|
| 73 |
+
if line.startswith(("CKPT ", "RUN_JSON ", "resume from", "auto-resume", "precision:",
|
| 74 |
+
"params:", "mix:", "checkpoint hub target", "segment boundary",
|
| 75 |
+
"latest.json", "TRAIN DONE", "validation loss", "Traceback", "Error")):
|
| 76 |
+
print(" KEY>", line[:300], flush=True)
|
| 77 |
+
keep.append(line)
|
| 78 |
+
del keep[:-40]
|
| 79 |
+
finally:
|
| 80 |
+
timer.cancel()
|
| 81 |
+
rc = p.wait()
|
| 82 |
+
if killed:
|
| 83 |
+
print(" TIMEOUT after %d s" % timeout, flush=True)
|
| 84 |
+
rc = -9
|
| 85 |
+
if rc != 0:
|
| 86 |
+
print(" TAIL:\n" + "\n".join(keep)[-2500:], flush=True)
|
| 87 |
+
print("%s_RC %s seconds %.1f elapsed %.0f" % (label, rc, time.time() - t0, time.time() - T0),
|
| 88 |
+
flush=True)
|
| 89 |
+
return rc, "\n".join(lines)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
rc, out = run([sys.executable, "-c",
|
| 93 |
+
"import sys; sys.path.insert(0, '/kaggle/working')\n"
|
| 94 |
+
"from huggingface_hub import snapshot_download\n"
|
| 95 |
+
"p = snapshot_download(repo_id='%s', repo_type='dataset',\n"
|
| 96 |
+
" local_dir='/kaggle/working/mixroot', max_workers=4)\n"
|
| 97 |
+
"print('mix at', p)\n" % MIX], "FETCH_MIX", 2400)
|
| 98 |
+
if rc != 0:
|
| 99 |
+
raise SystemExit("VERDICT P4PROBE_STOP could not fetch the published mix")
|
| 100 |
+
man = json.load(open(os.path.join(ROOT, "manifest.json")))
|
| 101 |
+
print("mix", man["n_shards"], "shards", format(int(man["total_tokens"]), ","), "tokens", flush=True)
|
| 102 |
+
|
| 103 |
+
common = [sys.executable, "-u", "train_ounce100m.py", "--root", ROOT, "--out", RUN,
|
| 104 |
+
"--hub-repo", PROBE, "--prune", "--seq-len", "1024", "--attn", "eager", "--grad-ckpt",
|
| 105 |
+
"--micro-batch", "4", "--accum", "2", "--tokens", str(TOKENS), "--lr", "6e-4",
|
| 106 |
+
"--push-every-steps", str(PUSH_EVERY), "--val-tokens", "100000", "--log-every", "10",
|
| 107 |
+
"--resume", "auto"]
|
| 108 |
+
|
| 109 |
+
shutil.rmtree(RUN, ignore_errors=True)
|
| 110 |
+
rc1, o1 = run(["torchrun", "--nproc_per_node=2"] + common + ["--stop-after-steps", str(STOP1)],
|
| 111 |
+
"LEG1_STOP_EARLY", 3000)
|
| 112 |
+
api = HfApi(token=os.environ["HF_TOKEN"])
|
| 113 |
+
try:
|
| 114 |
+
listed = sorted(hubckpt.hub_listing(PROBE, "dataset", token=os.environ["HF_TOKEN"]))
|
| 115 |
+
except Exception as e:
|
| 116 |
+
listed = ["<listing failed: %s>" % type(e).__name__]
|
| 117 |
+
pushed = sorted({k.split("/")[1] for k in listed if k.startswith("ckpt/checkpoint-")
|
| 118 |
+
and len(k.split("/")) > 1})
|
| 119 |
+
ptr = hubckpt.latest_pointer(PROBE, token=os.environ["HF_TOKEN"])
|
| 120 |
+
print("LEG1 pushed:", pushed, "pointer:", {k: ptr.get(k) for k in ("step", "path_in_repo", "error")},
|
| 121 |
+
flush=True)
|
| 122 |
+
|
| 123 |
+
shutil.rmtree(RUN, ignore_errors=True) # force the cold-resume path (E-029's shape)
|
| 124 |
+
rc2, o2 = run(["torchrun", "--nproc_per_node=2"] + common + ["--stop-after-steps", "0"],
|
| 125 |
+
"LEG2_TO_HORIZON", 3000)
|
| 126 |
+
try:
|
| 127 |
+
listed2 = sorted(hubckpt.hub_listing(PROBE, "dataset", token=os.environ["HF_TOKEN"]))
|
| 128 |
+
except Exception as e:
|
| 129 |
+
listed2 = ["<listing failed: %s>" % type(e).__name__]
|
| 130 |
+
ptr2 = hubckpt.latest_pointer(PROBE, token=os.environ["HF_TOKEN"])
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def harvest(text, tag):
|
| 134 |
+
for line in (text or "").splitlines():
|
| 135 |
+
if line.startswith(tag):
|
| 136 |
+
try:
|
| 137 |
+
return json.loads(line[len(tag):].strip())
|
| 138 |
+
except Exception:
|
| 139 |
+
return {"unparsed": line[:200]}
|
| 140 |
+
return {}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
rj1, rj2 = harvest(o1, "RUN_JSON "), harvest(o2, "RUN_JSON ")
|
| 144 |
+
res = {
|
| 145 |
+
"leg1_rc": rc1, "leg2_rc": rc2,
|
| 146 |
+
"leg1": {"final_step": rj1.get("final_step"), "segment_stop": rj1.get("segment_stop"),
|
| 147 |
+
"tokens_consumed": rj1.get("tokens_consumed"), "tok_per_s": rj1.get("tok_per_s"),
|
| 148 |
+
"params": rj1.get("params"), "peak_gpu_gb": rj1.get("peak_gpu_gb"),
|
| 149 |
+
"pushed": pushed, "pointer": {k: ptr.get(k) for k in ("step", "path_in_repo")}},
|
| 150 |
+
"leg2": {"final_step": rj2.get("final_step"), "segment_stop": rj2.get("segment_stop"),
|
| 151 |
+
"tokens_consumed": rj2.get("tokens_consumed"), "val_ppl": rj2.get("val_ppl"),
|
| 152 |
+
"pointer_after": {k: ptr2.get(k) for k in ("step", "path_in_repo")},
|
| 153 |
+
"has_final": any(k.startswith("final/") for k in listed2)},
|
| 154 |
+
"expect": {"steps_planned": 60, "push_every": PUSH_EVERY, "stop1": STOP1,
|
| 155 |
+
"tokens_at_stop1": STOP1 * 16384, "tokens_at_horizon": 60 * 16384},
|
| 156 |
+
}
|
| 157 |
+
res["checks"] = {
|
| 158 |
+
"leg1_stopped_at_the_stop_step": rj1.get("final_step") == STOP1,
|
| 159 |
+
"leg1_reported_a_segment": rj1.get("segment_stop") is True,
|
| 160 |
+
"leg1_pushed_every_interval": [p for p in pushed if p.startswith("checkpoint-")] ==
|
| 161 |
+
[f"checkpoint-{s}" for s in range(PUSH_EVERY, STOP1 + 1, PUSH_EVERY)],
|
| 162 |
+
"leg1_pointer_at_stop": ptr.get("step") == STOP1,
|
| 163 |
+
"leg2_resumed_and_finished": rj2.get("final_step") == 60,
|
| 164 |
+
"leg2_was_not_a_segment": rj2.get("segment_stop") is False,
|
| 165 |
+
"leg2_pushed_final": any(k.startswith("final/") for k in listed2),
|
| 166 |
+
"leg2_pointer_terminal": ptr2.get("step") == 60 and ptr2.get("path_in_repo") == "final",
|
| 167 |
+
"tokens_match_the_arithmetic": rj1.get("tokens_consumed") == STOP1 * 16384
|
| 168 |
+
and rj2.get("tokens_consumed") == 60 * 16384,
|
| 169 |
+
"params_are_the_frozen_model": rj1.get("params") == 106194240,
|
| 170 |
+
}
|
| 171 |
+
res["PROBE_PASSED"] = all(res["checks"].values()) and rc1 == 0 and rc2 == 0
|
| 172 |
+
print("PROBE_JSON_BEGIN")
|
| 173 |
+
print(json.dumps(res, indent=1, default=str))
|
| 174 |
+
print("PROBE_JSON_END")
|
| 175 |
+
print("VERDICT P4PROBE", "PASS" if res["PROBE_PASSED"] else "FAIL",
|
| 176 |
+
[k for k, v in res["checks"].items() if not v], "seconds", round(time.time() - T0, 1), flush=True)
|
| 177 |
+
raise SystemExit(0 if res["PROBE_PASSED"] else 5)
|