Cion-lab commited on
Commit
86efd09
·
verified ·
1 Parent(s): 18d95d5

Upload kernels/phase4_session.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. kernels/phase4_session.py +209 -0
kernels/phase4_session.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """One Phase 4 session of the ounce100m main run: resume, train to a checkpoint boundary, stop.
2
+
3
+ Why this file exists as a *launcher with a planner* rather than a plain torchrun line. The run needs
4
+ 35.9 GPU-hours and Kaggle gives 30 h/week on sessions whose cap I have not measured, so it will be
5
+ interrupted -- and an interruption that lands between two checkpoints throws away everything since the
6
+ last one. §2 fixes the checkpoint cadence at one per 10 % of tokens (381 steps, ~3.6 h at the measured
7
+ rate), and §3.1 forbids restarting the run once checkpoint 1 exists. The only lever left is *when a
8
+ session chooses to stop*, so this script computes the largest whole number of checkpoint intervals that
9
+ fits the session's remaining GPU hours and passes that as --stop-after-steps. Every session then ends
10
+ sitting on a pushed, Hub-verified, locally-pruned checkpoint, and no billed work is ever discarded.
11
+
12
+ Failure modes are deliberately loud: if the budget cannot cover one checkpoint interval it refuses to
13
+ start rather than burning up to 3.6 hours of quota on work that cannot be recovered, and if the Hub
14
+ pointer is unreadable for any reason other than "no checkpoint yet" it aborts (a resume that silently
15
+ starts from step 0 would train the same data twice and destroy the run's honesty).
16
+
17
+ No credentials in this file: they are fetched at run time by ounce100m_credentials (D-006).
18
+ """
19
+
20
+ import json
21
+ import os
22
+ import subprocess
23
+ import sys
24
+ import time
25
+
26
+ WORK = "/kaggle/working"
27
+ MIX_REPO = "Cion-lab/ounce100m-mix-v1"
28
+ CKPT_REPO = "Cion-lab/ounce100m-ckpt"
29
+
30
+ # The frozen recipe (D-011, D-012). Anything here that a future session might want to change is a
31
+ # change to the run itself, so it is asserted against the trainer's own numbers below rather than trusted.
32
+ SEQ_LEN = 1024
33
+ MICRO_BATCH = 4
34
+ ACCUM = 32
35
+ WORLD = 2
36
+ TOKENS = 1_000_000_000
37
+ TOKENS_PER_STEP = MICRO_BATCH * SEQ_LEN * ACCUM * WORLD # 262,144
38
+ HORIZON_STEPS = int(TOKENS / TOKENS_PER_STEP) # 3,814 -- the trainer's own int() division
39
+ CKPT_EVERY = max(1, HORIZON_STEPS // 10) # 381 -- §2's one-per-10 % cadence
40
+
41
+ # Planning rate: overwritten from P3's measured frozen-geometry cell (`P3_frozen_rate_tok_per_s`) at launch.
42
+ PLANNING_TOK_PER_S = float(os.environ.get("PLANNING_TOK_PER_S", "7732"))
43
+ SESSION_GPU_HOURS = float(os.environ.get("SESSION_GPU_HOURS", "11.0"))
44
+ QUOTA_LEFT_HOURS = float(os.environ.get("QUOTA_LEFT_HOURS", "30.0"))
45
+ RESERVE_HOURS = float(os.environ.get("RESERVE_HOURS", "0.6")) # startup, val eval, the last push
46
+
47
+
48
+ def sh(argv, label, timeout=None, env=None):
49
+ print(f"=== {label}", flush=True)
50
+ e = dict(os.environ)
51
+ e["PYTHONPATH"] = WORK
52
+ e.update(env or {})
53
+ t0 = time.time()
54
+ try:
55
+ p = subprocess.run(argv, cwd=WORK, capture_output=True, text=True, timeout=timeout, env=e)
56
+ except subprocess.TimeoutExpired as ex:
57
+ print(f"{label} TIMEOUT after {time.time() - t0:.0f}s", flush=True)
58
+ return -9, (ex.stdout or b"").decode()[-200000:], (ex.stderr or b"").decode()[-4000:]
59
+ out, err = p.stdout or "", p.stderr or ""
60
+ for line in out.splitlines():
61
+ print(" |", line[:300], flush=True)
62
+ if p.returncode != 0:
63
+ lines = err.splitlines()
64
+ keep = []
65
+ for i, ln in enumerate(lines): # first traceback, not a wrapper's tail (E-023)
66
+ if "Traceback" in ln or "Error" in ln:
67
+ keep = lines[max(0, i - 2):i + 30]
68
+ break
69
+ print(" STDERR_HEAD:\n" + "\n".join(keep or lines[:45])[-4000:], flush=True)
70
+ print(f"{label}_RC {p.returncode} seconds {round(time.time() - t0, 1)}", flush=True)
71
+ return p.returncode, out
72
+
73
+
74
+ def fetch(rev, want):
75
+ """Download the run's code at a pinned revision and assert every sha256 before executing any of it."""
76
+ base = f"https://huggingface.co/Cion-lab/ounce100m-code/resolve/{rev}"
77
+ import hashlib
78
+ for local, (repo_path, sha) in sorted(want.items()):
79
+ rc = subprocess.run(["curl", "-sfL", f"{base}/{repo_path}", "-o", local],
80
+ capture_output=True, text=True).returncode
81
+ assert rc == 0, f"fetch failed: {repo_path} at {rev}"
82
+ got = hashlib.sha256(open(os.path.join(WORK, local), "rb").read()).hexdigest()
83
+ assert got == sha, f"SHA MISMATCH {repo_path}: hub {got[:16]} != expected {sha[:16]}"
84
+ print("OK", local, got[:12], flush=True)
85
+ print("REV_OK", rev, flush=True)
86
+
87
+
88
+ def plan(start_step, free_gb):
89
+ """Largest whole number of checkpoint intervals that fits this session's remaining hours."""
90
+ seconds_per_ckpt = CKPT_EVERY * TOKENS_PER_STEP / PLANNING_TOK_PER_S
91
+ usable = min(SESSION_GPU_HOURS, max(0.0, QUOTA_LEFT_HOURS - RESERVE_HOURS))
92
+ n = int(usable * 3600.0 // seconds_per_ckpt)
93
+ remaining_intervals = max(0, (HORIZON_STEPS - start_step + CKPT_EVERY - 1) // CKPT_EVERY)
94
+ n = min(n, remaining_intervals)
95
+ stop = min(HORIZON_STEPS, start_step + n * CKPT_EVERY) if n else 0
96
+ return {"start_step": start_step, "horizon_steps": HORIZON_STEPS, "ckpt_every": CKPT_EVERY,
97
+ "tokens_per_step": TOKENS_PER_STEP, "planning_tok_per_s": PLANNING_TOK_PER_S,
98
+ "hours_per_checkpoint_interval": round(seconds_per_ckpt / 3600.0, 3),
99
+ "session_gpu_hours": SESSION_GPU_HOURS, "quota_left_hours": QUOTA_LEFT_HOURS,
100
+ "reserve_hours": RESERVE_HOURS, "usable_hours": round(usable, 3),
101
+ "intervals_this_session": n, "stop_after_steps": stop,
102
+ "tokens_at_stop": stop * TOKENS_PER_STEP,
103
+ "pct_of_run_at_stop": round(100.0 * stop * TOKENS_PER_STEP / TOKENS, 2),
104
+ "free_gb_before_start": free_gb}
105
+
106
+
107
+ def main():
108
+ os.chdir(WORK)
109
+ sys.path.insert(0, WORK)
110
+ rev = os.environ.get("OUNCE100M_REV") or sys.argv[1]
111
+ fetch(rev, {
112
+ "ounce100m_credentials.py": ("ounce100m_credentials.py",
113
+ "6525f62f03f2d73650a1eb4f70fcb52d1194caad4ca88b2d8bd8fd54f88339b6"),
114
+ "shard_dataset.py": ("train/shard_dataset.py",
115
+ "4111832e79445fb0ffce6a73a02da4c97255d6c7886762682e04a417f2695a78"),
116
+ "hubckpt.py": ("train/hubckpt.py",
117
+ "7a250e8232dc99d39108913f169c290e1ef03744217d71bc2dcf833f5d784c53"),
118
+ "train_ounce100m.py": ("train/train_ounce100m.py",
119
+ "c8462cac0cabebc65bcfdcd62be3ed53b03e08120e3a6eb9ca2c4dc235cace42"),
120
+ })
121
+
122
+ import ounce100m_credentials
123
+ print("creds:", json.dumps(ounce100m_credentials.install(verify=True)), flush=True)
124
+ rc, out = sh(["bash", "-c", "nvidia-smi --query-gpu=name,memory.used,memory.total "
125
+ "--format=csv,noheader; df -h /kaggle/working | tail -1; free -g | head -2"],
126
+ "env")
127
+ free_gb = None
128
+ for line in out.splitlines():
129
+ if "overlay" in line or line.startswith("/dev/"):
130
+ parts = line.split()
131
+ if len(parts) >= 4:
132
+ free_gb = parts[3]
133
+
134
+ # The Hub pointer is the only truth about where the run is (never local state -- §3.13).
135
+ import hubckpt
136
+ tok = os.environ["HF_TOKEN"]
137
+ ptr = hubckpt.latest_pointer(CKPT_REPO, repo_type="dataset", token=tok)
138
+ if ptr.get("error"):
139
+ print(f"FATAL: cannot read latest.json from {CKPT_REPO}: {ptr['error'][:300]}\n"
140
+ "Refusing to guess a start step: the alternative is training data that was already read.",
141
+ flush=True)
142
+ raise SystemExit(6)
143
+ start_step = int(ptr.get("step", 0)) if not ptr.get("not_found") else 0
144
+ print(f"hub says: {'no checkpoint yet -- starting from step 0' if start_step == 0 else ptr}",
145
+ flush=True)
146
+ if start_step >= HORIZON_STEPS:
147
+ print("VERDICT RUN_ALREADY_COMPLETE at step", start_step, flush=True)
148
+ return
149
+
150
+ # Data: the published mix, fetched by anonymous read so this works on any instance. The checks run
151
+ # here rather than inside the downloader so a quoting mistake cannot turn into a silent pass.
152
+ rc, _ = sh([sys.executable, "-c",
153
+ "import sys; sys.path.insert(0, '/kaggle/working');\n"
154
+ "from huggingface_hub import snapshot_download\n"
155
+ f"p = snapshot_download(repo_id='{MIX_REPO}', repo_type='dataset',\n"
156
+ " local_dir='/kaggle/working/mixroot')\n"
157
+ "print('downloaded to', p)\n"],
158
+ "fetch_mix", 3600)
159
+ if rc != 0:
160
+ raise SystemExit(7)
161
+ man = json.load(open(os.path.join(WORK, "mixroot", "manifest.json")))
162
+ windows = int(man["total_tokens"]) // SEQ_LEN
163
+ needed_windows = HORIZON_STEPS * MICRO_BATCH * ACCUM * WORLD
164
+ print(f"mix: {man['total_tokens']:,} train tokens in {man['n_shards']} shards, "
165
+ f">= {man['distinct_sources_per_shard_min']} sources per shard, "
166
+ f"{windows:,} windows of {SEQ_LEN}; the horizon needs {needed_windows:,} "
167
+ f"({100.0 * windows / needed_windows - 100:.1f} % margin)", flush=True)
168
+ if int(man["total_tokens"]) < TOKENS or windows < needed_windows:
169
+ # Training past the end of the dataset would end the epoch early and report a token count that
170
+ # was never reached. The token target is a claim in the final report, so it is checked here.
171
+ print(f"VERDICT REFUSED_TO_START: mix holds {man['total_tokens']:,} tokens / {windows:,} "
172
+ f"windows but the run needs {TOKENS:,} / {needed_windows:,}. Either the target drops to "
173
+ "0.9 B (D-012's fallback) or more sources are staged -- not a decision to make "
174
+ "implicitly by launching anyway.", flush=True)
175
+ raise SystemExit(9)
176
+
177
+ pl = plan(start_step, free_gb)
178
+ print("PLAN_JSON", json.dumps(pl, indent=1), flush=True)
179
+ if not pl["intervals_this_session"]:
180
+ print(f"VERDICT REFUSED_TO_START: {pl['usable_hours']} usable hours will not reach one "
181
+ f"{pl['hours_per_checkpoint_interval']} h checkpoint interval. Launching anyway would "
182
+ "burn quota on work that cannot be resumed from. Raise SESSION_GPU_HOURS or wait for "
183
+ "quota.", flush=True)
184
+ raise SystemExit(8)
185
+
186
+ argv = [sys.executable, "-u", "train_ounce100m.py",
187
+ "--root", f"{WORK}/mixroot", "--out", f"{WORK}/run",
188
+ "--hub-repo", CKPT_REPO, "--prune",
189
+ "--seq-len", str(SEQ_LEN), "--attn", "eager", "--grad-ckpt",
190
+ "--micro-batch", str(MICRO_BATCH), "--accum", str(ACCUM),
191
+ "--tokens", str(TOKENS), "--lr", "6e-4", "--warmup-frac", "0.02", "--decay-frac", "0.80",
192
+ "--log-every", "20", "--val-tokens", "2000000",
193
+ "--resume", "auto", "--stop-after-steps", str(pl["stop_after_steps"])]
194
+ rc, out = sh(["torchrun", "--nproc_per_node=2"] + argv, "TRAIN",
195
+ timeout=int((pl["intervals_this_session"] * pl["hours_per_checkpoint_interval"] + 1.5)
196
+ * 3600))
197
+ for line in out.splitlines():
198
+ if line.startswith(("CKPT ", "RUN_JSON ", "resume from", "auto-resume", "precision:",
199
+ "params:", "validation loss", "segment boundary")):
200
+ print("KEY>", line[:400], flush=True)
201
+ print("VERDICT PHASE4_SESSION rc", rc, "steps", start_step, "->", pl["stop_after_steps"],
202
+ "tokens_at_stop", pl["tokens_at_stop"], flush=True)
203
+ sh(["bash", "-c", "df -h /kaggle/working | tail -1"], "disk_after")
204
+ # The session's own wall clock is the measurement that sharpens every later plan.
205
+ raise SystemExit(rc)
206
+
207
+
208
+ if __name__ == "__main__":
209
+ main()