File size: 16,007 Bytes
840c3a4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 | #!/usr/bin/env python3
"""Resume the ELF quality matrix on independent single-GPU workers (no timing)."""
from __future__ import annotations
import argparse
from concurrent.futures import ThreadPoolExecutor, wait
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
import json
import os
from pathlib import Path
import queue
import signal
import subprocess
import sys
import threading
import time
from elf_common import ROOT, SOURCE_COMMIT, default_sampling, manifest, read_jsonl, sha256, write_json
from inspect_elf_progress import inspect_run, OWT_MAIN_STEPS
EOS_POLICY = "released_ids_or_raw_plus_eos_then_source_cap_v2"
TASKS = ("owt", "owt-prefix", "wmt14", "xsum")
@dataclass(frozen=True)
class Cell:
task: str
split: str
steps: int
count: int
input_path: str | None = None
input_sha256: str | None = None
@property
def key(self):
return f"{self.task}/{self.split}/steps_{self.steps}/seed_42/compile_0"
def build_matrix(root, tasks):
cells, blocked = [], []
for task in tasks:
if task == "owt":
cells.extend(Cell(task, "unconditional", s, 1024) for s in OWT_MAIN_STEPS)
continue
splits = (("c64-text-t5", OWT_MAIN_STEPS, 1024),) if task == "owt-prefix" else (
("validation", (1, 2, 4, 8, 16, 32, 64), 3000 if task == "wmt14" else 11332),
("test", (32, 64), 3003 if task == "wmt14" else 11334),
)
for split, steps, expected_count in splits:
path = root / "data/elf" / ("owt-prefix.jsonl" if task == "owt-prefix" else f"{task}-{split}.jsonl")
try:
meta = json.loads(path.with_suffix(".manifest.json").read_text())
digest = sha256(path)
rows = read_jsonl(path)
if (meta["task"] != task or meta["sha256"] != digest or meta["count"] != expected_count
or len(rows) != expected_count or len({r["id"] for r in rows}) != expected_count):
raise ValueError("input task/count/hash/IDs mismatch")
if task == "owt-prefix":
if meta.get("protocol") != "c64_text_t5_v1":
raise ValueError("prefix protocol mismatch")
source = root / "data/conditional/owt-c64/prompts.jsonl"
if meta.get("source_sha256") != sha256(source):
raise ValueError("prefix source changed; prepare prefix again")
elif meta.get("split") != split:
raise ValueError("input split mismatch")
except (OSError, KeyError, TypeError, ValueError) as error:
blocked.append({"task": task, "split": split, "status": "blocked_input", "reason": str(error)})
continue
cells.extend(Cell(task, split, s, expected_count, str(path), digest) for s in steps)
return sorted(cells, key=lambda c: (c.steps, c.task, c.split)), blocked
def matches_request(request, cell, asset_hash, assets):
checkpoint_task = "owt" if cell.task == "owt-prefix" else cell.task
record = assets[checkpoint_task]
expected = {
"task": cell.task, "steps": cell.steps, "seed": 42, "compile": False, "batch_size": 8,
"source_commit": SOURCE_COMMIT, "assets_manifest_sha256": asset_hash,
"checkpoint_task": checkpoint_task, "checkpoint_selection": "ema_params1",
"checkpoint_sha256": record["sha256"][record["checkpoint"]],
"sampling": default_sampling(cell.task, cell.steps),
"model_canvas_t5_tokens": 1088 if cell.task == "xsum" else 128 if cell.task == "wmt14" else 1024,
"protocol": "c64_text_t5_v1" if cell.task == "owt-prefix" else "elf_native_v1",
}
if any(request.get(key) != value for key, value in expected.items()):
return False
if cell.task in ("wmt14", "xsum") and request.get("condition_tokenization_policy") != EOS_POLICY:
return False
return (request.get("input") or {}).get("sha256") == cell.input_sha256
def reusable_result(root, cell, asset_hash, assets):
candidates, rejected = [], []
for path in sorted((root / "results").glob(f"elf*/quality/{cell.key}")):
try:
request = json.loads((path / "request.json").read_text())
if not matches_request(request, cell, asset_hash, assets):
continue
row = inspect_run(path)
if row["status"] not in ("quality_valid", "quality_invalid", "needs_evaluation"):
rejected.append({"run": str(path), "reason": row.get("reason", row["status"])})
continue
gen = json.loads((path / "generation.json").read_text())
keys = ("sampling", "input", "assets_manifest_sha256", "checkpoint_sha256", "source_commit",
"condition_tokenization_policy", "batch_size", "protocol")
if any(gen.get(k) != request.get(k) for k in keys):
raise ValueError("request/generation provenance differs")
diversity = cell.task == "owt-prefix"
total = cell.count + (min(256, cell.count) * 4 if diversity else 0)
if gen["prompt_count"] != cell.count or gen["sample_count"] != total or bool(gen.get("diversity")) != diversity:
continue # e.g. 1000-row sanity is not a 1024-row primary run.
if row["status"] == "quality_invalid":
reason = row.get("reason") or ""
if not reason.startswith("At least one completion has fewer than two GPT-2 tokens;"):
rejected.append({"run": str(path), "reason": "unrecognized invalid-quality reason"})
continue
candidates.append(row)
except (OSError, ValueError, KeyError, TypeError) as error:
rejected.append({"run": str(path), "reason": str(error)})
# A valid score and a recorded invalid score for one exact cell conflict;
# never silently choose the better run.
completed = [r for r in candidates if r["status"] != "needs_evaluation"]
if len({r["status"] for r in completed}) > 1:
raise ValueError(f"Conflicting completed results for {cell.key}; inspect runs manually")
return (completed or candidates or [None])[0], rejected
def fresh_path(output_root, cell):
for attempt in range(10000):
base = output_root if attempt == 0 else output_root.with_name(output_root.name + f"-retry{attempt}")
path = base / "quality" / cell.key
if not path.exists():
return path
raise RuntimeError("Too many existing retry directories")
def commands(root, python, cell, path, generate):
result = []
if generate:
cmd = [python, "-B", str(root / "scripts/run_elf.py"), "generate", "--root", str(root),
"--task", cell.task, "--steps", str(cell.steps), "--seed", "42", "--batch-size", "8",
"--num-samples", str(cell.count), "--output", str(path)]
if cell.input_path:
cmd += ["--input", cell.input_path]
if cell.task == "owt-prefix":
cmd += ["--diversity"]
result.append(("generate", cmd))
result.append(("evaluate", [python, "-B", str(root / "scripts/evaluate_elf.py"), "--root", str(root),
"--run", str(path), "--batch-size", "8"]))
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=ROOT)
parser.add_argument("--output-root", type=Path)
parser.add_argument("--tasks", nargs="+", choices=TASKS, default=list(TASKS))
parser.add_argument("--gpus", nargs="+", default=["0", "1"])
parser.add_argument("--plan", action="store_true", help="Read-only preview; no GPU queries, writes or inference")
args = parser.parse_args()
root = args.root.resolve()
output_root = (args.output_root or root / "results/elf-gpu01").resolve()
if output_root.parent != root / "results" or not output_root.name.startswith("elf"):
parser.error("output-root must be an elf* directory directly under ROOT/results")
if len(set(args.tasks)) != len(args.tasks):
parser.error("duplicate tasks")
assets = manifest(root)["assets"]
asset_hash = sha256(root / "data/elf/assets.json")
cells, blocked = build_matrix(root, args.tasks)
jobs, reports = [], []
for cell in cells:
existing, rejected = reusable_result(root, cell, asset_hash, assets)
action = ("skip" if existing["status"] != "needs_evaluation" else "evaluate") if existing else "generate+evaluate"
path = Path(existing["run"]) if existing else fresh_path(output_root, cell)
row = {"cell": asdict(cell), "action": action, "run": str(path), "rejected_candidates": rejected,
"status": existing["status"] if action == "skip" else "pending"}
reports.append(row)
print(f"{action:18} {cell.key} -> {path}", flush=True)
if action != "skip":
jobs.append((cell, row))
for row in blocked:
print(f"BLOCKED {row['task']}/{row['split']}: {row['reason']}", flush=True)
print(f"Plan: {len(cells)} cells, {len(jobs)} queued, {len(blocked)} blocked input groups; no timing.", flush=True)
if args.plan:
return 2 if blocked else 0
if sys.platform != "linux":
parser.error("GPU execution is server-only; use --plan for a read-only preview")
if not jobs:
print("Nothing to run. Existing invalid-quality results remain invalid, not successful PPL points.")
return 2 if blocked else 0
import fcntl
from contextlib import ExitStack
with ExitStack() as stack:
gpu_records = []
lock_dir = root / "results/elf-queue-locks"
lock_dir.mkdir(parents=True, exist_ok=True)
for gpu in args.gpus:
line = subprocess.check_output(["nvidia-smi", "-i", gpu, "--query-gpu=uuid,name", "--format=csv,noheader"], text=True).strip()
uuid, name = [s.strip() for s in line.split(",", 1)]
if not uuid.startswith("GPU-") or any(r["uuid"] == uuid for r in gpu_records):
raise ValueError("GPU selections must resolve to distinct physical GPUs")
handle = stack.enter_context((lock_dir / f"{uuid}.lock").open("a"))
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
gpu_records.append({"physical_selection": gpu, "uuid": uuid, "name": name})
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S") + f"-{os.getpid()}"
log_dir = output_root / "logs" / stamp
log_dir.mkdir(parents=True)
report_path = log_dir / "queue.json"
report = {"schema": "elf-quality-queue-v1", "gpus": gpu_records, "runs": reports, "blocked": blocked,
"assets_manifest_sha256": asset_hash, "timing": False}
mutex, stop = threading.Lock(), threading.Event()
processes = {}
work = queue.Queue()
for job in jobs:
work.put(job)
def publish():
write_json(report_path, report)
def worker(gpu):
env = dict(os.environ, CUDA_VISIBLE_DEVICES=gpu["uuid"], PYTHONDONTWRITEBYTECODE="1",
HF_HUB_OFFLINE="1", TRANSFORMERS_OFFLINE="1", WANDB_MODE="disabled")
while not stop.is_set():
try:
cell, row = work.get_nowait()
except queue.Empty:
break
path = Path(row["run"])
with mutex:
row.update(status="running", gpu=gpu["uuid"])
publish()
print(f"START GPU {gpu['physical_selection']} {cell.key}", flush=True)
try:
for stage, command in commands(root, sys.executable, cell, path, row["action"] == "generate+evaluate"):
if stop.is_set():
raise RuntimeError("queue interrupted")
log = log_dir / (cell.key.replace("/", "__") + f"__{stage}.log")
with log.open("w") as stream:
stream.write(json.dumps(command) + "\n")
stream.flush()
with mutex:
if stop.is_set():
raise RuntimeError("queue interrupted")
proc = subprocess.Popen(command, cwd=root, env=env, stdout=stream,
stderr=subprocess.STDOUT, start_new_session=True)
processes[gpu["uuid"]] = proc
code = proc.wait()
with mutex:
processes.pop(gpu["uuid"], None)
with mutex:
row[stage + "_log"] = str(log)
if code != 0:
checked, _ = reusable_result(root, cell, asset_hash, assets)
if stage == "evaluate" and checked and checked["run"] == str(path) and checked["status"] == "quality_invalid":
break
raise RuntimeError(f"{stage} exit={code}; see {log}")
checked, _ = reusable_result(root, cell, asset_hash, assets)
if not checked or checked["run"] != str(path) or checked["status"] not in ("quality_valid", "quality_invalid"):
raise RuntimeError("finished process did not publish a verified quality result")
with mutex:
row.update(status=checked["status"], reason=checked.get("reason"))
except Exception as error:
with mutex:
row.update(status="failed", reason=str(error))
finally:
with mutex:
publish()
print(f"DONE GPU {gpu['physical_selection']} {cell.key}: {row['status']}", flush=True)
work.task_done()
publish()
print(f"Logs and live queue report: {log_dir}", flush=True)
pool = ThreadPoolExecutor(max_workers=len(gpu_records))
futures = [pool.submit(worker, gpu) for gpu in gpu_records]
interrupted = False
try:
pending = futures
while pending:
_, pending = wait(pending, timeout=30)
if pending:
with mutex:
running = [r['cell'] for r in reports if r['status'] == 'running']
print(f"RUNNING {running}; remaining queued={work.qsize()}", flush=True)
for future in futures:
future.result()
except KeyboardInterrupt:
interrupted = True
stop.set()
with mutex:
active = list(processes.values())
for proc in active:
try:
os.killpg(proc.pid, signal.SIGTERM)
except ProcessLookupError:
pass
for proc in active:
try:
proc.wait(timeout=15)
except subprocess.TimeoutExpired:
try:
os.killpg(proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
finally:
pool.shutdown(wait=True)
with mutex:
publish()
counts = {s: sum(r["status"] == s for r in reports) for s in sorted({r["status"] for r in reports})}
print(f"Final statuses: {counts}; blocked input groups={len(blocked)}; report={report_path}", flush=True)
return 130 if interrupted else 2 if blocked or any(r["status"] in ("failed", "pending", "running") for r in reports) else 0
if __name__ == "__main__":
raise SystemExit(main())
|