Download code/scripts/grpo_watch.py from viai957/align: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/viai957/align/resolve/main/code/scripts/grpo_watch.py
- Command line
-
hf download hf://viai957/align/code/scripts/grpo_watch.py
-
curl -L -o grpo_watch.py https://huggingface.co/viai957/align/resolve/main/code/scripts/grpo_watch.py
10.3 kB
| #!/usr/bin/env python3 | |
| """GRPO Stage-B terminal dashboard. | |
| Run: | |
| watch -n 15 -c .venv/bin/python scripts/grpo_watch.py | |
| or: | |
| .venv/bin/python scripts/grpo_watch.py --loop 15 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import math | |
| import os | |
| import shutil | |
| import statistics as st | |
| import subprocess | |
| import sys | |
| import time | |
| from pathlib import Path | |
| RUN_DIR = Path("runs/grpo_stageb") | |
| HISTORY = RUN_DIR / "history.jsonl" | |
| CANARY = RUN_DIR / "canary.jsonl" | |
| LOG = Path("runs/grpo_stageb.log") # launch log sits beside the run dir | |
| WORKER_LOG = RUN_DIR / "worker.log" | |
| # ANSI | |
| RESET = "\033[0m" | |
| BOLD = "\033[1m" | |
| DIM = "\033[2m" | |
| RED = "\033[31m" | |
| GREEN = "\033[32m" | |
| YELLOW = "\033[33m" | |
| BLUE = "\033[34m" | |
| MAGENTA = "\033[35m" | |
| CYAN = "\033[36m" | |
| WHITE = "\033[37m" | |
| BG_RED = "\033[41m" | |
| BG_GREEN = "\033[42m" | |
| BG_YELLOW = "\033[43m" | |
| BG_BLUE = "\033[44m" | |
| BG_MAGENTA = "\033[45m" | |
| BG_CYAN = "\033[46m" | |
| def fmt(x, nd=4, na="β"): | |
| if x is None: | |
| return na | |
| if isinstance(x, float) and math.isnan(x): | |
| return "NaN" | |
| return f"{x:.{nd}f}" | |
| def bar(cur, total, width=28, fill="β", empty="β", color=GREEN): | |
| if total <= 0: | |
| return f"{DIM}{empty * width}{RESET}" | |
| frac = min(max(cur / total, 0.0), 1.0) | |
| n = int(frac * width) | |
| return f"{color}{fill * n}{DIM}{empty * (width - n)}{RESET}" | |
| def sparkline(values, width=40): | |
| if not values: | |
| return "" | |
| blocks = "βββββ βββ" | |
| vals = list(values)[-width:] | |
| lo, hi = min(vals), max(vals) | |
| rng = (hi - lo) or 1.0 | |
| return "".join(blocks[min(int((v - lo) / rng * (len(blocks) - 1)), len(blocks) - 1)] for v in vals) | |
| def read_jsonl(path: Path): | |
| if not path.exists(): | |
| return [] | |
| out = [] | |
| with open(path) as f: | |
| for line in f: | |
| line = line.strip() | |
| if line: | |
| try: | |
| out.append(json.loads(line)) | |
| except Exception: | |
| continue | |
| return out | |
| def tail(path: Path, n=6): | |
| if not path.exists(): | |
| return [] | |
| try: | |
| with open(path, "rb") as f: | |
| f.seek(0, os.SEEK_END) | |
| end = f.tell() | |
| size = min(end, 8192) | |
| f.seek(end - size) | |
| data = f.read().decode(errors="replace") | |
| return data.splitlines()[-n:] | |
| except Exception: | |
| return [] | |
| def gpu_lines(): | |
| try: | |
| out = subprocess.check_output( | |
| [ | |
| "nvidia-smi", | |
| "--query-gpu=index,utilization.gpu,memory.used,memory.total,temperature.gpu", | |
| "--format=csv,noheader,nounits", | |
| ], | |
| text=True, | |
| ) | |
| except Exception: | |
| return ["GPU: nvidia-smi unavailable"] | |
| lines = [] | |
| for row in out.strip().splitlines(): | |
| idx, util, used, total, temp = [c.strip() for c in row.split(",")] | |
| used_g = float(used) / 1024 | |
| total_g = float(total) / 1024 | |
| lines.append( | |
| f"GPU{idx} {bar(float(util), 100, width=14, color=CYAN)} {util:>3}% " | |
| f"{used_g:5.1f}/{total_g:.0f}G {temp}Β°C" | |
| ) | |
| return lines | |
| def proc_status(): | |
| torchrun = subprocess.run(["pgrep", "-f", "torchrun.*grpo"], capture_output=True, text=True).stdout.strip() | |
| vllm = subprocess.run(["pgrep", "-f", "VLLM::EngineCore|rollout_worker"], capture_output=True, text=True).stdout.strip() | |
| return bool(torchrun), bool(vllm) | |
| def render(): | |
| cols = shutil.get_terminal_size((100, 30)).columns | |
| rows = read_jsonl(HISTORY) | |
| canary = read_jsonl(CANARY) | |
| torchrun_up, vllm_up = proc_status() | |
| opt_steps = [r.get("optimizer_step", 0) for r in rows] | |
| cur_opt = max(opt_steps) if opt_steps else 0 | |
| micro_steps = [r.get("micro_step", 0) for r in rows] | |
| cur_micro = max(micro_steps) if micro_steps else 0 | |
| grad_accum = 85 # configs/grpo_stageb.toml | |
| micro_in_step = cur_micro - cur_opt * grad_accum | |
| frac_opt = cur_opt + micro_in_step / grad_accum # fractional step for the bar | |
| applied = [r for r in rows if r.get("grad_norm") == r.get("grad_norm")] | |
| gn = [r["grad_norm"] for r in applied] | |
| acc = [r["reward/accuracy"] for r in rows if r.get("reward/accuracy") is not None] | |
| ent = [r["entropy"] for r in rows if r.get("entropy") is not None] | |
| loss = [r["loss"] for r in rows if r.get("loss") is not None] | |
| lr = [r["lr"] for r in rows if r.get("lr") is not None] | |
| resp = [r["lengths/response_mean"] for r in rows if r.get("lengths/response_mean") is not None] | |
| total_steps = 500 | |
| # run start = first timestamped line of the launch log (ctime is NOT | |
| # creation time on Linux -- it tracks the last write, which would | |
| # make elapsed/pace collapse to ~0 while the run is healthy). | |
| run_start = None | |
| if LOG.exists(): | |
| try: | |
| with open(LOG) as f: | |
| for line in f: | |
| if " INFO " in line and line[:4].isdigit(): | |
| run_start = time.mktime(time.strptime(line[:19], "%Y-%m-%d %H:%M:%S")) | |
| break | |
| except Exception: | |
| pass | |
| if run_start is None: | |
| run_start = HISTORY.stat().st_mtime if HISTORY.exists() else time.time() | |
| elapsed = time.time() - run_start | |
| # Pace from observed micro-step rate (state file), not run start: | |
| # run start includes one-time boot (engine load, canary, first rollout), | |
| # which makes early pace readings garbage. | |
| state_f = RUN_DIR / ".watch_state.json" | |
| try: | |
| wstate = json.loads(state_f.read_text()) if state_f.exists() else {} | |
| except Exception: | |
| wstate = {} | |
| now = time.time() | |
| ref_micro, ref_ts = wstate.get("ref", (None, None)) | |
| pace_min = wstate.get("pace_min") | |
| if ref_micro is None: | |
| wstate["ref"] = (cur_micro, now) | |
| pace_min = None | |
| elif now - ref_ts > 180 and cur_micro > ref_micro: | |
| micros_per_min = (cur_micro - ref_micro) / ((now - ref_ts) / 60) | |
| # 85 training micros + one rollout generation per step | |
| pace_min = grad_accum / micros_per_min + 2.5 | |
| wstate["ref"] = (cur_micro, now) | |
| wstate["pace_min"] = pace_min | |
| try: | |
| state_f.write_text(json.dumps(wstate)) | |
| except Exception: | |
| pass | |
| eta_min = pace_min * (total_steps - frac_opt) if pace_min else None | |
| print(f"{BOLD}{CYAN}β{'β' * (cols - 2)}β{RESET}") | |
| title = f" GRPO Stage-B β’ optimizer step {cur_opt}/{total_steps} " | |
| print(f"{BOLD}{CYAN}β{title.center(cols - 2)}β{RESET}") | |
| print(f"{BOLD}{CYAN}β{'β' * (cols - 2)}β{RESET}") | |
| print() | |
| # Staleness alarm: history must be written every few minutes while alive | |
| hist_age = (time.time() - HISTORY.stat().st_mtime) if HISTORY.exists() else None | |
| stale = (hist_age is None) or (hist_age > 900) | |
| if stale and torchrun_up: | |
| print(f" {BG_RED}{BOLD}{WHITE} β WEDGE? history.jsonl untouched for " | |
| f"{(hist_age or 0)/60:.0f} min while torchrun is alive β check the log {RESET}") | |
| print() | |
| # Status line | |
| status = f"{GREEN}RUNNING{RESET}" if torchrun_up else f"{BG_RED}{BOLD}{WHITE} STOPPED β NO TRAINING PROCESS {RESET}" | |
| engine = f"{GREEN}vLLM up{RESET}" if vllm_up else f"{YELLOW}vLLM down{RESET}" | |
| print(f" status: {status} | engine: {engine} | rows: {len(rows)} | applied steps: {len(applied)}") | |
| print(f" progress: {bar(frac_opt, total_steps, width=cols - 22)} {frac_opt:.1f}/{total_steps}") | |
| print(f" step {cur_opt}: micro {micro_in_step}/{grad_accum} {bar(micro_in_step, grad_accum, width=20, color=YELLOW)}") | |
| if pace_min: | |
| print(f" pace: {pace_min:.1f} min/step | elapsed: {elapsed/60:.0f} min | ETA: {eta_min/60:.1f} h") | |
| else: | |
| print(f" pace: measuring⦠(needs a few min of micro history) | elapsed: {elapsed/60:.0f} min") | |
| print() | |
| # Two-column metric panels | |
| left_w = cols // 2 - 3 | |
| right_w = cols - left_w - 6 | |
| def panel(title, lines, width): | |
| out = [f"{BOLD}{MAGENTA}ββ {title} {'β' * max(0, width - len(title) - 4)}β{RESET}"] | |
| for ln in lines: | |
| out.append(f"{MAGENTA}β{RESET} {ln.ljust(width - 2)} {MAGENTA}β{RESET}") | |
| out.append(f"{MAGENTA}β{'β' * (width - 2)}β{RESET}") | |
| return out | |
| train_lines = [ | |
| f"loss {fmt(loss[-1] if loss else None)} {sparkline(loss, 24)}", | |
| f"grad_norm {fmt(gn[-1] if gn else None)} med {fmt(st.median(gn) if gn else None)} max {fmt(max(gn) if gn else None)}", | |
| f"entropy {fmt(ent[-1] if ent else None)} {sparkline(ent, 24)}", | |
| f"lr {fmt(lr[-1] if lr else None, 8)}", | |
| f"micro {cur_micro}", | |
| ] | |
| reward_lines = [ | |
| f"acc {fmt(acc[-1] if acc else None)} mean {fmt(st.mean(acc) if acc else None)}", | |
| f"acc first50 {fmt(st.mean(acc[:50]) if len(acc) >= 50 else None)} last50 {fmt(st.mean(acc[-50:]) if len(acc) >= 50 else None)}", | |
| f"resp_len {fmt(resp[-1] if resp else None, 1)} {sparkline(resp, 24)}", | |
| f"canary {canary[-1].get('canary/pass@1') if canary else 'β'} (n={canary[-1].get('canary/n') if canary else 'β'})", | |
| f"canary step {canary[-1].get('optimizer_step') if canary else 'β'}", | |
| ] | |
| left = panel("Training", train_lines, left_w) | |
| right = panel("Reward / Canary", reward_lines, right_w) | |
| for l, r in zip(left, right): | |
| print(f" {l} {r}") | |
| print() | |
| # GPU panel | |
| print(f" {BOLD}{BLUE}GPUs{RESET}") | |
| for ln in gpu_lines(): | |
| print(f" {ln}") | |
| print() | |
| # Recent log tail | |
| print(f" {BOLD}{BLUE}Recent log (grpo_stageb.log){RESET}") | |
| for ln in tail(LOG, 5): | |
| print(f" {DIM}{ln[: cols - 6]}{RESET}") | |
| print() | |
| print(f" {DIM}updated {time.strftime('%H:%M:%S')} | Ctrl-C to exit{RESET}") | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--loop", type=int, default=0, help="refresh seconds; 0 = render once") | |
| args = ap.parse_args() | |
| if args.loop <= 0: | |
| render() | |
| return | |
| try: | |
| while True: | |
| # clear screen without shell (avoid os.system injection sink) | |
| sys.stdout.write("\033[2J\033[H") | |
| sys.stdout.flush() | |
| render() | |
| time.sleep(args.loop) | |
| except KeyboardInterrupt: | |
| pass | |
| if __name__ == "__main__": | |
| main() | |