Skip to content

fix(benchmarks): honor --granularity across hybrid_v2/v3/v4; reject in palace/diary - #1490

Closed
nakata-app wants to merge 4 commits into
MemPalace:developfrom
nakata-app:fix/hybrid-v4-granularity
Closed

fix(benchmarks): honor --granularity across hybrid_v2/v3/v4; reject in palace/diary#1490
nakata-app wants to merge 4 commits into
MemPalace:developfrom
nakata-app:fix/hybrid-v4-granularity

Conversation

@nakata-app

@nakata-app nakata-app commented May 13, 2026

Copy link
Copy Markdown

Summary

build_palace_and_retrieve_hybrid_v4 (and three of its siblings) accepted a granularity parameter but never branched on it. With the defect in place, --granularity turn and --granularity session produced bitwise-identical metrics for those modes.

This PR fixes the dead-parameter defect across the affected modes, and includes a small benchmark sweep that uses the fix to test a hypothesis on hybrid_v4.

What's defective and what each commit does

function granularity branched before? this PR's fix
raw / aaak / rooms / hybrid / full yes none, already honored the flag
hybrid_v2 no per-turn corpus + session-id dedup (commit 2)
hybrid_v3 no per-turn corpus + session-id dedup (commit 2)
hybrid_v4 no per-turn corpus + session-id dedup (commit 1)
palace no explicit ValueError on granularity != "session", hall classification, drawers, and the preference wing are intrinsically session-keyed (commit 2)
diary no explicit ValueError on granularity != "session", LLM topic layer is computed and cached per sess_id (commit 2)

Fix shape (hybrid_v2 / v3 / v4)

  • Branch the corpus build on granularity. In turn mode, emit one doc per user turn with corpus IDs shaped as {sess_id}_turn_{i} so the existing session_id_from_corpus_id helper rolls turns up to sessions at eval time.
  • In turn mode, corpus_full[i] mirrors the full session text (not the single user turn) so Pass 2 of the assistant-reference two-pass has assistant content to query against. corpus_user[i] stays the granular per-turn signal.
  • Dedup by session id in both the assistant-reference two-pass and the main scoring path, so multiple high-scoring turns of the same session collapse to a single ranked entry.
  • Keep synthetic preference docs session-aggregated; map a pref-driven hit to the first user-turn index of its session.
  • Session-mode behavior is unchanged (sess_id == corpus_id, so session_id_from_corpus_id is a no-op there).

Sweep + hypothesis test (hybrid_v4)

benchmarks/c_beta/ (PLAN.md + RESULTS.md + scripts + logs). 50q dev split, default MiniLM, no LLM rerank. All numbers below are post-fix, the full 8-row matrix was re-run after the gemini-code-assist review caught a remaining defect in turn-mode corpus_full (see commit fbbbfa0 / 389650d).

mode gran hw R@1 R@10 NDCG@10
raw session , 0.840 0.980 0.905
raw turn , 0.880 0.960 0.885
hybrid_v4 session 0.0 0.880 1.000 0.937
hybrid_v4 session 0.30 0.880 1.000 0.944
hybrid_v4 session 0.60 0.880 0.980 0.936
hybrid_v4 turn 0.0 0.900 1.000 0.944
hybrid_v4 turn 0.30 0.920 1.000 0.954
hybrid_v4 turn 0.60 0.920 1.000 0.951

H0 (wash) rejected. Keyword-boost NDCG@10 lift hw=0.0→0.30: turn +0.010 vs session +0.007. Same concave shape (peak at hw=0.30, drop at hw=0.60) across both granularities, so the keyword signal is not a session-length artifact.

Secondary finding: post-fix, hybrid_v4 turn matches or beats hybrid_v4 session on NDCG@10 at every hw tested (Δ = +0.007, +0.010, +0.015 for hw = 0.0/0.30/0.60). The pre-fix table showed the inverse because turn mode was silently dropping assistant content from Pass 2. Sample is 50q, directional, not yet a default-flip recommendation. Full 500q bootstrap is the honest next step.

Test plan

  • 5q smoke on hybrid_v4 turn hw=0.30 → all hits
  • 50q dev-split sweep on hybrid_v4, 3 hw values, both granularities → see RESULTS.md
  • 5q smoke on hybrid_v2 turn hw=0.30 → all hits
  • 5q smoke on hybrid_v3 turn hw=0.30 → all hits
  • palace turn raises ValueError cleanly
  • palace session unchanged from baseline (R@10 1.000, NDCG@10 0.852)
  • Confirmed hybrid_v4 session metrics unchanged vs pre-fix run
  • gemini-code-assist review (3 items) addressed and re-validated by full sweep rerun
  • CI: ruff + pytest on PR

