maba-v2-architecture / maba_sparse /layers /sparse_attention.py
AndrewThompson1233's picture
commit26
8ebafcf
Raw History Blame Contribute Delete
12 kB
import math
from typing import Any, Optional, Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
from maba_sparse.kernels.dispatcher import dispatch_stream_superposition
from maba_sparse.layers.indexer import DGIndexer
class MabaSparseAttention(nn.Module):
def __init__(
self,
config: Optional[Any] = None,
dim: int = 640,
n_heads: int = 10,
d_head: int = 64,
d_c: int = 128,
window_size: int = 128,
block_size: int = 64,
top_k: int = 32,
hca_pool_size: int = 64,
dist_lambda: float = 0.5,
d_idx: int = 64,
ablation_mode: str = "full",
) -> None:
super().__init__()
if config is not None:
self.dim = getattr(config, "dim", getattr(config, "d_model", dim))
self.n_heads = getattr(config, "n_heads", getattr(config, "num_heads", n_heads))
self.d_head = getattr(config, "d_head", d_head)
self.d_c = getattr(config, "d_c", d_c)
self.window_size = getattr(config, "window_size", window_size)
self.block_size = getattr(config, "block_size", block_size)
self.top_k = getattr(config, "top_k", top_k)
self.hca_pool_size = getattr(config, "hca_pool_size", hca_pool_size)
self.dist_lambda = getattr(config, "dist_lambda", dist_lambda)
self.n_sink_tokens = getattr(config, "n_sink_tokens", 4)
d_idx = getattr(config, "d_idx", d_idx)
self.ablation_mode = ablation_mode if ablation_mode is not None else getattr(config, "ablation_mode", "full")
else:
self.dim = dim
self.n_heads = n_heads
self.d_head = d_head
self.d_c = d_c
self.window_size = window_size
self.block_size = block_size
self.top_k = top_k
self.hca_pool_size = hca_pool_size
self.dist_lambda = dist_lambda
self.n_sink_tokens = 4
self.ablation_mode = ablation_mode
self.scale = 1.0 / math.sqrt(self.d_head)
self.q_proj = nn.Linear(self.dim, self.n_heads * self.d_head, bias=False)
self.kv_down_proj = nn.Linear(self.dim, self.d_c, bias=False)
self.k_up_proj = nn.Linear(self.d_c, self.n_heads * self.d_head, bias=False)
self.v_up_proj = nn.Linear(self.d_c, self.n_heads * self.d_head, bias=False)
self.o_proj = nn.Linear(self.n_heads * self.d_head, self.dim, bias=False)
self.stream_gate = nn.Linear(self.dim, 3, bias=True)
self.indexer = DGIndexer(
dim=self.dim,
d_idx=d_idx,
block_size=self.block_size,
top_k=self.top_k,
dist_lambda=self.dist_lambda,
)
self._cached_k: Optional[torch.Tensor] = None
self._cached_v: Optional[torch.Tensor] = None
def reset_cache(self) -> None:
self.indexer.reset_cache()
self._cached_k = None
self._cached_v = None
def get_gate_weights(self, x: torch.Tensor) -> torch.Tensor:
return F.softmax(self.stream_gate(x), dim=-1)
def _compute_local_attention(
self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, L_full: int
) -> torch.Tensor:
b, h, lq, _ = q.shape
lkv = k.shape[2]
if lq == 1 and lkv > (self.window_size + self.n_sink_tokens):
sink_idx = torch.arange(min(self.n_sink_tokens, lkv), device=q.device)
win_start = max(min(self.n_sink_tokens, lkv), lkv - self.window_size)
win_idx = torch.arange(win_start, lkv, device=q.device)
sel_idx = torch.cat([sink_idx, win_idx])
k_sel = k[:, :, sel_idx, :]
v_sel = v[:, :, sel_idx, :]
a = torch.matmul(q, k_sel.transpose(-1, -2)) * self.scale
p = F.softmax(a, dim=-1)
return torch.matmul(p, v_sel)
a = torch.matmul(q, k.transpose(-1, -2)) * self.scale
qo = lkv - lq
i = (qo + torch.arange(lq, device=q.device)).unsqueeze(1)
j = torch.arange(lkv, device=q.device).unsqueeze(0)
c = j <= i
w = (i - j) < self.window_size
s = j < min(self.n_sink_tokens, lkv)
m = c & (w | s)
a = a.masked_fill(~m.unsqueeze(0).unsqueeze(0), float("-inf"))
p = torch.nan_to_num(F.softmax(a, dim=-1), nan=0.0)
return torch.matmul(p, v)
def _compute_sparse_attention(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
top_indices: torch.Tensor,
L_full: int,
) -> torch.Tensor:
b, h, lq, dh = q.shape
lkv = k.shape[2]
nb = (lkv + self.block_size - 1) // self.block_size
if nb == 0 or lq == 0:
return torch.zeros(b, h, lq, dh, device=q.device, dtype=q.dtype)
ak = top_indices.shape[-1]
pad_len = nb * self.block_size - lkv
kp = F.pad(k, (0, 0, 0, pad_len)) if pad_len > 0 else k
vp = F.pad(v, (0, 0, 0, pad_len)) if pad_len > 0 else v
kb = kp.view(b, h, nb, self.block_size, dh)
vb = vp.view(b, h, nb, self.block_size, dh)
qo = lkv - lq
if lq == 1:
bi = top_indices.squeeze(1)
gather_idx = bi.view(b, 1, ak, 1, 1).expand(b, h, ak, self.block_size, dh)
k_sel = torch.gather(kb, 2, gather_idx).reshape(b, h, ak * self.block_size, dh)
v_sel = torch.gather(vb, 2, gather_idx).reshape(b, h, ak * self.block_size, dh)
a = torch.matmul(q, k_sel.transpose(-1, -2)) * self.scale
token_offsets = (
bi.unsqueeze(-1) * self.block_size
+ torch.arange(self.block_size, device=q.device).view(1, 1, self.block_size)
).reshape(b, 1, ak * self.block_size)
valid = (token_offsets < lkv) & (token_offsets <= (lkv - 1))
a = a.masked_fill(~valid.unsqueeze(1), float("-inf"))
p = torch.nan_to_num(F.softmax(a, dim=-1), nan=0.0)
return torch.matmul(p, v_sel)
os_chunks = []
chunk_sz = 128
for q_start in range(0, lq, chunk_sz):
q_end = min(q_start + chunk_sz, lq)
c_len = q_end - q_start
qc = q[:, :, q_start:q_end, :]
bi = top_indices[:, q_start:q_end, :]
gather_idx = bi.view(b, 1, c_len, ak, 1, 1).expand(b, h, c_len, ak, self.block_size, dh)
kb_exp = kb.view(b, h, 1, nb, self.block_size, dh).expand(b, h, c_len, nb, self.block_size, dh)
vb_exp = vb.view(b, h, 1, nb, self.block_size, dh).expand(b, h, c_len, nb, self.block_size, dh)
k_sel = torch.gather(kb_exp, 3, gather_idx).reshape(b, h, c_len, ak * self.block_size, dh)
v_sel = torch.gather(vb_exp, 3, gather_idx).reshape(b, h, c_len, ak * self.block_size, dh)
ac = (qc.unsqueeze(-2) @ k_sel.transpose(-1, -2)).squeeze(-2) * self.scale
pos = (qo + torch.arange(q_start, q_end, device=q.device)).view(1, c_len, 1)
token_offsets = (
bi.unsqueeze(-1) * self.block_size
+ torch.arange(self.block_size, device=q.device).view(1, 1, 1, self.block_size)
).reshape(b, c_len, ak * self.block_size)
valid = (token_offsets < lkv) & (token_offsets <= pos)
ac = ac.masked_fill(~valid.unsqueeze(1), float("-inf"))
pc = torch.nan_to_num(F.softmax(ac, dim=-1), nan=0.0)
oc = (pc.unsqueeze(-2) @ v_sel).squeeze(-2)
os_chunks.append(oc)
return torch.cat(os_chunks, dim=2)
def _compute_hca_attention(
self, q: torch.Tensor, c_kv: torch.Tensor, L_full: int
) -> torch.Tensor:
b, h, lq, dh = q.shape
lkv = c_kv.shape[1]
r = self.hca_pool_size
if lkv == 0 or lq == 0:
return torch.zeros(b, h, lq, dh, device=q.device, dtype=q.dtype)
qo = lkv - lq
pos = qo + torch.arange(lq, device=q.device)
num_complete = lkv // r
early_mask = pos < (r - 1)
out = torch.empty(b, h, lq, dh, device=q.device, dtype=q.dtype)
if early_mask.any():
early_indices = torch.where(early_mask)[0]
early_pos = pos[early_mask]
max_early_pos = early_pos.max().item()
c_prefix = c_kv[:, : max_early_pos + 1]
c_cum = torch.cumsum(c_prefix, dim=1)
denom = torch.arange(1, max_early_pos + 2, device=c_kv.device).view(1, -1, 1)
c_mean = c_cum / denom
c_pool_early = c_mean[:, early_pos, :]
vh_early = self.v_up_proj(c_pool_early).view(b, len(early_pos), h, dh).transpose(1, 2)
out[:, :, early_indices, :] = vh_early
if (~early_mask).any() and num_complete > 0:
late_indices = torch.where(~early_mask)[0]
late_pos = pos[~early_mask]
q_late = q[:, :, late_indices, :]
c_complete = c_kv[:, : num_complete * r].view(b, num_complete, r, self.d_c).mean(dim=2)
kh = self.k_up_proj(c_complete).view(b, num_complete, h, dh).transpose(1, 2)
vh = self.v_up_proj(c_complete).view(b, num_complete, h, dh).transpose(1, 2)
a = torch.matmul(q_late, kh.transpose(-1, -2)) * self.scale
qi = late_pos.unsqueeze(1)
bs = torch.arange(num_complete, device=q.device) + 1
ei = (bs * r - 1).unsqueeze(0)
c = ei <= qi
a = a.masked_fill(~c.unsqueeze(0).unsqueeze(0), float("-inf"))
p = torch.nan_to_num(F.softmax(a, dim=-1), nan=0.0)
out[:, :, late_indices, :] = torch.matmul(p, vh)
return out
def forward(
self,
x: torch.Tensor,
past_c_kv: Optional[torch.Tensor] = None,
past_centroids: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
b, l, d = x.shape
if past_c_kv is None or (
self._cached_k is not None and self._cached_k.shape[2] != past_c_kv.shape[1]
) or (
self.indexer._cached_k_idx is not None and self.indexer._cached_k_idx.shape[1] != past_c_kv.shape[1]
):
self.reset_cache()
c_kv = self.kv_down_proj(x)
c_full = torch.cat([past_c_kv, c_kv], dim=1) if past_c_kv is not None else c_kv
lf = c_full.shape[1]
q = self.q_proj(x).view(b, l, self.n_heads, self.d_head).transpose(1, 2)
if (
not self.training
and past_c_kv is not None
and self._cached_k is not None
and self._cached_k.shape[0] == b
and self._cached_k.shape[2] == past_c_kv.shape[1]
):
k_new = self.k_up_proj(c_kv).view(b, l, self.n_heads, self.d_head).transpose(1, 2)
v_new = self.v_up_proj(c_kv).view(b, l, self.n_heads, self.d_head).transpose(1, 2)
k = torch.cat([self._cached_k, k_new], dim=2)
v = torch.cat([self._cached_v, v_new], dim=2)
else:
k = self.k_up_proj(c_full).view(b, lf, self.n_heads, self.d_head).transpose(1, 2)
v = self.v_up_proj(c_full).view(b, lf, self.n_heads, self.d_head).transpose(1, 2)
if not self.training:
self._cached_k = k
self._cached_v = v
else:
self._cached_k = None
self._cached_v = None
ol = self._compute_local_attention(q, k, v, lf)
idx, _ = self.indexer(
x, return_scores=False, past_centroids=past_centroids
)
os = self._compute_sparse_attention(q, k, v, idx, lf)
if self.ablation_mode == "no_hca":
oh = torch.zeros_like(ol)
else:
oh = self._compute_hca_attention(q, c_full, lf)
gate_logits = self.stream_gate(x)
o = dispatch_stream_superposition(ol, os, oh, gate_logits=gate_logits)
o = o.transpose(1, 2).contiguous().view(b, l, self.n_heads * self.d_head).to(q.dtype)
return self.o_proj(o), c_full
MABASALayer = MabaSparseAttention