AbstractPhil commited on
Commit
6365f5f
·
verified ·
1 Parent(s): b007aec

splat fast path (geolip 0.6.1 @a2436e6): fused 2K forward via oriented_cat, cached mask, lazy den stats, chunk 256 — 1.7x eager / 4.0x under torch.compile at ctx 2048, parity 1e-6 vs the naive oracle, grad-parity verified

Browse files
Files changed (1) hide show
  1. attention.py +59 -26
attention.py CHANGED
@@ -63,17 +63,44 @@ class CausalSDPA(nn.Module):
63
 
64
  class CausalSplatHUB(nn.Module):
65
  def __init__(self, d: int, K: int = 512, D: int = 32, tau: float = 0.1,
66
- chunk: int = 128):
67
  super().__init__()
68
  self.addr = AlephAddress(K, D, tau)
69
- self.chunk = chunk
70
  self.q = nn.Linear(d, D, bias=False)
71
  self.k = nn.Linear(d, D, bias=False)
72
  self.v = nn.Linear(d, d, bias=False)
73
  self.o = nn.Linear(d, d, bias=False)
74
  for m in (self.q, self.k, self.v, self.o):
75
  nn.init.orthogonal_(m.weight)
76
- self.last_den_stats = None # populated each forward for instruments
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  def _halves(self, x):
79
  qp, qn = self.addr.oriented(self.q(x))
@@ -81,36 +108,42 @@ class CausalSplatHUB(nn.Module):
81
  return qp, qn, kp, kn, self.v(x)
82
 
83
  def forward(self, x):
 
 
 
 
 
84
  B, n, d = x.shape
85
- qp, qn, kp, kn, v = self._halves(x)
 
 
86
  C = min(self.chunk, n)
87
  pad = (-n) % C
88
  if pad:
89
- z = lambda t: F.pad(t, (0, 0, 0, pad))
90
- qp, qn, kp, kn, v = z(qp), z(qn), z(kp), z(kn), z(v)
 
91
  nc = (n + pad) // C
92
- K = qp.shape[-1]
93
- qp, qn, kp, kn = (t.view(B, nc, C, K) for t in (qp, qn, kp, kn))
 
94
  v = v.view(B, nc, C, d)
95
- mask = torch.tril(torch.ones(C, C, device=x.device, dtype=v.dtype))
96
-
97
- num = torch.zeros(B, nc, C, d, device=x.device, dtype=v.dtype)
98
- den = torch.zeros(B, nc, C, 1, device=x.device, dtype=v.dtype)
99
- for kh, qh in ((kp, qp), (kn, qn)):
100
- S = torch.einsum("bick,bicd->bikd", kh, v) # per-chunk KxD sums
101
- P = torch.cumsum(S, dim=1) - S # exclusive prefix
102
- zS = kh.sum(dim=2) # (B, nc, K)
103
- zP = torch.cumsum(zS, dim=1) - zS
104
- att = torch.einsum("bick,bijk->bicj", qh, kh) * mask # (B,nc,C,C)
105
- num = num + torch.einsum("bick,bikd->bicd", qh, P) + att @ v
106
- den = den + torch.einsum("bick,bik->bic", qh, zP).unsqueeze(-1) \
107
- + att.sum(dim=-1, keepdim=True)
108
- num = num.view(B, nc * C, d)[:, :n]
109
- den = den.view(B, nc * C, 1)[:, :n]
110
  cl = dtype_floor(den)
111
- with torch.no_grad():
112
- self.last_den_stats = (den.min().item(), den.mean().item(),
113
- (den <= cl).float().mean().item())
114
  return self.o(num / den.clamp_min(cl))
115
 
116
  # ---------------------------------------------------- incremental decode
 
63
 
64
  class CausalSplatHUB(nn.Module):
65
  def __init__(self, d: int, K: int = 512, D: int = 32, tau: float = 0.1,
66
+ chunk: int = 256):
67
  super().__init__()
68
  self.addr = AlephAddress(K, D, tau)
69
+ self.chunk = chunk # 256 measured best at ctx 2048 (bench)
70
  self.q = nn.Linear(d, D, bias=False)
