Cion-lab commited on
Commit
45f1d40
·
verified ·
1 Parent(s): a6ae45d

phase4_prep: free CPU rehearsal of everything a GPU session can fail -- pinned fetch, creds, pointer read, planner, full sha256 of the published mix, per-leg window arithmetic, resume-equivalence at real boundaries

Browse files
Files changed (1) hide show
  1. kernels/phase4_prep.py +176 -0
kernels/phase4_prep.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 4 rehearsal on a CPU instance: every check a session can fail, run where failing costs no quota.
2
+ #
3
+ # The first GPU session of the run bills ~11 hours before it is provably healthy, and E-031 is exactly the
4
+ # shape of bug that only shows up mid-run (a dataset-length guard that compared the wrong quantity would
5
+ # have refused the run's third leg). So this kernel does, on free CPU: fetches the pinned code and asserts
6
+ # its sha256s, resolves credentials, reads the checkpoint pointer, downloads the PUBLISHED mix from the
7
+ # Hub, hashes every shard against the manifest, rehearses the session planner for the whole schedule, and
8
+ # proves resume-equivalence of the reader at the real checkpoint boundaries with the real seed.
9
+ #
10
+ # It ends with the same PLAN_JSON the GPU session will print, so the numbers can be compared before and
11
+ # after rather than trusting that the code did not change in between.
12
+ import hashlib, json, os, subprocess, sys, time
13
+
14
+ os.chdir("/kaggle/working")
15
+ sys.path.insert(0, "/kaggle/working")
16
+ REV = os.environ.get("OUNCE100M_REV") or "a6ae45de71482ecb6c810a25368db665f88e672d"
17
+ WANT = {
18
+ "ounce100m_credentials.py": ("ounce100m_credentials.py",
19
+ "6525f62f03f2d73650a1eb4f70fcb52d1194caad4ca88b2d8bd8fd54f88339b6"),
20
+ "shard_dataset.py": ("train/shard_dataset.py",
21
+ "4111832e79445fb0ffce6a73a02da4c97255d6c7886762682e04a417f2695a78"),
22
+ "hubckpt.py": ("train/hubckpt.py",
23
+ "c8c958417dc48db92f8be5c4b505581c746be9cc10110a358230ac47c480d5bf"),
24
+ "train_ounce100m.py": ("train/train_ounce100m.py",
25
+ "487edc1be54f03b5f1a312901580cc3e1504c412322ed074ceff869febbfb8bc"),
26
+ "phase4_session.py": ("kernels/phase4_session.py",
27
+ "a52c758ccca3f74d518d6fd1a765383c2851ea5893883429fba4c8f8b95bdfb2"),
28
+ }
29
+ BASE = "https://huggingface.co/Cion-lab/ounce100m-code/resolve/" + REV
30
+ for p, (rp, want) in sorted(WANT.items()):
31
+ r = subprocess.run(["curl", "-sfL", BASE + "/" + rp, "-o", p], capture_output=True, text=True)
32
+ assert r.returncode == 0, ("fetch failed", rp, (r.stderr or "")[:200])
33
+ got = hashlib.sha256(open(p, "rb").read()).hexdigest()
34
+ assert got == want, ("SHA MISMATCH", rp, got, want)
35
+ print("OK", p, got[:12], flush=True)
36
+ print("REV_OK", REV, flush=True)
37
+
38
+ import ounce100m_credentials as C
39
+ print("creds:", json.dumps(C.install(verify=True)), flush=True)
40
+ T0 = time.time()
41
+
42
+
43
+ def run(argv, label, timeout, env=None):
44
+ print("=== " + label, flush=True)
45
+ t0 = time.time()
46
+ e = dict(os.environ); e["PYTHONPATH"] = "/kaggle/working"
47
+ e.update(env or {})
48
+ p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
49
+ env=e, bufsize=1)
50
+ import threading
51
+ killed = []
52
+ timer = threading.Timer(timeout, lambda: (killed.append(True), p.kill()))
53
+ timer.daemon = True
54
+ timer.start()
55
+ out = []
56
+ try:
57
+ for line in p.stdout:
58
+ out.append(line.rstrip("\n"))
59
+ print(" |", line[:300], flush=True)
60
+ finally:
61
+ timer.cancel()
62
+ rc = p.wait()
63
+ if killed:
64
+ print(" TIMEOUT after %d s, killed" % timeout, flush=True)
65
+ rc = -9
66
+ print("%s_RC %s seconds %.1f elapsed %.0f" % (label, rc, time.time() - t0, time.time() - T0),
67
+ flush=True)
68
+ return rc, "\n".join(out)
69
+
70
+
71
+ # Stage 1 -- the launcher itself, in rehearsal mode. On a CPU box torchrun is never reached: PREP_ONLY
72
+ # stops right after the pointer read, the mix download and the planner. Everything upstream of the first
73
+ # optimiser step is exercised with the real files and the real Hub.
74
+ rc1, _ = run([sys.executable, "phase4_session.py", REV], "PREP_LAUNCHER", 3000,
75
+ env={"PHASE4_PREP_ONLY": "1",
76
+ "PLANNING_TOK_PER_S": os.environ.get("PLANNING_TOK_PER_S", "9696"),
77
+ "SESSION_GPU_HOURS": os.environ.get("SESSION_GPU_HOURS", "11.0"),
78
+ "QUOTA_LEFT_HOURS": os.environ.get("QUOTA_LEFT_HOURS", "27.5"),
79
+ "MIN_FREE_GB": "6"})
80
+ if rc1 != 0:
81
+ print("VERDICT PREP_STOP launcher rehearsal rc", rc1, flush=True)
82
+ raise SystemExit(2)
83
+
84
+ # Stage 2 -- the published mix, byte for byte, and the reader at the boundaries the run will actually
85
+ # cross. `snapshot_download` proves the repo is anonymously readable; this proves the bytes are the ones
86
+ # the manifest certifies and that a resume at step X reads the same data an uninterrupted run would have.
87
+ CHECK = """
88
+ import hashlib, json, os, sys
89
+ sys.path.insert(0, "/kaggle/working")
90
+ ROOT = "/kaggle/working/mixroot"
91
+ man = json.load(open(os.path.join(ROOT, "manifest.json")))
92
+ tok_s = int(man["total_tokens"])
93
+ bad, nb = [], 0
94
+ for grp in ("shards", "val_shards"):
95
+ for rec in man.get(grp, []):
96
+ p = os.path.join(ROOT, rec["file"])
97
+ if not os.path.exists(p):
98
+ bad.append((rec["file"], "absent")); continue
99
+ h = hashlib.sha256()
100
+ with open(p, "rb") as fh:
101
+ for chunk in iter(lambda: fh.read(1 << 22), b""):
102
+ h.update(chunk)
103
+ nb += 1
104
+ if h.hexdigest() != rec["sha256"]:
105
+ bad.append((rec["file"], "sha " + h.hexdigest()[:12] + " != " + rec["sha256"][:12]))
106
+ print("SHASHED", nb, "files", "total_tokens", format(tok_s, ","), "mismatches", len(bad), flush=True)
107
+ for b in bad[:10]:
108
+ print(" BAD", b, flush=True)
109
+ assert not bad, "the published mix does not match its own manifest"
110
+
111
+ import shard_dataset as SD
112
+ SEQ, MICRO, ACCUM, WORLD = 1024, 4, 32, 2
113
+ seqs_per_step = MICRO * WORLD * ACCUM
114
+ tps = seqs_per_step * SEQ
115
+ steps = int(1_000_000_000 / tps)
116
+ store = SD.PackedTokenStore(ROOT)
117
+ n_samples = store.total_tokens // SEQ
118
+ print("READER", format(store.total_tokens, ","), "tokens in", len(store.shards), "shards ->",
119
+ format(n_samples, ","), "windows of", SEQ, "|", steps, "steps x", tps, "tok =",
120
+ format(steps * tps, ","), "tokens", flush=True)
121
+ assert n_samples >= steps * seqs_per_step, "not enough windows for the horizon"
122
+
123
+ # Every leg the planner can produce, from every checkpoint boundary to the next.
124
+ fails = []
125
+ for start in range(0, steps + 1, 381):
126
+ stop = min(start + 381, steps)
127
+ need = (steps - start) * seqs_per_step # the trainer's own guard
128
+ avail = n_samples - start * seqs_per_step
129
+ ok = avail >= need and (stop - start) * seqs_per_step <= avail
130
+ if not ok:
131
+ fails.append((start, stop, need, avail))
132
+ print("LEG start %5d stop %5d need %9d avail %9d %s" % (
133
+ start, stop, need, avail, "ok" if ok else "FAIL"), flush=True)
134
+ print("LEGS", 1 + steps // 381, "checked,", len(fails), "failing", flush=True)
135
+ assert not fails, fails
136
+
137
+ # Resume-equivalence at three real boundaries, on the published bytes with the run's own seed.
138
+ full = SD.make_dataset(store, SEQ, 20260919, start_sample=0)
139
+ for boundary in (381, 1905, 3429):
140
+ s0 = boundary * seqs_per_step
141
+ resumed = SD.make_dataset(store, SEQ, 20260919, start_sample=s0)
142
+ assert len(resumed) == len(full) - s0, (len(resumed), len(full), s0)
143
+ same = True
144
+ for j in (0, 1, len(resumed) // 2, len(resumed) - 1):
145
+ a = full[s0 + j]["input_ids"].tolist()
146
+ b = resumed[j]["input_ids"].tolist()
147
+ same = same and a == b and full[s0 + j]["labels"].tolist() == resumed[j]["labels"].tolist()
148
+ print("RESUME_EQUIV step", boundary, "start_sample", s0, "len", len(resumed), "identical", same,
149
+ flush=True)
150
+ assert same, boundary
151
+ print("CHECK_OK windows", format(n_samples, ","), "shards", len(store.shards), flush=True)
152
+ """
153
+ open("prepcheck.py", "w").write(CHECK)
154
+ rc2, _ = run([sys.executable, "prepcheck.py"], "CHECK_PUBLISHED_MIX", 2400)
155
+
156
+ # Stage 3 -- the reader's cursor format round-trips, and the trainer imports and prints --help on this
157
+ # image. Both are cheap and both have bitten before (E-028: a build script that could not import its
158
+ # sibling module surfaced only after a GPU session had already been billed).
159
+ rc3, _ = run([sys.executable, "-c",
160
+ "import sys; sys.path.insert(0, '/kaggle/working')\n"
161
+ "import shard_dataset as SD, json, os\n"
162
+ "os.makedirs('/tmp/cur', exist_ok=True)\n"
163
+ "c = SD.cursor_dict(1024, 97536, 20260919, dataset_files_sha='ab' * 32, step=381)\n"
164
+ "SD.save_cursor('/tmp/cur', c)\n"
165
+ "back = SD.load_cursor('/tmp/cur')\n"
166
+ "print('CURSOR', json.dumps(back), 'roundtrip', back == c)\n"
167
+ "assert back == c\n"
168
+ "import subprocess\n"
169
+ "p = subprocess.run([sys.executable, 'train_ounce100m.py', '--help'],\n"
170
+ " capture_output=True, text=True)\n"
171
+ "print('TRAINER_HELP_RC', p.returncode, 'lines', len((p.stdout or '').splitlines()))\n"
172
+ "assert p.returncode == 0, (p.stdout or '')[-1500:] + (p.stderr or '')[-1500:]\n"],
173
+ "CURSOR_AND_IMPORT", 900)
174
+ print("VERDICT PHASE4_PREP rc_launcher", rc1, "rc_check", rc2, "rc_cursor", rc3,
175
+ "seconds", round(time.time() - T0, 1), flush=True)
176
+ raise SystemExit(0 if (rc1 == rc2 == rc3 == 0) else 4)