code wiki / (root) / nx_conv2d.nx

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}