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}