게시판바둑 on-policy 학습: 대국 로그를 KataGo 라벨로 바꾸기예제 소스

예제 소스

바둑 on-policy 학습: 대국 로그를 KataGo 라벨로 바꾸기 · 파일 169개

daemon_overnight_pipeline.py

#!/usr/bin/env python3
"""Overnight pipeline (no user confirmation):

1) Wait for Qwen smoke LoRA train to finish
2) Coordinate grounding verify: Qwen base, then smoke LoRA
3) If base locate accuracy >= 90%% → start full baduk_v3 Qwen LoRA train
4) Otherwise write FAIL note and stop (do not waste a night on broken coords)

Logs: logs/vlm_eval_day/overnight.log
Status: logs/vlm_eval_day/OVERNIGHT_STATUS.md
"""
from __future__ import annotations

import json
import os
import subprocess
import time
from pathlib import Path

ROOT = Path("~/project")
PY = str(ROOT / ".venv" / "bin" / "python")
LOG = ROOT / "logs" / "vlm_eval_day" / "overnight.log"
STATUS = ROOT / "logs" / "vlm_eval_day" / "OVERNIGHT_STATUS.md"
SMOKE_ADAPTER = ROOT / "vlm_models" / "lora_baduk_v3_qwen_smoke" / "adapters.safetensors"
FULL_OUT = ROOT / "vlm_models" / "lora_baduk_v3_qwen"
LOCATE_GATE = 90.0


def log(msg: str) -> None:
    line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}"
    print(line, flush=True)
    LOG.parent.mkdir(parents=True, exist_ok=True)
    with LOG.open("a", encoding="utf-8") as f:
        f.write(line + "\n")


def write_status(md: str) -> None:
    STATUS.write_text(md, encoding="utf-8")


def pgrep(pattern: str) -> bool:
    r = subprocess.run(["pgrep", "-f", pattern], capture_output=True)
    return r.returncode == 0


def wait_until(pred, label: str, timeout_s: float, poll_s: float = 20.0) -> bool:
    t0 = time.time()
    while time.time() - t0 < timeout_s:
        if pred():
            return True
        log(f"waiting: {label} ({int(time.time() - t0)}s)")
        time.sleep(poll_s)
    return pred()


def smoke_done() -> bool:
    if pgrep(r"mlx_vlm\.lora.*lora_baduk_v3_qwen_smoke"):
        return False
    if pgrep(r"train_lora.*lora_baduk_v3_qwen_smoke"):
        return False
    return SMOKE_ADAPTER.exists() and SMOKE_ADAPTER.stat().st_size > 1_000_000


def run(cmd: list[str], log_path: Path) -> int:
    log("RUN: " + " ".join(cmd))
    env = os.environ.copy()
    env["HF_HOME"] = str(ROOT / "vlm_models")
    env["HF_HUB_CACHE"] = str(ROOT / "vlm_models")
    env["TOKENIZERS_PARALLELISM"] = "********"
    env["PYTHONUNBUFFERED"] = "1"
    log_path.parent.mkdir(parents=True, exist_ok=True)
    with log_path.open("a", encoding="utf-8") as f:
        f.write(f"\n===== {time.strftime('%Y-%m-%d %H:%M:%S')} =====\n")
        f.flush()
        p = subprocess.run(cmd, cwd=str(ROOT), env=env, stdout=f, stderr=subprocess.STDOUT)
    log(f"EXIT {p.returncode}: {' '.join(cmd[:6])}...")
    return int(p.returncode)


def locate_acc(summary_path: Path) -> float:
    if not summary_path.exists():
        return -1.0
    data = json.loads(summary_path.read_text(encoding="utf-8"))
    loc = data.get("by_kind", {}).get("locate", {})
    return float(loc.get("acc", data.get("accuracy", -1.0)))


