tchbcb commited on
Commit
2bc1c6b
·
verified ·
1 Parent(s): f9d9abd

QwenJev v0.4.3: 25-public-games eval in-repo (run_eval incremental report, fetch_games, CPU feasibility verified)

Browse files
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
- Game files are the competition's environment_files/ (Kaggle Data page,
50
- arc-prize-2026-arc-agi-3). Point ARC_ENVIRONMENTS_DIR at it when running
51
- from another directory; defaults to ./environment_files."""
 
 
 
 
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
- arc = arc_agi.Arcade(operation_mode=OperationMode.OFFLINE,
59
- environments_dir=os.environ.get(
60
- "ARC_ENVIRONMENTS_DIR", "environment_files"))
61
- return arc.make(game_id, render_mode=render_mode) if render_mode else arc.make(game_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
 
64
  def grids_from(env) -> list:
65
- """Best-effort frame extraction across toolkit versions."""
66
- frame = env.observe() if hasattr(env, "observe") else getattr(env, "frame", None)
 
 
 
 
67
  if frame is None:
68
- frame = getattr(env, "latest_frame", None)
69
  for attr in ("frames", "frame", "grids"):
70
  grids = getattr(frame, attr, None)
71
- if grids:
72
- return [list(map(list, g)) for g in grids]
73
- if isinstance(frame, list) and frame and isinstance(frame[0], list):
74
- return [list(map(list, frame))]
 
 
 
 
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.observe() if hasattr(env, "observe") else 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": tuple(getattr(frame, "available_actions", ()) or ()),
84
- "state": str(getattr(frame, "state", "NOT_FINISHED")),
 
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 action_name == "ACTION6" and x is not None:
306
- return env.step(action, x=x, y=y)
 
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.1)**:`hf:<repo>@<subpath>` 自动下载检查点 + 重组适配器(sha256 校验)→ 后端 auto(GPU→hf fp16+LoRA merge / 否则 tiny)→ 逐游戏对局 → 带 provenance 的 JSON 报告。`--games all25` 跑 25 公开游戏(缺 arc-agi 给出指引);`--no-checkpoint --backend tiny` 即 CPU harness 测试 | 任意机器 |
 
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
- print("\n".join(PUBLIC_GAMES_FALLBACK))
 
 
 
 
 
 
 
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
- " + the competition's environment_files/ (Kaggle Data page:\n"
147
- " arc-prize-2026-arc-agi-3). Run from the directory holding\n"
148
- " environment_files/ or export ARC_ENVIRONMENTS_DIR=/path/to it.\n"
149
- "On Kaggle the data is attached automatically; see train.md §4.1.")
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
- URL = "https://aitun.cc/DED976KN"
 
 
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
- if CRC_MODE == "raw":
31
- return format(zlib.crc32(raw) & 0xFFFFFFFF, "08x")
32
- return format(zlib.crc32(p_b64.encode()) & 0xFFFFFFFF, "08x")
33
 
34
 
35
  def envelope(code: str) -> bytes:
 
 
 
36
  raw = code.encode()
37
  p = b64u(raw)
38
- lines = ["samaicmdbegin", "v=1", "enc=b64url", "crc=" + _crc(p, raw)]
39
- for i in range(0, len(p), 76):
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.2.0: vision tower on the System-1 inference path (tiny/hf backends, mock env, e2e local run)",
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(同题候选间)→ 温度除法(v0.3.0:Stage C 温度表按 `choice:2/3-5/6-10/11+` 桶查找,空表=未校准恒等于 1.0)→ softmax → 概率列��;
153
- - `choose(..., grid)` → 贪心或按分布采样,返回 `Decision(probs, candidates, chosen, index, confidence, escalate_p)`——后两个是 v0.3.0 新增的校准读数:归一化熵置信度 + 学习型升级概率(escalate 头与视觉塔共享同一次前向,零额外开销);
154
  - `forward_boolean` / `forward_score` 及其**可训练孪生** `forward_boolean_logits` / `forward_score_logits`(v0.2.0 修复:旧实现返回 detach 张量,梯度根本流不进模型);
155
- - `save()` / `load_heads()`:tiny 后端打包 backbone + 全部头(含 escalate 头与温度表,旧 checkpoint 无这些字段也能加载);hf 后端只存头(权重留在 HF 权重目录);
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
- - 比赛官方 Data 页自带 `environment_files/`(25 个公开游戏文件,Apache 2.0),`arc-agi` 包离线模式本地跑:**约 2000 FPS、无限实例、无需 API key**;
189
- - 游戏清单(与 arcprize.org/tasks 一致):`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`;
 
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
- make eval # 本地跑公开游戏,输出完成度/步数/耗时 JSON 报告
 
 
 
 
 
357
  ```
358
 
359
- **一键评测入口(v0.4.1 新增):`ops/run_eval.py`**——评测也进了仓,下次零手工步骤:
 
 
 
360
 
361
- ```bash
362
- python ops/run_eval.py # 默认:HF 拉取 t4_assets_v2 终模型 → 自动选后端 → mock 对局
363
- python ops/run_eval.py --games all25 # 25 个公开游戏(需 arc-agi 包 + environment_files,缺包直接报错并给指引)
364
- python ops/run_eval.py --backend tiny --no-checkpoint --max-actions 60 # 无 GPU 机器 harness 测试
365
- python ops/run_eval.py --checkpoint /本地目录 # 已有本地检查点免下载
366
- make eval-v2 # 同第一条(Makefile 目标)
367
- make eval-25 # 同第二条
368
- make eval-cpu # 同第三条
369
- ```
370
 
371
- 入口自动完成:① 从 HF 下载 `qwenjev_s1.pt` + `hf_lora` 分片并**重组适配器**(sha256 校验,§12.5 方案);
372
- ② 后端 auto 选择(有 CUDA → hf fp16 基座 + LoRA merge;否则 tiny);③ 逐游戏跑 HybridAgent
373
- (System-1 加载失败自动降级启发式策略,不中断);④ 报告含 provenance(后端/检查点来源/耗时/时间戳)。
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
- | M1.5 | **Stage C 校准**(吸收 Laya):RLCD + 温度按桶拟合 + escalate 头训练,调度器学习型唤醒 | 51 个单测通过,校准 ECE 报告产出,`make smoke` 六环节全绿 | ✅ |
389
- | M2 | 换 hf 后端:Qwen3-VL-2B 学生 + 4B 教师(T4)/ 9B 教师(96GB),25 公开游戏真实采集 | Choice 标签集 ≥ 5 万题,公开关 eval 完成率显著高于随机基线 | 🔶 T4 真机两轮全绿(mock 口径,§12);公开游戏采集待 arc-agi 环境 |
390
- | M3 | System-2 接入 + 调度调参(重点:escalate 阈值 0.70/0.15 按真实关数据校准) | 完成率升 & 9B 调用数下降 | |
391
  | M4 | Kaggle 打包提交 | 官方 LB 出分,进入迭代循环 | |
392
 
393
- 后续方向:共享前缀推理提速、tiny 骨干 ONNX 导出(借鉴 receptron/laya 的纯 CPU 运行时,Kaggle Code 禁网环境受益)、把 Score 头升级为学习型局面价值模型。
394
 
395
  ---
396
 
@@ -410,29 +355,18 @@ make eval-cpu # 同第三条
410
 
411
  ```bash
412
  make setup # 环境安装(Python 3.12+)
413
- make test # 单测 51 个(纯 CPU,零下载)
414
- make smoke # 一键本地跑通:mock采集→建库→蒸馏→训练→校准→评测(无GPU可用)
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
- if meta["state"] in ("WIN", "GAME_OVER"):
 
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, meta["levels_completed"])
57
  return {
58
  "game_id": game_id,
59
- "win": meta_from(env)["state"] == "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
  }