From 958ffd108525c7c469b463217bb67a740f0ff3e7 Mon Sep 17 00:00:00 2001 From: JabberELF Date: Fri, 22 May 2026 23:41:30 +0800 Subject: [PATCH] perf(memory): store holographic vectors as float32 --- plugins/memory/holographic/holographic.py | 73 ++++++-- plugins/memory/holographic/retrieval.py | 16 +- plugins/memory/holographic/store.py | 2 +- .../test_holographic_vector_storage.py | 166 ++++++++++++++++++ 4 files changed, 237 insertions(+), 20 deletions(-) create mode 100644 tests/plugins/test_holographic_vector_storage.py diff --git a/plugins/memory/holographic/holographic.py b/plugins/memory/holographic/holographic.py index e1401fde108da..3aa5ddcdbe08f 100644 --- a/plugins/memory/holographic/holographic.py +++ b/plugins/memory/holographic/holographic.py @@ -33,6 +33,7 @@ except ImportError: logger = logging.getLogger(__name__) _TWO_PI = 2.0 * math.pi +_FLOAT32_BLOB_PREFIX = b"HRR1" def _require_numpy() -> None: @@ -40,6 +41,12 @@ def _require_numpy() -> None: raise RuntimeError("numpy is required for holographic operations") +def _np(): + """Return the numpy module after the runtime availability guard.""" + _require_numpy() + return np # type: ignore[name-defined] + + def encode_atom(word: str, dim: int = 1024) -> "np.ndarray": """Deterministic phase vector via SHA-256 counter blocks. @@ -161,19 +168,63 @@ def encode_fact(content: str, entities: list[str], dim: int = 1024) -> "np.ndarr def phases_to_bytes(phases: "np.ndarray") -> bytes: - """Serialize phase vector to bytes. float64 tobytes — 8 KB at dim=1024.""" - _require_numpy() - return phases.tobytes() + """Serialize phase vectors as float32 blobs. - -def bytes_to_phases(data: bytes) -> "np.ndarray": - """Deserialize bytes back to phase vector. Inverse of phases_to_bytes. - - The .copy() call is required because frombuffer returns a read-only view - backed by the bytes object; callers expect a mutable array. + float32 halves SQLite BLOB storage versus the legacy float64 format + (4 KB + a 4-byte format prefix instead of 8 KB at dim=1024) while + preserving enough precision for phase-similarity retrieval. + ``bytes_to_phases`` keeps reading legacy float64 blobs for backward + compatibility. """ - _require_numpy() - return np.frombuffer(data, dtype=np.float64).copy() + numpy = _np() + payload = numpy.asarray(phases, dtype=numpy.float32).tobytes() + return _FLOAT32_BLOB_PREFIX + payload + + +def bytes_to_phases(data: bytes, dim: int | None = None) -> "np.ndarray": + """Deserialize a phase vector from new float32 or legacy float64 storage. + + New float32 blobs carry a small prefix so callers can round-trip without + knowing ``dim``. Legacy float64 blobs are raw NumPy bytes and remain + readable for backward compatibility. The returned array is copied and + promoted to float64 so downstream HRR math keeps the existing numerical + behavior. + """ + numpy = _np() + + if dim is not None: + float32_payload_bytes = dim * numpy.dtype(numpy.float32).itemsize + float32_blob_bytes = len(_FLOAT32_BLOB_PREFIX) + float32_payload_bytes + float64_bytes = dim * numpy.dtype(numpy.float64).itemsize + + if data.startswith(_FLOAT32_BLOB_PREFIX) and len(data) == float32_blob_bytes: + payload = data[len(_FLOAT32_BLOB_PREFIX):] + return numpy.frombuffer(payload, dtype=numpy.float32).astype(numpy.float64) + if len(data) == float64_bytes: + return numpy.frombuffer(data, dtype=numpy.float64).copy() + if data.startswith(_FLOAT32_BLOB_PREFIX): + payload_len = len(data) - len(_FLOAT32_BLOB_PREFIX) + raise ValueError( + f"HRR vector blob has {len(data)} bytes ({payload_len} payload bytes after " + f"the float32 prefix); expected {float32_blob_bytes} (prefixed float32) " + f"or {float64_bytes} (legacy float64) for dim={dim}" + ) + raise ValueError( + f"HRR legacy vector blob has {len(data)} bytes; expected " + f"{float64_bytes} (float64) for dim={dim}" + ) + + if data.startswith(_FLOAT32_BLOB_PREFIX): + payload = data[len(_FLOAT32_BLOB_PREFIX):] + if len(payload) % numpy.dtype(numpy.float32).itemsize != 0: + raise ValueError( + f"HRR float32 vector blob has invalid payload byte length: {len(payload)}" + ) + return numpy.frombuffer(payload, dtype=numpy.float32).astype(numpy.float64) + + if len(data) % numpy.dtype(numpy.float64).itemsize != 0: + raise ValueError(f"HRR legacy vector blob has invalid byte length: {len(data)}") + return numpy.frombuffer(data, dtype=numpy.float64).copy() def snr_estimate(dim: int, n_items: int) -> float: diff --git a/plugins/memory/holographic/retrieval.py b/plugins/memory/holographic/retrieval.py index 95b8f2ae95db3..2bbcb71f473e9 100644 --- a/plugins/memory/holographic/retrieval.py +++ b/plugins/memory/holographic/retrieval.py @@ -91,7 +91,7 @@ class FactRetriever: # HRR similarity if self.hrr_weight > 0 and fact.get("hrr_vector"): - fact_vec = hrr.bytes_to_phases(fact["hrr_vector"]) + fact_vec = hrr.bytes_to_phases(fact["hrr_vector"], dim=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] @@ -154,7 +154,7 @@ class FactRetriever: (bank_name,), ).fetchone() if bank_row: - bank_vec = hrr.bytes_to_phases(bank_row["vector"]) + bank_vec = hrr.bytes_to_phases(bank_row["vector"], dim=self.hrr_dim) extracted = hrr.unbind(bank_vec, probe_key) # Use extracted signal to score individual facts return self._score_facts_by_vector( @@ -189,7 +189,7 @@ class FactRetriever: scored = [] for row in rows: fact = dict(row) - fact_vec = hrr.bytes_to_phases(fact.pop("hrr_vector")) + fact_vec = hrr.bytes_to_phases(fact.pop("hrr_vector"), dim=self.hrr_dim) # Unbind probe key from fact to see if entity is structurally present residual = hrr.unbind(fact_vec, probe_key) # Compare residual against content signal @@ -253,7 +253,7 @@ class FactRetriever: scored = [] for row in rows: fact = dict(row) - fact_vec = hrr.bytes_to_phases(fact.pop("hrr_vector")) + fact_vec = hrr.bytes_to_phases(fact.pop("hrr_vector"), dim=self.hrr_dim) # Check structural similarity: unbind entity from fact residual = hrr.unbind(fact_vec, entity_vec) @@ -334,7 +334,7 @@ class FactRetriever: scored = [] for row in rows: fact = dict(row) - fact_vec = hrr.bytes_to_phases(fact.pop("hrr_vector")) + fact_vec = hrr.bytes_to_phases(fact.pop("hrr_vector"), dim=self.hrr_dim) entity_scores = [] for probe_key in entity_residuals: @@ -431,8 +431,8 @@ class FactRetriever: continue # Not enough entity overlap to be contradictory # Content similarity via HRR vectors - v1 = hrr.bytes_to_phases(f1["hrr_vector"]) - v2 = hrr.bytes_to_phases(f2["hrr_vector"]) + v1 = hrr.bytes_to_phases(f1["hrr_vector"], dim=self.hrr_dim) + v2 = hrr.bytes_to_phases(f2["hrr_vector"], dim=self.hrr_dim) content_sim = hrr.similarity(v1, v2) # High entity overlap + low content similarity = potential contradiction @@ -484,7 +484,7 @@ class FactRetriever: scored = [] for row in rows: fact = dict(row) - fact_vec = hrr.bytes_to_phases(fact.pop("hrr_vector")) + fact_vec = hrr.bytes_to_phases(fact.pop("hrr_vector"), dim=self.hrr_dim) sim = hrr.similarity(target_vec, fact_vec) fact["score"] = (sim + 1.0) / 2.0 * fact["trust_score"] scored.append(fact) diff --git a/plugins/memory/holographic/store.py b/plugins/memory/holographic/store.py index bf58d7a22dbcd..fcb193d4da227 100644 --- a/plugins/memory/holographic/store.py +++ b/plugins/memory/holographic/store.py @@ -561,7 +561,7 @@ class MemoryStore: self._conn.commit() return - vectors = [hrr.bytes_to_phases(row["hrr_vector"]) for row in rows] + vectors = [hrr.bytes_to_phases(row["hrr_vector"], dim=self.hrr_dim) for row in rows] bank_vector = hrr.bundle(*vectors) fact_count = len(vectors) diff --git a/tests/plugins/test_holographic_vector_storage.py b/tests/plugins/test_holographic_vector_storage.py new file mode 100644 index 0000000000000..56415b7446bd3 --- /dev/null +++ b/tests/plugins/test_holographic_vector_storage.py @@ -0,0 +1,166 @@ +"""Storage-size regression tests for holographic HRR vectors.""" + +from __future__ import annotations + +import pytest + +np = pytest.importorskip("numpy") + +from plugins.memory.holographic import holographic as hrr +from plugins.memory.holographic.retrieval import FactRetriever +from plugins.memory.holographic.store import MemoryStore + + +pytestmark = pytest.mark.skipif( + not hrr._HAS_NUMPY, + reason="holographic vector storage requires numpy", +) + + +def _float32_blob_size(dim: int) -> int: + return len(hrr._FLOAT32_BLOB_PREFIX) + dim * np.dtype(np.float32).itemsize + + +def test_phases_to_bytes_stores_float32_and_round_trips_with_dim() -> None: + dim = 1024 + phases = hrr.encode_atom("storage-size-regression", dim=dim) + + blob = hrr.phases_to_bytes(phases) + + assert len(blob) == _float32_blob_size(dim) + restored = hrr.bytes_to_phases(blob, dim=dim) + assert restored.shape == (dim,) + np.testing.assert_allclose(restored, phases, rtol=0, atol=1e-6) + + +def test_phases_to_bytes_round_trips_without_dim() -> None: + dim = 1024 + phases = hrr.encode_atom("dimensionless-round-trip", dim=dim) + + restored = hrr.bytes_to_phases(hrr.phases_to_bytes(phases)) + + assert restored.shape == (dim,) + np.testing.assert_allclose(restored, phases, rtol=0, atol=1e-6) + + +def test_phases_to_bytes_round_trips_ambiguous_small_dims_without_dim() -> None: + dim = 2 + phases = hrr.encode_atom("ambiguous-small-dimension", dim=dim) + + restored = hrr.bytes_to_phases(hrr.phases_to_bytes(phases)) + + assert restored.shape == (dim,) + np.testing.assert_allclose(restored, phases, rtol=0, atol=1e-6) + + +def test_bytes_to_phases_rejects_malformed_float32_blobs() -> None: + phases = hrr.encode_atom("malformed-float32-blob", dim=2) + blob = hrr.phases_to_bytes(phases) + + with pytest.raises(ValueError, match="expected .* for dim=3"): + hrr.bytes_to_phases(blob, dim=3) + + with pytest.raises(ValueError, match="invalid payload byte length"): + hrr.bytes_to_phases(hrr._FLOAT32_BLOB_PREFIX + b"x") + + +def test_bytes_to_phases_reads_legacy_float64_blobs_with_and_without_dim() -> None: + dim = 1024 + phases = hrr.encode_atom("legacy-float64-regression", dim=dim) + legacy_blob = phases.astype(np.float64, copy=False).tobytes() + + assert len(legacy_blob) == dim * np.dtype(np.float64).itemsize + restored_with_dim = hrr.bytes_to_phases(legacy_blob, dim=dim) + restored_without_dim = hrr.bytes_to_phases(legacy_blob) + + assert restored_with_dim.shape == (dim,) + assert restored_without_dim.shape == (dim,) + np.testing.assert_allclose(restored_with_dim, phases, rtol=0, atol=0) + np.testing.assert_allclose(restored_without_dim, phases, rtol=0, atol=0) + + +def test_bytes_to_phases_prefers_dim_matched_legacy_float64_on_prefix_collision() -> None: + dim = 4 + legacy_blob = hrr._FLOAT32_BLOB_PREFIX + b"\0" * ( + dim * np.dtype(np.float64).itemsize - len(hrr._FLOAT32_BLOB_PREFIX) + ) + + restored = hrr.bytes_to_phases(legacy_blob, dim=dim) + + assert restored.shape == (dim,) + np.testing.assert_array_equal( + restored, + np.frombuffer(legacy_blob, dtype=np.float64).copy(), + ) + + +def test_memory_store_reads_legacy_float64_vectors(tmp_path) -> None: + dim = 64 + db_path = tmp_path / "legacy_memory_store.db" + + with MemoryStore(db_path=db_path, hrr_dim=dim) as store: + fact_id = store.add_fact( + 'Bob Stone keeps "legacy HRR vectors" searchable.', + category="compat", + tags="legacy storage", + ) + + fact_blob = store._conn.execute( + "SELECT hrr_vector FROM facts WHERE fact_id = ?", + (fact_id,), + ).fetchone()["hrr_vector"] + bank_blob = store._conn.execute( + "SELECT vector FROM memory_banks WHERE bank_name = ?", + ("cat:compat",), + ).fetchone()["vector"] + + legacy_fact_blob = hrr.bytes_to_phases(fact_blob, dim=dim).astype(np.float64).tobytes() + legacy_bank_blob = hrr.bytes_to_phases(bank_blob, dim=dim).astype(np.float64).tobytes() + store._conn.execute( + "UPDATE facts SET hrr_vector = ? WHERE fact_id = ?", + (legacy_fact_blob, fact_id), + ) + store._conn.execute( + "UPDATE memory_banks SET vector = ? WHERE bank_name = ?", + (legacy_bank_blob, "cat:compat"), + ) + store._conn.commit() + + assert len(legacy_fact_blob) == dim * np.dtype(np.float64).itemsize + assert len(legacy_bank_blob) == dim * np.dtype(np.float64).itemsize + + retriever = FactRetriever(store, hrr_dim=dim) + results = retriever.search("legacy HRR vectors", category="compat", limit=1) + + assert results + assert results[0]["fact_id"] == fact_id + + +def test_memory_store_persists_fact_and_bank_vectors_as_float32(tmp_path) -> None: + dim = 64 + db_path = tmp_path / "memory_store.db" + + with MemoryStore(db_path=db_path, hrr_dim=dim) as store: + fact_id = store.add_fact( + 'Alice Smith stores "compact HRR vectors" for Python tests.', + category="perf", + tags="hrr storage", + ) + + fact_blob = store._conn.execute( + "SELECT hrr_vector FROM facts WHERE fact_id = ?", + (fact_id,), + ).fetchone()["hrr_vector"] + bank_blob = store._conn.execute( + "SELECT vector FROM memory_banks WHERE bank_name = ?", + ("cat:perf",), + ).fetchone()["vector"] + + assert len(fact_blob) == _float32_blob_size(dim) + assert len(bank_blob) == _float32_blob_size(dim) + + retriever = FactRetriever(store, hrr_dim=dim) + results = retriever.search("compact HRR vectors", category="perf", limit=1) + + assert results + assert results[0]["fact_id"] == fact_id