def main() -> int:
    # double-fork
    if os.fork() > 0:
        return 0
    os.setsid()
    if os.fork() > 0:
        os._exit(0)
    os.chdir(ROOT)
    os.umask(0)
    with LOG.open("a", encoding="utf-8") as f:
        os.dup2(f.fileno(), 1)
        os.dup2(f.fileno(), 2)
    dn = os.open(os.devnull, os.O_RDONLY)
    os.dup2(dn, 0)
    os.close(dn)

    write_status(
        f"# Overnight status\n\n- started: {time.strftime('%Y-%m-%d %H:%M:%S')}\n"
        "- phase: waiting_smoke\n"
    )
    log("=== OVERNIGHT PIPELINE START ===")

    # Kill older coord-only waiter if still around (avoid GPU fight later)
    subprocess.run(["pkill", "-f", "daemon_coord_verify_after_smoke.py"], capture_output=True)

    if not wait_until(smoke_done, "smoke train finish", timeout_s=4 * 3600):
        log("FAIL: smoke train timeout")
        write_status("# Overnight FAIL\n\nSmoke train did not finish in time.\n")
        os._exit(1)
    log("smoke train finished")
    write_status(
        f"# Overnight status\n\n- smoke: DONE @ {time.strftime('%H:%M:%S')}\n"
        "- phase: coord_verify\n"
    )

    # brief pause so Metal releases
    time.sleep(10)

    base_sum = ROOT / "logs" / "vlm_eval_day" / "coord_qwen_base_summary.json"
    smoke_sum = ROOT / "logs" / "vlm_eval_day" / "coord_qwen_smoke_summary.json"
    coord_log = ROOT / "logs" / "vlm_eval_day" / "coord_verify_run.log"

    rc_base = run(
        [
            PY,
            "-m",
            "vlm_server.verify_coords",
            "--model",
            "********",
            "--board-format",
            "aligned",
            "--trials",
            "48",
            "--out-jsonl",
            str(ROOT / "logs" / "vlm_eval_day" / "coord_qwen_base.jsonl"),
            "--out-summary",
            str(base_sum),
        ],
        coord_log,
    )
    time.sleep(5)
    rc_smoke = run(
        [
            PY,
            "-m",
            "vlm_server.verify_coords",
            "--model",
            "********",
            "--lora-dir",
            "********",
            "--board-format",
            "aligned",
            "--trials",
            "48",
            "--out-jsonl",
            str(ROOT / "logs" / "vlm_eval_day" / "coord_qwen_smoke.jsonl"),
            "--out-summary",
            str(smoke_sum),
        ],
        coord_log,
    )

    acc_base = locate_acc(base_sum)
    acc_smoke = locate_acc(smoke_sum)
    log(f"coord locate acc: base={acc_base} smoke={acc_smoke} (gate>={LOCATE_GATE})")

    gate_ok = acc_base >= LOCATE_GATE
    write_status(
        "\n".join(
            [
                "# Overnight status",
                "",
                f"- updated: {time.strftime('%Y-%m-%d %H:%M:%S')}",
                f"- smoke train: DONE",
                f"- coord base locate: **{acc_base}%** (rc={rc_base})",
                f"- coord smoke locate: **{acc_smoke}%** (rc={rc_smoke})",
                f"- gate (>= {LOCATE_GATE}% on **base**): {'PASS' if gate_ok else 'FAIL'}",
                f"- phase: {'full_train' if gate_ok else 'stopped'}",
                "",
                "Details: `coord_qwen_base_summary.json`, `coord_qwen_smoke_summary.json`",
                "Logs: `overnight.log`, `coord_verify_run.log`, `qwen_full_train.log`",
                "",
            ]
        )
    )

    if not gate_ok:
        log("GATE FAIL — skip full train")
        os._exit(3)

    log("GATE PASS — starting full Qwen LoRA train on baduk_v3")
    FULL_OUT.mkdir(parents=True, exist_ok=True)
    full_log = ROOT / "logs" / "vlm_eval_day" / "qwen_full_train.log"
    # Do not block forever in this process: launch train as child and wait
    rc_full = run(
        [
            PY,
            "-m",
            "vlm_server.train_lora",
            "--dataset-jsonl",
            "sft_data/baduk_v3/train.jsonl",
            "--val-jsonl",
            "sft_data/baduk_v3/val.jsonl",
            "--model",
            "********",
            "--output-dir",
            "vlm_models/lora_baduk_v3_qwen",
            "--epochs",
            "2",
            "--batch-size",
            "1",
            "--grad-accum",
            "16",
            "--lr",
            "5e-5",
            "--lora-rank",
            "16",
            "--lora-alpha",
            "32",
            "--max-seq-length",
            "4096",
            "--grad-checkpoint",
        ],
        full_log,
    )
    log(f"full train exit={rc_full}")
    write_status(
        "\n".join(
            [
                "# Overnight DONE",
                "",
                f"- finished: {time.strftime('%Y-%m-%d %H:%M:%S')}",
                f"- coord base locate: {acc_base}%",
                f"- coord smoke locate: {acc_smoke}%",
                f"- full train exit: {rc_full}",
                f"- adapter: `vlm_models/lora_baduk_v3_qwen/`",
                "",
                "Morning: check OVERNIGHT_STATUS.md + qwen_full_train.log",
                "",
            ]
        )
    )
    os._exit(0 if rc_full == 0 else 4)


if __name__ == "__main__":
    raise SystemExit(main())