perf(memory): store holographic vectors as float32
This commit is contained in:
parent
54eafee30b
commit
958ffd1085
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue