게시판SGF 기보로 학습하는 바둑 LLM 인공지능예제 소스

예제 소스

SGF 기보로 학습하는 바둑 LLM 인공지능 · 파일 169개

run_gemma_r5_match.py

#!/usr/bin/env python3
"""Gemma 4(흑)와 R5(백) 무제한 5판 후, 각자 국면을 KataGo 정답으로 순서대로 학습한다.

R5가 9319에서 이미 떠 있으면 그대로 쓴다. Gemma 는 9341에만 띄운다.
학습이 끝나면 서빙 중인 R5 어댑터는 바꾸지 않고, 새 어댑터는 models.json 에 이름만 추가한다.
"""

from __future__ import annotations

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

ROOT = Path(__file__).resolve().parents[1]
PY = ROOT / ".venv" / "bin" / "python"
OUT = ROOT / "logs" / "gemma4_r5_20261009"
REGISTRY = ROOT / "scripts" / "models.json"


def log(msg: str) -> None:
    print(msg, flush=True)


def port_open(port: int) -> bool:
    sock = socket.socket()
    sock.settimeout(1)
    try:
        sock.connect(("127.0.0.1", port))
        return True
    except OSError:
        return False
    finally:
        sock.close()


def wait_port(port: int, timeout_s: float) -> bool:
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        if port_open(port):
            return True
        time.sleep(5)
    return False


def start_serve(name: str, spec: dict, jsonl: Path) -> subprocess.Popen | None:
    if port_open(int(spec["port"])):
        log(f"[runner] {name} port {spec['port']} already up")
        return None
    reg = json.loads(REGISTRY.read_text(encoding="utf-8"))
    cmd = [
        str(PY),
        "-m",
        reg["serve"]["module"],
        "serve",
        "--host",
        reg["serve"]["host"],
        "--port",
        str(spec["port"]),
        "--model",
        spec["model"],
        "--log-jsonl",
        str(jsonl),
        *reg["serve"]["flags"],
    ]
    if spec.get("lora"):
        cmd.extend(["--lora-dir", spec["lora"]])
    jsonl.parent.mkdir(parents=True, exist_ok=True)
    logf = open(OUT / f"{name}_serve.stdout", "ab")
    log(f"[runner] start {name} port {spec['port']}")
    proc = subprocess.Popen(
        cmd,
        cwd=ROOT,
        stdout=logf,
        stderr=subprocess.STDOUT,
        start_new_session=True,
    )
    if not wait_port(int(spec["port"]), 600):
        raise SystemExit(f"{name} did not listen on {spec['port']}")
    log(f"[runner] {name} listening pid={proc.pid}")
    return proc


def stop_proc(proc: subprocess.Popen | None) -> None:
    if proc is None or proc.poll() is not None:
        return
    log(f"[runner] stop pid {proc.pid}")
    proc.terminate()
    try:
        proc.wait(timeout=30)
    except subprocess.TimeoutExpired:
        proc.kill()


def run(cmd: list[str], log_path: Path) -> int:
    log("[runner] " + " ".join(cmd))
    log_path.parent.mkdir(parents=True, exist_ok=True)
    with log_path.open("ab") as fh:
        rc = subprocess.call(cmd, cwd=ROOT, stdout=fh, stderr=subprocess.STDOUT)
    log(f"[runner] rc={rc} log={log_path}")
    return rc


def finished_games(db: Path) -> int:
    import sqlite3

    con = sqlite3.connect(db)
    n = con.execute("select count(*) from games where finished_at is not null and end_reason!='error'").fetchone()[0]
    con.close()
    return int(n)


def save_registry(reg: dict) -> None:
    REGISTRY.write_text(json.dumps(reg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def main() -> int:
    OUT.mkdir(parents=True, exist_ok=True)
    reg = json.loads(REGISTRY.read_text(encoding="utf-8"))
    models = reg["models"]
    gemma_proc = start_serve("gemma4", models["gemma4"], OUT / "gemma4.jsonl")
    if not port_open(int(models["r5"]["port"])):
        raise SystemExit("R5 port 9319 is down; not starting a second Qwen")

    rc = run(
        [
            str(PY),
            "scripts/play_two_models.py",
            "--black",
            "gemma4",
            "--white",
            "r5",
            "--games",
            "5",
            "--max-moves",
            "0",
            "--out-dir",
            str(OUT.relative_to(ROOT)),
        ],
        OUT / "play.log",
    )
    stop_proc(gemma_proc)
    if rc != 0:
        return rc
    db = OUT / "matches.sqlite"
    n = finished_games(db)
    log(f"[runner] finished games={n}")
    if n < 3:
        log("[runner] too few games, skip learning")
        return 0

    label_jobs = [
        ("W", "sft_data/onpolicy_r5_vs_gemma4", OUT / "label_r5.log"),
        ("B", "sft_data/onpolicy_gemma4_vs_r5", OUT / "label_gemma4.log"),
    ]
    for color, dest, log_path in label_jobs:
        rc = run(
            [
                str(PY),
                "-m",
                "vlm_server.dataset_onpolicy",
                "--db",
                str(db),
                "--color",
                color,
                "--out",
                dest,
            ],
            log_path,
        )
        if rc != 0:
            return rc

    qwen = models["r5"]["model"]
    gemma = models["gemma4"]["model"]
    r5_out = "********"
    gemma_out = "********"
    rc = run(
        [
            str(PY),
            "scripts/train_ladder_lora.py",
            "--shape",
            "onpolicy",
            "--data-name",
            "onpolicy_r5_vs_gemma4",
            "--log-name",
            "r5_from_gemma4",
            "--model",
            qwen,
            "--base",
            models["r5"]["lora"],
            "--out",
            r5_out,
            "--shape-n",
            "1200",
            "--skip-base-eval",
        ],
        OUT / "train_r5.log",
    )
    if rc != 0:
        return rc
    rc = run(
        [
            str(PY),
            "scripts/train_ladder_lora.py",
            "--shape",
            "onpolicy",
            "--data-name",
            "onpolicy_gemma4_vs_r5",
            "--log-name",
            "gemma4_from_r5",
            "--model",
            gemma,
            "--base",
            "-",
            "--out",
            gemma_out,
            "--shape-n",
            "1200",
            "--skip-base-eval",
        ],
        OUT / "train_gemma4.log",
    )
    if rc != 0:
        return rc

    reg = json.loads(REGISTRY.read_text(encoding="utf-8"))
    reg["models"]["gemma4"]["lora"] = gemma_out
    reg["models"]["gemma4"]["note"] = "R5 대국 학습 어댑터. 9341이 비어 있을 때만 이 가중치로 띄운다."
    reg["models"]["r5_from_gemma4"] = {
        "port": 9343,
        "model": qwen,
        "lora": r5_out,
        "note": "Gemma4 대국으로 배운 R5. 9319 서빙 어댑터는 바꾸지 않았다.",
    }
    save_registry(reg)
    log("[runner] learning done; serving R5 adapter unchanged")
    return 0


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