SHADOW-250M / shadow_runtime /retriever.py
NODEMIND's picture
SHADOW 250M Instruct
aef5ed5
Raw
History Blame Contribute Delete
7.84 kB
"""Archive retriever: exact lexical inverted index over 64-token blocks, with 2-hop follow-up.
An archive is a directory with tokens.u32 (uint32 token stream) and meta.json. Blocks of 64 tokens
are indexed by their content features (stop-filtered unigrams plus hashed bigrams and trigrams,
idf-weighted). A query returns the top-k blocks; a second hop follows identifiers named near the
match, so alias chains ("the record for K is stored under reference K2") resolve.
Also holds the tokenizer used by the runtime (sentencepiece + id remap).
"""
import os, sys, json, pathlib, hashlib, re
import numpy as np
HERE = pathlib.Path(__file__).resolve().parent; PKG = HERE.parent
BLK = 64
def load_archive(path):
tok = np.memmap(os.path.join(path, "tokens.u32"), np.uint32, "r")
meta = json.load(open(os.path.join(path, "meta.json")))
bp = os.path.join(path, "bank_valid.jsonl"); bp = bp if os.path.exists(bp) else os.path.join(path, "bank.jsonl") # validated bank if present
bank = [json.loads(l) for l in open(bp, encoding="utf-8")]
return tok, meta, bank
_STOP = None
def stop_ids(cache=None):
"""500 most frequent ids, fixed once from a held-out fineweb shard (deploy/stop500.npy)."""
global _STOP
if _STOP is None:
_STOP = np.load(PKG / "tokenizer" / "stop500.npy")
return _STOP
NBG = 1 << 20 # hashed bigram feature space
def _feats(ids):
"""content features of a token sequence: unique unigrams (stop-filtered) + hashed bigrams of ALL adjacent
tokens (digits/punctuation are frequent as unigrams but their sequences -- 'Vega-713' -- are the signal)."""
ids = np.asarray(ids, np.int64); st = stop_ids()
uni = np.unique(ids); uni = uni[(~np.isin(uni, st)) & (uni > 9)]
if len(ids) >= 2:
bg = (ids[:-1] * np.int64(1000003) + ids[1:] * np.int64(7919)) % NBG
bg = bg[(ids[:-1] > 9) & (ids[1:] > 9)]
if len(ids) >= 3: # trigrams too: 'Cygnus','-','9' / '9','2','2' make the key number discriminative
tg = (ids[:-2] * np.int64(1000003) + ids[1:-1] * np.int64(7919) + ids[2:] * np.int64(104729) + np.int64(17)) % NBG
bg = np.concatenate([bg, tg[(ids[:-2] > 9) & (ids[1:-1] > 9) & (ids[2:] > 9)]])
bg = np.unique(bg) + (1 << 17)
else: bg = np.zeros(0, np.int64)
return uni, bg
def _content(ids):
u, g = _feats(ids); return np.concatenate([u, g])
# ---------------- exact lexical inverted index (BM25-style, unigram + bigram features) ----------------
# The sketch retrievers above lose the rare-key signal inside 64-token blocks; an inverted index keeps it exactly.
# Deploy cost at 100M tokens: ~60M postings (~250 MB) next to the 32 GB 1-bit KV archive; query = 3-10 posting lists.
class Inverted:
def __init__(s, tok, k1=1.2, b=0.75):
import scipy.sparse as sp
nb = len(tok) // BLK; rows = []; cols = []
for bi in range(nb):
u, g = _feats(tok[bi * BLK:(bi + 1) * BLK]); f = np.concatenate([u, g]); rows.append(f); cols.append(np.full(len(f), bi, np.int64))
r = np.concatenate(rows); c = np.concatenate(cols)
s.M = sp.csr_matrix((np.ones(len(r), np.float32), (r, c)), shape=((1 << 17) + NBG, nb)) # feature x block
df = np.asarray(s.M.sum(1)).ravel(); s.idf = np.log(1 + (nb - df + 0.5) / (df + 0.5)).astype(np.float32)
s.nb = nb; s.tok = tok
def topk(s, ids, k):
f = _content(ids)
if len(f) == 0: return np.arange(min(k, s.nb)), np.zeros(min(k, s.nb))
sc = np.asarray((s.idf[f][None, :] @ s.M[f]).todense()).ravel() if False else s.idf[f] @ s.M[f]
sc = np.asarray(sc).ravel()
idx = np.argpartition(-sc, min(k, s.nb - 1))[:k]; idx = idx[np.argsort(-sc[idx], kind="stable")]
return idx, sc[idx]
def topk_hops(s, ids, k, rounds=2, k1=8, idf_min=5.0, win=14):
"""multi-hop: round 1 = top-k1 direct hits (kept). Then EACH round-1 block gets its own follow-up query made of
the rare features within +-win tokens (over the stream) of where the question's tokens matched inside it --
'The record for K is stored under reference K2.' -> K2's n-grams -- and contributes its best new block.
Per-source queries (not a union) so one alias chain cannot be drowned by the others' noise."""
idx1, sc1 = s.topk(ids, k1); out = [int(i) for i in idx1]; qset = set(_content(ids).tolist()); qtext = _dec(np.asarray(ids, np.int64))
qtok = set(np.asarray(ids, np.int64).tolist()) - set(stop_ids().tolist()); src = out[:]
# template features = present in >=2 of the round-1 blocks ('stored under reference', 'access code for vault'):
# they would pull in every block of the same template; only features UNIQUE to a source block are followed.
cnt = {}
for bi in src:
for f in set(_content(np.asarray(s.tok[bi * BLK:(bi + 1) * BLK], np.int64)).tolist()): cnt[f] = cnt.get(f, 0) + 1
template = {f for f, c in cnt.items() if c >= 2}
for r in range(rounds - 1):
new = []
for bi in src:
if len(out) + len(new) >= k: break
blk = np.asarray(s.tok[bi * BLK:(bi + 1) * BLK], np.int64)
pos = [i for i, t in enumerate(blk) if t in qtok]
if not pos: continue
lo = max(0, bi * BLK + min(pos) - 8); hi = min(len(s.tok), bi * BLK + max(pos) + win + 1)
w = np.asarray(s.tok[lo:hi], np.int64)
# follow-up query: entity-like identifiers mentioned near the match (Name-123 style, the shape of archive
# keys) other than the question's own; fall back to the rare non-template features of the window.
ents = [e for e in set(re.findall(r"[A-Z][a-z]+-\d+", _dec(w))) if e not in qtext]
if ents: fb = np.unique(np.concatenate([_content(enc(" " + e)) for e in ents]))
else:
fb = _content(w); fb = fb[(s.idf[fb] >= idf_min) & ~np.isin(fb, list(qset | template))]
if len(fb) == 0: continue
sc = np.asarray(s.idf[fb] @ s.M[fb]).ravel(); sc[out + new] = -1e9
for j in np.argsort(-sc)[:2]: # best new block of this chain (skip the source's own neighbourhood)
j = int(j)
if abs(j - bi) > 1 and j not in new: new.append(j); break
out += new; src = new
if not new: break
return np.array(out[:k]), np.zeros(min(k, len(out)))
# ---------------- tokenizer (sentencepiece + id remap; expansion for out-of-subset ids computed lazily) ----
_sp = None; _o2n = None; _n2o = None; _exp_cache = {}
def _load_tok():
global _sp, _o2n, _n2o
if _sp is None:
import sentencepiece as spm
_sp = spm.SentencePieceProcessor(model_file=str(PKG / "tokenizer" / "tokenizer.model"))
_n2o = np.fromfile(PKG / "tokenizer" / "new2old.u32", np.uint32).astype(np.int64)
_o2n = np.full(262144, -1, np.int64); _o2n[_n2o] = np.arange(len(_n2o))
def _expand(old_id):
"""out-of-subset token -> its byte-fallback sequence in the 131k id space (lossless)."""
seq = _exp_cache.get(old_id)
if seq is None:
piece = _sp.id_to_piece(int(old_id)).replace("▁", " ")
raw = _sp.decode([int(old_id)]).encode("utf-8") if not piece.startswith("<0x") else bytes([int(piece[3:5], 16)])
seq = [int(_o2n[_sp.piece_to_id(f"<0x{b:02X}>")]) for b in raw]
_exp_cache[old_id] = seq
return seq
def enc(text):
_load_tok()
out = []
for i in _sp.encode(text):
n = _o2n[i]
if n >= 0: out.append(int(n))
else: out.extend(_expand(i))
return out
def _dec(ids):
_load_tok()
return _sp.decode(_n2o[np.asarray(ids, np.int64)].tolist())