code wiki / _hdl_build / nx_embed_bench.nx
nx_embed_bench.nx source
↩ module page · 121 lines · 7244 B
1// nx_embed_bench.nx -- IN-PROCESS m-SWEEP: is prefill actually BATCHING?
2//
3// v1 measured cross-run and DISAGREED WITH ITSELF: 8tok=606ms/tok, 32tok=673, 125tok=403 (non-monotonic).
4// Two causes, both avoidable: it raced continuous sibling build load, and it re-loaded a 491MB model
5// every run (measured load variance alone 16554..20767ms, ~25%). nx_llm_forward_profile's own header
6// already warned this host is too noisy for cross-run attribution below 20% -- I hit a limitation the
7// ecosystem had already written down.
8// LAW: WHEN A MEASUREMENT DISAGREES WITH ITSELF ACROSS RUNS, FIX THE INSTRUMENT BEFORE INTERPRETING IT.
9//
10// v2 loads the model ONCE and sweeps m INSIDE one process, so load variance and cross-run drift cancel
11// and only the m-dependence remains. Two reps per m are printed so the reader SEES the noise instead of
12// trusting one sample.
13// FLAT ms_per_token across m => prefill is NOT batching (wire the m>1 packed path in lazy_matmul)
14// FALLING ms_per_token => batching works; the cost lives elsewhere
15// Compare against the m=1 DECODE cost of 350ms/token from nx_llm_forward_profile.
16// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
17import "nx_syscalls.nx"
18import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
19import "nx_tier.nx"
20import "nx_bpe.nx"
21import "nx_gguf.nx"
22import "nx_gguf_load.nx"
23import "nx_gguf_meta.nx"
24import "nx_f32_llm_read_dims.nx"
25import "nx_f32_bpe_load.nx"
26import "nx_f32.nx"
27import "nx_f32_llm_v4.nx"
28
29const EB_MAXTOK: i64 = 1024
30const EB_KV_SLOTS: i64 = 1024
31const EB_EPS: i64 = 0x358637BD
32const EB_ATTN_SCALE: i64 = 0x3E000000
33const EB_ROPE_BASE: i64 = 0x415D0EAB
34const EB_VOCAB_BYTES: i64 = 67108864
35const EB_VOCAB_IDS: i64 = 262144
36const EB_VOCAB_MERGES: i64 = 524288
37const EB_NPOINTS: i64 = 5
38const EB_REPS: i64 = 2
39
40func eb_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
41// MIGRATED to nxi_out (debt 1785563586). The old body mmapped 32 bytes per call and never freed it,
42// AND issued one sys_write PER DIGIT. Observed holding 4.5GB on this host on 2026-08-01. A bench
43// is the worst possible home for a per-call leak: its whole purpose is millions of iterations.
44func eb_n(v: i64) -> i64 { nxi_out(v); return 0 }
45func eb_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
46
47func main(argc: i64, argv: *i64) -> i64 {
48 // ---- ARM 1 OF THE ROOFLINE TEST (2026-08-01): force STREAMING 4-bit weights ---------------------
49 // The banked roofline (f32 221ms/tok vs int4 27ms/tok) assumes the hot loop READS 4-BIT WEIGHTS. This
50 // stack does not: _lw_try_fill dequants a Q4_K tensor ONCE into a PACKED 4-BYTE F32 cache (~7x the
51 // Q4_K bytes) whenever it fits budget, so on every model small enough to fit -- including this one --
52 // decode reads f32 and the int4 number describes a path nobody takes. Setting the budget to 0 forces
53 // pk_state=-1 on every weight, i.e. the pooled STREAMING Q4_K path, which is the configuration the
54 // roofline actually describes. Comparing this sweep against the cached baseline is therefore a DIRECT
55 // test of whether bytes-moved or dequant-compute dominates on this host -- the question three separate
56 // attempts have now argued about from theory. Measured, not asserted.
57 nx_lw_set_cache_budget(0)
58
59 var path: *u8 = "/volume1/homes/elderwesto/nx_bench/model/nx_real_model.gguf" as *u8
60 if argc > 1 { path = argv[1] as *u8 }
61
62 let t0: i64 = sys_now_ms()
63 let len_out: *i64 = sys_mmap(8) as *i64
64 let buf: *u8 = sys_read_file(path, len_out)
65 if buf == (0 as *u8) { eb_w("{\"refused\":\"no model\"}\n" as *u8); sys_exit(2); return 2 }
66 let hdr: *NxGgufHeader = sys_mmap(NX_GGUF_HDR_BYTES) as *NxGgufHeader
67 if nx_gguf_parse(buf, len_out[0], hdr) != NX_GGUF_OK { eb_w("{\"refused\":\"parse\"}\n" as *u8); sys_exit(2); return 2 }
68 let model: *NxF32LlamaModel = nx_f32_llama_model_alloc()
69 let oe: *i64 = sys_mmap(8) as *i64
70 if nx_f32_llm_read_dims_from_gguf(buf, len_out[0], hdr, model, oe) != NX_FLD_OK { eb_w("{\"refused\":\"dims\"}\n" as *u8); sys_exit(2); return 2 }
71 if nx_f32_llm_load_weights_v4_from_gguf(buf, hdr, model, oe) != NX_FLV4_OK { eb_w("{\"refused\":\"weights\"}\n" as *u8); sys_exit(2); return 2 }
72 let vocab: *NxBpeVocab = nx_bpe_vocab_new(EB_VOCAB_BYTES, EB_VOCAB_IDS, EB_VOCAB_MERGES)
73 let nt: *i64 = sys_mmap(8) as *i64
74 let nm: *i64 = sys_mmap(8) as *i64
75 if nx_f32_bpe_load_from_gguf(buf, len_out[0], hdr, vocab, nt, nm, oe) != NX_FBL_OK { eb_w("{\"refused\":\"bpe\"}\n" as *u8); sys_exit(2); return 2 }
76 let t1: i64 = sys_now_ms()
77
78 let doc: *u8 = "Dietary flavonoid intake and risk of cardiovascular disease. Flavonoids are polyphenolic compounds found in fruits vegetables tea and red wine. Epidemiological studies suggest an inverse association between flavonoid consumption and coronary heart disease mortality. We conducted a prospective cohort study of 34489 postmenopausal women followed for 16 years. Intake was assessed with a validated food frequency questionnaire. After multivariable adjustment for age smoking body mass index and other risk factors higher total flavonoid intake was associated with reduced cardiovascular mortality. These findings support a protective role for dietary flavonoids." as *u8
79 let toks: *i64 = sys_mmap(EB_MAXTOK * 8) as *i64
80 let n: nx_int = nx_bpe_encode_bytelevel(vocab, doc, eb_slen(doc) as nx_int, toks)
81 let hd: i64 = model.hidden_dim
82 let vec: *i64 = sys_mmap(hd * 8) as *i64
83
84 eb_w("{\"organ\":\"nx_embed_bench\",\"instrument\":\"in-process m-sweep, model loaded ONCE\",\"hidden_dim\":" as *u8); eb_n(hd)
85 eb_w(",\"model_load_ms\":" as *u8); eb_n(t1 - t0)
86 eb_w(",\"tokens_available\":" as *u8); eb_n(n as i64)
87 eb_w(",\"sweep\":[" as *u8)
88
89 let ms: *i64 = sys_mmap(64) as *i64
90 ms[0] = 8
91 ms[1] = 16
92 ms[2] = 32
93 ms[3] = 64
94 ms[4] = 125
95 var first: i64 = 1
96 var si: i64 = 0
97 while si < EB_NPOINTS {
98 var mm: i64 = ms[si]
99 if mm > (n as i64) { mm = n as i64 }
100 var rep: i64 = 0
101 while rep < EB_REPS {
102 let a0: i64 = sys_now_ms()
103 let cache: *NxF32KVCache = nx_f32_kv_cache_alloc(model.n_layers, model.n_kv_heads, EB_KV_SLOTS, model.head_dim)
104 let rv: nx_int = nx_f32_llm_embed_v4(model, toks, mm as nx_int, cache, EB_EPS, EB_ATTN_SCALE, EB_ROPE_BASE, 1, vec)
105 let a1: i64 = sys_now_ms()
106 if first == 0 { eb_w("," as *u8) }
107 first = 0
108 eb_w("{\"m\":" as *u8); eb_n(mm)
109 eb_w(",\"rep\":" as *u8); eb_n(rep)
110 if rv != NX_FLV4_OK { eb_w(",\"embed_rc\":" as *u8); eb_n(rv as i64); eb_w("}" as *u8) } else {
111 eb_w(",\"total_ms\":" as *u8); eb_n(a1 - a0)
112 eb_w(",\"ms_per_token\":" as *u8); eb_n((a1 - a0) / mm)
113 eb_w("}" as *u8)
114 }
115 rep = rep + 1
116 }
117 si = si + 1
118 }
119 eb_w("],\"read\":\"FLAT ms_per_token across m => prefill is NOT batching (wire the m>1 packed path). FALLING => batching works, cost is elsewhere. m=1 decode reference = 350ms/token.\"}\n" as *u8)
120 return 0
121}