recall: fix BM25 length-norm precision + host benchmark vs float BM25 (NDCG parity, faster, deterministic)#92
recall: fix BM25 length-norm precision + host benchmark vs float BM25 (NDCG parity, faster, deterministic)#92bahadirarda wants to merge 5 commits into
Conversation
…-divide) bm25_tf_norm computed the document-length ratio as BM25_B * (dl / avgl) -- an integer-FLOORED ratio. For the common dl < avgl document this floors to 0 and drops length normalization entirely; more generally it collapses the ratio to a step function. Reordering to (BM25_B * dl) / avgl keeps ~3 fractional digits of dl/avgl while staying pure integer. Measured on BEIR NFCorpus (tools/recall-bench, next commit), this recovers full float-BM25 ranking parity: * NDCG@10 0.2887 -> 0.3051 (ideal-float twin 0.3052; bm25s lucene 0.3052) * integer-vs-float top-10 overlap 0.7389 -> 0.9996, top-1 agree 52% -> 92% at zero cost to determinism (bit-exact), verifiability, or the ~22% integer speed edge. IDENTICAL for the single-token records the kernel feeds today (dl == avgl == 1 => 250 + 750 either way), so the M40 boot KAT is unchanged. Verified: 13 recall host tests pass; the 5 affected recall Kani harnesses (idf / tf_norm / term_score / absent-is-zero / doc-score-monotone) all VERIFICATION SUCCESSFUL (the proven properties are scale-agnostic).
A std, nested-workspace host harness (the tb-vmm / prov-signer precedent, so the kernel kbuild + -Zbuild-std lanes are untouched) that benchmarks the verified integer recall leaf against the float implementations everyone else uses, on BEIR NFCorpus, same box. It links the SAME Kani-proven tb_encode::recall::bm25_doc_score the kernel scores with -- never a second BM25 (the same-math rule); the index / candidate-gen / top-k are harness plumbing. Fairness: all systems consume identical tokens (bm25s standard pipeline, stopwords=english, no stemmer), k1=1.2 b=0.75 everywhere, one canonical evaluator (pytrec_eval). bm25s method='lucene' is Yuva's EXACT IDF twin, so the comparison isolates float vs integer fixed-point; the in-harness ideal-float run reproduces bm25s NDCG@10 to 4 decimals, validating the harness. Result (NFCorpus test, 323 q, one WSL box): the verified integer leaf matches float BM25 ranking quality (NDCG@10 0.3051 vs float 0.3052 / bm25s 0.3052 / rank-bm25 0.3056) while running ~22-30% faster than the same-harness ideal-float and being the only implementation that is bit-exact deterministic AND formally machine-checked. Absolute QPS is hardware-specific -- only the same-box figures are comparable. Includes prep_bench.py (tokenize + float baselines + interchange export) and eval_all.py (canonical scoring) so the run reproduces from a fresh checkout; README documents the steps + the literature grounding (BM25 vs dense out-of- domain; agentic direct-corpus-interaction).
…aming
Parameterize the corpus (IRBENCH_DS, default nfcorpus) and record SciFact
alongside NFCorpus. SciFact confirms the robust claims and corrects an
overclaim the single-dataset table invited:
* quality parity holds on BOTH sets (Yuva 0.6634 vs float 0.6625 NDCG@10 on
SciFact; int-vs-float top-10 overlap 0.997 on both);
* the length-norm fix's principled effect is FIDELITY (overlap 0.74->0.997 on
both), which recovered -5.4% NDCG on NFCorpus but was NDCG-neutral on SciFact
(floored 0.6667 vs fixed 0.6634 -- benign noise, not a gain -- stated plainly);
* the durable SPEED win is vs the SAME-HARNESS float (~17-22%); bm25s's numpy
vectorisation actually beats the leaf's scalar scoring on long-doc SciFact
(~3,290 vs ~2,520 QPS) while the leaf wins on short-doc NFCorpus. No universal
"faster than bm25s" claim.
Determinism + Kani-verification remain the leaf's unique, non-workload-dependent
advantages.
|
Update: added SciFact as a second dataset + honest corrections to the single-dataset framing. SciFact (5,183 docs, 300 test q): Yuva no-float 0.6634 NDCG@10 vs float twin 0.6625 — parity holds on a second dataset (int-vs-float top-10 overlap 0.997 on both). The in-harness ideal-float again equals bm25s to 4 decimals, revalidating the harness. Two things the single-dataset table over-implied, now stated plainly in the README:
|
…'t accumulate bm25_doc_score summed bm25_term_score values, each of which had ALREADY divided idf*tf_norm by SCALE -- so one rounding error accrued per query term. On short queries (NFCorpus, SciFact) this is invisible, but on long multi-term queries the accumulated error visibly drifts the integer ranking away from real BM25. Surfaced by adding BEIR ArguAna (queries are whole arguments, ~hundreds of terms) to tools/recall-bench: int-vs-float top-10 fidelity was only 0.75, and the integer leaf scored NDCG@10 0.3679 vs float 0.3551 -- NOT a win, just the divergence landing favourably on ArguAna's judgments (the same benign-noise trap SciFact showed before the length-norm fix). Fix: accumulate the per-term idf*tf_norm products at full fixed-point scale and divide by SCALE ONCE at the end (saturating throughout). Result across all three datasets is now faithful float-BM25 parity: * ArguAna fidelity 0.7535 -> 0.9962, Yuva NDCG 0.3679 -> 0.3552 (float 0.3551) * NFCorpus fidelity 0.9996 -> 0.9992, NDCG 0.3051 (float 0.3052) -- unchanged * SciFact fidelity 0.9970, NDCG 0.6634 (float 0.6625) -- unchanged IDENTICAL to bm25_term_score for a single-term query (idf*tf_norm/SCALE), so the kernel's single-token production path (which calls bm25_idf, not bm25_doc_score) and the multi-term recall_selftest KAT -- which asserts RELATIONAL properties (A>B, A>C, b_plus>=B), all robust to the <=1 per-score shift -- are unaffected. Verified: 13 recall host tests pass; kani_recall_doc_score_accumulation_monotone + term_score panic/absent harnesses VERIFICATION SUCCESSFUL (monotonicity holds: per-term products are non-negative, saturating_add accumulates, integer /SCALE is order-preserving); clippy clean.
…on fixes Consolidated NDCG/fidelity/QPS table across NFCorpus, SciFact, ArguAna showing float-BM25 parity + fidelity >=0.996 on all three after the two precision fixes, and the honest speed picture (integer beats same-harness float everywhere; bm25s's vectorisation wins as candidates-per-query grow, decisively on ArguAna).
|
Update 2: a THIRD dataset (ArguAna) surfaced a second precision bug — now fixed. ArguAna's queries are whole arguments (~hundreds of terms). There the integer leaf's fidelity was only 0.75 and it scored NDCG@10 0.3679 vs float 0.3551 — which looks like beating float but is not: it's the same benign-noise trap (rounding divergence landing favourably), not skill. Root cause: Final, all three datasets, after both fixes — faithful float-BM25 parity:
Identical to |
Summary
Research-grounded, benchmarked answer to "is Yuva's no-float verified recall actually good, and how does it stand against the people who do this?" — with an honest before/after and a real fix the benchmark surfaced.
Two commits:
tb-encode/recall: length-normalization precision fix — a one-line arithmetic reorder in the verified leaf.tools/recall-bench: a host harness that benchmarks the actual Kani-proven leaf against the float BM25 implementations everyone else uses, same-box, on BEIR NFCorpus.The bug the benchmark found
bm25_tf_normevaluated the document-length ratio asBM25_B * (dl / avgl)— an integer-floored ratio. For any document shorter than average (dl < avgl, the common case) this floors to0and silently disables length normalization; in general it collapses a continuous ratio to a 3–4 step function. Reordering to(BM25_B * dl) / avgl(multiply-before-divide) keeps ~3 fractional digits while staying pure integer / no-float.Identical for the single-token records the kernel feeds today (
dl == avgl == 1), so the M40 boot KAT is unchanged; it only sharpens multi-token ranking (where Cogi's memory is heading).Result — BEIR NFCorpus test, 323 queries, one WSL box
Before the fix, the integer leaf scored NDCG@10 0.2887 (−5.4% vs float) with int-vs-float top-10 overlap 0.74 / top-1 52%. After: 0.3051 (float twin 0.3052), overlap 0.9996, top-1 92%.
So the verified integer leaf now matches float BM25 ranking quality, runs ~22–30% faster than the same-harness ideal-float (integer arithmetic, no FP division), and is the only implementation that is both bit-exact deterministic and formally machine-checked.
Why this comparison is fair (and honest)
stopwords=english, no stemmer);k1=1.2 b=0.75everywhere; one canonical evaluator (pytrec_eval). The harness asserts the interchange's params match the leaf's frozen constants.bm25s method='lucene'is Yuva's exact IDF twin (ln((N+1)/(df+0.5))), so the comparison isolates float vs integer fixed-point and nothing else. The in-harness ideal-float run reproduces bm25s NDCG@10 to 4 decimals — validating the harness.tb_encode::recall::bm25_doc_scorethe kernel scores with — never a second BM25. Index / candidate-gen / top-k are harness plumbing (the kernel keeps those in tb-hal).Literature grounding
BM25 stays competitive-to-superior out-of-domain and on precise-terminology corpora (BEIR; beats
text-embedding-3-largeon finance/code term matching). For an agent reading its own memory, exact-match lexical retrieval gives deterministic, interpretable, verifiable-text matches that embedding similarity does not (Beyond Semantic Similarity: Agentic Search via Direct Corpus Interaction, arXiv 2605.05242). Yuva's leaf is the retrieval primitive; QA-memory systems (Mem0, Letta, A-MEM) sit a layer above and could run on it.Verification
VERIFICATION: SUCCESSFUL(the proven properties — denom>0,[0, TF_NORM_CEIL), non-negativity, monotonicity — are scale-agnostic).tools/recall-benchis a nested workspace (tb-vmm / prov-signer precedent) — the kernel kbuild /-Zbuild-stdlanes are untouched.prep_bench.py→cargo run --release→eval_all.py(README has the steps + dataset fetch).