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.
- package/assets/neural-pendulum/SPEC.md +470 -0
- package/assets/neural-pendulum/TASK.md +360 -0
- package/assets/neural-pendulum/hidden/test_infra.cpp +631 -0
- package/assets/neural-pendulum/include/nn/nn.hpp +88 -0
- package/assets/neural-pendulum/sim.cpp +126 -0
- package/assets/neural-pendulum/sim_eval +0 -0
- package/assets/neural-pendulum/workspace.zip +0 -0
- package/eval/neural-pendulum.py +15 -0
- package/lib/client/index.js +41 -4
- package/lib/client/index.js.map +1 -1
- package/lib/index.js +454 -60
- package/neural-cifar-eval/README.md +57 -0
- package/neural-cifar-eval/cifar_cache.py +201 -0
- package/neural-cifar-eval/common.py +67 -0
- package/neural-cifar-eval/fetch_cifar.py +31 -0
- package/neural-cifar-eval/hidden/.gitkeep +1 -0
- package/neural-cifar-eval/ops_eval.py +502 -0
- package/neural-cifar-eval/run_eval.py +208 -0
- package/neural-pendulum-eval/run_eval.py +302 -0
- package/neural-pendulum-eval/sim_eval +0 -0
- package/package.json +4 -2
- package/eval/__pycache__/common.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-cifar.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-fp32.cpython-310.pyc +0 -0
|
@@ -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
|
+
|