dsh-eval-guard 0.7.0 → 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.
@@ -18,7 +18,7 @@ agent 必须:
18
18
  1. 从零实现一套神经网络基础设施(张量、算子、反向传播、优化器、模型序列化、线程池)。
19
19
  2. 使用该基础设施训练一个神经网络控制器。
20
20
  3. 控制预编译双驱动三节摆:第一、第三关节施加有限力矩,中间关节被动,从全向下起摆,并保持三根杆全部向上至少 t_tgt = 10.0 s。
21
- 4. 保证基础设施通过公开测试。
21
+ 4. 保证基础设施实现 `include/nn/nn.hpp` 契约并通过隐藏测试。
22
22
  5. 提交的源码中不得出现 torch,测试环境不提供 Python/torch。
23
23
 
24
24
  允许在开发阶段使用 PyTorch 做数值自查和参考,但提交源码必须完全独立。
@@ -179,15 +179,17 @@ cmake --build build -j4
179
179
  必须产生:
180
180
 
181
181
  ~~~
182
- build/nn_selftest
182
+ build/libnn.a
183
183
  build/control
184
184
  ~~~
185
185
 
186
186
  ### 8.1 基础设施测试入口
187
187
 
188
- build/nn_selftest 必须运行公开基础设施测试并返回 0 表示通过。
188
+ 基础设施实现位于 `src/nn/*.cpp`,由 CMake 构建静态库 `build/libnn.a`;公开头文件 `include/nn/nn.hpp` 为不可修改的 API 契约。
189
189
 
190
- 公共测试覆盖:
190
+ 评测侧独立编译隐藏测试并链接 `build/libnn.a`,不以 `build/nn_selftest` 作为通过依据;`nn_selftest` 仅供 agent 自测。
191
+
192
+ 隐藏测试覆盖:
191
193
 
192
194
  - fp32 tensor、形状与广播
193
195
  - elementwise add/sub/mul/div
@@ -8,7 +8,7 @@
8
8
 
9
9
  - 从三杆全部向下的固定初始状态起摆
10
10
  - 保持三根杆全部向上至少 t_tgt = 10.0 秒
11
- - 同时保证神经网络基础设施全部通过公开测试
11
+ - 同时保证神经网络基础设施通过固定头文件契约并通过隐藏测试
12
12
 
13
13
  允许使用 PyTorch 做开发阶段的自查和数值对照。最终提交源码中不得出现 torch,评测环境不提供 Python/torch。
14
14
 
@@ -149,16 +149,18 @@ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
149
149
  cmake --build build -j4
150
150
  ~~~
151
151
 
152
- 必须产出两个入口:
152
+ 必须产出:
153
153
 
154
154
  ~~~
155
- build/nn_selftest
155
+ build/libnn.a
156
156
  build/control
157
157
  ~~~
158
158
 
159
159
  ### 1. 基础设施入口
160
160
 
161
- build/nn_selftest 必须运行公开基础设施测试并返回 0 表示通过。
161
+ 基础设施实现放在 `src/nn/*.cpp`,由 CMake 构建为 `build/libnn.a`;公开头文件 `include/nn/nn.hpp` 是固定 API 契约,不得修改声明或语义。
162
+
163
+ 评测侧会独立编译隐藏测试并链接 `build/libnn.a`,不使用你写的 `nn_selftest` 作为验证依据。`build/nn_selftest` 可选,仅用于你自己的开发自测。
162
164
 
163
165
  必须包含的基础设施:
164
166
 
@@ -177,7 +179,7 @@ build/nn_selftest 必须运行公开基础设施测试并返回 0 表示通过
177
179
 
178
180
  占位符或空实现会被判为基础设施错误。
179
181
 
180
- 公开测试文件在工作区中,评测时使用评测侧的干净副本。
182
+ 隐藏测试由评测侧注入,使用固定种子和独立数据,不复制到工作区。
181
183
 
182
184
  ### 2. 控制器入口
183
185
 
@@ -270,7 +272,7 @@ t_can >= 10.0
270
272
  - 提交源码中不得出现 torch
271
273
  - 评测环境无 Python/torch
272
274
  - 无网络
273
- - 不得修改仿真器或公开测试
275
+ - 不得修改仿真器或公开头文件 `include/nn/nn.hpp`
274
276
  - 不得提交编译产物
275
277
  - 赛后人工审核:控制器必须是神经网络,且必须实际调用你实现的神经网络基础设施;手写控制律、查表、固定轨迹或外部依赖会被判无效
276
278
 
@@ -350,9 +352,9 @@ T_done 是第一次返回 completed 的测试请求时间。
350
352
 
351
353
  ## 建议工作顺序
352
354
 
353
- 1. 阅读公开测试和 tools/sim 协议
355
+ 1. 阅读 `include/nn/nn.hpp` 契约与 tools/sim 协议
354
356
  2. 先实现最小可用的 tensor/算子/反向/优化器
355
- 3. 跑通 nn_selftest
357
+ 3. 用自测和 `include/nn/nn.hpp` 契约检查基础设施
356
358
  4. 用 tools/sim 采集数据并训练控制器
357
359
  5. 反复提交和测试,优先把 t_can 提升到 10 秒
358
360
  6. 完成后立即总结
