code wiki / (root) / nx_f32x4_matmul_gate.nx

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}