Skip to content

Commit 587414d

Browse files
committed
transformerless_lm: substrate-native attention operator (Subsim)
Per the user "scale won't matter if there is no difference overall": the architecture needs to extrapolate DIFFERENTLY from a dense transformer, not just compress weights. SubsimLM introduces a fundamentally different attention operator: STANDARD: attn = softmax(Q . K^T / sqrt(d)) -- additive dot-product similarity in d-dim Euclidean space SUBSTRATE: sig = W_sig . x # [B, T, K] dist = L1(sig[i] - sig[j]) # [B, T, T] attn = softmax(-dist / sqrt(K)) -- L1 distance in K-dim Fibonacci-basis signature space. L1 IS the substrate's canonical nearness metric (same metric used in attractor snapping and Zeckendorf decomp). Substrate-native at TWO levels: - WEIGHTS: W_sig, W_v, W_out are FibGen (substrate-compressed seeds) - OPERATOR: L1-distance similarity, not dot product. Compute cost: attention scores are O(T^2 * K) instead of O(T^2 * d). At d=4096 K=32 this is 128x cheaper for the score computation alone. 300-step smoke (d=128 baseline scale): Subsim at step 300: val 3.01 Dense at step 300: val ~2.96 (from prior runs) Only +1.7% behind dense -- much closer than plain FibGen managed (+20% at this scale). The L1-distance attention is learning meaningfully. storage_summary at d=128: 7.2x compression for SubsimLM. At larger d_model the compression ratio grows because dense d-dim attention scales as O(d^2) per layer while Subsim's W_sig is K-bottlenecked. Launched 2500-step 4-arch bench (dense, fibgen_K32_cross, subsim_K32, composed_transformerless) with text generation from each arch's best-val checkpoint.
1 parent 1a892e4 commit 587414d

2 files changed

