| |
| """Stage (hardlinks), upload, and byte-verify the two Nex-N2.5-mini repos. |
| usage: nex_publish.py stage|upload|verify|readme <std|imat> [README path] |
| Upload through upload_watchdog.sh (40G cap, stall kill + retry) - see nex_cards.sh upload |
| Env: default HF_HOME (token); HF_XET_CACHE / TMPDIR pinned to /mnt/models by the caller.""" |
| import hashlib, json, os, shutil, sys, urllib.request |
|
|
| W = "/mnt/models/nex-n2.5-mini"; N = "Nex-N2.5-mini"; U = "kingjones777" |
| REPOS = {"std": f"{U}/{N}-ROCmFP4-GGUF", "imat": f"{U}/{N}-ROCmFP4-imatrix-GGUF"} |
|
|
|
|
| def pubname(fn): |
| """Published filename: the quant token must be hyphen-delimited or the Hub cannot parse the variant |
| (`Q4_0_ROCMFP4_STRIX_LEAN` reads as the garbage label `Q4_0_ROCMFP`).""" |
| return fn.replace("Q4_0_ROCMFP4_", "Q4_0-ROCmFP4-") |
|
|
|
|
| MMPROJ = {f"mmproj-{N}-BF16.gguf": (f"{W}/out", f"mmproj-{N}-BF16.gguf")} |
| |
| |
| TPL = {"chat_template_enable_thinking.jinja": (f"{W}/tpl", "chat_template_enable_thinking.jinja")} |
| TIERS = ("Q4_0_ROCMFP4_STRIX_LEAN", "Q4_0_ROCMFP4_COHERENT", "Q4_0_ROCMFP4_FAST") |
| FILES = { |
| "std": {**{pubname(f"{N}-{t}.gguf"): (f"{W}/out", f"{N}-{t}.gguf") for t in TIERS}, **MMPROJ, **TPL}, |
| "imat": {**{pubname(f"{N}-imatrix-{t}.gguf"): (f"{W}/out-imat", f"{N}-imatrix-{t}.gguf") for t in TIERS}, |
| f"{N}.imatrix": (f"{W}/imat", f"{N}.imatrix"), **MMPROJ, **TPL}, |
| } |
| P = "recipe/pipeline/" |
| SCRIPTS = ("nex_download.sh", "verify_download.py", "nex_phase1.sh", "nex_phase2.sh", "diag_bf16.sh", "nex_phase2b.sh", |
| "nex_harness.py", "nex_bench.py", "nex_sizing.sh", |
| "nex_cachegate.py", "readback.py", "nex_aggregate.py", "nex_judge.py", "nex_render.py", "nex_publish.py", |
| "nex_cards.sh", "upload_watchdog.sh", "nex_seats.sh", |
| |
| "diag_tools_run.sh", "nex_tools_diag.py", "nex_reasoning_probe.py", "nex_tools_tpl.py", |
| "nex_seat_default_probe.py", "run_tools_c1.sh", "run_tools_roff.sh", "nex_refresh_cards.sh", "nex_publish_finish.sh") |
| LOGS = ( |
| "D2_verify_download.log", "phase1.log", "phase2.log", "Q_sizes.log", "N6_bench.log", "N7_sizing.log", |
| "b_n-vision-q106-faon.log", "b_n-vision-q106-faoff.log", |
| "C1_convert.log", "C2_mmproj.log", "C_readback.log", |
| "Q1_q106.log", "Q1_q102.log", "Q1_q103.log", "Q_readback.log", |
| "N1c_ppl_bf16_cpu.log", "N2c_imatrix_cpu.log", |
| |
| "N1_ppl_bf16.log", "diag_ppl_q106_rocm_c4.log", "diag_bf16.log", "diag_bf16_rocm_faoff.log", "diag_bf16_vk_faon.log", |
| "diag_q106_vk_faon.log", "diag_bf16_cpu.log", "diag_bf16_purecpu_c1.log", |
| "N3_q106i.log", "N3_q102i.log", "N3_q103i.log", "N3_readback.log", |
| *(f"{p}_kld_{t}.log" for p in ("N4", "N4v") for t in ("q106", "q102", "q103", "q106i", "q102i", "q103i")), |
| "N5_kld_q106_repeat.log", "N5v_kld_q106_repeat.log", "b_n-c3-q106.log", |
| "b_n-tools-q106.log", "diag_tools.log", "diag_tools_server.log", |
| "probe_reasoning.log", *(f"probe_reasoning_{c}.log" for c in |
| ("default", "fmt-deepseek", "srv-kwargs-high", "reasoning-on", "tpl-enable-thinking")), |
| "N6t_tools_tpl.log", "b_n-tools-q106-tpl.log", "b_n-tools-q106-tpl-probe.log", |
| "N6t_tools_tpl_medium.log", "b_n-tools-q106-tpl-medium.log", "b_n-tools-q106-tpl-medium-probe.log", |
| "probe_seat_default.log", "probe_seat_default_C1.log", "probe_seat_default_C2.log", |
| "N6t_tools_c1.log", "b_n-tools-q106-c1.log", "b_n-tools-q106-c1-probe.log", "b_n-vision-q106-c1-faon.log", |
| "N6t_tools_roff.log", "b_n-tools-q106-roff.log", "b_n-tools-q106-roff-r2.log", "b_n-tools-q106-roff-r3.log", |
| "b_n-tools-q106-roff-probe.log", "b_n-vision-q106-roff-faon.log", |
| "N8_unice.log", "N8a_seats.log", "N8b_seats.log", "N8c_seats.log", "N8d_seats.log", |
| ) |
| RAW = ("nex_repeat.jsonl", "nex_reference.jsonl", "nex_bench.jsonl", "nex_sizing.jsonl", "hub_check.json", |
| "nex_tools_diag.json", "nex_reasoning_probe.json", "nex_seat_default_probe.json", "nex_template_shim.json", |
| "nex_template_shim_medium.json", "nex_template_shim_c1.json", "nex_template_shim_roff.json", |
| "nex_seats.jsonl", "nex_seats_plan.json") |
| TEMPLATES = ("chat_template_enable_thinking_medium.jinja", "chat_template_enable_thinking_v2.jinja") |
| RECIPE = { |
| "recipe/results_summary.json": "results/summary.json", |
| **{P + x: x for x in SCRIPTS}, |
| **{f"recipe/raw/{x}": f"results/{x}" for x in RAW}, |
| **{f"recipe/logs/{x}": f"logs/{x}" for x in LOGS}, |
| **{f"recipe/templates/{x}": f"tpl/{x}" for x in TEMPLATES}, |
| "recipe/templates/chat_template_stock.jinja": "hf/chat_template.jinja", |
| } |
|
|
|
|
| def separation_errors(kind, names): |
| """The imatrix build is its OWN repo (King): no imatrix weights in the standard repo, no standard weights in the |
| imatrix repo. The vision projector is the only model file both carry.""" |
| errs = [] |
| for fn in names: |
| if not fn.endswith(".gguf") or fn.startswith("mmproj-"): |
| continue |
| is_imat = "-imatrix-" in fn |
| if kind == "std" and is_imat: |
| errs.append(f"imatrix model file in the standard repo: {fn}") |
| if kind == "imat" and not is_imat: |
| errs.append(f"standard model file in the imatrix repo: {fn}") |
| if kind == "imat" and f"{N}.imatrix" not in names: |
| errs.append("imatrix repo is missing the .imatrix file") |
| return errs |
|
|
|
|
| assert REPOS["std"] != REPOS["imat"] |
| assert set(FILES["std"]) & set(FILES["imat"]) == set(MMPROJ) | set(TPL), "only the projector + template may be shared" |
| for _k in FILES: |
| assert not separation_errors(_k, list(FILES[_k])), separation_errors(_k, list(FILES[_k])) |
|
|
|
|
| def git_blob_sha1(p): |
| """What the Hub reports as `oid` for a file stored in plain git (no LFS/Xet pointer).""" |
| h = hashlib.sha1(b"blob %d\0" % os.path.getsize(p)) |
| with open(p, "rb") as fh: |
| for b in iter(lambda: fh.read(64 << 20), b""): |
| h.update(b) |
| return h.hexdigest() |
|
|
|
|
| def sha(p): |
| h = hashlib.sha256() |
| with open(p, "rb") as fh: |
| for b in iter(lambda: fh.read(64 << 20), b""): |
| h.update(b) |
| return h.hexdigest() |
|
|
|
|
| kind = sys.argv[2]; stage = f"{W}/hf-upload/{kind}"; repo = REPOS[kind] |
| if sys.argv[1] == "stage": |
| missing = [f"{d}/{src}" for fn, (d, src) in FILES[kind].items() if not os.path.exists(f"{d}/{src}")] + \ |
| [src for src in RECIPE.values() if not os.path.exists(f"{W}/{src}")] |
| if missing: |
| raise SystemExit(f"STAGE ABORT - missing: {missing}") |
| shutil.rmtree(stage, ignore_errors=True) |
| for fn, (d, src) in FILES[kind].items(): |
| os.makedirs(stage, exist_ok=True); os.link(f"{d}/{src}", f"{stage}/{fn}") |
| for dst, src in RECIPE.items(): |
| os.makedirs(os.path.dirname(f"{stage}/{dst}"), exist_ok=True); shutil.copy2(f"{W}/{src}", f"{stage}/{dst}") |
| sums = {fn: sha(f"{stage}/{fn}") for fn in sorted(FILES[kind])} |
| with open(f"{stage}/SHA256SUMS", "w") as fh: |
| for fn, h in sums.items(): |
| fh.write(f"{h} {fn}\n") |
| sizes = {x: os.path.getsize(f"{stage}/{x}") for x in sums} |
| json.dump(sizes, open(f"{stage}/../staged_sizes_{kind}.json", "w")) |
| print(json.dumps({"stage": stage, "files": len(sums), "bytes": sum(sizes.values()), "recipe_files": len(RECIPE)})) |
| elif sys.argv[1] == "upload": |
| from huggingface_hub import HfApi |
| api = HfApi() |
| print("whoami:", api.whoami()["name"], flush=True) |
| api.create_repo(repo, repo_type="model", private=False, exist_ok=True) |
| api.upload_large_folder(repo_id=repo, repo_type="model", folder_path=stage, |
| ignore_patterns=["README.md", ".cache/**"], num_workers=2) |
| print("UPLOAD_DONE", repo, flush=True) |
| elif sys.argv[1] == "verify": |
| tree = json.load(urllib.request.urlopen(f"https://huggingface.co/api/models/{repo}/tree/main?recursive=true", timeout=60)) |
| remote = {t["path"]: t for t in tree if t.get("type") == "file"} |
| local = dict(l.split()[::-1] for l in open(f"{stage}/SHA256SUMS").read().splitlines()) |
| bad = [] |
| for fn, h in local.items(): |
| r = remote.get(fn) |
| if not r: |
| bad.append((fn, "MISSING remote")); continue |
| if r["size"] != os.path.getsize(f"{stage}/{fn}"): |
| bad.append((fn, f"size {r['size']}")); continue |
| lfs = r.get("lfs") |
| if lfs: |
| if lfs.get("oid") != h: |
| bad.append((fn, f"sha {lfs.get('oid')} != {h[:12]}")) |
| elif r.get("oid") != git_blob_sha1(f"{stage}/{fn}"): |
| bad.append((fn, f"git oid {r.get('oid')} does not match the staged bytes")) |
| for fn in list(RECIPE) + ["SHA256SUMS"]: |
| if fn not in remote: |
| bad.append((fn, "MISSING remote")) |
| stale = sorted(x for x in remote if x not in local and x not in RECIPE and x not in ("SHA256SUMS", "README.md", ".gitattributes")) |
| bad += [(x, "SEPARATION") for x in separation_errors(kind, list(remote))] |
| print(json.dumps({"repo": repo, "checked": len(local), "bad": bad, "unexpected_remote_files": stale, |
| "result": "PASS" if not bad and not stale else "FAIL"})) |
| sys.exit(0 if not bad and not stale else 1) |
| elif sys.argv[1] == "readme": |
| from huggingface_hub import HfApi |
| HfApi().upload_file(path_or_fileobj=sys.argv[3], path_in_repo="README.md", repo_id=repo, repo_type="model", |
| commit_message="Model card: measured results, reproduction, known issues") |
| print("README_DONE", repo) |
| else: |
| raise SystemExit(f"usage: {sys.argv[0]} stage|upload|verify|readme <std|imat> [README path]") |
|
|