nx_conv2d.nx source
↩ module page · 562 lines · 22295 B
1// nx_conv2d.nx -- multi-channel 2D convolution forward pass.
2//
3// L3 canonical primitive. Closes the diffusion / vision-model gap:
4// every modern image-gen architecture (VAE encoder/decoder, UNet
5// stages, attention pre/post conv blocks, ResNet / EfficientNet
6// classifiers) is built around Conv2D layers. Substrate had
7// nx_winograd_conv (per-tile math) but no multi-channel composer.
8//
9// L4 consumers queued: nx_unet_block, nx_vae_decode, nx_resnet_block.
10// Each composes nx_conv2d in their per-layer flow.
11//
12// ===== Tensor layout =============================================
13//
14// Input: [N, C_in, H, W] NHWC? No -- NCHW (PyTorch/Caffe
15// convention; canonical for
16// the Q10 substrate).
17// Weight: [C_out, C_in, KH, KW] KH, KW = kernel height/width.
18// v1 hardcodes KH=KW=3.
19// Bias: [C_out] nullable.
20// Output: [N, C_out, H', W'] stride 1 + padding 1 -> H'=H, W'=W.
21//
22// Padding mode: zero-padding by (KH-1)/2 = 1 around input. Same as
23// PyTorch default for "padding=1, stride=1".
24//
25// All tensors in Q10 (substrate convention). Caller is responsible
26// for ensuring intermediate sums don't overflow i64 -- for typical
27// dimensions (C_in <= 1024, KH*KW = 9, Q10 * Q10 = ~1e6) the
28// accumulator stays under 1e10, plenty of i64 headroom.
29//
30// ===== Algorithm =================================================
31//
32// v1: direct 3x3 convolution. 6 nested loops: n, c_out, h, w,
33// c_in, k. O(N * C_out * H * W * C_in * 9) ops.
34//
35// v2 (queued): Winograd F(2x2, 3x3) per-tile via nx_winograd_conv
36// (already shipped 2026-05-15). 2.25x reduction in multiplies.
37// Plumbed by adding a `winograd` flag dispatching to a different
38// inner loop.
39//
40// Per the bits-up cardinal + bounded-loop discipline.
41//
42// genealogy_id: lecun_1989_lenet + krizhevsky_2012_imagenet_conv +
43// paszke_2017_pytorch_conv2d + lavin_gray_2016_winograd
44// lineage_id: substrate_conv2d_v1_direct
45
46// nx_safety_envelope:
47// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
48// sil_target: SIL1
49// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
50// verdict: NOT_YET_EVALUATED
51
52import "nx_syscalls.nx"
53import "nx_tier.nx"
54import "nx_loop.nx"
55import "nx_tensor.nx"
56import "nx_thread_pool.nx"
57
58// ===== Constants ==================================================
59
60const NX_CV2_Q10: nx_int = 1024
61const NX_CV2_KH: nx_int = 3
62const NX_CV2_KW: nx_int = 3
63const NX_CV2_PAD: nx_int = 1 // (KH - 1) / 2
64
65// ===== Sealed-enum: Conv2dVerdict =================================
66
67const NX_CV2_OK: nx_int = 0
68const NX_CV2_ERR_BAD_DTYPE: nx_int = 1
69const NX_CV2_ERR_BAD_NDIM: nx_int = 2
70const NX_CV2_ERR_SHAPE_MISMATCH: nx_int = 3
71const NX_CV2_ERR_KERNEL_SIZE: nx_int = 4
72const NX_CV2_ERR_NOT_CONTIGUOUS: nx_int = 5
73const NX_CV2_ERR_POOL_WAIT: nx_int = 6
74const NX_CV2_N_VERDICTS: nx_int = 7
75
76func nx_cv2_verdict_is_valid(v: nx_int) -> nx_int {
77 if v < 0 { return 0 }
78 if v >= NX_CV2_N_VERDICTS { return 0 }
79 return 1
80}
81
82// ===== Forward pass ==============================================
83//
84// input: *NxTensor [N, C_in, H, W] Q10
85// weight: *NxTensor [C_out, C_in, 3, 3] Q10
86// bias: *i64 [C_out] Q10 (nullable; pass 0 for no bias)
87// output: *NxTensor [N, C_out, H, W] Q10
88//
89// Stride 1, zero-padding 1. In-place not supported (input and
90// output buffers must differ).
91
92func nx_conv2d_forward(input: *NxTensor, weight: *NxTensor,
93 bias: *i64, output: *NxTensor) -> nx_int {
94 if input.dtype != NX_DT_I64 { return NX_CV2_ERR_BAD_DTYPE }
95 if weight.dtype != NX_DT_I64 { return NX_CV2_ERR_BAD_DTYPE }
96 if output.dtype != NX_DT_I64 { return NX_CV2_ERR_BAD_DTYPE }
97 if input.ndim != 4 { return NX_CV2_ERR_BAD_NDIM }
98 if weight.ndim != 4 { return NX_CV2_ERR_BAD_NDIM }
99 if output.ndim != 4 { return NX_CV2_ERR_BAD_NDIM }
100 if weight.shape[2] != NX_CV2_KH { return NX_CV2_ERR_KERNEL_SIZE }
101 if weight.shape[3] != NX_CV2_KW { return NX_CV2_ERR_KERNEL_SIZE }
102
103 let N: nx_int = input.shape[0]
104 let C_in: nx_int = input.shape[1]
105 let H: nx_int = input.shape[2]
106 let W: nx_int = input.shape[3]
107 let C_out: nx_int = weight.shape[0]
108
109 if weight.shape[1] != C_in { return NX_CV2_ERR_SHAPE_MISMATCH }
110 if output.shape[0] != N { return NX_CV2_ERR_SHAPE_MISMATCH }
111 if output.shape[1] != C_out { return NX_CV2_ERR_SHAPE_MISMATCH }
112 if output.shape[2] != H { return NX_CV2_ERR_SHAPE_MISMATCH }
113 if output.shape[3] != W { return NX_CV2_ERR_SHAPE_MISMATCH }
114
115 if nx_t_is_contiguous(input) == 0 { return NX_CV2_ERR_NOT_CONTIGUOUS }
116 if nx_t_is_contiguous(weight) == 0 { return NX_CV2_ERR_NOT_CONTIGUOUS }
117 if nx_t_is_contiguous(output) == 0 { return NX_CV2_ERR_NOT_CONTIGUOUS }
118
119 let pi: *i64 = input.storage as *i64
120 let pw: *i64 = weight.storage as *i64
121 let po: *i64 = output.storage as *i64
122
123 // Strides for index math.
124 let in_chan_stride: nx_int = H * W
125 let in_batch_stride: nx_int = C_in * H * W
126 let wt_chan_stride: nx_int = NX_CV2_KH * NX_CV2_KW
127 let wt_outchan_stride: nx_int = C_in * NX_CV2_KH * NX_CV2_KW
128 let out_chan_stride: nx_int = H * W
129 let out_batch_stride: nx_int = C_out * H * W
130
131 // Iterate: N x C_out x H x W output positions.
132 var n: nx_int = 0
133 var n_iter: nx_int = 0
134 var n_verdict: nx_int = NX_LOOP_RUNNING
135 let N_BUDGET: nx_int = N
136 while n_verdict == NX_LOOP_RUNNING && n_iter < N_BUDGET {
137 var co: nx_int = 0
138 var co_iter: nx_int = 0
139 var co_verdict: nx_int = NX_LOOP_RUNNING
140 let CO_BUDGET: nx_int = C_out
141 while co_verdict == NX_LOOP_RUNNING && co_iter < CO_BUDGET {
142
143 // Bias term (or 0 if no bias).
144 var bias_v: i64 = 0
145 if (bias as i64) != 0 { bias_v = bias[co] }
146
147 var oh: nx_int = 0
148 var oh_iter: nx_int = 0
149 var oh_verdict: nx_int = NX_LOOP_RUNNING
150 let OH_BUDGET: nx_int = H
151 while oh_verdict == NX_LOOP_RUNNING && oh_iter < OH_BUDGET {
152 var ow: nx_int = 0
153 var ow_iter: nx_int = 0
154 var ow_verdict: nx_int = NX_LOOP_RUNNING
155 let OW_BUDGET: nx_int = W
156 while ow_verdict == NX_LOOP_RUNNING && ow_iter < OW_BUDGET {
157
158 // Accumulate over C_in * KH * KW.
159 var acc: i64 = 0
160 var ci: nx_int = 0
161 var ci_iter: nx_int = 0
162 var ci_verdict: nx_int = NX_LOOP_RUNNING
163 let CI_BUDGET: nx_int = C_in
164 while ci_verdict == NX_LOOP_RUNNING && ci_iter < CI_BUDGET {
165
166 var kh: nx_int = 0
167 var kh_iter: nx_int = 0
168 var kh_verdict: nx_int = NX_LOOP_RUNNING
169 while kh_verdict == NX_LOOP_RUNNING && kh_iter < NX_CV2_KH {
170 let ih: nx_int = oh + kh - NX_CV2_PAD
171 var kw: nx_int = 0
172 var kw_iter: nx_int = 0
173 var kw_verdict: nx_int = NX_LOOP_RUNNING
174 while kw_verdict == NX_LOOP_RUNNING && kw_iter < NX_CV2_KW {
175 let iw: nx_int = ow + kw - NX_CV2_PAD
176 // Bounds check for zero padding.
177 var in_v: i64 = 0
178 if ih >= 0 {
179 if ih < H {
180 if iw >= 0 {
181 if iw < W {
182 let in_idx: nx_int = n * in_batch_stride + ci * in_chan_stride + ih * W + iw
183 in_v = pi[in_idx]
184 }
185 }
186 }
187 }
188 let wt_idx: nx_int = co * wt_outchan_stride + ci * wt_chan_stride + kh * NX_CV2_KW + kw
189 acc = acc + in_v * pw[wt_idx]
190 kw = kw + 1
191 kw_iter = kw_iter + 1
192 }
193 kh = kh + 1
194 kh_iter = kh_iter + 1
195 }
196 ci = ci + 1
197 ci_iter = ci_iter + 1
198 }
199
200 // Q10 scaling: input * weight is Q20; divide once
201 // by Q10 to bring back to Q10. Add bias (Q10).
202 let out_idx: nx_int = n * out_batch_stride + co * out_chan_stride + oh * W + ow
203 po[out_idx] = acc / NX_CV2_Q10 + bias_v
204
205 ow = ow + 1
206 ow_iter = ow_iter + 1
207 }
208 oh = oh + 1
209 oh_iter = oh_iter + 1
210 }
211 co = co + 1
212 co_iter = co_iter + 1
213 }
214 n = n + 1
215 n_iter = n_iter + 1
216 }
217 return NX_CV2_OK
218}
219
220// ===== Multi-threaded forward pass ================================
221//
222// ADDITIVE: the serial path above is untouched -- it is the
223// bit-exactness oracle for this path (two independent
224// implementations compared by nx_conv2d_mt_gate).
225//
226// Partition: the flattened output-row index r in [0, N*C_out*H)
227// maps 1:1 onto output rows (n, co, oh, *). Workers take
228// contiguous disjoint bands of r -- disjoint output writes, so no
229// atomics are needed. Same banding pattern as nx_conv_speedup
230// (measured 6.76x, 8 workers); pointers travel via a ctx struct,
231// not statics.
232//
233// Two entry points (fuller-API doctrine):
234// nx_conv2d_forward_pool -- caller-owned pool; amortizes worker
235// spawn across many layers/calls.
236// nx_conv2d_forward_mt -- one-shot: builds a pool (nworkers < 1
237// sizes from hardware), runs, shuts down.
238
239struct NxCv2Ctx {
240 in_ptr: i64,
241 wt_ptr: i64,
242 bias_ptr: i64,
243 out_ptr: i64,
244 c_in: i64,
245 h_dim: i64,
246 w_dim: i64,
247 c_out: i64,
248 r0: i64,
249 r1: i64,
250}
251
252const NX_CV2_CTX_BYTES: i64 = 80
253
254// Compute output rows [r0, r1). Same arithmetic, same accumulation
255// order, and same Q10 divide as the serial loops -- kept textually
256// parallel so the two implementations stay comparable oracles.
257func _nx_cv2_rows(cx: *NxCv2Ctx) -> i64 {
258 let pi: *i64 = cx.in_ptr as *i64
259 let pw: *i64 = cx.wt_ptr as *i64
260 let po: *i64 = cx.out_ptr as *i64
261 let C_in: i64 = cx.c_in
262 let H: i64 = cx.h_dim
263 let W: i64 = cx.w_dim
264 let C_out: i64 = cx.c_out
265
266 let in_chan_stride: i64 = H * W
267 let in_batch_stride: i64 = C_in * H * W
268 let wt_chan_stride: i64 = NX_CV2_KH * NX_CV2_KW
269 let wt_outchan_stride: i64 = C_in * NX_CV2_KH * NX_CV2_KW
270 let out_chan_stride: i64 = H * W
271 let out_batch_stride: i64 = C_out * H * W
272
273 var r: i64 = cx.r0
274 while r < cx.r1 {
275 let n: i64 = r / (C_out * H)
276 let rem: i64 = r % (C_out * H)
277 let co: i64 = rem / H
278 let oh: i64 = rem % H
279
280 var bias_v: i64 = 0
281 if cx.bias_ptr != 0 {
282 let pb: *i64 = cx.bias_ptr as *i64
283 bias_v = pb[co]
284 }
285
286 var ow: i64 = 0
287 while ow < W {
288 var acc: i64 = 0
289 var ci: i64 = 0
290 while ci < C_in {
291 var kh: i64 = 0
292 while kh < NX_CV2_KH {
293 let ih: i64 = oh + kh - NX_CV2_PAD
294 var kw: i64 = 0
295 while kw < NX_CV2_KW {
296 let iw: i64 = ow + kw - NX_CV2_PAD
297 var in_v: i64 = 0
298 if ih >= 0 { if ih < H { if iw >= 0 { if iw < W {
299 let in_idx: i64 = n * in_batch_stride + ci * in_chan_stride + ih * W + iw
300 in_v = pi[in_idx]
301 } } } }
302 let wt_idx: i64 = co * wt_outchan_stride + ci * wt_chan_stride + kh * NX_CV2_KW + kw
303 acc = acc + in_v * pw[wt_idx]
304 kw = kw + 1
305 }
306 kh = kh + 1
307 }
308 ci = ci + 1
309 }
310 let out_idx: i64 = n * out_batch_stride + co * out_chan_stride + oh * W + ow
311 po[out_idx] = acc / NX_CV2_Q10 + bias_v
312 ow = ow + 1
313 }
314 r = r + 1
315 }
316 return 0
317}
318
319func _nx_cv2_task(ctx_i: i64) -> i64 {
320 return _nx_cv2_rows(ctx_i as *NxCv2Ctx)
321}
322
323// Forward pass on a caller-owned pool. Bands the row space across
324// pool.n_workers (clamped to the row count). Safe to call
325// repeatedly on the same pool: waits on the completed-counter DELTA,
326// not the absolute count. Single-submitter assumption: no other
327// thread may submit to this pool during the call, or the delta wait
328// under-counts.
329func nx_conv2d_forward_pool(pool: *NxThreadPool, input: *NxTensor,
330 weight: *NxTensor, bias: *i64,
331 output: *NxTensor) -> nx_int {
332 if input.dtype != NX_DT_I64 { return NX_CV2_ERR_BAD_DTYPE }
333 if weight.dtype != NX_DT_I64 { return NX_CV2_ERR_BAD_DTYPE }
334 if output.dtype != NX_DT_I64 { return NX_CV2_ERR_BAD_DTYPE }
335 if input.ndim != 4 { return NX_CV2_ERR_BAD_NDIM }
336 if weight.ndim != 4 { return NX_CV2_ERR_BAD_NDIM }
337 if output.ndim != 4 { return NX_CV2_ERR_BAD_NDIM }
338 if weight.shape[2] != NX_CV2_KH { return NX_CV2_ERR_KERNEL_SIZE }
339 if weight.shape[3] != NX_CV2_KW { return NX_CV2_ERR_KERNEL_SIZE }
340
341 let N: nx_int = input.shape[0]
342 let C_in: nx_int = input.shape[1]
343 let H: nx_int = input.shape[2]
344 let W: nx_int = input.shape[3]
345 let C_out: nx_int = weight.shape[0]
346
347 if weight.shape[1] != C_in { return NX_CV2_ERR_SHAPE_MISMATCH }
348 if output.shape[0] != N { return NX_CV2_ERR_SHAPE_MISMATCH }
349 if output.shape[1] != C_out { return NX_CV2_ERR_SHAPE_MISMATCH }
350 if output.shape[2] != H { return NX_CV2_ERR_SHAPE_MISMATCH }
351 if output.shape[3] != W { return NX_CV2_ERR_SHAPE_MISMATCH }
352
353 if nx_t_is_contiguous(input) == 0 { return NX_CV2_ERR_NOT_CONTIGUOUS }
354 if nx_t_is_contiguous(weight) == 0 { return NX_CV2_ERR_NOT_CONTIGUOUS }
355 if nx_t_is_contiguous(output) == 0 { return NX_CV2_ERR_NOT_CONTIGUOUS }
356
357 let total_rows: i64 = N * C_out * H
358 if total_rows < 1 { return NX_CV2_OK }
359 var bands: i64 = pool.n_workers
360 if bands > total_rows { bands = total_rows }
361 if bands < 1 { bands = 1 }
362
363 // Per-band ctx records: one mmap per call, freed after the wait
364 // (leak-class doctrine -- no per-call leaks in a layer loop).
365 let ctxs: *u8 = sys_mmap(bands * NX_CV2_CTX_BYTES)
366 let rpb: i64 = (total_rows + bands - 1) / bands
367 let done_before: i64 = nx_pool_n_completed(pool)
368 var b: i64 = 0
369 while b < bands {
370 let cx: *NxCv2Ctx = ((ctxs as i64) + b * NX_CV2_CTX_BYTES) as *NxCv2Ctx
371 cx.in_ptr = input.storage as i64
372 cx.wt_ptr = weight.storage as i64
373 cx.bias_ptr = bias as i64
374 cx.out_ptr = output.storage as i64
375 cx.c_in = C_in
376 cx.h_dim = H
377 cx.w_dim = W
378 cx.c_out = C_out
379 cx.r0 = b * rpb
380 var r1: i64 = (b + 1) * rpb
381 if r1 > total_rows { r1 = total_rows }
382 cx.r1 = r1
383 nx_pool_submit(pool, _nx_cv2_task, cx as i64)
384 b = b + 1
385 }
386 let wv: i64 = nx_pool_wait(pool, done_before + bands)
387 sys_munmap(ctxs, bands * NX_CV2_CTX_BYTES)
388 if wv != 0 { return NX_CV2_ERR_POOL_WAIT }
389 return NX_CV2_OK
390}
391
392// One-shot multi-threaded forward. nworkers < 1 sizes from the
393// hardware (nx_hw_worker_count). A resolved worker count of 1 (or a
394// single-row problem) delegates to the serial path -- no pool cost.
395func nx_conv2d_forward_mt(input: *NxTensor, weight: *NxTensor,
396 bias: *i64, output: *NxTensor,
397 nworkers: nx_int) -> nx_int {
398 var nw: i64 = nworkers
399 if nw < 1 { nw = nx_hw_worker_count() }
400 // Clamp to the row count so tiny problems don't spawn idle
401 // workers. Shape reads are guarded; full validation happens in
402 // the pool/serial path this call delegates to.
403 if input.ndim == 4 { if weight.ndim == 4 {
404 let rows: i64 = input.shape[0] * weight.shape[0] * input.shape[2]
405 if nw > rows { nw = rows }
406 } }
407 if nw <= 1 { return nx_conv2d_forward(input, weight, bias, output) }
408 let pool: *NxThreadPool = nx_pool_new(nw, 0)
409 let v: nx_int = nx_conv2d_forward_pool(pool, input, weight, bias, output)
410 nx_pool_shutdown(pool)
411 return v
412}
413
414// ===== Self-test ==================================================
415//
416// Closed-form invariants:
417//
418// (a) Identity convolution: weight = identity 3x3 kernel (1.0 at
419// centre, 0 elsewhere). output == input bit-exact.
420//
421// (b) Box-blur 3x3 kernel (all 1/9): output is the local mean
422// of the 3x3 neighborhood (zero-padded edges).
423//
424// (c) Bad shape -> verdict.
425//
426// (d) Verdict gate.
427
428func main() -> i64 {
429 let N: nx_int = 1
430 let C_in: nx_int = 1
431 let C_out: nx_int = 1
432 let H: nx_int = 4
433 let W: nx_int = 4
434
435 // --- Allocate tensors ---
436 let in_sh: *nx_int = sys_mmap(4 * 8) as *nx_int
437 in_sh[0]=N; in_sh[1]=C_in; in_sh[2]=H; in_sh[3]=W
438 let wt_sh: *nx_int = sys_mmap(4 * 8) as *nx_int
439 wt_sh[0]=C_out; wt_sh[1]=C_in; wt_sh[2]=NX_CV2_KH; wt_sh[3]=NX_CV2_KW
440 let out_sh: *nx_int = sys_mmap(4 * 8) as *nx_int
441 out_sh[0]=N; out_sh[1]=C_out; out_sh[2]=H; out_sh[3]=W
442
443 let err: *nx_int = sys_mmap(8) as *nx_int
444 err[0] = 0
445 let input: *NxTensor = nx_t_alloc(NX_DT_I64, in_sh, 4, err)
446 let weight: *NxTensor = nx_t_alloc(NX_DT_I64, wt_sh, 4, err)
447 let output: *NxTensor = nx_t_alloc(NX_DT_I64, out_sh, 4, err)
448 if err[0] != 0 { return 5 }
449
450 // --- (a) Identity kernel ---
451 // weight[0, 0, :, :] = [[0,0,0],[0,Q10,0],[0,0,0]] (Q10=1024)
452 let pw: *i64 = weight.storage as *i64
453 var wi: nx_int = 0
454 while wi < 9 { pw[wi] = 0; wi = wi + 1 }
455 pw[4] = NX_CV2_Q10 // centre
456
457 // input[0, 0, h, w] = h * 10 + w (in Q10 directly = integer; don't scale)
458 let pi: *i64 = input.storage as *i64
459 var h: nx_int = 0
460 while h < H {
461 var w: nx_int = 0
462 while w < W {
463 pi[h * W + w] = (h * 10 + w) * NX_CV2_Q10
464 w = w + 1
465 }
466 h = h + 1
467 }
468
469 let v_id: nx_int = nx_conv2d_forward(input, weight, 0 as *i64, output)
470 if v_id != NX_CV2_OK { return 10 + v_id }
471
472 let po: *i64 = output.storage as *i64
473 // Compare bit-exact: identity should reproduce input.
474 var h2: nx_int = 0
475 while h2 < H {
476 var w2: nx_int = 0
477 while w2 < W {
478 if po[h2 * W + w2] != pi[h2 * W + w2] { return 20 }
479 w2 = w2 + 1
480 }
481 h2 = h2 + 1
482 }
483
484 // --- (b) Box-blur kernel ---
485 // weight = [[Q10/9, Q10/9, Q10/9], [Q10/9, Q10/9, Q10/9], [Q10/9, Q10/9, Q10/9]]
486 // The Q10 must come out averaged; division gives 113.7 -> rounds to 113 Q10.
487 let one_ninth_q10: i64 = NX_CV2_Q10 / 9 // 113
488 var wj: nx_int = 0
489 while wj < 9 { pw[wj] = one_ninth_q10; wj = wj + 1 }
490
491 // Set input to a constant 100 (in Q10 = 102400).
492 var hk: nx_int = 0
493 while hk < H {
494 var wk: nx_int = 0
495 while wk < W {
496 pi[hk * W + wk] = 100 * NX_CV2_Q10
497 wk = wk + 1
498 }
499 hk = hk + 1
500 }
501
502 nx_conv2d_forward(input, weight, 0 as *i64, output)
503 // Interior pixel (1,1) has all 9 neighbors present so output =
504 // 9 * (100 * Q10) * (Q10/9) / Q10 = 100 * (Q10/9) * 9 / Q10 * Q10 ...
505 // Carefully: acc = sum over 9 of (100*Q10) * (Q10/9) = 100*Q10 * Q10 = (close).
506 // After divide by Q10: 9 * 100 * Q10 / 9 = 100 * Q10 (approx)
507 // Due to integer truncation in (Q10/9), each contribution loses
508 // a small fraction. Expected interior value: 100 * Q10 ± rounding.
509 let centre_val: i64 = po[1 * W + 1]
510 let expected: i64 = 100 * NX_CV2_Q10
511 let drift: i64 = centre_val - expected
512 // Accept up to 10% drift due to integer kernel rounding.
513 let bound: i64 = expected / 10
514 if drift > bound { return 30 }
515 if drift < 0 - bound { return 31 }
516
517 // --- (c) Bad shape ---
518 let bad_sh: *nx_int = sys_mmap(4 * 8) as *nx_int
519 bad_sh[0]=999; bad_sh[1]=C_in; bad_sh[2]=H; bad_sh[3]=W // wrong N
520 let bad_out: *NxTensor = nx_t_alloc(NX_DT_I64, bad_sh, 4, err)
521 let v_bad: nx_int = nx_conv2d_forward(input, weight, 0 as *i64, bad_out)
522 if v_bad != NX_CV2_ERR_SHAPE_MISMATCH { return 40 }
523
524 // --- (d) Verdict gate ---
525 var vi: nx_int = 0
526 while vi < NX_CV2_N_VERDICTS {
527 if nx_cv2_verdict_is_valid(vi) != 1 { return 50 + vi }
528 vi = vi + 1
529 }
530
531 // --- (e) Multi-threaded forward: identity kernel, bit-exact ---
532 // Rebuild identity weights + the distinct-value input ((b)
533 // overwrote both), run the MT path with 2 workers into a fresh
534 // output tensor, expect output == input bit-exact.
535 var we: nx_int = 0
536 while we < 9 { pw[we] = 0; we = we + 1 }
537 pw[4] = NX_CV2_Q10
538 var he: nx_int = 0
539 while he < H {
540 var wcol: nx_int = 0
541 while wcol < W {
542 pi[he * W + wcol] = (he * 10 + wcol) * NX_CV2_Q10
543 wcol = wcol + 1
544 }
545 he = he + 1
546 }
547 let out2_sh: *nx_int = sys_mmap(4 * 8) as *nx_int
548 out2_sh[0]=N; out2_sh[1]=C_out; out2_sh[2]=H; out2_sh[3]=W
549 let output2: *NxTensor = nx_t_alloc(NX_DT_I64, out2_sh, 4, err)
550 if err[0] != 0 { return 60 }
551 let v_mt: nx_int = nx_conv2d_forward_mt(input, weight, 0 as *i64, output2, 2)
552 if v_mt != NX_CV2_OK { return 61 }
553 let po2: *i64 = output2.storage as *i64
554 let n_out: nx_int = N * C_out * H * W
555 var ei: nx_int = 0
556 while ei < n_out {
557 if po2[ei] != pi[ei] { return 62 }
558 ei = ei + 1
559 }
560
561 return 0
562}