AbstractPhil commited on
Commit
f9a4b59
·
verified ·
1 Parent(s): 9b60c70

standalone splat attention — single reusable file for observers

Browse files
Files changed (1) hide show
  1. splat_attention.py +250 -0
splat_attention.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =========================================================================
2
+ # splat_attention.py — standalone Splat Attention (aleph-addressed,
3
+ # softmax-free attention through a shared blackboard)
4
+ # =========================================================================
5
+ # From the AlephLM-0 / aleph-splat research line (AbstractPhil). Single
6
+ # file, no dependencies beyond torch. Experimental — the measured record,
7
+ # including failures, is summarized below so you know what you're holding.
8
+ #
9
+ # THE MECHANISM
10
+ # Every head is a tiny frozen "aleph" codebook: K unit anchor
11
+ # directions read through a closed-form SIGNED address
12
+ # u_k = cos(x, a_k)/tau, w_k = sinh(u_k) / sum_j cosh(u_j)
13
+ # (a reconstructive read — no argmax, no top-k, no softmax selection;
14
+ # weights are signed, so an anchor can contribute negatively).
15
+ # Attention is a write/read through the codebook cells:
16
+ # write: cells = sum_j wg_j (x) v_j (per head)
17
+ # read: out_i = wg_i @ cells / sum|wg_i| (weighted average)
18
+ # Affinity between tokens is address AGREEMENT through the K-cell
19
+ # bottleneck: O(L*M*K) per layer — LINEAR in sequence length.
20
+ #
21
+ # MEASURED (A40/4090, fp16 autocast, fwd+bwd, us/token):
22
+ # vs nn.MultiheadAttention(8 heads, d=512): ~3-6x SLOWER at L<=256,
23
+ # parity ~L=2048, ~2x FASTER at L=8192 (flat cost vs quadratic).
24
+ # torch.compile (inductor, Linux) gives a further 3-4x on this module.
25
+ # Associative recall through splat-sharded heads at equal total cells:
26
+ # top-1 .9995 @ 2k context / .934 @ 8k where one monolithic codebook
27
+ # reads .042/.0015 — partition+locality rescues superposition.
28
+ #
29
+ # THE FAILURE YOU MUST KNOW ABOUT (measured, structural): with LOCAL
30
+ # positional membership only (gaussian windows) at short L, supports
31
+ # shrink to a few tokens, attention degenerates to a local blur,
32
+ # cross-position transport dies, and a cls-pooled encoder COLLAPSES
33
+ # (representation erank ~5, retrieval at noise). Two repairs, both
34
+ # included here:
35
+ # rotary=True position enters as a RoPE rotation of the ADDRESS
36
+ # QUERY against the frozen codebook: relative
37
+ # position R(i-j) appears in every affinity, heads
38
+ # stay GLOBAL, transport exists at every L
39
+ # (probe: cross-position recall .548 at L=128 where
40
+ # local-only gave ~0; retrieval decays gracefully
41
+ # with query/write offset: .64 -> .31 over 0 -> 64).
42
+ # global_frac>0 reserve a fraction of heads with uniform
43
+ # membership alongside the local windows.
44
+ # Defaults below are the SAFE configuration (rotary=True).
45
+ #
46
+ # DESIGN CARD (from the measurement battery):
47
+ # K=4-8 per head (small codebooks saturate their sign-code space at
48
+ # ~0.5 bits/half-axis; big ones waste it) | M scales with data rank
49
+ # (rich data pays monotonically to M=2048) | frames born random and
50
+ # independent — constructed rotations buy nothing; differentiation is
51
+ # maintained by training pressure itself | overlap sigma/spacing in
52
+ # [1,2] when using local windows | composition by budget, never by
53
+ # softmax over heads (comparative composition measurably loses ~.10) |
54
+ # storage capacity scales with TOTAL cells regardless of partition —
55
+ # address capacity and memory capacity are different resources.
56
+ #
57
+ # USAGE
58
+ # from splat_attention import SplatAttention
59
+ # attn = SplatAttention(d_model=512, M=64, K=8, rotary=True)
60
+ # y = attn(x) # x: (B, L, d), y: (B, L, d)
61
+ # y = attn(x, key_padding_mask=kpm) # kpm: (B, L) True = pad
62
+ # python splat_attention.py # runs the demo + a small speed bench
63
+ #
64
+ # Status: research prototype. Trained-at-scale results pending; treat
65
+ # every number above as what it is — a measurement on the stated probe.
66
+ # =========================================================================
67
+
68
+ import math
69
+
70
+ import torch
71
+ import torch.nn as nn
72
+ import torch.nn.functional as F
73
+
74
+
75
+ def rope_rotate(x, pos, base=10000.0):
76
+ """RoPE rotation of the address query. pos: (L,) float positions."""
77
+ D = x.shape[-1]
78
+ half = D // 2
79
+ freqs = base ** (-torch.arange(half, device=x.device,
80
+ dtype=torch.float32) / half)
81
+ ang = pos.unsqueeze(-1) * freqs
82
+ c, sn = torch.cos(ang), torch.sin(ang)
83
+ x1, x2 = x[..., :half], x[..., half:]
84
+ return torch.cat([x1 * c - x2 * sn, x1 * sn + x2 * c], dim=-1)
85
+
86
+
87
+ class SplatAttention(nn.Module):
88
+ """Aleph-addressed attention. See module docstring.
89
+
90
+ Args:
91
+ d_model: model width
92
+ M: number of heads (tiny codebooks)
93
+ K: anchors per head (4-8 recommended)
94
+ tau: address temperature (0.1)
95
+ dropout: output dropout
96
+ rotary: position via RoPE on the address query; heads
97
+ global (RECOMMENDED — see the failure note)
98
+ global_frac: fraction of heads with uniform membership when
99
+ rotary=False (transport insurance for windows)
100
+ overlap: window sigma as a multiple of spacing (local mode)
101
+ sigma_floor: minimum window sigma in tokens (local mode)
102
+ head_gates: learnable per-head attenuation, born at identity
103
+ train_centers: learnable window centers/widths (local mode) —
104
+ only functions on a transport-capable geometry
105
+ train_codebooks: unfreeze the anchor frames
106
+ mchunk: heads per computation chunk (memory control)
107
+ checkpoint_chunks: recompute chunks in backward (training-time
108
+ memory saver; needs torch.utils.checkpoint)
109
+ """
110
+
111
+ def __init__(self, d_model, M=64, K=8, tau=0.1, dropout=0.0,
112
+ rotary=True, global_frac=0.0, overlap=1.5,
113
+ sigma_floor=0.0, head_gates=False, train_centers=False,
114
+ train_codebooks=False, mchunk=16,
115
+ checkpoint_chunks=False):
116
+ super().__init__()
117
+ self.M, self.K, self.tau = M, K, tau
118
+ self.rotary = rotary
119
+ self.global_frac = global_frac
120
+ self.overlap, self.sigma_floor = overlap, sigma_floor
121
+ self.mchunk = mchunk
122
+ self.checkpoint_chunks = checkpoint_chunks
123
+ book = F.normalize(torch.randn(M * K, d_model), dim=-1)
124
+ if train_codebooks:
125
+ self.codebook = nn.Parameter(book)
126
+ else:
127
+ self.register_buffer("codebook", book)
128
+ self.w_v = nn.Linear(d_model, d_model)
129
+ self.w_o = nn.Linear(d_model, d_model)
130
+ self.drop = nn.Dropout(dropout)
131
+ if train_centers:
132
+ self.center_off = nn.Parameter(torch.zeros(M))
133
+ self.log_sig = nn.Parameter(torch.zeros(M))
134
+ else:
135
+ self.center_off = self.log_sig = None
136
+ if head_gates:
137
+ self.head_gate = nn.Parameter(torch.zeros(M))
138
+ else:
139
+ self.head_gate = None
140
+
141
+ def _book(self):
142
+ return (F.normalize(self.codebook, dim=-1)
143
+ if isinstance(self.codebook, nn.Parameter)
144
+ else self.codebook)
145
+
146
+ def _membership(self, L, device, dtype):
147
+ if self.rotary:
148
+ return torch.ones(self.M, L, device=device, dtype=dtype)
149
+ pos = torch.arange(L, device=device, dtype=torch.float32)
150
+ frac = torch.linspace(0, 1, self.M, device=device)
151
+ if self.center_off is not None:
152
+ frac = (frac + self.center_off.float()).clamp(0, 1)
153
+ sig = (self.overlap * max(L / self.M, 1.0)
154
+ * torch.exp(self.log_sig.float()).unsqueeze(1))
155
+ sig = sig.clamp(min=max(self.sigma_floor, 1e-6))
156
+ else:
157
+ sig = max(self.overlap * max(L / self.M, 1.0),
158
+ self.sigma_floor, 1e-6)
159
+ centers = frac * (L - 1)
160
+ g = torch.exp(-0.5 * ((pos.unsqueeze(0) - centers.unsqueeze(1))
161
+ / sig) ** 2)
162
+ g = g / g.sum(dim=0, keepdim=True).clamp(min=1e-9)
163
+ n_glob = int(round(self.M * self.global_frac))
164
+ if n_glob > 0:
165
+ g[:n_glob] = 1.0
166
+ return g.to(dtype)
167
+
168
+ def _chunk(self, xn, v, g_c, live, c0, Mc):
169
+ sl = self._book()[c0 * self.K:(c0 + Mc) * self.K]
170
+ u = (xn @ sl.T).view(*xn.shape[:2], Mc, self.K) / self.tau
171
+ m = u.abs().amax(dim=-1, keepdim=True)
172
+ ep, en = torch.exp(u - m), torch.exp(-u - m)
173
+ w = (ep - en) / (ep + en).sum(dim=-1, keepdim=True)
174
+ wg = w * g_c.T.unsqueeze(0).unsqueeze(-1)
175
+ if self.head_gate is not None:
176
+ gam = 2 * torch.sigmoid(self.head_gate[c0:c0 + Mc])
177
+ wg = wg * gam.view(1, 1, -1, 1)
178
+ wg = (wg * live.unsqueeze(-1).unsqueeze(-1)).to(v.dtype)
179
+ cells = torch.einsum("blmk,bld->bmkd", wg, v) # write
180
+ part = torch.einsum("blmk,bmkd->bld", wg, cells) # read
181
+ den = wg.abs().sum(dim=(2, 3))
182
+ return part, den
183
+
184
+ def forward(self, x, key_padding_mask=None):
185
+ B, L, d = x.shape
186
+ xn = F.normalize(x, dim=-1)
187
+ if self.rotary:
188
+ pos = torch.arange(L, device=x.device, dtype=torch.float32)
189
+ xn = F.normalize(rope_rotate(xn.float(), pos),
190
+ dim=-1).to(xn.dtype)
191
+ g = self._membership(L, x.device, x.dtype)
192
+ live = ((~key_padding_mask).to(x.dtype)
193
+ if key_padding_mask is not None
194
+ else torch.ones(B, L, device=x.device, dtype=x.dtype))
195
+ v = self.w_v(x)
196
+ out = torch.zeros_like(x)
197
+ den = torch.zeros(B, L, device=x.device, dtype=x.dtype)
198
+ for c0 in range(0, self.M, self.mchunk):
199
+ Mc = min(self.mchunk, self.M - c0)
200
+ if (self.checkpoint_chunks and self.training
201
+ and torch.is_grad_enabled()):
202
+ import torch.utils.checkpoint as _ck
203
+ part, dpart = _ck.checkpoint(
204
+ self._chunk, xn, v, g[c0:c0 + Mc], live, c0, Mc,
205
+ use_reentrant=False, preserve_rng_state=False)
206
+ else:
207
+ part, dpart = self._chunk(xn, v, g[c0:c0 + Mc], live,
208
+ c0, Mc)
209
+ out = out.add_(part)
210
+ den = den.add_(dpart)
211
+ out = out / den.unsqueeze(-1).clamp_min(1e-9)
212
+ return self.drop(self.w_o(out))
213
+
214
+
215
+ def _demo():
216
+ torch.manual_seed(0)
217
+ dev = "cuda" if torch.cuda.is_available() else "cpu"
218
+ print(f"SplatAttention demo (device={dev})")
219
+ attn = SplatAttention(d_model=256, M=32, K=8, rotary=True).to(dev)
220
+ x = torch.randn(2, 128, 256, device=dev)
221
+ y = attn(x)
222
+ print(f" forward: {tuple(x.shape)} -> {tuple(y.shape)}")
223
+ y.sum().backward()
224
+ print(f" backward OK; trainable params: "
225
+ f"{sum(p.numel() for p in attn.parameters() if p.requires_grad):,}"
226
+ f" (+ frozen codebook {attn._book().numel():,})")
227
+ if dev == "cuda":
228
+ import time
229
+ mha = nn.MultiheadAttention(256, 8, batch_first=True).to(dev)
230
+ for L, B in [(128, 32), (2048, 2)]:
231
+ xx = torch.randn(B, L, 256, device=dev, requires_grad=True)
232
+ def t(fn, n=10):
233
+ for _ in range(3):
234
+ fn()
235
+ torch.cuda.synchronize()
236
+ t0 = time.time()
237
+ for _ in range(n):
238
+ fn()
239
+ torch.cuda.synchronize()
240
+ return (time.time() - t0) / n / (B * L) * 1e6
241
+ ts = t(lambda: attn(xx).sum().backward())
242
+ tm = t(lambda: mha(xx, xx, xx,
243
+ need_weights=False)[0].sum().backward())
244
+ print(f" L={L}: splat {ts:.2f} vs MHA {tm:.2f} us/token")
245
+ print(" (try rotary=False, global_frac=0.25 for windowed mode, "
246
+ "head_gates=True for learnable attenuation)")
247
+
248
+
249
+ if __name__ == "__main__":
250
+ _demo()