Skip to content

Commit 3d4c094

Browse files
committed
transformerless_lm: FibGen — Generator-from-seed weights (100x compression)
Per the user "we are a single person research team, we will do what cant be done" — going for the highest-risk piece of the inference-first thesis: ALL weights computed on the fly from a small Fibonacci-indexed seed, never stored as a dense tensor. models_fibgen.FibGenLinear: Drop-in nn.Linear replacement. W in R^{out, in} is generated as W[i,j] = sum_{k=1..K} [a_k cos(2pi F(k) i/out) cos(2pi F(k) j/in) + b_k sin*cos + c_k cos*sin + d_k sin*sin] where F(k) is the k-th unique positive Fibonacci number and the stored seed is 4K scalars per layer. At K=16 a 128x512 Linear stores 64 scalars (+ 512 bias) vs the dense equivalent's 66,048 params. 115x compression at this layer. models_fibgen.FibGenLM: char-level LM with EVERY linear layer FibGen-generated, including embedding and LM head. Position encoding stays CRT-Fibonacci (already substrate-aligned). Full model at d=128, n_blocks=4, K=16: stored params: 8,064 dense_equivalent: 809,984 compression: 100.4x Smoke test at 100 steps (lazy-loading data, TinyShakespeare): dense_crt val=4.13 (still ~uniform; needs more steps) fibgen_K16 val=3.59 (clearly learning — well below uniform) FibGen STARTS substrate-aligned (sin/cos Fibonacci-frequency mixings of integer positions) so warmup is dramatically faster than dense random init. Whether it CONVERGES to comparable loss is the question the full 1500-step bench will answer. train_fibgen.py runs the full sweep across K in {8, 16, 32} comparing to a dense_crt baseline. Uses lazy_data.get_fib_strided_batch by default.
1 parent f604be6 commit 3d4c094

2 files changed

Lines changed: 450 additions & 0 deletions

