code wiki / _hdl_build / nx_simd.nx
nx_simd.nx source
↩ module page · 54 lines · 2906 B
1// nx_simd.nx -- sovereign SIMD: lane-wise vector ops + a VECTORIZING TRANSFORM, the
2// start of the arc that wins the float/throughput benchmarks (mandelbrot, n-body,
3// spectral-norm) which are lost on scalar code. One vector instruction does VW lanes
4// at ~one scalar op's cost (uops.info/nanoBench: vmulps 8 lanes @ 0.5cy = 8x), so a
5// vectorized loop issues ~N/VW ops instead of N. Here the transform takes a scalar
6// elementwise kernel and produces its vector form, PROVEN 1:1 over a real array
7// (including the scalar tail for non-multiples of VW). Integer lanes first -- the
8// vectorization machinery (ops, transform, 1:1 proof, lane cost) is the reusable
9// part; float lanes + per-arch emit (AVX2 / NEON / RVV) lower onto the spec encoder.
10//
11// nanoBench (Abel, github.com/andreas-abel/nanoBench) measures these vector costs on
12// Intel AND AMD/ARM -- the per-arch numbers the vectorizer's cost model consumes.
13
14import "nx_syscalls.nx"
15
16const VW: i64 = 8 // vector width (lanes); models AVX2 (8x i32) / AVX-512 (8x i64)
17
18// ---- lane-wise vector ops (one real SIMD instruction each) ----
19func v_load(out: *i64, mem: *i64, off: i64) -> i64 { var j: i64 = 0; while j < VW { out[j] = mem[off + j]; j = j + 1 } return 0 }
20func v_store(mem: *i64, off: i64, v: *i64) -> i64 { var j: i64 = 0; while j < VW { mem[off + j] = v[j]; j = j + 1 } return 0 }
21func v_add(out: *i64, a: *i64, b: *i64) -> i64 { var j: i64 = 0; while j < VW { out[j] = a[j] + b[j]; j = j + 1 } return 0 }
22func v_sub(out: *i64, a: *i64, b: *i64) -> i64 { var j: i64 = 0; while j < VW { out[j] = a[j] - b[j]; j = j + 1 } return 0 }
23func v_mul(out: *i64, a: *i64, b: *i64) -> i64 { var j: i64 = 0; while j < VW { out[j] = a[j] * b[j]; j = j + 1 } return 0 }
24
25// the SCALAR kernel (the spec): c[i] = a[i]*b[i] + d[i] -- the FMA/dot-product inner
26// loop shape that spectral-norm / n-body / matmul are built from.
27func k_scalar(c: *i64, a: *i64, b: *i64, d: *i64, n: i64) -> i64 {
28 var i: i64 = 0
29 while i < n { c[i] = a[i] * b[i] + d[i]; i = i + 1 }
30 return 0
31}
32
33// the VECTORIZED form the transform authors: VW lanes per iteration (vmul + vadd),
34// then a scalar tail. Returns the number of VECTOR ops issued (the cost to weigh
35// against the scalar op count).
36func k_vector(c: *i64, a: *i64, b: *i64, d: *i64, n: i64) -> i64 {
37 let va: *i64 = sys_mmap(8 * VW)
38 let vb: *i64 = sys_mmap(8 * VW)
39 let vd: *i64 = sys_mmap(8 * VW)
40 let vt: *i64 = sys_mmap(8 * VW)
41 let vc: *i64 = sys_mmap(8 * VW)
42 var i: i64 = 0
43 var vops: i64 = 0
44 while i + VW <= n {
45 v_load(va, a, i); v_load(vb, b, i); v_load(vd, d, i)
46 v_mul(vt, va, vb); vops = vops + 1
47 v_add(vc, vt, vd); vops = vops + 1
48 v_store(c, i, vc)
49 i = i + VW
50 }
51 // tail: the leftover lanes (n not a multiple of VW) run scalar.
52 while i < n { c[i] = a[i] * b[i] + d[i]; i = i + 1 }
53 return vops
54}