build_palace_and_retrieve_hybrid_v4 accepted a granularity parameter but
never branched on it: the corpus loop always emitted one document per
session, so `--granularity turn` was silently equivalent to `--granularity
session`. With the defect in place, hybrid_v4 turn and session runs
produced bitwise-identical metrics at every hybrid_weight.

Fix:
- Branch the corpus build on granularity. In turn mode, emit one doc per
  user turn with corpus IDs shaped as {sess_id}_turn_{i} so the existing
  session_id_from_corpus_id helper rolls turns up to sessions at eval.
- Dedup by session id in both the assistant-reference two-pass and the
  main scoring path, so multiple high-scoring turns of the same session
  collapse to a single ranked entry.
- Keep synthetic preference docs session-aggregated; map a pref-driven
  hit to the first user-turn index of its session.

C-β v0.1 sweep results (50q dev split, MiniLM, no LLM rerank) under
benchmarks/c_beta/. Wash hypothesis rejected: keyword-boost lift at turn
granularity (NDCG@10 +0.010) is comparable to session-level lift (+0.007),
with the same concave shape (peak at hw=0.30).

Other hybrid modes (hybrid, hybrid_v2, hybrid_v3, palace, diary, aaak,
rooms) likely carry the same dead-parameter defect — not addressed here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a benchmark sweep to evaluate the impact of session versus turn granularity on retrieval performance, specifically investigating the hybrid_v4 keyword boost. It includes shell scripts for running sweeps and parsing results, along with documentation of findings that revealed a previous defect where the granularity parameter was ignored. The core logic in benchmarks/longmemeval_bench.py was updated to support turn-level indexing and session-based deduplication. Feedback identifies a critical bug in turn-level indexing that breaks assistant-reference logic, the use of non-portable absolute paths in scripts, and a parsing error in the sweep script that results in incorrect metric reporting.

Comment thread benchmarks/longmemeval_bench.py Outdated
turn_idx += 1
continue
corpus_user.append(t["content"])
corpus_full.append(t["content"])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In turn granularity mode, corpus_full is currently populated with only the individual user turn content. This breaks the hybrid_v4 assistant-reference logic (Pass 1), which relies on corpus_full containing the assistant's responses to match quoted phrases. To ensure the two-pass functionality works as intended in turn mode, corpus_full should contain the full session text for each indexed turn.

Suggested change
corpus_full.append(t["content"])
corpus_full.append("\n".join(all_turns))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fbbbfa0. In turn mode corpus_full[i] now mirrors the full session text ("\n".join(all_turns)) while corpus_user[i] keeps the per-turn signal, so Pass 2 of the assistant-reference two-pass has assistant content to query against.

Same pattern applied to hybrid_v2 and hybrid_v3 (commit 0d2ffcb).

Re-ran the full 8-row matrix on the fixed code (389650d, benchmarks/c_beta/RESULTS.md and sweep_results.csv updated):

gran hw R@1 R@10 NDCG@10
session 0.30 0.880 1.000 0.944
turn 0.30 0.920 1.000 0.954

Pre-fix turn,hw=0.30 was NDCG@10 0.934 — confirms the two-pass was degraded in turn mode and the fix recovers +0.020.

Comment thread benchmarks/c_beta/run_sweep.sh Outdated
Comment on lines +3 to +9
DATA=/Users/macmini/Projects/metis-pair/benchmarks/data/longmemeval/longmemeval_s_cleaned.json
SPLIT=benchmarks/lme_split_50_450.json
OUTDIR=benchmarks/c_beta
LOG=$OUTDIR/sweep.log
CSV=$OUTDIR/sweep_results.csv

cd /Users/macmini/Projects/mempalace

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The script contains absolute paths specific to a local machine (e.g., /Users/macmini/...). This prevents the benchmark suite from being portable across different environments. It is recommended to use relative paths or environment variables to define project and data locations.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fbbbfa0. All three scripts (run_sweep.sh, run_turn_sweep.sh, rebuild_csv.sh) now resolve the repo root from BASH_SOURCE instead of hard-coding /Users/macmini/Projects/mempalace. The external dataset path lives behind a DATA env var with a sensible default and a clear error when the file is missing.

