dsh-eval-guard 0.6.1 → 0.7.0

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,57 @@
1
+ # neural-cifar-eval
2
+
3
+ Standalone evaluator for the **Neural-CIFAR** task. Copy this whole folder to a
4
+ more performance-stable Linux machine to rerun the model evaluation without the
5
+ DSH plugin UI.
6
+
7
+ ## Contents
8
+
9
+ - `run_eval.py` — builds the candidate, runs the original operator metrics,
10
+ then measures CIFAR-10 validation accuracy/FPS at 4 threads and all threads.
11
+ - `ops_eval.py` / `common.py` — self-contained operator evaluator.
12
+ - `fetch_cifar.py` — downloads the official CIFAR-10 test split and writes
13
+ `hidden/val_images.bin` + `hidden/val_labels.bin`.
14
+ - `hidden/` — hidden validation data; not part of the agent workspace.
15
+
16
+ ## Prepare hidden validation
17
+
18
+ ```bash
19
+ python3 fetch_cifar.py
20
+ # optional proxy:
21
+ python3 fetch_cifar.py --proxy http://127.0.0.1:7897
22
+ ```
23
+
24
+ CIFAR-10 cache is shared under `~/.dsh/eval-guard/cache/cifar10/` and is
25
+ protected by `.download.lock`. If the lock exists, another DSH instance is
26
+ downloading; wait for it or delete the lock file manually. A complete cache
27
+ with no lock is reused directly. Downloads print progress.
28
+
29
+ ## Run
30
+
31
+ ```bash
32
+ python3 run_eval.py --workspace /path/to/session/workspace
33
+ ```
34
+
35
+ The candidate workspace must contain `build/nn_task`, `build/train`,
36
+ `build/infer`, and a trained `model/` directory. The evaluator does not train;
37
+ it loads the already-trained model.
38
+
39
+ The first output lines are always:
40
+
41
+ ```text
42
+ all_operations_executable = ...
43
+ max_weighted_mean_relative_error = ...
44
+ speed_geomean = ...
45
+ model_4_thread = ...
46
+ model_all_threads = ...
47
+ ```
48
+
49
+ Then every operator row is printed. There is no unified score and no resolved
50
+ gate. The 4-thread 0.02 s/image requirement is reported as
51
+ `meets_4thread_budget`, but does not change any PASS/FAIL result.
52
+
53
+ ## Notes
54
+
55
+ - PyTorch is required on the evaluation machine for the operator oracle.
56
+ - The 4-thread and all-thread model runs use the same trained model.
57
+ - The validation data and labels never enter the candidate workspace.
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env python3
2
+ """Shared CIFAR-10 cache/downloader with lock and live progress.
3
+
4
+ Cache (fixed under the user's home):
5
+ ~/.dsh/eval-guard/cache/cifar10/
6
+ train_images.bin, train_labels.bin
7
+ val_images.bin, val_labels.bin
8
+ .download.lock (only while a download is in progress)
9
+
10
+ A complete cache is reused directly. The lock is created with O_EXCL; if it
11
+ already exists, another DSH instance is downloading (or crashed while doing
12
+ so), and the user must wait or delete the lock file manually.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import os
18
+ import shutil
19
+ import signal
20
+ import socket
21
+ import sys
22
+ import tarfile
23
+ import tempfile
24
+ import time
25
+ import urllib.request
26
+ from pathlib import Path
27
+
28
+ URL = 'https://www.cs.toronto.edu/~kriz/cifar-10-binary.tar.gz'
29
+ CACHE = Path.home() / '.dsh' / 'eval-guard' / 'cache' / 'cifar10'
30
+ TRAIN_N = 50000
31
+ VAL_N = 10000
32
+ TRAIN_IMAGES = TRAIN_N * 3072
33
+ VAL_IMAGES = VAL_N * 3072
34
+ TOKEN = os.environ.get('EVAL_INSTALL_TOKEN', '')
35
+ EXPECTED = {
36
+ 'train_images.bin': TRAIN_IMAGES,
37
+ 'train_labels.bin': TRAIN_N,
38
+ 'val_images.bin': VAL_IMAGES,
39
+ 'val_labels.bin': VAL_N,
40
+ }
41
+
42
+
43
+ def cache_complete(cache: Path) -> bool:
44
+ return all((cache / name).is_file() and (cache / name).stat().st_size == size
45
+ for name, size in EXPECTED.items())
46
+
47
+
48
+ def lock_path(cache: Path) -> Path:
49
+ return cache / '.download.lock'
50
+
51
+
52
+ def normalize_proxy(raw: str) -> str:
53
+ raw = raw.strip()
54
+ if raw == '':
55
+ return ''
56
+ return raw if '://' in raw else 'http://' + raw
57
+
58
+
59
+ def opener(proxy: str):
60
+ if proxy:
61
+ handler = urllib.request.ProxyHandler({'http': proxy, 'https': proxy})
62
+ return urllib.request.build_opener(handler)
63
+ return urllib.request.build_opener()
64
+
65
+
66
+ def download(url: str, proxy: str) -> None:
67
+ req = urllib.request.Request(url, headers={'User-Agent': 'dsh-eval-guard-cifar/1.0'})
68
+ with opener(proxy).open(req, timeout=60) as response, tempfile.NamedTemporaryFile(
69
+ prefix='cifar10-', suffix='.tar.gz', dir=str(CACHE), delete=False) as temp:
70
+ total = int(response.headers.get('Content-Length') or 0)
71
+ done = 0
72
+ start = time.time()
73
+ while True:
74
+ chunk = response.read(1024 * 256)
75
+ if not chunk:
76
+ break
77
+ temp.write(chunk)
78
+ done += len(chunk)
79
+ if total:
80
+ pct = done * 100.0 / total
81
+ print(f'\rCIFAR-10 download: {pct:5.1f}% {done/1048576:7.1f}/{total/1048576:7.1f} MiB', end='', flush=True)
82
+ else:
83
+ print(f'\rCIFAR-10 download: {done/1048576:7.1f} MiB', end='', flush=True)
84
+ print(f'\nCIFAR-10 download complete ({done/1048576:.1f} MiB, {time.time()-start:.1f}s)')
85
+ temp.flush()
86
+ os.fsync(temp.fileno())
87
+ return Path(temp.name)
88
+
89
+
90
+ def materialize(tar_path: Path, cache: Path) -> None:
91
+ print('CIFAR-10 extracting and writing cache ...', flush=True)
92
+ with tempfile.TemporaryDirectory(prefix='cifar10-extract-', dir=str(cache)) as td:
93
+ with tarfile.open(tar_path, 'r:gz') as tf:
94
+ tf.extractall(td)
95
+ base = Path(td) / 'cifar-10-batches-bin'
96
+ tmp_train_images = cache / 'train_images.bin.tmp'
97
+ tmp_train_labels = cache / 'train_labels.bin.tmp'
98
+ tmp_val_images = cache / 'val_images.bin.tmp'
99
+ tmp_val_labels = cache / 'val_labels.bin.tmp'
100
+ with tmp_train_images.open('wb') as images, tmp_train_labels.open('wb') as labels:
101
+ for i in range(1, 6):
102
+ raw = (base / f'data_batch_{i}.bin').read_bytes()
103
+ for off in range(0, len(raw), 3073):
104
+ labels.write(raw[off:off + 1])
105
+ images.write(raw[off + 1:off + 3073])
106
+ raw = (base / 'test_batch.bin').read_bytes()
107
+ with tmp_val_images.open('wb') as images, tmp_val_labels.open('wb') as labels:
108
+ for off in range(0, len(raw), 3073):
109
+ labels.write(raw[off:off + 1])
110
+ images.write(raw[off + 1:off + 3073])
111
+ for name in EXPECTED:
112
+ os.replace(cache / (name + '.tmp'), cache / name)
113
+
114
+
115
+ def copy_into(cache: Path, split: str, target: Path) -> None:
116
+ target.mkdir(parents=True, exist_ok=True)
117
+ if split in ('train', 'both'):
118
+ shutil.copyfile(cache / 'train_images.bin', target / 'train_images.bin')
119
+ shutil.copyfile(cache / 'train_labels.bin', target / 'train_labels.bin')
120
+ if split in ('val', 'both'):
121
+ shutil.copyfile(cache / 'val_images.bin', target / 'val_images.bin')
122
+ shutil.copyfile(cache / 'val_labels.bin', target / 'val_labels.bin')
123
+
124
+
125
+ def main() -> int:
126
+ parser = argparse.ArgumentParser()
127
+ parser.add_argument('--cache', default=str(CACHE))
128
+ parser.add_argument('--proxy', default=os.environ.get('EVAL_CIFAR_PROXY', ''))
129
+ parser.add_argument('--url', default=os.environ.get('EVAL_CIFAR_URL', '') or URL)
130
+ parser.add_argument('--copy-train', default='')
131
+ parser.add_argument('--copy-val', default='')
132
+ args = parser.parse_args()
133
+ cache = Path(args.cache).expanduser()
134
+ cache.mkdir(parents=True, exist_ok=True)
135
+ lock = lock_path(cache)
136
+
137
+ if lock.exists():
138
+ try:
139
+ info = lock.read_text(encoding='utf-8', errors='replace').strip()
140
+ except Exception:
141
+ info = ''
142
+ print('CIFAR-10 cache is locked by another download.', file=sys.stderr)
143
+ print('If no other DSH instance is downloading, delete:', str(lock), file=sys.stderr)
144
+ if info:
145
+ print('Lock info:', info, file=sys.stderr)
146
+ return 2
147
+
148
+ if not cache_complete(cache):
149
+ fd = None
150
+ try:
151
+ fd = os.open(str(lock), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
152
+ except FileExistsError:
153
+ print('CIFAR-10 cache is locked by another download. Wait or delete:', str(lock), file=sys.stderr)
154
+ return 2
155
+ try:
156
+ with os.fdopen(fd, 'w', encoding='utf-8') as handle:
157
+ fd = None
158
+ handle.write(f'pid={os.getpid()} host={socket.gethostname()} time={time.strftime("%Y-%m-%d %H:%M:%S")} token={TOKEN}\n')
159
+ def _terminate(signum, _frame):
160
+ try:
161
+ lock.unlink()
162
+ except FileNotFoundError:
163
+ pass
164
+ os._exit(128 + signum)
165
+ signal.signal(signal.SIGTERM, _terminate)
166
+ signal.signal(signal.SIGINT, _terminate)
167
+ if not cache_complete(cache):
168
+ tar_path = download(args.url, normalize_proxy(args.proxy))
169
+ try:
170
+ materialize(tar_path, cache)
171
+ finally:
172
+ try:
173
+ tar_path.unlink()
174
+ except OSError:
175
+ pass
176
+ finally:
177
+ if fd is not None:
178
+ try:
179
+ os.close(fd)
180
+ except OSError:
181
+ pass
182
+ try:
183
+ lock.unlink()
184
+ except FileNotFoundError:
185
+ pass
186
+
187
+ if not cache_complete(cache):
188
+ print('CIFAR-10 cache is incomplete after download.', file=sys.stderr)
189
+ return 1
190
+ print('CIFAR-10 cache:', cache)
191
+ if args.copy_train:
192
+ copy_into(cache, 'train', Path(args.copy_train))
193
+ print('copied training split to', args.copy_train)
194
+ if args.copy_val:
195
+ copy_into(cache, 'val', Path(args.copy_val))
196
+ print('copied validation split to', args.copy_val)
197
+ return 0
198
+
199
+
200
+ if __name__ == '__main__':
201
+ raise SystemExit(main())
@@ -0,0 +1,67 @@
1
+ """Shared helpers for single-workspace evaluator wrappers."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ from pathlib import Path
12
+
13
+ EVAL_ROOT = Path(__file__).resolve().parent
14
+
15
+
16
+ def parse_workspace() -> Path:
17
+ parser = argparse.ArgumentParser(description='single-workspace evaluator')
18
+ parser.add_argument('--workspace', required=True)
19
+ args = parser.parse_args()
20
+ return Path(args.workspace).resolve()
21
+
22
+
23
+ def emit(payload: dict) -> None:
24
+ target = os.environ.get('EVAL_RESULT_FILE')
25
+ if target is None or target == '':
26
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
27
+ return
28
+ Path(target).write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding='utf-8')
29
+
30
+
31
+ def clean_copy(source: Path, target: Path, ignore_venv: bool = False,
32
+ ignore_names: tuple[str, ...] = ()) -> None:
33
+ ignored = set(ignore_names)
34
+
35
+ def ignore(directory: str, names: list[str]) -> set[str]:
36
+ if Path(directory) == source:
37
+ skip = set(ignored)
38
+ if ignore_venv:
39
+ skip.add('.venv')
40
+ return {name for name in names if name in skip}
41
+ return set()
42
+ shutil.copytree(source, target, symlinks=True, ignore=ignore)
43
+
44
+
45
+ def run_legacy(script: Path, args: list[str], out: Path) -> dict:
46
+ cmd = [sys.executable, '-X', 'utf8', str(script), *args, '--out', str(out)]
47
+ env = {k: v for k, v in os.environ.items() if k not in {'PYTHONPATH', 'PYTHONHOME'}}
48
+ completed = subprocess.run(cmd, cwd=str(EVAL_ROOT), capture_output=True, text=True, encoding='utf-8', errors='replace', env=env, timeout=1800)
49
+ if completed.returncode != 0:
50
+ return {'ok': False, 'error': (completed.stderr or completed.stdout or 'evaluator failed')[-4000:]}
51
+ summary = out / 'summary.json'
52
+ if not summary.is_file():
53
+ return {'ok': False, 'error': 'summary.json missing'}
54
+ return {'ok': True, 'summary': json.loads(summary.read_text(encoding='utf-8'))}
55
+
56
+
57
+ def candidate_row(summary: dict, name: str = 'candidate') -> dict:
58
+ rows = summary.get('results') or summary.get('participants') or summary.get('rows') or []
59
+ for row in rows:
60
+ if row.get('name') == name:
61
+ return row
62
+ return {}
63
+
64
+
65
+ def fail(payload: dict) -> int:
66
+ emit(payload)
67
+ return 1
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env python3
2
+ """Fetch the hidden CIFAR-10 validation split via the shared locked cache."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ HERE = Path(__file__).resolve().parent
12
+
13
+
14
+ def main() -> int:
15
+ parser = argparse.ArgumentParser()
16
+ parser.add_argument('--out', default=str(HERE / 'hidden'))
17
+ parser.add_argument('--proxy', default=os.environ.get('EVAL_CIFAR_PROXY', ''))
18
+ parser.add_argument('--url', default=os.environ.get('EVAL_CIFAR_URL', ''))
19
+ args = parser.parse_args()
20
+ out = Path(args.out)
21
+ out.mkdir(parents=True, exist_ok=True)
22
+ cmd = [sys.executable, '-X', 'utf8', str(HERE / 'cifar_cache.py'), '--copy-val', str(out)]
23
+ if args.proxy:
24
+ cmd += ['--proxy', args.proxy]
25
+ if args.url:
26
+ cmd += ['--url', args.url]
27
+ return subprocess.run(cmd, cwd=str(HERE)).returncode
28
+
29
+
30
+ if __name__ == '__main__':
31
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+