nx_conv2d.nx
buildroot/runtime/nx_conv2d.nx
about
nx_conv2d.nx -- multi-channel 2D convolution forward pass.
L3 canonical primitive. Closes the diffusion / vision-model gap:
every modern image-gen architecture (VAE encoder/decoder, UNet
stages, attention pre/post conv blocks, ResNet / EfficientNet
classifiers) is built around Conv2D layers. Substrate had
nx_winograd_conv (per-tile math) but no multi-channel composer.
L4 consumers queued: nx_unet_block, nx_vae_decode, nx_resnet_block.
Each composes nx_conv2d in their per-layer flow.
===== Tensor layout =============================================
Input: [N, C_in, H, W] NHWC? No -- NCHW (PyTorch/Caffe
convention; canonical for
the Q10 substrate).
Weight: [C_out, C_in, KH, KW] KH, KW = kernel height/width.
v1 hardcodes KH=KW=3.
Bias: [C_out] nullable.
Output: [N, C_out, H', W'] stride 1 + padding 1 -> H'=H, W'=W.
Padding mode: zero-padding by (KH-1)/2 = 1 around input. Same as
PyTorch default for "padding=1, stride=1".
All tensors in Q10 (substrate convention). Caller is responsible
for ensuring intermediate sums don't overflow i64 -- for typical
dimensions (C_in <= 1024, KH*KW = 9, Q10 * Q10 = ~1e6) the
accumulator stays under 1e10, plenty of i64 headroom.
===== Algorithm =================================================
v1: direct 3x3 convolution. 6 nested loops: n, c_out, h, w,
c_in, k. O(N * C_out * H * W * C_in * 9) ops.
v2 (queued): Winograd F(2x2, 3x3) per-tile via nx_winograd_conv
(already shipped 2026-05-15). 2.25x reduction in multiplies.
Plumbed by adding a `winograd` flag dispatching to a different
inner loop.
Per the bits-up cardinal + bounded-loop discipline.
dependencies 5 imports · 2 importers
imports: nx_syscalls.nxnx_tier.nxnx_loop.nxnx_tensor.nxnx_thread_pool.nx
imported by: nx_conv2d_mt_gate.nxnx_unet_block.nx
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| 239 | struct NxCv2Ctx |
consts
| 60 | const NX_CV2_Q10: nx_int = 1024 |
| 61 | const NX_CV2_KH: nx_int = 3 |
| 62 | const NX_CV2_KW: nx_int = 3 |
| 63 | const NX_CV2_PAD: nx_int = 1 // (KH - 1) / 2 |
| 67 | const NX_CV2_OK: nx_int = 0 |
| 68 | const NX_CV2_ERR_BAD_DTYPE: nx_int = 1 |
| 69 | const NX_CV2_ERR_BAD_NDIM: nx_int = 2 |
| 70 | const NX_CV2_ERR_SHAPE_MISMATCH: nx_int = 3 |
| 71 | const NX_CV2_ERR_KERNEL_SIZE: nx_int = 4 |
| 72 | const NX_CV2_ERR_NOT_CONTIGUOUS: nx_int = 5 |
| 73 | const NX_CV2_ERR_POOL_WAIT: nx_int = 6 |
| 74 | const NX_CV2_N_VERDICTS: nx_int = 7 |
| 252 | const NX_CV2_CTX_BYTES: i64 = 80 |
functions
| 76 | func nx_cv2_verdict_is_valid(v: nx_int) -> nx_int called by 1: main |
| 92 | func nx_conv2d_forward(input: *NxTensor, weight: *NxTensor, |
| 257 | func _nx_cv2_rows(cx: *NxCv2Ctx) -> i64 called by 1: _nx_cv2_task |
| 319 | func _nx_cv2_task(ctx_i: i64) -> i64 calls 1: _nx_cv2_rows |
| 329 | func nx_conv2d_forward_pool(pool: *NxThreadPool, input: *NxTensor, |
| 395 | func nx_conv2d_forward_mt(input: *NxTensor, weight: *NxTensor, called by 2: mainmain calls 5: nx_hw_worker_countnx_conv2d_forwardnx_pool_newnx_conv2d_forward_poolnx_pool_shutdown |
| 428 | func main() -> i64 |