review round 3 (probe/model-card/eval): rank-visible eval failure + cross-rank peak memory + complete run_summary; card derived-and-asserted from config; harness install --no-deps and task ids confirmed; torchrun redirects 3 + tee 3
Browse files- build/publish_model.py +46 -16
build/publish_model.py
CHANGED
|
@@ -13,6 +13,7 @@ import argparse
|
|
| 13 |
import hashlib
|
| 14 |
import json
|
| 15 |
import os
|
|
|
|
| 16 |
import struct
|
| 17 |
import sys
|
| 18 |
import urllib.request
|
|
@@ -60,6 +61,16 @@ def get_json(url, token=None):
|
|
| 60 |
|
| 61 |
def card(cfg, man, aud, run, repo, files, n_params):
|
| 62 |
p = cfg.get("num_hidden_layers"), cfg.get("hidden_size"), cfg.get("vocab_size")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
cf = man.get('contamination_filter') or {}
|
| 64 |
tok_consumed = int(run.get("tokens_consumed") or 0)
|
| 65 |
loss = " -> ".join(str(x) for x in (run.get("prior_loss"), run.get("final_loss")) if x is not None)
|
|
@@ -75,7 +86,7 @@ tags: [ounce100m, from-scratch, base, decoder-only, {man['tokenizer']['id'].spli
|
|
| 75 |
|
| 76 |
# {repo.split('/')[-1]}
|
| 77 |
|
| 78 |
-
A **{
|
| 79 |
{tok_consumed:,} tokens** of `{MIX}`, in {run.get('steps_planned', '?')} optimiser steps on
|
| 80 |
**2×Nvidia Tesla T4** inside Kaggle notebook sessions. It is the deliverable of a project whose whole point
|
| 81 |
was to find out what a ~100 M-parameter model reaches from ~1 B tokens of a curated public mix, and to
|
|
@@ -85,8 +96,8 @@ report it honestly, including where that is near chance.
|
|
| 85 |
|
| 86 |
| | |
|
| 87 |
|---|---|
|
| 88 |
-
| Parameters | **{n_params:,}** (
|
| 89 |
-
| Architecture | {p[0]} layers, hidden {p[1]}, GQA
|
| 90 |
| Sequence length | {run.get('seq_len')} — which is why ARC's 25-shot prompts are truncated from the left by the eval harness |
|
| 91 |
| Tokens consumed | **{tok_consumed:,}** ({100.0 * tok_consumed / 1e9:.2f} % of the 1.0 B target) |
|
| 92 |
| 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 |
|
|
@@ -95,9 +106,9 @@ report it honestly, including where that is near chance.
|
|
| 95 |
| Throughput | {run.get('tok_per_s'):,} tok/s sustained |
|
| 96 |
| Attention | `{run.get('effective_attn', run.get('attn'))}` — measured faster than SDPA on Turing |
|
| 97 |
| Gradient checkpointing | {run.get('grad_ckpt')} |
|
| 98 |
-
| Wall clock | {run.get('seconds', 0) / 3600.0:.2f} h
|
| 99 |
| Loss | {loss} |
|
| 100 |
-
| Validation PPL | {run.get('val_ppl')} on {MIX}'s held-out `val/` shards (
|
| 101 |
|
| 102 |
## Data
|
| 103 |
|
|
@@ -156,9 +167,15 @@ def main():
|
|
| 156 |
ap.add_argument("--dry-run", action="store_true")
|
| 157 |
a = ap.parse_args()
|
| 158 |
|
|
|
|
| 159 |
import ounce100m_credentials
|
| 160 |
-
|
| 161 |
tok = os.environ["HF_TOKEN"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
from huggingface_hub import HfApi, create_repo, snapshot_download
|
| 163 |
|
| 164 |
man = get_json(f"https://huggingface.co/datasets/{MIX}/resolve/main/manifest.json")
|
|
@@ -166,7 +183,8 @@ def main():
|
|
| 166 |
|
| 167 |
if a.verify_only:
|
| 168 |
dest = "/tmp/cleanroom_" + os.path.basename(a.repo)
|
| 169 |
-
|
|
|
|
| 170 |
got = snapshot_download(a.repo, local_dir=dest)
|
| 171 |
print("clean-room download ->", got, flush=True)
|
| 172 |
import json as _j
|
|
@@ -176,7 +194,10 @@ def main():
|
|
| 176 |
import torch
|
| 177 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 178 |
tkn = AutoTokenizer.from_pretrained(dest)
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
| 180 |
assert m.num_parameters() == n_p, (m.num_parameters(), n_p)
|
| 181 |
assert cfg["tie_word_embeddings"] and m.get_input_embeddings().weight.data_ptr() == \
|
| 182 |
m.get_output_embeddings().weight.data_ptr(), "tied embeddings did not survive the round trip"
|
|
@@ -212,19 +233,24 @@ def main():
|
|
| 212 |
if os.path.exists(os.path.join(src, "run_summary.json")):
|
| 213 |
run = json.load(open(os.path.join(src, "run_summary.json")))
|
| 214 |
n_t, n_p, dt = safetensors_counts(os.path.join(src, "model.safetensors"))
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
files = {f: {"sha256": sha(os.path.join(src, f)), "bytes": os.path.getsize(os.path.join(src, f))}
|
| 218 |
for f in sorted(have) if f in CARD_FILES or f.endswith(".json")}
|
| 219 |
print("tensors", n_t, "params", f"{n_p:,}", "dtypes", dt, flush=True)
|
| 220 |
if not a.dry_run and not run:
|
| 221 |
raise SystemExit("no run_summary.json next to the export: the card would be full of blanks. "
|
| 222 |
"Pass --from-dir at the trainer's final/ export, or --dry-run to preview.")
|
| 223 |
-
readme = card(cfg, man, aud, dict(run,
|
| 224 |
-
open(os.path.join(src, "README.md"), "w").write(readme)
|
| 225 |
print("card:", f"{n_p:,} params from the safetensors header; {len(files)} files hashed", flush=True)
|
| 226 |
if a.dry_run:
|
| 227 |
-
print(readme)
|
| 228 |
return
|
| 229 |
|
| 230 |
api = HfApi(token=tok)
|
|
@@ -252,9 +278,13 @@ def main():
|
|
| 252 |
R = {"repo": a.repo, "params": n_p, "tensors": n_t, "files": len(back), "missing": missing}
|
| 253 |
for f in ("model.safetensors", "config.json"):
|
| 254 |
try:
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
except Exception as e:
|
| 259 |
R[f"anon_{f}"] = {"error": f"{type(e).__name__}: {str(e)[:120]}"}
|
| 260 |
R["published_verified"] = bool(not missing and all(R.get(f"anon_{x}", {}).get("sha_ok")
|
|
|
|
| 13 |
import hashlib
|
| 14 |
import json
|
| 15 |
import os
|
| 16 |
+
import shutil
|
| 17 |
import struct
|
| 18 |
import sys
|
| 19 |
import urllib.request
|
|
|
|
| 61 |
|
| 62 |
def card(cfg, man, aud, run, repo, files, n_params):
|
| 63 |
p = cfg.get("num_hidden_layers"), cfg.get("hidden_size"), cfg.get("vocab_size")
|
| 64 |
+
# The card states the architecture; half of it was typed rather than read, so a variant export would
|
| 65 |
+
# have published a description of a model it is not. Derive it and refuse on drift.
|
| 66 |
+
frozen = {"num_hidden_layers": 22, "hidden_size": 576, "intermediate_size": 1536,
|
| 67 |
+
"num_attention_heads": 9, "num_key_value_heads": 3, "vocab_size": 49152,
|
| 68 |
+
"tie_word_embeddings": True, "rms_norm_eps": 1e-5}
|
| 69 |
+
drift = {k: (cfg.get(k), v) for k, v in frozen.items() if cfg.get(k) != v}
|
| 70 |
+
if drift:
|
| 71 |
+
raise SystemExit("config.json does not match the frozen architecture, so the card would be a false "
|
| 72 |
+
"statement about this model: " + json.dumps(drift, default=str)[:600])
|
| 73 |
+
rope = cfg.get("rope_theta") or (cfg.get("rope_parameters") or {}).get("rope_theta")
|
| 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)
|
|
|
|
| 86 |
|
| 87 |
# {repo.split('/')[-1]}
|
| 88 |
|
| 89 |
+
A **{n_params:,}-parameter decoder-only transformer trained from scratch on
|
| 90 |
{tok_consumed:,} tokens** of `{MIX}`, in {run.get('steps_planned', '?')} optimiser steps on
|
| 91 |
**2×Nvidia Tesla T4** inside Kaggle notebook sessions. It is the deliverable of a project whose whole point
|
| 92 |
was to find out what a ~100 M-parameter model reaches from ~1 B tokens of a curated public mix, and to
|
|
|
|
| 96 |
|
| 97 |
| | |
|
| 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 |
|
|
|
|
| 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 |
|
|
|
|
| 167 |
ap.add_argument("--dry-run", action="store_true")
|
| 168 |
a = ap.parse_args()
|
| 169 |
|
| 170 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # E-028
|
| 171 |
import ounce100m_credentials
|
| 172 |
+
ounce100m_credentials.install(verify=True)
|
| 173 |
tok = os.environ["HF_TOKEN"]
|
| 174 |
+
# Order matters: huggingface_hub resolves its cache locations at import time, so a "clean room" that
|
| 175 |
+
# sets HF_HOME afterwards is testing Kaggle's warm disk instead of the Hub (review finding A6).
|
| 176 |
+
if a.verify_only:
|
| 177 |
+
os.environ["HF_HOME"] = "/tmp/cleanroom_hf"
|
| 178 |
+
os.environ["HF_HUB_CACHE"] = "/tmp/cleanroom_hf/hub"
|
| 179 |
from huggingface_hub import HfApi, create_repo, snapshot_download
|
| 180 |
|
| 181 |
man = get_json(f"https://huggingface.co/datasets/{MIX}/resolve/main/manifest.json")
|
|
|
|
| 183 |
|
| 184 |
if a.verify_only:
|
| 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
|
|
|
|
| 194 |
import torch
|
| 195 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 196 |
tkn = AutoTokenizer.from_pretrained(dest)
|
| 197 |
+
try: # transformers 5 renamed torch_dtype; Gate 5 is this very call
|
| 198 |
+
m = AutoModelForCausalLM.from_pretrained(dest, dtype=torch.float16)
|
| 199 |
+
except TypeError:
|
| 200 |
+
m = AutoModelForCausalLM.from_pretrained(dest, torch_dtype=torch.float16)
|
| 201 |
assert m.num_parameters() == n_p, (m.num_parameters(), n_p)
|
| 202 |
assert cfg["tie_word_embeddings"] and m.get_input_embeddings().weight.data_ptr() == \
|
| 203 |
m.get_output_embeddings().weight.data_ptr(), "tied embeddings did not survive the round trip"
|
|
|
|
| 233 |
if os.path.exists(os.path.join(src, "run_summary.json")):
|
| 234 |
run = json.load(open(os.path.join(src, "run_summary.json")))
|
| 235 |
n_t, n_p, dt = safetensors_counts(os.path.join(src, "model.safetensors"))
|
| 236 |
+
# `params` in run_summary.json is a dict {sum_numel, unique, trainable}; comparing that to an integer
|
| 237 |
+
# is always unequal and would have failed every real export (review finding A1).
|
| 238 |
+
claim = run.get("params_total") or run.get("params")
|
| 239 |
+
if isinstance(claim, dict):
|
| 240 |
+
claim = claim.get("sum_numel")
|
| 241 |
+
if claim and int(claim) != n_p:
|
| 242 |
+
raise SystemExit(f"run_summary claims {claim} params, the published safetensors file has {n_p}")
|
| 243 |
files = {f: {"sha256": sha(os.path.join(src, f)), "bytes": os.path.getsize(os.path.join(src, f))}
|
| 244 |
for f in sorted(have) if f in CARD_FILES or f.endswith(".json")}
|
| 245 |
print("tensors", n_t, "params", f"{n_p:,}", "dtypes", dt, flush=True)
|
| 246 |
if not a.dry_run and not run:
|
| 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)
|
|
|
|
| 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}",
|
| 283 |
+
timeout=900) as r:
|
| 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")
|