code wiki / _hdl_build / nx_ltr_lib_gate.nx

nx_ltr_lib_gate.nx source

↩ module page · 116 lines · 5683 B

1// nx_ltr_lib_gate.nx -- teeth for the learning-to-rank reranker (nx_ltr_lib.ltr_cv) on a synthetic 2// fixture it builds itself (search rung R0, 2026-09-14). A SIGNAL fixture where one feature predicts 3// the sparse relevant candidates -- the trained arm's 5-fold held-out nDCG must be high; a SHUFFLE 4// neg-control where gold is decorrelated from every feature -- with 20 candidates and only 3 relevant 5// a random ordering pushes them out of the top-10, so it must score LOW (the model may not hallucinate 6// signal); determinism (integer, so the same fixture twice is the same number). No corpus, no fork. 7// Declared in organ_gate.conf. 8 9import "nx_ltr_lib.nx" 10import "nx_gate_verdict.nx" 11 12const GL_NQ: i64 = 20 13const GL_NC: i64 = 20 // 20 candidates, only 3 relevant -> a bad ordering loses them past nDCG@10 14const GL_BOX: i64 = 8 15 16// deterministic small pseudo-noise in 0..1023, uncorrelated with the signal below 17func gl_noise(q: i64, c: i64, k: i64) -> i64 { 18 var h: i64 = (q * 131 + c * 17 + k * 1315423911) & 0x7fffffff 19 h = (h * 2654435761) & 0x3ff 20 return h 21} 22 23// idcg@10 for a query's gold row (graded), from a scratch descending sort 24func gl_idcg(gold: *i64, base: i64, nc: i64) -> i64 { 25 let s: *i64 = sys_mmap(nc * 8) as *i64 26 var i: i64 = 0 27 while i < nc { s[i] = gold[base + i]; i = i + 1 } 28 i = 1 29 while i < nc { 30 let cur: i64 = s[i] 31 var j: i64 = i - 1 32 var go: i64 = 1 33 while go == 1 { if j >= 0 { if s[j] < cur { s[j + 1] = s[j]; j = j - 1 } else { go = 0 } } else { go = 0 } } 34 s[j + 1] = cur 35 i = i + 1 36 } 37 var idcg: i64 = 0 38 var r: i64 = 0 39 while r < LTR_TOPK { if r < nc { idcg = idcg + s[r] * ltr_disc(r + 1) } r = r + 1 } 40 sys_munmap(s as *u8, nc * 8) 41 return idcg 42} 43 44func gl_mod(a: i64, m: i64) -> i64 { return a - (a / m) * m } 45 46// build a fixture. mode 0 = SIGNAL (feature 2 high exactly on the relevant candidates), mode 1 = 47// SHUFFLE (feature 2 high on three positions, but the relevant candidates are elsewhere, so no 48// feature tracks relevance). Three relevant per query, gold 3/2/1; every other candidate gold 0. 49func gl_build(feat: *i64, gold: *i64, ncand: *i64, idcg: *i64, fold: *i64, mode: i64) -> i64 { 50 var q: i64 = 0 51 while q < GL_NQ { 52 ncand[q] = GL_NC 53 fold[q] = gl_mod(q, LTR_FOLDS) 54 var c: i64 = 0 55 while c < GL_NC { 56 let base: i64 = (q * GL_NC + c) * LTR_NFEAT 57 var k: i64 = 0 58 while k < LTR_NFEAT { feat[base + k] = gl_noise(q, c, k); k = k + 1 } 59 gold[q * GL_NC + c] = 0 60 c = c + 1 61 } 62 // feature-2 is high on p1,p2,p3 in BOTH modes (so the feature carries the same information) 63 let p1: i64 = gl_mod(q, GL_NC) 64 let p2: i64 = gl_mod(q + 7, GL_NC) 65 let p3: i64 = gl_mod(q + 13, GL_NC) 66 feat[(q * GL_NC + p1) * LTR_NFEAT + 2] = 5000 67 feat[(q * GL_NC + p2) * LTR_NFEAT + 2] = 4000 68 feat[(q * GL_NC + p3) * LTR_NFEAT + 2] = 3000 69 if mode == 0 { 70 // SIGNAL: relevance sits exactly where feature 2 is high 71 gold[q * GL_NC + p1] = 3 72 gold[q * GL_NC + p2] = 2 73 gold[q * GL_NC + p3] = 1 74 } else { 75 // SHUFFLE: relevance sits elsewhere; the high-feature-2 positions are gold 0 76 gold[q * GL_NC + gl_mod(q + 3, GL_NC)] = 3 77 gold[q * GL_NC + gl_mod(q + 11, GL_NC)] = 2 78 gold[q * GL_NC + gl_mod(q + 17, GL_NC)] = 1 79 } 80 idcg[q] = gl_idcg(gold, q * GL_NC, GL_NC) 81 q = q + 1 82 } 83 return 0 84} 85 86func main() -> i64 { 87 gv_puts("=== nx_ltr_lib gate (coordinate-ascent LTR, 5-fold CV, sparse signal/shuffle over 20 candidates) ===\n" as *u8) 88 let ctr: *i64 = gv_ctr() 89 let feat: *i64 = sys_mmap(GL_NQ * GL_NC * LTR_NFEAT * 8) as *i64 90 let gold: *i64 = sys_mmap(GL_NQ * GL_NC * 8) as *i64 91 let ncand: *i64 = sys_mmap(GL_NQ * 8) as *i64 92 let idcg: *i64 = sys_mmap(GL_NQ * 8) as *i64 93 let fold: *i64 = sys_mmap(GL_NQ * 8) as *i64 94 let cnt: *i64 = sys_mmap(GL_BOX) as *i64 95 96 gl_build(feat, gold, ncand, idcg, fold, 0) 97 let cv_signal: i64 = ltr_cv(feat, gold, fold, GL_NQ, ncand, GL_NC, idcg, cnt) 98 let scored: i64 = cnt[0] 99 gv_check("T1 fixture-reached: every query is scored once across the folds" as *u8, (scored == GL_NQ) as i64, ctr) 100 gv_check("T2 the trained arm learns the signal: 5-fold held-out nDCG is high (>800 permil)" as *u8, (cv_signal > 800) as i64, ctr) 101 102 let cv_signal2: i64 = ltr_cv(feat, gold, fold, GL_NQ, ncand, GL_NC, idcg, cnt) 103 gv_check("T3 determinism: the same fixture yields the identical CV number" as *u8, (cv_signal2 == cv_signal) as i64, ctr) 104 105 gl_build(feat, gold, ncand, idcg, fold, 1) 106 let cv_shuffle: i64 = ltr_cv(feat, gold, fold, GL_NQ, ncand, GL_NC, idcg, cnt) 107 gv_check("T4 neg-control-shuffled-labels: gold decorrelated from every feature scores LOW (<450 permil)" as *u8, (cv_shuffle < 450) as i64, ctr) 108 gv_check("T5 neg-control-margin: the signal fixture beats the shuffle by a wide margin (>400 permil)" as *u8, (cv_signal - cv_shuffle > 400) as i64, ctr) 109 110 gv_values_head() 111 gv_kv("cv_signal_permil" as *u8, cv_signal) 112 gv_kv("cv_signal_permil_run2" as *u8, cv_signal2) 113 gv_kv("cv_shuffle_permil" as *u8, cv_shuffle) 114 gv_kv("queries_scored" as *u8, scored) 115 return gv_verdict("NX-LTR-LIB-GATE" as *u8, ctr, "a coordinate-ascent reranker (Metzler-Croft, initialised at bm25-only) evaluated by 5-fold cross-validation learns a sparse signal feature to a high held-out nDCG, refuses to score shuffled labels above the random-ordering floor, and is bit-deterministic" as *u8) 116}