Cion-lab commited on
Commit
1878839
·
verified ·
1 Parent(s): c6b42bb

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
Files changed (1) hide show
  1. 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)