hdcstorage / hdc /slicegate.py
AEUPH's picture
HDCSTORAGE: hyperdimensional computer, webOS and agentic MCP layer (stdlib-only Python)
ad474c9 verified
Raw History Blame Contribute Delete
10.4 kB
"""Wide slice gates: several bits of adder per stage.
The 1-bit gate in :mod:`hdc.adder` is 8 rows and 32 stages. A gate that
handles ``k`` bits at once is ``2**(2k+1)`` rows and ``ceil(32/k)`` stages, so
the trade is explicit:
k=1 8 rows 32 stages
k=2 32 rows 16 stages
k=4 512 rows 8 stages
k=8 131072 rows 4 stages
Everything here is content-addressable in the same sense as the 1-bit gate: a
stage's inputs select a row *by vector resemblance*, and the row's outputs are
recovered by unbinding fields and cleaning them up against the gate atoms.
Two design points that are easy to get wrong
--------------------------------------------
**A lazy table.** 131 072 rows at d=10 000 would be 164 MB of vectors, and
enumerating them costs ~1.6 s of XOR folding before the first addition. Since
a 32-bit add only ever touches 4 byte-rows, the table is materialised *on
demand*: each visited row is composed, decoded and written into storage once,
then cached. The worst case is still complete; the common case is 4 rows.
**Exact keys, similarity fallback.** A gate key is an exact XOR of
slot-and-position atoms, so it is looked up in a dictionary — a 131 072-row
similarity scan would cost more than the 32-stage ripple it replaces. The
fuzzy path is still there (:meth:`SliceGate.resolve_similar`) because a
damaged key should degrade to "the nearest row" rather than fail; it is just
not on the hot path. Any row resolution therefore reports a confidence, and
an exact hit reports 1.0.
Why the key does not collapse
-----------------------------
:mod:`hdc.adder` documents the parity bug: XORing a *shared* zero/one pair
loses everything but parity, so ``1^1^1`` and ``1^0^0`` collide. Here every
(slot, position, value) triple has its own atom, so the key is an XOR of
distinct vectors encoding all ``2k+1`` inputs, and the 2**(2k+1) keys stay
distinct.
"""
from __future__ import annotations
from dataclasses import dataclass
from .hypervec import (
CODEC_THRESHOLD,
DEFAULT_DIM,
bind,
bundle,
hvc,
role,
sim,
)
from .memory import HDCStorage
#: Above this many rows a gate is built lazily instead of eagerly.
EAGER_MAX_ROWS = 4096
@dataclass(frozen=True)
class SliceStep:
"""One multi-bit slice of the carry chain."""
index: int
a: int
b: int
carry_in: int
total: int
carry_out: int
confidence: float
radix: int
def render(self) -> str:
mask = (1 << self.radix) - 1
return (
"slice %2d..%-2d a=0x%0*x b=0x%0*x cin=%d -> sum=0x%0*x cout=%d"
" (lookup sim %.3f)"
% (
self.index,
self.index + self.radix - 1,
self.radix // 4 or 1,
self.a & mask,
self.radix // 4 or 1,
self.b & mask,
self.carry_in,
self.radix // 4 or 1,
self.total & mask,
self.carry_out,
self.confidence,
)
)
@dataclass(frozen=True)
class SliceRow:
"""One materialised gate row."""
a: int
b: int
carry_in: int
total: int
carry_out: int
key: int
cell: str
def render(self) -> str:
return "a=0x%x b=0x%x cin=%d -> sum=0x%x cout=%d %s" % (
self.a, self.b, self.carry_in, self.total, self.carry_out, self.cell
)
class SliceGate:
"""A ``k``-bit adder slice whose truth table lives in HDC storage."""
def __init__(
self,
radix: int = 8,
d: int = DEFAULT_DIM,
storage: HDCStorage | None = None,
threshold: float = CODEC_THRESHOLD,
eager: bool | None = None,
):
if not 1 <= radix <= 8:
raise ValueError("radix must be between 1 and 8 bits")
self.radix = radix
self.d = d
self.threshold = threshold
self.row_count = 1 << (2 * radix + 1)
self.eager = (self.row_count <= EAGER_MAX_ROWS) if eager is None else eager
self.storage = storage if storage is not None else HDCStorage(
d=d, capacity=max(64, self.row_count if self.eager else 4096),
label="gate%d" % radix,
)
#: One atom per (slot, position, value): this is what keeps keys distinct.
self.a_atoms = {(i, b): hvc("gate/s%d/a/%d/%d" % (radix, i, b), d)
for i in range(radix) for b in (0, 1)}
self.b_atoms = {(i, b): hvc("gate/s%d/b/%d/%d" % (radix, i, b), d)
for i in range(radix) for b in (0, 1)}
self.cin_atoms = {b: hvc("gate/s%d/cin/%d" % (radix, b), d) for b in (0, 1)}
self.cout_atoms = {b: hvc("gate/s%d/cout/%d" % (radix, b), d) for b in (0, 1)}
self.sum_atoms = {(i, b): hvc("gate/s%d/sum/%d/%d" % (radix, i, b), d)
for i in range(radix) for b in (0, 1)}
self.rows: dict[int, SliceRow] = {}
self.lookups = 0
self.materialised = 0
self.similarity_fallbacks = 0
if self.eager:
self._build_all()
# ------------------------------------------------------------- the gate
def key_of(self, a: int, b: int, c: int) -> int:
"""Exact gate address: XOR of every slot/position/value atom."""
key = 0
for i in range(self.radix):
key ^= self.a_atoms[(i, (a >> i) & 1)]
for i in range(self.radix):
key ^= self.b_atoms[(i, (b >> i) & 1)]
key ^= self.cin_atoms[1 if c else 0]
return key & ((1 << self.d) - 1)
def _cell_name(self, a: int, b: int, c: int) -> str:
width = (2 * self.radix + 1 + 3) // 4
return "adder/s%d/%0*x" % (self.radix, width, (c << (2 * self.radix)) | (b << self.radix) | a)
def _build_row(self, a: int, b: int, c: int) -> SliceRow:
"""Compose the row's record, write it to storage, and memoise it."""
mask = (1 << self.radix) - 1
total = (a + b + c) & mask
carry = (a + b + c) >> self.radix
key = self.key_of(a, b, c)
fields = [
bind(role("key", self.d), key, self.d),
bind(role("carry", self.d), self.cout_atoms[carry], self.d),
]
for i in range(self.radix):
fields.append(
bind(role("sum%d" % i, self.d), self.sum_atoms[(i, (total >> i) & 1)], self.d)
)
record = bundle(fields, self.d)
cell = self._cell_name(a, b, c)
self.storage.write(cell, record, symbol=True)
row = SliceRow(a, b, c, total, carry, key, cell)
self.rows[key] = row
self.materialised += 1
return row
def _build_all(self) -> None:
mask = (1 << self.radix) - 1
for c in (0, 1):
for b in range(mask + 1):
for a in range(mask + 1):
self._build_row(a, b, c)
def resolve(self, a: int, b: int, c: int) -> tuple[SliceRow, float]:
"""Look up the row for these inputs; materialise it on first use."""
self.lookups += 1
key = self.key_of(a, b, c)
row = self.rows.get(key)
if row is not None:
return row, 1.0
return self._build_row(a, b, c), 1.0
def resolve_similar(self, a: int, b: int, c: int) -> tuple[SliceRow, float]:
"""Content-addressable fallback: nearest materialised row.
Only useful when a key arrives damaged (or comes from outside this
gate). Exact keys resolve at 1.0 through :meth:`resolve`; this path
reports the similarity it actually achieved.
"""
key = self.key_of(a, b, c)
if key in self.rows:
return self.rows[key], 1.0
best_row, best = None, -2.0
for candidate, row in self.rows.items():
s = sim(candidate, key, self.d)
if s > best:
best, best_row = s, row
if best_row is None:
self.similarity_fallbacks += 1
return self._build_row(a, b, c), 0.0
self.similarity_fallbacks += 1
return best_row, best
# ------------------------------------------------------------- evaluate
def evaluate(self, a: int, b: int, c: int) -> tuple[int, int, float]:
"""Run one slice: resolve the row, then read its fields back out."""
row, key_confidence = self.resolve(a, b, c)
record = self.storage.read(row.cell)
carry, carry_margin = self._read_bit(
bind(record, role("carry", self.d), self.d), self.cout_atoms
)
total = 0
margins = [carry_margin]
for i in range(self.radix):
bit, margin = self._read_bit(
bind(record, role("sum%d" % i, self.d), self.d),
{0: self.sum_atoms[(i, 0)], 1: self.sum_atoms[(i, 1)]},
)
total |= bit << i
margins.append(margin)
if (row.total, row.carry_out) != (total, carry):
raise AssertionError(
"gate row read back as %s but was built as %s"
% ((total, carry), (row.total, row.carry_out))
)
return total, carry, min(min(margins), key_confidence)
def _read_bit(self, probe: int, atoms: dict) -> tuple[int, float]:
s0 = sim(probe, atoms[0], self.d)
s1 = sim(probe, atoms[1], self.d)
if s1 >= s0:
return 1, s1 - s0
return 0, s0 - s1
# ---------------------------------------------------------------- views
def summary(self) -> dict:
return {
"radix": self.radix,
"stages_for_32": (32 + self.radix - 1) // self.radix,
"table_rows": self.row_count,
"materialised": len(self.rows),
"eager": self.eager,
"lookups": self.lookups,
"similarity_fallbacks": self.similarity_fallbacks,
"cells": len(self.storage),
}
def table_text(self, limit: int = 12) -> str:
head = "gate radix=%d: %d rows total, %d materialised%s" % (
self.radix,
self.row_count,
len(self.rows),
" (eager)" if self.eager else " (lazy)",
)
rows = [head]
for row in list(self.rows.values())[:limit]:
rows.append(" " + row.render())
if len(self.rows) > limit:
rows.append(" ... %d more rows" % (len(self.rows) - limit))
return "\n".join(rows)