Download model-5/src/remote.py from Jyo-K/PoPTard5: direct link, hf CLI and curl.
- Browser
- Download file 8.39 kB
-
https://huggingface.co/Jyo-K/PoPTard5/resolve/main/model-5/src/remote.py
- Command line
-
hf download hf://Jyo-K/PoPTard5/model-5/src/remote.py
-
curl -L -o remote.py https://huggingface.co/Jyo-K/PoPTard5/resolve/main/model-5/src/remote.py
8.39 kB
| """Remote (Colab) lanes for cross-encoder inference, exchanged through a PRIVATE Hugging Face dataset repo. | |
| python remote.py export --key b --split test --per-lane 20 [--lanes A,B] claim + package + upload | |
| python remote.py import --key b --split test pull results (runs until done) | |
| python remote.py status --key b --split test | |
| export: takes the highest-numbered shards of the current need list that are neither finished nor claimed, and | |
| writes a server-side claim "remote-<lane>:0" for each (local GPU workers skip them; common.claim treats a remote | |
| claim as stale after REMOTE_STALE_S without progress, so a dead Colab session never strands work). Each shard is | |
| packaged with its exact text pairs; a parity sample (pairs the server already scored with the same model) lets the | |
| notebook verify it reproduces the server's scores before it uploads anything. The fine-tuned model is uploaded once. | |
| import: downloads each finished shard, checks length and finiteness, writes it atomically where the pipeline's own | |
| workers would have, removes the claim, and refreshes the lane's remaining claims while that lane makes progress. | |
| The token is read from $HF_TOKEN or ~/.hf_m5_token and never written anywhere else.""" | |
| import argparse | |
| import json | |
| import os | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| from common import get_logger, save_npy, save_parquet, shard_path | |
| from config import CES, CFG, WORK | |
| log = get_logger("remote") | |
| REPO_NAME = os.environ.get("M5_REMOTE_REPO_NAME", "model5-ce-offload") | |
| def _token(): | |
| t = os.environ.get("HF_TOKEN") | |
| p = Path.home() / ".hf_m5_token" | |
| if not t and p.exists(): | |
| t = p.read_text().strip() | |
| if not t: | |
| raise SystemExit("no HF token: put a WRITE token in ~/.hf_m5_token (chmod 600) or $HF_TOKEN") | |
| return t | |
| def _api(): | |
| from huggingface_hub import HfApi | |
| api = HfApi(token=_token()) | |
| repo = os.environ.get("M5_REMOTE_REPO") or f"{api.whoami()['name']}/{REPO_NAME}" | |
| api.create_repo(repo, repo_type="dataset", private=True, exist_ok=True) | |
| return api, repo | |
| def _current(key, split): | |
| from cross_encoder import _need, _need_file, _need_paths | |
| need, h = _need(key, split) | |
| f = _need_file(key, split, h) | |
| if not f.exists(): | |
| raise SystemExit(f"need list {h} for ce_{key}/{split} not written yet (is ce_infer_{split}_{key} running?)") | |
| need = pd.read_parquet(f) | |
| return need, h, _need_paths(key, split, h, len(need)) | |
| def _claim_file(p): | |
| return Path(str(p) + ".claim") | |
| def export(key, split, lanes, per_lane): | |
| from cross_encoder import texts | |
| api, repo = _api() | |
| need, h, paths = _current(key, split) | |
| tag = f"ce_{key}_{split}_{h}" | |
| S = CFG["ce_shard_pairs"] | |
| free = [i for i in range(len(paths)) if not paths[i].exists() and not _claim_file(paths[i]).exists()] | |
| take = sorted(free, reverse=True)[:per_lane * len(lanes)] | |
| if not take: | |
| raise SystemExit("no unclaimed shards left to offload") | |
| assign = {ln: sorted(take[j::len(lanes)]) for j, ln in enumerate(lanes)} | |
| for ln, ids in assign.items(): # claim first, so local workers stop picking them up | |
| for i in ids: | |
| _claim_file(paths[i]).write_text(f"remote-{ln}:0") | |
| log.info(f"{tag}: claimed for remote lanes {assign}") | |
| out = WORK / "remote" / tag | |
| out.mkdir(parents=True, exist_ok=True) | |
| from cross_encoder import _text_shard, _write_text_shards | |
| _write_text_shards(key, split, h, need, paths) | |
| import shutil | |
| for i in take: | |
| f = out / f"shard_{i:05d}.parquet" | |
| if not f.exists(): | |
| shutil.copy2(_text_shard(key, split, h, i), f) | |
| # parity sample: 2,000 pairs the server already scored with this model (train side, same text template) | |
| from cross_encoder import _read_cache, texts as _t | |
| cache = _read_cache(key, "train").sample(2000, random_state=0) | |
| pa, pb = _t("train", "s1", key), _t("train", "pool", key) | |
| save_parquet(pd.DataFrame({"a": pa[cache.q.values], "b": pb[cache.c.values], "server_logit": cache.ce.values}), | |
| out / "parity.parquet") | |
| spec = CES[key] | |
| manifest = {"tag": tag, "key": key, "split": split, "list": h, "lanes": assign, "max_len": spec["max_len"], | |
| "infer_batch": spec["infer_batch"], "model_dir": f"models/ce_{key}", "created": time.time(), | |
| "scoring": "AutoModelForSequenceClassification fp32 weights, torch.autocast(bfloat16), " | |
| "tokenizer(a, b, truncation=True, max_length, padding=True), logits.squeeze(-1), " | |
| "batches of infer_batch pairs in ascending len(a)+len(b) order", | |
| "parity_tolerance_logit": 0.1} # p99; max <= 20x (cross-GPU bf16 kernels differ slightly) | |
| (out / "manifest.json").write_text(json.dumps(manifest, indent=1)) | |
| have = set(api.list_repo_files(repo, repo_type="dataset")) | |
| if f"models/ce_{key}/config.json" not in have: | |
| log.info(f"uploading the fine-tuned model to {repo} (about 2.3 GB, once)") | |
| api.upload_folder(folder_path=str(WORK / f"ce_{key}" / "final"), path_in_repo=f"models/ce_{key}", | |
| repo_id=repo, repo_type="dataset") | |
| api.upload_folder(folder_path=str(out), path_in_repo=f"jobs/{tag}", repo_id=repo, repo_type="dataset") | |
| log.info(f"exported {len(take)} shards to {repo}/jobs/{tag}; lanes {list(assign)}") | |
| print(json.dumps({"repo": repo, "tag": tag, "lanes": assign}, indent=1)) | |
| def _results(api, repo, tag): | |
| fs = api.list_repo_files(repo, repo_type="dataset") | |
| pre = f"results/{tag}/shard_" | |
| return sorted(int(f[len(pre):len(pre) + 5]) for f in fs if f.startswith(pre) and f.endswith(".npy")) | |
| def do_import(key, split, poll=60): | |
| from huggingface_hub import hf_hub_download | |
| api, repo = _api() | |
| need, h, paths = _current(key, split) | |
| tag = f"ce_{key}_{split}_{h}" | |
| man = json.loads((WORK / "remote" / tag / "manifest.json").read_text()) | |
| lane_of = {i: ln for ln, ids in man["lanes"].items() for i in ids} | |
| S = CFG["ce_shard_pairs"] | |
| while True: | |
| pending = [i for i in lane_of if not paths[i].exists()] | |
| if not pending: | |
| log.info(f"{tag}: all remote shards are in") | |
| return | |
| progressed = set() | |
| for i in _results(api, repo, tag): | |
| if i not in lane_of or paths[i].exists(): | |
| continue | |
| f = hf_hub_download(repo, f"results/{tag}/shard_{i:05d}.npy", repo_type="dataset", | |
| local_dir=str(WORK / "remote" / tag / "dl"), token=_token()) | |
| v = np.load(f) | |
| n = len(need.q.values[i * S:(i + 1) * S]) | |
| if len(v) != n or not np.isfinite(v).all(): | |
| log.error(f"{tag} shard {i}: bad result (len {len(v)} vs {n}); leaving it to local workers") | |
| else: | |
| save_npy(paths[i], v.astype(np.float32)) | |
| log.info(f"{tag} shard {i}: imported from lane {lane_of[i]}") | |
| progressed.add(lane_of[i]) | |
| _claim_file(paths[i]).unlink(missing_ok=True) | |
| for i in pending: # a lane that is making progress keeps its claims alive | |
| if lane_of[i] in progressed and _claim_file(paths[i]).exists(): | |
| os.utime(_claim_file(paths[i])) | |
| time.sleep(poll) | |
| def status(key, split): | |
| api, repo = _api() | |
| need, h, paths = _current(key, split) | |
| tag = f"ce_{key}_{split}_{h}" | |
| done_local = sum(p.exists() for p in paths) | |
| remote = _results(api, repo, tag) | |
| claims = {p.name: _claim_file(p).read_text() for p in paths if _claim_file(p).exists()} | |
| print(json.dumps({"repo": repo, "tag": tag, "shards": len(paths), "done": done_local, | |
| "remote_results": remote, "claims": claims}, indent=1)) | |
| if __name__ == "__main__": | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("cmd", choices=["export", "import", "status"]) | |
| ap.add_argument("--key", default="b") | |
| ap.add_argument("--split", default="test") | |
| ap.add_argument("--lanes", default="A,B") | |
| ap.add_argument("--per-lane", type=int, default=20) | |
| a = ap.parse_args() | |
| if a.cmd == "export": | |
| export(a.key, a.split, a.lanes.split(","), a.per_lane) | |
| elif a.cmd == "import": | |
| do_import(a.key, a.split) | |
| else: | |
| status(a.key, a.split) | |