Comment thread benchmarks/c_beta/run_sweep.sh Outdated
Comment on lines +40 to +41
r1=$(awk '/SESSION-LEVEL METRICS/{flag=1;next} flag && /Recall@ 1:/ {print $2; exit}' "$stdout")
r5=$(awk '/SESSION-LEVEL METRICS/{flag=1;next} flag && /Recall@ 5:/ {print $2; exit}' "$stdout")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The awk field index for Recall@ 1: and Recall@ 5: is incorrect in this script. Due to the space between the @ symbol and the single-digit number in the output format, the numeric value resides in the 3rd field ($3), not the 2nd. This results in incorrect values being written to the CSV (e.g., 1: instead of the actual metric), as seen in the provided sweep.log.

Suggested change
r1=$(awk '/SESSION-LEVEL METRICS/{flag=1;next} flag && /Recall@ 1:/ {print $2; exit}' "$stdout")
r5=$(awk '/SESSION-LEVEL METRICS/{flag=1;next} flag && /Recall@ 5:/ {print $2; exit}' "$stdout")
r1=$(awk '/SESSION-LEVEL METRICS/{flag=1;next} flag && /Recall@ 1:/ {print $3; exit}' "$stdout")
r5=$(awk '/SESSION-LEVEL METRICS/{flag=1;next} flag && /Recall@ 5:/ {print $3; exit}' "$stdout")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fbbbfa0. Recall@ 1: / Recall@ 5: (single-digit @k with the extra space) now read $3; Recall@10: / Recall@30: (no space) still read $2. Inline comment in the script documents why the two cases differ. rebuild_csv.sh was already using $3 for the single-digit case, which is why the committed CSV was correct despite the bug in run_sweep.sh.

…e/diary

Audit of the remaining retrieval functions for the same dead-parameter
defect fixed in the previous commit on hybrid_v4.

hybrid_v2, hybrid_v3
    Same defect — the corpus loop joined all user turns into a single
    session-level doc regardless of --granularity. Applied the same fix
    pattern: branch the corpus build on granularity (turn mode emits one
    doc per user turn with {sess_id}_turn_{i} corpus IDs), and dedup by
    session id in both the assistant-reference two-pass and the main
    scoring path. Synthetic preference docs stay session-aggregated and
    resolve to the first user-turn index of their session.

palace, diary
    Algorithm is intrinsically session-keyed:
      - palace: hall classification, closets, drawers, and the preference
        wing are all per-session structures.
      - diary: the LLM topic layer is computed once per session and
        cached by sess_id.
    A turn-level rewrite would change the algorithm, not just the data
    layout, so the honest behavior is to reject the parameter rather
    than silently fall back to session. Both now raise a clear
    ValueError when called with granularity != "session".

raw / aaak / rooms / hybrid / full
    Already honored the flag at the corpus level. Not touched. Their
    index-based final dedup (vs. session-id dedup used in the hybrid_v2
    /v3/v4 fixes) is a separate concern outside this scope.

Smoke (5q dev split):
  - hybrid_v2 turn hw=0.30 → 1.000 R@k / NDCG@k
  - hybrid_v3 turn hw=0.30 → 1.000 R@k / NDCG@k
  - palace turn → ValueError as expected
  - palace session → unchanged from baseline (R@10 1.000, NDCG@10 0.852)
@nakata-app nakata-app changed the title fix(benchmarks): honor --granularity in hybrid_v4 retrieval fix(benchmarks): honor --granularity across hybrid_v2/v3/v4; reject in palace/diary May 13, 2026
1. corpus_full must hold session text in turn mode (HIGH, hybrid_v2/v3/v4)
   --------------------------------------------------------------
   The turn branch wrote only the single user turn into corpus_full,
   so the assistant-reference two-pass (Pass 2 queries corpus_full
   for quoted/assistant content) had nothing to match against in
   turn granularity. Fix: corpus_full now duplicates the full
   session text per user turn while corpus_user keeps the granular
   per-turn signal. Removed the now-stale "session-level boosts
   compensate" comment — they do not, the two-pass quoted match
   reads corpus_full directly.

2. run_sweep.sh: awk field index for single-digit @k (MEDIUM)
   ----------------------------------------------------------
   "Recall@ 1:" prints with a space, so the value lands in $3, not
   $2. The previous script wrote literal "1:" / "5:" into the CSV.
   rebuild_csv.sh already used $3 (which is why the committed CSV
   is correct), but run_sweep.sh would corrupt any fresh CSV.

