code wiki / (root) / nx_softfloat_cost_kat.nx

nx_softfloat_cost_kat.nx source

↩ module page · 106 lines · 4956 B

1// nx_softfloat_cost_kat.nx -- WHAT DOES SOFTWARE FLOAT ACTUALLY COST US? The most basic number in a 2// no-float ecosystem, and nobody had measured it. 3// 4// WHY (2026-07-31): the perf lane converged on 'the inner loop is compute-bound on emulated float', but 5// that was INFERRED, not measured. The chain: nx_batchscale_kat showed the matmul FLAT across m (1.12x); 6// nx_matmul_tile_kat then read the weights 8x fewer times and gained 1.02x -- proving memory traffic was 7// NOT the constraint; and the implied MAC rate was ~164 MFLOP/s, absurd for a modern core. Meanwhile 8// nx_lw_cache_gate measured the Q4_K packed path at 4431 mflops on the SAME host -- ~27x faster, computing 9// on INTEGER MANTISSAS. All of that POINTS at software float as the tax. None of it MEASURES it. 10// 11// THIS DOES: identical loop shape, identical trip count, identical accumulate-and-consume discipline -- 12// one arm in emulated f32 (__f32_mul/__f32_add), one in native i64. The ratio IS the no-float tax per MAC. 13// 14// WHY BOTH ACCUMULATORS ARE PRINTED: a dead-code eliminator that dropped an unused result would make the 15// integer arm look infinitely fast and produce a beautiful, meaningless number. Both accumulators are 16// consumed by the output, and T3/T4 assert them -- a zero or wrong accumulator means the loop did not run 17// and the timing is a lie. A benchmark that cannot prove its own loop executed is not a benchmark. 18// 19// WHAT THE ANSWER CHANGES: if the tax is large then quantisation is not primarily a BYTES decision, it is 20// an ARITHMETIC decision -- integer/mantissa MACs skip the emulation entirely. Same action, different 21// reason, and the reason decides what gets built next. 22// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 23import "nx_f32.nx" 24import "nx_f32_cvt.nx" 25import "nx_fmt.nx" 26 27const SF_N: i64 = 2000000 28 29func sf_nl() -> i64 { fmt_puts("\n" as *u8); return 0 } 30 31func sf_t(name: *u8, cond: i64, ctr: *i64) { 32 if cond == 1 { fmt_puts(" ok " as *u8); ctr[0] = ctr[0] + 1 } 33 else { fmt_puts(" FAIL " as *u8) } 34 fmt_puts(name) 35 sf_nl() 36 ctr[1] = ctr[1] + 1 37} 38 39func main() -> i64 { 40 var ctr: *i64 = sys_mmap(64) as *i64 41 ctr[0] = 0 42 ctr[1] = 0 43 44 fmt_puts("=== nx_softfloat_cost_kat -- the no-float tax, measured per MAC ===" as *u8); sf_nl() 45 fmt_puts("loop trips = " as *u8); fmt_putn(SF_N); sf_nl() 46 47 let fa: i64 = nx_i32_to_f32(3) 48 let fb: i64 = nx_i32_to_f32(7) 49 50 var facc: i64 = nx_i32_to_f32(0) 51 let t0: i64 = sys_now_us() 52 var i: i64 = 0 53 while i < SF_N { 54 facc = __f32_add(facc, __f32_mul(fa, fb)) 55 i = i + 1 56 } 57 let us_f32: i64 = sys_now_us() - t0 58 59 var iacc: i64 = 0 60 let ia: i64 = 3 61 let ib: i64 = 7 62 let t1: i64 = sys_now_us() 63 var j: i64 = 0 64 while j < SF_N { 65 iacc = iacc + ia * ib 66 j = j + 1 67 } 68 let us_int: i64 = sys_now_us() - t1 69 70 fmt_puts(" f32_acc(bits)=" as *u8); fmt_putn(facc) 71 fmt_puts(" int_acc=" as *u8); fmt_putn(iacc); sf_nl() 72 73 var uf: i64 = us_f32 74 if uf < 1 { uf = 1 } 75 var ui: i64 = us_int 76 if ui < 1 { ui = 1 } 77 78 fmt_puts(" emulated_f32_us=" as *u8); fmt_putn(us_f32) 79 fmt_puts(" MACs_per_us=" as *u8); fmt_putn(SF_N / uf); sf_nl() 80 fmt_puts(" native_i64_us =" as *u8); fmt_putn(us_int) 81 fmt_puts(" MACs_per_us=" as *u8); fmt_putn(SF_N / ui); sf_nl() 82 83 let tax_x100: i64 = us_f32 * 100 / ui 84 fmt_puts(" ** NO-FLOAT TAX = " as *u8); fmt_putn(tax_x100) 85 fmt_puts(" x100 (emulated f32 MAC vs native integer MAC) **" as *u8); sf_nl() 86 87 sf_t("T1 f32 arm timed and non-zero" as *u8, us_f32 > 0, ctr) 88 sf_t("T2 integer arm timed and non-zero" as *u8, us_int > 0, ctr) 89 sf_t("T3 NON-VACUITY: f32 accumulator non-zero (its loop was not eliminated)" as *u8, facc != 0, ctr) 90 sf_t("T4 NON-VACUITY: integer accumulator is EXACTLY 3*7*N (its loop really ran)" as *u8, 91 iacc == 21 * SF_N, ctr) 92 sf_t("T5 the tax is REAL, not noise (emulated f32 at least 2x integer)" as *u8, tax_x100 >= 200, ctr) 93 94 sf_nl() 95 fmt_puts("READ BEFORE CHOOSING A NUMERIC FORMAT: a large tax means quantisation is an ARITHMETIC" as *u8); sf_nl() 96 fmt_puts(" decision, not a bytes decision -- integer/mantissa MACs skip emulation entirely. Measured" as *u8); sf_nl() 97 fmt_puts(" corroboration on this host: Q4_K packed 4431 mflops vs 33 streaming (nx_lw_cache_gate)," as *u8); sf_nl() 98 fmt_puts(" while the f32 matmul implies only ~164 MFLOP/s (nx_matmul_tile_kat)." as *u8); sf_nl() 99 fmt_puts("envelope: tight synthetic loop, constant operands -- measures the OP, not a kernel's cache" as *u8); sf_nl() 100 fmt_puts(" behaviour. One run on a shared host; one window is not a rate." as *u8); sf_nl() 101 102 fmt_puts("SOFTFLOAT-COST-KAT " as *u8); fmt_putn(ctr[0]); fmt_puts("/" as *u8); fmt_putn(ctr[1]) 103 if ctr[0] == ctr[1] { fmt_puts(" GREEN" as *u8); sf_nl(); return 0 } 104 fmt_puts(" RED" as *u8); sf_nl() 105 return 1 106}