71
  self.k = nn.Linear(d, D, bias=False)
72
  self.v = nn.Linear(d, d, bias=False)
73
  self.o = nn.Linear(d, d, bias=False)
74
  for m in (self.q, self.k, self.v, self.o):
75
  nn.init.orthogonal_(m.weight)
76
+ self._mask_cache: dict = {}
77
+ self._den_raw = None # (den tensor, floor) until read
78
+ self._den_stats = None # cached floats after first read
79
+
80
+ # den stats are LAZY: the reference forward paid three .item() GPU
81
+ # syncs per call just to keep this attribute warm; instruments read
82
+ # it at most once per health interval. Property keeps the tuple API.
83
+ @property
84
+ def last_den_stats(self):
85
+ if self._den_stats is None and self._den_raw is not None:
86
+ den, cl = self._den_raw
87
+ with torch.no_grad():
88
+ self._den_stats = (den.min().item(), den.mean().item(),
89
+ (den <= cl).float().mean().item())
90
+ return self._den_stats
91
+
92
+ @last_den_stats.setter
93
+ def last_den_stats(self, value):
94
+ self._den_stats = value
95
+ self._den_raw = None
96
+
97
+ def _mask(self, C: int, device, dtype):
98
+ key = (C, device, dtype)
99
+ m = self._mask_cache.get(key)
100
+ if m is None:
101
+ m = torch.tril(torch.ones(C, C, device=device, dtype=dtype))
102
+ self._mask_cache[key] = m
103
+ return m
104
 
105
  def _halves(self, x):
106
  qp, qn = self.addr.oriented(self.q(x))
 
108
  return qp, qn, kp, kn, self.v(x)
109
 
110
  def forward(self, x):
111
+ """Fast path: the two oriented halves run as ONE 2K-wide pass —
112
+ every term is a sum of bilinear forms over the halves, so one
113
+ pass over cat(p, n) is the same arithmetic in half the kernels
114
+ (equal to forward_naive to fp reorder, ~1.5e-06; speed-harness
115
+ verdict 2026-08-15: 1.7x eager, 4.0x under torch.compile)."""
116
  B, n, d = x.shape
117
+ qc = self.addr.oriented_cat(self.q(x)) # (B, n, 2K)
118
+ kc = self.addr.oriented_cat(self.k(x))
119
+ v = self.v(x)
120
  C = min(self.chunk, n)
121
  pad = (-n) % C
122
  if pad:
123
+ qc = F.pad(qc, (0, 0, 0, pad))
124
+ kc = F.pad(kc, (0, 0, 0, pad))
125
+ v = F.pad(v, (0, 0, 0, pad))
126
  nc = (n + pad) // C
127
+ K2 = qc.shape[-1]
128
+ qc = qc.view(B, nc, C, K2)
129
+ kc = kc.view(B, nc, C, K2)
130
  v = v.view(B, nc, C, d)
131
+ mask = self._mask(C, x.device, v.dtype)
132
+
133
+ S = torch.einsum("bick,bicd->bikd", kc, v) # per-chunk 2KxD sums
134
+ P = torch.cumsum(S, dim=1) - S # exclusive prefix
135
+ zS = kc.sum(dim=2) # (B, nc, 2K)
136
+ zP = torch.cumsum(zS, dim=1) - zS
137
+ att = torch.einsum("bick,bijk->bicj", qc, kc) * mask # (B,nc,C,C)
138
+ num = torch.einsum("bick,bikd->bicd", qc, P) + att @ v
139
+ den = torch.einsum("bick,bik->bic", qc, zP).unsqueeze(-1) \
140
+ + att.sum(dim=-1, keepdim=True)
141
+
142
+ num = num.reshape(B, nc * C, d)[:, :n]
143
+ den = den.reshape(B, nc * C, 1)[:, :n]
 
 
144
  cl = dtype_floor(den)
145
+ self._den_raw = (den.detach(), cl)
146
+ self._den_stats = None
 
147
  return self.o(num / den.clamp_min(cl))
148
 
149
  # ---------------------------------------------------- incremental decode