plur-bench
June 29, 2026 · View on GitHub
Reproducible benchmarks for the PLUR.ai memory engine, across three axes of memory quality (see below). Apache-2.0, fully local, keyless CI.
The retrieval track runs per-question canonical-doc-scored benchmarks on real LongMemEval-S — the same protocol gbrain, mem0, Letta, and the LongMemEval paper use — sleep- and crash-resistant via per-question JSON checkpoints. Designed for an A100 research host but runs on any CPU with patience.
What this measures — three axes
Memory quality is not one number. plur-bench keeps three distinct axes in separate tables and never conflates them:
| Axis | The question it answers | Where | Status | Try it offline |
|---|---|---|---|---|
| Retrieval recall | Is the right evidence retrieved? (R@K on LongMemEval-S) | benchmark/per-question.ts | 97.6% R@5, parity-with-frontier; paired verdict via pnpm stats (McNemar/Wilson/TOST) | pnpm stats:test |
| Editability dividend (H12) | Does being able to correct or delete a memory improve downstream outcomes? (the thing a flat, frozen store can't buy) | benchmark/editability.ts · EDITABILITY.md | harness + smoke green; adversarial run pending | pnpm editability:smoke |
| Agent-task A/B | Does memory change what the agent does, not just what it retrieves? (with vs without PLUR) | agent-bench/ | harness + smoke green; real win-rate pending | pnpm agent-bench:demo |
Retrieval recall and end-to-end answer accuracy are themselves different axes (separate tables below). The editability and agent-task axes answer what recall cannot: whether the memory's design — correctability, decay, injection — changes outcomes. Each axis ships an offline path (*:test / *:smoke / *:demo, no API key) and a real path that runs on a configured box.
Headline result (LongMemEval-S, N=500, chunk granularity)
Measured with this harness using PLUR's vendored production retrieval code, at chunk granularity — the only setting comparable to gbrain (see Granularity).
| Stack | R@1 | R@5 | R@10 | Notes |
|---|---|---|---|---|
| PLUR BM25 alone (airgapped) | — | 92.2% | — | no embedder, no API |
| PLUR hybrid + bge-reranker (airgapped) | — | 97.6% | — | local cross-encoder |
| PLUR hybrid, openai-3-large (control) | 89.2% | 97.0% | 98.8% | gbrain's embedder + unit |
| gbrain published (vector / hybrid) | — | 97.4% / 97.6% | — | the published reference |
PLUR matches the best published open retrieval result on LongMemEval. The harness was validated against gbrain's number (#4): run under gbrain's embedder and unit, PLUR reproduces 97.0% R@5 vs their 97.4–97.6% — within 1 pp.
These are Recall@K (retrieval) figures: the fraction of questions whose ground-truth evidence lands in the top-K. That is not the same axis as the end-to-end answer-accuracy numbers other systems headline (mem0/Mastra ~94–95%); the two are not directly comparable. An LLM-judge accuracy tier is available via
--scoring llm_judge— see Scoring modes.
Why chunk granularity. Earlier turn-granularity numbers (BM25 93.2%, hybrid 96.2%) are not comparable to gbrain — finer units make "a unit from the right session is in the top-5" easier to hit. gbrain indexes one ~300-word chunk per session; matching that (#3) was the prerequisite for any honest comparison, and the harness equivalence was then validated (#4). Always publish chunk-granularity numbers against gbrain.
Per-category, from the validated openai-3-large control run (hybrid, chunk):
| Category | N | R@1 | R@5 | R@10 |
|---|---|---|---|---|
| knowledge-update | 78 | 96.2% | 98.7% | 100.0% |
| multi-session | 133 | 93.2% | 97.7% | 99.2% |
| single-session-assistant | 56 | 100.0% | 100.0% | 100.0% |
| single-session-preference | 30 | 63.3% | 93.3% | 96.7% |
| single-session-user | 70 | 88.6% | 98.6% | 100.0% |
| temporal-reasoning | 133 | 82.7% | 94.0% | 97.0% |
Comparison vs published numbers
Auto-generated by
pnpm reportfrom run manifests +config/published-numbers.yaml. Do not edit by hand.
Retrieval recall and end-to-end accuracy are different axes and are listed in separate tables — never compared directly. A third axis — agent-task outcomes (does memory change what the agent does, not just what it retrieves) — lives in agent-bench/: a with/without-PLUR A/B harness (ported from datacore-bench). Try it offline: pnpm agent-bench:demo.
Retrieval — Recall@K
| System | Metric | Value | Granularity | Source / comparability |
|---|---|---|---|---|
| gbrain | R@5 | 97.6% | chunk | src |
| gbrain | R@5 | 19.8% | chunk | src |
| supermemory | R@5 | 86.0% | — | src |
End-to-end accuracy (LLM-judge)
| System | Metric | Value | Judge | Source / comparability |
|---|---|---|---|---|
| supermemory | accuracy | 85.9% | — | src |
| zep | accuracy | 71.2% | gpt-4o | src ⚠ unverified |
| mem0 (paper) | locomo_j | 66.9 J | gpt-4o-mini | src |
| mem0 (2026) | accuracy | 94.4% | gpt-4o | src ⚠ unverified |
| letta (filesystem agent) | accuracy | 74.0% | gpt-4o-mini | src ⚠ unverified |
| mastra | accuracy | 95.0% | — | src ⚠ unverified |
No local run manifests found in results/ — run a benchmark to add this-harness rows. Published rows above always render.
Regenerate with pnpm report (reads run manifests + config/published-numbers.yaml). See Methodology metadata for what makes a row comparable.
TL;DR — how to reproduce
git clone https://github.com/plur-ai/plur-bench
cd plur-bench
pnpm install
cp .env.example .env # OPENAI_API_KEY for openai-3-large + llm_judge
./scripts/fetch-corpus.sh # ~260 MB LongMemEval-S (verifies sha256)
# Retrieval (R@K) at CHUNK granularity — the only setting comparable to gbrain:
caffeinate -i -s pnpm bm25:chunk # ~25s for all 500 questions
caffeinate -i -s pnpm hybrid:chunk # ~5 hours CPU (bge-small embedding)
caffeinate -i -s pnpm rerank:chunk # ~3 days CPU — recommend A100
pnpm report # regenerate the comparison table
End-to-end accuracy (LLM-judge) and controls — see Scoring modes:
PLUR_JUDGE=mock pnpm bm25:chunk --scoring llm_judge # offline dry-run, no key
OPENAI_API_KEY=… pnpm rerank:chunk --scoring llm_judge --runs 10
pnpm full-context # control ceiling · pnpm no-memory # control floor
Batched / resumable (checkpoints persist; stop and re-run the same command):
caffeinate -i -s pnpm rerank:chunk --limit 50 # first 50; resume by re-running
Or Docker:
docker compose up benchmark-runner # CPU
docker compose --profile gpu up benchmark-runner-gpu # A100
Prompt to paste into Claude Code / Codex (research-machine workflow)
I'm on the PLUR research machine running benchmarks. The repo is plur-bench; the harness is
benchmark/per-question.ts. The harness is feature-complete — the remaining work is producing the definitive numbers on this box (A100 +OPENAI_API_KEY). Always run at--granularity chunk(the only setting comparable to gbrain; harness equivalence is validated, ~97% R@5).Please:
- Corpus: run
./scripts/fetch-corpus.sh; it verifies the sha256. Ifconfig/datasets.yamlshows the dataset UNPINNED, record the printed digest there and re-run (issue #23).- GPU: confirm
nvidia-smishows the A100 andPLUR_ONNX_PROVIDER=cudaloads.- Retrieval (R@K), chunk granularity, with checkpoints:
pnpm bm25:chunk pnpm hybrid:chunk PLUR_ONNX_PROVIDER=cuda PLUR_DISABLE_REACTIVATION=1 pnpm rerank:chunk PLUR_EMBEDDER=openai-3-large PLUR_OPENAI_3_LARGE_DIM=1536 pnpm rerank:chunk # headline stack pnpm bm25:chunk:dedup / hybrid:chunk:dedup / rerank:chunk:dedup # gbrain best-per-page parity (#21)- End-to-end accuracy (vs mem0/Zep/Letta/Mastra),
OPENAI_API_KEYset:pnpm rerank:chunk --scoring llm_judge --runs 10 pnpm full-context # control ceiling (mandatory) · pnpm no-memory # control floor pnpm bm25 --corpus locomo --preset mem0-paper --scoring llm_judge # vs mem0 paper (#27)- Each question writes a checkpoint; runs resume by re-running the same command. Each run also writes
results/<run-id>/manifest.json(full methodology + accuracy/cost).- Run
pnpm reportto regenerate the comparison table from the manifests +config/published-numbers.yaml, and post the table. Keep R@K and accuracy in separate columns — they are different axes.- Significance + equivalence (the H1 gate): the per-question arms are PAIRED on question_id, so a bare ladder of point estimates is not a verdict. Run
pnpm stats comparebetween adjacent arms to get the McNemar exact p, per-arm Wilson 95% CIs, and the TOST equivalence verdict:pnpm stats compare results/hybrid-chunk-cp results/rerank-chunk-cp # is the reranker's lift significant? pnpm stats compare results/bm25-chunk-cp results/hybrid-chunk-cp # is dense > sparse significant?Reference (chunk granularity, validated): gbrain hybrid R@5 = 97.6%; PLUR
openai-3-largecontrol R@5 = 97.0% (within 1pp). gbrain's "BM25 19.8%" is mislabelled Postgres FTS — not a real BM25 baseline.
Methodology — what makes this comparable to gbrain
| Element | Choice | Why |
|---|---|---|
| Corpus | LongMemEval-S 500 questions from the official HuggingFace release, sha256-pinned in config/datasets.yaml | Same dataset gbrain published on; the pin makes runs byte-reproducible — see Corpus integrity |
| Protocol | per-question: each question gets its own isolated corpus (only its haystack) | This is what gbrain, mem0, Letta all use. Combined-corpus protocol is dramatically harder and not what the LongMemEval paper specifies. |
| Scoring | canonical_doc: a retrieved unit from answer_session_ids must be in top-K | Same hit definition gbrain uses (session-membership). |
| Granularity | what a "unit" is — --granularity turn|chunk|session | The lever that makes scores comparable. gbrain = chunk. Default turn is not comparable. See below. |
| Session dedup | --dedup-sessions collapses top-K to distinct sessions before scoring | Matches gbrain's best-per-page; off by default. See Session dedup. |
| Retrieval code | PLUR's searchEngrams + hybridSearchWithMeta + applyReranker (vendored from packages/core/src/) | Tests the production code path users run |
Significance & equivalence (the H1 gate)
A ladder of point estimates (BM25 → +dense → +reranker) is not a verdict.
The arms are measured on the same questions, so they are paired — the right
tests are paired ones. scripts/paired_stats.py (pnpm stats) reads two arms'
per-question checkpoint dirs, joins on question_id, and reports:
- McNemar exact test (two-sided binomial on the discordant pairs) — is the difference between two arms significant? At our operating point (~97% R@5 on N=500) the discordant counts are tiny, so a 0.6pp reranker "lift" is almost certainly not significant.
- Wilson 95% CIs per arm — the honest uncertainty band on each recall.
- TOST equivalence (default margin ±2pp) — the test that actually backs the public claim. "Local parity with the cloud frontier" is an equivalence claim, and equivalence has to be tested directly, not inferred from a non-significant difference. TOST asks: is the paired difference provably inside ±2pp?
pnpm stats compare results/hybrid-chunk-cp results/rerank-chunk-cp
pnpm stats:test # known-answer unit tests for the estimators
Retrieval R@K is deterministic (fixed embeddings, deterministic BM25/RRF/
reranker), so there is no seed variance on the retrieval axis — the binomial
Wilson CI is the uncertainty, and re-running gives the identical number. Cross-
seed SD (pnpm stats seeds ...) is meaningful only on the accuracy axis
(--scoring llm_judge, sampled answer generation), never on R@K. Don't report a
"±SD over seeds" on a recall number — it is structurally 0.
This is the defensible framing the numbers support: dense embedding is the workhorse; hybrid and the reranker are at statistical parity with it on this benchmark, fully local. Never "we beat the frontier."
Corpus integrity & reproducibility
A benchmark number means nothing unless two people can prove they ran on the
same bytes. config/datasets.yaml is the single source of truth for each
corpus — its source, the file the harness reads, its license, question count,
and a pinned sha256:
datasets:
longmemeval-s:
name: LongMemEval-S
source: hf:xiaowu0162/longmemeval
file: data/longmemeval_s.json # path within the HuggingFace repo
local: corpus/longmemeval_s.json # where the harness reads it
sha256: "" # pinned digest (empty = not yet pinned)
n_questions: 500
- Fetch verifies.
./scripts/fetch-corpus.shdownloads the corpus, places it atcorpus/longmemeval_s.json, and checks its sha256 against the pin. A corrupt, partial, or wrong file fails the run; it never silently accepts unverified bytes. - First-run pinning. Until
sha256is filled, fetch/verify prints the computed digest and stops (UNPINNED). Confirm it is the official release, paste the digest intoconfig/datasets.yaml, commit — from then on every run is gated against it. - Every run is traceable. The harness logs
corpus_sha256=…at load and, if the dataset is pinned, confirms the match (or warns loudly on mismatch). This digest is what #7 embeds in each run manifest asdataset.sha256.
pnpm verify-corpus # check corpus/longmemeval_s.json against the pin
# or an arbitrary file:
npx tsx scripts/verify-corpus.ts --file <path> --expect <sha256>
Exit codes: 0 match · 1 mismatch · 2 unpinned (digest printed) · 3 missing/args.
LongMemEval ships several variants (longmemeval-cleaned, longmemeval-v2); we
pin the exact file we measured on rather than tracking "latest" — see the
comments in config/datasets.yaml.
Methodology metadata & run manifests
A number is only comparable to another if you can see exactly how each was produced. The Zep↔mem0 dispute is the cautionary tale: their scores for each other diverged by up to 25 pp purely from adapter/judge/protocol differences. So every run records its full methodology.
Each run writes results/<run-id>/manifest.json — the citable artifact —
containing a versioned methodology block plus the aggregate and per-category
stats:
{
"schema_version": 1,
"dataset": { "name": "longmemeval_s", "sha256": "…", "source": "hf:xiaowu0162/longmemeval", "n_questions": 500 },
"protocol": { "style": "per-question", "granularity": "chunk", "top_k": 10, "session_dedup": false },
"system": { "name": "plur", "mode": "hybrid", "embedder": "bge-small-en-v1.5", "reranker": null, "vendored_core_ref": null, "adapter_config": {} },
"scoring": { "metric": "canonical_doc_r@k", "judge_model": null, "judge_prompt_sha256": null, "runs": 1 },
"environment": { "harness_git_sha": "…", "node": "v22.…", "os": "darwin 25.…", "hardware": "…", "lockfile_sha256": "…" },
"comparability": { "target": null, "caveats": [] }
}
- Methodology hash. A short fingerprint over the protocol-affecting fields (dataset, protocol, system, scoring metric/judge — not environment or run counts, so two machines on the same protocol agree). It is stamped into every per-question checkpoint.
- Mixed-protocol guard. On resume, a checkpoint whose stamped hash differs from the current run is rejected — you cannot accidentally pool results measured under different settings into one directory. (Checkpoints written before this stamping warn rather than reject.)
- The
dataset.sha256is the corpus pin from Corpus integrity;comparability.targetreferences an entry inconfig/published-numbers.yaml, andpnpm reportflags whether the run's protocol actually matches that target (see Comparison vs published numbers).
Scoring modes — retrieval vs end-to-end accuracy
--scoring selects what a "correct" answer means:
| Mode | Measures | Cost | When |
|---|---|---|---|
canonical_doc (default) | Recall@K — did a unit from an answer session land in top-K | free, offline | retrieval quality; gbrain comparison |
llm_judge | end-to-end answer accuracy — a judge model rules the generated answer correct/incorrect | paid API (or mock) | comparison vs mem0/Zep/Letta/Mastra headline numbers |
The two are different axes and not interchangeable — canonical_doc is
retrieval recall; llm_judge is answer accuracy.
llm_judge pipeline: the top-K retrieved units are formatted into a context
block → an answer model generates a prediction → a judge model rules it
correct/incorrect against the gold answer, repeated --runs N times for
accuracy ± SD (binary correct/incorrect, mean over questions; matches mem0's
paper protocol).
# offline, no API key — deterministic mock judge (CI / smoke):
PLUR_JUDGE=mock pnpm bm25 --scoring llm_judge --runs 3
# real judge (default gpt-4o) + answer model (default gpt-4o-mini):
OPENAI_API_KEY=sk-… pnpm hybrid --scoring llm_judge --runs 10
- Pluggable judges (
src/plur-core/judges/, mirrors embedders/rerankers):PLUR_JUDGE=openai|anthropic|mock,PLUR_JUDGE_MODEL,PLUR_ANSWER_MODEL. Anthropic is a stub; swapping providers is config-only. - Versioned judge prompt —
config/prompts/judge-binary-v1.txt; its sha256 is recorded inmethodology.scoring.judge_prompt_sha256. - API-key gated —
--scoring llm_judgewith the OpenAI judge exits cleanly ifOPENAI_API_KEYis unset; tests and CI never require a key (mock judge). - Resumable — per-question judge verdicts are checkpointed; raising
--runsre-judges, a smaller dir is transparently upgraded. The manifest carriesaccuracy(mean ± SD, per-run) andcost(tokens in/out).
Control baselines
Two honest control rows the field tends to omit (both --scoring llm_judge only):
--mode | The answer model sees | Establishes |
|---|---|---|
full-context | the entire haystack, no retrieval | the ceiling — on LOCOMO this beats mem0's own system (72.9 vs 68.4 J) |
no-memory | the question only | the floor — what the model already knows without any history |
PLUR_JUDGE=mock pnpm full-context # or pnpm no-memory
- They do no retrieval, so Recall@K is
nullin the manifest (not0%— it is not applicable). Accuracy is the figure; token cost is the point: full-context sends ~115k tokens/question on LongMemEval-S, PLUR retrieves a few hundred. That efficiency gap is the headline these controls exist to expose. - Manifests carry
system.name = "control:full-context" | "control:no-memory"so the comparison report (#8) renders them as reference rows in every LLM-judge table.
LOCOMO & the mem0-paper preset
LOCOMO lets PLUR be compared to mem0's paper numbers (LOCOMO J = 66.88 base / 68.44 graph, arXiv:2504.19413). It is the most-cited and most-disputed dataset in the space — we run it with its flaws documented, not avoided.
# fetch locomo10.json yourself (CC BY-NC — see below), then:
PLUR_JUDGE=mock pnpm bm25 --corpus locomo --preset mem0-paper --scoring llm_judge
--format locomoconverts LOCOMO into the per-question shape: each conversation's sessions become the haystack; a question'sevidencedia-ids (D<session>:<turn>) become itsanswer_session_ids. That one mapping makes both scoring tiers work unchanged —canonical_docR@K scores evidence-session membership,llm_judgeanswers from the retrieved turns.--preset mem0-paperpins the comparability-critical choices:llm_judgescoring,--runs 10,--top-k 30, judge + answer modelgpt-4o-mini, category-5 (adversarial) excluded, andcomparability.target = mem0-paper-locomo. Explicit flags still override; env still wins for models.--corpus locomoresolves to thelocomoentry inconfig/datasets.yaml(path + format). Use--corpus <path> --format locomofor an arbitrary file.- The adapter records its speaker/role + dia-id mapping in the manifest's
system.adapter_config— this is the exact point where mem0's run of Zep was disputed, so it must be auditable.
Caveats (documented, not hidden):
- License: the mc10 LOCOMO release is CC BY-NC 4.0 (non-commercial). It is not vendored here — fetch it yourself; the sha is unpinned until you do.
- Full-context is mandatory in any LOCOMO table: conversations are 16k–26k
tokens, so they fit a modern context window and the full-context control tends
to beat memory systems. Always include
--mode full-context. - The dispute: Zep's LOCOMO J has been reported as 58.4 / 66.0 / 75.1 / 84
depending on the harness — root-caused to adapter errors (speaker/role mapping,
timestamps in text vs
created_at, sequential vs parallel search). Never cite a single Zep LOCOMO number without naming the harness. Recorded aszep-locomo-disputedinconfig/published-numbers.yaml.
Schema note: the adapter follows the
snap-research/locomolocomo10.jsonfield layout; confirm the mapping against the release on the first real run.
Granularity — the comparability lever
How a conversation is sliced into search units decides what the numbers mean.
Controlled with --granularity:
| Mode | Search unit | When to use |
|---|---|---|
turn (default) | one unit per chat message | PLUR's original runs; keeps prior numbers reproducible |
chunk | one ~300-word slice per session (50-word overlap) | gbrain comparison — matches gbrain's indexing exactly |
session | one unit per whole session | coarse control; only meaningful for bm25 (vector modes truncate long sessions at the embedding model's context limit) |
chunk mode reproduces gbrain v0.28.8's chunker — 300-word slices, 50-word
overlap, 6000-character cap (their src/core/chunkers/recursive.ts). (gbrain's
contextual-retrieval feature shipped later, in v0.40.3.0, so the published 97.6%
is plain chunk-hybrid — no special wrapper to replicate.)
pnpm bm25:chunk # or: pnpm hybrid:chunk / pnpm rerank:chunk
# equivalently:
npx tsx benchmark/per-question.ts --mode bm25 --granularity chunk --checkpoint-dir results/bm25-chunk-cp
Checkpoints are namespaced per granularity, so chunk and turn runs can share a results directory without overwriting each other.
Only
chunk-granularity numbers should be published against gbrain. Track: #3 (this granularity fix) and #4 (proving the harness reproduces gbrain's 97.6% by runningopenai-3-largeat chunk granularity before trusting the airgapped bge-small number).
Session dedup — exact best-per-page parity
Granularity matches the ingestion unit; this matches the scoring unit. At
chunk granularity a single long session can occupy several of the top-K slots,
so a raw top-5 might cover only 2–3 distinct sessions. gbrain dedups to the
best chunk per page (its best_per_page CTE) before applying the limit, so
its top-K always represents K distinct sessions. Without dedup, plur-bench's
R@K is therefore slightly conservative relative to gbrain.
--dedup-sessions collapses the ranked results to the highest-ranked unit per
session before scoring, restoring exact protocol parity:
pnpm bm25:chunk:dedup # or: pnpm hybrid:chunk:dedup / pnpm rerank:chunk:dedup
# equivalently:
npx tsx benchmark/per-question.ts --mode hybrid --granularity chunk --dedup-sessions \
--checkpoint-dir results/hybrid-chunk-dedup-cp
Properties (see benchmark/dedup.test.ts):
- Default off — existing checkpointed runs stay reproducible.
hit_1is unchanged — the top-ranked session is never moved.hit_5/hit_10can only rise — dedup never hides a session, so the number is>=the non-dedup number; it removes a downward bias, it does not flatter PLUR beyond gbrain's own rule.- No-op at
sessiongranularity (units are already one per session); the effect is largest atchunk.
Checkpoints are namespaced by both granularity and dedup, so dedup and non-dedup runs can share a results directory without overwriting each other.
Repository structure
plur-bench/
├── README.md # this file
├── Dockerfile # node + pnpm + py + system deps
├── docker-compose.yml # 3 profiles: cpu, gpu (A100), postgres
├── entrypoint.sh # modes: matrix (default), single, shell
├── package.json
├── benchmark/
│ └── per-question.ts # the working harness (per-question, canonical-doc, resumable)
├── src/plur-core/ # vendored PLUR retrieval code
│ ├── fts.ts # searchEngrams (PLUR BM25)
│ ├── hybrid-search.ts # rrfMerge, applyReranker, hybridSearchWithMeta
│ ├── embeddings.ts # legacy cache-backed embedding search
│ ├── embedders/ # minilm, bge-small, bge-base, embedding-gemma, openai-3-large
│ ├── rerankers/ # bge-reranker-v2-m3, jina-reranker-v2-base
│ ├── schemas/ # Engram zod schema
│ ├── sync.ts logger.ts ...
├── config/
│ └── matrix.yaml # for the matrix runner (optional, second-tier)
├── corpus/ # fetched at setup (gitignored)
│ └── .gitkeep
├── results/ # checkpoints + summaries (gitignored)
│ └── .gitkeep
└── .env.example # OPENAI_API_KEY (openai-3-large + llm_judge), etc.
Checkpoint resume — how it works
--checkpoint-dir <path> is the key flag. Each completed question writes
<dir>/<question_id>[.<granularity>][.dedup].json (the turn, non-dedup run
keeps the legacy <question_id>.json name):
{"question_id":"e47becba","category":"single-session-user","mode":"rerank","granularity":"chunk","dedup_sessions":true,"hit_1":true,"hit_5":true,"hit_10":true,"latency_ms":195234}
On startup the harness loads existing checkpoints into the running stats, then skips any question with a matching checkpoint. This means:
- Sleep / lid close: stops, resume seamlessly
- Kill / crash: stops, resume seamlessly
- Reboot: stops, resume seamlessly
- Want to run in batches:
--limit 50then--limit 100then--limit 150— each pass processes only what's not yet checkpointed
Atomic write (<file>.tmp + rename) means a kill mid-write doesn't corrupt the checkpoint file.
On the GPU rerank story
The reranker is a 568M-param cross-encoder. On CPU each query scores ~50 candidate (query, doc) pairs through the model — that's the 196 s/query observed locally. On A100:
onnxruntime-node-cudaputs inference on GPU- bge-reranker-v2-m3 in q8 quantization, batched 50 pairs per call → expected ~2-5 s/query
- Full 500 questions → ~30-60 min wall time
The Dockerfile builds with onnxruntime-node by default; for GPU set PLUR_ONNX_PROVIDER=cuda and the compose gpu profile mounts NVIDIA devices.
Continuous integration
CI is a keyless gate — it never downloads the real corpus and never calls an
external API (judge logic is covered by the mock; real-judge runs are manual).
.github/workflows/ci.yml runs on every push and PR:
pnpm typecheck # tsc --noEmit
pnpm test # unit tests (schema, scoring, judge-mock, report, …)
pnpm ci-smoke # run the harness on corpus/fixture-longmemeval.json, then assert:
# • a schema-valid manifest is produced
# • fixture R@5 lands in the pinned band [0.8, 1.0]
# • the rendered report matches test/golden/fixture-comparison.md
A PR that breaks the manifest schema, scoring, or the report generator fails the
gate. If you intentionally change the registry or report format, regenerate the
golden with pnpm ci-smoke:bless and commit it.
Next bench-repo improvements (for the colleague)
- GPU verification — confirm
onnxruntime-node-cudaactually accelerates bge-reranker-v2-m3 on the A100 (the 10-50× claim needs a measurement) - LLM-as-judge scoring mode — ✅ shipped:
--scoring llm_judge(see Scoring modes). Remaining: run it with a real judge on the full corpus and record the accuracy figure. - OpenAI-3-large run —
PLUR_EMBEDDER=openai-3-largewith the Matryoshka 1536d truncation we discussed. Expected R@5 ≈ 97-98% (matching or beating gbrain's headline). plur sync --reembedintegration — currently we re-embed per question; on the A100 this is fine but could be cached across the corpus if memory allows.
License
Apache 2.0.