|
| 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