Download model-5/src/common.py from Jyo-K/PoPTard5: direct link, hf CLI and curl.
- Browser
- Download file 8.07 kB
-
https://huggingface.co/Jyo-K/PoPTard5/resolve/main/model-5/src/common.py
- Command line
-
hf download hf://Jyo-K/PoPTard5/model-5/src/common.py
-
curl -L -o common.py https://huggingface.co/Jyo-K/PoPTard5/resolve/main/model-5/src/common.py
8.07 kB
| """Logging, atomic writes, stage state, GPU discovery, and a crash-safe shard work queue.""" | |
| import json | |
| import logging | |
| import os | |
| import socket | |
| import subprocess | |
| import sys | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| from config import CFG, LOGS, STATE, WORK | |
| def get_logger(name): | |
| LOGS.mkdir(parents=True, exist_ok=True) | |
| log = logging.getLogger(name) | |
| if not log.handlers: | |
| log.setLevel(logging.INFO) | |
| fmt = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s", "%m-%d %H:%M:%S") | |
| h = logging.StreamHandler(sys.stdout) | |
| h.setFormatter(fmt) | |
| log.addHandler(h) | |
| log.propagate = False | |
| return log | |
| def _tmp(path): | |
| path = Path(path) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| return path.with_name(f".{path.name}.tmp{os.getpid()}") | |
| def save_parquet(df, path): | |
| tmp = _tmp(path) | |
| df.to_parquet(tmp, index=False) | |
| os.replace(tmp, path) | |
| def save_npz(path, **arrays): | |
| tmp = _tmp(path) | |
| with open(tmp, "wb") as f: | |
| np.savez(f, **arrays) | |
| os.replace(tmp, path) | |
| def save_npy(path, arr): | |
| tmp = _tmp(path) | |
| with open(tmp, "wb") as f: | |
| np.save(f, arr) | |
| os.replace(tmp, path) | |
| def save_json(obj, path): | |
| tmp = _tmp(path) | |
| tmp.write_text(json.dumps(obj, indent=2, default=str)) | |
| os.replace(tmp, path) | |
| def load_json(path): | |
| return json.loads(Path(path).read_text()) | |
| def stage_done(name): | |
| return (STATE / f"{name}.done").exists() | |
| def mark_done(name, info=None): | |
| save_json({"finished": time.strftime("%Y-%m-%d %H:%M:%S"), **(info or {})}, STATE / f"{name}.done") | |
| class Progress: | |
| def __init__(self, log, total, what, every=60): | |
| self.log, self.total, self.what, self.every = log, total, what, every | |
| self.t0 = self.last = time.time() | |
| self.done = 0 | |
| def step(self, n=1): | |
| self.done += n | |
| now = time.time() | |
| if now - self.last >= self.every or self.done >= self.total: | |
| self.last = now | |
| rate = self.done / max(now - self.t0, 1e-9) | |
| eta = (self.total - self.done) / rate if rate > 0 else float("nan") | |
| self.log.info(f"{self.what}: {self.done}/{self.total} ({100 * self.done / max(self.total, 1):.1f}%) " | |
| f"elapsed {(now - self.t0) / 60:.1f}m eta {eta / 60:.1f}m") | |
| def gpu_status(): | |
| """[(index, free_mb, util_pct)] for the GPUs this pipeline may use (M5_GPUS restricts).""" | |
| try: | |
| out = subprocess.run(["nvidia-smi", "--query-gpu=index,memory.free,utilization.gpu", | |
| "--format=csv,noheader,nounits"], capture_output=True, text=True, timeout=30).stdout | |
| except Exception: | |
| return [] | |
| allowed = os.environ.get("M5_GPUS") | |
| allowed = {int(x) for x in allowed.split(",") if x != ""} if allowed else None | |
| res = [] | |
| for line in out.strip().splitlines(): | |
| try: | |
| i, free, util = [int(x.strip()) for x in line.split(",")] | |
| except ValueError: | |
| continue | |
| if allowed is None or i in allowed: | |
| res.append((i, free, util)) | |
| return res | |
| def gpu_patient(fn, log, what, wait_s=60, max_wait_s=3 * 3600): | |
| """Run fn(); on CUDA OOM (other users filling the shared GPU) free cache, wait, retry.""" | |
| import torch | |
| t0 = time.time() | |
| while True: | |
| try: | |
| return fn() | |
| except torch.cuda.OutOfMemoryError: | |
| torch.cuda.empty_cache() | |
| if time.time() - t0 > max_wait_s: | |
| raise | |
| log.warning(f"{what}: GPU out of memory (shared card busy), retrying in {wait_s}s") | |
| time.sleep(wait_s) | |
| def n_workers(): | |
| """Worker processes: the configured cap, reduced when other users already load the (shared) CPUs.""" | |
| ncpu = os.cpu_count() or 8 | |
| try: | |
| busy = os.getloadavg()[0] | |
| except OSError: | |
| busy = 0 | |
| return int(max(8, min(CFG["n_workers"], ncpu - busy - 4))) | |
| def shard_path(kind, split, i, ext="parquet", sub=None): | |
| base = WORK / kind / split | |
| if sub: | |
| base = base / sub | |
| return base / f"shard_{i:05d}.{ext}" | |
| def read_shards(kind, split, n, columns=None, sub=None, ext="parquet"): | |
| return pd.concat([pd.read_parquet(shard_path(kind, split, i, ext, sub), columns=columns) for i in range(n)], | |
| ignore_index=True) | |
| # ----------------------------------------------------------------------------------------------- work queue | |
| def _pid_alive(pid): | |
| try: | |
| os.kill(pid, 0) | |
| return True | |
| except ProcessLookupError: | |
| return False | |
| except PermissionError: | |
| return True | |
| REMOTE_STALE_S = 2.5 * 3600 # a remote (Colab) claim with no refresh for this long goes back to local workers | |
| def claim(out_path): | |
| """Claim the job that produces out_path. Returns True if this process owns it now. A claim is a file created | |
| with O_EXCL holding host:pid; claims of dead processes (crash, kill, OOM) are broken and re-taken, so any | |
| number of workers on any GPUs can drain one shard list without double work or lost shards.""" | |
| out_path = Path(out_path) | |
| if out_path.exists(): | |
| return False | |
| c = out_path.with_name(out_path.name + ".claim") | |
| c.parent.mkdir(parents=True, exist_ok=True) | |
| me = f"{socket.gethostname()}:{os.getpid()}" | |
| for _ in range(2): | |
| try: | |
| fd = os.open(c, os.O_CREAT | os.O_EXCL | os.O_WRONLY) | |
| os.write(fd, me.encode()) | |
| os.close(fd) | |
| if out_path.exists(): # finished between our check and our claim | |
| c.unlink(missing_ok=True) | |
| return False | |
| return True | |
| except FileExistsError: | |
| try: | |
| host, pid = c.read_text().split(":") | |
| if host.startswith("remote"): # a Colab lane (src/remote.py): stale after REMOTE_STALE_S | |
| stale = time.time() - c.stat().st_mtime > REMOTE_STALE_S | |
| else: | |
| stale = host == socket.gethostname() and not _pid_alive(int(pid)) | |
| except (ValueError, FileNotFoundError): | |
| stale = True | |
| if not stale: | |
| return False | |
| c.unlink(missing_ok=True) | |
| return False | |
| def release(out_path): | |
| Path(str(out_path) + ".claim").unlink(missing_ok=True) | |
| def drain(paths, fn, log, what): | |
| """Run fn(i) for every index whose output does not exist yet, claiming each one first. Safe to run from | |
| several processes at once. Returns the number of jobs this process completed.""" | |
| todo = [i for i, p in enumerate(paths) if not Path(p).exists()] | |
| prog = Progress(log, len(todo), what) | |
| n = 0 | |
| for i in todo: | |
| if not claim(paths[i]): | |
| continue | |
| try: | |
| fn(i) | |
| n += 1 | |
| finally: | |
| release(paths[i]) | |
| prog.step() | |
| return n | |
| def spawn_gpu_workers(stage_args, log, min_free_mb): | |
| """Start one worker process per assigned GPU (python stages.py <stage_args...>), wait for all, fail if any | |
| failed. Workers share the job list through claim().""" | |
| gpus = [g for g in os.environ.get("M5_ASSIGNED_GPUS", "0").split(",") if g != ""] | |
| src = Path(__file__).resolve().parent | |
| procs = [] | |
| for g in gpus: | |
| env = dict(os.environ, CUDA_VISIBLE_DEVICES=g, M5_ASSIGNED_GPUS=g) | |
| # the trailing owner tag is ignored by stages.py; it labels the process for anyone listing GPU jobs on the | |
| # shared account (other sessions have been killing unrecognised GPU processes) | |
| procs.append(subprocess.Popen([sys.executable, "-u", str(src / "stages.py"), *stage_args, | |
| "--owner=model-5-pipeline-DO-NOT-KILL"], env=env, cwd=str(src))) | |
| log.info(f"worker {stage_args} on GPU {g} pid={procs[-1].pid}") | |
| codes = [p.wait() for p in procs] | |
| if any(codes): | |
| raise RuntimeError(f"GPU workers {stage_args} failed: {codes}") | |
| def disk_free_gb(path): | |
| st = os.statvfs(path) | |
| return st.f_bavail * st.f_frsize / 1e9 | |