code wiki / _hdl_build / nx_pagerank_build.nx
nx_pagerank_build.nx source
↩ module page · 213 lines · 11307 B
1// nx_pagerank_build.nx -- P1 STEP 2: build the web LINK GRAPH from out:<cid> edge rows (captured by
2// nx_web_crawl_step's outlink capture) and compute PageRank authority, storing pr:<cid> priors back into the
3// web shard. Node identity = cid(url) (the frontier/outlink hash) -> an out: key's cid is the source node,
4// its packed-i64 value are the target nodes. Interns cids to dense indices, runs pr_compute (nx_pagerank),
5// stores pr:<cid> = rank. Serve-time authority (step 3) = content_cid -> url:<cid> -> ci_hash(url) -> pr:<cid>.
6// usage: nx_pagerank_build [domain=web] [iters=100]
7// license_tier: ORIGINAL
8import "nx_corpus_ingest.nx" // dss_prefix / seg_store (ss_open/ss_hget/ss_begin/ss_add/ss_commit/ss_r32) / ci_hash
9import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
10import "nx_pagerank.nx" // pr_compute / pr_argmax / pr_sum / PR_SCALE
11const PB_MAGIC_16777216: i64 = 16777216
12const PB_MAGIC_1000000: i64 = 1000000
13
14// SCALE RUNG (2026-07-23): linear pb_intern + the 64B-string key dedup were O(N^2)/O(K^2) -- fine at the
15// 1925-node crawl graph, fatal at bulk-WARC scale (one CC file ~= millions of target nodes). Interning is
16// now an open-addressing hash table; source-key dedup is a hash SET on the source cid (same identity).
17const PB_MAXNODE: i64 = 16777216 // distinct nodes per run (16M; one ~40k-page WARC links ~1-4M targets)
18const PB_MAXEDGE: i64 = 33554432 // directed edges per run (32M)
19const PB_HSIZE: i64 = 33554432 // hash slots = 2^25 (2x MAXNODE load-factor headroom)
20const PB_HSHIFT: i64 = 38 // take the TOP 25 bits of the 63-bit mixed hash (63 - 25)
21const PB_HMUL: i64 = 7046029254386353131 // odd 63-bit multiplicative-mix constant (Fibonacci-hash family)
22
23func pb_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
24// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
25// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
26// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
27// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
28func pb_num(v: i64) -> i64 { nxi_out(v); return 0 }
29// cid out of an "out:<cid>" key (digits after "out:")
30func pb_key_cid(k: *u8, kl: i64) -> i64 {
31 var v: i64 = 0; var i: i64 = 4
32 while i < kl { let c: i64 = k[i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 }
33 return v
34}
35// build "pr:<cid>" key (the authority prior row). Returns length.
36func pb_prkey(cid: i64, out: *u8) -> i64 {
37 out[0] = 112 as u8 // p
38 out[1] = 114 as u8 // r
39 out[2] = 58 as u8 // :
40 var m: i64 = cid
41 let t: *u8 = sys_mmap(28)
42 var k: i64 = 0
43 if m == 0 { t[0] = 48 as u8; k = 1 }
44 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
45 var i: i64 = 0
46 while i < k { out[3 + i] = t[k - 1 - i]; i = i + 1 }
47 out[3 + k] = 0 as u8
48 return 3 + k
49}
50// slot for a cid in a PB_HSIZE open-addressing table (cid is always >0 per ci_hash; 0 = empty slot)
51func pb_hslot(cid: i64) -> i64 {
52 var s: i64 = cid * PB_HMUL
53 s = s & 0x7fffffffffffffff
54 return (s >> PB_HSHIFT) & (PB_HSIZE - 1)
55}
56// intern cid -> dense node index via the hash table (O(1) expected). cidtab[slot]=cid, idxtab[slot]=index.
57// Returns the index, or -1 when the node cap is hit (caller counts + skips -- NEVER silent).
58func pb_intern(cid: i64, cidtab: *i64, idxtab: *i64, node: *i64, nn: *i64) -> i64 {
59 var slot: i64 = pb_hslot(cid)
60 var probes: i64 = 0
61 while probes < PB_HSIZE {
62 if cidtab[slot] == cid { return idxtab[slot] }
63 if cidtab[slot] == 0 {
64 let idx: i64 = nn[0]
65 if idx >= PB_MAXNODE { return 0 - 1 }
66 cidtab[slot] = cid
67 idxtab[slot] = idx
68 node[idx] = cid
69 nn[0] = idx + 1
70 return idx
71 }
72 slot = (slot + 1) & (PB_HSIZE - 1)
73 probes = probes + 1
74 }
75 return 0 - 1
76}
77// hash-SET membership+insert for processed SOURCE cids (replaces the O(K^2) 64B-string dedup).
78// Returns 1 = was already present, 0 = inserted now (or table full -> treat as present to stay safe).
79func pb_seen(cid: i64, seentab: *i64) -> i64 {
80 var slot: i64 = pb_hslot(cid)
81 var probes: i64 = 0
82 while probes < PB_HSIZE {
83 if seentab[slot] == cid { return 1 }
84 if seentab[slot] == 0 { seentab[slot] = cid; return 0 }
85 slot = (slot + 1) & (PB_HSIZE - 1)
86 probes = probes + 1
87 }
88 return 1
89}
90// walk every segment's key index for "out:"-prefixed keys (cross-segment dedup = pb_seen hash set on the
91// SOURCE cid -- same identity, O(1) vs the old O(K^2) string table), ss_hget latest value = packed i64
92// target cids, intern src+target cids to dense nodes, emit directed edges src->target. Returns M.
93func pb_collect(h: *i64, cidtab: *i64, idxtab: *i64, seentab: *i64, node: *i64, nn: *i64, efrom: *i64, eto: *i64, srcflag: *u8) -> i64 {
94 if (h as i64) == 0 { return 0 }
95 let ns: i64 = h[0]
96 let pp: *i64 = sys_mmap(16) as *i64
97 let pl: *i64 = sys_mmap(16) as *i64
98 let kbuf: *u8 = sys_mmap(64) // ONE reused key buffer (the old per-key mmap leaked a page per key)
99 var M: i64 = 0
100 var capped: i64 = 0
101 var s: i64 = 0
102 while s < ns {
103 let kb: *u8 = h[1 + 8 * s] as *u8
104 if h[2 + 8 * s] >= 8 {
105 let m9: i64 = ss_r32(kb, 4)
106 var e9: i64 = 0
107 while e9 < m9 {
108 let eo: i64 = 8 + 4 * m9 + ss_r32(kb, 8 + 4 * e9)
109 if (kb[eo] as i64) == 1 {
110 let kl9: i64 = ss_r32(kb, eo + 1)
111 if kl9 >= 5 { if kl9 < 60 {
112 if kb[eo + 5] == (111 as u8) { if kb[eo + 6] == (117 as u8) { if kb[eo + 7] == (116 as u8) { if kb[eo + 8] == (58 as u8) {
113 var c: i64 = 0
114 while c < kl9 { kbuf[c] = kb[eo + 5 + c]; c = c + 1 }
115 kbuf[kl9] = 0 as u8
116 let scid: i64 = pb_key_cid(kbuf, kl9)
117 if pb_seen(scid, seentab) == 0 {
118 if ss_hget(h, kbuf, pp, pl) == 1 { if pl[0] >= 8 {
119 let si: i64 = pb_intern(scid, cidtab, idxtab, node, nn)
120 if si < 0 { capped = 1 } else {
121 srcflag[si] = 1 as u8 // this node IS one of OUR documents (an out: source)
122 let tv: *i64 = pp[0] as *i64
123 let nt: i64 = pl[0] / 8
124 var t: i64 = 0
125 while t < nt {
126 if M < PB_MAXEDGE {
127 let ti: i64 = pb_intern(tv[t], cidtab, idxtab, node, nn)
128 if ti < 0 { capped = 1 } else { efrom[M] = si; eto[M] = ti; M = M + 1 }
129 } else { capped = 1 }
130 t = t + 1
131 }
132 }
133 } }
134 }
135 } } } }
136 } }
137 }
138 e9 = e9 + 1
139 }
140 }
141 s = s + 1
142 }
143 if capped == 1 { pb_puts("PB CAPPED: node/edge cap hit -- graph TRUNCATED (raise PB_MAXNODE/PB_MAXEDGE)\n" as *u8) }
144 return M
145}
146// full build: open shard -> collect edges -> pr_compute -> store pr:<cid>. segid0 = first segid for the pr:
147// segment(s) (caller supplies a fresh one). Returns node count N (>=0), or -1 on empty/no shard.
148func pb_run(prefix: *u8, iters: i64, segid0: i64) -> i64 {
149 let h: *i64 = ss_open2(prefix, 1) // mmap-open: shard pages in on demand (low RSS) -> fits on the NAS, no OOM
150 if (h as i64) == 0 { return 0 - 1 }
151 pb_puts(" PR ss_open(mmap) OK segments=" as *u8); pb_num(h[0]); pb_puts("\n" as *u8)
152 let node: *i64 = sys_mmap(8 * PB_MAXNODE)
153 let cidtab: *i64 = sys_mmap(8 * PB_HSIZE) // anon mmap = zero pages -> empty slots are cid 0
154 let idxtab: *i64 = sys_mmap(8 * PB_HSIZE)
155 let seentab: *i64 = sys_mmap(8 * PB_HSIZE)
156 let efrom: *i64 = sys_mmap(8 * PB_MAXEDGE)
157 let eto: *i64 = sys_mmap(8 * PB_MAXEDGE)
158 let srcflag: *u8 = sys_mmap(PB_MAXNODE) // 1 = node is one of OUR docs (out: source) -> gets a pr: row
159 let nn: *i64 = sys_mmap(16) as *i64
160 nn[0] = 0
161 pb_puts(" PR arrays allocated, collecting edges...\n" as *u8)
162 let M: i64 = pb_collect(h, cidtab, idxtab, seentab, node, nn, efrom, eto, srcflag)
163 let N: i64 = nn[0]
164 pb_puts(" PR collect done N=" as *u8); pb_num(N); pb_puts(" M=" as *u8); pb_num(M); pb_puts("\n" as *u8)
165 if N <= 0 { return 0 }
166 let rank: *i64 = sys_mmap(8 * N)
167 pr_compute(N, M, efrom, eto, rank, iters)
168 pb_puts(" PR compute done, persisting document priors...\n" as *u8)
169 // persist pr:<node[i]> = rank[i] ONLY for our documents (srcflag=1). External outlink-target leaf nodes
170 // carry authority FLOW during pr_compute but are never served, so storing their priors is pure waste --
171 // at bulk scale that was ~10M phantom rows (the persist phase that crashed the VM, seq631). Filtering to
172 // source docs cuts it ~100x (~150k rows) AND keeps the served shard small. Buffers hoisted (ss_add2
173 // copies immediately; per-row mmap would breach the kernel map cap at scale).
174 var segid: i64 = segid0
175 let w: *i64 = ss_begin_cap(PB_MAGIC_16777216) // 16MiB pr segments: ~700k pr rows/segment, manifest stays short
176 let pkey: *u8 = sys_mmap(32)
177 let rv: *i64 = sys_mmap(16) as *i64
178 var stored: i64 = 0
179 var i: i64 = 0
180 while i < N {
181 if srcflag[i] == (1 as u8) {
182 pb_prkey(node[i], pkey)
183 rv[0] = rank[i]
184 if ss_add(w, 1, pkey, rv as *u8, 8) < 0 {
185 ss_commit(prefix, w, segid)
186 segid = segid + 1
187 w[1] = 0
188 ss_add(w, 1, pkey, rv as *u8, 8)
189 }
190 stored = stored + 1
191 }
192 i = i + 1
193 }
194 ss_commit(prefix, w, segid)
195 pb_puts(" PR persisted document priors=" as *u8); pb_num(stored); pb_puts(" (of " as *u8); pb_num(N); pb_puts(" graph nodes)\n" as *u8)
196 return N
197}
198func main(argc: i64, argv: *i64) -> i64 {
199 var domain: *u8 = "web" as *u8
200 var iters: i64 = 100
201 if argc >= 2 { domain = argv[1] as *u8 }
202 // fresh segid base from the monotonic clock (us) -- unique per run, never collides a live segment
203 let ts: *i64 = sys_mmap(16) as *i64
204 __syscall(228, 1, ts as i64, 0, 0, 0, 0) // clock_gettime(CLOCK_MONOTONIC, ts)
205 let segid0: i64 = ts[0] * PB_MAGIC_1000000 + ts[1] / 1000
206 let prefix: *u8 = sys_mmap(512)
207 dss_prefix(domain, prefix)
208 pb_puts("nx_pagerank_build domain=" as *u8); pb_puts(domain); pb_puts("\n" as *u8)
209 let N: i64 = pb_run(prefix, iters, segid0)
210 if N < 0 { pb_puts("no shard\n" as *u8); return 1 }
211 pb_puts("PageRank: nodes="); pb_num(N); pb_puts(" pr: rows stored (authority priors)\n" as *u8)
212 return 0
213}