code wiki / _hdl_build / nx_ppmi_lib.nx

nx_ppmi_lib.nx source

↩ module page · 221 lines · 8350 B

1// nx_ppmi_lib.nx -- THE canonical PPMI model library (eats debt seq283). 2// The loader + sparse-merge cosine over knowledge/index/semppmi_v1.bin had been hand-rolled 3// twice: pb_* inside nx_recall_dense.nx (the recall lane's reranker) and sj_* inside 4// nx_dr_semjudge.nx (the deep-research judge). Two copies of the same numeric code silently 5// diverging would change similarity scores without any gate noticing -- exactly the drift 6// class D001 was raised for. This is the ONE copy both should import. 7// 8// MIGRATION STATE (honest): nx_dr_semjudge migrates onto this lib now. nx_recall_dense is a 9// LIVE organ owned by the recall lane and is NOT edited here -- it migrates on its owner's 10// next touch (D001 migrate-on-touch), at which point seq283 fully closes. 11// 12// Model format (NXPPMI1): 32B header [magic, nv, nt] then vocab-hash[nv], row-index[nv+1], 13// norm^2[nv], ctx-id[nt], value[nt]. Vocabulary is keyed by db_semhash -- resolving a word 14// with any other hash silently yields -1 and zeroes every similarity. 15// Every func <=6 params (NAS nx_cc >6-arg skew, seq239). No hardware writes (Rule 26). 16// 17// module: nishi-core.index.ppmi_lib 18// depends: nx_qabench_engine.nx (db_semhash/db_bsearch_i64), nx_syscalls.nx 19// genealogy_id: levy_goldberg_2014_ppmi 20import "nx_qabench_engine.nx" 21import "nx_syscalls.nx" 22 23// g-slots: 70 blob, 71 nv, 72 nt, 73 vh, 74 ridx, 75 nrm2, 76 tctx, 77 tval, 78 loaded 24func ppl_isqrt(v: i64) -> i64 { 25 if v <= 0 { return 0 } 26 var x: i64 = v 27 var y: i64 = (x + 1) / 2 28 while y < x { x = y; let q: i64 = v / x; y = (x + q) / 2 } 29 return x 30} 31 32// Load a PPMI model from `path` into g. Returns 1 on success, 0 on any failure (fail-closed). 33// Path is a PARAMETER so callers can A/B different models (e.g. the general QA-prose model vs 34// a domain-augmented one) with identical code -- a hardcoded path made that impossible. 35func ppl_load(g: *i64, path: *u8) -> i64 { 36 g[78] = 0 37 let fd: i64 = sys_openat_rd(path) 38 if fd < 0 { return 0 } 39 let hdrb: *u8 = sys_mmap(4096) 40 var hgot: i64 = 0 41 var hr: i64 = 1 42 while hr > 0 { if hgot >= 32 { hr = 0 } else { hr = sys_read(fd, (hdrb as i64 + hgot) as *u8, 32 - hgot); if hr > 0 { hgot = hgot + hr } } } 43 if hgot < 32 { sys_close(fd); return 0 } 44 let hh: *i64 = (hdrb as i64 + 8) as *i64 45 let hnv: i64 = hh[0] 46 let hnt: i64 = hh[1] 47 let need0: i64 = 32 + (hnv*8) + ((hnv+1)*8) + (hnv*8) + (hnt*8) + (hnt*8) 48 if need0 <= 32 { sys_close(fd); return 0 } 49 if need0 > 536870912 { sys_close(fd); return 0 } 50 let blob: *u8 = sys_mmap(need0 + 4096) 51 var i0: i64 = 0 52 while i0 < 32 { blob[i0] = hdrb[i0]; i0 = i0 + 1 } 53 var total: i64 = 32 54 var r: i64 = 1 55 while r > 0 { 56 let left: i64 = need0 - total 57 if left <= 0 { r = 0 } else { 58 var want: i64 = 262144 59 if want > left { want = left } 60 r = sys_read(fd, (blob as i64 + total) as *u8, want) 61 if r > 0 { total = total + r } 62 } 63 } 64 sys_close(fd) 65 if total < 64 { return 0 } 66 if blob[0] != (78 as u8) { return 0 } 67 if blob[6] != (49 as u8) { return 0 } 68 let hi: *i64 = (blob as i64 + 8) as *i64 69 let nv: i64 = hi[0] 70 let nt: i64 = hi[1] 71 let need: i64 = 32 + (nv*8) + ((nv+1)*8) + (nv*8) + (nt*8) + (nt*8) 72 if total < need { return 0 } 73 g[70] = blob as i64 74 g[71] = nv 75 g[72] = nt 76 var off: i64 = 32 77 g[73] = (blob as i64) + off; off = off + nv*8 78 g[74] = (blob as i64) + off; off = off + (nv+1)*8 79 g[75] = (blob as i64) + off; off = off + nv*8 80 g[76] = (blob as i64) + off; off = off + nt*8 81 g[77] = (blob as i64) + off 82 g[78] = 1 83 return 1 84} 85 86// PPMI cosine between two vocab rows (sorted sparse merge), permille. 87func ppl_cos(g: *i64, a: i64, b: i64) -> i64 { 88 let ridx: *i64 = g[74] as *i64 89 let nrm2: *i64 = g[75] as *i64 90 let tctx: *i64 = g[76] as *i64 91 let tval: *i64 = g[77] as *i64 92 var ia: i64 = ridx[a] 93 var ib: i64 = ridx[b] 94 let ea: i64 = ridx[a+1] 95 let eb: i64 = ridx[b+1] 96 var dot: i64 = 0 97 while ia < ea { 98 if ib >= eb { ia = ea } else { 99 if tctx[ia] == tctx[ib] { dot = dot + tval[ia]*tval[ib]; ia = ia + 1; ib = ib + 1 } 100 else { if tctx[ia] < tctx[ib] { ia = ia + 1 } else { ib = ib + 1 } } 101 } 102 } 103 if dot <= 0 { return 0 } 104 let d1: i64 = ppl_isqrt(nrm2[a]) 105 let d2: i64 = ppl_isqrt(nrm2[b]) 106 if d1 == 0 { return 0 } 107 if d2 == 0 { return 0 } 108 var cv: i64 = (dot*1000)/(d1*d2) 109 if cv > 1000 { cv = 1000 } 110 return cv 111} 112 113// cosine with identity + out-of-vocabulary guard (id -1 contributes nothing). 114func ppl_dcos(g: *i64, a: i64, b: i64) -> i64 { 115 if a < 0 { return 0 } 116 if b < 0 { return 0 } 117 if a == b { return 1000 } 118 return ppl_cos(g, a, b) 119} 120 121// Resolve buf[start,end) to a PPMI vocab id, or -1 if OOV. Lowercases into scratch first so 122// the hash matches the vocabulary as built (db_semhash is the ONLY correct hash here). 123func ppl_wid_range(g: *i64, buf: *u8, start: i64, end: i64) -> i64 { 124 let n: i64 = end - start 125 if n <= 0 { return 0 - 1 } 126 if n > 64 { return 0 - 1 } 127 let t: *u8 = sys_mmap(80) 128 var i: i64 = 0 129 while i < n { 130 var c: i64 = buf[start + i] as i64 131 if c >= 65 { if c <= 90 { c = c + 32 } } 132 t[i] = c as u8 133 i = i + 1 134 } 135 return db_bsearch_i64(g[73] as *i64, g[71], db_semhash(t, 0, n)) 136} 137 138// Tokenize text into PPMI vocab ids (-1 = OOV). Returns the token count. 139func ppl_tokenize_ids(g: *i64, buf: *u8, len: i64, out: *i64, maxn: i64) -> i64 { 140 var cnt: i64 = 0 141 var i: i64 = 0 142 var start: i64 = 0 - 1 143 while i < len { 144 let c: i64 = buf[i] as i64 145 var alnum: i64 = 0 146 if c >= 48 { if c <= 57 { alnum = 1 } } 147 if c >= 65 { if c <= 90 { alnum = 1 } } 148 if c >= 97 { if c <= 122 { alnum = 1 } } 149 if alnum == 1 { 150 if start < 0 { start = i } 151 } else { 152 if start >= 0 { 153 if cnt < maxn { out[cnt] = ppl_wid_range(g, buf, start, i); cnt = cnt + 1 } 154 start = 0 - 1 155 } 156 } 157 i = i + 1 158 } 159 if start >= 0 { if cnt < maxn { out[cnt] = ppl_wid_range(g, buf, start, len); cnt = cnt + 1 } } 160 return cnt 161} 162 163// Informativeness weight of a vocab row (an IDF proxy read straight off the model, so it needs 164// no document collection): ubiquitous function words occupy MANY contexts, discriminative words 165// few. Weight therefore DECREASES with row length. Out-of-vocabulary ids weigh 0 -- which also 166// removes them from the denominator instead of letting them drag a mean toward zero. 167func ppl_idf_w(g: *i64, a: i64) -> i64 { 168 if a < 0 { return 0 } 169 let ridx: *i64 = g[74] as *i64 170 let n: i64 = ridx[a+1] - ridx[a] 171 if n < 1 { return 0 } 172 return 1000000 / (100 + n) 173} 174 175// IDF-WEIGHTED late-interaction coverage. The flat mean below treats "the" exactly like 176// "personalization", so terms present in every candidate add a constant baseline that 177// COMPRESSES the gap the judge is measured on. This weights each term by informativeness, so 178// discriminative terms drive the score. Permille, same scale as ppl_maxsim. 179func ppl_maxsim_idf(g: *i64, a: *i64, na: i64, b: *i64, nb: i64) -> i64 { 180 if na < 1 { return 0 } 181 var num: i64 = 0 182 var den: i64 = 0 183 var i: i64 = 0 184 while i < na { 185 let w: i64 = ppl_idf_w(g, a[i]) 186 if w > 0 { 187 var best: i64 = 0 188 var j: i64 = 0 189 while j < nb { 190 let c: i64 = ppl_dcos(g, a[i], b[j]) 191 if c > best { best = c } 192 j = j + 1 193 } 194 num = num + best * w 195 den = den + w 196 } 197 i = i + 1 198 } 199 if den < 1 { return 0 } 200 return num / den 201} 202 203// Late-interaction coverage of `a` by `b`, permille: mean over a-terms of the MAX cosine 204// against any b-term. Exact match scores 1000. 205func ppl_maxsim(g: *i64, a: *i64, na: i64, b: *i64, nb: i64) -> i64 { 206 if na < 1 { return 0 } 207 var total: i64 = 0 208 var i: i64 = 0 209 while i < na { 210 var best: i64 = 0 211 var j: i64 = 0 212 while j < nb { 213 let c: i64 = ppl_dcos(g, a[i], b[j]) 214 if c > best { best = c } 215 j = j + 1 216 } 217 total = total + best 218 i = i + 1 219 } 220 return total / na 221}