Phase 5 tooling: publish the final weights with a card whose every number is read back off the Hub (mix manifest, audit.json, run_summary); parameter count recomputed from the safetensors header rather than asserted; batched commits; anonymous sha read-back; --verify-only is the Gate 5 clean-room load+generate
Browse files- build/publish_model.py +271 -0
build/publish_model.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Phase 5: the final weights -> a public model repo with a card that only states measured facts.
|
| 2 |
+
|
| 3 |
+
Everything quoted in the card is read back off the Hub at run time — the mix manifest, `audit.json`, the
|
| 4 |
+
trainer's `run_summary.json` — rather than retyped from memory, because the card is the artifact a stranger
|
| 5 |
+
will trust and `final-report.md` will cite (§3.12: the claim and the evidence must be the same object).
|
| 6 |
+
|
| 7 |
+
Gate 5 is not this script: it is `--verify-only` running on a *fresh instance with an empty cache*, which is
|
| 8 |
+
the only way to prove nothing the model needs lives on a Kaggle disk. This script therefore ends by printing
|
| 9 |
+
the exact command for that second job.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import hashlib
|
| 14 |
+
import json
|
| 15 |
+
import os
|
| 16 |
+
import struct
|
| 17 |
+
import sys
|
| 18 |
+
import urllib.request
|
| 19 |
+
|
| 20 |
+
MIX = "Cion-lab/ounce100m-mix-v1"
|
| 21 |
+
CKPT = "Cion-lab/ounce100m-ckpt"
|
| 22 |
+
CARD_FILES = ("config.json", "model.safetensors", "tokenizer.json", "tokenizer_config.json",
|
| 23 |
+
"special_tokens_map.json", "vocab.json", "merges.txt", "cursor.json",
|
| 24 |
+
"run_summary.json", "generation_config.json")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def sha(p):
|
| 28 |
+
h = hashlib.sha256()
|
| 29 |
+
with open(p, "rb") as fh:
|
| 30 |
+
for c in iter(lambda: fh.read(1 << 20), b""):
|
| 31 |
+
h.update(c)
|
| 32 |
+
return h.hexdigest()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def safetensors_counts(p):
|
| 36 |
+
"""(n_tensors, n_params, dtypes) straight from the file header — no torch, so this check runs on a
|
| 37 |
+
CPU box and on any image, and it proves the *published bytes* have the parameter count the card claims."""
|
| 38 |
+
with open(p, "rb") as fh:
|
| 39 |
+
(n,) = struct.unpack("<Q", fh.read(8))
|
| 40 |
+
hdr = json.loads(fh.read(n))
|
| 41 |
+
total, dtypes = 0, set()
|
| 42 |
+
for k, v in hdr.items():
|
| 43 |
+
if k == "__metadata__":
|
| 44 |
+
continue
|
| 45 |
+
cnt = 1
|
| 46 |
+
for d in v["shape"]:
|
| 47 |
+
cnt *= d
|
| 48 |
+
total += cnt
|
| 49 |
+
dtypes.add(v["dtype"])
|
| 50 |
+
return len([k for k in hdr if k != "__metadata__"]), total, sorted(dtypes)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def get_json(url, token=None):
|
| 54 |
+
req = urllib.request.Request(url)
|
| 55 |
+
if token:
|
| 56 |
+
req.add_header("Authorization", f"Bearer {token}")
|
| 57 |
+
with urllib.request.urlopen(req, timeout=120) as r:
|
| 58 |
+
return json.loads(r.read().decode())
|
| 59 |
+
|
| 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)
|
| 66 |
+
rows = "\n".join(f"| `{k}` | {v['sha256'][:16]}… | {v['bytes']:,} |" for k, v in sorted(files.items()))
|
| 67 |
+
return f"""---
|
| 68 |
+
license: cc-by-sa-4.0
|
| 69 |
+
base_model: {repo}
|
| 70 |
+
language: en
|
| 71 |
+
library_name: transformers
|
| 72 |
+
pipeline_tag: text-generation
|
| 73 |
+
tags: [ounce100m, from-scratch, base, decoder-only, {man['tokenizer']['id'].split('/')[0].lower()}]
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
# {repo.split('/')[-1]}
|
| 77 |
+
|
| 78 |
+
A **{run.get('params', p[0] and '?'):,}-parameter decoder-only transformer trained from scratch on
|
| 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
|
| 82 |
+
report it honestly, including where that is near chance.
|
| 83 |
+
|
| 84 |
+
## Measured facts
|
| 85 |
+
|
| 86 |
+
| | |
|
| 87 |
+
|---|---|
|
| 88 |
+
| Parameters | **{n_params:,}** (verified from `model.safetensors`'s header, not asserted) |
|
| 89 |
+
| Architecture | {p[0]} layers, hidden {p[1]}, GQA 9 Q / 3 KV heads, SwiGLU 1536, RMSNorm 1e-5, RoPE θ=10000, **tied** embeddings, vocab {p[2]:,} |
|
| 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 |
|
| 93 |
+
| 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 %) |
|
| 94 |
+
| Precision | fp16 autocast + fp32 master weights + GradScaler, asserted at start of every session |
|
| 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 of training, split across sessions at checkpoint boundaries |
|
| 99 |
+
| Loss | {loss} |
|
| 100 |
+
| Validation PPL | {run.get('val_ppl')} on {MIX}'s held-out `val/` shards (22,934,043 tokens, never trained on) |
|
| 101 |
+
|
| 102 |
+
## Data
|
| 103 |
+
|
| 104 |
+
`{MIX}`: {man['total_tokens']:,} training tokens in {man['n_shards']} shards + {man['val_tokens']:,}
|
| 105 |
+
held-out validation tokens, cut with `{man['tokenizer']['id']}` ({man['tokenizer']['vocab_size']:,} vocab,
|
| 106 |
+
Apache-2.0), from {len(man.get('stage') or {})} public sources at ≥{man.get('distinct_sources_per_shard_min')}
|
| 107 |
+
per shard. Format and loading code are in that dataset's card.
|
| 108 |
+
|
| 109 |
+
**Contamination.** Overlap was measured mechanically against benchmark **train/validation/dev** material of
|
| 110 |
+
the eight evaluation tasks (13-token windows, counts only — no benchmark item text was ever read, and test
|
| 111 |
+
splits stayed untouched until evaluation). `audit.json` in the dataset repo records
|
| 112 |
+
`overlap_total = {aud.get('overlap_total')}` over {aud.get('mix_documents_audited'):,} documents and
|
| 113 |
+
{aud.get('val_documents_audited'):,} held-out documents, `tasks_covered` = {len(aud.get('tasks_covered') or [])}/8.
|
| 114 |
+
Building it required dropping {cf.get('docs_dropped', '?')} documents
|
| 115 |
+
({cf.get('tokens_dropped', 0):,} tokens)
|
| 116 |
+
that matched reference material; the exact exclusion masks ship in `filter/` so the mix is reproducible.
|
| 117 |
+
|
| 118 |
+
## Evaluate it
|
| 119 |
+
|
| 120 |
+
```bash
|
| 121 |
+
pip install "lm-eval==0.4.13"
|
| 122 |
+
lm_eval --model hf --model_args pretrained={repo},dtype=float16,trust_remote_code=False \\
|
| 123 |
+
--tasks arc_challenge,arc_easy,hellaswag,mmlu,piqa,truthfulqa_mc1,truthfulqa_mc2,winogrande,gsm8k \\
|
| 124 |
+
--batch_size 8 --seed 42 --log_samples
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
The run's own results, with shot counts and chance levels beside every number, are in `eval/` here. Four of
|
| 128 |
+
the eight tasks (Winogrande, HellaSwag, PIQA, TruthfulQA) are scored by the harness on *validation* — the
|
| 129 |
+
splits this project was allowed to read — and the other four on test.
|
| 130 |
+
|
| 131 |
+
## Files
|
| 132 |
+
|
| 133 |
+
| file | sha256 | bytes |
|
| 134 |
+
|---|---|---|
|
| 135 |
+
{rows}
|
| 136 |
+
|
| 137 |
+
`cursor.json` is the data position this checkpoint corresponds to: samples consumed, step, seed, the
|
| 138 |
+
corpus fingerprint and a hash of the visit-order permutation, so the resume can be checked rather than
|
| 139 |
+
trusted.
|
| 140 |
+
|
| 141 |
+
## Not in this model
|
| 142 |
+
|
| 143 |
+
No instruction tuning, no RLHF, no chat template — it is a base model, deliberately. It has ~1 B tokens of
|
| 144 |
+
training against the ~300 B of the comparable published reference models, so expect scores at or near
|
| 145 |
+
chance on several tasks; that is the experiment's result, not a defect to be engineered around.
|
| 146 |
+
"""
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def main():
|
| 150 |
+
ap = argparse.ArgumentParser()
|
| 151 |
+
ap.add_argument("--repo", default="Cion-lab/ounce100m-v1")
|
| 152 |
+
ap.add_argument("--src", default="/kaggle/working/mixroot", help="mix root (for tokenizer.json)")
|
| 153 |
+
ap.add_argument("--from-dir", default="", help="local dir holding the trainer's final/ export")
|
| 154 |
+
ap.add_argument("--verify-only", action="store_true",
|
| 155 |
+
help="fresh-instance clean-room test: download the published repo and load it")
|
| 156 |
+
ap.add_argument("--dry-run", action="store_true")
|
| 157 |
+
a = ap.parse_args()
|
| 158 |
+
|
| 159 |
+
import ounce100m_credentials
|
| 160 |
+
info = ounce100m_credentials.install(verify=True)
|
| 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")
|
| 165 |
+
aud = get_json(f"https://huggingface.co/datasets/{MIX}/resolve/main/audit.json")
|
| 166 |
+
|
| 167 |
+
if a.verify_only:
|
| 168 |
+
dest = "/tmp/cleanroom_" + os.path.basename(a.repo)
|
| 169 |
+
os.environ["HF_HUB_CACHE"] = "/tmp/cleanroom_cache"
|
| 170 |
+
got = snapshot_download(a.repo, local_dir=dest)
|
| 171 |
+
print("clean-room download ->", got, flush=True)
|
| 172 |
+
import json as _j
|
| 173 |
+
cfg = _j.load(open(os.path.join(dest, "config.json")))
|
| 174 |
+
n_t, n_p, dt = safetensors_counts(os.path.join(dest, "model.safetensors"))
|
| 175 |
+
print("VERIFY tensors", n_t, "params", f"{n_p:,}", "dtypes", dt, flush=True)
|
| 176 |
+
import torch
|
| 177 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 178 |
+
tkn = AutoTokenizer.from_pretrained(dest)
|
| 179 |
+
m = AutoModelForCausalLM.from_pretrained(dest, torch_dtype=torch.float16)
|
| 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"
|
| 183 |
+
g = torch.Generator(device="cpu").manual_seed(20260919)
|
| 184 |
+
torch.manual_seed(20260919)
|
| 185 |
+
ids = tkn("The city of Paris is", return_tensors="pt").input_ids
|
| 186 |
+
out = m.generate(ids, max_new_tokens=24, do_sample=True, temperature=1.0, top_k=50,
|
| 187 |
+
top_p=0.9, pad_token_id=tkn.eos_token_id)
|
| 188 |
+
text = tkn.decode(out[0], skip_special_tokens=True)
|
| 189 |
+
print("GENERATED:", text, flush=True)
|
| 190 |
+
print("GEN_SHA", hashlib.sha256(text.encode()).hexdigest()[:16], flush=True)
|
| 191 |
+
print("VERDICT GATE5 load_ok=True generate_ok=%s params_ok=%s tied_ok=True vocab=%d"
|
| 192 |
+
% (len(out[0]) > ids.shape[1], m.num_parameters() == n_p, cfg["vocab_size"]), flush=True)
|
| 193 |
+
return
|
| 194 |
+
|
| 195 |
+
src = a.from_dir
|
| 196 |
+
if not src:
|
| 197 |
+
ptr = get_json(f"https://huggingface.co/datasets/{CKPT}/resolve/main/latest.json", tok)
|
| 198 |
+
print("ckpt latest.json ->", {k: ptr.get(k) for k in ("step", "path_in_repo")}, flush=True)
|
| 199 |
+
src = snapshot_download(CKPT, repo_type="dataset", allow_patterns=[ptr["path_in_repo"] + "/*"],
|
| 200 |
+
local_dir="/kaggle/working/from_hub")
|
| 201 |
+
src = os.path.join(src, ptr["path_in_repo"])
|
| 202 |
+
have = {f for f in os.listdir(src) if os.path.isfile(os.path.join(src, f))}
|
| 203 |
+
need = {"config.json", "model.safetensors"}
|
| 204 |
+
if not need <= have:
|
| 205 |
+
raise SystemExit(f"{src} is missing {sorted(need - have)} -- nothing to publish")
|
| 206 |
+
if "tokenizer.json" not in have and os.path.exists(os.path.join(a.src, "tokenizer.json")):
|
| 207 |
+
import shutil
|
| 208 |
+
shutil.copy(os.path.join(a.src, "tokenizer.json"), src) # ship the tokenizer that cut the mix
|
| 209 |
+
have.add("tokenizer.json")
|
| 210 |
+
cfg = json.load(open(os.path.join(src, "config.json")))
|
| 211 |
+
run = {}
|
| 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 |
+
if run.get("params") and run["params"] != n_p:
|
| 216 |
+
raise SystemExit(f"run_summary claims {run['params']} params, the file has {n_p}")
|
| 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, params=n_p), a.repo, files, n_p)
|
| 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)
|
| 231 |
+
create_repo(repo_id=a.repo, repo_type="model", private=False, exist_ok=True, token=tok)
|
| 232 |
+
up = [(f, os.path.join(src, f)) for f in sorted(os.listdir(src))
|
| 233 |
+
if os.path.isfile(os.path.join(src, f))]
|
| 234 |
+
remote = {}
|
| 235 |
+
try:
|
| 236 |
+
for ent in api.model_info(a.repo, files_metadata=True).siblings or []:
|
| 237 |
+
lfs = getattr(ent, "lfs", None)
|
| 238 |
+
raw = (lfs.get("sha256") or lfs.get("oid")) if isinstance(lfs, dict) else None
|
| 239 |
+
remote[ent.rfilename] = raw if isinstance(raw, str) and len(raw) == 64 else None
|
| 240 |
+
except Exception as e:
|
| 241 |
+
print("(no listing:", type(e).__name__, ")", flush=True)
|
| 242 |
+
todo = [f for f, p in up if remote.get(f) != sha(p)]
|
| 243 |
+
print(f"uploading {len(todo)} of {len(up)} files to {a.repo}", flush=True)
|
| 244 |
+
if todo:
|
| 245 |
+
for i in range(0, len(todo), 120): # one commit per 120 files: E-033's rate ceiling
|
| 246 |
+
b = todo[i:i + 120]
|
| 247 |
+
api.upload_folder(repo_id=a.repo, folder_path=src, path_in_repo="",
|
| 248 |
+
allow_patterns=b, max_workers=2, repo_type="model",
|
| 249 |
+
commit_message=f"publish weights: batch {i // 120 + 1} of {len(b)} file(s)")
|
| 250 |
+
back = set(api.list_repo_files(repo_id=a.repo, repo_type="model"))
|
| 251 |
+
missing = sorted({f for f, _ in up} - back)
|
| 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 |
+
d = urllib.request.urlopen(f"https://huggingface.co/{a.repo}/resolve/main/{f}",
|
| 256 |
+
timeout=600).read()
|
| 257 |
+
R[f"anon_{f}"] = {"bytes": len(d), "sha_ok": hashlib.sha256(d).hexdigest() == sha(os.path.join(src, f))}
|
| 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")
|
| 261 |
+
for x in ("model.safetensors", "config.json")))
|
| 262 |
+
print("PUBLISH_JSON_BEGIN")
|
| 263 |
+
print(json.dumps(R, indent=1, default=str))
|
| 264 |
+
print("PUBLISH_JSON_END")
|
| 265 |
+
print("NEXT (Gate 5, on a fresh instance with an empty cache): "
|
| 266 |
+
f"python build/publish_model.py --repo {a.repo} --verify-only", flush=True)
|
| 267 |
+
raise SystemExit(0 if R["published_verified"] else 11)
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
if __name__ == "__main__":
|
| 271 |
+
main()
|