| """FractalLinearAttention: causal, multi-level linear attention.
|
|
|
| Faithfully ported from the original system (src/attention.rs) in pure PyTorch.
|
|
|
| Math (Katharopoulos 2020, normalized causal form):
|
| Feature map: phi(x; level) = elu_plus_one(x + omega_level, alpha=1)
|
| with omega_level = (phi^2)^{-level}, phi^2 = ((1+sqrt(5)/2)^2 ~= 2.618
|
| Causal recurrence (INCLUSIVE: at step t, S and z are updated before computing y_t).
|
| Multi-level aggregation: output = sum of softmax(level_logits) * attn_level(x).
|
| Complexity: O(L * d_head^2) per head per level.
|
| End-to-end differentiable.
|
| """
|
|
|
| import math
|
| import torch
|
| import torch.nn as nn
|
|
|
| from .stats import elu_plus_one, stable_softmax
|
|
|
|
|
| def _mandelbrot_offsets(n_levels: int) -> torch.Tensor:
|
| """Offsets ω_level = (φ2)^{-level} for level = 0..n_levels-1.
|
|
|
| Geometric decay. Renamed honestly: the original called these
|
| "Mandelbrot frequencies" but there is no Mandelbrot iteration —
|
| just a geometric sequence of base φ2.
|
| """
|
| phi = (1.0 + math.sqrt(5.0)) / 2.0
|
| phi_sq = phi * phi
|
| levels = torch.arange(n_levels, dtype=torch.float32)
|
| return phi_sq ** (-levels)
|
|
|
|
|
| class FractalLinearAttention(nn.Module):
|
| """Multi-level causal linear attention.
|
|
|
| Args:
|
| d_model : model dimension (input/output).
|
| n_heads : number of attention heads.
|
| d_head : dimension per head. Must satisfy n_heads · d_head == d_model.
|
| n_levels : number of fractal levels (distinct Mandelbrot offsets).
|
| """
|
|
|
| def __init__(self, d_model: int, n_heads: int, d_head: int, n_levels: int = 3):
|
| super().__init__()
|
| if n_heads * d_head != d_model:
|
| raise ValueError(
|
| f"Constraint not satisfied: n_heads·d_head ({n_heads*d_head}) "
|
| f"≠ d_model ({d_model})"
|
| )
|
| if n_levels < 1:
|
| raise ValueError("n_levels must be >= 1")
|
|
|
| self.d_model = d_model
|
| self.n_heads = n_heads
|
| self.d_head = d_head
|
| self.n_levels = n_levels
|
| d_qkv = n_heads * d_head
|
|
|
|
|
|
|
|
|
|
|
| scale = math.sqrt(2.0 / (d_model + d_qkv))
|
| self.w_qkv = nn.Parameter(
|
| torch.empty(3, d_model, d_qkv).uniform_(-scale, scale)
|
| )
|
| self.b_qkv = nn.Parameter(torch.zeros(3, d_qkv))
|
|
|
|
|
| scale_out = math.sqrt(2.0 / (d_qkv + d_model))
|
| self.w_out = nn.Parameter(
|
| torch.empty(d_qkv, d_model).uniform_(-scale_out, scale_out)
|
| )
|
| self.b_out = nn.Parameter(torch.zeros(d_model))
|
|
|
|
|
| self.level_logits = nn.Parameter(torch.zeros(n_levels))
|
|
|
|
|
| offsets = _mandelbrot_offsets(n_levels)
|
| self.register_buffer("level_offsets", offsets)
|
|
|
| def feature_map(self, x: torch.Tensor, level: int) -> torch.Tensor:
|
| """φ(x; level) = elu_plus_one(x + ω_level).
|
|
|
| x: (..., d_head). The offset ω_level is a scalar added to all of x.
|
| """
|
|
|
| assert 0 <= level < self.n_levels, f"level {level} outside [0, {self.n_levels})"
|
| offset = self.level_offsets[level]
|
| return elu_plus_one(x + offset, alpha=1.0)
|
|
|
| def _linear_attention_causal_one_head(
|
| self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor
|
| ) -> torch.Tensor:
|
| """Causal recurrence for ONE head, over a batch.
|
|
|
| q, k : (B, L, d_head) — already φ-mapped (feature map applied).
|
| v : (B, L, d_head) — raw (no feature map on v).
|
| Returns y: (B, L, d_head).
|
|
|
| Math:
|
| S_t = Σ_{i≤t} k_i ⊗ v_i (B, d_head, d_head)
|
| z_t = Σ_{i≤t} k_i (B, d_head)
|
| y_t = (q_t · S_t) / (q_t · z_t)
|
| """
|
| B, L, D = q.shape
|
|
|
| S = torch.zeros(B, D, D, dtype=q.dtype, device=q.device)
|
| z = torch.zeros(B, D, dtype=q.dtype, device=q.device)
|
| outputs = []
|
| for t in range(L):
|
| kt = k[:, t, :]
|
| vt = v[:, t, :]
|
|
|
|
|
| S = S + kt.unsqueeze(2) * vt.unsqueeze(1)
|
| z = z + kt
|
| qt = q[:, t, :]
|
| num = torch.bmm(qt.unsqueeze(1), S).squeeze(1)
|
| denom = (qt * z).sum(dim=1, keepdim=True)
|
|
|
| safe = denom.abs() > 1e-10
|
| y_t = torch.where(safe, num / (denom + 1e-20), torch.zeros_like(num))
|
| outputs.append(y_t)
|
| return torch.stack(outputs, dim=1)
|
|
|
| def _linear_attention_causal_vectorized(
|
| self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor,
|
| carry: tuple = None,
|
| ) -> torch.Tensor:
|
| """Vectorized version of _linear_attention_causal_one_head.
|
|
|
| Same mathematics, but without a Python loop over L. The trick:
|
| precompute the cumulative sums S_t and z_t via a lower-triangular
|
| convolution, then compute all y_t in parallel.
|
|
|
| Equivalence guaranteed by test_attention_vectorized.py (atol 1e-5).
|
|
|
| L8 STATE-CARRY: if `carry = (S0, z0)` is provided (each (B, D, D) and
|
| (B, D)), the running state is INITIALIZED with (S0, z0) instead of
|
| zeros — letting the attention continue across a chunk boundary.
|
|
|
| Returns:
|
| y : (B, L, D) — always the output.
|
| state : (S_final, z_final) — ONLY returned when `carry is not None`
|
| (i.e. a state-carry call). A plain call returns just `y` to
|
| preserve backward compatibility with existing callers.
|
| """
|
| B, L, D = q.shape
|
|
|
|
|
|
|
| outer = torch.einsum("btp,btq->btpq", k, v)
|
|
|
| mask = torch.tril(torch.ones(L, L, dtype=q.dtype, device=q.device))
|
|
|
| S = torch.einsum("tj,bjpq->btpq", mask, outer)
|
|
|
|
|
| z = torch.einsum("tj,bjp->btp", mask, k)
|
|
|
|
|
|
|
|
|
| if carry is not None:
|
| S0, z0 = carry
|
| S = S + S0.unsqueeze(1)
|
| z = z + z0.unsqueeze(1)
|
|
|
|
|
|
|
| num = torch.einsum("btp,btpq->btq", q, S)
|
|
|
| denom = (q * z).sum(dim=-1, keepdim=True)
|
|
|
| safe = denom.abs() > 1e-10
|
| y = torch.where(safe, num / (denom + 1e-20), torch.zeros_like(num))
|
|
|
| if carry is not None:
|
|
|
|
|
| S_final = S[:, -1]
|
| z_final = z[:, -1]
|
| return y, (S_final, z_final)
|
| return y
|
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| """x: (B, L, d_model) → output (B, L, d_model).
|
|
|
| L8 OPTIMIZATION: the original forward looped over levels AND heads
|
| (n_levels × n_heads Python calls to the vectorized attention). Profiling
|
| showed this Python overhead + per-call kernel launch was the dominant
|
| cost (not Kuramoto as the README claimed). We now batch ALL heads and
|
| ALL levels into ONE call to _linear_attention_causal_vectorized by
|
| treating (B, n_levels, n_heads) as a single batch dimension.
|
| """
|
| B, L, _ = x.shape
|
| H, D = self.n_heads, self.d_head
|
| nlev = self.n_levels
|
|
|
|
|
| q_all = torch.einsum("bld,de->ble", x, self.w_qkv[0]) + self.b_qkv[0]
|
| k_all = torch.einsum("bld,de->ble", x, self.w_qkv[1]) + self.b_qkv[1]
|
| v_all = torch.einsum("bld,de->ble", x, self.w_qkv[2]) + self.b_qkv[2]
|
|
|
| q_all = q_all.view(B, L, H, D)
|
| k_all = k_all.view(B, L, H, D)
|
| v_all = v_all.view(B, L, H, D)
|
|
|
|
|
| offsets = self.level_offsets
|
|
|
|
|
| q_lev = q_all.unsqueeze(1) + offsets.view(nlev, 1, 1, 1)
|
| k_lev = k_all.unsqueeze(1) + offsets.view(nlev, 1, 1, 1)
|
| q_feat = elu_plus_one(q_lev, alpha=1.0)
|
| k_feat = elu_plus_one(k_lev, alpha=1.0)
|
|
|
| v_lev = v_all.unsqueeze(1).expand(B, nlev, L, H, D)
|
|
|
|
|
|
|
| q_flat = q_feat.permute(0, 1, 3, 2, 4).reshape(B * nlev * H, L, D)
|
| k_flat = k_feat.permute(0, 1, 3, 2, 4).reshape(B * nlev * H, L, D)
|
| v_flat = v_lev.permute(0, 1, 3, 2, 4).reshape(B * nlev * H, L, D)
|
| y_flat = self._linear_attention_causal_vectorized(q_flat, k_flat, v_flat)
|
|
|
| y = y_flat.reshape(B, nlev, H, L, D).permute(0, 1, 3, 2, 4).reshape(B, nlev, L, H * D)
|
|
|
| level_weights = stable_softmax(self.level_logits, dim=-1)
|
|
|
| attn = (y * level_weights.view(1, nlev, 1, 1)).sum(dim=1)
|
|
|
|
|
| return attn @ self.w_out + self.b_out
|
|
|