dsh-eval-guard 0.7.1 → 0.7.3
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/ISSUES.md +235 -0
- package/README.md +46 -18
- package/assets/neural-cifar/workspace.zip +0 -0
- package/assets/neural-cpp/workspace.zip +0 -0
- package/assets/neural-fp32/workspace.zip +0 -0
- package/assets/neural-pendulum/SPEC.md +41 -38
- package/assets/neural-pendulum/TASK.md +146 -32
- package/assets/neural-pendulum/sim.cpp +4 -2
- package/assets/neural-pendulum/sim_eval +0 -0
- package/assets/neural-pendulum/workspace.zip +0 -0
- package/eval/README.md +19 -9
- package/eval/__pycache__/bomberman.cpython-310.pyc +0 -0
- package/eval/__pycache__/commit0.cpython-310.pyc +0 -0
- package/eval/__pycache__/common.cpython-310.pyc +0 -0
- package/eval/__pycache__/ctf.cpython-310.pyc +0 -0
- package/eval/__pycache__/featurebench.cpython-310.pyc +0 -0
- package/eval/__pycache__/land.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-cifar.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-cpp.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-fp32.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-pendulum.cpython-310.pyc +0 -0
- package/eval/__pycache__/swe-bench-pro.cpython-310.pyc +0 -0
- package/eval/__pycache__/tank.cpython-310.pyc +0 -0
- package/eval/__pycache__/warehouse.cpython-310.pyc +0 -0
- package/eval/bomberman.py +5 -3
- package/eval/commit0.py +10 -3
- package/eval/common.py +169 -5
- package/eval/ctf.py +4 -2
- package/eval/featurebench.py +12 -8
- package/eval/land.py +4 -2
- package/eval/legacy/__pycache__/bomberman.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/commit0.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/ctf.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/featurebench.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/land.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/swe_bench_pro.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/tank.cpython-310.pyc +0 -0
- package/eval/legacy/__pycache__/warehouse.cpython-310.pyc +0 -0
- package/eval/legacy/bomberman.py +5 -2
- package/eval/legacy/ctf.py +5 -2
- package/eval/legacy/featurebench.py +55 -15
- package/eval/legacy/land.py +6 -3
- package/eval/legacy/swe_bench_pro.py +57 -16
- package/eval/legacy/tank.py +2 -2
- package/eval/legacy/warehouse.py +4 -4
- package/eval/neural-cpp.py +9 -6
- package/eval/neural-fp32.py +17 -5
- package/eval/swe-bench-pro.py +11 -7
- package/eval/tank.py +11 -2
- package/eval/warehouse.py +17 -1
- package/lib/client/index.js +73 -46
- package/lib/client/index.js.map +1 -1
- package/lib/index.js +894 -301
- package/native/dsh-eval-supervisor +0 -0
- package/native/supervisor.c +190 -26
- package/neural-cifar-eval/README.md +5 -0
- package/neural-cifar-eval/__pycache__/cifar_cache.cpython-310.pyc +0 -0
- package/neural-cifar-eval/__pycache__/common.cpython-310.pyc +0 -0
- package/neural-cifar-eval/__pycache__/fetch_cifar.cpython-310.pyc +0 -0
- package/neural-cifar-eval/__pycache__/ops_eval.cpython-310.pyc +0 -0
- package/neural-cifar-eval/__pycache__/run_eval.cpython-310.pyc +0 -0
- package/neural-cifar-eval/common.py +13 -0
- package/neural-cifar-eval/ops_eval.py +17 -5
- package/neural-cifar-eval/run_eval.py +42 -32
- package/neural-pendulum-eval/README.md +62 -0
- package/neural-pendulum-eval/__pycache__/ops_eval.cpython-310.pyc +0 -0
- package/neural-pendulum-eval/__pycache__/run_eval.cpython-310.pyc +0 -0
- package/neural-pendulum-eval/ops_eval.py +603 -0
- package/neural-pendulum-eval/run_eval.py +218 -137
- package/neural-pendulum-eval/sim_eval +0 -0
- package/package.json +7 -11
- package/assets/neural-pendulum/hidden/test_infra.cpp +0 -631
- package/assets/neural-pendulum/include/nn/nn.hpp +0 -88
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Operator oracle for the Neural-Pendulum backend.
|
|
3
|
+
|
|
4
|
+
Numeric references are computed with PyTorch on data generated in-process from
|
|
5
|
+
fixed seeds; the workspace never receives expected tensors. Candidate CLI
|
|
6
|
+
outputs are compared with a weighted mean relative error (sum|cand-ref| /
|
|
7
|
+
sum|ref|) against fixed per-operation thresholds.
|
|
8
|
+
|
|
9
|
+
The operation set, cases, seeds and thresholds are identical to the oracle used
|
|
10
|
+
by Neural-FP32 / Neural-CIFAR, so a backend that passes those tasks passes here.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import math
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
import time
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
import torch
|
|
27
|
+
import torch.nn.functional as F
|
|
28
|
+
|
|
29
|
+
TASK = 'neural-pendulum'
|
|
30
|
+
|
|
31
|
+
HOST_EVAL_KEYS = frozenset({
|
|
32
|
+
'EVAL_RESULT_FILE', 'EVAL_JOB_DIR', 'EVAL_INSTALL_TOKEN',
|
|
33
|
+
'EVAL_BASELINE_DIR', 'EVAL_VENV_MANIFEST',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def child_env() -> dict[str, str]:
|
|
38
|
+
"""Environment for build/candidate children: no host evaluation handles."""
|
|
39
|
+
return {
|
|
40
|
+
key: value for key, value in os.environ.items()
|
|
41
|
+
if key not in HOST_EVAL_KEYS and key not in {'PYTHONPATH', 'PYTHONHOME'}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Never let install leftovers or host-side evaluation state influence the build.
|
|
45
|
+
IGNORED_COPY_NAMES = frozenset({
|
|
46
|
+
'build', 'work', '.tmp', '.cache', '.git', '.eval', '.submissions',
|
|
47
|
+
'node_modules', '__pycache__', '.venv',
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
# The oracle must not benefit the candidate: single thread, deterministic
|
|
51
|
+
# kernels, highest matmul precision.
|
|
52
|
+
torch.set_num_threads(1)
|
|
53
|
+
torch.set_float32_matmul_precision('highest')
|
|
54
|
+
torch.use_deterministic_algorithms(True)
|
|
55
|
+
torch.backends.mkldnn.deterministic = True
|
|
56
|
+
torch.manual_seed(1234)
|
|
57
|
+
|
|
58
|
+
SEEDS = (11, 22, 33)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def write_tensor(path: Path, t: torch.Tensor) -> None:
|
|
62
|
+
arr = t.detach().cpu().contiguous().numpy().astype('<f4', copy=False)
|
|
63
|
+
path.write_bytes(arr.tobytes())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_tensor(path: Path, shape: tuple[int, ...]) -> torch.Tensor:
|
|
67
|
+
arr = np.frombuffer(path.read_bytes(), dtype='<f4').copy()
|
|
68
|
+
return torch.from_numpy(arr).reshape(shape)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def mre(cand: torch.Tensor, ref: torch.Tensor) -> float:
|
|
72
|
+
if cand.shape != ref.shape:
|
|
73
|
+
return float('inf')
|
|
74
|
+
c = cand.detach().cpu().numpy().astype(np.float64)
|
|
75
|
+
r = ref.detach().cpu().numpy().astype(np.float64)
|
|
76
|
+
if not np.isfinite(c).all():
|
|
77
|
+
return float('inf')
|
|
78
|
+
den = max(float(np.abs(r).sum()), 1e-30)
|
|
79
|
+
return float(np.abs(c - r).sum() / den)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Case:
|
|
83
|
+
def __init__(self, inputs: dict[str, torch.Tensor], outputs: dict[str, torch.Tensor],
|
|
84
|
+
args, flops: float):
|
|
85
|
+
self.inputs = inputs
|
|
86
|
+
self.outputs = outputs
|
|
87
|
+
self.args = args
|
|
88
|
+
self.flops = flops
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def run_operation(name: str, threshold: float, cases: list[Case], binary: Path, root: Path,
|
|
92
|
+
invocation_timeout: float = 60.0) -> dict:
|
|
93
|
+
op_dir = root / 'ops' / name
|
|
94
|
+
op_dir.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
executable = True
|
|
96
|
+
total_diff = 0.0
|
|
97
|
+
total_ref = 0.0
|
|
98
|
+
total_sec = 0.0
|
|
99
|
+
total_flops = 0.0
|
|
100
|
+
first_error = ''
|
|
101
|
+
compared = False
|
|
102
|
+
for idx, case in enumerate(cases):
|
|
103
|
+
case_dir = op_dir / str(idx)
|
|
104
|
+
case_dir.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
paths: dict[str, str] = {}
|
|
106
|
+
for key, tensor in case.inputs.items():
|
|
107
|
+
p = case_dir / (key + '.bin')
|
|
108
|
+
write_tensor(p, tensor)
|
|
109
|
+
paths[key] = str(p)
|
|
110
|
+
for key in case.outputs:
|
|
111
|
+
paths[key] = str(case_dir / (key + '.bin'))
|
|
112
|
+
cmd = [str(binary)] + [str(x) for x in case.args(paths)]
|
|
113
|
+
started = time.perf_counter()
|
|
114
|
+
try:
|
|
115
|
+
proc = subprocess.run(cmd, cwd=str(case_dir), capture_output=True, text=True,
|
|
116
|
+
encoding='utf-8', errors='replace', timeout=invocation_timeout,
|
|
117
|
+
env=child_env())
|
|
118
|
+
except subprocess.TimeoutExpired:
|
|
119
|
+
executable = False
|
|
120
|
+
first_error = first_error or 'timeout'
|
|
121
|
+
continue
|
|
122
|
+
elapsed = time.perf_counter() - started
|
|
123
|
+
if proc.returncode != 0:
|
|
124
|
+
executable = False
|
|
125
|
+
first_error = first_error or (proc.stderr or proc.stdout or ('exit ' + str(proc.returncode)))[-500:]
|
|
126
|
+
continue
|
|
127
|
+
total_sec += elapsed
|
|
128
|
+
total_flops += case.flops
|
|
129
|
+
for key, ref in case.outputs.items():
|
|
130
|
+
p = case_dir / (key + '.bin')
|
|
131
|
+
if not p.is_file():
|
|
132
|
+
executable = False
|
|
133
|
+
first_error = first_error or ('missing output ' + key)
|
|
134
|
+
continue
|
|
135
|
+
try:
|
|
136
|
+
cand = read_tensor(p, tuple(ref.shape))
|
|
137
|
+
except Exception as exc:
|
|
138
|
+
executable = False
|
|
139
|
+
first_error = first_error or ('failed to read ' + key + ': ' + str(exc))
|
|
140
|
+
continue
|
|
141
|
+
# Numeric comparison is done even if the candidate is otherwise
|
|
142
|
+
# valid; a failed numeric comparison does not suppress speed.
|
|
143
|
+
total_ref += max(float(ref.detach().cpu().numpy().astype(np.float64).__abs__().sum()), 1e-30)
|
|
144
|
+
total_diff += float(np.abs(cand.detach().cpu().numpy().astype(np.float64) -
|
|
145
|
+
ref.detach().cpu().numpy().astype(np.float64)).sum())
|
|
146
|
+
compared = True
|
|
147
|
+
value = (total_diff / total_ref) if compared and total_ref > 0 else None
|
|
148
|
+
passed = executable and value is not None and value <= threshold
|
|
149
|
+
speed = (total_flops / total_sec / 1e9) if total_sec > 0 else None
|
|
150
|
+
return {
|
|
151
|
+
'name': name,
|
|
152
|
+
'executable': executable,
|
|
153
|
+
'passed': passed,
|
|
154
|
+
'weighted_mean_relative_error': value,
|
|
155
|
+
'threshold': threshold,
|
|
156
|
+
'speed_gflops': speed,
|
|
157
|
+
'error': first_error if not executable else '',
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def t(shape, seed, scale=1.0):
|
|
162
|
+
g = torch.Generator().manual_seed(seed)
|
|
163
|
+
return torch.randn(shape, generator=g, dtype=torch.float32) * scale
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
# Case builders. Every operation uses three independent data/parameter sets.
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
def cases_elementwise(op: str) -> list[Case]:
|
|
171
|
+
out = []
|
|
172
|
+
for s in SEEDS:
|
|
173
|
+
a = t((64, 256), s)
|
|
174
|
+
b = t((64, 256), s + 100)
|
|
175
|
+
if op == 'div':
|
|
176
|
+
b = torch.where(b.abs() < 0.2, b + 0.5, b)
|
|
177
|
+
ref = {'add': a + b, 'sub': a - b, 'mul': a * b, 'div': a / b}[op]
|
|
178
|
+
out.append(Case({'a': a, 'b': b}, {'out': ref},
|
|
179
|
+
lambda p: ['elementwise', '--op', op, '--shape', '64,256',
|
|
180
|
+
'--a', p['a'], '--b', p['b'], '--out', p['out']],
|
|
181
|
+
a.numel()))
|
|
182
|
+
return out
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def cases_matmul() -> list[Case]:
|
|
186
|
+
out = []
|
|
187
|
+
for s in SEEDS:
|
|
188
|
+
a = t((256, 256), s)
|
|
189
|
+
b = t((256, 256), s + 1)
|
|
190
|
+
out.append(Case({'a': a, 'b': b}, {'out': a @ b},
|
|
191
|
+
lambda p: ['matmul', '--a-shape', '256,256', '--b-shape', '256,256',
|
|
192
|
+
'--a', p['a'], '--b', p['b'], '--out', p['out']],
|
|
193
|
+
2 * 256 * 256 * 256))
|
|
194
|
+
return out
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def cases_linear() -> list[Case]:
|
|
198
|
+
out = []
|
|
199
|
+
for s in SEEDS:
|
|
200
|
+
x = t((256, 256), s)
|
|
201
|
+
w = t((128, 256), s + 1)
|
|
202
|
+
b = t((128,), s + 2)
|
|
203
|
+
ref = x @ w.T + b
|
|
204
|
+
out.append(Case({'x': x, 'w': w, 'bias': b}, {'out': ref},
|
|
205
|
+
lambda p: ['linear', '--x-shape', '256,256', '--w-shape', '128,256',
|
|
206
|
+
'--b-shape', '128', '--x', p['x'], '--w', p['w'],
|
|
207
|
+
'--bias', p['bias'], '--out', p['out']],
|
|
208
|
+
2 * 256 * 256 * 128))
|
|
209
|
+
return out
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def cases_relu() -> list[Case]:
|
|
213
|
+
out = []
|
|
214
|
+
for s in SEEDS:
|
|
215
|
+
x = t((512, 512), s)
|
|
216
|
+
out.append(Case({'x': x}, {'out': torch.relu(x)},
|
|
217
|
+
lambda p: ['relu', '--shape', '512,512', '--x', p['x'], '--out', p['out']],
|
|
218
|
+
x.numel()))
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def cases_softmax() -> list[Case]:
|
|
223
|
+
out = []
|
|
224
|
+
for s in SEEDS:
|
|
225
|
+
x = t((256, 256), s)
|
|
226
|
+
out.append(Case({'x': x}, {'out': torch.softmax(x, dim=-1)},
|
|
227
|
+
lambda p: ['softmax', '--shape', '256,256', '--x', p['x'], '--out', p['out']],
|
|
228
|
+
5 * x.numel()))
|
|
229
|
+
return out
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def cases_layernorm() -> list[Case]:
|
|
233
|
+
out = []
|
|
234
|
+
for s in SEEDS:
|
|
235
|
+
x = t((256, 256), s)
|
|
236
|
+
gamma = torch.randn(256, generator=torch.Generator().manual_seed(s + 1)) * 0.2 + 1.0
|
|
237
|
+
beta = t((256,), s + 2) * 0.1
|
|
238
|
+
ref = F.layer_norm(x, (256,), gamma, beta, 1e-5)
|
|
239
|
+
out.append(Case({'x': x, 'gamma': gamma, 'beta': beta}, {'out': ref},
|
|
240
|
+
lambda p: ['layernorm', '--shape', '256,256', '--x', p['x'],
|
|
241
|
+
'--gamma', p['gamma'], '--beta', p['beta'],
|
|
242
|
+
'--eps', '1e-5', '--out', p['out']],
|
|
243
|
+
8 * x.numel()))
|
|
244
|
+
return out
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def cases_mse() -> list[Case]:
|
|
248
|
+
out = []
|
|
249
|
+
for s in SEEDS:
|
|
250
|
+
pred = t((512, 512), s)
|
|
251
|
+
target = t((512, 512), s + 1)
|
|
252
|
+
ref = ((pred - target) ** 2).mean().reshape(1)
|
|
253
|
+
out.append(Case({'pred': pred, 'target': target}, {'out': ref},
|
|
254
|
+
lambda p: ['mse', '--pred-shape', '512,512', '--target-shape', '512,512',
|
|
255
|
+
'--pred', p['pred'], '--target', p['target'], '--out', p['out']],
|
|
256
|
+
2 * pred.numel()))
|
|
257
|
+
return out
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def conv_output(h: int, w: int, k: int, stride: int, pad: int, dil: int) -> tuple[int, int]:
|
|
261
|
+
return ((h + 2 * pad - dil * (k - 1) - 1) // stride + 1,
|
|
262
|
+
(w + 2 * pad - dil * (k - 1) - 1) // stride + 1)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def cases_conv2d() -> list[Case]:
|
|
266
|
+
out = []
|
|
267
|
+
for s in SEEDS:
|
|
268
|
+
x = t((8, 3, 32, 32), s)
|
|
269
|
+
w = t((8, 3, 3, 3), s + 1)
|
|
270
|
+
b = t((8,), s + 2)
|
|
271
|
+
ref = F.conv2d(x, w, b, stride=1, padding=1, dilation=1, groups=1)
|
|
272
|
+
ho, wo = conv_output(32, 32, 3, 1, 1, 1)
|
|
273
|
+
out.append(Case({'x': x, 'w': w, 'bias': b}, {'out': ref},
|
|
274
|
+
lambda p: ['conv2d', '--x-shape', '8,3,32,32', '--w-shape', '8,3,3,3',
|
|
275
|
+
'--b-shape', '8', '--x', p['x'], '--w', p['w'], '--bias', p['bias'],
|
|
276
|
+
'--stride', '1,1', '--padding', '1,1', '--dilation', '1,1',
|
|
277
|
+
'--groups', '1', '--out', p['out']],
|
|
278
|
+
2 * 8 * 8 * ho * wo * 3 * 3 * 3))
|
|
279
|
+
return out
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def cases_matmul_backward() -> list[Case]:
|
|
283
|
+
out = []
|
|
284
|
+
for s in SEEDS:
|
|
285
|
+
a = t((256, 256), s).requires_grad_()
|
|
286
|
+
b = t((256, 256), s + 1).requires_grad_()
|
|
287
|
+
g = t((256, 256), s + 2)
|
|
288
|
+
c = a @ b
|
|
289
|
+
da, db = torch.autograd.grad(c, (a, b), g)
|
|
290
|
+
out.append(Case({'a': a.detach(), 'b': b.detach(), 'grad': g},
|
|
291
|
+
{'da': da, 'db': db},
|
|
292
|
+
lambda p: ['matmul-backward', '--a-shape', '256,256', '--b-shape', '256,256',
|
|
293
|
+
'--a', p['a'], '--b', p['b'], '--grad', p['grad'],
|
|
294
|
+
'--da', p['da'], '--db', p['db']],
|
|
295
|
+
4 * 256 * 256 * 256))
|
|
296
|
+
return out
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def cases_linear_backward() -> list[Case]:
|
|
300
|
+
out = []
|
|
301
|
+
for s in SEEDS:
|
|
302
|
+
x = t((256, 256), s).requires_grad_()
|
|
303
|
+
w = t((128, 256), s + 1).requires_grad_()
|
|
304
|
+
bias = t((128,), s + 2).requires_grad_()
|
|
305
|
+
y = x @ w.T + bias
|
|
306
|
+
g = t((256, 128), s + 3)
|
|
307
|
+
dx, dw, db = torch.autograd.grad(y, (x, w, bias), g)
|
|
308
|
+
out.append(Case({'x': x.detach(), 'w': w.detach(), 'bias': bias.detach(), 'grad': g},
|
|
309
|
+
{'dx': dx, 'dw': dw, 'db': db},
|
|
310
|
+
lambda p: ['linear-backward', '--x-shape', '256,256', '--w-shape', '128,256',
|
|
311
|
+
'--b-shape', '128', '--x', p['x'], '--w', p['w'], '--bias', p['bias'],
|
|
312
|
+
'--grad', p['grad'], '--dx', p['dx'], '--dw', p['dw'], '--db', p['db']],
|
|
313
|
+
4 * 256 * 256 * 128))
|
|
314
|
+
return out
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def cases_relu_backward() -> list[Case]:
|
|
318
|
+
out = []
|
|
319
|
+
for s in SEEDS:
|
|
320
|
+
x = t((512, 512), s).requires_grad_()
|
|
321
|
+
g = t((512, 512), s + 1)
|
|
322
|
+
y = torch.relu(x)
|
|
323
|
+
dx = torch.autograd.grad(y, x, g)[0]
|
|
324
|
+
out.append(Case({'x': x.detach(), 'grad': g}, {'dx': dx},
|
|
325
|
+
lambda p: ['relu-backward', '--shape', '512,512', '--x', p['x'],
|
|
326
|
+
'--grad', p['grad'], '--dx', p['dx']],
|
|
327
|
+
x.numel()))
|
|
328
|
+
return out
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def cases_layernorm_backward() -> list[Case]:
|
|
332
|
+
out = []
|
|
333
|
+
for s in SEEDS:
|
|
334
|
+
x = t((256, 256), s).requires_grad_()
|
|
335
|
+
gamma = (torch.randn(256, generator=torch.Generator().manual_seed(s + 1)) * 0.2 + 1.0).requires_grad_()
|
|
336
|
+
g = t((256, 256), s + 2)
|
|
337
|
+
y = F.layer_norm(x, (256,), gamma, None, 1e-5)
|
|
338
|
+
dx, dgamma = torch.autograd.grad(y, (x, gamma), g)
|
|
339
|
+
out.append(Case({'x': x.detach(), 'gamma': gamma.detach(), 'grad': g},
|
|
340
|
+
{'dx': dx, 'dgamma': dgamma},
|
|
341
|
+
lambda p: ['layernorm-backward', '--shape', '256,256', '--x', p['x'],
|
|
342
|
+
'--gamma', p['gamma'], '--grad', p['grad'], '--eps', '1e-5',
|
|
343
|
+
'--dx', p['dx'], '--dgamma', p['dgamma']],
|
|
344
|
+
10 * x.numel()))
|
|
345
|
+
return out
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def cases_mse_backward() -> list[Case]:
|
|
349
|
+
out = []
|
|
350
|
+
for s in SEEDS:
|
|
351
|
+
pred = t((512, 512), s).requires_grad_()
|
|
352
|
+
target = t((512, 512), s + 1).requires_grad_()
|
|
353
|
+
loss = ((pred - target) ** 2).mean()
|
|
354
|
+
g = torch.tensor(1.0)
|
|
355
|
+
dp, dt = torch.autograd.grad(loss, (pred, target), g)
|
|
356
|
+
out.append(Case({'pred': pred.detach(), 'target': target.detach(), 'grad': g.reshape(1)},
|
|
357
|
+
{'dp': dp, 'dtarget': dt},
|
|
358
|
+
lambda p: ['mse-backward', '--pred-shape', '512,512', '--target-shape', '512,512',
|
|
359
|
+
'--pred', p['pred'], '--target', p['target'], '--grad', p['grad'],
|
|
360
|
+
'--dp', p['dp'], '--dtarget', p['dtarget']],
|
|
361
|
+
4 * pred.numel()))
|
|
362
|
+
return out
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def cases_conv2d_backward() -> list[Case]:
|
|
366
|
+
out = []
|
|
367
|
+
for s in SEEDS:
|
|
368
|
+
x = t((4, 3, 16, 16), s).requires_grad_()
|
|
369
|
+
w = t((8, 3, 3, 3), s + 1).requires_grad_()
|
|
370
|
+
b = t((8,), s + 2).requires_grad_()
|
|
371
|
+
y = F.conv2d(x, w, b, stride=1, padding=1)
|
|
372
|
+
g = t((4, 8, 16, 16), s + 3)
|
|
373
|
+
dx, dw, db = torch.autograd.grad(y, (x, w, b), g)
|
|
374
|
+
out.append(Case({'x': x.detach(), 'w': w.detach(), 'bias': b.detach(), 'grad': g},
|
|
375
|
+
{'dx': dx, 'dw': dw, 'db': db},
|
|
376
|
+
lambda p: ['conv2d-backward', '--x-shape', '4,3,16,16', '--w-shape', '8,3,3,3',
|
|
377
|
+
'--b-shape', '8', '--x', p['x'], '--w', p['w'], '--bias', p['bias'],
|
|
378
|
+
'--grad', p['grad'], '--stride', '1,1', '--padding', '1,1',
|
|
379
|
+
'--dilation', '1,1', '--groups', '1',
|
|
380
|
+
'--dx', p['dx'], '--dw', p['dw'], '--db', p['db']],
|
|
381
|
+
4 * 4 * 8 * 16 * 16 * 3 * 3 * 3))
|
|
382
|
+
return out
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def adamw_ref(p, g, m, v, lr, b1, b2, eps, wd, step):
|
|
386
|
+
m2 = b1 * m + (1 - b1) * g
|
|
387
|
+
v2 = b2 * v + (1 - b2) * g * g
|
|
388
|
+
mh = m2 / (1 - b1 ** step)
|
|
389
|
+
vh = v2 / (1 - b2 ** step)
|
|
390
|
+
p2 = p * (1 - lr * wd) - lr * mh / (torch.sqrt(vh) + eps)
|
|
391
|
+
return p2, m2, v2
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def cases_adamw() -> list[Case]:
|
|
395
|
+
out = []
|
|
396
|
+
for s, step in zip(SEEDS, (1, 3, 7)):
|
|
397
|
+
p = t((256, 256), s)
|
|
398
|
+
g = t((256, 256), s + 1)
|
|
399
|
+
m = torch.zeros_like(p)
|
|
400
|
+
v = torch.zeros_like(p)
|
|
401
|
+
ref = adamw_ref(p, g, m, v, 1e-3, 0.9, 0.999, 1e-8, 0.01, step)
|
|
402
|
+
out.append(Case({'param': p, 'grad': g, 'm': m, 'v': v},
|
|
403
|
+
{'param_out': ref[0], 'm_out': ref[1], 'v_out': ref[2]},
|
|
404
|
+
lambda pth, step=step: ['adamw', '--param', pth['param'], '--grad', pth['grad'],
|
|
405
|
+
'--m', pth['m'], '--v', pth['v'], '--lr', '0.001',
|
|
406
|
+
'--beta1', '0.9', '--beta2', '0.999', '--eps', '1e-8',
|
|
407
|
+
'--weight-decay', '0.01', '--step', str(step),
|
|
408
|
+
'--out-param', pth['param_out'], '--out-m', pth['m_out'], '--out-v', pth['v_out']],
|
|
409
|
+
10 * p.numel()))
|
|
410
|
+
return out
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def cases_sgd_momentum() -> list[Case]:
|
|
414
|
+
out = []
|
|
415
|
+
for s in SEEDS:
|
|
416
|
+
p = t((256, 256), s)
|
|
417
|
+
g = t((256, 256), s + 1)
|
|
418
|
+
buf = t((256, 256), s + 2) * 0.01
|
|
419
|
+
lr, mu = 0.05, 0.9
|
|
420
|
+
b2 = mu * buf + g
|
|
421
|
+
p2 = p - lr * b2
|
|
422
|
+
out.append(Case({'param': p, 'grad': g, 'buffer': buf}, {'param_out': p2, 'buffer_out': b2},
|
|
423
|
+
lambda pth: ['sgd-momentum', '--param', pth['param'], '--grad', pth['grad'],
|
|
424
|
+
'--momentum-buffer', pth['buffer'], '--lr', '0.05',
|
|
425
|
+
'--momentum', '0.9', '--out-param', pth['param_out'],
|
|
426
|
+
'--out-buffer', pth['buffer_out']],
|
|
427
|
+
6 * p.numel()))
|
|
428
|
+
return out
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
OPERATIONS = [
|
|
432
|
+
('elementwise.add', 1e-6, lambda: cases_elementwise('add')),
|
|
433
|
+
('elementwise.sub', 1e-6, lambda: cases_elementwise('sub')),
|
|
434
|
+
('elementwise.mul', 1e-6, lambda: cases_elementwise('mul')),
|
|
435
|
+
('elementwise.div', 1e-6, lambda: cases_elementwise('div')),
|
|
436
|
+
('matmul', 1e-5, cases_matmul),
|
|
437
|
+
('linear', 1e-5, cases_linear),
|
|
438
|
+
('relu', 1e-6, cases_relu),
|
|
439
|
+
('softmax', 1e-5, cases_softmax),
|
|
440
|
+
('layernorm', 1e-5, cases_layernorm),
|
|
441
|
+
('mse', 1e-6, cases_mse),
|
|
442
|
+
('conv2d', 1e-4, cases_conv2d),
|
|
443
|
+
('matmul-backward', 1e-4, cases_matmul_backward),
|
|
444
|
+
('linear-backward', 1e-4, cases_linear_backward),
|
|
445
|
+
('relu-backward', 1e-4, cases_relu_backward),
|
|
446
|
+
('layernorm-backward', 1e-4, cases_layernorm_backward),
|
|
447
|
+
('mse-backward', 1e-4, cases_mse_backward),
|
|
448
|
+
('conv2d-backward', 1e-4, cases_conv2d_backward),
|
|
449
|
+
('adamw', 1e-5, cases_adamw),
|
|
450
|
+
('sgd-momentum', 1e-6, cases_sgd_momentum),
|
|
451
|
+
]
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def prepare_source(workspace: Path, root: Path) -> Path:
|
|
455
|
+
"""Copy a candidate tree into an isolated directory and return its root."""
|
|
456
|
+
candidate = root / 'candidate'
|
|
457
|
+
source_root = workspace
|
|
458
|
+
|
|
459
|
+
def ignore(directory: str, names: list[str]) -> set[str]:
|
|
460
|
+
if Path(directory) == source_root:
|
|
461
|
+
return {name for name in names if name in IGNORED_COPY_NAMES}
|
|
462
|
+
return set()
|
|
463
|
+
|
|
464
|
+
shutil.copytree(source_root, candidate, symlinks=True, ignore=ignore)
|
|
465
|
+
source = candidate
|
|
466
|
+
if not (source / 'CMakeLists.txt').is_file() and (candidate / 'workspace' / 'CMakeLists.txt').is_file():
|
|
467
|
+
source = candidate / 'workspace'
|
|
468
|
+
if not (source / 'CMakeLists.txt').is_file():
|
|
469
|
+
raise RuntimeError('CMakeLists.txt missing at workspace root or workspace/')
|
|
470
|
+
return source
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def build_binaries(source: Path, configure_timeout: int = 180, build_timeout: int = 480) -> Path:
|
|
474
|
+
"""Configure and build the source tree; return its build directory."""
|
|
475
|
+
env = child_env()
|
|
476
|
+
build = source / 'build'
|
|
477
|
+
if build.exists():
|
|
478
|
+
shutil.rmtree(build, ignore_errors=True)
|
|
479
|
+
build.mkdir(parents=True)
|
|
480
|
+
cfg = subprocess.run(['cmake', '-S', '.', '-B', 'build', '-DCMAKE_BUILD_TYPE=Release'],
|
|
481
|
+
cwd=str(source), capture_output=True, text=True, encoding='utf-8',
|
|
482
|
+
errors='replace', env=env, timeout=configure_timeout)
|
|
483
|
+
if cfg.returncode != 0:
|
|
484
|
+
raise RuntimeError('cmake configure failed\n' + (cfg.stdout + cfg.stderr)[-4000:])
|
|
485
|
+
made = subprocess.run(['cmake', '--build', 'build', '-j4'], cwd=str(source), capture_output=True,
|
|
486
|
+
text=True, encoding='utf-8', errors='replace', env=env, timeout=build_timeout)
|
|
487
|
+
if made.returncode != 0:
|
|
488
|
+
raise RuntimeError('build failed\n' + (made.stdout + made.stderr)[-4000:])
|
|
489
|
+
return build
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def evaluate_ops(binary: Path, root: Path, deadline: float | None = None) -> dict:
|
|
493
|
+
"""Run every operation case and return the complete report.
|
|
494
|
+
|
|
495
|
+
Deadline is an absolute time.monotonic() value. Once it passes, the remaining
|
|
496
|
+
operations are reported as not executable with a deadline error so a hanging
|
|
497
|
+
candidate cannot stall evaluation forever.
|
|
498
|
+
"""
|
|
499
|
+
rows = []
|
|
500
|
+
for name, threshold, builder in OPERATIONS:
|
|
501
|
+
if deadline is not None and time.monotonic() > deadline:
|
|
502
|
+
rows.append({'name': name, 'executable': False, 'passed': False,
|
|
503
|
+
'weighted_mean_relative_error': None, 'threshold': threshold,
|
|
504
|
+
'speed_gflops': None, 'error': 'evaluation deadline exceeded'})
|
|
505
|
+
continue
|
|
506
|
+
try:
|
|
507
|
+
cases = builder()
|
|
508
|
+
row = run_operation(name, threshold, cases, binary, root)
|
|
509
|
+
except Exception as exc:
|
|
510
|
+
row = {'name': name, 'executable': False, 'passed': False,
|
|
511
|
+
'weighted_mean_relative_error': None, 'threshold': threshold,
|
|
512
|
+
'speed_gflops': None, 'error': str(exc)}
|
|
513
|
+
rows.append(row)
|
|
514
|
+
|
|
515
|
+
all_exec = all(row['executable'] for row in rows)
|
|
516
|
+
failed = [row['name'] for row in rows if not row['passed']]
|
|
517
|
+
errs = [row['weighted_mean_relative_error'] for row in rows
|
|
518
|
+
if row['weighted_mean_relative_error'] is not None]
|
|
519
|
+
speeds = [row['speed_gflops'] for row in rows if row['speed_gflops'] and row['speed_gflops'] > 0]
|
|
520
|
+
max_err = max(errs) if errs else None
|
|
521
|
+
geomean = math.exp(sum(math.log(v) for v in speeds) / len(speeds)) if speeds else None
|
|
522
|
+
first_error = next((row['error'] for row in rows if row['error']), '')
|
|
523
|
+
return {
|
|
524
|
+
'task': TASK,
|
|
525
|
+
'ok': True,
|
|
526
|
+
'all_operations_executable': all_exec,
|
|
527
|
+
'all_operations_passed': len(failed) == 0,
|
|
528
|
+
'failed_operations': failed,
|
|
529
|
+
'max_weighted_mean_relative_error': max_err,
|
|
530
|
+
'speed_geomean': geomean,
|
|
531
|
+
'operations': rows,
|
|
532
|
+
'error': first_error,
|
|
533
|
+
'pytorch': torch.__version__,
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def infrastructure_error(ops: dict) -> str:
|
|
538
|
+
"""A compact human-readable reason for an infra failure."""
|
|
539
|
+
not_executable = [row['name'] for row in ops.get('operations', []) if not row['executable']]
|
|
540
|
+
above = []
|
|
541
|
+
for row in ops.get('operations', []):
|
|
542
|
+
value = row.get('weighted_mean_relative_error')
|
|
543
|
+
if row['executable'] and not row['passed'] and value is not None:
|
|
544
|
+
above.append(f"{row['name']} mre={value:.3g} > {row['threshold']:g}")
|
|
545
|
+
parts = []
|
|
546
|
+
if not_executable:
|
|
547
|
+
parts.append('not executable: ' + ', '.join(not_executable))
|
|
548
|
+
if above:
|
|
549
|
+
parts.append('numeric error above threshold: ' + '; '.join(above))
|
|
550
|
+
detail = str(ops.get('error', ''))
|
|
551
|
+
if detail:
|
|
552
|
+
parts.append('first error: ' + detail[:300])
|
|
553
|
+
if not parts:
|
|
554
|
+
return 'infrastructure check failed'
|
|
555
|
+
return 'infrastructure check failed: ' + ' | '.join(parts)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def print_summary(ops: dict) -> None:
|
|
559
|
+
print(f"all_operations_executable = {str(ops.get('all_operations_executable')).lower()}")
|
|
560
|
+
print(f"all_operations_passed = {str(ops.get('all_operations_passed')).lower()}")
|
|
561
|
+
print(f"max_weighted_mean_relative_error = {ops.get('max_weighted_mean_relative_error')}")
|
|
562
|
+
print(f"speed_geomean = {ops.get('speed_geomean')}")
|
|
563
|
+
for row in ops.get('operations', []):
|
|
564
|
+
print(f"{row['name']}: executable={row['executable']} pass={row['passed']} "
|
|
565
|
+
f"mre={row['weighted_mean_relative_error']} speed_gflops={row['speed_gflops']}")
|
|
566
|
+
if row['error']:
|
|
567
|
+
print(f" error={row['error'][:300]}")
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def main() -> int:
|
|
571
|
+
parser = argparse.ArgumentParser(description='Neural-Pendulum operator oracle')
|
|
572
|
+
parser.add_argument('--workspace', required=True)
|
|
573
|
+
args = parser.parse_args()
|
|
574
|
+
workspace = Path(args.workspace).resolve()
|
|
575
|
+
with tempfile.TemporaryDirectory(prefix='neural-pendulum-ops-') as td:
|
|
576
|
+
root = Path(td)
|
|
577
|
+
try:
|
|
578
|
+
source = prepare_source(workspace, root)
|
|
579
|
+
build = build_binaries(source)
|
|
580
|
+
binary = build / 'nn_task'
|
|
581
|
+
if not binary.is_file():
|
|
582
|
+
raise RuntimeError('build/nn_task missing')
|
|
583
|
+
ops = evaluate_ops(binary, root)
|
|
584
|
+
except Exception as exc:
|
|
585
|
+
text = json.dumps({'task': TASK, 'ok': False, 'error': str(exc)}, ensure_ascii=False, indent=2)
|
|
586
|
+
target = os.environ.get('EVAL_RESULT_FILE')
|
|
587
|
+
if target:
|
|
588
|
+
Path(target).write_text(text, encoding='utf-8')
|
|
589
|
+
else:
|
|
590
|
+
print(text)
|
|
591
|
+
return 1
|
|
592
|
+
print_summary(ops)
|
|
593
|
+
text = json.dumps(ops, ensure_ascii=False, indent=2)
|
|
594
|
+
target = os.environ.get('EVAL_RESULT_FILE')
|
|
595
|
+
if target:
|
|
596
|
+
Path(target).write_text(text, encoding='utf-8')
|
|
597
|
+
else:
|
|
598
|
+
print(text)
|
|
599
|
+
return 0 if ops['all_operations_passed'] else 1
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
if __name__ == '__main__':
|
|
603
|
+
raise SystemExit(main())
|