code wiki / _hdl_build / nx_ppmi_lib.nx
nx_ppmi_lib.nx source
↩ module page · 220 lines · 8573 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"
22import "nx_vecmath.nx"
23
24// g-slots: 69 wid-scratch (lazily allocated ONCE -- a per-token sys_mmap in the maxsim hot loop
25// cost ~4M allocations per BEIR rerank run), 70 blob, 71 nv, 72 nt, 73 vh, 74 ridx, 75 nrm2,
26// 76 tctx, 77 tval, 78 loaded. CALLER CONTRACT: g must be a zeroed block of >= 632 bytes
27// (sys_mmap(1024)); a 64-byte g overruns the arena by 560 bytes and crashes when armed.
28func ppl_isqrt(v: i64) -> i64 { return vm_isqrt(v) }
29
30// Load a PPMI model from `path` into g. Returns 1 on success, 0 on any failure (fail-closed).
31// Path is a PARAMETER so callers can A/B different models (e.g. the general QA-prose model vs
32// a domain-augmented one) with identical code -- a hardcoded path made that impossible.
33func ppl_load(g: *i64, path: *u8) -> i64 {
34 g[78] = 0
35 let fd: i64 = sys_openat_rd(path)
36 if fd < 0 { return 0 }
37 let hdrb: *u8 = sys_mmap(4096)
38 var hgot: i64 = 0
39 var hr: i64 = 1
40 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 } } }
41 if hgot < 32 { sys_close(fd); return 0 }
42 let hh: *i64 = (hdrb as i64 + 8) as *i64
43 let hnv: i64 = hh[0]
44 let hnt: i64 = hh[1]
45 let need0: i64 = 32 + (hnv*8) + ((hnv+1)*8) + (hnv*8) + (hnt*8) + (hnt*8)
46 if need0 <= 32 { sys_close(fd); return 0 }
47 if need0 > 536870912 { sys_close(fd); return 0 }
48 let blob: *u8 = sys_mmap(need0 + 4096)
49 var i0: i64 = 0
50 while i0 < 32 { blob[i0] = hdrb[i0]; i0 = i0 + 1 }
51 var total: i64 = 32
52 var r: i64 = 1
53 while r > 0 {
54 let left: i64 = need0 - total
55 if left <= 0 { r = 0 } else {
56 var want: i64 = 262144
57 if want > left { want = left }
58 r = sys_read(fd, (blob as i64 + total) as *u8, want)
59 if r > 0 { total = total + r }
60 }
61 }
62 sys_close(fd)
63 if total < 64 { return 0 }
64 if blob[0] != (78 as u8) { return 0 }
65 if blob[6] != (49 as u8) { return 0 }
66 let hi: *i64 = (blob as i64 + 8) as *i64
67 let nv: i64 = hi[0]
68 let nt: i64 = hi[1]
69 let need: i64 = 32 + (nv*8) + ((nv+1)*8) + (nv*8) + (nt*8) + (nt*8)
70 if total < need { return 0 }
71 g[70] = blob as i64
72 g[71] = nv
73 g[72] = nt
74 var off: i64 = 32
75 g[73] = (blob as i64) + off; off = off + nv*8
76 g[74] = (blob as i64) + off; off = off + (nv+1)*8
77 g[75] = (blob as i64) + off; off = off + nv*8
78 g[76] = (blob as i64) + off; off = off + nt*8
79 g[77] = (blob as i64) + off
80 g[78] = 1
81 return 1
82}
83
84// PPMI cosine between two vocab rows (sorted sparse merge), permille.
85func ppl_cos(g: *i64, a: i64, b: i64) -> i64 {
86 let ridx: *i64 = g[74] as *i64
87 let nrm2: *i64 = g[75] as *i64
88 let tctx: *i64 = g[76] as *i64
89 let tval: *i64 = g[77] as *i64
90 var ia: i64 = ridx[a]
91 var ib: i64 = ridx[b]
92 let ea: i64 = ridx[a+1]
93 let eb: i64 = ridx[b+1]
94 var dot: i64 = 0
95 while ia < ea {
96 if ib >= eb { ia = ea } else {
97 if tctx[ia] == tctx[ib] { dot = dot + tval[ia]*tval[ib]; ia = ia + 1; ib = ib + 1 }
98 else { if tctx[ia] < tctx[ib] { ia = ia + 1 } else { ib = ib + 1 } }
99 }
100 }
101 if dot <= 0 { return 0 }
102 let d1: i64 = ppl_isqrt(nrm2[a])
103 let d2: i64 = ppl_isqrt(nrm2[b])
104 if d1 == 0 { return 0 }
105 if d2 == 0 { return 0 }
106 var cv: i64 = (dot*1000)/(d1*d2)
107 if cv > 1000 { cv = 1000 }
108 return cv
109}
110
111// cosine with identity + out-of-vocabulary guard (id -1 contributes nothing).
112func ppl_dcos(g: *i64, a: i64, b: i64) -> i64 {
113 if a < 0 { return 0 }
114 if b < 0 { return 0 }
115 if a == b { return 1000 }
116 return ppl_cos(g, a, b)
117}
118
119// Resolve buf[start,end) to a PPMI vocab id, or -1 if OOV. Lowercases into scratch first so
120// the hash matches the vocabulary as built (db_semhash is the ONLY correct hash here).
121func ppl_wid_range(g: *i64, buf: *u8, start: i64, end: i64) -> i64 {
122 let n: i64 = end - start
123 if n <= 0 { return 0 - 1 }
124 if n > 64 { return 0 - 1 }
125 if g[69] == 0 { g[69] = sys_mmap(80) as i64 }
126 let t: *u8 = g[69] as *u8
127 var i: i64 = 0
128 while i < n {
129 var c: i64 = buf[start + i] as i64
130 if c >= 65 { if c <= 90 { c = c + 32 } }
131 t[i] = c as u8
132 i = i + 1
133 }
134 return db_bsearch_i64(g[73] as *i64, g[71], db_semhash(t, 0, n))
135}
136
137// Tokenize text into PPMI vocab ids (-1 = OOV). Returns the token count.
138func ppl_tokenize_ids(g: *i64, buf: *u8, len: i64, out: *i64, maxn: i64) -> i64 {
139 var cnt: i64 = 0
140 var i: i64 = 0
141 var start: i64 = 0 - 1
142 while i < len {
143 let c: i64 = buf[i] as i64
144 var alnum: i64 = 0
145 if c >= 48 { if c <= 57 { alnum = 1 } }
146 if c >= 65 { if c <= 90 { alnum = 1 } }
147 if c >= 97 { if c <= 122 { alnum = 1 } }
148 if alnum == 1 {
149 if start < 0 { start = i }
150 } else {
151 if start >= 0 {
152 if cnt < maxn { out[cnt] = ppl_wid_range(g, buf, start, i); cnt = cnt + 1 }
153 start = 0 - 1
154 }
155 }
156 i = i + 1
157 }
158 if start >= 0 { if cnt < maxn { out[cnt] = ppl_wid_range(g, buf, start, len); cnt = cnt + 1 } }
159 return cnt
160}
161
162// Informativeness weight of a vocab row (an IDF proxy read straight off the model, so it needs
163// no document collection): ubiquitous function words occupy MANY contexts, discriminative words
164// few. Weight therefore DECREASES with row length. Out-of-vocabulary ids weigh 0 -- which also
165// removes them from the denominator instead of letting them drag a mean toward zero.
166func ppl_idf_w(g: *i64, a: i64) -> i64 {
167 if a < 0 { return 0 }
168 let ridx: *i64 = g[74] as *i64
169 let n: i64 = ridx[a+1] - ridx[a]
170 if n < 1 { return 0 }
171 return 1000000 / (100 + n)
172}
173
174// IDF-WEIGHTED late-interaction coverage. The flat mean below treats "the" exactly like
175// "personalization", so terms present in every candidate add a constant baseline that
176// COMPRESSES the gap the judge is measured on. This weights each term by informativeness, so
177// discriminative terms drive the score. Permille, same scale as ppl_maxsim.
178func ppl_maxsim_idf(g: *i64, a: *i64, na: i64, b: *i64, nb: i64) -> i64 {
179 if na < 1 { return 0 }
180 var num: i64 = 0
181 var den: i64 = 0
182 var i: i64 = 0
183 while i < na {
184 let w: i64 = ppl_idf_w(g, a[i])
185 if w > 0 {
186 var best: i64 = 0
187 var j: i64 = 0
188 while j < nb {
189 let c: i64 = ppl_dcos(g, a[i], b[j])
190 if c > best { best = c }
191 j = j + 1
192 }
193 num = num + best * w
194 den = den + w
195 }
196 i = i + 1
197 }
198 if den < 1 { return 0 }
199 return num / den
200}
201
202// Late-interaction coverage of `a` by `b`, permille: mean over a-terms of the MAX cosine
203// against any b-term. Exact match scores 1000.
204func ppl_maxsim(g: *i64, a: *i64, na: i64, b: *i64, nb: i64) -> i64 {
205 if na < 1 { return 0 }
206 var total: i64 = 0
207 var i: i64 = 0
208 while i < na {
209 var best: i64 = 0
210 var j: i64 = 0
211 while j < nb {
212 let c: i64 = ppl_dcos(g, a[i], b[j])
213 if c > best { best = c }
214 j = j + 1
215 }
216 total = total + best
217 i = i + 1
218 }
219 return total / na
220}