code wiki / (root) / nx_q4k_fused_vs_x4_gate.nx

nx_q4k_fused_vs_x4_gate.nx source

↩ module page · 237 lines · 13318 B

1// nx_q4k_fused_vs_x4_gate.nx -- THE HEAD-TO-HEAD the SOTA decision actually rests on: 2// the FUSED INTEGER dequant-dot (nx_q4k_dot_row_col) against the LIVE forward kernel 3// (fq4m_rows_x4 / nx_f32_q4k_matmul_x4), in ONE binary, ONE shape, ONE thread. 4// 5// WHY THIS GATE EXISTS. nx_q4k_speed_bench reports the fused integer route beating f32 by ~20-48x, but 6// its f32 leg is SINGLE-THREADED SCALAR EMULATED f32 -- which is NOT the live path. The live path is 7// nx_f32_q4k_matmul_pool_x4: hardware __f32x4_dot AND multicore. Quoting that 20-48x as the shipped win 8// would repeat the exact "10.25x vs SERIAL scalar" error already caught once in this lane. A speedup is 9// only real against WHAT IS ACTUALLY DISPATCHED. 10// 11// THREADING IS DELIBERATELY EXCLUDED, AND THAT IS SOUND. Both routes band-parallelise over the SAME axis 12// (output columns j) with identical banding, so a pool multiplies both sides equally and the 13// single-thread ratio carries over. It is also CONSERVATIVE for the fused side: fused reads only the 14// packed Q4_K bytes while x4 materialises k f32 values per column, so under real multicore memory 15// pressure fused should scale strictly BETTER, never worse. Any win measured here is a LOWER BOUND. 16// 17// NUMERIC REGIME (why bit-exact comparison is legitimate here, and why that is NOT a general claim). 18// Weights are synthetic Q4_K super-blocks with d=1.0, dmin=0, all scales=1, mins=0, so every dequantised 19// weight is an exact integer 0..15. The activation column holds small exact integers. Then: 20// x4 computes an exact-integer f32 sum (|dot| <= 15*3*1024 = 46080 << 2^24, so f32 add is EXACT). 21// fused computes d1=2^24, m1=0, so v = 2^24*q4 (Q24); v*col_q10 is Q34; >>24 recovers Q10 exactly. 22// Both therefore represent the SAME integer and must agree BIT-EXACTLY. On REAL model weights they will 23// NOT -- fused is activation quantisation (W4A-fixed), a genuine precision change. This gate proves the 24// kernels compute the same function; it does NOT license dispatch on real weights without a separate 25// fidelity tooth (nx_q4k_ggml_kat's 2% band is the existing instrument for that). 26// 27// genealogy_id: nx_q4k_x8_gate (data construction, exact regime, refutation-tooth discipline) 28import "nx_f32_q4k_matmul.nx" 29import "nx_q4k_matmul.nx" 30import "nx_dequant_iter.nx" 31import "nx_f32_cvt.nx" 32import "nx_fmt.nx" 33import "nx_gate_verdict.nx" 34 35const FVX_M: i64 = 1 // decode shape -- the dominant serving case and where fused should be strongest 36const FVX_K: i64 = 1024 37const FVX_N: i64 = 512 38 39func fv_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 40func fv_num(v: i64) -> i64 { 41 let b: *u8 = sys_mmap(32); var m: i64 = v 42 if m == 0 { b[0] = 48 as u8; sys_write(1, b, 1); return 0 } 43 if m < 0 { fv_puts("-" as *u8); m = 0 - m } 44 let t: *u8 = sys_mmap(32); var k: i64 = 0 45 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 46 var i: i64 = 0 47 while i < k { b[i] = t[k - 1 - i]; i = i + 1 } 48 sys_write(1, b, k); return 0 49} 50func fv_lcg(s: i64) -> i64 { var v: i64 = s * 1103515245 + 12345; v = v & 2147483647; return v } 51 52// Dense exact-regime Q4_K super-block: d=1.0 (f16 0x3C00), dmin=0, sc=1, m=0. 53func fv_block(buf: *u8, off: i64, seed: i64) -> i64 { 54 buf[off + 0] = 0x00 as u8 55 buf[off + 1] = 0x3C as u8 56 buf[off + 2] = 0 as u8 57 buf[off + 3] = 0 as u8 58 var i: i64 = 0 59 while i < 4 { 60 buf[off + 4 + i] = 0x01 as u8 61 buf[off + 8 + i] = 0x00 as u8 62 buf[off + 12 + i] = 0x01 as u8 63 i = i + 1 64 } 65 var s: i64 = seed 66 var z: i64 = 0 67 while z < 128 { s = fv_lcg(s); buf[off + 16 + z] = (s & 255) as u8; z = z + 1 } 68 return 0 69} 70func fv_fill_w(buf: *u8, n_rows: i64, k: i64, seed: i64) -> i64 { 71 let bpr: i64 = (k / 256) * 144 72 var s: i64 = seed 73 var r: i64 = 0 74 while r < n_rows { 75 var b: i64 = 0 76 while b < k / 256 { s = fv_lcg(s); fv_block(buf, r * bpr + b * 144, s); b = b + 1 } 77 r = r + 1 78 } 79 return 0 80} 81 82func main(argc: i64, argv: *i64) -> i64 { 83 fv_puts("=== nx_q4k_fused_vs_x4_gate: FUSED INTEGER dot vs the LIVE x4 forward kernel ===\n" as *u8) 84 var pass: i64 = 0 85 var fail: i64 = 0 86 87 let bpr: i64 = (FVX_K / 256) * 144 88 let n_blocks: i64 = FVX_K / 256 89 let W: *u8 = sys_mmap(FVX_N * bpr) 90 fv_fill_w(W, FVX_N, FVX_K, 99) 91 92 // ONE activation vector, expressed in BOTH domains from the same integers. 93 let A: *i64 = sys_mmap(FVX_K * 8) as *i64 // f32 bits, for x4 94 let colq10: *i64 = sys_mmap(FVX_K * 8) as *i64 // Q10 fixed point, for fused 95 var i: i64 = 0 96 while i < FVX_K { 97 let v: i64 = (i - (i / 7) * 7) - 3 // exact small ints in [-3, 3] 98 A[i] = nx_i32_to_f32(v) 99 colq10[i] = v * 1024 100 i = i + 1 101 } 102 103 let C4: *i64 = sys_mmap(FVX_M * FVX_N * 8) as *i64 104 let CF: *i64 = sys_mmap(FVX_M * FVX_N * 8) as *i64 105 let it: *NxQ4KBlockIter = nx_q4k_iter_alloc() 106 107 // ---- T1: the live x4 kernel runs ---- 108 let r4: nx_int = nx_f32_q4k_matmul_x4(A, W, 0, C4, FVX_M, FVX_K, FVX_N) 109 if r4 == NX_FQ4M_OK { pass = pass + 1; fv_puts(" T1 x4 (live kernel) OK\n" as *u8) } else { fail = fail + 1; fv_puts(" T1 x4 rc FAIL\n" as *u8) } 110 111 // ---- T2: fused produces the SAME values (exact regime) ---- 112 var j: i64 = 0 113 while j < FVX_N { 114 let dot_q34: i64 = nx_q4k_dot_row_col(W, j * bpr, n_blocks, colq10, it) 115 CF[j] = nx_q10_to_f32(nx_q4km_q20_to_q10(dot_q34)) 116 j = j + 1 117 } 118 var mism: i64 = 0 119 var jj: i64 = 0 120 while jj < FVX_N { if CF[jj] != C4[jj] { mism = mism + 1 } jj = jj + 1 } 121 if mism == 0 { pass = pass + 1; fv_puts(" T2 fused == x4 BIT-EXACT over all columns\n" as *u8) } else { fail = fail + 1; fv_puts(" T2 MISMATCH columns=" as *u8); fv_num(mism); fv_puts("\n" as *u8) } 122 123 // ---- T3: NEGATIVE CONTROL -- this comparison must be ABLE to fail. ---- 124 // Perturb one activation in the Q10 domain only; the two routes must then DISAGREE. 125 // Without this, T2 passing could mean "compared nothing". 126 colq10[0] = colq10[0] + 1024 127 let dot_bad: i64 = nx_q4k_dot_row_col(W, 0, n_blocks, colq10, it) 128 let cf_bad: i64 = nx_q10_to_f32(nx_q4km_q20_to_q10(dot_bad)) 129 colq10[0] = colq10[0] - 1024 130 if cf_bad != C4[0] { pass = pass + 1; fv_puts(" T3 neg-control: perturbed input DOES change the verdict\n" as *u8) } else { fail = fail + 1; fv_puts(" T3 neg-control DEAD -- T2 proves nothing\n" as *u8) } 131 132 // ---- T5: nx_f32_to_q10 IN ITS REAL ROLE -- the adoption blocker. ---- 133 // The forward carries activations as f32 bits; the fused dot needs a Q10 column. Until now this 134 // gate hand-built the Q10 column as v*1024, which tests the KERNEL but not the CONVERSION the 135 // forward would actually have to perform. Rebuild the column with nx_f32_to_q10(A[i]) -- the real 136 // path -- and require the fused result to STILL match x4 bit-exactly. A round-trip self-test would 137 // not catch a conversion that is self-consistent but disagrees with the f32 kernel. 138 let colq10b: *i64 = sys_mmap(FVX_K * 8) as *i64 139 var ci: i64 = 0 140 while ci < FVX_K { colq10b[ci] = nx_f32_to_q10(A[ci]); ci = ci + 1 } 141 var cvt_bad: i64 = 0 142 var cj: i64 = 0 143 while cj < FVX_K { if colq10b[cj] != colq10[cj] { cvt_bad = cvt_bad + 1 } cj = cj + 1 } 144 if cvt_bad == 0 { pass = pass + 1; fv_puts(" T5a nx_f32_to_q10 reproduces the Q10 column exactly\n" as *u8) } else { fail = fail + 1; fv_puts(" T5a nx_f32_to_q10 WRONG on " as *u8); fv_num(cvt_bad); fv_puts(" of K entries\n" as *u8) } 145 146 var mism2: i64 = 0 147 var jb: i64 = 0 148 while jb < FVX_N { 149 let db: i64 = nx_q4k_dot_row_col(W, jb * bpr, n_blocks, colq10b, it) 150 if nx_q10_to_f32(nx_q4km_q20_to_q10(db)) != C4[jb] { mism2 = mism2 + 1 } 151 jb = jb + 1 152 } 153 if mism2 == 0 { pass = pass + 1; fv_puts(" T5b fused-with-CONVERTED-column == x4 BIT-EXACT (adoption path is sound)\n" as *u8) } else { fail = fail + 1; fv_puts(" T5b converted-column MISMATCH columns=" as *u8); fv_num(mism2); fv_puts("\n" as *u8) } 154 155 // ---- T4: THE MEASUREMENT. Same shape, same thread, same data. ---- 156 let t0: i64 = sys_now_us() 157 nx_f32_q4k_matmul_x4(A, W, 0, C4, FVX_M, FVX_K, FVX_N) 158 let t1: i64 = sys_now_us() 159 var jf: i64 = 0 160 while jf < FVX_N { 161 CF[jf] = nx_q10_to_f32(nx_q4km_q20_to_q10(nx_q4k_dot_row_col(W, jf * bpr, n_blocks, colq10, it))) 162 jf = jf + 1 163 } 164 let t2: i64 = sys_now_us() 165 let us4: i64 = t1 - t0 166 let usf: i64 = t2 - t1 167 fv_puts(" x4_us=" as *u8); fv_num(us4) 168 fv_puts(" fused_us=" as *u8); fv_num(usf); fv_puts("\n" as *u8) 169 if usf > 0 { fv_puts(" fused_vs_x4_x100=" as *u8); fv_num((us4 * 100) / usf); fv_puts(" (>100 means fused is faster)\n" as *u8) } 170 171 // THE REFUTATION TOOTH, written BEFORE the run: if fused is not faster than the kernel actually 172 // dispatched today, the fused route does NOT justify the numeric change it costs, and must not be 173 // wired into the forward. A negative result here is a real result -- exactly as it was for x8. 174 if usf < us4 { pass = pass + 1; fv_puts(" T4 fused FASTER than the live x4 kernel\n" as *u8) } else { fail = fail + 1; fv_puts(" T4 fused NOT faster than live x4 -- DO NOT DISPATCH; the 20-48x was vs scalar-emulated f32, not vs the live path\n" as *u8) } 175 176 // ---- T6/T7: THE m-BLOCKED FUSED GEMM -- the whole point of the rung. ---- 177 // nx_q4k_dot_row_col takes ONE column, so looping it m times would re-traverse and re-dequantise 178 // the weight row m times, throwing away the amortisation fq4m_rows already has. fq4m_rows_fused 179 // dequantises each weight element ONCE and accumulates into all m accumulators. The claim is 180 // therefore NOT just "fused is fast at m=1" -- it is that fused stays ahead as m grows, which is 181 // the compute-bound prefill regime. Test BOTH shapes; a win at m=1 alone would not establish it. 182 let MB: i64 = 8 183 let A8: *i64 = sys_mmap(MB * FVX_K * 8) as *i64 184 let C48: *i64 = sys_mmap(MB * FVX_N * 8) as *i64 185 let CF8: *i64 = sys_mmap(MB * FVX_N * 8) as *i64 186 var ri: i64 = 0 187 while ri < MB { 188 var ck: i64 = 0 189 while ck < FVX_K { 190 let vv: i64 = ((ri + ck) - ((ri + ck) / 7) * 7) - 3 191 A8[ri * FVX_K + ck] = nx_i32_to_f32(vv) 192 ck = ck + 1 193 } 194 ri = ri + 1 195 } 196 197 // T6: m=1 GEMM entry point agrees with x4 (the dispatcher-flip surface, not just the raw dot). 198 let rf1: nx_int = nx_f32_q4k_matmul_fused(A, W, 0, CF, FVX_M, FVX_K, FVX_N) 199 nx_f32_q4k_matmul_x4(A, W, 0, C4, FVX_M, FVX_K, FVX_N) 200 var mm1: i64 = 0 201 var q1: i64 = 0 202 while q1 < FVX_N { if CF[q1] != C4[q1] { mm1 = mm1 + 1 } q1 = q1 + 1 } 203 if rf1 == NX_FQ4M_OK { if mm1 == 0 { pass = pass + 1; fv_puts(" T6 fused GEMM m=1 == x4 BIT-EXACT\n" as *u8) } else { fail = fail + 1; fv_puts(" T6 fused GEMM m=1 MISMATCH cols=" as *u8); fv_num(mm1); fv_puts("\n" as *u8) } } else { fail = fail + 1; fv_puts(" T6 fused GEMM m=1 rc FAIL\n" as *u8) } 204 205 // T7: m=8 correctness -- this is where a naive per-column fused loop would still be CORRECT but 206 // slow, so correctness alone does not prove the m-blocking; T8 times it. 207 let rf8: nx_int = nx_f32_q4k_matmul_fused(A8, W, 0, CF8, MB, FVX_K, FVX_N) 208 nx_f32_q4k_matmul_x4(A8, W, 0, C48, MB, FVX_K, FVX_N) 209 var mm8: i64 = 0 210 var q8: i64 = 0 211 while q8 < MB * FVX_N { if CF8[q8] != C48[q8] { mm8 = mm8 + 1 } q8 = q8 + 1 } 212 if rf8 == NX_FQ4M_OK { if mm8 == 0 { pass = pass + 1; fv_puts(" T7 fused GEMM m=8 == x4 BIT-EXACT over all m*n cells\n" as *u8) } else { fail = fail + 1; fv_puts(" T7 fused GEMM m=8 MISMATCH cells=" as *u8); fv_num(mm8); fv_puts("\n" as *u8) } } else { fail = fail + 1; fv_puts(" T7 fused GEMM m=8 rc FAIL\n" as *u8) } 213 214 // T8: THE PREFILL MEASUREMENT. Per-token cost is the honest unit here: an m x k x n GEMM does 215 // 2*k*n FLOPs per token regardless of m, so what must not happen is fused LOSING its lead as m grows. 216 let p0: i64 = sys_now_us() 217 nx_f32_q4k_matmul_x4(A8, W, 0, C48, MB, FVX_K, FVX_N) 218 let p1: i64 = sys_now_us() 219 nx_f32_q4k_matmul_fused(A8, W, 0, CF8, MB, FVX_K, FVX_N) 220 let p2: i64 = sys_now_us() 221 let x4_8: i64 = p1 - p0 222 let fu_8: i64 = p2 - p1 223 fv_puts(" m=8 x4_us=" as *u8); fv_num(x4_8) 224 fv_puts(" fused_us=" as *u8); fv_num(fu_8); fv_puts("\n" as *u8) 225 if fu_8 > 0 { fv_puts(" m8_fused_vs_x4_x100=" as *u8); fv_num((x4_8 * 100) / fu_8); fv_puts("\n" as *u8) } 226 if fu_8 < x4_8 { pass = pass + 1; fv_puts(" T8 fused still FASTER at m=8 (m-blocking holds in the batched regime)\n" as *u8) } else { fail = fail + 1; fv_puts(" T8 fused LOST its lead at m=8 -- m-blocking did NOT hold; do not claim the prefill regime\n" as *u8) } 227 228 // D001: inherit nx_gate_verdict so nx_gate_green can judge this gate from outside and flake/erosion 229 // stay visible, instead of rolling our own GREEN/RED line that nothing downstream can read. 230 let ctr: *i64 = gv_ctr() 231 ctr[0] = pass 232 ctr[1] = pass + fail 233 let rc: i64 = gv_verdict("Q4K-FUSED-VS-X4-GATE" as *u8, ctr, 234 "fused integer GEMM beats the live x4 kernel and matches it bit-exactly" as *u8) 235 sys_exit(rc) 236 return rc 237}