dsh-eval-guard 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env python3
2
+ """Standalone Neural-CIFAR evaluator.
3
+
4
+ Copies the workspace into a temporary build directory, runs the original
5
+ operator benchmark, then loads the already-trained model and measures CIFAR-10
6
+ validation accuracy/FPS at 4 threads and at all logical CPUs.
7
+
8
+ The hidden validation images/labels live beside this script in hidden/ and are
9
+ never written into the agent workspace.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import math
16
+ import os
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+ import tempfile
21
+ import time
22
+ from pathlib import Path
23
+
24
+ import numpy as np
25
+
26
+ HERE = Path(__file__).resolve().parent
27
+ sys.path.insert(0, str(HERE))
28
+ from common import clean_copy
29
+
30
+ HIDDEN = HERE / 'hidden'
31
+ OPS = HERE / 'ops_eval.py'
32
+
33
+
34
+ def run(cmd, cwd, timeout=1800):
35
+ env = dict(os.environ)
36
+ env.pop('PYTHONPATH', None)
37
+ env.pop('PYTHONHOME', None)
38
+ return subprocess.run(cmd, cwd=str(cwd), capture_output=True, text=True,
39
+ encoding='utf-8', errors='replace', env=env, timeout=timeout)
40
+
41
+
42
+ def build_candidate(workspace: Path, root: Path):
43
+ candidate = root / 'candidate'
44
+ clean_copy(workspace, candidate, ignore_names=(
45
+ 'data', 'build', 'work', '.tmp', '.cache', '.home', '.config', '.local',
46
+ '.git', '.venv', 'node_modules',
47
+ ))
48
+ source = candidate
49
+ if not (source / 'CMakeLists.txt').is_file() and (candidate / 'workspace' / 'CMakeLists.txt').is_file():
50
+ source = candidate / 'workspace'
51
+ if not (source / 'CMakeLists.txt').is_file():
52
+ raise RuntimeError('CMakeLists.txt missing at workspace root or workspace/')
53
+ build = source / 'build'
54
+ if build.exists():
55
+ shutil.rmtree(build, ignore_errors=True)
56
+ build.mkdir(parents=True)
57
+ cfg = run(['cmake', '-S', '.', '-B', 'build', '-DCMAKE_BUILD_TYPE=Release'], source, 180)
58
+ if cfg.returncode != 0:
59
+ raise RuntimeError('cmake configure failed\n' + (cfg.stdout + cfg.stderr)[-4000:])
60
+ made = run(['cmake', '--build', 'build', '-j4'], source, 1800)
61
+ if made.returncode != 0:
62
+ raise RuntimeError('build failed\n' + (made.stdout + made.stderr)[-4000:])
63
+ return source, build
64
+
65
+
66
+ def read_preds(path: Path, n: int) -> np.ndarray:
67
+ a = np.frombuffer(path.read_bytes(), dtype=np.uint8).copy()
68
+ if a.size != n:
69
+ raise RuntimeError(f'prediction size {a.size} != {n}')
70
+ return a
71
+
72
+
73
+ def model_metrics(source: Path, threads: int, budget: float | None = None) -> dict:
74
+ infer = source / 'build' / 'infer'
75
+ images = HIDDEN / 'val_images.bin'
76
+ labels = HIDDEN / 'val_labels.bin'
77
+ if not infer.is_file():
78
+ return {'executable': False, 'accuracy': None, 'fps': None,
79
+ 'mean_seconds_per_image': None, 'error': 'build/infer missing',
80
+ 'meets_4thread_budget': None}
81
+ if not images.is_file() or not labels.is_file():
82
+ return {'executable': False, 'accuracy': None, 'fps': None,
83
+ 'mean_seconds_per_image': None, 'error': 'hidden validation data missing; run fetch_cifar.py',
84
+ 'meets_4thread_budget': None}
85
+ n = labels.stat().st_size
86
+ out = source / f'predictions_{threads}.bin'
87
+ if out.exists():
88
+ out.unlink()
89
+ cmd = [str(infer), '--model', str(source / 'model'),
90
+ '--images', str(images), '--out', str(out), '--threads', str(threads)]
91
+ started = time.perf_counter()
92
+ try:
93
+ proc = run(cmd, source, 1200)
94
+ except subprocess.TimeoutExpired:
95
+ return {'executable': False, 'accuracy': None, 'fps': None,
96
+ 'mean_seconds_per_image': None, 'error': 'inference timeout',
97
+ 'meets_4thread_budget': None}
98
+ elapsed = time.perf_counter() - started
99
+ if proc.returncode != 0:
100
+ return {'executable': False, 'accuracy': None, 'fps': None,
101
+ 'mean_seconds_per_image': None,
102
+ 'error': (proc.stderr or proc.stdout or 'infer failed')[-1000:],
103
+ 'meets_4thread_budget': None}
104
+ try:
105
+ pred = read_preds(out, n)
106
+ true = np.frombuffer(labels.read_bytes(), dtype=np.uint8)
107
+ acc = float((pred == true).mean() * 100.0)
108
+ except Exception as exc:
109
+ return {'executable': False, 'accuracy': None, 'fps': None,
110
+ 'mean_seconds_per_image': None, 'error': str(exc),
111
+ 'meets_4thread_budget': None}
112
+ fps = n / elapsed if elapsed > 0 else None
113
+ mean_s = elapsed / n if n else None
114
+ result = {'executable': True, 'accuracy': acc, 'fps': fps,
115
+ 'mean_seconds_per_image': mean_s, 'images': n,
116
+ 'threads': threads, 'error': ''}
117
+ if budget is not None:
118
+ result['meets_4thread_budget'] = bool(mean_s is not None and mean_s <= budget)
119
+ return result
120
+
121
+
122
+ def main() -> int:
123
+ parser = argparse.ArgumentParser()
124
+ parser.add_argument('--workspace', required=True)
125
+ args = parser.parse_args()
126
+ workspace = Path(args.workspace).resolve()
127
+
128
+ if not OPS.is_file():
129
+ return fail({'task': 'neural-cifar', 'ok': False, 'error': 'ops_eval.py missing'})
130
+
131
+ with tempfile.TemporaryDirectory(prefix='neural-cifar-eval-') as td:
132
+ root = Path(td)
133
+ # Reuse the existing FP32 operator evaluator, but run it in a temp
134
+ # copy so its build output does not affect the model workspace.
135
+ ops_result = root / 'ops.json'
136
+ env = dict(os.environ)
137
+ env['EVAL_RESULT_FILE'] = str(ops_result)
138
+ env.pop('PYTHONPATH', None)
139
+ env.pop('PYTHONHOME', None)
140
+ try:
141
+ proc = subprocess.run([sys.executable, '-X', 'utf8', str(OPS), '--workspace', str(workspace)],
142
+ cwd=str(HERE), capture_output=True, text=True,
143
+ encoding='utf-8', errors='replace', env=env, timeout=2400)
144
+ except subprocess.TimeoutExpired:
145
+ return fail({'task': 'neural-cifar', 'ok': False, 'error': 'operator evaluation timed out'})
146
+ if proc.returncode != 0 or not ops_result.is_file():
147
+ return fail({'task': 'neural-cifar', 'ok': False, 'error': 'operator evaluation failed',
148
+ 'output': (proc.stdout + proc.stderr)[-4000:]})
149
+ ops = json.loads(ops_result.read_text(encoding='utf-8'))
150
+
151
+ try:
152
+ source, _build = build_candidate(workspace, root)
153
+ except Exception as exc:
154
+ return fail({'task': 'neural-cifar', 'ok': False, 'error': str(exc)})
155
+
156
+ if not (HIDDEN / 'val_images.bin').is_file() or not (HIDDEN / 'val_labels.bin').is_file():
157
+ try:
158
+ fetch = subprocess.run([sys.executable, '-X', 'utf8', str(HERE / 'fetch_cifar.py')],
159
+ cwd=str(HERE), capture_output=True, text=True,
160
+ encoding='utf-8', errors='replace', timeout=1800)
161
+ except subprocess.TimeoutExpired:
162
+ fetch = None
163
+ if fetch is None or fetch.returncode != 0:
164
+ return fail({'task': 'neural-cifar', 'ok': False,
165
+ 'error': 'hidden validation data missing and fetch failed',
166
+ 'output': (fetch.stdout + fetch.stderr)[-3000:] if fetch is not None else 'timeout'})
167
+
168
+ model4 = model_metrics(source, 4, budget=0.02)
169
+ modelall = model_metrics(source, os.cpu_count() or 1)
170
+
171
+ print(f"all_operations_executable = {str(ops.get('all_operations_executable')).lower()}")
172
+ print(f"max_weighted_mean_relative_error = {ops.get('max_weighted_mean_relative_error')}")
173
+ print(f"speed_geomean = {ops.get('speed_geomean')}")
174
+ print(f"model_4_thread = {json.dumps(model4, ensure_ascii=False)}")
175
+ print(f"model_all_threads = {json.dumps(modelall, ensure_ascii=False)}")
176
+ for row in ops.get('operations', []):
177
+ print(f"{row.get('name')}: executable={row.get('executable')} pass={row.get('passed')} "
178
+ f"mre={row.get('weighted_mean_relative_error')} speed_gflops={row.get('speed_gflops')}")
179
+
180
+ result = {
181
+ 'task': 'neural-cifar',
182
+ 'ok': True,
183
+ 'all_operations_executable': ops.get('all_operations_executable'),
184
+ 'max_weighted_mean_relative_error': ops.get('max_weighted_mean_relative_error'),
185
+ 'speed_geomean': ops.get('speed_geomean'),
186
+ 'model_4_thread': model4,
187
+ 'model_all_threads': modelall,
188
+ 'operations': ops.get('operations', []),
189
+ }
190
+ target = os.environ.get('EVAL_RESULT_FILE')
191
+ if target:
192
+ Path(target).write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
193
+ else:
194
+ print(json.dumps(result, ensure_ascii=False, indent=2))
195
+ return 0
196
+
197
+
198
+ def fail(payload: dict) -> int:
199
+ target = os.environ.get('EVAL_RESULT_FILE')
200
+ if target:
201
+ Path(target).write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding='utf-8')
202
+ else:
203
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
204
+ return 1
205
+
206
+
207
+ if __name__ == '__main__':
208
+ raise SystemExit(main())
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env python3
2
+ """Standalone evaluator for the Neural-Pendulum task.
3
+
4
+ It uses the latest preserved submission under .eval/submissions (falling back to
5
+ the live workspace), builds it from source, runs the public self-test, drives
6
+ the controller against a fixed-initial-state evaluation simulator, and computes
7
+ the task's time-integral score from .eval/events.jsonl.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import math
13
+ import os
14
+ import selectors
15
+ import shutil
16
+ import subprocess
17
+ import sys
18
+ import tempfile
19
+ import time
20
+ from pathlib import Path
21
+
22
+ HERE = Path(__file__).resolve().parent
23
+ SIM = HERE / 'sim_eval'
24
+ DT = 0.01
25
+ MAX_STEPS = 3000
26
+ T_TGT = 10.0
27
+ UP_TOL = 0.05
28
+
29
+
30
+ def read_line(proc: subprocess.Popen, timeout: float) -> str:
31
+ selector = selectors.DefaultSelector()
32
+ selector.register(proc.stdout, selectors.EVENT_READ)
33
+ try:
34
+ if not selector.select(timeout):
35
+ raise RuntimeError('timeout')
36
+ line = proc.stdout.readline()
37
+ if line == '':
38
+ raise RuntimeError('process closed')
39
+ return line.strip()
40
+ finally:
41
+ selector.close()
42
+
43
+
44
+ def write_line(proc: subprocess.Popen, obj: dict) -> None:
45
+ assert proc.stdin is not None
46
+ proc.stdin.write(json.dumps(obj) + '\n')
47
+ proc.stdin.flush()
48
+
49
+
50
+ def read_events(workspace: Path) -> list[dict]:
51
+ path = workspace / '.eval' / 'events.jsonl'
52
+ out: list[dict] = []
53
+ if path.is_file():
54
+ for line in path.read_text(encoding='utf-8', errors='replace').splitlines():
55
+ try:
56
+ value = json.loads(line)
57
+ if isinstance(value, dict):
58
+ out.append(value)
59
+ except Exception:
60
+ continue
61
+ return out
62
+
63
+
64
+ def task_start(workspace: Path, events: list[dict]) -> float:
65
+ meta = workspace / '.eval' / 'meta.json'
66
+ if meta.is_file():
67
+ try:
68
+ return float(json.loads(meta.read_text(encoding='utf-8')).get('started_at'))
69
+ except Exception:
70
+ pass
71
+ if events:
72
+ return float(events[0].get('time', events[0].get('at', time.time())))
73
+ return time.time()
74
+
75
+
76
+ def score_for(event: dict) -> float:
77
+ stage = event.get('stage', '')
78
+ if stage not in ('scoring', 'completed') and event.get('valid') is not True:
79
+ return -1.0
80
+ try:
81
+ t_can = float(event.get('t_can', 0.0))
82
+ except Exception:
83
+ return -1.0
84
+ if not math.isfinite(t_can):
85
+ return -1.0
86
+ return (min(max(t_can, 0.0), T_TGT) - T_TGT) / T_TGT
87
+
88
+
89
+ def final_score(workspace: Path, events: list[dict], now: float | None = None) -> tuple[float, float | None]:
90
+ """Return (integral, T_done); negative score is penalized over time."""
91
+ now = time.time() if now is None else now
92
+ start = task_start(workspace, events)
93
+ tests = [e for e in events if (e.get('event') or e.get('type')) == 'test']
94
+ if not tests:
95
+ return -1.0 * max(0.0, now - start), None
96
+ done_at = None
97
+ prev_at = start
98
+ current = -1.0
99
+ total = 0.0
100
+ for e in tests:
101
+ at = float(e.get('time', e.get('at', prev_at)))
102
+ if at < prev_at:
103
+ at = prev_at
104
+ total += current * (at - prev_at)
105
+ current = score_for(e)
106
+ prev_at = at
107
+ if e.get('completed'):
108
+ done_at = at
109
+ break
110
+ end = done_at if done_at is not None else now
111
+ total += current * max(0.0, end - prev_at)
112
+ return total, done_at
113
+
114
+
115
+ def archive_submissions(workspace: Path) -> int:
116
+ root = os.environ.get('EVAL_SUBMISSIONS_ROOT', '')
117
+ src_root = workspace / '.eval' / 'submissions'
118
+ if not root or not src_root.is_dir():
119
+ return 0
120
+ target = Path(root)
121
+ target.mkdir(parents=True, exist_ok=True)
122
+ count = 0
123
+ for source in src_root.iterdir():
124
+ if not source.is_dir():
125
+ continue
126
+ destination = target / source.name
127
+ if destination.exists():
128
+ continue
129
+ shutil.copytree(source, destination, symlinks=True)
130
+ count += 1
131
+ return count
132
+
133
+
134
+ def latest_submission(workspace: Path) -> Path | None:
135
+ root = workspace / '.eval' / 'submissions'
136
+ if not root.is_dir():
137
+ return None
138
+ dirs = [p for p in root.iterdir() if p.is_dir()]
139
+ if not dirs:
140
+ return None
141
+ return max(dirs, key=lambda p: p.stat().st_mtime)
142
+
143
+
144
+ def fail(message: str, infra_ok: bool = False, workspace: Path | None = None) -> dict:
145
+ result = {'infra_ok': infra_ok, 't_can': 0.0, 'success': False, 'error': message}
146
+ if workspace is not None:
147
+ events = read_events(workspace)
148
+ result['final_score'], done = final_score(workspace, events)
149
+ result['done_at'] = done
150
+ result['submissions'] = len(list((workspace / '.eval' / 'submissions').glob('*'))) if (workspace / '.eval' / 'submissions').is_dir() else 0
151
+ return result
152
+
153
+
154
+ def main() -> int:
155
+ import argparse
156
+ parser = argparse.ArgumentParser()
157
+ parser.add_argument('--workspace', required=True)
158
+ args = parser.parse_args()
159
+
160
+ workspace = Path(args.workspace).resolve()
161
+ events = read_events(workspace)
162
+ root = Path(tempfile.mkdtemp(prefix='neural-pendulum-eval-'))
163
+ candidate = root / 'candidate'
164
+ result = {'infra_ok': False, 't_can': 0.0, 'success': False, 'error': ''}
165
+ sim = None
166
+ control = None
167
+ try:
168
+ archived = archive_submissions(workspace)
169
+ selected = latest_submission(workspace)
170
+ if selected is None:
171
+ return emit(fail('no submission', workspace=workspace))
172
+ source = selected
173
+ shutil.copytree(source, candidate, symlinks=True)
174
+ if not (candidate / 'CMakeLists.txt').is_file():
175
+ return emit(fail('CMakeLists.txt missing', workspace=workspace))
176
+
177
+ env = dict(os.environ)
178
+ env.pop('PYTHONPATH', None)
179
+ env.pop('PYTHONHOME', None)
180
+ build = candidate / 'build'
181
+ if build.exists():
182
+ shutil.rmtree(build, ignore_errors=True)
183
+ cfg = subprocess.run(
184
+ ['cmake', '-S', '.', '-B', 'build', '-DCMAKE_BUILD_TYPE=Release'],
185
+ cwd=str(candidate), capture_output=True, text=True, env=env, timeout=180,
186
+ )
187
+ if cfg.returncode != 0:
188
+ return emit(fail('cmake configure failed\n' + (cfg.stdout + cfg.stderr)[-4000:], workspace=workspace))
189
+ made = subprocess.run(
190
+ ['cmake', '--build', 'build', '-j4'],
191
+ cwd=str(candidate), capture_output=True, text=True, env=env, timeout=1800,
192
+ )
193
+ if made.returncode != 0:
194
+ return emit(fail('build failed\n' + (made.stdout + made.stderr)[-4000:], workspace=workspace))
195
+
196
+ lib = candidate / 'build' / 'libnn.a'
197
+ if not lib.is_file():
198
+ return emit(fail('build/libnn.a missing', workspace=workspace))
199
+ hidden = HERE.parent / 'assets' / 'neural-pendulum' / 'hidden' / 'test_infra.cpp'
200
+ if not hidden.is_file():
201
+ return emit(fail('hidden infrastructure test missing', workspace=workspace))
202
+ infra_exe = root / 'test_infra'
203
+ compiler = shutil.which('g++')
204
+ if compiler is None:
205
+ return emit(fail('g++ missing', workspace=workspace))
206
+ ic = subprocess.run([compiler, '-std=c++17', '-O3', '-pthread',
207
+ '-I', str(candidate / 'include'), str(hidden), str(lib),
208
+ '-lm', '-o', str(infra_exe)], cwd=str(root), capture_output=True,
209
+ text=True, env=env, timeout=300)
210
+ if ic.returncode != 0:
211
+ return emit(fail('hidden infrastructure test compile failed\n' + (ic.stdout + ic.stderr)[-4000:], workspace=workspace))
212
+ it = subprocess.run([str(infra_exe)], cwd=str(root), capture_output=True,
213
+ text=True, env=env, timeout=600)
214
+ if it.returncode != 0:
215
+ return emit(fail('hidden infrastructure test failed\n' + (it.stdout + it.stderr)[-4000:], workspace=workspace))
216
+
217
+ control_path = candidate / 'build' / 'control'
218
+ if not control_path.is_file():
219
+ return emit(fail('build/control missing', infra_ok=True, workspace=workspace))
220
+
221
+ sim = subprocess.Popen([str(SIM), '--eval'], stdin=subprocess.PIPE,
222
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
223
+ text=True, bufsize=1)
224
+ control = subprocess.Popen([str(control_path)], cwd=str(candidate),
225
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
226
+ stderr=subprocess.PIPE, text=True, bufsize=1)
227
+
228
+ write_line(sim, {'cmd': 'reset'})
229
+ obs = json.loads(read_line(sim, 10.0))['obs']
230
+ write_line(control, {'type': 'reset'})
231
+ ready = json.loads(read_line(control, 10.0))
232
+ if ready.get('type') != 'ready':
233
+ return emit(fail('controller did not reply ready', infra_ok=True, workspace=workspace))
234
+
235
+ t_can = 0.0
236
+ up_start = None
237
+ completed = False
238
+ error = ''
239
+ for step in range(MAX_STEPS):
240
+ if not isinstance(obs, list) or len(obs) != 3:
241
+ error = 'simulator returned bad observation'
242
+ break
243
+ if max(abs(float(x)) for x in obs) < UP_TOL:
244
+ if up_start is None:
245
+ up_start = step * DT
246
+ t_can = max(t_can, step * DT - up_start)
247
+ else:
248
+ up_start = None
249
+ if t_can >= T_TGT:
250
+ completed = True
251
+ break
252
+
253
+ write_line(control, {'type': 'step', 'step': step, 'state': obs})
254
+ action = json.loads(read_line(control, 0.1))
255
+ torque = action.get('torque')
256
+ if (not isinstance(torque, list) or len(torque) != 2
257
+ or not all(isinstance(v, (int, float)) and math.isfinite(v) for v in torque)
258
+ or any(abs(float(v)) > 1.0 for v in torque)):
259
+ error = 'controller returned invalid torque'
260
+ break
261
+ write_line(sim, {'cmd': 'step', 'torque': torque})
262
+ reply = json.loads(read_line(sim, 10.0))
263
+ if 'error' in reply:
264
+ break
265
+ obs = reply['obs']
266
+
267
+ integral, done_at = final_score(workspace, events)
268
+ result = {'infra_ok': True, 't_can': float(t_can),
269
+ 'success': bool(completed), 'error': error,
270
+ 'final_score': integral, 'done_at': done_at,
271
+ 'submissions': len(list((workspace / '.eval' / 'submissions').glob('*'))) if (workspace / '.eval' / 'submissions').is_dir() else 0,
272
+ 'archived_submissions': archived,
273
+ 'source': str(selected.relative_to(workspace)) if selected is not None else 'live-workspace'}
274
+ return emit(result)
275
+ except Exception as exc:
276
+ return emit(fail(str(exc), workspace=workspace))
277
+ finally:
278
+ for proc in (control, sim):
279
+ if proc is not None:
280
+ try:
281
+ proc.terminate()
282
+ proc.wait(timeout=2)
283
+ except Exception:
284
+ try:
285
+ proc.kill()
286
+ except Exception:
287
+ pass
288
+ shutil.rmtree(root, ignore_errors=True)
289
+
290
+
291
+ def emit(result: dict) -> int:
292
+ target = os.environ.get('EVAL_RESULT_FILE')
293
+ text = json.dumps(result, ensure_ascii=False, indent=2)
294
+ if target:
295
+ Path(target).write_text(text, encoding='utf-8')
296
+ else:
297
+ print(text)
298
+ return 0
299
+
300
+
301
+ if __name__ == '__main__':
302
+ raise SystemExit(main())
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-eval-guard",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "description": "Evaluation guard for DSH: workspace-only filesystem, no network/subagents, wall-clock limits, and task installation for the meow-ai-arena tank task.",
6
6
  "license": "MIT",
@@ -45,7 +45,9 @@
45
45
  "eval",
46
46
  "cordis.patch.yml",
47
47
  "README.md",
48
- "scripts"
48
+ "scripts",
49
+ "neural-cifar-eval",
50
+ "neural-pendulum-eval"
49
51
  ],
50
52
  "scripts": {
51
53
  "build": "tsdown",