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}