@@ -0,0 +1,631 @@
1
+ #include <nn/nn.hpp>
2
+
3
+ #include <algorithm>
4
+ #include <chrono>
5
+ #include <cmath>
6
+ #include <cstdint>
7
+ #include <cstring>
8
+ #include <cstdio>
9
+ #include <functional>
10
+ #include <random>
11
+ #include <string>
12
+ #include <thread>
13
+ #include <vector>
14
+
15
+ using nn::DType;
16
+ using nn::Shape;
17
+ using nn::Tensor;
18
+
19
+ static int g_passed = 0;
20
+ static int g_failed = 0;
21
+
22
+ static void report(const std::string& name, bool ok) {
23
+ if (ok) { ++g_passed; std::printf("TEST %s PASS\n", name.c_str()); }
24
+ else { ++g_failed; std::printf("TEST %s FAIL\n", name.c_str()); }
25
+ }
26
+
27
+ static bool close(float a, float b, float tol = 1e-4f) {
28
+ return std::fabs(a - b) <= tol * (1.0f + std::fabs(a) + std::fabs(b));
29
+ }
30
+
31
+ static std::vector<float> f32_data(const Tensor& t) {
32
+ Tensor c = t.contiguous();
33
+ std::vector<float> out(static_cast<size_t>(c.numel()));
34
+ if (out.empty()) return out;
35
+ std::memcpy(out.data(), c.data(), out.size() * sizeof(float));
36
+ return out;
37
+ }
38
+
39
+ static Tensor make_f32(const Shape& shape, const std::vector<float>& values) {
40
+ return Tensor::from_data(values.data(), shape, DType::F32);
41
+ }
42
+
43
+ static bool eq_data(const Tensor& t, const std::vector<float>& expected, float tol = 1e-4f) {
44
+ const std::vector<float> got = f32_data(t);
45
+ if (got.size() != expected.size()) return false;
46
+ for (size_t i = 0; i < got.size(); ++i) if (!close(got[i], expected[i], tol)) return false;
47
+ return true;
48
+ }
49
+
50
+ static bool shape_is(const Tensor& t, const Shape& shape) { return t.shape() == shape; }
51
+
52
+ static void test_dtype_metadata() {
53
+ bool ok = nn::dtype_size(DType::F32) == 4 && nn::dtype_size(DType::F64) == 8 &&
54
+ nn::dtype_size(DType::I8) == 1 && nn::dtype_size(DType::I16) == 2 &&
55
+ nn::dtype_size(DType::I32) == 4 && nn::dtype_size(DType::I64) == 8 &&
56
+ nn::dtype_size(DType::I128) == 16 && nn::dtype_size(DType::U128) == 16 &&
57
+ nn::dtype_size(DType::BF16) == 2 && nn::dtype_size(DType::FP8_E5M2) == 1 &&
58
+ nn::dtype_size(DType::MXFP4) == 1;
59
+ report("dtype_metadata", ok);
60
+ }
61
+
62
+ static void test_casts_float() {
63
+ Tensor a = make_f32({3}, {1.5f, -2.25f, 3.0f});
64
+ Tensor d = a.to(DType::F64);
65
+ Tensor back = d.to(DType::F32);
66
+ report("cast_float", d.dtype() == DType::F64 && eq_data(back, {1.5f, -2.25f, 3.0f}));
67
+ }
68
+
69
+ static void test_casts_int() {
70
+ Tensor a = make_f32({5}, {3.6f, -1.2f, 300.0f, -300.0f, 0.0f});
71
+ Tensor i = a.to(DType::I32);
72
+ Tensor narrow = a.to(DType::I8);
73
+ int32_t iv[5]{};
74
+ int8_t nv[5]{};
75
+ std::memcpy(iv, i.contiguous().data(), sizeof(iv));
76
+ std::memcpy(nv, narrow.contiguous().data(), sizeof(nv));
77
+ bool ok = iv[0] == 4 && iv[1] == -1 && iv[2] == 300 && iv[3] == -300 && iv[4] == 0;
78
+ ok = ok && nv[0] == 4 && nv[1] == -1 && nv[2] == 127 && nv[3] == -128 && nv[4] == 0;
79
+ report("cast_int_saturate", ok);
80
+ }
81
+
82
+ static void test_integer_casts() {
83
+ Tensor a = make_f32({4}, {0.0f, 1.0f, 2.0f, 3.0f});
84
+ bool ok = true;
85
+ for (DType d : {DType::I8, DType::I16, DType::I32, DType::I64, DType::I128,
86
+ DType::U8, DType::U16, DType::U32, DType::U64, DType::U128}) {
87
+ Tensor back = a.to(d).to(DType::F32);
88
+ ok = ok && eq_data(back, {0.0f, 1.0f, 2.0f, 3.0f});
89
+ }
90
+ report("integer_casts", ok);
91
+ }
92
+
93
+ static void test_low_precision_casts() {
94
+ Tensor a = make_f32({4}, {1.5f, -2.0f, 0.5f, 0.0f});
95
+ Tensor bf = a.to(DType::BF16).to(DType::F32);
96
+ Tensor fp8 = a.to(DType::FP8_E5M2).to(DType::F32);
97
+ Tensor mx = make_f32({32}, std::vector<float>(32, 1.0f)).to(DType::MXFP4).to(DType::F32);
98
+ bool ok = eq_data(bf, {1.5f, -2.0f, 0.5f, 0.0f}, 0.01f);
99
+ ok = ok && eq_data(fp8, {1.5f, -2.0f, 0.5f, 0.0f}, 0.01f);
100
+ ok = ok && eq_data(mx, std::vector<float>(32, 1.0f), 0.01f);
101
+ report("low_precision_casts", ok);
102
+ }
103
+
104
+ static void test_views() {
105
+ std::vector<float> v(24);
106
+ for (int i = 0; i < 24; ++i) v[i] = static_cast<float>(i);
107
+ Tensor a = make_f32({2, 3, 4}, v);
108
+ Tensor p = a.permute({2, 0, 1});
109
+ bool ok = shape_is(p, {4, 2, 3});
110
+ const std::vector<float> pv = f32_data(p);
111
+ ok = ok && pv.size() == 24 && close(pv[0], 0.0f) && close(pv[1], 4.0f) && close(pv[2], 8.0f);
112
+ Tensor c = a.reshape({6, 4});
113
+ ok = ok && shape_is(c, {6, 4}) && eq_data(c, v);
114
+ report("views_permute_reshape", ok);
115
+ }
116
+
117
+ static void test_slices() {
118
+ std::vector<float> v(10);
119
+ for (int i = 0; i < 10; ++i) v[i] = static_cast<float>(i);
120
+ Tensor a = make_f32({10}, v);
121
+ Tensor s = a.slice(0, 1, 8, 2);
122
+ Tensor r = a.slice(0, 8, 1, -2);
123
+ bool ok = eq_data(s, {1, 3, 5, 7}) && eq_data(r, {8, 6, 4, 2});
124
+ report("slices_negative_step", ok);
125
+ }
126
+
127
+ static void test_elementwise_broadcast() {
128
+ Tensor a = make_f32({2, 3}, {1, 2, 3, 4, 5, 6});
129
+ Tensor b = make_f32({3}, {10, 20, 30});
130
+ Tensor c = nn::add(a, b);
131
+ Tensor d = nn::mul(c, b);
132
+ bool ok = shape_is(c, {2, 3}) && eq_data(c, {11, 22, 33, 14, 25, 36});
133
+ ok = ok && eq_data(d, {110, 440, 990, 140, 500, 1080});
134
+ report("elementwise_broadcast", ok);
135
+ }
136
+
137
+ static void test_reductions() {
138
+ Tensor a = make_f32({2, 3}, {1, 2, 3, 4, 5, 6});
139
+ Tensor s = nn::sum(a, 1);
140
+ Tensor m = nn::mean(a, -1);
141
+ report("reductions", eq_data(s, {6, 15}) && close(f32_data(m)[0], 3.5f));
142
+ }
143
+
144
+ static void test_matmul_2d() {
145
+ Tensor a = make_f32({2, 3}, {1, 2, 3, 4, 5, 6});
146
+ Tensor b = make_f32({3, 2}, {7, 8, 9, 10, 11, 12});
147
+ Tensor c = nn::matmul(a, b);
148
+ report("matmul_2d", shape_is(c, {2, 2}) && eq_data(c, {58, 64, 139, 154}));
149
+ }
150
+
151
+ static void test_matmul_batch_and_nd() {
152
+ Tensor a = make_f32({2, 2, 3}, {1, 2, 3, 4, 5, 6, 1, 0, 0, 0, 1, 0});
153
+ Tensor b = make_f32({2, 3, 2}, {1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0});
154
+ Tensor c = nn::matmul(a, b);
155
+ bool ok = shape_is(c, {2, 2, 2}) && eq_data(c, {1, 2, 4, 5, 1, 0, 0, 1});
156
+ Tensor a2 = make_f32({2, 3, 4}, std::vector<float>(24, 1.0f));
157
+ Tensor b2 = make_f32({2, 4, 5}, std::vector<float>(40, 1.0f));
158
+ Tensor c2 = nn::matmul_nd(a2, b2, {0}, {0}, {1, 2}, {1, 2});
159
+ ok = ok && shape_is(c2, {2, 3, 5}) && eq_data(c2, std::vector<float>(30, 4.0f));
160
+ report("matmul_batch_nd", ok);
161
+ }
162
+
163
+ static void test_autograd_add_mul() {
164
+ Tensor a = make_f32({2}, {2.0f, 3.0f}); a.set_requires_grad(true);
165
+ Tensor b = make_f32({2}, {5.0f, 7.0f}); b.set_requires_grad(true);
166
+ Tensor c = nn::add(nn::mul(a, b), a);
167
+ nn::sum(c).backward();
168
+ bool ok = eq_data(a.grad(), {6.0f, 8.0f}) && eq_data(b.grad(), {2.0f, 3.0f});
169
+ report("autograd_add_mul", ok);
170
+ }
171
+
172
+ static void test_autograd_matmul() {
173
+ Tensor a = make_f32({2, 3}, {1, 2, 3, 4, 5, 6}); a.set_requires_grad(true);
174
+ Tensor b = make_f32({3, 2}, {7, 8, 9, 10, 11, 12}); b.set_requires_grad(true);
175
+ nn::sum(nn::matmul(a, b)).backward();
176
+ bool ok = eq_data(a.grad(), {15, 19, 23, 15, 19, 23});
177
+ ok = ok && eq_data(b.grad(), {5, 5, 7, 7, 9, 9});
178
+ report("autograd_matmul", ok);
179
+ }
180
+
181
+ static void test_autograd_view_slice() {
182
+ Tensor x = make_f32({2, 3}, {1, 2, 3, 4, 5, 6}); x.set_requires_grad(true);
183
+ Tensor s = x.slice(1, 0, 2);
184
+ nn::sum(s).backward();
185
+ report("autograd_slice", eq_data(x.grad(), {1, 1, 0, 1, 1, 0}));
186
+ }
187
+
188
+ static void test_stochastic_stats() {
189
+ const float lo = 1.0f;
190
+ const float hi = 1.0078125f; // bf16 adjacent around 1.0
191
+ const float mid = (lo + hi) * 0.5f;
192
+ std::vector<float> vals(4096, mid);
193
+ Tensor a = make_f32({4096}, vals);
194
+ Tensor q = a.to(DType::BF16, nn::Rounding::Stochastic, 12345);
195
+ Tensor f = q.to(DType::F32);
196
+ const std::vector<float> out = f32_data(f);
197
+ int upper = 0;
198
+ for (float v : out) { if (v > mid) ++upper; }
199
+ report("stochastic_stats", upper > 1500 && upper < 2600);
200
+ }
201
+
202
+ static void test_threads() {
203
+ nn::set_thread_mode(nn::ThreadMode::Development);
204
+ nn::set_num_threads(4);
205
+ int dev = nn::active_workers();
206
+ bool ok = dev >= 1 && dev <= nn::kDevMaxThreads;
207
+ nn::set_thread_mode(nn::ThreadMode::Evaluation);
208
+ nn::set_num_threads(0);
209
+ ok = ok && nn::active_workers() >= 1;
210
+ report("thread_policy", ok);
211
+ }
212
+
213
+ static void test_all_dtype_alloc() {
214
+ bool ok = true;
215
+ for (DType d : {DType::F32, DType::F64, DType::I8, DType::I16, DType::I32, DType::I64,
216
+ DType::I128, DType::U8, DType::U16, DType::U32, DType::U64, DType::U128,
217
+ DType::BF16, DType::FP8_E5M2, DType::MXFP4}) {
218
+ Tensor t = Tensor::empty(d == DType::MXFP4 ? Shape{32} : Shape{2, 3}, d);
219
+ ok = ok && t.numel() == (d == DType::MXFP4 ? 32 : 6) && t.dtype() == d;
220
+ }
221
+ report("all_dtype_alloc", ok);
222
+ }
223
+
224
+ static void bench_matmul() {
225
+ const int n = 256;
226
+ std::vector<float> av(static_cast<size_t>(n) * n), bv(static_cast<size_t>(n) * n);
227
+ for (size_t i = 0; i < av.size(); ++i) { av[i] = static_cast<float>((i * 17) % 101) * 0.01f; bv[i] = static_cast<float>((i * 29) % 97) * 0.01f; }
228
+ Tensor a = make_f32({n, n}, av);
229
+ Tensor b = make_f32({n, n}, bv);
230
+ nn::set_thread_mode(nn::ThreadMode::Evaluation);
231
+ nn::set_num_threads(0);
232
+ Tensor warm = nn::matmul(a, b);
233
+ (void)warm;
234
+ const int iters = 3;
235
+ auto start = std::chrono::steady_clock::now();
236
+ for (int i = 0; i < iters; ++i) { Tensor c = nn::matmul(a, b); (void)c; }
237
+ auto end = std::chrono::steady_clock::now();
238
+ double ms = std::chrono::duration<double, std::milli>(end - start).count() / iters;
239
+ double gflops = (2.0 * n * n * n) / (ms * 1e6);
240
+ std::printf("PERF_MS %.6f\nPERF_GFLOPS %.6f\n", ms, gflops);
241
+ }
242
+
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Metric section. Reference values are computed by local scalar reference code
246
+ // BEFORE calling the candidate implementation. Candidate timing only wraps
247
+ // candidate calls; reference loops are never inside the timed region.
248
+ // ---------------------------------------------------------------------------
249
+
250
+ static std::vector<float> ref_matmul_f32(const std::vector<float>& a, const std::vector<float>& b,
251
+ int M, int K, int N) {
252
+ std::vector<float> c(static_cast<size_t>(M) * N, 0.0f);
253
+ for (int i = 0; i < M; ++i)
254
+ for (int k = 0; k < K; ++k)
255
+ for (int j = 0; j < N; ++j)
256
+ c[static_cast<size_t>(i) * N + j] += a[static_cast<size_t>(i) * K + k] * b[static_cast<size_t>(k) * N + j];
257
+ return c;
258
+ }
259
+
260
+ static const float kMxTable[8] = {0.0f, 0.5f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 6.0f};
261
+
262
+ static float mx_quant_one(float x, float scale, std::mt19937_64* rng) {
263
+ const float q = x / scale;
264
+ const float aq = std::fabs(q);
265
+ int code = 0;
266
+ if (aq >= kMxTable[7]) {
267
+ code = 7;
268
+ } else {
269
+ int lo = 0, hi = 1;
270
+ for (int c = 0; c < 7; ++c) if (kMxTable[c] <= aq && aq <= kMxTable[c + 1]) { lo = c; hi = c + 1; break; }
271
+ const float t = (aq - kMxTable[lo]) / (kMxTable[hi] - kMxTable[lo]);
272
+ if (rng != nullptr && t > 0.0f && t < 1.0f) {
273
+ std::uniform_real_distribution<double> d(0.0, 1.0);
274
+ code = d(*rng) < t ? hi : lo;
275
+ } else {
276
+ code = t < 0.5f ? lo : hi;
277
+ }
278
+ }
279
+ const float v = kMxTable[code] * scale;
280
+ return q < 0 ? -v : v;
281
+ }
282
+
283
+ static std::vector<float> mx_quantize(const std::vector<float>& x, std::mt19937_64* rng) {
284
+ std::vector<float> out(x.size());
285
+ for (size_t base = 0; base < x.size(); base += 32) {
286
+ float mx = 0.0f;
287
+ for (size_t j = base; j < std::min(x.size(), base + 32); ++j) mx = std::max(mx, std::fabs(x[j]));
288
+ int e = 127;
289
+ if (mx > 0) e = static_cast<int>(std::floor(std::log2(mx / 4.0f))) + 127;
290
+ e = std::max(0, std::min(254, e));
291
+ const float scale = std::pow(2.0f, e - 127);
292
+ for (size_t j = base; j < std::min(x.size(), base + 32); ++j) out[j] = mx_quant_one(x[j], scale, rng);
293
+ }
294
+ return out;
295
+ }
296
+
297
+ static std::vector<float> ref_matmul_mx(const std::vector<float>& a, const std::vector<float>& b,
298
+ int M, int K, int N, std::mt19937_64* rng) {
299
+ std::vector<float> aq = mx_quantize(a, rng), bq = mx_quantize(b, rng);
300
+ std::vector<float> c = ref_matmul_f32(aq, bq, M, K, N);
301
+ return mx_quantize(c, rng);
302
+ }
303
+
304
+ static std::vector<__int128> ref_matmul_i128(const std::vector<__int128>& a,
305
+ const std::vector<__int128>& b, int M, int K, int N) {
306
+ std::vector<__int128> c(static_cast<size_t>(M) * N, 0);
307
+ for (int i = 0; i < M; ++i)
308
+ for (int k = 0; k < K; ++k)
309
+ for (int j = 0; j < N; ++j)
310
+ c[static_cast<size_t>(i) * N + j] += a[static_cast<size_t>(i) * K + k] * b[static_cast<size_t>(k) * N + j];
311
+ return c;
312
+ }
313
+
314
+ static double mean_abs_error_f32(const std::vector<float>& got, const std::vector<float>& ref) {
315
+ double s = 0.0;
316
+ for (size_t i = 0; i < got.size(); ++i) s += std::fabs(static_cast<double>(got[i]) - ref[i]);
317
+ return got.empty() ? 0.0 : s / got.size();
318
+ }
319
+
320
+ static double mean_abs_error_i128(const std::vector<__int128>& got, const std::vector<__int128>& ref) {
321
+ double s = 0.0;
322
+ for (size_t i = 0; i < got.size(); ++i) {
323
+ __int128 d = got[i] - ref[i];
324
+ if (d < 0) d = -d;
325
+ s += static_cast<double>(d);
326
+ }
327
+ return got.empty() ? 0.0 : s / got.size();
328
+ }
329
+
330
+ static std::vector<__int128> read_i128(const Tensor& t) {
331
+ Tensor c = t.contiguous();
332
+ std::vector<__int128> out(static_cast<size_t>(c.numel()), 0);
333
+ if (c.dtype() != DType::I128) return out;
334
+ std::memcpy(out.data(), c.data(), out.size() * sizeof(__int128));
335
+ return out;
336
+ }
337
+
338
+ static void emit_metric(const char* dtype, int stochastic, int M, int K, int N,
339
+ double fwd_acc, double fwd_err, double fwd_ms,
340
+ double bwd_acc, double bwd_err, double bwd_ms) {
341
+ const double ops = 2.0 * M * K * N;
342
+ const double fwd_gflops = fwd_ms > 0 ? ops / (fwd_ms * 1e6) : -1.0;
343
+ const double bwd_gflops = bwd_ms > 0 ? ops / (bwd_ms * 1e6) : -1.0;
344
+ std::printf("METRIC dtype=%s stochastic=%d m=%d k=%d n=%d fwd_accuracy=%.6f fwd_mean_err=%.9g fwd_ms=%.6f fwd_gflops=%.6f bwd_accuracy=%.6f bwd_mean_err=%.9g bwd_ms=%.6f bwd_gflops=%.6f\n",
345
+ dtype, stochastic, M, K, N, fwd_acc, fwd_err, fwd_ms, fwd_gflops,
346
+ bwd_acc, bwd_err, bwd_ms, bwd_gflops);
347
+ }
348
+
349
+ static double time_once_ms(const std::function<void()>& fn, int iters = 3) {
350
+ // warmup
351
+ fn();
352
+ auto start = std::chrono::steady_clock::now();
353
+ for (int i = 0; i < iters; ++i) fn();
354
+ auto end = std::chrono::steady_clock::now();
355
+ return std::chrono::duration<double, std::milli>(end - start).count() / iters;
356
+ }
357
+
358
+ static void fill_rand(std::vector<float>& v, uint64_t seed, float lo, float hi) {
359
+ std::mt19937_64 rng(seed);
360
+ std::uniform_real_distribution<float> d(lo, hi);
361
+ for (float& x : v) x = d(rng);
362
+ }
363
+
364
+ struct StatScore {
365
+ double accuracy = 0.0;
366
+ double mean_err = 0.0;
367
+ double std_ratio = 0.0;
368
+ double ref_mean_avg = 0.0;
369
+ double ref_std_avg = 0.0;
370
+ double cand_mean_avg = 0.0;
371
+ double cand_std_avg = 0.0;
372
+ };
373
+
374
+ static void add_stats(std::vector<double>& sum, std::vector<double>& sq, const std::vector<float>& v) {
375
+ for (size_t i = 0; i < v.size(); ++i) {
376
+ sum[i] += v[i];
377
+ sq[i] += static_cast<double>(v[i]) * v[i];
378
+ }
379
+ }
380
+
381
+ static StatScore score_stats(const std::vector<double>& ref_sum, const std::vector<double>& ref_sq,
382
+ const std::vector<double>& cand_sum, const std::vector<double>& cand_sq,
383
+ int samples) {
384
+ StatScore out;
385
+ const double n = std::max(1, samples);
386
+ int ok = 0;
387
+ for (size_t i = 0; i < ref_sum.size(); ++i) {
388
+ const double rm = ref_sum[i] / n;
389
+ const double rv = std::max(0.0, ref_sq[i] / n - rm * rm);
390
+ const double rs = std::sqrt(rv);
391
+ const double cm = cand_sum[i] / n;
392
+ const double cv = std::max(0.0, cand_sq[i] / n - cm * cm);
393
+ const double cs = std::sqrt(cv);
394
+ bool good = false;
395
+ if (rs <= 1e-12) good = std::fabs(cm - rm) <= 1e-6 && cs <= 1e-6;
396
+ else good = std::fabs(cm - rm) <= 3.0 * rs && cs >= 0.1 * rs && cs <= 10.0 * rs;
397
+ if (good) ++ok;
398
+ out.mean_err += std::fabs(cm - rm);
399
+ out.std_ratio += (rs > 1e-12) ? cs / rs : 1.0;
400
+ out.ref_mean_avg += rm;
401
+ out.ref_std_avg += rs;
402
+ out.cand_mean_avg += cm;
403
+ out.cand_std_avg += cs;
404
+ }
405
+ const double count = static_cast<double>(ref_sum.size());
406
+ out.accuracy = 100.0 * ok / count;
407
+ out.mean_err /= count;
408
+ out.std_ratio /= count;
409
+ out.ref_mean_avg /= count;
410
+ out.ref_std_avg /= count;
411
+ out.cand_mean_avg /= count;
412
+ out.cand_std_avg /= count;
413
+ return out;
414
+ }
415
+
416
+ static void metric_f32() {
417
+ const int M = 256, K = 256, N = 256;
418
+ const int DATA = 4;
419
+ double acc_f = 0.0, err_f = 0.0, ms_f = 0.0, acc_b = 0.0, err_b = 0.0, ms_b = 0.0;
420
+ for (int d = 0; d < DATA; ++d) {
421
+ std::vector<float> a(static_cast<size_t>(M) * K), b(static_cast<size_t>(K) * N);
422
+ fill_rand(a, 1000 + d, -1.0f, 1.0f);
423
+ fill_rand(b, 2000 + d, -1.0f, 1.0f);
424
+ const std::vector<float> ref_c = ref_matmul_f32(a, b, M, K, N);
425
+ std::vector<float> ref_da(static_cast<size_t>(M) * K), ref_db(static_cast<size_t>(K) * N);
426
+ for (int i = 0; i < M; ++i) for (int k = 0; k < K; ++k) { float s = 0; for (int j = 0; j < N; ++j) s += b[k * N + j]; ref_da[i * K + k] = s; }
427
+ for (int k = 0; k < K; ++k) for (int j = 0; j < N; ++j) { float s = 0; for (int i = 0; i < M; ++i) s += a[i * K + k]; ref_db[k * N + j] = s; }
428
+
429
+ Tensor at = make_f32({M, K}, a), bt = make_f32({K, N}, b);
430
+ const std::vector<float> got = f32_data(nn::matmul(at, bt));
431
+ double err = mean_abs_error_f32(got, ref_c);
432
+ int ok = 0; for (size_t i = 0; i < got.size(); ++i) if (std::fabs(got[i] - ref_c[i]) <= 1e-3f + 1e-4f * std::fabs(ref_c[i])) ++ok;
433
+ acc_f += 100.0 * ok / got.size(); err_f += err;
434
+ ms_f += time_once_ms([&] { Tensor c = nn::matmul(at, bt); (void)c; });
435
+
436
+ Tensor ag = make_f32({M, K}, a), bg = make_f32({K, N}, b);
437
+ ag.set_requires_grad(true); bg.set_requires_grad(true);
438
+ ms_b += time_once_ms([&] {
439
+ ag = make_f32({M, K}, a); bg = make_f32({K, N}, b);
440
+ ag.set_requires_grad(true); bg.set_requires_grad(true);
441
+ Tensor c = nn::matmul(ag, bg);
442
+ nn::sum(c).backward();
443
+ });
444
+ const std::vector<float> got_da = f32_data(ag.grad()), got_db = f32_data(bg.grad());
445
+ double err_da = mean_abs_error_f32(got_da, ref_da), err_db = mean_abs_error_f32(got_db, ref_db);
446
+ int ok_da = 0, ok_db = 0;
447
+ for (size_t i = 0; i < got_da.size(); ++i) if (std::fabs(got_da[i] - ref_da[i]) <= 1e-3f) ++ok_da;
448
+ for (size_t i = 0; i < got_db.size(); ++i) if (std::fabs(got_db[i] - ref_db[i]) <= 1e-3f) ++ok_db;
449
+ acc_b += 100.0 * (ok_da + ok_db) / (got_da.size() + got_db.size());
450
+ err_b += (err_da + err_db) / 2.0;
451
+ }
452
+ emit_metric("f32", 0, M, K, N, acc_f / DATA, err_f / DATA, ms_f / DATA, acc_b / DATA, err_b / DATA, ms_b / DATA);
453
+ }
454
+
455
+ static void metric_i128() {
456
+ const int M = 256, K = 256, N = 256;
457
+ const int DATA = 4;
458
+ double acc_f = 0.0, err_f = 0.0, ms_f = 0.0, acc_b = 0.0, err_b = 0.0, ms_b = 0.0;
459
+ for (int d = 0; d < DATA; ++d) {
460
+ std::vector<__int128> a(static_cast<size_t>(M) * K), b(static_cast<size_t>(K) * N);
461
+ for (size_t i = 0; i < a.size(); ++i) a[i] = static_cast<int>((i * 17 + d * 13) % 9) - 4;
462
+ for (size_t i = 0; i < b.size(); ++i) b[i] = static_cast<int>((i * 29 + d * 7) % 9) - 4;
463
+ const std::vector<__int128> ref_c = ref_matmul_i128(a, b, M, K, N);
464
+ std::vector<__int128> ref_da(static_cast<size_t>(M) * K), ref_db(static_cast<size_t>(K) * N);
465
+ for (int i = 0; i < M; ++i) for (int k = 0; k < K; ++k) { __int128 s = 0; for (int j = 0; j < N; ++j) s += b[k * N + j]; ref_da[i * K + k] = s; }
466
+ for (int k = 0; k < K; ++k) for (int j = 0; j < N; ++j) { __int128 s = 0; for (int i = 0; i < M; ++i) s += a[i * K + k]; ref_db[k * N + j] = s; }
467
+
468
+ Tensor at = Tensor::from_data(a.data(), {M, K}, DType::I128);
469
+ Tensor bt = Tensor::from_data(b.data(), {K, N}, DType::I128);
470
+ const std::vector<__int128> got = read_i128(nn::matmul(at, bt));
471
+ double err = mean_abs_error_i128(got, ref_c);
472
+ int ok = 0; for (size_t i = 0; i < got.size(); ++i) if (got[i] == ref_c[i]) ++ok;
473
+ acc_f += 100.0 * ok / got.size(); err_f += err;
474
+ ms_f += time_once_ms([&] { Tensor c = nn::matmul(at, bt); (void)c; });
475
+
476
+ Tensor ag = Tensor::from_data(a.data(), {M, K}, DType::I128);
477
+ Tensor bg = Tensor::from_data(b.data(), {K, N}, DType::I128);
478
+ ag.set_requires_grad(true); bg.set_requires_grad(true);
479
+ ms_b += time_once_ms([&] {
480
+ ag = Tensor::from_data(a.data(), {M, K}, DType::I128);
481
+ bg = Tensor::from_data(b.data(), {K, N}, DType::I128);
482
+ ag.set_requires_grad(true); bg.set_requires_grad(true);
483
+ Tensor c = nn::matmul(ag, bg);
484
+ nn::sum(c).backward();
485
+ });
486
+ const std::vector<__int128> got_da = read_i128(ag.grad()), got_db = read_i128(bg.grad());
487
+ double err_da = mean_abs_error_i128(got_da, ref_da), err_db = mean_abs_error_i128(got_db, ref_db);
488
+ int ok_da = 0, ok_db = 0;
489
+ for (size_t i = 0; i < got_da.size(); ++i) if (got_da[i] == ref_da[i]) ++ok_da;
490
+ for (size_t i = 0; i < got_db.size(); ++i) if (got_db[i] == ref_db[i]) ++ok_db;
491
+ acc_b += 100.0 * (ok_da + ok_db) / (got_da.size() + got_db.size());
492
+ err_b += (err_da + err_db) / 2.0;
493
+ }
494
+ emit_metric("i128", 0, M, K, N, acc_f / DATA, err_f / DATA, ms_f / DATA, acc_b / DATA, err_b / DATA, ms_b / DATA);
495
+ }
496
+
497
+ static void metric_mx() {
498
+ const int M = 256, K = 256, N = 256;
499
+ const int DATA = 4;
500
+ double acc_f = 0.0, err_f = 0.0, ms_f = 0.0, acc_b = 0.0, err_b = 0.0, ms_b = 0.0;
501
+ for (int d = 0; d < DATA; ++d) {
502
+ std::vector<float> a(static_cast<size_t>(M) * K), b(static_cast<size_t>(K) * N);
503
+ fill_rand(a, 3000 + d, -2.0f, 2.0f);
504
+ fill_rand(b, 4000 + d, -2.0f, 2.0f);
505
+ std::vector<float> aq = mx_quantize(a, nullptr), bq = mx_quantize(b, nullptr);
506
+ const std::vector<float> ref_c = mx_quantize(ref_matmul_f32(aq, bq, M, K, N), nullptr);
507
+ std::vector<float> ref_da(static_cast<size_t>(M) * K), ref_db(static_cast<size_t>(K) * N);
508
+ for (int i = 0; i < M; ++i) for (int k = 0; k < K; ++k) { float s = 0; for (int j = 0; j < N; ++j) s += bq[k * N + j]; ref_da[i * K + k] = s; }
509
+ for (int k = 0; k < K; ++k) for (int j = 0; j < N; ++j) { float s = 0; for (int i = 0; i < M; ++i) s += aq[i * K + k]; ref_db[k * N + j] = s; }
510
+ ref_da = mx_quantize(ref_da, nullptr); ref_db = mx_quantize(ref_db, nullptr);
511
+
512
+ Tensor at = make_f32({M, K}, a).to(DType::MXFP4);
513
+ Tensor bt = make_f32({K, N}, b).to(DType::MXFP4);
514
+ const std::vector<float> got = f32_data(nn::matmul(at, bt).to(DType::F32));
515
+ double err = mean_abs_error_f32(got, ref_c);
516
+ int ok = 0; for (size_t i = 0; i < got.size(); ++i) if (std::fabs(got[i] - ref_c[i]) <= 0.75f + 0.1f * std::fabs(ref_c[i])) ++ok;
517
+ acc_f += 100.0 * ok / got.size(); err_f += err;
518
+ ms_f += time_once_ms([&] { Tensor c = nn::matmul(at, bt); (void)c; });
519
+
520
+ Tensor ag = make_f32({M, K}, a).to(DType::MXFP4), bg = make_f32({K, N}, b).to(DType::MXFP4);
521
+ ag.set_requires_grad(true); bg.set_requires_grad(true);
522
+ ms_b += time_once_ms([&] {
523
+ ag = make_f32({M, K}, a).to(DType::MXFP4); bg = make_f32({K, N}, b).to(DType::MXFP4);
524
+ ag.set_requires_grad(true); bg.set_requires_grad(true);
525
+ Tensor c = nn::matmul(ag, bg);
526
+ nn::sum(c).backward();
527
+ });
528
+ const std::vector<float> got_da = f32_data(ag.grad().to(DType::F32)), got_db = f32_data(bg.grad().to(DType::F32));
529
+ double err_da = mean_abs_error_f32(got_da, ref_da), err_db = mean_abs_error_f32(got_db, ref_db);
530
+ int ok_da = 0, ok_db = 0;
531
+ for (size_t i = 0; i < got_da.size(); ++i) if (std::fabs(got_da[i] - ref_da[i]) <= 0.75f + 0.1f * std::fabs(ref_da[i])) ++ok_da;
532
+ for (size_t i = 0; i < got_db.size(); ++i) if (std::fabs(got_db[i] - ref_db[i]) <= 0.75f + 0.1f * std::fabs(ref_db[i])) ++ok_db;
533
+ acc_b += 100.0 * (ok_da + ok_db) / (got_da.size() + got_db.size());
534
+ err_b += (err_da + err_db) / 2.0;
535
+ }
536
+ emit_metric("mxfp4", 0, M, K, N, acc_f / DATA, err_f / DATA, ms_f / DATA, acc_b / DATA, err_b / DATA, ms_b / DATA);
537
+
538
+ const int STOCH_DATA = 2;
539
+ const int samples = 1024;
540
+ double s_acc_f = 0.0, s_err_f = 0.0, s_ms_f = 0.0, s_acc_b = 0.0, s_err_b = 0.0, s_ms_b = 0.0;
541
+ double s_ref_mean = 0.0, s_ref_std = 0.0, s_cand_mean = 0.0, s_cand_std = 0.0;
542
+ for (int d = 0; d < STOCH_DATA; ++d) {
543
+ std::vector<float> a(static_cast<size_t>(M) * K), b(static_cast<size_t>(K) * N);
544
+ fill_rand(a, 5000 + d, -2.0f, 2.0f);
545
+ fill_rand(b, 6000 + d, -2.0f, 2.0f);
546
+ const size_t out_n = static_cast<size_t>(M) * N;
547
+ std::vector<double> ref_sum(out_n, 0.0), ref_sq(out_n, 0.0), cand_sum(out_n, 0.0), cand_sq(out_n, 0.0);
548
+ std::vector<double> ref_da_sum(static_cast<size_t>(M) * K, 0.0), ref_da_sq(ref_da_sum.size(), 0.0);
549
+ std::vector<double> ref_db_sum(static_cast<size_t>(K) * N, 0.0), ref_db_sq(ref_db_sum.size(), 0.0);
550
+ std::vector<double> cand_da_sum(ref_da_sum.size(), 0.0), cand_da_sq(ref_da_sum.size(), 0.0);
551
+ std::vector<double> cand_db_sum(ref_db_sum.size(), 0.0), cand_db_sq(ref_db_sum.size(), 0.0);
552
+ std::mt19937_64 ref_rng(0xabcdefULL + d);
553
+ for (int t = 0; t < samples; ++t) {
554
+ const std::vector<float> raq = mx_quantize(a, &ref_rng), rbq = mx_quantize(b, &ref_rng);
555
+ const std::vector<float> rc = mx_quantize(ref_matmul_f32(raq, rbq, M, K, N), &ref_rng);
556
+ add_stats(ref_sum, ref_sq, rc);
557
+ std::vector<float> rda(static_cast<size_t>(M) * K), rdb(static_cast<size_t>(K) * N);
558
+ for (int i = 0; i < M; ++i) for (int k = 0; k < K; ++k) { float s = 0; for (int j = 0; j < N; ++j) s += rbq[k * N + j]; rda[i * K + k] = s; }
559
+ for (int k = 0; k < K; ++k) for (int j = 0; j < N; ++j) { float s = 0; for (int i = 0; i < M; ++i) s += raq[i * K + k]; rdb[k * N + j] = s; }
560
+ rda = mx_quantize(rda, nullptr); rdb = mx_quantize(rdb, nullptr);
561
+ add_stats(ref_da_sum, ref_da_sq, rda); add_stats(ref_db_sum, ref_db_sq, rdb);
562
+
563
+ const uint64_t seed = 100000 + static_cast<uint64_t>(d) * samples + t;
564
+ Tensor qa = make_f32({M, K}, a).to(DType::MXFP4, nn::Rounding::Stochastic, seed);
565
+ Tensor qb = make_f32({K, N}, b).to(DType::MXFP4, nn::Rounding::Stochastic, seed + 7919);
566
+ Tensor c = nn::matmul(qa, qb);
567
+ add_stats(cand_sum, cand_sq, f32_data(c.to(DType::F32)));
568
+ qa.set_requires_grad(true); qb.set_requires_grad(true);
569
+ Tensor cg = nn::matmul(qa, qb);
570
+ nn::sum(cg).backward();
571
+ add_stats(cand_da_sum, cand_da_sq, f32_data(qa.grad().to(DType::F32)));
572
+ add_stats(cand_db_sum, cand_db_sq, f32_data(qb.grad().to(DType::F32)));
573
+ }
574
+ StatScore f = score_stats(ref_sum, ref_sq, cand_sum, cand_sq, samples);
575
+ StatScore bscore = score_stats(ref_da_sum, ref_da_sq, cand_da_sum, cand_da_sq, samples);
576
+ StatScore bs2 = score_stats(ref_db_sum, ref_db_sq, cand_db_sum, cand_db_sq, samples);
577
+ s_acc_f += f.accuracy; s_err_f += f.mean_err;
578
+ s_acc_b += (bscore.accuracy + bs2.accuracy) / 2.0;
579
+ s_err_b += (bscore.mean_err + bs2.mean_err) / 2.0;
580
+ s_ref_mean += f.ref_mean_avg; s_ref_std += f.ref_std_avg;
581
+ s_cand_mean += f.cand_mean_avg; s_cand_std += f.cand_std_avg;
582
+ s_ms_f += time_once_ms([&] {
583
+ Tensor qa = make_f32({M, K}, a).to(DType::MXFP4, nn::Rounding::Stochastic, 7);
584
+ Tensor qb = make_f32({K, N}, b).to(DType::MXFP4, nn::Rounding::Stochastic, 11);
585
+ Tensor c = nn::matmul(qa, qb); (void)c;
586
+ }, 1);
587
+ s_ms_b += time_once_ms([&] {
588
+ Tensor qa = make_f32({M, K}, a).to(DType::MXFP4, nn::Rounding::Stochastic, 7);
589
+ Tensor qb = make_f32({K, N}, b).to(DType::MXFP4, nn::Rounding::Stochastic, 11);
590
+ qa.set_requires_grad(true); qb.set_requires_grad(true);
591
+ Tensor c = nn::matmul(qa, qb);
592
+ nn::sum(c).backward();
593
+ }, 1);
594
+ std::printf("STAT data=%d samples=%d fwd_acc=%.6f fwd_mean_err=%.9g fwd_std_ratio=%.9g bwd_acc=%.6f bwd_mean_err=%.9g bwd_std_ratio=%.9g ref_mean=%.9g ref_std=%.9g cand_mean=%.9g cand_std=%.9g\n",
595
+ d, samples, f.accuracy, f.mean_err, f.std_ratio, (bscore.accuracy + bs2.accuracy) / 2.0,
596
+ (bscore.mean_err + bs2.mean_err) / 2.0, (bscore.std_ratio + bs2.std_ratio) / 2.0,
597
+ f.ref_mean_avg, f.ref_std_avg, f.cand_mean_avg, f.cand_std_avg);
598
+ }
599
+ emit_metric("mxfp4", 1, M, K, N, s_acc_f / STOCH_DATA, s_err_f / STOCH_DATA, s_ms_f / STOCH_DATA,
600
+ s_acc_b / STOCH_DATA, s_err_b / STOCH_DATA, s_ms_b / STOCH_DATA);
601
+ std::printf("STAT dtype=mxfp4 stochastic=1 data=%d samples=%d ref_mean_avg=%.9g ref_std_avg=%.9g cand_mean_avg=%.9g cand_std_avg=%.9g\n",
602
+ STOCH_DATA, samples, s_ref_mean / STOCH_DATA, s_ref_std / STOCH_DATA,
603
+ s_cand_mean / STOCH_DATA, s_cand_std / STOCH_DATA);
604
+ }
605
+
606
+
607
+ int main() {
608
+ test_dtype_metadata();
609
+ test_casts_float();
610
+ test_casts_int();
611
+ test_integer_casts();
612
+ test_low_precision_casts();
613
+ test_views();
614
+ test_slices();
615
+ test_elementwise_broadcast();
616
+ test_reductions();
617
+ test_matmul_2d();
618
+ test_matmul_batch_and_nd();
619
+ test_autograd_add_mul();
620
+ test_autograd_matmul();
621
+ test_autograd_view_slice();
622
+ test_stochastic_stats();
623
+ test_threads();
624
+ test_all_dtype_alloc();
625
+ bench_matmul();
626
+ try { metric_f32(); } catch (const std::exception& e) { std::printf("METRIC dtype=f32 stochastic=0 error=%s\n", e.what()); }
627
+ try { metric_i128(); } catch (const std::exception& e) { std::printf("METRIC dtype=i128 stochastic=0 error=%s\n", e.what()); }
628
+ try { metric_mx(); } catch (const std::exception& e) { std::printf("METRIC dtype=mxfp4 stochastic=0 error=%s\n", e.what()); }
629
+ std::printf("SUMMARY %d %d\n", g_passed, g_failed);
630
+ return g_failed == 0 ? 0 : 1;
631
+ }
@@ -0,0 +1,88 @@
1
+ // Neural-CPP public interface. Implement everything declared here.
2
+ #pragma once
3
+
4
+ #include <cstddef>
5
+ #include <cstdint>
6
+ #include <memory>
7
+ #include <string>
8
+ #include <vector>
9
+
10
+ namespace nn {
11
+
12
+ using Shape = std::vector<int64_t>;
13
+
14
+ enum class DType : uint8_t {
15
+ F32, F64,
16
+ I8, I16, I32, I64, I128,
17
+ U8, U16, U32, U64, U128,
18
+ BF16, FP8_E5M2, MXFP4
19
+ };
20
+
21
+ enum class Rounding : uint8_t { NearestEven, TowardZero, Stochastic };
22
+ enum class ThreadMode : uint8_t { Development, Evaluation };
23
+
24
+ constexpr int kDevMaxThreads = 4;
25
+
26
+ std::size_t dtype_size(DType dtype);
27
+ const char* dtype_name(DType dtype);
28
+
29
+ class Tensor {
30
+ public:
31
+ Tensor();
32
+ Tensor(const Tensor&) = default;
33
+ Tensor(Tensor&&) noexcept = default;
34
+ Tensor& operator=(const Tensor&) = default;
35
+ Tensor& operator=(Tensor&&) noexcept = default;
36
+ ~Tensor();
37
+
38
+ static Tensor empty(const Shape& shape, DType dtype);
39
+ static Tensor from_data(const void* data, const Shape& shape, DType dtype);
40
+
41
+ const Shape& shape() const;
42
+ DType dtype() const;
43
+ int64_t ndim() const;
44
+ int64_t numel() const;
45
+ bool is_contiguous() const;
46
+
47
+ const void* data() const;
48
+ void* data();
49
+
50
+ Tensor to(DType dst, Rounding mode = Rounding::NearestEven, uint64_t seed = 0) const;
51
+ Tensor view(const Shape& shape) const;
52
+ Tensor reshape(const Shape& shape) const;
53
+ Tensor permute(const std::vector<int>& dims) const;
54
+ Tensor slice(int dim, int64_t start, int64_t stop, int64_t step = 1) const;
55
+ Tensor contiguous() const;
56
+ Tensor clone() const;
57
+
58
+ void set_requires_grad(bool value);
59
+ bool requires_grad() const;
60
+ Tensor grad() const;
61
+ void backward();
62
+
63
+ // Implementation detail. Exposed so the library implementer can define
64
+ // free functions without the header becoming a barrier.
65
+ struct Impl;
66
+ std::shared_ptr<Impl> impl_;
67
+ };
68
+
69
+ Tensor add(const Tensor& a, const Tensor& b);
70
+ Tensor sub(const Tensor& a, const Tensor& b);
71
+ Tensor mul(const Tensor& a, const Tensor& b);
72
+ Tensor div(const Tensor& a, const Tensor& b);
73
+ Tensor sum(const Tensor& a, int dim = -1);
74
+ Tensor mean(const Tensor& a, int dim = -1);
75
+
76
+ Tensor matmul(const Tensor& a, const Tensor& b);
77
+ Tensor matmul_nd(const Tensor& a, const Tensor& b,
78
+ const std::vector<int>& a_batch_dims,
79
+ const std::vector<int>& b_batch_dims,
80
+ const std::vector<int>& a_matrix_dims,
81
+ const std::vector<int>& b_matrix_dims);
82
+
83
+ void set_thread_mode(ThreadMode mode);
84
+ void set_num_threads(int n);
85
+ int num_threads();
86
+ int active_workers();
87
+
88
+ } // namespace nn
package/lib/index.js CHANGED
@@ -2123,16 +2123,16 @@ const NEURAL_PENDULUM_DEFINITION = {
2123
2123
  return [
2124
2124
  "你是一名控制与神经网络工程师。任务:从零实现纯 C++17、仅 CPU、fp32 的神经网络基础设施,并用它训练一个控制器,控制预编译的三节摆从接近全向下起摆,使三根杆全部进入竖直向上容差并尽可能长时间保持。",
2125
2125
  "",
2126
- "请先完整阅读 TASK.md;它是权威规则。硬约束:仅 C++17 标准库、仅 CPU、fp32;不使用 Python/OpenCL/CUDA/BLAS/Eigen/第三方库;必须从提交源码构建出 build/control 与 build/nn_selftest。",
2126
+ "请先完整阅读 TASK.md;它是权威规则。硬约束:仅 C++17 标准库、仅 CPU、fp32;不使用 Python/OpenCL/CUDA/BLAS/Eigen/第三方库;必须从提交源码构建出 build/control 与 build/libnn.a;基础设施需实现工作区公开头文件 include/nn/nn.hpp 的契约,隐藏测试由评测侧独立编译链接。",
2127
2127
  "",
2128
2128
  "环境细节:joint1 与 joint3 驱动,joint2 被动;控制频率 100 Hz;每回合最多 3000 步(30 s),达到上限后继续 step 返回 {\"error\":\"episode_finished\"},必须 reset。环境只返回三个关节角度(弧度,不除 pi),不返回 reward/loss/cost/done;环境不提供杆长、质量、惯量、角速度或 tip。训练 reset 带小范围随机初始角度和角速度;评测使用固定初始状态,无随机扰动。",
2129
2129
  "",
2130
2130
  "优化目标:从接近全向下起摆,使三根杆全部进入 0.05 rad 的竖直向上容差,并尽可能长时间保持;评测指标为 t_can。损失函数、奖励、优化器、网络结构和训练方法由你自行设计,不规定任何训练方案。",
2131
2131
  "",
2132
- "评测会从干净副本重新构建提交源码,运行 build/nn_selftest,然后在固定初始状态上驱动 build/control,统计连续全向上保持时间 t_can。不要把任何未提交的二进制、临时构建产物或外部进程当作交付依赖。",
2132
+ "评测会从干净副本重新构建提交源码,编译并运行宿主侧隐藏基础设施测试,然后在固定初始状态上驱动 build/control,统计连续全向上保持时间 t_can。不要把任何未提交的二进制、临时构建产物或外部进程当作交付依赖。",
2133
2133
  "使用宿主工具 eval_submit 提交当前源码快照,使用 eval_test 请求宿主评测最新提交。提交快照、事件、测试结果和最终分数全部保存在工作区之外的宿主目录;不要尝试读取或修改这些数据,也不要依赖任何工作区内的测试结果文件。最终评测使用最新提交快照,按宿主事件时间积分计算分数。",
2134
2134
  "",
2135
- "工作方式:先完成最小可构建的基础设施和 control 协议;再持续做控制/训练实验并记录可复现结果;最终保证工作区从零构建、nn_selftest 通过、control 可被评测驱动,并留下 NOTES.md/RESULTS.md。",
2135
+ "工作方式:先完成最小可构建的基础设施和 control 协议;再持续做控制/训练实验并记录可复现结果;最终保证工作区从零构建、隐藏基础设施测试通过、control 可被评测驱动,并留下 NOTES.md/RESULTS.md。",
2136
2136
  "",
2137
2137
  "不要向我提问,也不要等待确认,独立完成。"
2138
2138
  ].join("\n");
@@ -193,13 +193,26 @@ def main() -> int:
193
193
  if made.returncode != 0:
194
194
  return emit(fail('build failed\n' + (made.stdout + made.stderr)[-4000:], workspace=workspace))
195
195
 
196
- selftest = candidate / 'build' / 'nn_selftest'
197
- if not selftest.is_file():
198
- return emit(fail('build/nn_selftest missing', workspace=workspace))
199
- st = subprocess.run([str(selftest)], cwd=str(candidate), capture_output=True,
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,
200
213
  text=True, env=env, timeout=600)
201
- if st.returncode != 0:
202
- return emit(fail('infrastructure self-test failed\n' + (st.stdout + st.stderr)[-4000:], workspace=workspace))
214
+ if it.returncode != 0:
215
+ return emit(fail('hidden infrastructure test failed\n' + (it.stdout + it.stderr)[-4000:], workspace=workspace))
203
216
 
204
217
  control_path = candidate / 'build' / 'control'
205
218
  if not control_path.is_file():
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-eval-guard",
3
- "version": "0.7.0",
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",