Lines changed: 168 additions & 1 deletion

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"""Substrate-similarity attention: L1 distance in K-dim Fibonacci basis.
2+
3+
Per the user's "we need an architecture that extrapolates differently,
4+
not just compresses": standard attention's Q·K^T dot product has nothing
5+
substrate-aware about it. This module replaces it with L1 distance in a
6+
K-dim Fibonacci-basis signature space — the substrate's canonical
7+
nearness metric, the same one used for attractor snapping.
8+
9+
The architectural claim: nearness in K-dim Fibonacci basis IS the
10+
substrate-aligned way to ask "do these two tokens share structure?"
11+
The dot-product operator only knows about magnitudes and orientations
12+
in a generic Euclidean space.
13+
14+
Attention computation:
15+
sig[t] = W_sig · x[t] # [K]: substrate signature
16+
dist[i,j] = ||sig[i] - sig[j]||_1 # L1 in Fibonacci basis
17+
attn[i,j] = softmax(-dist[i,j] / sqrt(K)) # nearness ~ attention
18+
19+
Compute cost: O(T·d·K) for the projection + O(T²·K) for the pairwise
20+
L1 (vs O(T·d²) + O(T²·d) for dense attention). At d=4096, K=32 the
21+
L1-score computation is 128× cheaper than dense Q·K^T.
22+
23+
The model uses FibGen weights too (compressed storage). So we have
24+
SUBSTRATE COMPRESSED WEIGHTS + SUBSTRATE NATIVE OPERATOR. Two distinct
25+
substrate properties stacked.
26+
"""
27+
28+
import math
29+
import sys
30+
from pathlib import Path
31+
32+
import torch
33+
import torch.nn as nn
34+
import torch.nn.functional as F
35+
36+
sys.path.insert(0, str(Path(__file__).parent))
37+
from models_fibgen import FibGenLinear, FIBONACCI
38+
39+
40+
class SubstrateSimilarityAttention(nn.Module):
41+
"""L1-distance attention in K-dim Fibonacci-basis signature space.
42+
43+
Substrate-native at TWO levels:
44+
- WEIGHTS: W_sig, W_v, W_out are FibGen (Fibonacci-basis seeds, ~100x
45+
smaller storage than dense).
46+
- OPERATOR: attention scores via L1 distance in the K-dim signature
47+
space, NOT Q·K^T. Tokens with matching Fibonacci signatures
48+
attend; tokens with disparate signatures are gated out.
49+
"""
50+
51+
def __init__(self, d_model: int, K: int = 32, seq_len: int = 128,
52+
fibgen_K: int = 32, mode: str = "cross"):
53+
super().__init__()
54+
self.d_model = d_model
55+
self.K = K
56+
self.W_sig = FibGenLinear(d_model, K, K=fibgen_K, mode=mode, bias=False)
57+
self.W_v = FibGenLinear(d_model, d_model, K=fibgen_K, mode=mode, bias=False)
58+
self.W_out = FibGenLinear(d_model, d_model, K=fibgen_K, mode=mode, bias=False)
59+
# Standard causal mask; substrate-distance attention is dense in
60+
# principle. Could also use Fibonacci-offset mask for sparsity.
61+
mask = torch.tril(torch.ones(seq_len, seq_len))
62+
self.register_buffer("mask", mask)
63+
64+
def forward(self, x: torch.Tensor) -> torch.Tensor:
65+
B, T, D = x.shape
66+
sig = self.W_sig(x) # [B, T, K]
67+
v = self.W_v(x) # [B, T, D]
68+
# Pairwise L1 distance across the T axis: [B, T, T]
69+
diff = sig.unsqueeze(2) - sig.unsqueeze(1) # [B, T, T, K]
70+
dist = diff.abs().sum(dim=-1) # [B, T, T]
71+
scores = -dist / math.sqrt(self.K)
72+
# Causal mask: cells where mask=0 set to -inf so softmax zeros them.
73+
m = self.mask[:T, :T]
74+
scores = scores.masked_fill(m == 0, float("-inf"))
75+
attn = F.softmax(scores, dim=-1)
76+
out = attn @ v
77+
return self.W_out(out)
78+
79+
80+
class SubsimBlock(nn.Module):
81+
"""Substrate-similarity attention + FibGen FFN."""
82+
83+
def __init__(self, d_model: int, seq_len: int, K: int = 32,
84+
fibgen_K: int = 32, mode: str = "cross"):
85+
super().__init__()
86+
self.attn = SubstrateSimilarityAttention(d_model, K=K, seq_len=seq_len,
87+
fibgen_K=fibgen_K, mode=mode)
88+
# FFN with FibGen weights (separate K for FFN if desired)
89+
self.w1 = FibGenLinear(d_model, 4 * d_model, K=fibgen_K, mode=mode)
90+
self.w2 = FibGenLinear(4 * d_model, d_model, K=fibgen_K, mode=mode)
91+
self.ln1 = nn.LayerNorm(d_model)
92+
self.ln2 = nn.LayerNorm(d_model)
93+
94+
def forward(self, x):
95+
x = x + self.attn(self.ln1(x))
96+
x = x + self.w2(F.gelu(self.w1(self.ln2(x))))
97+
return x
98+
99+
100+
class SubsimLM(nn.Module):
101+
"""Char-level LM with:
102+
- Standard learned embedding (subspace defined by the input vocabulary)
103+
- CRT-Fibonacci positional encoding
104+
- SubstrateSimilarityAttention (L1-distance in K-dim Fibonacci basis)
105+
- FibGen FFN weights
106+
- Tied LM head
107+
"""
108+
109+
def __init__(self, vocab_size: int, d_model: int, n_blocks: int,
110+
seq_len: int, K: int = 32, fibgen_K: int = 32,
111+
mode: str = "cross"):
112+
super().__init__()
113+
self.seq_len = seq_len
114+
self.K = K
115+
self.embed = nn.Embedding(vocab_size, d_model)
116+
pe = self._crt_pe(seq_len, d_model)
117+
self.register_buffer("pe", pe)
118+
self.blocks = nn.ModuleList([
119+
SubsimBlock(d_model, seq_len, K=K, fibgen_K=fibgen_K, mode=mode)
120+
for _ in range(n_blocks)
121+
])
122+
self.ln_f = nn.LayerNorm(d_model)
123+
self.head = nn.Linear(d_model, vocab_size, bias=False)
124+
self.head.weight = self.embed.weight
125+
126+
@staticmethod
127+
def _crt_pe(seq_len: int, d_model: int) -> torch.Tensor:
128+
pe = torch.zeros(seq_len, d_model)
129+
pos = torch.arange(0, seq_len, dtype=torch.float)
130+
moduli = [5, 8, 13, 21, 34, 55, 89, 144]
131+
n_pairs = d_model // 2
132+
for i in range(n_pairs):
133+
m = moduli[i % len(moduli)]
134+
angle = 2 * math.pi * (pos % m) / m
135+
pe[:, 2 * i] = torch.sin(angle)
136+
pe[:, 2 * i + 1] = torch.cos(angle)
137+
return pe
138+
139+
def forward(self, token_ids):
140+
B, T = token_ids.shape
141+
h = self.embed(token_ids) + self.pe[:T]
142+
for block in self.blocks:
143+
h = block(h)
144+
h = self.ln_f(h)
145+
return self.head(h)
146+
147+
def storage_summary(self):
148+
stored = 0
149+
dense_eq = 0
150+
for m in self.modules():
151+
if isinstance(m, FibGenLinear):
152+
stored += m.n_stored_params
153+
dense_eq += m.n_dense_equivalent_params
154+
for n, p in self.named_parameters():
155+
# Approximation: any param not inside a FibGen counts as itself.
156+
# (The embedding and LayerNorms are intentionally not compressed.)
157+
if not any(s in n for s in ("W_sig", "W_v", "W_out", ".w1.", ".w2.")):
158+
stored += p.numel()
159+
dense_eq += p.numel()
160+
return {"stored": stored, "dense_equivalent": dense_eq,
161+
"compression": dense_eq / max(stored, 1)}

experiments/transformerless_lm/sample_text.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from corpus import make_dataset
2727
from models import make_model
2828
from models_fibgen import FibGenLM, FibGenTransformerless
29+
from models_subsim import SubsimLM
2930
from train_distractor_mix import build_distractor_stream
3031
from lazy_data import fib_positions_in_window, get_fib_strided_batch
3132

@@ -131,7 +132,7 @@ def main():
131132
)
132133
fib_positions = fib_positions_in_window(args.seq_len)
133134

134-
# Build the three archs
135+
# Build the four archs (now includes SubsimLM — substrate-native operator)
135136
archs = {
136137
"dense_crt": lambda: make_model(
137138
"crt_only", vocab_size=vocab_size, seq_len=args.seq_len,
@@ -141,6 +142,11 @@ def main():
141142
vocab_size=vocab_size, d_model=args.d_model,
142143
n_blocks=args.n_blocks, seq_len=args.seq_len, K=32, mode="cross",
143144
),
145+
"subsim_K32": lambda: SubsimLM(
146+
vocab_size=vocab_size, d_model=args.d_model,
147+
n_blocks=args.n_blocks, seq_len=args.seq_len,
148+
K=32, fibgen_K=32, mode="cross",
149+
),
144150
"composed_transformerless": lambda: FibGenTransformerless(
145151
vocab_size=vocab_size, d_model=args.d_model, n_blocks=args.n_blocks,
146152
seq_len=args.seq_len, K=32, mode="cross", n_specialists=5,

0 commit comments

Comments
 (0)