File tree

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
"""Generator-from-seed weights: the inference-first thesis's Piece 3.
2+
3+
A linear layer's W ∈ R^{out × in} is not STORED but GENERATED at each
4+
forward pass from a small Fibonacci-indexed seed. The seed has K
5+
components, each contributing a separable Fibonacci-frequency mixing
6+
of the integer position indices (i, j):
7+
8+
W[i, j] = Σ_{k=1..K} [ a_k · cos(2π·F(k)·i/out) · cos(2π·F(k)·j/in)
9+
+ b_k · sin(2π·F(k)·i/out) · cos(2π·F(k)·j/in)
10+
+ c_k · cos(2π·F(k)·i/out) · sin(2π·F(k)·j/in)
11+
+ d_k · sin(2π·F(k)·i/out) · sin(2π·F(k)·j/in) ]
12+
13+
where F(k) is the k-th unique positive Fibonacci number, and the seed
14+
is (a_k, b_k, c_k, d_k) for k = 1..K — 4K scalars per layer.
15+
16+
Total stored parameters per layer: 4K (regardless of in_features or
17+
out_features). For K=16, that's 64 floats — vs 65,536 for a dense
18+
128×128 Linear. 1024× compression.
19+
20+
Per-forward cost: ONE matrix construction (4K · in · out FLOPs) plus
21+
the standard matmul (B · T · in · out FLOPs). For B·T >> 4K (typical
22+
batch and sequence), the generator cost amortizes to negligible.
23+
24+
At inference: a single layer's generator can be PRECOMPUTED once and
25+
cached, making per-token cost identical to a stored weight. The win
26+
is storage: the cache is ephemeral, the seed is the only persistent
27+
artifact.
28+
29+
This is the highest-risk piece in the transformerless thesis: whether
30+
a model with ALL weights generated from Fibonacci bases can learn
31+
anything useful at all. If it tanks, we know the substrate basis is
32+
too restrictive at the weight level even though it works for positions.
33+
If it learns even partially, we have a foothold for radical inference-
34+
time compression.
35+
"""
36+
37+
import math
38+
from typing import Optional
39+
40+
import torch
41+
import torch.nn as nn
42+
import torch.nn.functional as F
43+
44+
45+
FIBONACCI = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597]
46+
47+
48+
class FibGenLinear(nn.Module):
49+
"""Drop-in replacement for nn.Linear where W is generated from a seed.
50+
51+
Args:
52+
in_features: input dim.
53+
out_features: output dim.
54+
K: number of Fibonacci-frequency components in the generator.
55+
Higher K = more capacity, more params. K=16 → 64 params
56+
(vs in·out for a stored matrix).
57+
bias: whether to include a learnable bias vector.
58+
init_scale: scales the seed initialization. The generated W has
59+
magnitude ~ init_scale · sqrt(4K), so smaller init_scale
60+
gives smaller initial weights.
61+
"""
62+
63+
def __init__(self, in_features: int, out_features: int, K: int = 16,
64+
bias: bool = True, init_scale: float = 0.1):
65+
super().__init__()
66+
self.in_features = in_features
67+
self.out_features = out_features
68+
self.K = min(K, len(FIBONACCI))
69+
# Seed: 4 coefficients per Fibonacci component (cc, sc, cs, ss).
70+
self.seed = nn.Parameter(
71+
torch.randn(self.K, 4) * (init_scale / max(1, math.sqrt(self.K)))
72+
)
73+
if bias:
74+
self.bias = nn.Parameter(torch.zeros(out_features))
75+
else:
76+
self.register_parameter("bias", None)
77+
# Precompute the cos/sin of position·Fibonacci-frequency for both
78+
# axes. These are FIXED — no gradient flows through positions.
79+
i_idx = torch.arange(out_features).float()
80+
j_idx = torch.arange(in_features).float()
81+
freqs = torch.tensor(FIBONACCI[:self.K], dtype=torch.float)
82+
# angles: [out, K], [in, K]
83+
a_i = 2 * math.pi * i_idx.unsqueeze(1) * freqs.unsqueeze(0) / max(out_features, 1)
84+
a_j = 2 * math.pi * j_idx.unsqueeze(1) * freqs.unsqueeze(0) / max(in_features, 1)
85+
self.register_buffer("cos_i", torch.cos(a_i)) # [out, K]
86+
self.register_buffer("sin_i", torch.sin(a_i))
87+
self.register_buffer("cos_j", torch.cos(a_j)) # [in, K]
88+
self.register_buffer("sin_j", torch.sin(a_j))
89+
90+
def generate_W(self) -> torch.Tensor:
91+
# seed: [K, 4] → split into 4 [K] tensors.
92+
a, b, c, d = self.seed[:, 0], self.seed[:, 1], self.seed[:, 2], self.seed[:, 3]
93+
# W = sum_k (
94+
# a_k · cos_i[:, k] · cos_j[:, k]^T +
95+
# b_k · sin_i[:, k] · cos_j[:, k]^T +
96+
# c_k · cos_i[:, k] · sin_j[:, k]^T +
97+
# d_k · sin_i[:, k] · sin_j[:, k]^T
98+
# )
99+
# Each term is an [out, in] outer product.
100+
# Compose via einsum: [out, K] · [K] · [K, in] (with the diagonal)
101+
# → [out, in].
102+
W = torch.einsum("ok,k,jk->oj", self.cos_i, a, self.cos_j)
103+
W = W + torch.einsum("ok,k,jk->oj", self.sin_i, b, self.cos_j)
104+
W = W + torch.einsum("ok,k,jk->oj", self.cos_i, c, self.sin_j)
105+
W = W + torch.einsum("ok,k,jk->oj", self.sin_i, d, self.sin_j)
106+
return W
107+
108+
def forward(self, x: torch.Tensor) -> torch.Tensor:
109+
W = self.generate_W()
110+
return F.linear(x, W, self.bias)
111+
112+
@property
113+
def n_stored_params(self) -> int:
114+
n = self.seed.numel()
115+
if self.bias is not None:
116+
n += self.bias.numel()
117+
return n
118+
119+
@property
120+
def n_dense_equivalent_params(self) -> int:
121+
n = self.in_features * self.out_features
122+
if self.bias is not None:
123+
n += self.out_features
124+
return n
125+
126+
127+
class FibGenAttention(nn.Module):
128+
"""Single-head self-attention with all linear layers FibGen-generated."""
129+
130+
def __init__(self, d_model: int, K: int = 16):
131+
super().__init__()
132+
self.d_model = d_model
133+
self.qkv = FibGenLinear(d_model, 3 * d_model, K=K)
134+
self.out = FibGenLinear(d_model, d_model, K=K)
135+
136+
def forward(self, x: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
137+
B, T, D = x.shape
138+
qkv = self.qkv(x)
139+
q, k, v = qkv.chunk(3, dim=-1)
140+
scale = 1.0 / math.sqrt(D)
141+
scores = (q @ k.transpose(-2, -1)) * scale
142+
scores = scores.masked_fill(mask == 0, float("-inf"))
143+
attn = F.softmax(scores, dim=-1)
144+
out = attn @ v
145+
return self.out(out)
146+
147+
148+
class FibGenFeedForward(nn.Module):
149+
"""FFN with FibGen-generated linear layers."""
150+
151+
def __init__(self, d_model: int, expansion: int = 4, K: int = 16):
152+
super().__init__()
153+
d_inner = d_model * expansion
154+
self.w1 = FibGenLinear(d_model, d_inner, K=K)
155+
self.w2 = FibGenLinear(d_inner, d_model, K=K)
156+
157+
def forward(self, x: torch.Tensor) -> torch.Tensor:
158+
return self.w2(F.gelu(self.w1(x)))
159+
160+
161+
class FibGenBlock(nn.Module):
162+
def __init__(self, d_model: int, K: int = 16):
163+
super().__init__()
164+
self.attn = FibGenAttention(d_model, K=K)
165+
self.ff = FibGenFeedForward(d_model, K=K)
166+
self.ln1 = nn.LayerNorm(d_model)
167+
self.ln2 = nn.LayerNorm(d_model)
168+
169+
def forward(self, x, mask):
170+
x = x + self.attn(self.ln1(x), mask)
171+
x = x + self.ff(self.ln2(x))
172+
return x
173+
174+
175+
class FibGenLM(nn.Module):
176+
"""Char-level LM with EVERY linear layer FibGen-generated.
177+
178+
Embedding is also FibGen: the "embedding table" is generated from
179+
a seed, so vocab_size × d_model storage becomes 4K · log_2(vocab)
180+
or similar. For char-level vocab=65 this is a small win, but at
181+
LLM scale (vocab=32k+) the embedding is a major param sink.
182+
183+
LM head tied to embedding (standard).
184+
"""
185+
186+
def __init__(self, vocab_size: int, d_model: int, n_blocks: int,
187+
seq_len: int, K: int = 16):
188+
super().__init__()
189+
self.seq_len = seq_len
190+
self.K = K
191+
# Embedding implemented as FibGen + index → FibGen produces a
192+
# [vocab, d_model] table that we index into.
193+
self.embed_gen = FibGenLinear(vocab_size, d_model, K=K, bias=False)
194+
# Positional encoding stays CRT-Fibonacci (already substrate-aligned,
195+
# and it's a buffer, not a learned weight).
196+
pe = self._crt_pe(seq_len, d_model)
197+
self.register_buffer("pe", pe)
198+
self.blocks = nn.ModuleList([
199+
FibGenBlock(d_model, K=K) for _ in range(n_blocks)
200+
])
201+
self.ln_f = nn.LayerNorm(d_model)
202+
# Head: FibGen too (or tied with embed — but tied with a generator
203+
# means head and embed share the SAME generator seed which forces
204+
# a constraint. Pick untied for now to test capacity.)
205+
self.head = FibGenLinear(d_model, vocab_size, K=K, bias=False)
206+
mask = torch.tril(torch.ones(seq_len, seq_len))
207+
self.register_buffer("mask", mask)
208+
209+
@staticmethod
210+
def _crt_pe(seq_len: int, d_model: int) -> torch.Tensor:
211+
pe = torch.zeros(seq_len, d_model)
212+
pos = torch.arange(0, seq_len, dtype=torch.float)
213+
moduli = [5, 8, 13, 21, 34, 55, 89, 144]
214+
n_pairs = d_model // 2
215+
for i in range(n_pairs):
216+
m = moduli[i % len(moduli)]
217+
angle = 2 * math.pi * (pos % m) / m
218+
pe[:, 2 * i] = torch.sin(angle)
219+
pe[:, 2 * i + 1] = torch.cos(angle)
220+
return pe
221+
222+
def forward(self, token_ids: torch.Tensor) -> torch.Tensor:
223+
B, T = token_ids.shape
224+
# Embedding via one-hot · FibGen-generated [vocab, d_model] table.
225+
# Equivalent to W[token_ids] for a stored embedding.
226+
W_emb = self.embed_gen.generate_W() # [d_model, vocab]
227+
h = W_emb.t()[token_ids] # [B, T, d_model]
228+
h = h + self.pe[:T]
229+
mask = self.mask[:T, :T]
230+
for block in self.blocks:
231+
h = block(h, mask)
232+
h = self.ln_f(h)
233+
return self.head(h)
234+
235+
def storage_summary(self) -> dict:
236+
"""Stored param count + the dense-equivalent count."""
237+
stored = 0
238+
dense_eq = 0
239+
for m in self.modules():
240+
if isinstance(m, FibGenLinear):
241+
stored += m.n_stored_params
242+
dense_eq += m.n_dense_equivalent_params
243+
# Add bias/LN params (these are NOT FibGen-generated)
244+
for n, p in self.named_parameters():
245+
if "seed" in n or "bias" in n and any(
246+
m_name in n for m_name in ("embed_gen", "head", "qkv", "out", "w1", "w2")
247+
):
248+
continue
249+
stored += p.numel()
250+
dense_eq += p.numel()
251+
return {
252+
"stored": stored,
253+
"dense_equivalent": dense_eq,
254+
"compression": dense_eq / max(stored, 1),
255+
}

0 commit comments

Comments
 (0)