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.
Files changed (73) hide show
  1. package/ISSUES.md +235 -0
  2. package/README.md +46 -18
  3. package/assets/neural-cifar/workspace.zip +0 -0
  4. package/assets/neural-cpp/workspace.zip +0 -0
  5. package/assets/neural-fp32/workspace.zip +0 -0
  6. package/assets/neural-pendulum/SPEC.md +41 -38
  7. package/assets/neural-pendulum/TASK.md +146 -32
  8. package/assets/neural-pendulum/sim.cpp +4 -2
  9. package/assets/neural-pendulum/sim_eval +0 -0
  10. package/assets/neural-pendulum/workspace.zip +0 -0
  11. package/eval/README.md +19 -9
  12. package/eval/__pycache__/bomberman.cpython-310.pyc +0 -0
  13. package/eval/__pycache__/commit0.cpython-310.pyc +0 -0
  14. package/eval/__pycache__/common.cpython-310.pyc +0 -0
  15. package/eval/__pycache__/ctf.cpython-310.pyc +0 -0
  16. package/eval/__pycache__/featurebench.cpython-310.pyc +0 -0
  17. package/eval/__pycache__/land.cpython-310.pyc +0 -0
  18. package/eval/__pycache__/neural-cifar.cpython-310.pyc +0 -0
  19. package/eval/__pycache__/neural-cpp.cpython-310.pyc +0 -0
  20. package/eval/__pycache__/neural-fp32.cpython-310.pyc +0 -0
  21. package/eval/__pycache__/neural-pendulum.cpython-310.pyc +0 -0
  22. package/eval/__pycache__/swe-bench-pro.cpython-310.pyc +0 -0
  23. package/eval/__pycache__/tank.cpython-310.pyc +0 -0
  24. package/eval/__pycache__/warehouse.cpython-310.pyc +0 -0
  25. package/eval/bomberman.py +5 -3
  26. package/eval/commit0.py +10 -3
  27. package/eval/common.py +169 -5
  28. package/eval/ctf.py +4 -2
  29. package/eval/featurebench.py +12 -8
  30. package/eval/land.py +4 -2
  31. package/eval/legacy/__pycache__/bomberman.cpython-310.pyc +0 -0
  32. package/eval/legacy/__pycache__/commit0.cpython-310.pyc +0 -0
  33. package/eval/legacy/__pycache__/ctf.cpython-310.pyc +0 -0
  34. package/eval/legacy/__pycache__/featurebench.cpython-310.pyc +0 -0
  35. package/eval/legacy/__pycache__/land.cpython-310.pyc +0 -0
  36. package/eval/legacy/__pycache__/swe_bench_pro.cpython-310.pyc +0 -0
  37. package/eval/legacy/__pycache__/tank.cpython-310.pyc +0 -0
  38. package/eval/legacy/__pycache__/warehouse.cpython-310.pyc +0 -0
  39. package/eval/legacy/bomberman.py +5 -2
  40. package/eval/legacy/ctf.py +5 -2
  41. package/eval/legacy/featurebench.py +55 -15
  42. package/eval/legacy/land.py +6 -3
  43. package/eval/legacy/swe_bench_pro.py +57 -16
  44. package/eval/legacy/tank.py +2 -2
  45. package/eval/legacy/warehouse.py +4 -4
  46. package/eval/neural-cpp.py +9 -6
  47. package/eval/neural-fp32.py +17 -5
  48. package/eval/swe-bench-pro.py +11 -7
  49. package/eval/tank.py +11 -2
  50. package/eval/warehouse.py +17 -1
  51. package/lib/client/index.js +73 -46
  52. package/lib/client/index.js.map +1 -1
  53. package/lib/index.js +894 -301
  54. package/native/dsh-eval-supervisor +0 -0
  55. package/native/supervisor.c +190 -26
  56. package/neural-cifar-eval/README.md +5 -0
  57. package/neural-cifar-eval/__pycache__/cifar_cache.cpython-310.pyc +0 -0
  58. package/neural-cifar-eval/__pycache__/common.cpython-310.pyc +0 -0
  59. package/neural-cifar-eval/__pycache__/fetch_cifar.cpython-310.pyc +0 -0
  60. package/neural-cifar-eval/__pycache__/ops_eval.cpython-310.pyc +0 -0
  61. package/neural-cifar-eval/__pycache__/run_eval.cpython-310.pyc +0 -0
  62. package/neural-cifar-eval/common.py +13 -0
  63. package/neural-cifar-eval/ops_eval.py +17 -5
  64. package/neural-cifar-eval/run_eval.py +42 -32
  65. package/neural-pendulum-eval/README.md +62 -0
  66. package/neural-pendulum-eval/__pycache__/ops_eval.cpython-310.pyc +0 -0
  67. package/neural-pendulum-eval/__pycache__/run_eval.cpython-310.pyc +0 -0
  68. package/neural-pendulum-eval/ops_eval.py +603 -0
  69. package/neural-pendulum-eval/run_eval.py +218 -137
  70. package/neural-pendulum-eval/sim_eval +0 -0
  71. package/package.json +7 -11
  72. package/assets/neural-pendulum/hidden/test_infra.cpp +0 -631
  73. 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())