Download code/scripts/p2/pipeline_gen_verify.py from viai957/align: direct link, hf CLI and curl.
- Browser
- Download file 9.4 kB
-
https://huggingface.co/viai957/align/resolve/main/code/scripts/p2/pipeline_gen_verify.py
- Command line
-
hf download hf://viai957/align/code/scripts/p2/pipeline_gen_verify.py
-
curl -L -o pipeline_gen_verify.py https://huggingface.co/viai957/align/resolve/main/code/scripts/p2/pipeline_gen_verify.py
9.4 kB
| """Generation and verification as ONE heavy job, overlapped. | |
| THE IDLE-GPU DEFECT. The P2 queue ran `lean_gen.py` (GPU) and then `verify_generations.py` | |
| (CPU, Lean) back to back. Verification of a 1,952-sample miniF2F run took 1.43 h and the pool | |
| round's would take ~12 h; the four A10Gs sat at 0% for all of it. Running the verifier as a | |
| second job beside the generator is not an option either: /apps/CLAUDE.md rule 2 forbids two heavy | |
| jobs at once, and the repo's flock enforces it. | |
| So this supervisor takes the heavy-job lock ONCE and runs both phases as children that inherit it | |
| (`--inherit-lock`, the mechanism scripts/p2/launch_baseline.py already uses): the generator owns | |
| the GPUs, and every `--every` seconds a verification pass checks the COMPLETE lines written so far. | |
| A pass works on a snapshot that stops at the last newline, because the generator appends while we | |
| read and the strict JSONL reader (p2_common.iter_jsonl) rightly refuses a half-written line. | |
| Verification is keyed and resumable, so each pass only verifies what is new, and the final pass | |
| after the generator exits covers the tail. One lock, one job, both resources busy. | |
| Exit codes: 0 both phases complete; the generator's or verifier's non-zero code otherwise | |
| (143 when stopped by a signal); 4 when the generator exited 0 but its manifest does not say | |
| `complete` (a partial sample set must never read as a finished stage -- U12-2); 5 when GPU memory | |
| did not drain after the generator (an orphan holds it -- U12-3). Re-running the same command | |
| resumes both phases. | |
| After EVERY generator exit (clean, failed or stopped) the generator's process group is reaped by its | |
| exact pgid, orphan `psm_*` /dev/shm segments of this phase are swept, and GPU memory is waited on | |
| until drained, using scripts/p2/launch_baseline.py's helpers (vLLM's EngineCore is documented to | |
| outlive its parent: 19 GB of GPU memory, 0.19-1.4 GB of /dev/shm). The next GPU stage (sft.py, | |
| export, eval generation) therefore starts on a clean host even though this supervisor releases the | |
| lock on exit. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import signal | |
| import subprocess | |
| import sys | |
| import time | |
| from pathlib import Path | |
| REPO = Path(__file__).resolve().parents[2] | |
| sys.path.insert(0, str(REPO)) | |
| PYR = str(REPO / ".venv-rollout" / "bin" / "python") | |
| PY = str(REPO / ".venv" / "bin" / "python") | |
| _LB = None | |
| def launch_baseline(): | |
| """scripts/p2/launch_baseline.py, loaded by path (the repo idiom): its kill_group / sweep_orphan_shm / | |
| wait_gpu_drained are the reviewed implementation; a second copy would drift.""" | |
| global _LB | |
| if _LB is None: | |
| import importlib.util | |
| spec = importlib.util.spec_from_file_location("_p2_launch_baseline", REPO / "scripts/p2/launch_baseline.py") | |
| _LB = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(_LB) | |
| return _LB | |
| def generation_complete(gen_out: str) -> tuple[bool, str]: | |
| """Did the LAST lean_gen session on `gen_out` finish every planned sample? (its manifest says so)""" | |
| mp = gen_out + ".manifest.json" | |
| if not os.path.exists(mp): | |
| return False, "no manifest" | |
| with open(mp, encoding="utf-8") as f: | |
| sessions = json.load(f).get("sessions", []) | |
| if not sessions: | |
| return False, "manifest has no sessions" | |
| st = sessions[-1].get("status") | |
| return st == "complete", f"last lean_gen session status {st!r}" | |
| def reap_after_gen(proc, started_at: float, events, lb=None, log=None) -> None: | |
| """Kill survivors of the generator's process group, sweep its /dev/shm litter, wait for the GPUs to drain.""" | |
| lb = lb or launch_baseline() | |
| log = log or (lambda **kw: log_event(events, **kw)) | |
| lb.Runner.kill_group(proc, 15.0) | |
| gone, nbytes = lb.sweep_orphan_shm(started_at) | |
| if gone: | |
| log(event="shm_swept", segments=len(gone), bytes=nbytes) | |
| lb.wait_gpu_drained() | |
| log(event="gpu_drained") | |
| def snapshot_complete_lines(src: Path, dst: Path) -> int: | |
| """Copy `src` up to and including its last newline into `dst` (atomically). Returns the | |
| number of complete lines. A trailing partial line -- the generator mid-append -- is left out.""" | |
| if not src.exists(): | |
| return 0 | |
| data = src.read_bytes() | |
| cut = data.rfind(b"\n") + 1 | |
| tmp = dst.with_suffix(dst.suffix + ".tmp") | |
| tmp.write_bytes(data[:cut]) | |
| os.replace(tmp, dst) | |
| return data[:cut].count(b"\n") | |
| def log_event(path: Path, **kw) -> None: | |
| kw["t"] = time.strftime("%Y-%m-%d %H:%M:%S") | |
| with open(path, "a", encoding="utf-8") as f: | |
| f.write(json.dumps(kw) + "\n") | |
| def main(argv=None) -> int: | |
| ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| ap.add_argument("--run-dir", required=True) | |
| ap.add_argument("--gen-args", required=True, help="arguments for lean_gen.py, as ONE quoted string (must include --out)") | |
| ap.add_argument("--problems", required=True) | |
| ap.add_argument("--verify-out", required=True) | |
| ap.add_argument("--workers", type=int, default=8) | |
| ap.add_argument("--every", type=float, default=300.0, help="seconds between verification passes") | |
| ap.add_argument("--feedback", action="store_true") | |
| ap.add_argument("--label", default="p2-gen-verify") | |
| a = ap.parse_args(argv) | |
| from align.utils import guard | |
| run = Path(a.run_dir) | |
| run.mkdir(parents=True, exist_ok=True) | |
| events = run / "pipeline.jsonl" | |
| gen_args = a.gen_args.split() | |
| gen_out = Path(gen_args[gen_args.index("--out") + 1]) | |
| snap = run / (gen_out.stem + ".snapshot.jsonl") | |
| guard.acquire_heavy_job_lock(a.label, blocking=True) | |
| guard.preflight_host_check() # AFTER the wait: a lock wait can last hours, a stale preflight proves nothing | |
| me = os.getpid() | |
| log_event(events, event="lock_acquired", pid=me) | |
| stop = {"sig": None} | |
| def on_signal(sig, _frm): | |
| stop["sig"] = sig | |
| signal.signal(signal.SIGTERM, on_signal) | |
| signal.signal(signal.SIGINT, on_signal) | |
| gen_log = open(run / "gen.log", "ab") | |
| started_at = time.time() - 1.0 | |
| gen = subprocess.Popen( | |
| [PYR, str(REPO / "extras/vllm_rollout/lean_gen.py"), *gen_args, "--inherit-lock", str(me)], | |
| stdout=gen_log, stderr=subprocess.STDOUT, start_new_session=True, | |
| ) | |
| log_event(events, event="gen_start", pid=gen.pid, args=gen_args) | |
| def verify_pass(source: Path) -> int: | |
| cmd = [PY, str(REPO / "scripts/p2/verify_generations.py"), "--generations", str(source), | |
| "--problems", a.problems, "--out", a.verify_out, "--workers", str(a.workers), | |
| "--inherit-lock", str(me)] | |
| if a.feedback: | |
| cmd.append("--feedback") | |
| with open(run / "verify.log", "ab") as vl: | |
| v = subprocess.Popen(cmd, stdout=vl, stderr=subprocess.STDOUT, start_new_session=True) | |
| while v.poll() is None: | |
| if stop["sig"] is not None: | |
| os.killpg(v.pid, signal.SIGTERM) | |
| v.wait() | |
| break | |
| time.sleep(2) | |
| return v.returncode | |
| rc_v = 0 | |
| last_lines = -1 | |
| while gen.poll() is None and stop["sig"] is None: | |
| n = snapshot_complete_lines(gen_out, snap) | |
| if n > last_lines and n > 0: | |
| log_event(events, event="verify_pass", lines=n) | |
| rc_v = verify_pass(snap) | |
| log_event(events, event="verify_pass_end", lines=n, rc=rc_v) | |
| if rc_v != 0 and stop["sig"] is None: | |
| # a failing verifier must not cost the GPU work: keep generating, stop the | |
| # overlapped passes, and let the final pass (or a re-run) surface the error | |
| log_event(events, event="verify_overlap_disabled", rc=rc_v) | |
| last_lines = float("inf") | |
| rc_v = 0 | |
| continue | |
| last_lines = n | |
| t_end = time.monotonic() + a.every | |
| while time.monotonic() < t_end and gen.poll() is None and stop["sig"] is None: | |
| time.sleep(5) | |
| if stop["sig"] is not None: | |
| if gen.poll() is None: | |
| os.killpg(gen.pid, signal.SIGTERM) | |
| try: | |
| gen.wait(timeout=120) | |
| except subprocess.TimeoutExpired: | |
| os.killpg(gen.pid, signal.SIGKILL) | |
| gen.wait() | |
| try: | |
| reap_after_gen(gen, started_at, events) | |
| except RuntimeError as e: | |
| log_event(events, event="gpu_not_drained", error=str(e)) | |
| log_event(events, event="stopped", signal=stop["sig"], verify_rc=rc_v) | |
| return 143 | |
| rc_g = gen.wait() | |
| log_event(events, event="gen_end", rc=rc_g) | |
| try: | |
| reap_after_gen(gen, started_at, events) | |
| except RuntimeError as e: | |
| log_event(events, event="gpu_not_drained", error=str(e)) | |
| return rc_g or 5 | |
| if rc_g != 0: | |
| return rc_g | |
| complete, why = generation_complete(gen_out) | |
| if not complete: | |
| log_event(events, event="gen_incomplete", detail=why) | |
| return 4 | |
| # final pass over the complete file (the generator has exited, so every line is whole) | |
| rc_v = verify_pass(gen_out) | |
| log_event(events, event="final_verify_end", rc=rc_v) | |
| if snap.exists(): | |
| snap.unlink() | |
| return 0 if stop["sig"] is None and rc_v == 0 else (143 if stop["sig"] is not None else rc_v) | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |