perf(memory): hoist loop-invariant HRR encodes out of retrieval loops
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.
This commit is contained in:
@@ -70,6 +70,15 @@ class FactRetriever:
|
||||
|
||||
# Stage 2: Rerank with Jaccard + trust + optional decay
|
||||
query_tokens = self._tokenize(query)
|
||||
# The query vector is loop-invariant — encode it at most once, on
|
||||
# the first candidate that actually carries an HRR vector. Lazy on
|
||||
# purpose: migrated stores can have FTS candidates whose hrr_vector
|
||||
# was never backfilled (MemoryStore._init_db adds the column
|
||||
# without backfilling), and those must not pay for an encode
|
||||
# nothing will use. encode_text is deterministic (SHA-256 counter
|
||||
# blocks), so the hoisted vector is bit-identical to what the
|
||||
# per-candidate calls produced.
|
||||
query_vec = None
|
||||
scored = []
|
||||
|
||||
for fact in candidates:
|
||||
@@ -83,7 +92,8 @@ class FactRetriever:
|
||||
# HRR similarity
|
||||
if self.hrr_weight > 0 and fact.get("hrr_vector"):
|
||||
fact_vec = hrr.bytes_to_phases(fact["hrr_vector"])
|
||||
query_vec = hrr.encode_text(query, self.hrr_dim)
|
||||
if query_vec is None:
|
||||
query_vec = hrr.encode_text(query, self.hrr_dim)
|
||||
hrr_sim = (hrr.similarity(query_vec, fact_vec) + 1.0) / 2.0 # shift to [0,1]
|
||||
else:
|
||||
hrr_sim = 0.5 # neutral
|
||||
@@ -173,6 +183,9 @@ class FactRetriever:
|
||||
# Final fallback: keyword search
|
||||
return self.search(entity, category=category, limit=limit)
|
||||
|
||||
# role_content is loop-invariant — encode it once (deterministic
|
||||
# SHA-256-based atom) instead of once per fact row.
|
||||
role_content = hrr.encode_atom("__hrr_role_content__", self.hrr_dim)
|
||||
scored = []
|
||||
for row in rows:
|
||||
fact = dict(row)
|
||||
@@ -180,7 +193,6 @@ class FactRetriever:
|
||||
# Unbind probe key from fact to see if entity is structurally present
|
||||
residual = hrr.unbind(fact_vec, probe_key)
|
||||
# Compare residual against content signal
|
||||
role_content = hrr.encode_atom("__hrr_role_content__", self.hrr_dim)
|
||||
content_vec = hrr.bind(hrr.encode_text(fact["content"], self.hrr_dim), role_content)
|
||||
sim = hrr.similarity(residual, content_vec)
|
||||
fact["score"] = (sim + 1.0) / 2.0 * fact["trust_score"]
|
||||
@@ -234,6 +246,10 @@ class FactRetriever:
|
||||
|
||||
# Score each fact by how much the entity's atom appears in its vector
|
||||
# This catches both role-bound entity matches AND content word matches
|
||||
# Both role atoms are loop-invariant — encode them once here
|
||||
# (deterministic SHA-256-based atoms) instead of twice per fact row.
|
||||
role_entity = hrr.encode_atom("__hrr_role_entity__", self.hrr_dim)
|
||||
role_content = hrr.encode_atom("__hrr_role_content__", self.hrr_dim)
|
||||
scored = []
|
||||
for row in rows:
|
||||
fact = dict(row)
|
||||
@@ -243,8 +259,6 @@ class FactRetriever:
|
||||
residual = hrr.unbind(fact_vec, entity_vec)
|
||||
# A high-similarity residual to ANY known role vector means this entity
|
||||
# plays a structural role in the fact
|
||||
role_entity = hrr.encode_atom("__hrr_role_entity__", self.hrr_dim)
|
||||
role_content = hrr.encode_atom("__hrr_role_content__", self.hrr_dim)
|
||||
|
||||
entity_role_sim = hrr.similarity(residual, role_entity)
|
||||
content_role_sim = hrr.similarity(residual, role_content)
|
||||
|
||||
@@ -95,3 +95,141 @@ def test_prefetch_recovers_prose_query(retriever_with_facts):
|
||||
assert "deployment rollback" in results[0]["content"].lower()
|
||||
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Loop-invariant encode hoists (perf) — search/probe/related must encode
|
||||
# constant vectors ONCE per call, not once per candidate/row.
|
||||
# encode_text/encode_atom are deterministic (SHA-256 counter blocks), so the
|
||||
# hoisted vectors are bit-identical to the per-iteration values they replace.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
from plugins.memory.holographic import holographic as hrr
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hoisted_retriever():
|
||||
"""30 facts with HRR vectors, default dim (smaller dims trip an
|
||||
inhomogeneous-shape edge in the fact encoder)."""
|
||||
store = MemoryStore(":memory:")
|
||||
for i in range(30):
|
||||
store.add_fact(
|
||||
content=f"deploy target {i} setting alpha beta gamma option {i % 7}",
|
||||
category="fact" if i % 2 else "preference",
|
||||
tags=f"entity_{i % 5} deploy",
|
||||
)
|
||||
retriever = FactRetriever(store=store)
|
||||
yield retriever
|
||||
store.close()
|
||||
|
||||
|
||||
def _counting_spy(monkeypatch, attr):
|
||||
calls = []
|
||||
real = getattr(hrr, attr)
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
calls.append(args)
|
||||
return real(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(hrr, attr, wrapper)
|
||||
return calls
|
||||
|
||||
|
||||
def test_encode_functions_are_deterministic():
|
||||
"""Soundness premise of the hoists: same input -> identical vector."""
|
||||
import numpy as np
|
||||
|
||||
assert np.array_equal(hrr.encode_text("deploy target", 1024),
|
||||
hrr.encode_text("deploy target", 1024))
|
||||
assert np.array_equal(hrr.encode_atom("__hrr_role_content__", 1024),
|
||||
hrr.encode_atom("__hrr_role_content__", 1024))
|
||||
|
||||
|
||||
def test_search_encodes_query_vector_once(hoisted_retriever, monkeypatch):
|
||||
calls = _counting_spy(monkeypatch, "encode_text")
|
||||
results = hoisted_retriever.search("deploy target setting")
|
||||
assert results # the HRR path actually engaged
|
||||
assert len(calls) == 1, (
|
||||
f"query vector encoded {len(calls)}x in one search() — "
|
||||
"loop-invariant hoist regressed"
|
||||
)
|
||||
|
||||
|
||||
def test_search_results_bit_identical_to_unhoisted(hoisted_retriever):
|
||||
"""Parity: hoisted search() must produce the exact pre-fix results.
|
||||
|
||||
Replicates the pre-fix loop (query vector encoded per candidate) as the
|
||||
reference and compares full scored output for exact equality.
|
||||
"""
|
||||
r = hoisted_retriever
|
||||
query = "deploy target setting"
|
||||
new_results = r.search(query)
|
||||
|
||||
# --- pre-fix reference ---
|
||||
candidates = r._fts_candidates(query, None, 0.3, 10 * 3)
|
||||
query_tokens = r._tokenize(query)
|
||||
scored = []
|
||||
for fact in candidates:
|
||||
content_tokens = r._tokenize(fact["content"])
|
||||
tag_tokens = r._tokenize(fact.get("tags", ""))
|
||||
all_tokens = content_tokens | tag_tokens
|
||||
jaccard = r._jaccard_similarity(query_tokens, all_tokens)
|
||||
fts_score = fact.get("fts_rank", 0.0)
|
||||
if r.hrr_weight > 0 and fact.get("hrr_vector"):
|
||||
fact_vec = hrr.bytes_to_phases(fact["hrr_vector"])
|
||||
query_vec = hrr.encode_text(query, r.hrr_dim) # per-candidate
|
||||
hrr_sim = (hrr.similarity(query_vec, fact_vec) + 1.0) / 2.0
|
||||
else:
|
||||
hrr_sim = 0.5
|
||||
relevance = (r.fts_weight * fts_score
|
||||
+ r.jaccard_weight * jaccard
|
||||
+ r.hrr_weight * hrr_sim)
|
||||
fact["score"] = relevance * fact["trust_score"]
|
||||
scored.append(fact)
|
||||
scored.sort(key=lambda x: x["score"], reverse=True)
|
||||
old_results = scored[:10]
|
||||
for fact in old_results:
|
||||
fact.pop("hrr_vector", None)
|
||||
|
||||
assert new_results == old_results
|
||||
|
||||
|
||||
def test_related_encodes_role_atoms_once(hoisted_retriever, monkeypatch):
|
||||
calls = _counting_spy(monkeypatch, "encode_atom")
|
||||
results = hoisted_retriever.related("entity_1")
|
||||
assert results
|
||||
role_calls = [a for a in calls
|
||||
if a and str(a[0]).startswith("__hrr_role_")]
|
||||
assert len(role_calls) == 2, (
|
||||
f"role atoms encoded {len(role_calls)}x in one related() — "
|
||||
"expected exactly 2 (role_entity + role_content, hoisted)"
|
||||
)
|
||||
|
||||
|
||||
def test_probe_encodes_role_atom_once(hoisted_retriever, monkeypatch):
|
||||
calls = _counting_spy(monkeypatch, "encode_atom")
|
||||
results = hoisted_retriever.probe("entity_1")
|
||||
assert results
|
||||
role_content_calls = [a for a in calls
|
||||
if a and a[0] == "__hrr_role_content__"]
|
||||
assert len(role_content_calls) == 1, (
|
||||
f"role_content atom encoded {len(role_content_calls)}x in one "
|
||||
"probe() — loop-invariant hoist regressed"
|
||||
)
|
||||
|
||||
|
||||
def test_search_without_vectors_never_encodes(hoisted_retriever, monkeypatch):
|
||||
"""Migrated DBs can have FTS candidates with NULL hrr_vector
|
||||
(MemoryStore._init_db adds the column without backfilling existing
|
||||
facts). The lazy hoist must not encode a query vector nothing will
|
||||
use — pre-fix main encoded only beneath fact.get('hrr_vector')."""
|
||||
store = hoisted_retriever.store
|
||||
store._conn.execute("UPDATE facts SET hrr_vector = NULL")
|
||||
store._conn.commit()
|
||||
calls = _counting_spy(monkeypatch, "encode_text")
|
||||
results = hoisted_retriever.search("deploy target setting")
|
||||
assert results # candidates exist; neutral hrr_sim=0.5 path
|
||||
assert calls == [], (
|
||||
f"encode_text called {len(calls)}x with zero vector candidates — "
|
||||
"lazy hoist regressed to eager"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user