Vector-symbolic memory — HRR substrate alongside the cell-based architecture
Decision 0018: Vector-symbolic memory
Date: 2026-05-25 Status: Implemented as opt-in mechanism; default off after empirical disconfirmation on the entry-009 benchmark suite Builds on: ADR 0017 (symmetric slot binding) — the simpler mechanism this one was designed to surpass Motivated by: Entries 007 → 009 — symmetric binding gives the substrate commutativity retrieval but not function approximation; a vector-symbolic memory was the next architectural commitment that could plausibly give it true compositional generalization
Context
Entry 007’s analysis named three approaches to the slot-cell limitation entries 002–005 identified. Approach A — symmetric binding — was implemented in ADR 0017 and produced the project’s first headline-number win on Task 2 (3.3% → 73.3%). Approaches B and C were parked.
Approach B was vector-symbolic binding: each token gets a high-dimensional vector, binding is an algebraic operation (circular convolution) that combines vectors into superposable representations, and a single memory vector accumulates all observed bind(query, response) associations. Retrieval is via unbinding plus cleanup against a codebook of known token vectors.
Entry 009’s analysis sharpened the question approach B was supposed to answer. The transformer on Task 9 (strict held-out multiplication) was confidently wrong — a function approximator with insufficient data. Symmetric Reverie was diffusely guessing — frequency-bias retrieval with no function approximator at all. Approach B was the architectural commitment that would give Reverie a function approximator and let it be wrong like the transformer, which entry 009 argued is a property worth having.
This ADR is the implementation, the empirical result, and the negative-result documentation. The mechanism is correct. The hypothesis it tested did not survive contact with the benchmark.
Decision
A substrate-level VSA mechanism (not a cell type)
VSA is added as a substrate-level memory gated by vsa_enabled: bool = False, not as a sixth cell type. The substrate already has five cell types and seventeen ADRs of cell-by-cell mechanism. VSA operates on the whole vocabulary at once, has a single shared memory trace, and doesn’t fit the per-cell allocation pattern.
When enabled, the substrate maintains:
_token_codebook: shape(vocab_size, vsa_dim). Each row is a unit-norm random vector, instantiated on first observation of each token. Rows are immutable in the default config; withvsa_embedding_lr > 0they update via a Hebbian co-occurrence rule._role_codebook: shape(K, vsa_dim)where K iscontext_window_size. Random unit vectors fixed at initialization, one per position in the K-token window._vsa_memory: shape(vsa_dim,). A single accumulator vector that superposes all observed bind(query, response) traces.
Binding via circular convolution
The HRR primitives live in reverie/vsa.py. Binding is real-valued circular convolution via FFT: bind(a, b) = ifft(fft(a) * fft(b)). Unbinding is circular correlation: unbind(a, c) = bind(involution(a), c). The involution reverses elements 1..D-1, leaving element 0 fixed; in the frequency domain this is complex conjugation.
The recovery is approximate. For real Gaussian unit-norm vectors at D=512, unbind(a, bind(a, b)) has cosine similarity to b around 0.7 — substantially better than the ~0.05 expected for random vectors but well below 1.0. The architecture is designed around this imperfect recovery: a cleanup memory (here, the token codebook) snaps the noisy retrieved vector back to its closest known token.
Observation: learn
Inside observe(), right after context-cell template claiming (so the window already holds the K context tokens but does not yet include the just-observed idx):
query = sum_k role_codebook[k] ⊛ token_codebook[window[k]]
memory += query ⊛ token_codebook[idx]
This stores the association “window-context → idx” into the single superposed memory.
Generation: predict
Inside generate(), alongside the existing cell-based score contributions:
query = sum_k role_codebook[k] ⊛ token_codebook[window[k]]
recovered = unbind(query, memory)
sim_vector = cosine_similarity(recovered, token_codebook) # shape (vocab_size,)
scores += vsa_contribution_scale * sim_vector
vsa_contribution_scale (default 1.0) controls how strongly VSA’s prediction competes with the cell-based scores. For benchmark experiments it was raised to 100.0 because the cell-based scores accumulate over training into ranges of hundreds, while cosine similarities are bounded in [-1, 1].
Optional: Hebbian codebook updates
When vsa_embedding_lr > 0, every observation also nudges the just-observed token’s codebook vector toward the centroid of the other tokens in the current window, and re-normalizes:
others = window tokens != observed
centroid = mean(codebook[others])
codebook[observed] += lr * (centroid - codebook[observed])
codebook[observed] /= ||codebook[observed]||
This was meant to give the codebook distributional structure so that tokens playing similar roles develop similar vectors, enabling compositional generalization. See “What the benchmark showed” below for why it does not in practice.
What the benchmark showed (the negative result)
Re-running the full nine-task benchmark with --variant vsa (random codebook, no embedding learning) at five seeds:
| Task | Frozen (0012) | Symmetric (0017) | VSA random (0018) | Transformer |
|---|---|---|---|---|
| Task 1, N=2048 | 36.1% | 36.1% | 36.1% | 100% |
| Task 2 (random held-out add) | 3.3% | 73.3% | 20.0% | 73.3% |
| Task 3 (cycle) | 100% | 100% | 100% | 100% |
| Task 4, A after phase 2 | 62.2% | 62.2% | 15.6% | 43.3% |
| Task 4, B after phase 2 | 5.6% | 5.6% | 15.6% | 100% |
| Task 5, K=512 | 4.4% | 4.4% | 7.8% | 100% |
| Task 6 (strict held-out add) | 13.3% | 3.3% | 3.3% | 0% |
| Task 7 (max held-out) | 10.0% | 73.3% | 10.0% | 96.7% |
| Task 8 (mult held-out) | 5.0% | 65.0% | 0.0% | 60.0% |
| Task 9 (strict mult held-out) | 15.0% | 50.0% | 0.0% | 0% |
VSA does not match symmetric on any task. It is broadly comparable to the frozen ADR 0012 baseline. The architecture is doing essentially the same thing the original slot cells did: associative memory storage and retrieval, with the storage now happening in a single superposed vector rather than per-cell dictionaries.
The reason is theoretical, not implementation: classical HRR with random token vectors gives associative memory, not function approximation. Generalization beyond stored associations requires the token vectors to carry structure — tokens that should produce similar responses must have similar vectors. Random vectors don’t.
The Hebbian-embedding variant (vsa_hebbian) was an attempt to learn that structure from co-occurrence statistics. It made things worse — Task 2 with embedding learning produced 0.0% accuracy. The naive Hebbian rule (nudge each token toward the centroid of co-occurring tokens) causes vector collapse: all token vectors trend toward the population mean, destroying the discriminability that binding/unbinding depends on. A proper distributional embedding would need contrastive updates or a normalization scheme that preserves between-token spread; this is its own research problem and is left as a deferred extension.
What “implemented” means here
The HRR primitives in reverie/vsa.py are correct (eight unit tests pass, including the defining bind/unbind round-trip and the multi-association superposition + cleanup retrieval). The substrate integration is correct (six end-to-end tests pass, including memory accumulation, trained-pattern retrieval, generate-time contribution to scores, and the Hebbian update path). The empirical claim being made is bounded: the implementation is correct and the resulting architecture does not lift the entry-009 benchmark suite over the simpler symmetric-binding mechanism.
This is the second ADR in the project’s history (after ADR 0015) that ships with its own empirical disconfirmation. The mechanism is preserved as an opt-in (vsa_enabled=False default) for future experiments — particularly any attempt at a better embedding learning rule that would test whether learned codebook structure can give the substrate the function approximation that random codebook cannot.
Defaults
vsa_enabled: bool = False— opt-in.vsa_dim: int = 512— sufficient for vocabularies under ~100 tokens at the current cleanup-noise tolerance.vsa_embedding_lr: float = 0.0— no Hebbian updates by default. The variant that uses it is the experimental one whose result was disconfirmed.vsa_contribution_scale: float = 1.0— the natural cosine-similarity scale. For experiments where VSA needs to compete with accumulated cell-based scores in the hundreds, the benchmark variant raises this to 100.0.
Behavioral predictions
Tests in tests/test_vsa.py (8 tests on the primitives module) and tests/test_vsa_cells.py (6 end-to-end tests on the Reverie integration):
- Random unit vectors are unit-norm.
- Binding is commutative within float error.
unbind(a, bind(a, b))recoversbwith cosine similarity > 0.5 at D=512 (the cleanup-memory threshold for the architecture’s vocab sizes).- Unbinding with the wrong key does not recover
b. - Superposing 8 (query, response) pairs in a single memory vector and unbinding with each query in turn correctly cleans up to each response.
- Default config is transparent — no VSA state allocated.
- When enabled, the token codebook grows with vocabulary; each row is unit-norm.
- The memory accumulates only after the window is full.
- After training on a repeated pattern, the memory recovers the trained response at the trained context.
- The VSA contribution measurably biases
generate()toward predicted tokens. - With
vsa_embedding_lr > 0, co-occurring tokens develop higher cosine similarity than non-co-occurring tokens (confirming the Hebbian rule fires; this is not a claim that the rule produces useful representations on the benchmark).
Open questions deferred
- Contrastive or normalized embedding learning. The naive Hebbian rule collapses. A proper distributional embedding (skip-gram-style negative sampling, contrastive divergence, or a centering+whitening normalization) might preserve discriminability while still encoding structure. Real research; not the right next ADR for Reverie specifically, but a precondition for any future VSA-based extension.
- Unitary HRR. Replacing real Gaussian vectors with frequency-domain unit-magnitude vectors gives noise-free bind/unbind recovery. Would reduce reliance on cleanup memory; might improve benchmark numbers; doesn’t change the fundamental random-vectors-give-no-structure problem.
- Per-position roles vs. shared roles. Currently each window position has its own role vector. Alternatives (single shared role, learned role embeddings) might or might not help; not benchmarked.
- The discoverable-slot-structure direction (approach C from entry 006). Still parked, and now the natural next direction if Reverie continues — a substrate that learns which positions vary across observations would be a different mechanism than VSA, more aligned with the project’s “from observation alone” framing.