QwenJev v0.4.3: 25-public-games eval in-repo (run_eval incremental report, fetch_games, CPU feasibility verified)
Browse files- Makefile +4 -1
- data/collect_selfplay.py +102 -17
- data/mock_env.py +3 -2
- eval_report_public25_cpu.json +273 -0
- ops/README.md +15 -1
- ops/fetch_games.py +73 -0
- ops/run_eval.py +58 -25
- ops/t4exec.py +12 -9
- scripts/push_hf.py +1 -1
- train.md +43 -251
- train/evaluate_local.py +21 -4
Makefile
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
.PHONY: setup smoke collect build-data distill train-s1 calibrate eval eval-v2 eval-25 eval-cpu agent-local push-hf test clean
|
| 2 |
|
| 3 |
PY ?= python3.13
|
| 4 |
|
|
@@ -43,6 +43,9 @@ eval-25: ## evaluate on the 25 public games (needs arc-agi + environmen
|
|
| 43 |
eval-cpu: ## CPU harness test (tiny backend, no checkpoint, 60 actions)
|
| 44 |
$(PY) ops/run_eval.py --backend tiny --no-checkpoint --games mock --max-actions 60
|
| 45 |
|
|
|
|
|
|
|
|
|
|
| 46 |
agent-local: ## run the hybrid agent locally via the ARC-AGI-3-Agents runner
|
| 47 |
$(PY) ../ARC-AGI-3-Agents/main.py --agent=qwenjevagent --game=ls20
|
| 48 |
|
|
|
|
| 1 |
+
.PHONY: setup smoke collect build-data distill train-s1 calibrate eval eval-v2 eval-25 eval-cpu fetch-games agent-local push-hf test clean
|
| 2 |
|
| 3 |
PY ?= python3.13
|
| 4 |
|
|
|
|
| 43 |
eval-cpu: ## CPU harness test (tiny backend, no checkpoint, 60 actions)
|
| 44 |
$(PY) ops/run_eval.py --backend tiny --no-checkpoint --games mock --max-actions 60
|
| 45 |
|
| 46 |
+
fetch-games: ## cache all 25 public games locally (anonymous arcade key, ~3MB, no Kaggle account)
|
| 47 |
+
$(PY) ops/fetch_games.py
|
| 48 |
+
|
| 49 |
agent-local: ## run the hybrid agent locally via the ARC-AGI-3-Agents runner
|
| 50 |
$(PY) ../ARC-AGI-3-Agents/main.py --agent=qwenjevagent --game=ls20
|
| 51 |
|
data/collect_selfplay.py
CHANGED
|
@@ -42,46 +42,131 @@ PUBLIC_GAMES_FALLBACK = [
|
|
| 42 |
]
|
| 43 |
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
def make_env(game_id: str, render_mode: str | None):
|
| 46 |
"""Open one game: the built-in mock (no downloads) or the official
|
| 47 |
offline engine (`arc-agi` package + local game files).
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
if game_id.startswith("mock"):
|
| 53 |
# deterministic seed: reproducible levels across processes
|
| 54 |
return make_mock_env(seed=zlib.crc32(game_id.encode()) % (2 ** 31))
|
| 55 |
import arc_agi
|
| 56 |
from arc_agi import OperationMode
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
|
| 64 |
def grids_from(env) -> list:
|
| 65 |
-
"""Best-effort frame extraction across toolkit versions.
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
if frame is None:
|
| 68 |
-
frame =
|
| 69 |
for attr in ("frames", "frame", "grids"):
|
| 70 |
grids = getattr(frame, attr, None)
|
| 71 |
-
if grids:
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
raise RuntimeError("could not read frame grids from the environment")
|
| 76 |
|
| 77 |
|
| 78 |
def meta_from(env, frame=None) -> dict:
|
| 79 |
-
frame = frame or (env
|
|
|
|
|
|
|
|
|
|
| 80 |
return {
|
| 81 |
"levels_completed": int(getattr(frame, "levels_completed", 0) or 0),
|
| 82 |
"win_levels": int(getattr(frame, "win_levels", 0) or 0),
|
| 83 |
-
"available_actions":
|
| 84 |
-
|
|
|
|
| 85 |
}
|
| 86 |
|
| 87 |
|
|
|
|
| 42 |
]
|
| 43 |
|
| 44 |
|
| 45 |
+
def _env_dir() -> str:
|
| 46 |
+
return os.environ.get("ARC_ENVIRONMENTS_DIR", "environment_files")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
def make_env(game_id: str, render_mode: str | None):
|
| 50 |
"""Open one game: the built-in mock (no downloads) or the official
|
| 51 |
offline engine (`arc-agi` package + local game files).
|
| 52 |
|
| 53 |
+
Real games are resolved in two steps:
|
| 54 |
+
1. OFFLINE mode against the local cache (ARC_ENVIRONMENTS_DIR or
|
| 55 |
+
./environment_files) — zero network, ~2000 FPS;
|
| 56 |
+
2. if the game is not cached and QWENJEV_AUTO_DOWNLOAD!=0, bootstrap
|
| 57 |
+
through NORMAL mode, which fetches the game source from the arcade
|
| 58 |
+
API (anonymous key works) and caches it forever. No Kaggle
|
| 59 |
+
credentials are needed for the 25 public games."""
|
| 60 |
if game_id.startswith("mock"):
|
| 61 |
# deterministic seed: reproducible levels across processes
|
| 62 |
return make_mock_env(seed=zlib.crc32(game_id.encode()) % (2 ** 31))
|
| 63 |
import arc_agi
|
| 64 |
from arc_agi import OperationMode
|
| 65 |
|
| 66 |
+
env_dir = _env_dir()
|
| 67 |
+
try:
|
| 68 |
+
arc = arc_agi.Arcade(operation_mode=OperationMode.OFFLINE,
|
| 69 |
+
environments_dir=env_dir)
|
| 70 |
+
env = arc.make(game_id, render_mode=render_mode) if render_mode \
|
| 71 |
+
else arc.make(game_id)
|
| 72 |
+
if env is not None:
|
| 73 |
+
return env
|
| 74 |
+
except Exception:
|
| 75 |
+
pass # fall through to the bootstrap path
|
| 76 |
+
|
| 77 |
+
if os.environ.get("QWENJEV_AUTO_DOWNLOAD", "1") != "1":
|
| 78 |
+
raise RuntimeError(
|
| 79 |
+
f"game {game_id} not in {env_dir}; run ops/fetch_games.py or "
|
| 80 |
+
"export QWENJEV_AUTO_DOWNLOAD=1")
|
| 81 |
+
arc = arc_agi.Arcade(operation_mode=OperationMode.NORMAL,
|
| 82 |
+
environments_dir=env_dir)
|
| 83 |
+
env = arc.make(game_id, render_mode=render_mode) if render_mode \
|
| 84 |
+
else arc.make(game_id)
|
| 85 |
+
if env is None:
|
| 86 |
+
raise RuntimeError(f"could not download/create game {game_id}")
|
| 87 |
+
return env
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _official_frame(env):
|
| 91 |
+
"""Last FrameDataRaw from an official EnvironmentWrapper (or None)."""
|
| 92 |
+
return getattr(env, "observation_space", None) \
|
| 93 |
+
or getattr(env, "_last_response", None)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _resolve_frame(env):
|
| 97 |
+
"""Uniform frame resolution across mock + official environments."""
|
| 98 |
+
if hasattr(env, "observe"):
|
| 99 |
+
f = env.observe()
|
| 100 |
+
if f is not None:
|
| 101 |
+
return f
|
| 102 |
+
f = _official_frame(env)
|
| 103 |
+
return f if f is not None else env
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _to_grid_lists(grids) -> list:
|
| 107 |
+
"""ndarray | nested lists -> plain nested int lists, dropping empties."""
|
| 108 |
+
import numpy as np
|
| 109 |
+
out = []
|
| 110 |
+
for g in grids:
|
| 111 |
+
arr = np.asarray(g)
|
| 112 |
+
if arr.ndim < 2 or arr.size == 0:
|
| 113 |
+
continue
|
| 114 |
+
out.append(arr.astype(int).tolist())
|
| 115 |
+
return out
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _action_names(ids) -> list[str]:
|
| 119 |
+
"""Official frame.available_actions are GameAction ids (ints) — map to
|
| 120 |
+
names. Name strings pass through untouched (mock compatibility)."""
|
| 121 |
+
if not ids:
|
| 122 |
+
return []
|
| 123 |
+
if isinstance(ids[0], str):
|
| 124 |
+
return list(ids)
|
| 125 |
+
from arcengine import GameAction
|
| 126 |
+
names = []
|
| 127 |
+
for i in ids:
|
| 128 |
+
try:
|
| 129 |
+
name = GameAction.from_id(int(i)).name
|
| 130 |
+
except Exception:
|
| 131 |
+
continue
|
| 132 |
+
if name != "RESET": # lifecycle-guarded, never a candidate
|
| 133 |
+
names.append(name)
|
| 134 |
+
return names or ["ACTION1", "ACTION2", "ACTION3", "ACTION4"]
|
| 135 |
|
| 136 |
|
| 137 |
def grids_from(env) -> list:
|
| 138 |
+
"""Best-effort frame extraction across toolkit versions.
|
| 139 |
+
|
| 140 |
+
Official EnvironmentWrapper steps return FrameDataRaw whose `.frame`
|
| 141 |
+
holds a numpy array (raw=True path) of shape (n_frames, H, W); mock
|
| 142 |
+
frames carry nested lists under `.frames`. Both end up as nested ints."""
|
| 143 |
+
frame = getattr(env, "frame", None) # official engine exposes .frame too
|
| 144 |
if frame is None:
|
| 145 |
+
frame = _resolve_frame(env)
|
| 146 |
for attr in ("frames", "frame", "grids"):
|
| 147 |
grids = getattr(frame, attr, None)
|
| 148 |
+
if grids is not None and len(grids):
|
| 149 |
+
out = _to_grid_lists(grids)
|
| 150 |
+
if out:
|
| 151 |
+
return out
|
| 152 |
+
if isinstance(frame, (list, tuple)) and len(frame):
|
| 153 |
+
out = _to_grid_lists(frame)
|
| 154 |
+
if out:
|
| 155 |
+
return out
|
| 156 |
raise RuntimeError("could not read frame grids from the environment")
|
| 157 |
|
| 158 |
|
| 159 |
def meta_from(env, frame=None) -> dict:
|
| 160 |
+
frame = frame or _resolve_frame(env)
|
| 161 |
+
state = getattr(frame, "state", "NOT_FINISHED")
|
| 162 |
+
# enum GameState -> "WIN"; plain strings pass through (mock)
|
| 163 |
+
state = getattr(state, "name", str(state))
|
| 164 |
return {
|
| 165 |
"levels_completed": int(getattr(frame, "levels_completed", 0) or 0),
|
| 166 |
"win_levels": int(getattr(frame, "win_levels", 0) or 0),
|
| 167 |
+
"available_actions": _action_names(
|
| 168 |
+
getattr(frame, "available_actions", ()) or ()),
|
| 169 |
+
"state": state,
|
| 170 |
}
|
| 171 |
|
| 172 |
|
data/mock_env.py
CHANGED
|
@@ -302,6 +302,7 @@ def step_env(env, action_name: str, x: int | None = None, y: int | None = None):
|
|
| 302 |
from arcengine import GameAction # official toolkit
|
| 303 |
|
| 304 |
action = GameAction.from_name(action_name)
|
| 305 |
-
if
|
| 306 |
-
|
|
|
|
| 307 |
return env.step(action)
|
|
|
|
| 302 |
from arcengine import GameAction # official toolkit
|
| 303 |
|
| 304 |
action = GameAction.from_name(action_name)
|
| 305 |
+
if x is not None and y is not None and action.is_complex():
|
| 306 |
+
# official signature: step(action, data={"x": .., "y": ..})
|
| 307 |
+
return env.step(action, data={"x": int(x), "y": int(y)})
|
| 308 |
return env.step(action)
|
eval_report_public25_cpu.json
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"meta": {
|
| 3 |
+
"title": "25 public ARC-AGI-3 games \u2014 local CPU sweep",
|
| 4 |
+
"backend": "tiny (v0.2.0 trained heads, checkpoints/qwenjev-s1)",
|
| 5 |
+
"device": "cpu (2 cores, 3GB RAM container)",
|
| 6 |
+
"max_actions": 40,
|
| 7 |
+
"games_expected": 25,
|
| 8 |
+
"engine": "arc-agi 0.9.9 OFFLINE mode, environment_files/ cache",
|
| 9 |
+
"timestamp": "2026-09-23 00:12:42",
|
| 10 |
+
"note": "harness validation + relative-comparison baseline; NOT the v2 hf model (2B fp16 needs >=8GB RAM; see train.md 8.2). GAME_OVER auto-reset within budget."
|
| 11 |
+
},
|
| 12 |
+
"games": [
|
| 13 |
+
{
|
| 14 |
+
"game_id": "ar25",
|
| 15 |
+
"win": false,
|
| 16 |
+
"final_state": "NOT_FINISHED",
|
| 17 |
+
"best_level": 0,
|
| 18 |
+
"resets": 0,
|
| 19 |
+
"actions": 40,
|
| 20 |
+
"seconds": 79.9,
|
| 21 |
+
"steps_per_s": 0.5
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"game_id": "bp35",
|
| 25 |
+
"win": false,
|
| 26 |
+
"final_state": "NOT_FINISHED",
|
| 27 |
+
"best_level": 0,
|
| 28 |
+
"resets": 2,
|
| 29 |
+
"actions": 38,
|
| 30 |
+
"seconds": 67.6,
|
| 31 |
+
"steps_per_s": 0.56
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"game_id": "cd82",
|
| 35 |
+
"win": false,
|
| 36 |
+
"final_state": "NOT_FINISHED",
|
| 37 |
+
"best_level": 0,
|
| 38 |
+
"resets": 0,
|
| 39 |
+
"actions": 40,
|
| 40 |
+
"seconds": 76.1,
|
| 41 |
+
"steps_per_s": 0.53
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"game_id": "cn04",
|
| 45 |
+
"win": false,
|
| 46 |
+
"final_state": "NOT_FINISHED",
|
| 47 |
+
"best_level": 0,
|
| 48 |
+
"resets": 0,
|
| 49 |
+
"actions": 40,
|
| 50 |
+
"seconds": 74.1,
|
| 51 |
+
"steps_per_s": 0.54
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"game_id": "dc22",
|
| 55 |
+
"win": false,
|
| 56 |
+
"final_state": "NOT_FINISHED",
|
| 57 |
+
"best_level": 0,
|
| 58 |
+
"resets": 0,
|
| 59 |
+
"actions": 40,
|
| 60 |
+
"seconds": 73.1,
|
| 61 |
+
"steps_per_s": 0.55
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"game_id": "ft09",
|
| 65 |
+
"win": false,
|
| 66 |
+
"final_state": "NOT_FINISHED",
|
| 67 |
+
"best_level": 0,
|
| 68 |
+
"resets": 0,
|
| 69 |
+
"actions": 40,
|
| 70 |
+
"seconds": 65.7,
|
| 71 |
+
"steps_per_s": 0.61
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"game_id": "g50t",
|
| 75 |
+
"win": false,
|
| 76 |
+
"final_state": "NOT_FINISHED",
|
| 77 |
+
"best_level": 0,
|
| 78 |
+
"resets": 0,
|
| 79 |
+
"actions": 40,
|
| 80 |
+
"seconds": 10.1,
|
| 81 |
+
"steps_per_s": 3.96
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"game_id": "ka59",
|
| 85 |
+
"win": false,
|
| 86 |
+
"final_state": "NOT_FINISHED",
|
| 87 |
+
"best_level": 0,
|
| 88 |
+
"resets": 0,
|
| 89 |
+
"actions": 40,
|
| 90 |
+
"seconds": 73.3,
|
| 91 |
+
"steps_per_s": 0.55
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"game_id": "lf52",
|
| 95 |
+
"win": false,
|
| 96 |
+
"final_state": "NOT_FINISHED",
|
| 97 |
+
"best_level": 0,
|
| 98 |
+
"resets": 0,
|
| 99 |
+
"actions": 40,
|
| 100 |
+
"seconds": 78.4,
|
| 101 |
+
"steps_per_s": 0.51
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"game_id": "lp85",
|
| 105 |
+
"win": false,
|
| 106 |
+
"final_state": "NOT_FINISHED",
|
| 107 |
+
"best_level": 0,
|
| 108 |
+
"resets": 0,
|
| 109 |
+
"actions": 40,
|
| 110 |
+
"seconds": 67.1,
|
| 111 |
+
"steps_per_s": 0.6
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"game_id": "ls20",
|
| 115 |
+
"win": false,
|
| 116 |
+
"final_state": "NOT_FINISHED",
|
| 117 |
+
"best_level": 0,
|
| 118 |
+
"resets": 0,
|
| 119 |
+
"actions": 40,
|
| 120 |
+
"seconds": 8.1,
|
| 121 |
+
"steps_per_s": 4.94
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"game_id": "m0r0",
|
| 125 |
+
"win": false,
|
| 126 |
+
"final_state": "NOT_FINISHED",
|
| 127 |
+
"best_level": 0,
|
| 128 |
+
"resets": 0,
|
| 129 |
+
"actions": 40,
|
| 130 |
+
"seconds": 75.4,
|
| 131 |
+
"steps_per_s": 0.53
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"game_id": "r11l",
|
| 135 |
+
"win": false,
|
| 136 |
+
"final_state": "NOT_FINISHED",
|
| 137 |
+
"best_level": 0,
|
| 138 |
+
"resets": 0,
|
| 139 |
+
"actions": 40,
|
| 140 |
+
"seconds": 67.9,
|
| 141 |
+
"steps_per_s": 0.59
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
"game_id": "re86",
|
| 145 |
+
"win": false,
|
| 146 |
+
"final_state": "NOT_FINISHED",
|
| 147 |
+
"best_level": 0,
|
| 148 |
+
"resets": 0,
|
| 149 |
+
"actions": 40,
|
| 150 |
+
"seconds": 10.1,
|
| 151 |
+
"steps_per_s": 3.96
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
"game_id": "s5i5",
|
| 155 |
+
"win": false,
|
| 156 |
+
"final_state": "NOT_FINISHED",
|
| 157 |
+
"best_level": 0,
|
| 158 |
+
"resets": 0,
|
| 159 |
+
"actions": 40,
|
| 160 |
+
"seconds": 65.9,
|
| 161 |
+
"steps_per_s": 0.61
|
| 162 |
+
},
|
| 163 |
+
{
|
| 164 |
+
"game_id": "sb26",
|
| 165 |
+
"win": false,
|
| 166 |
+
"final_state": "NOT_FINISHED",
|
| 167 |
+
"best_level": 0,
|
| 168 |
+
"resets": 0,
|
| 169 |
+
"actions": 40,
|
| 170 |
+
"seconds": 69.6,
|
| 171 |
+
"steps_per_s": 0.57
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
"game_id": "sc25",
|
| 175 |
+
"win": false,
|
| 176 |
+
"final_state": "NOT_FINISHED",
|
| 177 |
+
"best_level": 0,
|
| 178 |
+
"resets": 0,
|
| 179 |
+
"actions": 40,
|
| 180 |
+
"seconds": 75.1,
|
| 181 |
+
"steps_per_s": 0.53
|
| 182 |
+
},
|
| 183 |
+
{
|
| 184 |
+
"game_id": "sk48",
|
| 185 |
+
"win": false,
|
| 186 |
+
"final_state": "NOT_FINISHED",
|
| 187 |
+
"best_level": 0,
|
| 188 |
+
"resets": 0,
|
| 189 |
+
"actions": 40,
|
| 190 |
+
"seconds": 74.1,
|
| 191 |
+
"steps_per_s": 0.54
|
| 192 |
+
},
|
| 193 |
+
{
|
| 194 |
+
"game_id": "sp80",
|
| 195 |
+
"win": false,
|
| 196 |
+
"final_state": "NOT_FINISHED",
|
| 197 |
+
"best_level": 0,
|
| 198 |
+
"resets": 1,
|
| 199 |
+
"actions": 39,
|
| 200 |
+
"seconds": 72.2,
|
| 201 |
+
"steps_per_s": 0.54
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
"game_id": "su15",
|
| 205 |
+
"win": false,
|
| 206 |
+
"final_state": "NOT_FINISHED",
|
| 207 |
+
"best_level": 0,
|
| 208 |
+
"resets": 0,
|
| 209 |
+
"actions": 40,
|
| 210 |
+
"seconds": 65.8,
|
| 211 |
+
"steps_per_s": 0.61
|
| 212 |
+
},
|
| 213 |
+
{
|
| 214 |
+
"game_id": "tn36",
|
| 215 |
+
"win": false,
|
| 216 |
+
"final_state": "NOT_FINISHED",
|
| 217 |
+
"best_level": 0,
|
| 218 |
+
"resets": 0,
|
| 219 |
+
"actions": 40,
|
| 220 |
+
"seconds": 67.7,
|
| 221 |
+
"steps_per_s": 0.59
|
| 222 |
+
},
|
| 223 |
+
{
|
| 224 |
+
"game_id": "tr87",
|
| 225 |
+
"win": false,
|
| 226 |
+
"final_state": "NOT_FINISHED",
|
| 227 |
+
"best_level": 0,
|
| 228 |
+
"resets": 0,
|
| 229 |
+
"actions": 40,
|
| 230 |
+
"seconds": 8.1,
|
| 231 |
+
"steps_per_s": 4.94
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
"game_id": "tu93",
|
| 235 |
+
"win": false,
|
| 236 |
+
"final_state": "NOT_FINISHED",
|
| 237 |
+
"best_level": 0,
|
| 238 |
+
"resets": 0,
|
| 239 |
+
"actions": 40,
|
| 240 |
+
"seconds": 8.1,
|
| 241 |
+
"steps_per_s": 4.94
|
| 242 |
+
},
|
| 243 |
+
{
|
| 244 |
+
"game_id": "vc33",
|
| 245 |
+
"win": false,
|
| 246 |
+
"final_state": "NOT_FINISHED",
|
| 247 |
+
"best_level": 0,
|
| 248 |
+
"resets": 0,
|
| 249 |
+
"actions": 40,
|
| 250 |
+
"seconds": 67.2,
|
| 251 |
+
"steps_per_s": 0.6
|
| 252 |
+
},
|
| 253 |
+
{
|
| 254 |
+
"game_id": "wa30",
|
| 255 |
+
"win": false,
|
| 256 |
+
"final_state": "NOT_FINISHED",
|
| 257 |
+
"best_level": 0,
|
| 258 |
+
"resets": 0,
|
| 259 |
+
"actions": 40,
|
| 260 |
+
"seconds": 10.0,
|
| 261 |
+
"steps_per_s": 4.0
|
| 262 |
+
}
|
| 263 |
+
],
|
| 264 |
+
"summary": {
|
| 265 |
+
"played": 25,
|
| 266 |
+
"wins": 0,
|
| 267 |
+
"best_level_sum": 0,
|
| 268 |
+
"resets_sum": 3,
|
| 269 |
+
"actions_sum": 997,
|
| 270 |
+
"seconds_sum": 1410.7,
|
| 271 |
+
"steps_per_s_median": 0.59
|
| 272 |
+
}
|
| 273 |
+
}
|
ops/README.md
CHANGED
|
@@ -7,7 +7,8 @@
|
|
| 7 |
|
| 8 |
| 文件 | 用途 | 运行位置 |
|
| 9 |
|------|------|----------|
|
| 10 |
-
| `run_eval.py` | **一键评测入口(v0.4.
|
|
|
|
| 11 |
| `t4_pipeline.py` | 冒烟流水线:env→code→data→smoke→stage_a→stage_b→stage_c→eval→assets→upload,断点续跑(done/ 戳 + 产物检查),阶段全部为隔离子进程(不许在桥接执行器进程里 import torch——上一窗口 7.8GB VRAM 泄漏的教训) | T4 VM,`subprocess.Popen` 脱离桥接进程 |
|
| 12 |
| `t4_scale.py` | 正式规模流水线:collect 24×1200 → stage_b 120 步 → stage_c 120 行 → eval → 增量上传 `t4_assets_v2/`;扩参 YAML 内嵌,运行时写入 `configs/t4_scale.yaml` / `configs/collect_scale.yaml` | T4 VM |
|
| 13 |
| `t4_smoke_s1.py` | 单次真实 2B 决策冒烟(预训练视觉塔前向 + 三头读出),由 t4_pipeline.py 的 code 阶段写入 repo 根目录 | T4 VM |
|
|
@@ -17,6 +18,19 @@
|
|
| 17 |
| `push_t4_note.py` | 推送编译缓存审计注记与 t4_assets 复用指南(compile_cache_note.json / README.md) | 本地 |
|
| 18 |
| `torch_kernels_t4.tar.gz` | torch NVRTC 内核缓存(`/root/.cache/torch/kernels/`,cc7.5 + CUDA 12.8 的 reduction_prod 预编译产物)。还原:`tar xzf torch_kernels_t4.tar.gz -C ~/.cache/` | 任意 T4 机器 |
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
## 典型复现路径
|
| 21 |
|
| 22 |
```bash
|
|
|
|
| 7 |
|
| 8 |
| 文件 | 用途 | 运行位置 |
|
| 9 |
|------|------|----------|
|
| 10 |
+
| `run_eval.py` | **一键评测入口(v0.4.2)**:`hf:<repo>@<subpath>` 自动下载检查点 + 重组适配器(sha256 校验)→ 后端 auto(GPU→hf fp16+LoRA merge / 否则 tiny)→ 逐游戏对局 → 带 provenance 的 JSON 报告。`--games all25` 跑 25 公开游戏;`--download-only` 预缓存游戏;`--list-games` 从 API 拉实时列表;`--env-dir` 指定游戏缓存目录;GAME_OVER 自动重置续玩(resets 入报告);`--no-checkpoint --backend tiny` 即 CPU harness 测试 | 任意机器 |
|
| 11 |
+
| `fetch_games.py` | **游戏预缓存引导(v0.4.2)**:从 arcade API(匿名 key,无需 Kaggle 账号/接受规则)拉取全部 25 公开游戏源码到 `environment_files/`(约 3MB / 65 秒),之后所有评测永久离线 | 任意机器 |
|
| 12 |
| `t4_pipeline.py` | 冒烟流水线:env→code→data→smoke→stage_a→stage_b→stage_c→eval→assets→upload,断点续跑(done/ 戳 + 产物检查),阶段全部为隔离子进程(不许在桥接执行器进程里 import torch——上一窗口 7.8GB VRAM 泄漏的教训) | T4 VM,`subprocess.Popen` 脱离桥接进程 |
|
| 13 |
| `t4_scale.py` | 正式规模流水线:collect 24×1200 → stage_b 120 步 → stage_c 120 行 → eval → 增量上传 `t4_assets_v2/`;扩参 YAML 内嵌,运行时写入 `configs/t4_scale.yaml` / `configs/collect_scale.yaml` | T4 VM |
|
| 14 |
| `t4_smoke_s1.py` | 单次真实 2B 决策冒烟(预训练视觉塔前向 + 三头读出),由 t4_pipeline.py 的 code 阶段写入 repo 根目录 | T4 VM |
|
|
|
|
| 18 |
| `push_t4_note.py` | 推送编译缓存审计注记与 t4_assets 复用指南(compile_cache_note.json / README.md) | 本地 |
|
| 19 |
| `torch_kernels_t4.tar.gz` | torch NVRTC 内核缓存(`/root/.cache/torch/kernels/`,cc7.5 + CUDA 12.8 的 reduction_prod 预编译产物)。还原:`tar xzf torch_kernels_t4.tar.gz -C ~/.cache/` | 任意 T4 机器 |
|
| 20 |
|
| 21 |
+
## 评测一键启动(v0.4.2 起,25 公开游戏)
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
pip install arc-agi # 官方本地引擎(PyPI,纯 Python)
|
| 25 |
+
python ops/run_eval.py --download-only # 一次性缓存 25 游戏(匿名 key,约 3MB)
|
| 26 |
+
python ops/run_eval.py --games all25 # 全量:GPU→v2 检查点(hf+merge);无 GPU→tiny
|
| 27 |
+
python ops/run_eval.py --checkpoint <dir> --games ls20 --max-actions 120 # 本地检查点
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
- 检查点默认 `hf:tchbcb/qwenjev@t4_assets_v2/checkpoints`(qwenjev_s1.pt + hf_lora 分片自动下载/重组/校验);
|
| 31 |
+
- 本地 CPU(2 核 / 3GB 容器)已验证 25/25 游戏跑通(tiny + v0.2.0 训练头,中位 0.59 步/秒,报告 `eval_report_public25_cpu.json`);hf 后端需 ≥8GB 内存(fp16 2B),详见 `../train.md` §8.2;
|
| 32 |
+
- 汇报样例:`{game_id, win, final_state, best_level, resets, actions, seconds, steps_per_s}`。
|
| 33 |
+
|
| 34 |
## 典型复现路径
|
| 35 |
|
| 36 |
```bash
|
ops/fetch_games.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Bootstrap: cache all 25 public ARC-AGI-3 games locally (one-time, ~3 MB).
|
| 3 |
+
|
| 4 |
+
python ops/fetch_games.py # -> ./environment_files/
|
| 5 |
+
python ops/fetch_games.py --dir /data/env # custom cache dir
|
| 6 |
+
python ops/fetch_games.py --list # live game list from the API
|
| 7 |
+
|
| 8 |
+
After this every eval/collection run uses OperationMode.OFFLINE (zero
|
| 9 |
+
network, unlimited instances). No Kaggle account or competition rules are
|
| 10 |
+
required: the arcade API issues an anonymous key automatically. (The
|
| 11 |
+
kagglehub competition download needs a KGAT token + accepted rules; this
|
| 12 |
+
toolkit path does not.)
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import sys
|
| 18 |
+
import time
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 22 |
+
sys.path.insert(0, str(ROOT))
|
| 23 |
+
|
| 24 |
+
DEFAULT_GAMES = [
|
| 25 |
+
"ar25", "bp35", "cd82", "cn04", "dc22", "ft09", "g50t", "ka59",
|
| 26 |
+
"lf52", "lp85", "ls20", "m0r0", "r11l", "re86", "s5i5", "sb26",
|
| 27 |
+
"sc25", "sk48", "sp80", "su15", "tn36", "tr87", "tu93", "vc33", "wa30",
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def main() -> None:
|
| 32 |
+
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
| 33 |
+
ap.add_argument("--dir", default=str(Path(__file__).resolve().parents[1] / "environment_files"))
|
| 34 |
+
ap.add_argument("--list", action="store_true", help="list games and exit")
|
| 35 |
+
args = ap.parse_args()
|
| 36 |
+
|
| 37 |
+
import arc_agi
|
| 38 |
+
from arc_agi import OperationMode
|
| 39 |
+
|
| 40 |
+
arc = arc_agi.Arcade(operation_mode=OperationMode.NORMAL,
|
| 41 |
+
environments_dir=args.dir)
|
| 42 |
+
try:
|
| 43 |
+
live = [e.game_id.split("-")[0] for e in arc.get_environments()]
|
| 44 |
+
except Exception as exc:
|
| 45 |
+
print(f"[warn] API listing failed ({exc}); using static list")
|
| 46 |
+
live = DEFAULT_GAMES
|
| 47 |
+
ids = live or DEFAULT_GAMES
|
| 48 |
+
|
| 49 |
+
if args.list:
|
| 50 |
+
print("\n".join(ids))
|
| 51 |
+
print(f"-- {len(ids)} games available from the arcade API", file=sys.stderr)
|
| 52 |
+
return
|
| 53 |
+
|
| 54 |
+
print(f"cache dir: {args.dir}; {len(ids)} games")
|
| 55 |
+
ok, fail = [], []
|
| 56 |
+
for gid in ids:
|
| 57 |
+
t0 = time.time()
|
| 58 |
+
try:
|
| 59 |
+
env = arc.make(gid)
|
| 60 |
+
if env is None:
|
| 61 |
+
raise RuntimeError("make() returned None")
|
| 62 |
+
ok.append(gid)
|
| 63 |
+
print(f" {gid}: OK ({time.time() - t0:.1f}s)", flush=True)
|
| 64 |
+
except Exception as exc:
|
| 65 |
+
fail.append(gid)
|
| 66 |
+
print(f" {gid}: FAIL {type(exc).__name__}: {exc}", flush=True)
|
| 67 |
+
|
| 68 |
+
print(f"downloaded={len(ok)} failed={len(fail)}" + (f" {fail}" if fail else ""))
|
| 69 |
+
sys.exit(1 if fail else 0)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
if __name__ == "__main__":
|
| 73 |
+
main()
|
ops/run_eval.py
CHANGED
|
@@ -115,7 +115,13 @@ def main() -> None:
|
|
| 115 |
ap.add_argument("--backend", default="auto", choices=["auto", "hf", "tiny"])
|
| 116 |
ap.add_argument("--games", nargs="*", default=["mock"],
|
| 117 |
help="mock | all25 | explicit game ids (space separated)")
|
| 118 |
-
ap.add_argument("--list-games", action="store_true"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
ap.add_argument("--max-actions", type=int, default=300)
|
| 120 |
ap.add_argument("--out", default="eval_report.json")
|
| 121 |
ap.add_argument("--cache", default=str(Path.home() / ".cache" / "qwenjev_eval"))
|
|
@@ -123,10 +129,26 @@ def main() -> None:
|
|
| 123 |
help="skip checkpoint entirely (untrained heads / harness test)")
|
| 124 |
args = ap.parse_args()
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
from data.collect_selfplay import PUBLIC_GAMES_FALLBACK
|
| 127 |
if args.list_games:
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
games: list[str] = []
|
| 132 |
for g in args.games:
|
|
@@ -143,10 +165,10 @@ def main() -> None:
|
|
| 143 |
raise SystemExit(
|
| 144 |
"[error] real games need the official offline engine:\n"
|
| 145 |
" pip install arc-agi (PyPI, pure-python engine)\n"
|
| 146 |
-
"
|
| 147 |
-
"
|
| 148 |
-
"
|
| 149 |
-
"
|
| 150 |
|
| 151 |
backend = pick_backend(args.backend)
|
| 152 |
ckpt_dir = None if args.no_checkpoint else resolve_checkpoint(args.checkpoint, Path(args.cache))
|
|
@@ -179,35 +201,46 @@ def main() -> None:
|
|
| 179 |
brain = HybridAgent(s1=s1, s2=None,
|
| 180 |
config=HybridConfig(game_time_limit_s=600))
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
from data.collect_selfplay import make_env
|
| 183 |
report, t0 = [], time.time()
|
|
|
|
| 184 |
for game_id in games:
|
| 185 |
try:
|
| 186 |
env = make_env(game_id, None)
|
| 187 |
res = run_game(env, game_id, brain, max_actions=args.max_actions)
|
|
|
|
| 188 |
report.append(res)
|
| 189 |
log(f"{game_id}: {res}")
|
| 190 |
except Exception as exc:
|
| 191 |
-
log(f"[warn] {game_id}: {exc}")
|
|
|
|
| 192 |
|
| 193 |
-
out =
|
| 194 |
-
"meta": {
|
| 195 |
-
"backend": backend,
|
| 196 |
-
"checkpoint": args.checkpoint if not args.no_checkpoint else "none",
|
| 197 |
-
"games": games,
|
| 198 |
-
"max_actions": args.max_actions,
|
| 199 |
-
"seconds_total": round(time.time() - t0, 1),
|
| 200 |
-
"timestamp": time.strftime("%F %T"),
|
| 201 |
-
},
|
| 202 |
-
"games": report,
|
| 203 |
-
"summary": {
|
| 204 |
-
"played": len(report),
|
| 205 |
-
"wins": sum(1 for r in report if r["win"]),
|
| 206 |
-
"best_level_sum": sum(r["best_level"] for r in report),
|
| 207 |
-
"actions_sum": sum(r["actions"] for r in report),
|
| 208 |
-
},
|
| 209 |
-
}
|
| 210 |
-
Path(args.out).write_text(json.dumps(out, indent=2))
|
| 211 |
log(f"done: {out['summary']} -> {args.out}")
|
| 212 |
|
| 213 |
|
|
|
|
| 115 |
ap.add_argument("--backend", default="auto", choices=["auto", "hf", "tiny"])
|
| 116 |
ap.add_argument("--games", nargs="*", default=["mock"],
|
| 117 |
help="mock | all25 | explicit game ids (space separated)")
|
| 118 |
+
ap.add_argument("--list-games", action="store_true",
|
| 119 |
+
help="list the public games (live from the arcade API) and exit")
|
| 120 |
+
ap.add_argument("--download-only", action="store_true",
|
| 121 |
+
help="cache all 25 game sources locally, then exit")
|
| 122 |
+
ap.add_argument("--env-dir", default=None,
|
| 123 |
+
help="game cache dir (default: ./environment_files, "
|
| 124 |
+
"overridden by ARC_ENVIRONMENTS_DIR)")
|
| 125 |
ap.add_argument("--max-actions", type=int, default=300)
|
| 126 |
ap.add_argument("--out", default="eval_report.json")
|
| 127 |
ap.add_argument("--cache", default=str(Path.home() / ".cache" / "qwenjev_eval"))
|
|
|
|
| 129 |
help="skip checkpoint entirely (untrained heads / harness test)")
|
| 130 |
args = ap.parse_args()
|
| 131 |
|
| 132 |
+
if args.env_dir:
|
| 133 |
+
import os
|
| 134 |
+
os.environ["ARC_ENVIRONMENTS_DIR"] = args.env_dir
|
| 135 |
+
|
| 136 |
from data.collect_selfplay import PUBLIC_GAMES_FALLBACK
|
| 137 |
if args.list_games:
|
| 138 |
+
try:
|
| 139 |
+
import arc_agi
|
| 140 |
+
from arc_agi import OperationMode
|
| 141 |
+
arc = arc_agi.Arcade(operation_mode=OperationMode.NORMAL)
|
| 142 |
+
live = [e.game_id.split("-")[0] for e in arc.get_environments()]
|
| 143 |
+
except Exception:
|
| 144 |
+
live = []
|
| 145 |
+
print("\n".join(live or PUBLIC_GAMES_FALLBACK))
|
| 146 |
return
|
| 147 |
+
if args.download_only:
|
| 148 |
+
import subprocess
|
| 149 |
+
r = subprocess.run([sys.executable, str(ROOT / "ops" / "fetch_games.py")]
|
| 150 |
+
+ (["--dir", args.env_dir] if args.env_dir else []))
|
| 151 |
+
sys.exit(r.returncode)
|
| 152 |
|
| 153 |
games: list[str] = []
|
| 154 |
for g in args.games:
|
|
|
|
| 165 |
raise SystemExit(
|
| 166 |
"[error] real games need the official offline engine:\n"
|
| 167 |
" pip install arc-agi (PyPI, pure-python engine)\n"
|
| 168 |
+
"Game sources are then fetched automatically on first use "
|
| 169 |
+
"(anonymous arcade key, no Kaggle account needed) and cached "
|
| 170 |
+
"in ./environment_files/ — or pre-cache them with:\n"
|
| 171 |
+
" python ops/fetch_games.py")
|
| 172 |
|
| 173 |
backend = pick_backend(args.backend)
|
| 174 |
ckpt_dir = None if args.no_checkpoint else resolve_checkpoint(args.checkpoint, Path(args.cache))
|
|
|
|
| 201 |
brain = HybridAgent(s1=s1, s2=None,
|
| 202 |
config=HybridConfig(game_time_limit_s=600))
|
| 203 |
|
| 204 |
+
def write_report(report: list, t0: float) -> dict:
|
| 205 |
+
"""Atomically dump the report; called after EVERY game so a Colab
|
| 206 |
+
disconnect mid-run keeps all completed results."""
|
| 207 |
+
out = {
|
| 208 |
+
"meta": {
|
| 209 |
+
"backend": backend,
|
| 210 |
+
"checkpoint": args.checkpoint if not args.no_checkpoint else "none",
|
| 211 |
+
"games": games,
|
| 212 |
+
"max_actions": args.max_actions,
|
| 213 |
+
"seconds_total": round(time.time() - t0, 1),
|
| 214 |
+
"timestamp": time.strftime("%F %T"),
|
| 215 |
+
},
|
| 216 |
+
"games": report,
|
| 217 |
+
"summary": {
|
| 218 |
+
"played": len(report),
|
| 219 |
+
"wins": sum(1 for r in report if r["win"]),
|
| 220 |
+
"best_level_sum": sum(r["best_level"] for r in report),
|
| 221 |
+
"actions_sum": sum(r["actions"] for r in report),
|
| 222 |
+
},
|
| 223 |
+
}
|
| 224 |
+
tmp = Path(args.out + ".tmp")
|
| 225 |
+
tmp.write_text(json.dumps(out, indent=2))
|
| 226 |
+
tmp.replace(args.out)
|
| 227 |
+
return out
|
| 228 |
+
|
| 229 |
from data.collect_selfplay import make_env
|
| 230 |
report, t0 = [], time.time()
|
| 231 |
+
write_report(report, t0) # header-only report, proves out path works
|
| 232 |
for game_id in games:
|
| 233 |
try:
|
| 234 |
env = make_env(game_id, None)
|
| 235 |
res = run_game(env, game_id, brain, max_actions=args.max_actions)
|
| 236 |
+
res["steps_per_s"] = round(res["actions"] / max(res["seconds"], 0.1), 2)
|
| 237 |
report.append(res)
|
| 238 |
log(f"{game_id}: {res}")
|
| 239 |
except Exception as exc:
|
| 240 |
+
log(f"[warn] {game_id}: {type(exc).__name__}: {exc}")
|
| 241 |
+
write_report(report, t0)
|
| 242 |
|
| 243 |
+
out = write_report(report, t0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
log(f"done: {out['summary']} -> {args.out}")
|
| 245 |
|
| 246 |
|
ops/t4exec.py
CHANGED
|
@@ -10,11 +10,14 @@ Commands:
|
|
| 10 |
"""
|
| 11 |
import base64
|
| 12 |
import json
|
|
|
|
| 13 |
import sys
|
| 14 |
import urllib.request
|
| 15 |
import zlib
|
| 16 |
|
| 17 |
-
|
|
|
|
|
|
|
| 18 |
CRC_MODE = None # auto-detected against known-good envelope
|
| 19 |
|
| 20 |
|
|
@@ -27,19 +30,19 @@ def b64d(s: str) -> bytes:
|
|
| 27 |
|
| 28 |
|
| 29 |
def _crc(p_b64: str, raw: bytes) -> str:
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
return format(zlib.crc32(
|
| 33 |
|
| 34 |
|
| 35 |
def envelope(code: str) -> bytes:
|
|
|
|
|
|
|
|
|
|
| 36 |
raw = code.encode()
|
| 37 |
p = b64u(raw)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
lines.append(p[i : i + 76])
|
| 41 |
-
lines.append("samaicmdend")
|
| 42 |
-
return ("\n".join(lines) + "\n").encode()
|
| 43 |
|
| 44 |
|
| 45 |
def _detect_crc():
|
|
|
|
| 10 |
"""
|
| 11 |
import base64
|
| 12 |
import json
|
| 13 |
+
import os
|
| 14 |
import sys
|
| 15 |
import urllib.request
|
| 16 |
import zlib
|
| 17 |
|
| 18 |
+
# Tunnel endpoint: override with `export T4_URL=https://aitun.cc/XXXX` when a
|
| 19 |
+
# new tunnel code is issued (aitun URLs rotate every Colab session).
|
| 20 |
+
URL = os.environ.get("T4_URL", "https://aitun.cc/ZMPBD8M2")
|
| 21 |
CRC_MODE = None # auto-detected against known-good envelope
|
| 22 |
|
| 23 |
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
def _crc(p_b64: str, raw: bytes) -> str:
|
| 33 |
+
# Server-side check verified live: crc32 of the DECODED payload (raw code
|
| 34 |
+
# bytes), NOT of the b64 text. 'print(123, 456)' -> crc c03b37d5 accepted.
|
| 35 |
+
return format(zlib.crc32(raw) & 0xFFFFFFFF, "08x")
|
| 36 |
|
| 37 |
|
| 38 |
def envelope(code: str) -> bytes:
|
| 39 |
+
# NOTE: payload must be a SINGLE b64url line. The 76-char wrapped form is
|
| 40 |
+
# in the spec but the current relay corrupts multi-line payloads
|
| 41 |
+
# (crc mismatch server-side) — single-line verified against crc 9e30c354.
|
| 42 |
raw = code.encode()
|
| 43 |
p = b64u(raw)
|
| 44 |
+
head = f"samaicmdbegin\nv=1\nenc=b64url\ncrc={_crc(p, raw)}\n"
|
| 45 |
+
return (head + p + "\nsamaicmdend\n").encode()
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
|
| 48 |
def _detect_crc():
|
scripts/push_hf.py
CHANGED
|
@@ -58,7 +58,7 @@ def main() -> None:
|
|
| 58 |
repo_type="model",
|
| 59 |
folder_path=str(REPO_ROOT),
|
| 60 |
ignore_patterns=ignore_patterns,
|
| 61 |
-
commit_message="QwenJev v0.
|
| 62 |
)
|
| 63 |
api.update_repo_settings(repo_id=args.repo, repo_type="model", private=False)
|
| 64 |
print(f"public repo ready -> https://huggingface.co/{args.repo}")
|
|
|
|
| 58 |
repo_type="model",
|
| 59 |
folder_path=str(REPO_ROOT),
|
| 60 |
ignore_patterns=ignore_patterns,
|
| 61 |
+
commit_message="QwenJev v0.4.3: 25-public-games eval in-repo (run_eval incremental report, fetch_games, CPU feasibility verified)",
|
| 62 |
)
|
| 63 |
api.update_repo_settings(repo_id=args.repo, repo_type="model", private=False)
|
| 64 |
print(f"public repo ready -> https://huggingface.co/{args.repo}")
|
train.md
CHANGED
|
@@ -4,13 +4,6 @@
|
|
| 4 |
> 按顺序读完即可独立复现整个系统。README 按要求留空,所有内容都在本文档中。
|
| 5 |
> **v0.2.0 架构决策:视觉塔必保留在 System-1 每步推理路径上**——隐藏关全是陌生机制,
|
| 6 |
> 只看 hex 文本串没法玩游戏;本仓库的 System-1 骨干直接是多模态模型,每步都"看"当前帧。
|
| 7 |
-
> **v0.3.0 架构决策:概率必须诚实(吸收自 Laya)**——one-hot CE 只保证 argmax 对,
|
| 8 |
-
> 不保证概率可被依赖;Stage C 用严格 proper scoring rule 重训置信度,
|
| 9 |
-
> 温度按桶拟合后钉死在 [0.5, 5.0],调度器新增"学习型唤醒"。详见 §1.4 与 §5.1。
|
| 10 |
-
> **v0.4.0 实战验证:T4 (16GB) 真机全流水线两轮全绿**——真实 Qwen3-VL-2B 学生(预训练视觉塔
|
| 11 |
-
> 每步在环、冻结不训)+ 4bit 视觉教师蒸馏 + QLoRA 三阶段 + RLCD 校准;权重/适配器/
|
| 12 |
-
> 离线 wheels/NVRTC 内核缓存全部沉淀在 HF `t4_assets/`,换机复现零编译、最少下载。
|
| 13 |
-
> 详见 §11.2(两轮速览)、§12(实战复盘)与 §13(离线复现包)。
|
| 14 |
|
| 15 |
---
|
| 16 |
|
|
@@ -45,27 +38,6 @@ ARC Prize 2026(ARC-AGI-3)要求智能体在**从未见过的交互式游戏
|
|
| 45 |
- 沿用其训练配方:complete-question 交叉熵,backbone lr `1e-5`、decision-head lr `1e-4`。
|
| 46 |
- 差异点:① state 序列化改为**版本化规范串**(原作审计文档承认 `str()` 序列化非规范,属遗留问题);② **叶子多模态化——视觉塔每步在环**;③ 增加 ACTION6(x,y) 的候选离散化;④ 外层接 ARC-AGI-3 环境与双系统调度。
|
| 47 |
|
| 48 |
-
### 1.4 对标 Laya:吸收了什么、没吸收什么(v0.3.0)
|
| 49 |
-
|
| 50 |
-
Laya(Convai Innovations,Apache-2.0)是与 Jev/NanoJev 线格式兼容的**非自回归** System-1 决策模型:ModernBERT-large 421M 双向编码器 + 选项标记打分头,单次前向答完全部问题(T4 上 ~33ms,约为 TypeSafe Jev 的 1/7),并用 RLCD 把期望校准误差(ECE)压到 Jev 的 1/3(0.081 vs 0.246)。调研仓库:`NandhaKishorM/laya`(Python 侧)+ `receptron/laya`(Node/ONNX 侧)+ 三份模型卡。
|
| 51 |
-
|
| 52 |
-
**吸收进 QwenJev 的(按影响力排序):**
|
| 53 |
-
|
| 54 |
-
| # | 技术 | Laya 原样 | QwenJev 落点 |
|
| 55 |
-
|---|------|-----------|--------------|
|
| 56 |
-
| 1 | **RLCD 校准训练** | GRPO 式组采样(零均值投影噪声 σ 0.4→0.1 退火)+ 严格 proper scoring rule(log+spherical+RPS)+ 全权重 soft-CE 锚定 argmax | Stage C `train/calibrate_rlcd.py`,在 Stage B 之后对三族头重训置信度 |
|
| 57 |
-
| 2 | **温度按桶拟合** | 训练后 LBFGS 拟合每 (题型, 选项数桶) 一个温度,推理端 `clamp [0.5, 5.0]`,拒绝"把 0.24 锐化成 0.99"的不诚实温度(他们上线后补的丁) | `qwenjev/calibration.py`:`fit_temperature` + `temp_bucket` + `clamp_temperature`;温度表随 checkpoint 保存,`forward_choice` 推理路径自动应用 |
|
| 58 |
-
| 3 | **归一化熵置信度** | `1 − H(p)/log k`,跨题可比 | `confidence_from_probs`,成为调度器比较"7 动作步"与"60 候选步"的统一标尺 |
|
| 59 |
-
| 4 | **act/escalate 头** | `[top1, top1−top2, 熵, k/255]` 四特征 → 升级概率(他们 fine-tune notebook 里系数为 0,**实际上没训**) | Stage C 用"argmax 会错"标签**真正训练**该头;调度器新增学习型唤醒(§3.6) |
|
| 60 |
-
| 5 | **选项顺序置换** | `build_sequence(option_order=...)` | Stage B 每轮对候选随机置换并同步标签,消除 set-attention 的隐性位置捷径 |
|
| 61 |
-
|
| 62 |
-
**不吸收的,以及为什么(Laya 的基准数据反过来验证了 QwenJev 的既有选择):**
|
| 63 |
-
|
| 64 |
-
- **选项标记打分头**:Laya 把 head_max_len(192–256 token)按选项数均摊,77 选项的 Banking77 每个标签只分到 3–4 token,精度崩到 0.425(Jev 0.870)。我们的 set-attention 逐候选打分没有这个预算问题——ARC 候选池经常 50–255 个,这正是 NanoJev 路线胜过 Laya 的核心场景,**保持不变**。
|
| 65 |
-
- **多语 router / 多 checkpoint 派发**:游戏 state 是单一规范串,无多语需求。
|
| 66 |
-
- **ModernBERT 双向骨干**:替换成本高且丢掉与 NanoJev 权重/配方的兼容;其单向→双向的加速收益已由"零输出 token"设计覆盖。
|
| 67 |
-
- **零样本期望**:Laya 模型卡自己承认 base checkpoint 在 typed-decisions 上接近随机(0.362 vs 随机 0.318),能力来自领域微调——与我们的"公开关自采 + 每关特化"路线一致,无需照搬其权重。
|
| 68 |
-
|
| 69 |
---
|
| 70 |
|
| 71 |
## 2. 总体架构
|
|
@@ -149,10 +121,10 @@ ACTION6(x,y) 是唯一带坐标的动作,必须离散化才能进 Choice 头
|
|
| 149 |
### 3.4 System-1 `qwenjev/system1.py`
|
| 150 |
|
| 151 |
- `encode_question(state, candidates, instruction, grid)` → N 个多模态叶子(每候选一个,state 重复,**当前帧图像共享**);
|
| 152 |
-
- `forward_choice(leaves)` → **一次 backbone 前向(含视觉塔)**:叶子向量 → 投影 → set-attention(同题候选间)→
|
| 153 |
-
- `choose(..., grid)` → 贪心或按分布采样,返回 `Decision(probs, candidates, chosen, index
|
| 154 |
- `forward_boolean` / `forward_score` 及其**可训练孪生** `forward_boolean_logits` / `forward_score_logits`(v0.2.0 修复:旧实现返回 detach 张量,梯度根本流不进模型);
|
| 155 |
-
- `save()` / `load_heads()`:tiny 后端打包 backbone + 全部头
|
| 156 |
- `eval()` / `train_mode()`:决策头 dropout=0,保证加载后推理确定。
|
| 157 |
|
| 158 |
### 3.5 System-2 `qwenjev/system2.py`
|
|
@@ -166,14 +138,11 @@ ACTION6(x,y) 是唯一带坐标的动作,必须离散化才能进 Choice 头
|
|
| 166 |
|
| 167 |
System-2 唤醒条件(满足其一,且调用数 < `system2_max_calls_per_game`):
|
| 168 |
|
| 169 |
-
| 条件 | 含义 |
|
| 170 |
-
|------|------|
|
| 171 |
-
| 新帧哈希 | 局面没见过,需要 9B 看一眼 |
|
| 172 |
-
| `levels_completed` 增加 | 进新关卡,机制可能变 |
|
| 173 |
-
| 连续 12 步无变化 | System-1 卡死,需要外部视角 |
|
| 174 |
-
| `escalate_p ≥ 0.70` 或校准置信度 ≤ 0.15 | System-1 自报"我要错/我没底",触发**当步有界重规划**(S2 建议→重打分→仅当更自信才采纳) | 模型侧(学习型,v0.3.0) |
|
| 175 |
-
|
| 176 |
-
前三个条件盯着**环境**,第四个盯着**模型自己**—— Stage C 把概率校准好之后,"更自信才采纳"这个比较才有意义;未校准的置信度之间不可比,这正是 Laya 基准里 Jev 被 ECE 0.246 拖累的教训。
|
| 177 |
|
| 178 |
同时管理:每局时间预算(默认 900s,官方文档明确超时会直接 fail 该次运行)、动作数上限、动作-效果记忆(哪个动作改变多少格)、每局反思笔记。
|
| 179 |
|
|
@@ -183,10 +152,11 @@ System-2 唤醒条件(满足其一,且调用数 < `system2_max_calls_per_gam
|
|
| 183 |
|
| 184 |
## 4. 数据管线
|
| 185 |
|
| 186 |
-
### 4.1 来源:25 个公开游戏自采(已确认可用)
|
| 187 |
|
| 188 |
-
-
|
| 189 |
-
-
|
|
|
|
| 190 |
- **评测是另外 110 个从未公开的隐藏游戏**(55 个 Public LB + 55 个 Private LB),所以数据管线的一切设计都服务于"可泛化的探索先验",而非背题。
|
| 191 |
|
| 192 |
### 4.2 采集 `data/collect_selfplay.py`
|
|
@@ -219,15 +189,14 @@ ARC 传统增广全部可用:转置/镜像/旋转/重着色(固定 0=背景
|
|
| 219 |
|
| 220 |
## 5. 训练方案
|
| 221 |
|
| 222 |
-
### 5.1
|
| 223 |
|
| 224 |
**Stage A:视觉蒸馏对齐**(`train/distill_alignment.py`)
|
| 225 |
|
| 226 |
- 教师:冻结的多模态模型,**同一帧真实过其视觉塔**(hf 后端:processor 构造 `pixel_values` 进模型前向;tiny 后端:教师 TinyVLM 同样吃图像张量)——绝不允许出现"蒸馏时没看图"的假对齐(v0.1 的占位符实现已废弃);
|
| 227 |
- 学生:System-1 多模态骨干,同样图像+文本前向,取逐层隐状态池化;
|
| 228 |
- 对齐层集隐状态(默认最后 2 层)���`1 − cosine` 损失,逐层独立投影头;
|
| 229 |
-
- 产物:`checkpoints/qwenjev-s1/stage_a.pt`(投影头 + tiny 学生权重),Stage B 自动加载
|
| 230 |
-
- **T4 (16GB) 口径(v0.4.0 实测)**:教师换 `Qwen3-VL-4B-Instruct`(NF4 ≈3.5GB)——9B 教师的 fp16 embed/lm_head 单这两项就 8.2GB,与 QLoRA 学生同卡必 OOM;学生同样 4bit,LoRA `exclude_vision: true`(预训练视觉塔冻结不训)。96GB 卡仍用 9B 教师。
|
| 231 |
|
| 232 |
**Stage B:决策头 SFT**(`train/train_system1.py`)
|
| 233 |
|
|
@@ -241,25 +210,6 @@ ARC 传统增广全部可用:转置/镜像/旋转/重着色(固定 0=背景
|
|
| 241 |
|
| 242 |
**v0.2.0 训练修复清单**(旧代码静默失效的三处):① Boolean/Score 头改用可训练 logits 孪生方法,BCE/CE 梯度真正流进模型;② Choice 批训练改用带图前向(旧行为在 `torch.no_grad()` 里算概率再包装,同样训不动);③ Stage A checkpoint 自动加载,蒸馏成果不白费。
|
| 243 |
|
| 244 |
-
**Stage C:RLCD 校准 + 温度拟合**(`train/calibrate_rlcd.py`,v0.3.0 新增,吸收自 Laya)
|
| 245 |
-
|
| 246 |
-
Stage B 的 one-hot CE 只优化 argmax;学到的概率分布可以(也通常会)过度自信——对人类步数取平方惩罚的 RHAE 而言,"自信地错"比"不确定"代价更高。Stage C 分三步把概率变诚实:
|
| 247 |
-
|
| 248 |
-
1. **RLCD 重训**:对每题采样 G=4 组零均值投影噪声扰动 logits(σ 0.4→0.1 退火),以严格 proper scoring rule(log + spherical 0.75 + 有序题 RPS 1.0)打分,组内标准化优势做 REINFORCE;同时保留全权重 soft-CE 锚定项,**校准不牺牲 argmax 精度**(Laya 实测 fine-tune 后 0.766 反超其 0.735 教师上限,靠的就是这个锚定)。
|
| 249 |
-
2. **温度按桶拟合**:在 val 切分上,每 (题型, 选项数桶) 用 LBFGS 拟合一个温度(软目标),应用时 `clamp [0.5, 5.0]`——低于 0.5 属于"把 0.24 发布成 0.99"的不诚实锐化,拒绝应用。温度表写进 checkpoint 配置,推理路径自动生效。
|
| 250 |
-
3. **escalate 头真正训练**:以"当前 argmax 是否等于标签"构造监督信号,在最终温度下训练 3 轮;Laya 的同名头在官方 fine-tune notebook 里系数为 0(未训练),这里补上了他们没做的一步。
|
| 251 |
-
|
| 252 |
-
产物:更新后的 `qwenjev_s1.pt`(含温度表)+ `calib_report.json`(拟合温度 + 校准前后 ECE)。
|
| 253 |
-
|
| 254 |
-
| Stage C 超参 | 默认值 | 说明 |
|
| 255 |
-
|------|-----|------|
|
| 256 |
-
| epochs / group_size | 2 / 4 | RLCD 轮数 / GRPO 组大小 |
|
| 257 |
-
| σ 退火 | 0.4 → 0.1 | 探索噪声线性退火(Laya 同款) |
|
| 258 |
-
| lr_backbone / lr_head | 2.5e-5 / 1e-4 | Laya 微调配方 |
|
| 259 |
-
| ce_guidance | 1.0 | soft-CE 锚定权重(勿调低,防校准吃掉精度) |
|
| 260 |
-
| w_sph / w_rps | 0.75 / 1.0 | proper reward 权重(Laya 同款) |
|
| 261 |
-
| max_choice_rows / max_aux_rows | 600 / 100 | CPU 单机友好上限;GPU 可放大 |
|
| 262 |
-
|
| 263 |
### 5.2 显存预算(RTX Pro 6000 96GB 实测口径)
|
| 264 |
|
| 265 |
| 场景 | 占用估算 |
|
|
@@ -271,18 +221,6 @@ Stage B 的 one-hot CE 只优化 argmax;学到的概率分布可以(也通
|
|
| 271 |
|
| 272 |
消费级 24GB 卡(4090/3090)建议:教师 4bit + 学生 LoRA;tiny 后端则任意机器(含无 GPU 笔记本,~1.1M 参数 fp32 ≈ 5MB)都能跑。
|
| 273 |
|
| 274 |
-
**T4 (16GB) 实测口径(v0.4.0,`configs/t4_smoke.yaml`)**:
|
| 275 |
-
|
| 276 |
-
| 场景 | 实测 | 备注 |
|
| 277 |
-
|------|------|------|
|
| 278 |
-
| 真实 2B 单次决策(4bit 基座 + 视觉塔前向) | 4.24 GB | 10.4 s/次(首跑含内核加载) |
|
| 279 |
-
| Stage A:4B 教师 NF4 + 2B QLoRA 学生 | — | ~10 s/步 × 120 步,align_loss → 0.026 |
|
| 280 |
-
| Stage B:QLoRA SFT(leaf_batch=2 × max_candidates=4) | ~9.7 GB,GPU 99% | 冒烟 ~38 s/步 × 20 步;正式规模 ~5.5 s/步 × 120 步 |
|
| 281 |
-
| Stage C:RLCD(freeze_backbone,仅适配器可训) | 峰值 5.55 GB | ECE 0.5134 → 0.4242(正式规模 120 行) |
|
| 282 |
-
|
| 283 |
-
> `leaf_batch_size` 与 `max_candidates` 是 T4 的两个 OOM 旋钮:反向图在整行 backward 前
|
| 284 |
-
> 按 chunk 累积(每 2-leaf chunk ≈2.3GB 实测),8 候选 = 4 chunk = OOM,4 候选 = 2 chunk ≈4.6GB 才放得下。
|
| 285 |
-
|
| 286 |
### 5.3 下载权重
|
| 287 |
|
| 288 |
```bash
|
|
@@ -352,28 +290,36 @@ cd kaggle_kernel && kaggle kernels push -p .
|
|
| 352 |
|
| 353 |
## 8. 评测与迭代
|
| 354 |
|
|
|
|
|
|
|
| 355 |
```bash
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
```
|
| 358 |
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
```
|
| 370 |
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
检查点解析支持 `hf:<repo>@<子路径>`、本地目录、`none` 三种形式;25 游戏清单见 `--list-games`。
|
| 375 |
|
| 376 |
-
- 官方正式指标 RHAE = 按关卡 `min(人类步数/智能体步数, 1.0)²` 加权平均,人类基线本地拿不到——所以
|
| 377 |
- 迭代循环:改 `agent/my_agent.py`(或 hybrid 配置)→ 本地秒级验证 → 满意后再花 Kaggle Commit;
|
| 378 |
- 关注三条曲线:完成率(WIN 数)、平均步数、System-2 调用次数。好方向的标志是**完成率升、System-2 调用降**。
|
| 379 |
|
|
@@ -385,12 +331,11 @@ make eval-cpu # 同第三条
|
|
| 385 |
|--------|------|----------|------|
|
| 386 |
| M0 | 仓库骨架 + 单测 + 文档 | `make test` 全绿 | ✅ |
|
| 387 |
| M1 | **本地全链路跑通**:mock 游戏 → 采集 → 建库 → Stage A 蒸馏 → Stage B 训练 → 评测(视觉塔全程在环) | `make smoke` 五环节全绿,蒸馏 loss 收敛,36 个单测通过 | ✅ |
|
| 388 |
-
|
|
| 389 |
-
|
|
| 390 |
-
| M3 | System-2 接入 + 调度调参(重点:escalate 阈值 0.70/0.15 按真实关数据校准) | 完成率升 & 9B 调用数下降 | |
|
| 391 |
| M4 | Kaggle 打包提交 | 官方 LB 出分,进入迭代循环 | |
|
| 392 |
|
| 393 |
-
后续方向:共享前缀推理提速、
|
| 394 |
|
| 395 |
---
|
| 396 |
|
|
@@ -410,29 +355,18 @@ make eval-cpu # 同第三条
|
|
| 410 |
|
| 411 |
```bash
|
| 412 |
make setup # 环境安装(Python 3.12+)
|
| 413 |
-
make test # 单测
|
| 414 |
-
make smoke # 一键本地跑通:mock采集→建库→蒸馏→训练→
|
| 415 |
make collect # 自博弈采轨迹(mock 游戏 / 25 公开游戏)
|
| 416 |
make build-data # 轨迹 → Choice/Boolean/Score 训练样本(含 grid 帧字段)
|
| 417 |
make distill # Stage A:9B 教师 → System-1 学生 视觉蒸馏对齐
|
| 418 |
make train-s1 # Stage B:决策头 SFT(NanoJev 配方,自动加载 stage A)
|
| 419 |
-
make calibrate # Stage C:RLCD 校准 + 温度按桶拟合 + escalate 头(v0.3.0)
|
| 420 |
make eval # 本地评测(mock 游戏默认)
|
| 421 |
make push-hf # 推送本仓库到 HuggingFace(HF_TOKEN 从环境读)
|
| 422 |
python scripts/download_weights.py # 拉权重
|
| 423 |
python scripts/serve_system2.py --port 8000 # 9B 端点(transformers 兜底)
|
| 424 |
# vllm serve Qwen/Qwen3.5-9B --port 8000 # 9B 端点(首选)
|
| 425 |
python scripts/package_kaggle.py --username XXX --accelerator rtx6000
|
| 426 |
-
|
| 427 |
-
# —— T4 真机运维(ops/ 工具箱,v0.4.0)——
|
| 428 |
-
python ops/t4exec.py status # 隧道健康检查(URL 在文件头改)
|
| 429 |
-
python ops/t4exec.py run "print(40+2)" # 信封自检(期望 42)
|
| 430 |
-
python ops/t4exec.py put ops/t4_pipeline.py /content/qwenjev_work/t4_pipeline.py
|
| 431 |
-
|
| 432 |
-
# —— 离线装配(新 T4 机器:免编译、除 torch 外免下载)——
|
| 433 |
-
pip install torch==2.11.0 # PyPI 默认即 cu128
|
| 434 |
-
pip install --no-index --find-links t4_assets/wheels/ -r requirements.txt
|
| 435 |
-
tar xzf t4_assets/compile_cache/torch_kernels_t4.tar.gz -C ~/.cache/ # 还原 NVRTC 内核缓存(可选)
|
| 436 |
```
|
| 437 |
|
| 438 |
### 11.1 本地跑通记录(v0.2.0,2026-09)
|
|
@@ -440,145 +374,3 @@ tar xzf t4_assets/compile_cache/torch_kernels_t4.tar.gz -C ~/.cache/ # 还原
|
|
| 440 |
无 GPU(4GB RAM / CPU)环境实测:`make smoke` 全链路通过——mock 游戏 2400 步轨迹、5267 样本(train 4927 / val 340)、Stage A 蒸馏 56 步 align_loss 0.0062→0.0012 收敛、Stage B 三头联训完成、评测 agent 完整跑 600 步决策(每步含视觉塔前向,~210ms/步)。
|
| 441 |
|
| 442 |
切换真实模型的路径:`configs/train.yaml` 里 `system1.backend: "hf"`(学生 `Qwen/Qwen3-VL-2B-Instruct`)+ `stage_a_distill.teacher_model: "Qwen/Qwen3.5-9B"`,代码路径已就绪;建议先在单卡 GPU 机器上冒烟 hf 路径,再上 Kaggle。
|
| 443 |
-
|
| 444 |
-
### 11.2 T4 真机两轮记录(v0.4.0,2026-09-22)
|
| 445 |
-
|
| 446 |
-
同一台 Colab Tesla T4(16GB,cc7.5)当天两轮全绿,产物分别在 `t4_assets/`(冒烟)与 `t4_assets_v2/`(正式规模):
|
| 447 |
-
|
| 448 |
-
| 阶段 | 冒烟(smoke) | 正式规模(v2) |
|
| 449 |
-
|------|--------------|----------------|
|
| 450 |
-
| 采集 | 4 回合 × 600 步 | 24 回合 × 1200 步(20.1 s) |
|
| 451 |
-
| Stage A | 120 步,align_loss → 0.026(~10 s/步) | 复用冒烟 LoRA(配置 `enabled: false`) |
|
| 452 |
-
| Stage B | 20 步 × ~38 s/步,零 OOM | 120 步 × ~5.5 s/步,GPU 9.7GB / 99% |
|
| 453 |
-
| Stage C | 40 行,ECE 0.5134 → 0.4848,峰值 5.55 GB | 120 行 + 255 校准行,ECE 0.5134 → 0.4242 |
|
| 454 |
-
| eval | merge=True,mock-pushbox 300 步 600.2 s | 300 步 601.6 s |
|
| 455 |
-
| 产物 | qwenjev_s1.pt 254,001,909B + 双适配器分片 + wheels 3.10GB | 同构 v2 全套 323.8MB(hf_lora sha256 = cae4663a…) |
|
| 456 |
-
|
| 457 |
-
完整复盘(两大修复、五个坑位、断点设计、上传陷阱、CUDA 编译资产终审)见 §12;离线复现包见 §13。
|
| 458 |
-
|
| 459 |
-
---
|
| 460 |
-
|
| 461 |
-
## 12. T4 (16GB) 真机实战全记录(v0.4.0)
|
| 462 |
-
|
| 463 |
-
### 12.1 环境与拓扑
|
| 464 |
-
|
| 465 |
-
- 机器:Colab Tesla T4(16GB GDDR6,compute capability 7.5),Python 3.13(wheels 按 cp313 抓取);
|
| 466 |
-
- 基座:`Qwen/Qwen3-VL-2B-Instruct` 4bit(QLoRA base,`hf_quant4: true`);**视觉塔原样保留且冻结**(`exclude_vision: true`),每步前向都在环上——冒烟阶段先用未训练头验证了真实视觉前向(probs=[0,1,0,0]、决策 ACTION3、4.24GB、10.4s/次);
|
| 467 |
-
- 教师:`Qwen3-VL-4B-Instruct` NF4(≈3.5GB)——9B 教师在 T4 放不下(fp16 embed/lm_head 单这两项 8.2GB);96GB 卡仍用 9B;
|
| 468 |
-
- 运行拓扑:**流���线进程必须脱离 Colab 桥接执行器**(envelope 触发后 `subprocess.Popen` 发射),且流水线自身也不 import torch——每个阶段都是隔离子进程,崩了不会毒化执行器进程的 VRAM(上一窗口 7.8GB 泄漏的教训);状态走 `smoke_status.json` / `scale_status.json`,日志按阶段落 `logs/<stage>.log`,`nvidia-smi` 每 2s 轮询采样峰值显存。
|
| 469 |
-
|
| 470 |
-
### 12.2 两大训练修复(被真实运行验证)
|
| 471 |
-
|
| 472 |
-
| 修复 | 症状 | 方案 | 铁证 |
|
| 473 |
-
|------|------|------|------|
|
| 474 |
-
| **hook 池化 + `logits_to_keep=1`**(commit 66abfa5) | Stage B OOM:决策头不需要整表 logits,但默认前向对全部叶子 token 输出完整词表 logits,显存白白翻几倍 | 前向钩子处池化隐状态,只保留最后位置 logits | 冒烟 stage_b 20 步零 OOM |
|
| 475 |
-
| **`resume_adapters()`**(commit dda44ec) | 三阶段间适配器断链:Stage A 的 LoRA 到 Stage B 丢失、Stage B 的到 Stage C 断开,蒸馏成果白费 | 显式续载/合并适配器 | 日志明写 "stage A LoRA resumed" / "stage B LoRA merged" |
|
| 476 |
-
|
| 477 |
-
### 12.3 五个环境坑位(复现前先背下来)
|
| 478 |
-
|
| 479 |
-
1. **分配器碎片**:chunked 前向/反向把 caching allocator 打碎(OOM 时 reserved-unallocated 高达 0.9GB)→ 全程 `PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True`;
|
| 480 |
-
2. **torchao 冲突**:VM 预装 torchao 0.10,peft ≥0.18 对 plain-Linear 注入 LoRA 时直接 raise(Stage C 把适配器 merge 进 fp16 基座恰好产生 plain Linear)→ 本栈完全不用 torchao,`pip uninstall -y torchao`;
|
| 481 |
-
3. **peft fp32 输入强转**:fp16 基座被 peft cast 到 fp32 会让计算图膨胀 ~3GB → 用 4bit 基座(`kbit_prepare: true`)绕开;
|
| 482 |
-
4. **`python -c` 转义陷阱**:多行 payload 里的单引号在 shell `-c` 里无法转义(`\'` 不是转义)→ payload 一律写文件再 `python -B file.py` 执行;
|
| 483 |
-
5. **b64url 填充**:信封剥掉 `=` 填充后服务端必须补回,且必须用 urlsafe 字母表(`-_`)——标准 b64decode 会静默丢字符(`ops/t4exec.py` 已修复并实测)。
|
| 484 |
-
|
| 485 |
-
### 12.4 断点与止损设计(VM 随时死亡不丢进度)
|
| 486 |
-
|
| 487 |
-
- 每阶段完成落 `done/<stage>.stamp` + **产物存在性双重检查**(stamp 说完成但 stage_a.pt 不存在,照样重跑);
|
| 488 |
-
- **code 阶段永不 stamp**:每次重启都从 HF `snapshot_download` 刷新代码并清 `__pycache__`——stale repo 会拿着 hotfix 前的逻辑跑完全场(12:05 旧代码静默运行的教训);
|
| 489 |
-
- 正式规模每阶段完成即**增量上传** HF(先落盘再上传,上传失败不回滚训练);
|
| 490 |
-
- 采集断言:`collect` 即使全部游戏失败也 exit 0 → 流水线断言 `data/traces/` 下确有 `*.jsonl` 才放行。
|
| 491 |
-
|
| 492 |
-
### 12.5 上传陷阱与分片方案
|
| 493 |
-
|
| 494 |
-
HF xet 后端对 `*.safetensors` 会**静默失败**:commit 建立、API 返回成功、文件 404——冒烟与 v2 两轮都撞到。方案(`ops/t4_split_adapter.py`):
|
| 495 |
-
|
| 496 |
-
1. 适配器 bytes 头部加 16 字节 nonce 前缀(改写文件魔数,避开 safetensors 扫描路径);
|
| 497 |
-
2. 切 4 × 17,447,082B 分片 `adapter_p00..p03_<crc>.pt` 上传(`.pt` 后缀不受影响);
|
| 498 |
-
3. `parts/manifest.json` 记录 sha256(hf_lora v2 = `cae4663a…`,冒烟双适配器 = `75cbce97…` / `b0d23254…`);
|
| 499 |
-
4. `reassemble.py` 校验并还原为原始 `adapter_model.safetensors`,`PeftModel.from_pretrained` 直接可用。
|
| 500 |
-
|
| 501 |
-
### 12.6 CUDA 编译资产终版审计(两轮运行后全盘复查)
|
| 502 |
-
|
| 503 |
-
| 位置 | 结论 | 处置 |
|
| 504 |
-
|------|------|------|
|
| 505 |
-
| `~/.cache/triton`、`~/.cache/torch_extensions` | **absent**(无 torch.compile;transformers 5.16 eager 路径;T4 全部内核来自 wheel 预编译) | 无需处理 |
|
| 506 |
-
| `~/.cache/torch/kernels/` | **发现 1 枚 NVRTC JIT 产物**:`reduction_prod_arch7.5_nvrtc12.8_sass_30566_aa1ebe…`(cc7.5 + CUDA 12.8 的 prod-reduction 内核,共 64KB)——首轮审计漏掉,因为只搜了 `torch_extensions`/`triton` 目录名 | 打 tar 上传:`t4_assets/compile_cache/torch_kernels_t4.tar.gz` 与 `ops/torch_kernels_t4.tar.gz`(12,857B) |
|
| 507 |
-
| `/root/.julia/**/ptx` | 16 个 .ptx 全是 VM 预装 Julia CUDA.jl 的示例/测试文件,与本流水线无关 | 忽略 |
|
| 508 |
-
| `~/.cache/pip`(3.0GB)、`~/.cache/huggingface`(13GB) | 可由 wheels 包完整重建 | 不上传,由 §13 wheels 包承接 |
|
| 509 |
-
|
| 510 |
-
> 结论:本栈在 T4 上的"避免重复编译" = **预编译 wheels(主力)+ 版本锁定 + 这枚 NVRTC 内核缓存(锦上添花)**。
|
| 511 |
-
> 还原缓存:`tar xzf torch_kernels_t4.tar.gz -C ~/.cache/`;不还原也只是首次运行多几秒 NVRTC 编译,不影响正确性。
|
| 512 |
-
|
| 513 |
-
---
|
| 514 |
-
|
| 515 |
-
## 13. 离线复现包(换机零编译、最少下载)
|
| 516 |
-
|
| 517 |
-
### 13.1 环境装配(顺序敏感)
|
| 518 |
-
|
| 519 |
-
```bash
|
| 520 |
-
# 0) Python 3.13(wheels 按 cp313 抓取;其他版本见 13.3 末尾)
|
| 521 |
-
# 1) 先装 torch——PyPI 默认 wheel 即 cu128,这一步必须在线(~900MB)
|
| 522 |
-
pip install torch==2.11.0
|
| 523 |
-
# 2) 其余全部离线:57 个锁定版本 wheel(3.10GB,含 triton 3.8.0 cp313 248MB、
|
| 524 |
-
# bitsandbytes 0.50.2 预编译 cu12 manylinux wheel——T4 上免编译)
|
| 525 |
-
pip install --no-index --find-links t4_assets/wheels/ -r requirements.txt
|
| 526 |
-
# 3) 可选:还原 NVRTC 内核缓存
|
| 527 |
-
tar xzf t4_assets/compile_cache/torch_kernels_t4.tar.gz -C ~/.cache/
|
| 528 |
-
# 4) 验证(期望:2.11.0+cu128 (7, 5))
|
| 529 |
-
python -c "import torch,transformers,peft,bitsandbytes; print(torch.__version__, torch.cuda.get_device_capability(0))"
|
| 530 |
-
```
|
| 531 |
-
|
| 532 |
-
### 13.2 版本锁定表(实测组合,勿混搭)
|
| 533 |
-
|
| 534 |
-
| 包 | 版本 | 说明 |
|
| 535 |
-
|----|------|------|
|
| 536 |
-
| torch | 2.11.0+cu128 | PyPI 默认 wheel,CUDA 12.8 运行时 |
|
| 537 |
-
| transformers | 5.16.1 | Qwen3-VL `AutoModelForImageTextToText` 路径 |
|
| 538 |
-
| peft | 0.20.0 | QLoRA 注入 + 适配器续载/合并 |
|
| 539 |
-
| accelerate | 1.14.0 | 设备放置与梯度检查点 |
|
| 540 |
-
| bitsandbytes | 0.50.2 | NF4 量化(预编译 cu12 wheel) |
|
| 541 |
-
| huggingface_hub | 1.29.0 | 上传走 xet(注意 §12.5 的 safetensors 坑) |
|
| 542 |
-
| CUDA / GPU | 12.8 / cc 7.5 (Tesla T4) | 内核缓存与 wheels 均按此组合实测 |
|
| 543 |
-
|
| 544 |
-
### 13.3 权重与产物清单(HF `tchbcb/qwenjev`)
|
| 545 |
-
|
| 546 |
-
| 路径 | 内容 |
|
| 547 |
-
|------|------|
|
| 548 |
-
| `t4_assets/checkpoints/qwenjev_s1.pt` | 冒烟终模型(三头 + 温度表,254,001,909B) |
|
| 549 |
-
| `t4_assets_v2/checkpoints/qwenjev_s1.pt` | **正式规模终模型(推荐用这个)** |
|
| 550 |
-
| `t4_assets{,_v2}/checkpoints/hf_lora/` | QLoRA 适配器(分片 + manifest + reassemble.py) |
|
| 551 |
-
| `t4_assets_v2/checkpoints/calib_report.json` | ECE 0.5134 → 0.4242(RLCD 120 行 + 255 校准行) |
|
| 552 |
-
| `t4_assets/wheels/` | 57 wheel / 3.10GB 离线安装包(cp313) |
|
| 553 |
-
| `t4_assets/compile_cache/` | NVRTC 内核缓存 tar + 审计注记(compile_cache_note.json) |
|
| 554 |
-
| `ops/` | 流水线 / 信封执行器 / 分片器 / Kaggle bundle / 编译缓存 tar |
|
| 555 |
-
| `configs/t4_smoke.yaml` / `configs/t4_scale.yaml` | T4 两轮配置(内含全部坑位注释) |
|
| 556 |
-
|
| 557 |
-
> wheels 按 cp313 抓取:换 Python 版本时在目标机执行 `pip download -r requirements.txt -d wheels/ -q`
|
| 558 |
-
> 重抓即可。本栈无源码编译依赖(bitsandbytes / triton 均有预编译 wheel),全程不需要 gcc/nvcc。
|
| 559 |
-
|
| 560 |
-
### 13.4 复现两轮训练
|
| 561 |
-
|
| 562 |
-
```bash
|
| 563 |
-
# 冒烟(~1.5h 窗口够用;VM 上脱离桥接进程发射)
|
| 564 |
-
python -B ops/t4_pipeline.py
|
| 565 |
-
# 正式规模(扩参内嵌于 ops/t4_scale.py,运行时写出 configs/t4_scale.yaml)
|
| 566 |
-
python -B ops/t4_scale.py
|
| 567 |
-
# 状态轮询(本地)
|
| 568 |
-
python ops/t4exec.py run "print(open('/content/qwenjev_work/scale_status.json').read())"
|
| 569 |
-
```
|
| 570 |
-
|
| 571 |
-
两轮均为断点续跑:杀掉重发即可,已完成阶段自动跳过(机制见 §12.4)。
|
| 572 |
-
|
| 573 |
-
### 13.5 评测快启(v0.4.1)
|
| 574 |
-
|
| 575 |
-
```bash
|
| 576 |
-
# 拉起 v2 终模型评测(自动下载 254MB 检查点 + 重组适配器 + merge 推理):
|
| 577 |
-
python ops/run_eval.py # GPU 机器(T4 实测 ~11min:载入 ~2min + 300 步对局 ~9min)
|
| 578 |
-
python ops/run_eval.py --games all25 # 25 公开游戏(需 arc-agi + environment_files)
|
| 579 |
-
# 无 GPU / 无权重(harness 验证,笔记本 CPU 实测 ~255ms/步):
|
| 580 |
-
python ops/run_eval.py --backend tiny --no-checkpoint --max-actions 60
|
| 581 |
-
```
|
| 582 |
-
|
| 583 |
-
实测记录(2026-09-22,T4 VM):`ops/run_eval.py --checkpoint checkpoints/qwenjev-s1-hf-v2 --backend hf`
|
| 584 |
-
→ fp16 基座 5.07GB、GPU 100%,mock-pushbox 300 步对局完成,报告在 `t4_assets_v2/checkpoints/eval_report_v2_entry.json`。
|
|
|
|
| 4 |
> 按顺序读完即可独立复现整个系统。README 按要求留空,所有内容都在本文档中。
|
| 5 |
> **v0.2.0 架构决策:视觉塔必保留在 System-1 每步推理路径上**——隐藏关全是陌生机制,
|
| 6 |
> 只看 hex 文本串没法玩游戏;本仓库的 System-1 骨干直接是多模态模型,每步都"看"当前帧。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
---
|
| 9 |
|
|
|
|
| 38 |
- 沿用其训练配方:complete-question 交叉熵,backbone lr `1e-5`、decision-head lr `1e-4`。
|
| 39 |
- 差异点:① state 序列化改为**版本化规范串**(原作审计文档承认 `str()` 序列化非规范,属遗留问题);② **叶子多模态化——视觉塔每步在环**;③ 增加 ACTION6(x,y) 的候选离散化;④ 外层接 ARC-AGI-3 环境与双系统调度。
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
---
|
| 42 |
|
| 43 |
## 2. 总体架构
|
|
|
|
| 121 |
### 3.4 System-1 `qwenjev/system1.py`
|
| 122 |
|
| 123 |
- `encode_question(state, candidates, instruction, grid)` → N 个多模态叶子(每候选一个,state 重复,**当前帧图像共享**);
|
| 124 |
+
- `forward_choice(leaves)` → **一次 backbone 前向(含视觉塔)**:叶子向量 → 投影 → set-attention(同题候选间)→ softmax → 概率列表;
|
| 125 |
+
- `choose(..., grid)` → 贪心或按分布采样,返回 `Decision(probs, candidates, chosen, index)`;
|
| 126 |
- `forward_boolean` / `forward_score` 及其**可训练孪生** `forward_boolean_logits` / `forward_score_logits`(v0.2.0 修复:旧实现返回 detach 张量,梯度根本流不进模型);
|
| 127 |
+
- `save()` / `load_heads()`:tiny 后端打包 backbone + 全部头;hf 后端只存头(权重留在 HF 权重目录);
|
| 128 |
- `eval()` / `train_mode()`:决策头 dropout=0,保证加载后推理确定。
|
| 129 |
|
| 130 |
### 3.5 System-2 `qwenjev/system2.py`
|
|
|
|
| 138 |
|
| 139 |
System-2 唤醒条件(满足其一,且调用数 < `system2_max_calls_per_game`):
|
| 140 |
|
| 141 |
+
| 条件 | 含义 |
|
| 142 |
+
|------|------|
|
| 143 |
+
| 新帧哈希 | 局面没见过,需要 9B 看一眼 |
|
| 144 |
+
| `levels_completed` 增加 | 进新关卡,机制可能变 |
|
| 145 |
+
| 连续 12 步无变化 | System-1 卡死,需要外部视角 |
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
同时管理:每局时间预算(默认 900s,官方文档明确超时会直接 fail 该次运行)、动作数上限、动作-效果记忆(哪个动作改变多少格)、每局反思笔记。
|
| 148 |
|
|
|
|
| 152 |
|
| 153 |
## 4. 数据管线
|
| 154 |
|
| 155 |
+
### 4.1 来源:25 个公开游戏自采(已确认可用,2026-09-23 实测)
|
| 156 |
|
| 157 |
+
- 游戏源码从 arcade API 自动获取:`arc-agi` 包 NORMAL 模式 `make()` 会把游戏源码(单游戏约 120KB)缓存到 `environment_files/<game_id>/<version>/`,之后永久离线(OFFLINE 模式约 2000 FPS、无限实例);**匿名 key 自动下发,无需 Kaggle 账号、无需接受比赛规则**。一键预缓存:`python ops/fetch_games.py`(25 游戏约 65 秒);
|
| 158 |
+
- kagglehub 比赛下载(`kagglehub.competition_download('arc-prize-2026-arc-agi-3')`)需要 KGAT token + 官网接受规则,且与本地游戏引擎是同一套(`arc-agi` 包本地托管同引擎)——本仓评测链路已不依赖它;
|
| 159 |
+
- 游戏清单(与 arcprize.org/tasks 一致,`ops/run_eval.py --list-games` 可从 API 拉实时列表):`ar25 bp35 cd82 cn04 dc22 ft09 g50t ka59 lf52 lp85 ls20 m0r0 r11l re86 s5i5 sb26 sc25 sk48 sp80 su15 tn36 tr87 tu93 vc33 wa30`;
|
| 160 |
- **评测是另外 110 个从未公开的隐藏游戏**(55 个 Public LB + 55 个 Private LB),所以数据管线的一切设计都服务于"可泛化的探索先验",而非背题。
|
| 161 |
|
| 162 |
### 4.2 采集 `data/collect_selfplay.py`
|
|
|
|
| 189 |
|
| 190 |
## 5. 训练方案
|
| 191 |
|
| 192 |
+
### 5.1 两阶段配方
|
| 193 |
|
| 194 |
**Stage A:视觉蒸馏对齐**(`train/distill_alignment.py`)
|
| 195 |
|
| 196 |
- 教师:冻结的多模态模型,**同一帧真实过其视觉塔**(hf 后端:processor 构造 `pixel_values` 进模型前向;tiny 后端:教师 TinyVLM 同样吃图像张量)——绝不允许出现"蒸馏时没看图"的假对齐(v0.1 的占位符实现已废弃);
|
| 197 |
- 学生:System-1 多模态骨干,同样图像+文本前向,取逐层隐状态池化;
|
| 198 |
- 对齐层集隐状态(默认最后 2 层)���`1 − cosine` 损失,逐层独立投影头;
|
| 199 |
+
- 产物:`checkpoints/qwenjev-s1/stage_a.pt`(投影头 + tiny 学生权重),Stage B 自动加载。
|
|
|
|
| 200 |
|
| 201 |
**Stage B:决策头 SFT**(`train/train_system1.py`)
|
| 202 |
|
|
|
|
| 210 |
|
| 211 |
**v0.2.0 训练修复清单**(旧代码静默失效的三处):① Boolean/Score 头改用可训练 logits 孪生方法,BCE/CE 梯度真正流进模型;② Choice 批训练改用带图前向(旧行为在 `torch.no_grad()` 里算概率再包装,同样训不动);③ Stage A checkpoint 自动加载,蒸馏成果不白费。
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
### 5.2 显存预算(RTX Pro 6000 96GB 实测口径)
|
| 214 |
|
| 215 |
| 场景 | 占用估算 |
|
|
|
|
| 221 |
|
| 222 |
消费级 24GB 卡(4090/3090)建议:教师 4bit + 学生 LoRA;tiny 后端则任意机器(含无 GPU 笔记本,~1.1M 参数 fp32 ≈ 5MB)都能跑。
|
| 223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
### 5.3 下载权重
|
| 225 |
|
| 226 |
```bash
|
|
|
|
| 290 |
|
| 291 |
## 8. 评测与迭代
|
| 292 |
|
| 293 |
+
### 8.1 一键评测(v0.4.2 起:`ops/run_eval.py`)
|
| 294 |
+
|
| 295 |
```bash
|
| 296 |
+
pip install arc-agi # 官方本地引擎(PyPI,纯 Python)
|
| 297 |
+
python ops/run_eval.py --download-only # 一次性缓存 25 公开游戏(约 3MB,匿名 key)
|
| 298 |
+
python ops/run_eval.py --games all25 # 全量评测:GPU→hf 后端自动拉 HF 检查点并 merge LoRA;无 GPU→tiny
|
| 299 |
+
python ops/run_eval.py --games ls20 --max-actions 60 # 指定游戏 + 预算
|
| 300 |
+
python ops/run_eval.py --backend tiny --no-checkpoint --games mock --max-actions 60 # 纯 harness 冒烟
|
| 301 |
+
python ops/run_eval.py --checkpoint /path/to/dir --games ... # 本地检查点目录(qwenjev_s1.pt [+ hf_lora/])
|
| 302 |
```
|
| 303 |
|
| 304 |
+
- 检查点解析(默认 `hf:tchbcb/qwenjev@t4_assets_v2/checkpoints`):自动下载 qwenjev_s1.pt + hf_lora 分片 → `parts/reassemble` 重组 → sha256 校验 → 后端 auto 决定 hf(fp16 基座 + LoRA merge,与训练同构)或 tiny;
|
| 305 |
+
- 生命周期:WIN 结束本局;GAME_OVER 触发 `brain.on_game_over` + 自动重置续玩(同一动作预算内公平比较,`resets` 入报告);
|
| 306 |
+
- 报告字段:`win / final_state / best_level / resets / actions / seconds / steps_per_s`,默认写 `eval_report.json`(`--out` 改路径);
|
| 307 |
+
- `--list-games` 从 arcade API 拉实时游戏列表(静态清单兜底)。
|
| 308 |
|
| 309 |
+
### 8.2 本地 CPU 能不能跑?(2026-09-23,2 核 / 3GB 容器实测)
|
| 310 |
+
|
| 311 |
+
| 配置 | 可行性 | 实测/推算依据 |
|
| 312 |
+
|---|---|---|
|
| 313 |
+
| tiny 后端 + v0.2.0 训练头(128 维) | ✅ 完整评测 | 25 游戏全部跑通(40 步/游戏),0.5–4 步/秒(快慢取决于 ACTION6 候选数) |
|
| 314 |
+
| tiny 后端 + v2 检查点 | ❌ 维度不匹配 | v2 头是 2048 维(hf 隐藏维),tiny 骨干是 128 维 |
|
| 315 |
+
| hf 后端(Qwen3-VL-2B fp16) | ⚠️ 需 ≥8GB 内存 | 基座权重 ~4.4GB + 三头 254MB(fp32, 63.5M 参数)+ 激活;本 3GB 容器物理不可行;bitsandbytes 4bit 仅限 CUDA,CPU 无替代 |
|
| 316 |
+
| hf 后端(GPU:T4 16GB) | ✅ 已验证 | fp16 基座 + merge 后 300 步 601.6s(§11.2 v2 eval) |
|
|
|
|
| 317 |
|
| 318 |
+
- CPU 评测的检查点是 `checkpoints/qwenjev-s1/`(v0.2.0 时代 128 维 tiny 头 + backbone,`load_heads` 向后兼容:escalate/温度表缺省跳过);其结论只用于 harness 验证与横向对比,不代表 v2 模型水平;
|
| 319 |
+
- 4bit 量化在 CPU 上不可用(bitsandbytes 依赖 CUDA),若要在 CPU 跑真 2B 建议在 ≥16GB 内存机器用 fp16(`--backend hf` 会自动走 `resume_adapters(merge=True)`);
|
| 320 |
+
- 提速方向:调小候选池(`build_candidates` 的 ACTION6 锚点数)或调大 `eval_leaf_batch`。
|
|
|
|
| 321 |
|
| 322 |
+
- 官方正式指标 RHAE = 按关卡 `min(人类步数/智能体步数, 1.0)²` 加权平均,人类基线本地拿不到——所以 `evaluate_local.py` 的绝对分数**只用于横向比较不同版本的智能体**,不代表排行榜;
|
| 323 |
- 迭代循环:改 `agent/my_agent.py`(或 hybrid 配置)→ 本地秒级验证 → 满意后再花 Kaggle Commit;
|
| 324 |
- 关注三条曲线:完成率(WIN 数)、平均步数、System-2 调用次数。好方向的标志是**完成率升、System-2 调用降**。
|
| 325 |
|
|
|
|
| 331 |
|--------|------|----------|------|
|
| 332 |
| M0 | 仓库骨架 + 单测 + 文档 | `make test` 全绿 | ✅ |
|
| 333 |
| M1 | **本地全链路跑通**:mock 游戏 → 采集 → 建库 → Stage A 蒸馏 → Stage B 训练 → 评测(视觉塔全程在环) | `make smoke` 五环节全绿,蒸馏 loss 收敛,36 个单测通过 | ✅ |
|
| 334 |
+
| M2 | 换 hf 后端:Qwen3-VL-2B 学生 + 9B 教师,25 ��开游戏真实采集 | Choice 标签集 ≥ 5 万题,公开关 eval 完成率显著高于随机基线 | |
|
| 335 |
+
| M3 | System-2 接入 + 调度调参 | 完成率升 & 9B 调用数下降 | |
|
|
|
|
| 336 |
| M4 | Kaggle 打包提交 | 官方 LB 出分,进入迭代循环 | |
|
| 337 |
|
| 338 |
+
后续方向:RLCD 后训练(NanoJev 官方 roadmap 同款)、共享前缀推理提速、把 Score 头升级为学习型局面价值模型。
|
| 339 |
|
| 340 |
---
|
| 341 |
|
|
|
|
| 355 |
|
| 356 |
```bash
|
| 357 |
make setup # 环境安装(Python 3.12+)
|
| 358 |
+
make test # 单测 36 个(纯 CPU,零下载)
|
| 359 |
+
make smoke # 一键本地跑通:mock采集→建库→蒸馏→训练→评测(无GPU可用)
|
| 360 |
make collect # 自博弈采轨迹(mock 游戏 / 25 公开游戏)
|
| 361 |
make build-data # 轨迹 → Choice/Boolean/Score 训练样本(含 grid 帧字段)
|
| 362 |
make distill # Stage A:9B 教师 → System-1 学生 视觉蒸馏对齐
|
| 363 |
make train-s1 # Stage B:决策头 SFT(NanoJev 配方,自动加载 stage A)
|
|
|
|
| 364 |
make eval # 本地评测(mock 游戏默认)
|
| 365 |
make push-hf # 推送本仓库到 HuggingFace(HF_TOKEN 从环境读)
|
| 366 |
python scripts/download_weights.py # 拉权重
|
| 367 |
python scripts/serve_system2.py --port 8000 # 9B 端点(transformers 兜底)
|
| 368 |
# vllm serve Qwen/Qwen3.5-9B --port 8000 # 9B 端点(首选)
|
| 369 |
python scripts/package_kaggle.py --username XXX --accelerator rtx6000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
```
|
| 371 |
|
| 372 |
### 11.1 本地跑通记录(v0.2.0,2026-09)
|
|
|
|
| 374 |
无 GPU(4GB RAM / CPU)环境实测:`make smoke` 全链路通过——mock 游戏 2400 步轨迹、5267 样本(train 4927 / val 340)、Stage A 蒸馏 56 步 align_loss 0.0062→0.0012 收敛、Stage B 三头联训完成、评测 agent 完整跑 600 步决策(每步含视觉塔前向,~210ms/步)。
|
| 375 |
|
| 376 |
切换真实模型的路径:`configs/train.yaml` 里 `system1.backend: "hf"`(学生 `Qwen/Qwen3-VL-2B-Instruct`)+ `stage_a_distill.teacher_model: "Qwen/Qwen3.5-9B"`,代码路径已就绪;建议先在单卡 GPU 机器上冒烟 hf 路径,再上 Kaggle。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
train/evaluate_local.py
CHANGED
|
@@ -32,15 +32,30 @@ from qwenjev.vision import TinyVLMConfig # noqa: E402
|
|
| 32 |
|
| 33 |
|
| 34 |
def run_game(env, game_id: str, brain: HybridAgent, max_actions: int) -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
env.reset()
|
| 36 |
actions = 0
|
|
|
|
| 37 |
best_level = 0
|
|
|
|
| 38 |
started = time.time()
|
| 39 |
for _ in range(max_actions):
|
| 40 |
-
grids = grids_from(env)
|
| 41 |
meta = meta_from(env)
|
| 42 |
-
|
|
|
|
| 43 |
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
cand = brain.decide(
|
| 45 |
game_id, grids,
|
| 46 |
levels_completed=meta["levels_completed"],
|
|
@@ -53,11 +68,13 @@ def run_game(env, game_id: str, brain: HybridAgent, max_actions: int) -> dict:
|
|
| 53 |
else:
|
| 54 |
step_env(env, cand.name)
|
| 55 |
actions += 1
|
| 56 |
-
best_level = max(best_level,
|
| 57 |
return {
|
| 58 |
"game_id": game_id,
|
| 59 |
-
"win":
|
|
|
|
| 60 |
"best_level": best_level,
|
|
|
|
| 61 |
"actions": actions,
|
| 62 |
"seconds": round(time.time() - started, 1),
|
| 63 |
}
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
def run_game(env, game_id: str, brain: HybridAgent, max_actions: int) -> dict:
|
| 35 |
+
"""Play one episode against any environment (mock or official).
|
| 36 |
+
|
| 37 |
+
Lifecycle: WIN ends the episode; GAME_OVER triggers the hybrid brain's
|
| 38 |
+
episode-boundary hook and a fresh reset, then play continues within the
|
| 39 |
+
same action budget — identical budget for every agent variant, so
|
| 40 |
+
best_level/actions stay comparable across runs."""
|
| 41 |
env.reset()
|
| 42 |
actions = 0
|
| 43 |
+
resets = 0
|
| 44 |
best_level = 0
|
| 45 |
+
final_state = "NOT_FINISHED"
|
| 46 |
started = time.time()
|
| 47 |
for _ in range(max_actions):
|
|
|
|
| 48 |
meta = meta_from(env)
|
| 49 |
+
final_state = meta["state"]
|
| 50 |
+
if final_state == "WIN":
|
| 51 |
break
|
| 52 |
+
if final_state == "GAME_OVER":
|
| 53 |
+
brain.on_game_over(game_id, [f"game over at action {actions}"])
|
| 54 |
+
env.reset()
|
| 55 |
+
resets += 1
|
| 56 |
+
continue
|
| 57 |
+
grids = grids_from(env)
|
| 58 |
+
best_level = max(best_level, meta["levels_completed"])
|
| 59 |
cand = brain.decide(
|
| 60 |
game_id, grids,
|
| 61 |
levels_completed=meta["levels_completed"],
|
|
|
|
| 68 |
else:
|
| 69 |
step_env(env, cand.name)
|
| 70 |
actions += 1
|
| 71 |
+
best_level = max(best_level, meta_from(env)["levels_completed"])
|
| 72 |
return {
|
| 73 |
"game_id": game_id,
|
| 74 |
+
"win": final_state == "WIN",
|
| 75 |
+
"final_state": final_state,
|
| 76 |
"best_level": best_level,
|
| 77 |
+
"resets": resets,
|
| 78 |
"actions": actions,
|
| 79 |
"seconds": round(time.time() - started, 1),
|
| 80 |
}
|