nx_f32x4_matmul_gate.nx source
↩ module page · 126 lines · 6740 B
1// nx_f32x4_matmul_gate.nx -- PROVES the packed-SIMD compute lever end-to-end.
2// The new __f32x4_dot intrinsic (added through nx_parse -> nx_ir(reuse ir_emit_f32_binop) -> OP_F32X4_DOT
3// -> x86 codegen movups+mulps+scalar-hsum -> nxasm packed encoding) does 4 f32 multiplies in ONE mulps
4// instruction. This builds the SAME matmul two ways -- scalar (__f32_mul/__f32_add, one MAC/instr) and
5// packed (__f32x4_dot, 4 MAC/mulps) -- and (1) verifies they agree BIT-EXACT (small-int f32 so the
6// summation order cannot change the result), (2) checks a known-answer dot, and (3) MEASURES the speedup.
7// This is the top item on the autonomous research queue: compute was ~469x off the silicon floor and packed
8// SIMD is the decisive lever. Honest: per-4 horizontal-sum is overhead; a vector-accumulating kernel would
9// approach the full 4x -- this first packed kernel measures whatever it measures, no overclaim.
10// No hw writes (Rule 26). expect_exit: 0 license_tier: ORIGINAL
11import "nx_syscalls.nx"
12import "nx_gate_verdict.nx"
13
14func fx_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
15func fx_num(v: i64) -> i64 { let b: *u8=sys_mmap(28); var m: i64=v; if m<0{m=0-m;sys_write(1,"-" as *u8,1)} let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var i: i64=0; while i<k{b[i]=t[k-1-i];i=i+1} sys_write(1,b,k); return 0 }
16
17// pack the low 32 bits of an i64-carried f32 as 4 little-endian bytes (so movups loads 4 contiguous f32)
18func pack4(buf: *u8, idx: i64, bits: i64) -> i64 {
19 buf[idx*4+0] = (bits) as u8
20 buf[idx*4+1] = (bits>>8) as u8
21 buf[idx*4+2] = (bits>>16) as u8
22 buf[idx*4+3] = (bits>>24) as u8
23 return 0
24}
25
26// scalar GEMM: C[i][j] = sum_k A[i][k]*Bt[j][k], one f32 MAC per iteration (i64-carried f32)
27func scalar_mm(sa: *i64, sb: *i64, sc: *i64, M: i64, N: i64, K: i64) -> i64 {
28 var i: i64=0
29 while i<M { var j: i64=0
30 while j<N {
31 var acc: i64 = __f32_from_i64(0)
32 var k: i64=0
33 while k<K { acc = __f32_add(acc, __f32_mul(sa[i*K+k], sb[j*K+k])); k=k+1 }
34 sc[i*N+j] = acc
35 j=j+1 }
36 i=i+1 }
37 return 0
38}
39
40// packed GEMM: same result, but __f32x4_dot does 4 lanes (one mulps) per step
41func packed_mm(pa: *u8, pb: *u8, pc: *i64, M: i64, N: i64, K: i64) -> i64 {
42 let pab: i64 = pa as i64
43 let pbb: i64 = pb as i64
44 var i: i64=0
45 while i<M { var j: i64=0
46 while j<N {
47 var acc: i64 = __f32_from_i64(0)
48 var k: i64=0
49 while k<K { acc = __f32_add(acc, __f32x4_dot((pab + (i*K+k)*4) as *u8, (pbb + (j*K+k)*4) as *u8)); k=k+4 }
50 pc[i*N+j] = acc
51 j=j+1 }
52 i=i+1 }
53 return 0
54}
55
56func main() -> i64 {
57 fx_puts("PACKED-SIMD f32 matmul: __f32x4_dot (movups+mulps+hsum) vs scalar __f32_mul -- the compute-physics lever\n\n" as *u8)
58 let M: i64=64
59 let N: i64=64
60 let K: i64=64
61 let pa: *u8 = sys_mmap(M*K*4)
62 let pb: *u8 = sys_mmap(N*K*4)
63 let sa: *i64 = sys_mmap(M*K*8) as *i64
64 let sb: *i64 = sys_mmap(N*K*8) as *i64
65 let pc: *i64 = sys_mmap(M*N*8) as *i64
66 let sc: *i64 = sys_mmap(M*N*8) as *i64
67
68 var i: i64=0
69 while i<M { var k: i64=0
70 while k<K { let v: i64 = __f32_from_i64(((i+k)%4)+1); sa[i*K+k]=v; pack4(pa, i*K+k, v); k=k+1 }
71 i=i+1 }
72 var j: i64=0
73 while j<N { var k2: i64=0
74 while k2<K { let w: i64 = __f32_from_i64(((j+k2)%4)+1); sb[j*K+k2]=w; pack4(pb, j*K+k2, w); k2=k2+1 }
75 j=j+1 }
76
77 scalar_mm(sa, sb, sc, M, N, K)
78 packed_mm(pa, pb, pc, M, N, K)
79 var mism: i64=0
80 i=0
81 while i<M { var j2: i64=0
82 while j2<N { if __f32_to_i64(sc[i*N+j2]) != __f32_to_i64(pc[i*N+j2]) { mism=mism+1 } j2=j2+1 }
83 i=i+1 }
84 let sample: i64 = __f32_to_i64(sc[0])
85 let psample: i64 = __f32_to_i64(pc[0])
86 var exp: i64=0
87 var kk: i64=0
88 while kk<K { let av: i64=((0+kk)%4)+1; exp=exp+av*av; kk=kk+1 }
89
90 let REPS: i64=50
91 let t0: i64 = sys_now_us()
92 var r: i64=0
93 while r<REPS { scalar_mm(sa, sb, sc, M, N, K); r=r+1 }
94 let t1: i64 = sys_now_us()
95 var r2: i64=0
96 while r2<REPS { packed_mm(pa, pb, pc, M, N, K); r2=r2+1 }
97 let t2: i64 = sys_now_us()
98 let sus: i64 = t1-t0
99 let pus: i64 = t2-t1
100 var spd: i64=0
101 if pus>0 { spd = sus*100/pus }
102
103 fx_puts(" matmul " as *u8); fx_num(M); fx_puts("x" as *u8); fx_num(N); fx_puts("x" as *u8); fx_num(K); fx_puts(", " as *u8); fx_num(REPS); fx_puts(" reps each\n" as *u8)
104 fx_puts(" C[0][0] scalar=" as *u8); fx_num(sample); fx_puts(" packed=" as *u8); fx_num(psample); fx_puts(" (expected " as *u8); fx_num(exp); fx_puts(")\n" as *u8)
105 fx_puts(" bit-exact mismatches (scalar vs packed): " as *u8); fx_num(mism); fx_puts(" / " as *u8); fx_num(M*N); fx_puts("\n" as *u8)
106 fx_puts(" scalar matmul: " as *u8); fx_num(sus); fx_puts(" us packed matmul: " as *u8); fx_num(pus); fx_puts(" us\n" as *u8)
107 fx_puts(" SPEEDUP = " as *u8); fx_num(spd/100); fx_puts("." as *u8); fx_num((spd%100)/10); fx_num(spd%10); fx_puts("x (packed mulps = 4 f32 mults / instruction)\n\n" as *u8)
108
109 var pass: i64=0
110 var ttl: i64=0
111 ttl=ttl+1; fx_puts(" T1 __f32x4_dot compiled+ran (new intrinsic: nx_parse -> IR -> x86 codegen -> nxasm packed): " as *u8); if psample>0 { pass=pass+1; fx_puts("PASS\n" as *u8) } else { fx_puts("FAIL\n" as *u8) }
112 ttl=ttl+1; fx_puts(" T2 CORRECT: packed == scalar BIT-EXACT (0 mismatches over all " as *u8); fx_num(M*N); fx_puts(" cells): " as *u8); if mism==0 { pass=pass+1; fx_puts("PASS\n" as *u8) } else { fx_puts("FAIL\n" as *u8) }
113 ttl=ttl+1; fx_puts(" T3 known-answer dot C[0][0] == " as *u8); fx_num(exp); fx_puts(": " as *u8); if sample==exp { if psample==exp { pass=pass+1; fx_puts("PASS\n" as *u8) } else { fx_puts("FAIL\n" as *u8) } } else { fx_puts("FAIL\n" as *u8) }
114 ttl=ttl+1; fx_puts(" T4 packed MEASURED faster than scalar (>1.0x): " as *u8); if spd>100 { pass=pass+1; fx_puts("PASS\n" as *u8) } else { fx_puts("FAIL\n" as *u8) }
115
116 fx_puts("NX-F32X4-MATMUL-GATE passed " as *u8); fx_num(pass); fx_puts("/" as *u8); fx_num(ttl)
117 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check
118 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled
119 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify.
120 let ctr__dry: *i64 = gv_ctr()
121 ctr__dry[0] = pass
122 ctr__dry[1] = ttl
123 let rc__dry: i64 = gv_verdict("F32X4-MATMUL-GATE" as *u8, ctr__dry, "packed SIMD WORKS end-to-end + measured faster -- the compute-physics lever is REAL)" as *u8)
124 sys_exit(rc__dry)
125 return rc__dry
126}