3. Absolute paths broke portability (MEDIUM)
   ------------------------------------------
   run_sweep.sh, run_turn_sweep.sh, rebuild_csv.sh all hard-coded
   /Users/macmini/Projects/mempalace. Switched to BASH_SOURCE-based
   repo root resolution. DATA path moved behind an env var with a
   sensible default and a clear error when the file is missing.
After fixing the assistant-reference two-pass for turn granularity
(corpus_full now mirrors session text, not the single user turn), the
previous turn-mode rows in the sweep were measuring a degraded code
path. Re-ran the full 8-row matrix on the fixed code.

Key changes vs pre-fix table:
- hybrid_v4 turn R@10 saturates at 1.000 across all hw (was 0.980)
- hybrid_v4 turn NDCG@10 lifts +0.020 at hw=0.30 (0.934 → 0.954)
- Sign of session-vs-turn gap flipped: turn now ≥ session at every hw

Hypothesis result is unchanged: keyword boost lift survives at turn
granularity (+0.010 NDCG@10 lift hw=0.0→0.30 vs +0.007 at session),
same concave shape with peak at hw=0.30. H0 (wash) still rejected.

New secondary finding: turn ≥ session NDCG@10 across the whole sweep
(Δ = +0.007, +0.010, +0.015 for hw = 0.0/0.30/0.60). Pre-fix table
showed the inverse because turn mode was silently dropping assistant
content from Pass 2. Direction is clear on 50q but the sample is small;
full 500q bootstrap is the honest next step before promoting turn as
default.

RESULTS.md also corrects the stale caveat that claimed hybrid_v2/v3
were "not audited" — they got the same fix in commit 0d2ffcb.
@nakata-app

Copy link
Copy Markdown
Author

Hi maintainers — quick status on this PR:

  • Gemini's 3 review comments (1 HIGH on turn-mode corpus_full, 2 MEDIUM on run_sweep.sh absolute paths + awk field index) are all addressed in fbbbfa0.
  • 389650d reruns the full sweep post-fix; updated numbers are in benchmarks/c_beta/RESULTS.md. Headline: hybrid_v4 turn hw=0.30 → NDCG@10 0.954 (pre-fix 0.934, Δ +0.020); wash hypothesis still rejected, peak at hw=0.30.
  • CI shows action_required because this is a fork PR — would appreciate a workflow approval whenever you have a moment so the test matrix can run against the latest commits.

Happy to address any further feedback. Thanks!

jphein added a commit to techempower-org/mempalace that referenced this pull request May 15, 2026
…ranularity

cherry-pick: nakata-app upstream MemPalace#1490 — honor --granularity across hybrid_v2/v3/v4
jphein added a commit to techempower-org/mempalace that referenced this pull request May 15, 2026
…lete (#81)

README header was stale: said \"JP's fork of milla-jovovich/mempalace\"
(actual upstream is MemPalace/mempalace), pointed at jphein/* shields
and links (we transferred to techempower-org), reported a 160K-drawer
chromadb palace (now 273K-drawer postgres+pgvector+AGE), and described
the substrate cutover as in-flight on feat/pgvector-age-impl (cutover
shipped to main on 2026-05-13–15).

Refreshes:

* Title + author line + shield links → techempower-org
* Production palace size + backend → 273K-drawer postgres
* Substrate section status block → shipped, with refs to the operator
  runbook and bench artifact
* Storage layer bullet → "main as of 2026-05-15; chromadb path still
  works behind MEMPALACE_BACKEND=chroma"

CHANGELOG: new [Unreleased] section above [3.3.5] capturing the
fork-side work that landed 2026-05-14 / 2026-05-15 — postgres cutover,
hybrid retrieval (vector ∪ BM25 ∪ graph), symbol_header_prefix kwarg,
n=200 git-derived probe set, RRF verifier (2-way + 3-way), bench
captures, the pgvector lazy-index race fix, the EmbeddingFunction
.embed_query doc note, and the @nakata-app MemPalace#1490 cherry-pick.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@igorls igorls added bug Something isn't working area/search Search and retrieval labels May 15, 2026
@nakata-app

Copy link
Copy Markdown
Author

@igorls @milla-jovovich — CI hasn't triggered because this is a first-time contributor fork PR and GitHub is holding the workflow run pending maintainer approval. Could one of you click "Approve and run workflows" so ruff + pytest run against the PR? Thanks.

@nakata-app nakata-app closed this by deleting the head repository May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/search Search and retrieval bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants