예제 소스
바둑 on-policy 학습: 대국 로그를 KataGo 라벨로 바꾸기 · 파일 169개
daemon_overnight_mix.py
#!/usr/bin/env python3
"""밤새 혼합(좌표+수) 학습 ↔ 좌표 검증 루프.
# Cursor 터미널에서 로그 보이며 실행 (권장)
python3 scripts/daemon_overnight_mix.py --foreground
# OS 분리 데몬 (비권장)
python3 scripts/daemon_overnight_mix.py
시작: vlm_models/lora_mix_qwen_r1 (없으면 lora_coord_qwen)
마감: 다음날 08:00
"""
from __future__ import annotations
import json
import os
import random
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path("~/project")
PY = str(ROOT / ".venv" / "bin" / "python")
LOG = ROOT / "logs" / "vlm_eval_day" / "mix_overnight.log"
STATUS = ROOT / "logs" / "vlm_eval_day" / "MIX_STATUS.md"
COORD_JSONL = ROOT / "sft_data" / "coord_v3" / "train.jsonl"
MOVE_JSONL = ROOT / "sft_data" / "baduk_v3_aligned" / "train.jsonl"
MIX_R1 = ROOT / "vlm_models" / "lora_mix_qwen_r1"
COORD = ROOT / "vlm_models" / "lora_coord_qwen"
DEADLINE_HOUR = 8
LOCATE_GATE = 90.0
MODEL = "********"
ROUNDS = [
(1200, 2000, 1200, "1.5e-5"),
(1000, 2500, 1500, "1e-5"),
(1000, 3000, 1500, "1e-5"),
(800, 3500, 1800, "8e-6"),
(800, 4000, 1800, "8e-6"),
(600, 4500, 2000, "6e-6"),
]
FOREGROUND = False
def now() -> str:
return time.strftime("%Y-%m-%d %H:%M:%S")
def past_deadline() -> bool:
"""시작일(스탬프) 다음날 DEADLINE_HOUR 이후면 True."""
stamp = ROOT / "logs" / "vlm_eval_day" / ".overnight_start_ymd"
t = time.localtime()
today = time.strftime("%Y-%m-%d", t)
if not stamp.exists():
stamp.write_text(today, encoding="utf-8")
start = today
else:
start = stamp.read_text(encoding="utf-8").strip() or today
if today == start:
return False
return t.tm_hour >= DEADLINE_HOUR
def refresh_deadline_window() -> None:
"""재개 시 오늘을 시작일로 리셋 → 내일 08:00까지."""
stamp = ROOT / "logs" / "vlm_eval_day" / ".overnight_start_ymd"
stamp.parent.mkdir(parents=True, exist_ok=True)
stamp.write_text(time.strftime("%Y-%m-%d"), encoding="utf-8")
def round_passed(tag: int) -> bool:
summary = ROOT / f"logs/vlm_eval_day/coord_mix_r{tag}_summary.json"
adapter = ROOT / f"vlm_models/lora_mix_qwen_r{tag}/adapters.safetensors"
if not summary.exists() or not adapter.exists():
return False
return locate_acc(summary) >= LOCATE_GATE
def log(msg: str) -> None:
line = f"[{now()}] {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 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===== {now()} =====\n")
f.flush()
if FOREGROUND:
p = subprocess.Popen(
cmd,
cwd=str(ROOT),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
assert p.stdout is not None
for line in p.stdout:
sys.stdout.write(line)
sys.stdout.flush()
f.write(line)
f.flush()
rc = int(p.wait())
else:
p = subprocess.run(cmd, cwd=str(ROOT), env=env, stdout=f, stderr=subprocess.STDOUT)
rc = int(p.returncode)
log(f"EXIT {rc}")
return rc
def locate_acc(summary: Path) -> float:
if not summary.exists():
return -1.0
d = json.loads(summary.read_text(encoding="utf-8"))
return float(d.get("by_kind", {}).get("locate", {}).get("acc", d.get("accuracy", -1)))
def build_mix(coord_n: int, move_n: int, out_path: Path, seed: int) -> int:
rng = random.Random(seed)
with COORD_JSONL.open(encoding="utf-8") as f:
cl = [x for x in f if x.strip()]
with MOVE_JSONL.open(encoding="utf-8") as f:
ml = [x for x in f if x.strip()]
rng.shuffle(cl)
rng.shuffle(ml)
lines = cl[: min(coord_n, len(cl))] + ml[: min(move_n, len(ml))]
rng.shuffle(lines)
out_path.parent.mkdir(parents=True, exist_ok=True)
with out_path.open("w", encoding="utf-8") as f:
f.writelines(lines)
return len(lines)
def pipeline() -> int:
os.chdir(ROOT)
subprocess.run(["pkill", "-f", "vlm_server serve"], capture_output=True)
base = MIX_R1 if (MIX_R1 / "adapters.safetensors").exists() else COORD
if not (base / "adapters.safetensors").exists():
log("FATAL: no base adapter")
return 2
# 재개: 마감 창을 오늘→내일 08:00 으로 연장
refresh_deadline_window()
mode = "foreground/Cursor" if FOREGROUND else "daemon"
write_status(
f"# Overnight mix\n\n- resumed: {now()}\n- mode: **{mode}**\n"
f"- until: tomorrow {DEADLINE_HOUR}:00\n"
f"- base: `{base.name}`\n"
f"- focus: skip PASS rounds, continue incomplete\n"
)
log(f"=== OVERNIGHT MIX RESUME mode={mode} base={base} until tomorrow {DEADLINE_HOUR}:00 ===")
if not COORD_JSONL.exists() or not MOVE_JSONL.exists():
log("FATAL: datasets missing")
return 2
good_adapter = str(base)
history: list[str] = []
fail_streak = 0
out_start = 2 if base == MIX_R1 else 1
# 이미 PASS 한 라운드 반영
for tip in range(out_start, out_start + len(ROUNDS) + 2):
if round_passed(tip):
good_adapter = str(ROOT / f"vlm_models/lora_mix_qwen_r{tip}")
history.append(f"- R{tip} already PASS locate (skip) → `lora_mix_qwen_r{tip}`")
log(f"skip R{tip} (already PASS)")
for round_i, (coord_n, move_n, max_samples, lr) in enumerate(ROUNDS):
if past_deadline():
break
tag = out_start + round_i
if round_passed(tag):
continue
if fail_streak > 0:
coord_n = int(coord_n * 1.4)
move_n = max(500, int(move_n * 0.55))
lr = "1e-5"
out_dir = ROOT / f"vlm_models/lora_mix_qwen_r{tag}"
mix_path = ROOT / f"sft_data/mix_overnight_r{tag}/train.jsonl"
n = build_mix(coord_n, move_n, mix_path, seed=300 + tag)
# 중단된 라운드: 체크포인트가 있으면 그 위에서 남은 iters만
resume_from = good_adapter
train_samples = max_samples
if (out_dir / "adapters.safetensors").exists() and not round_passed(tag):
# r3 @1000/1500 등 → 남은 분량만 추가 학습
remain = max(300, max_samples // 3)
resume_from = str(out_dir)
train_samples = remain
log(f"R{tag}: continue from partial checkpoint +{train_samples} iters")
log(
f"R{tag}: mix={n} coord~{coord_n} move~{move_n} samples={train_samples} "
f"lr={lr} resume={resume_from}"
)
write_status(
"\n".join(
[
"# Overnight mix",
f"- updated: {now()}",
f"- mode: **{mode}**",
f"- round: **r{tag}** / train",
f"- mix: coord~{coord_n} + move~{move_n}",
f"- resume: `{resume_from}`",
f"- lr: {lr}, max_samples: {train_samples}",
"",
*history[-12:],
"",
]
)
)
if past_deadline():
break
rc = run(
[
PY,
"-m",
"vlm_server.train_lora",
"--dataset-jsonl",
str(mix_path),
"--model",
MODEL,
"--output-dir",
str(out_dir),
"--resume-from",
resume_from,
"--epochs",
"1",
"--batch-size",
"1",
"--grad-accum",
"8",
"--lr",
lr,
"--lora-rank",
"16",
"--lora-alpha",
"32",
"--max-seq-length",
"4096",
"--max-samples",
str(train_samples),
"--grad-checkpoint",
],
ROOT / f"logs/vlm_eval_day/mix_train_r{tag}.log",
)
if rc != 0:
history.append(f"- R{tag} train FAILED rc={rc}")
fail_streak += 1
continue
sum_path = ROOT / f"logs/vlm_eval_day/coord_mix_r{tag}_summary.json"
run(
[
PY,
"-m",
"vlm_server.verify_coords",
"--model",
MODEL,
"--lora-dir",
str(out_dir),
"--board-format",
"aligned",
"--trials",
"48",
"--out-jsonl",
str(ROOT / f"logs/vlm_eval_day/coord_mix_r{tag}.jsonl"),
"--out-summary",
str(sum_path),
],
ROOT / "logs/vlm_eval_day/coord_verify_mix.log",
)
acc = locate_acc(sum_path)
log(f"R{tag} locate={acc}% gate={LOCATE_GATE}")
if acc >= LOCATE_GATE:
good_adapter = str(out_dir)
fail_streak = 0
history.append(f"- R{tag} PASS locate **{acc}%** → `{out_dir.name}`")
else:
fail_streak += 1
history.append(
f"- R{tag} FAIL locate **{acc}%** → rollback `{Path(good_adapter).name}`"
)
if fail_streak >= 3:
history.append("- fail_streak>=3 → stop")
break
write_status(
"\n".join(
[
"# Overnight mix",
f"- updated: {now()}",
f"- mode: **{mode}**",
f"- last: r{tag} locate **{acc}%**",
f"- best: `{good_adapter}`",
f"- fail_streak: {fail_streak}",
"",
"## history",
*history,
"",
]
)
)
write_status(
(STATUS.read_text(encoding="utf-8") if STATUS.exists() else "")
+ f"\n\n## finished\n- {now()}\n- best: `{good_adapter}`\n"
)
log(f"=== OVERNIGHT MIX END best={good_adapter} ===")
return 0
def main() -> int:
global FOREGROUND
FOREGROUND = "--foreground" in sys.argv
if FOREGROUND:
return pipeline()
# OS double-fork daemon
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)
os._exit(pipeline())
if __name__ == "__main__":
raise SystemExit(main())