perf(memory): hoist loop-invariant HRR encodes out of retrieval loops - #76142
perf(memory): hoist loop-invariant HRR encodes out of retrieval loops#76142spfcraze wants to merge 1 commit into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused performance cleanup. The current main premise is valid: search() encodes at plugins/memory/holographic/retrieval.py:86 per HRR candidate, while probe() and related() repeat role-atom encodes at lines 183 and 246-247.
Problems
plugins/memory/holographic/retrieval.py:76eagerly encodes the query wheneverhrr_weight > 0, even if no candidate has an HRR vector. Current main only encodes beneathfact.get("hrr_vector")(plugins/memory/holographic/retrieval.py:84-86). This matters for migrated databases:MemoryStore._init_db()can addhrr_vectorwithout backfilling existing facts (plugins/memory/holographic/store.py:178-182).
Suggested changes
- Guard the hoisted query encode on the presence of at least one candidate HRR vector, and add a NULL-vector candidate regression test asserting
encode_textis not called.
Automated hermes-sweeper review.
| # The query vector is loop-invariant — encode it once here instead | ||
| # of once per candidate inside the loop. encode_text is | ||
| # deterministic (SHA-256 counter blocks), so the hoisted vector is | ||
| # bit-identical to what the per-candidate calls produced. |
There was a problem hiding this comment.
Please defer this encode unless at least one candidate has hrr_vector. Current main only calls encode_text in that branch; migrated stores can contain FTS candidates with NULL vectors because _init_db() adds the column without backfilling existing facts.
FactRetriever.search() re-encoded the query vector once per candidate, related() re-encoded both role atoms once per fact row, and probe() re-encoded the role-content atom once per row. All three encoders are deterministic (SHA-256 counter blocks), so the hoisted vectors are bit-identical to the per-iteration values they replace. Measured (300-fact store, dim=1024, median of 30 calls): search() 11.62 -> 1.46 ms/call (8.0x; encode_text 30 -> 1 per call), related() 63.08 -> 16.17 ms/call (3.9x; encode_atom 601 -> 3 per call), probe() 431.93 -> 389.36 ms/call (1.1x; dominated by per-fact content encoding, which is inherent to the algorithm and unchanged). Tests: call-count regression tests for each hoist plus a bit-exact parity test of search() against the pre-fix per-candidate loop.
e0be660 to
da9e682
Compare
|
Fixed in da9e682 (amended, force-pushed). The hoist is now lazy: Regression test added as suggested: Hot case unchanged: vector-bearing stores still encode exactly once per |
|
Thanks @spfcraze — hoists verified sound (deterministic encodes, parity-pinned) and salvaged into #76881 with your authorship preserved via cherry-pick, plus one test fix: the MemoryStore(":memory:") fixture actually created a literal ./:memory: file that leaked state across runs. Closing in favor of the salvage. |
Review follow-up on the #76142 salvage: MemoryStore path-resolves and shares one process-wide connection per file, so MemoryStore(":memory:") creates a literal ./:memory: FILE whose state leaks across test runs — the second run of the file failed all three spy tests because the NULL-vector test had permanently wiped hrr_vector in the leaked db. tmp_path isolates each run; verified two consecutive runs green + full tests/plugins/memory/ green.
Review follow-up on the NousResearch#76142 salvage: MemoryStore path-resolves and shares one process-wide connection per file, so MemoryStore(":memory:") creates a literal ./:memory: FILE whose state leaks across test runs — the second run of the file failed all three spy tests because the NULL-vector test had permanently wiped hrr_vector in the leaked db. tmp_path isolates each run; verified two consecutive runs green + full tests/plugins/memory/ green.
What does this PR do?
Hoist loop-invariant HRR vector encodes out of FactRetriever hot loops. search() re-encoded the query vector once per candidate (30x redundant work per user message), related() re-encoded both role atoms per fact row (601 encode_atom calls where 3 suffice), and probe() re-encoded the role-content atom per row. All encoders are deterministic (SHA-256 counter blocks), so hoisting is bit-identical.
Related Issue
No direct issue — discovered via code review and reproduced live (see below).
Related PRs reviewed during the duplicate check (none covers this change):
Changes Made
fix/holographic-query-hoist— 2 file(s) changed vs base:plugins/memory/holographic/retrieval.pytests/plugins/memory/test_holographic_retrieval.pyplugins/memory/holographic/retrieval.py: 3 hoists (search query_vec, probe role_content, related role_entity+role_content), ~10 lines moved, zero behavior change. tests/plugins/memory/test_holographic_retrieval.py: +5 tests — encode determinism, call-count regression tests for each hoist (search: encode_text==1; related: role atoms==2; probe: role_content==1), and a bit-exact parity test of search() against the pre-fix per-candidate loop.
How to Test
Measured on a 300-fact store (dim=1024, median of 30 calls, repo venv): search() 11.62 -> 1.46 ms/call (8.0x; encode_text 30 -> 1 per call); related() 63.08 -> 16.17 ms/call (3.9x; encode_atom 601 -> 3 per call); probe() 431.93 -> 389.36 ms/call (1.1x — probe is dominated by per-fact CONTENT encoding, inherent to the algorithm and intentionally unchanged; only its redundant role-atom re-encode was hoisted).
Validation completed (recorded by prp):
tests/plugins/memory/test_holographic_retrieval.py.tests/plugins/memory/: branch 233 passed / 0 failed vs baseline 228 passed / 0 failed — zero branch-only failures.Logs
Sabotage verification output: