Cion-lab commited on
Commit
c89f91b
·
verified ·
1 Parent(s): 7fa7ceb

publish_model: anonymous clean room, every-file read-back, list-only-on-404 resume, card fields read from run_summary (review E-045)

Browse files
Files changed (1) hide show
  1. build/publish_model.py +78 -22
build/publish_model.py CHANGED
@@ -74,6 +74,30 @@ def card(cfg, man, aud, run, repo, files, n_params):
74
  cf = man.get('contamination_filter') or {}
75
  tok_consumed = int(run.get("tokens_consumed") or 0)
76
  loss = " -> ".join(str(x) for x in (run.get("prior_loss"), run.get("final_loss")) if x is not None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  rows = "\n".join(f"| `{k}` | {v['sha256'][:16]}… | {v['bytes']:,} |" for k, v in sorted(files.items()))
78
  return f"""---
79
  license: cc-by-sa-4.0
@@ -98,17 +122,18 @@ report it honestly, including where that is near chance.
98
  |---|---|
99
  | Parameters | **{n_params:,}** (recomputed from `model.safetensors`'s own header, not asserted) |
100
  | Architecture | {p[0]} layers, hidden {p[1]}, GQA {cfg["num_attention_heads"]} Q / {cfg["num_key_value_heads"]} KV heads, SwiGLU {cfg["intermediate_size"]}, RMSNorm {cfg["rms_norm_eps"]}, RoPE θ={rope}, **tied** embeddings, vocab {p[2]:,} |
101
- | Sequence length | {run.get('seq_len')} — which is why ARC's 25-shot prompts are truncated from the left by the eval harness |
102
  | Tokens consumed | **{tok_consumed:,}** ({100.0 * tok_consumed / 1e9:.2f} % of the 1.0 B target) |
103
- | Batch | micro {run.get('micro_batch')} × accum {run.get('accum')} × 2 ranks = {run.get('micro_batch', 0) * run.get('accum', 0) * 2 * run.get('seq_len', 1024):,} tokens/step |
104
- | Optimiser | AdamW β(0.9, 0.95), wd 0.1, clip 1.0, LR 6e-4, trapezoid schedule (plateau then linear decay over the final 20 %) |
105
- | Precision | fp16 autocast + fp32 master weights + GradScaler, asserted at start of every session |
106
- | Throughput | {run.get('tok_per_s'):,} tok/s sustained |
107
- | Attention | `{run.get('effective_attn', run.get('attn'))}` — measured faster than SDPA on Turing |
108
- | Gradient checkpointing | {run.get('grad_ckpt')} |
 
109
  | Wall clock | {run.get('session_seconds', run.get('seconds', 0)) / 3600.0:.2f} h in the session that produced this export. The run is split across sessions at checkpoint boundaries, so this is **not** the total training time |
110
  | Loss | {loss} |
111
- | Validation PPL | {run.get('val_ppl') if run.get('val_ppl') is not None else 'NOT MEASURED (' + str(run.get('val_loss_error')) + ')'} on {MIX}'s held-out `val/` shards ({man['val_tokens']:,} tokens, never trained on) |
112
 
113
  ## Data
114
 
@@ -185,8 +210,13 @@ def main():
185
  dest = "/tmp/cleanroom_" + os.path.basename(a.repo)
186
  shutil.rmtree(dest, ignore_errors=True)
187
  shutil.rmtree("/tmp/cleanroom_hf", ignore_errors=True)
188
- got = snapshot_download(a.repo, local_dir=dest)
189
- print("clean-room download ->", got, flush=True)
 
 
 
 
 
190
  import json as _j
191
  cfg = _j.load(open(os.path.join(dest, "config.json")))
192
  n_t, n_p, dt = safetensors_counts(os.path.join(dest, "model.safetensors"))
@@ -247,25 +277,44 @@ def main():
247
  raise SystemExit("no run_summary.json next to the export: the card would be full of blanks. "
248
  "Pass --from-dir at the trainer's final/ export, or --dry-run to preview.")
249
  readme = card(cfg, man, aud, dict(run, params_total=n_p), a.repo, files, n_p)
250
- open(os.path.join(src, "README.md"), "w", encoding="utf-8").write(readme)
251
  print("card:", f"{n_p:,} params from the safetensors header; {len(files)} files hashed", flush=True)
252
  if a.dry_run:
 
 
253
  print(readme.encode("utf-8", "replace").decode("utf-8", "replace"))
254
  return
 
255
 
256
  api = HfApi(token=tok)
257
  create_repo(repo_id=a.repo, repo_type="model", private=False, exist_ok=True, token=tok)
258
  up = [(f, os.path.join(src, f)) for f in sorted(os.listdir(src))
259
  if os.path.isfile(os.path.join(src, f))]
 
260
  remote = {}
261
  try:
262
- for ent in api.model_info(a.repo, files_metadata=True).siblings or []:
263
- lfs = getattr(ent, "lfs", None)
264
- raw = (lfs.get("sha256") or lfs.get("oid")) if isinstance(lfs, dict) else None
265
- remote[ent.rfilename] = raw if isinstance(raw, str) and len(raw) == 64 else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  except Exception as e:
267
- print("(no listing:", type(e).__name__, ")", flush=True)
268
- todo = [f for f, p in up if remote.get(f) != sha(p)]
 
269
  print(f"uploading {len(todo)} of {len(up)} files to {a.repo}", flush=True)
270
  if todo:
271
  for i in range(0, len(todo), 120): # one commit per 120 files: E-033's rate ceiling
@@ -276,7 +325,11 @@ def main():
276
  back = set(api.list_repo_files(repo_id=a.repo, repo_type="model"))
277
  missing = sorted({f for f, _ in up} - back)
278
  R = {"repo": a.repo, "params": n_p, "tensors": n_t, "files": len(back), "missing": missing}
279
- for f in ("model.safetensors", "config.json"):
 
 
 
 
280
  try:
281
  h, nbytes = hashlib.sha256(), 0 # stream it: 1.7 GB does not belong in RAM
282
  with urllib.request.urlopen(f"https://huggingface.co/{a.repo}/resolve/main/{f}",
@@ -284,11 +337,14 @@ def main():
284
  for chunk in iter(lambda: r.read(1 << 20), b""):
285
  nbytes += len(chunk)
286
  h.update(chunk)
287
- R[f"anon_{f}"] = {"bytes": nbytes, "sha_ok": h.hexdigest() == sha(os.path.join(src, f))}
288
  except Exception as e:
289
- R[f"anon_{f}"] = {"error": f"{type(e).__name__}: {str(e)[:120]}"}
290
- R["published_verified"] = bool(not missing and all(R.get(f"anon_{x}", {}).get("sha_ok")
291
- for x in ("model.safetensors", "config.json")))
 
 
 
292
  print("PUBLISH_JSON_BEGIN")
293
  print(json.dumps(R, indent=1, default=str))
294
  print("PUBLISH_JSON_END")
 
74
  cf = man.get('contamination_filter') or {}
75
  tok_consumed = int(run.get("tokens_consumed") or 0)
76
  loss = " -> ".join(str(x) for x in (run.get("prior_loss"), run.get("final_loss")) if x is not None)
77
+ # Same rule as the architecture: anything the card states about *this* model comes from its own
78
+ # `run_summary.json`, and a field that is not there stops the publish rather than rendering as None.
79
+ # The Optimiser/Precision/Batch rows used to be typed prose, which is how a card ends up describing the
80
+ # recipe somebody meant to run (review E-045/2).
81
+ need = ("optim", "lr", "warmup_frac", "decay_frac", "world", "tokens_per_step", "seq_len",
82
+ "micro_batch", "accum", "attn", "grad_ckpt", "tok_per_s", "seed", "data_seed")
83
+ absent = [k for k in need if run.get(k) is None]
84
+ if absent:
85
+ raise SystemExit(f"run_summary.json carries no {absent}, so the card cannot state them without "
86
+ "guessing. Publish the summary the trainer wrote, or say in the card what is "
87
+ "unknown -- never both.")
88
+ geom = run["micro_batch"] * run["accum"] * run["world"] * run["seq_len"]
89
+ if geom != run["tokens_per_step"]:
90
+ raise SystemExit(f"the batch row would be a false statement: {run['micro_batch']} x "
91
+ f"{run['accum']} x {run['world']} x {run['seq_len']} = {geom} != "
92
+ f"tokens_per_step {run['tokens_per_step']}")
93
+ if run.get("val_ppl") is None and run.get("val_skipped") and run.get("val_loss_error") is None:
94
+ val_row = ("not measured on this leg -- a session that stops at a checkpoint boundary skips the "
95
+ "validation pass by design; the run's PPL comes from the horizon export")
96
+ elif run.get("val_ppl") is None:
97
+ val_row = f"NOT MEASURED ({run.get('val_loss_error')})"
98
+ else:
99
+ val_row = (f"**{run['val_ppl']:.2f}** on {MIX}'s held-out `val/` shards "
100
+ f"({man['val_tokens']:,} tokens, never trained on)")
101
  rows = "\n".join(f"| `{k}` | {v['sha256'][:16]}… | {v['bytes']:,} |" for k, v in sorted(files.items()))
102
  return f"""---
103
  license: cc-by-sa-4.0
 
122
  |---|---|
123
  | Parameters | **{n_params:,}** (recomputed from `model.safetensors`'s own header, not asserted) |
124
  | Architecture | {p[0]} layers, hidden {p[1]}, GQA {cfg["num_attention_heads"]} Q / {cfg["num_key_value_heads"]} KV heads, SwiGLU {cfg["intermediate_size"]}, RMSNorm {cfg["rms_norm_eps"]}, RoPE θ={rope}, **tied** embeddings, vocab {p[2]:,} |
125
+ | Sequence length | {run['seq_len']} — which is why ARC's 25-shot prompts are truncated from the left by the eval harness |
126
  | Tokens consumed | **{tok_consumed:,}** ({100.0 * tok_consumed / 1e9:.2f} % of the 1.0 B target) |
127
+ | Batch | micro {run['micro_batch']} × accum {run['accum']} × {run['world']} ranks = {run['tokens_per_step']:,} tokens/step |
128
+ | Optimiser | `{run['optim']}`, LR {run['lr']}, warmup fraction {run['warmup_frac']}, trapezoid decay over the final {100.0 * (1.0 - (run['decay_frac'] or 0)):.1f} % of the schedule |
129
+ | Precision | fp16 autocast + fp32 master weights + GradScaler — asserted at the start of every session, and each session's log line `precision: fp16=True bf16=False scaler=GradScaler` is the evidence |
130
+ | Throughput | {run['tok_per_s']:,} tok/s in the session that produced this export |
131
+ | Attention | `{run.get('effective_attn') or run['attn']}` |
132
+ | Gradient checkpointing | `{run['grad_ckpt']}` |
133
+ | Randomness | model seed {run['seed']}, data seed {run['data_seed']} |
134
  | Wall clock | {run.get('session_seconds', run.get('seconds', 0)) / 3600.0:.2f} h in the session that produced this export. The run is split across sessions at checkpoint boundaries, so this is **not** the total training time |
135
  | Loss | {loss} |
136
+ | Validation PPL | {val_row} |
137
 
138
  ## Data
139
 
 
210
  dest = "/tmp/cleanroom_" + os.path.basename(a.repo)
211
  shutil.rmtree(dest, ignore_errors=True)
212
  shutil.rmtree("/tmp/cleanroom_hf", ignore_errors=True)
213
+ # Gate 5's whole claim is "a stranger can load this". `install()` above puts HF_TOKEN in the
214
+ # environment and `huggingface_hub` picks it up from there, so without this the clean room would
215
+ # be authenticated and a private or gated repo would pass (review E-045/1).
216
+ for k in ("HF_TOKEN", "HUGGING_FACE_HUB_TOKEN", "HUGGING_FACE_TOKEN"):
217
+ os.environ.pop(k, None)
218
+ got = snapshot_download(a.repo, local_dir=dest, token=False)
219
+ print("clean-room download (anonymous) ->", got, flush=True)
220
  import json as _j
221
  cfg = _j.load(open(os.path.join(dest, "config.json")))
222
  n_t, n_p, dt = safetensors_counts(os.path.join(dest, "model.safetensors"))
 
277
  raise SystemExit("no run_summary.json next to the export: the card would be full of blanks. "
278
  "Pass --from-dir at the trainer's final/ export, or --dry-run to preview.")
279
  readme = card(cfg, man, aud, dict(run, params_total=n_p), a.repo, files, n_p)
 
280
  print("card:", f"{n_p:,} params from the safetensors header; {len(files)} files hashed", flush=True)
281
  if a.dry_run:
282
+ # A preview that writes into the export is not a preview: it leaves a README behind that the next
283
+ # real run then treats as one of the files to publish (review E-045, minor).
284
  print(readme.encode("utf-8", "replace").decode("utf-8", "replace"))
285
  return
286
+ open(os.path.join(src, "README.md"), "w", encoding="utf-8").write(readme)
287
 
288
  api = HfApi(token=tok)
289
  create_repo(repo_id=a.repo, repo_type="model", private=False, exist_ok=True, token=tok)
290
  up = [(f, os.path.join(src, f)) for f in sorted(os.listdir(src))
291
  if os.path.isfile(os.path.join(src, f))]
292
+ local_sha = {f: sha(p) for f, p in up}
293
  remote = {}
294
  try:
295
+ for attempt in (1, 2, 3):
296
+ try:
297
+ for ent in api.model_info(a.repo, files_metadata=True).siblings or []:
298
+ lfs = getattr(ent, "lfs", None)
299
+ raw = (lfs.get("sha256") or lfs.get("oid")) if isinstance(lfs, dict) else None
300
+ remote[ent.rfilename] = raw if isinstance(raw, str) and len(raw) == 64 else None
301
+ break
302
+ except Exception as e:
303
+ # E-034 again: "this repo has nothing in it yet" and "the Hub did not answer" are different
304
+ # facts. The first means upload everything; the second, swallowed into `remote = {}` here,
305
+ # means silently re-uploading 1.3 GB of bytes that are already on the Hub.
306
+ if type(e).__name__ in ("RepositoryNotFoundError", "GatedRepoError"):
307
+ print("(remote listing empty -- first publish)", flush=True)
308
+ break
309
+ if attempt == 3:
310
+ raise
311
+ print(f"(listing failed {type(e).__name__}, retry {attempt}/3)", flush=True)
312
+ import time as _t
313
+ _t.sleep(10 * attempt)
314
  except Exception as e:
315
+ raise SystemExit(f"cannot read the remote listing after 3 tries ({type(e).__name__}: "
316
+ f"{str(e)[:160]}) -- refusing to guess what is already uploaded")
317
+ todo = [f for f, _ in up if remote.get(f) != local_sha[f]]
318
  print(f"uploading {len(todo)} of {len(up)} files to {a.repo}", flush=True)
319
  if todo:
320
  for i in range(0, len(todo), 120): # one commit per 120 files: E-033's rate ceiling
 
325
  back = set(api.list_repo_files(repo_id=a.repo, repo_type="model"))
326
  missing = sorted({f for f, _ in up} - back)
327
  R = {"repo": a.repo, "params": n_p, "tensors": n_t, "files": len(back), "missing": missing}
328
+ # Every file, not just the weights and the config: `list_repo_files` proves a name exists, and a
329
+ # tokenizer whose bytes did not land loads as a *different* tokenizer, which is the failure that
330
+ # silently invalidates every number in Phase 6 (E-031's rule, applied to the model repo).
331
+ anon = {}
332
+ for f, _ in up:
333
  try:
334
  h, nbytes = hashlib.sha256(), 0 # stream it: 1.7 GB does not belong in RAM
335
  with urllib.request.urlopen(f"https://huggingface.co/{a.repo}/resolve/main/{f}",
 
337
  for chunk in iter(lambda: r.read(1 << 20), b""):
338
  nbytes += len(chunk)
339
  h.update(chunk)
340
+ anon[f] = {"bytes": nbytes, "sha_ok": h.hexdigest() == local_sha[f]}
341
  except Exception as e:
342
+ anon[f] = {"error": f"{type(e).__name__}: {str(e)[:120]}"}
343
+ R["anon_readback"] = {k: (v.get("sha_ok") if "sha_ok" in v else v.get("error")) for k, v in anon.items()}
344
+ bad = sorted(k for k, v in anon.items() if not v.get("sha_ok"))
345
+ R["published_verified"] = bool(not missing and not bad)
346
+ if bad:
347
+ R["anon_failed"] = bad[:10]
348
  print("PUBLISH_JSON_BEGIN")
349
  print(json.dumps(R, indent=1, default=str))
350
  print("PUBLISH_JSON_END")