|
|
|
|
|
|
|
|
|
|
|
|
|
|
| import math
|
| import torch
|
| import torch.nn as nn
|
| import torch.nn.functional as F
|
| from torch.nn.attention import sdpa_kernel, SDPBackend
|
|
|
| from model.config import PyCraftConfig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| class RMSNorm(nn.Module):
|
| def __init__(self, dim: int, eps: float = 1e-6):
|
| super().__init__()
|
| self.eps = eps
|
| self.weight = nn.Parameter(torch.ones(dim))
|
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
|
|
|
| rms = x.pow(2).mean(dim=-1, keepdim=True).add(self.eps).rsqrt()
|
| return x * rms * self.weight
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| class RotaryEmbedding(nn.Module):
|
| def __init__(self, head_dim: int, max_seq_len: int, theta: float = 10000.0):
|
| super().__init__()
|
|
|
|
|
| inv_freq = 1.0 / (
|
| theta ** (torch.arange(0, head_dim, 2).float() / head_dim)
|
| )
|
| self.register_buffer("inv_freq", inv_freq, persistent=False)
|
|
|
|
|
| self._build_cache(max_seq_len)
|
|
|
| def _build_cache(self, seq_len: int):
|
| positions = torch.arange(seq_len, device=self.inv_freq.device).float()
|
|
|
| freqs = torch.outer(positions, self.inv_freq)
|
|
|
| emb = torch.cat([freqs, freqs], dim=-1)
|
| self.register_buffer("cos_cache", emb.cos(), persistent=False)
|
| self.register_buffer("sin_cache", emb.sin(), persistent=False)
|
|
|
| def _rotate_half(self, x: torch.Tensor) -> torch.Tensor:
|
| """Rotate the second half of the last dimension to implement complex multiply."""
|
| half = x.shape[-1] // 2
|
| x1, x2 = x[..., :half], x[..., half:]
|
| return torch.cat([-x2, x1], dim=-1)
|
|
|
| def forward(
|
| self,
|
| q: torch.Tensor,
|
| k: torch.Tensor,
|
| offset: int = 0,
|
| ):
|
| seq_len = q.shape[2]
|
| cos = self.cos_cache[offset: offset + seq_len]
|
| sin = self.sin_cache[offset: offset + seq_len]
|
|
|
|
|
| cos = cos.unsqueeze(0).unsqueeze(0)
|
| sin = sin.unsqueeze(0).unsqueeze(0)
|
|
|
| q_rot = q * cos + self._rotate_half(q) * sin
|
| k_rot = k * cos + self._rotate_half(k) * sin
|
| return q_rot, k_rot
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| class GroupedQueryAttention(nn.Module):
|
| def __init__(self, config: PyCraftConfig):
|
| super().__init__()
|
| self.d_model = config.d_model
|
| self.n_heads = config.n_heads
|
| self.n_kv_heads = config.n_kv_heads
|
| self.head_dim = config.head_dim
|
| self.use_qk_norm = config.use_qk_norm
|
| self.n_rep = config.n_heads_per_kv
|
|
|
|
|
| self.wq = nn.Linear(config.d_model, config.n_heads *
|
| config.head_dim, bias=False)
|
| self.wk = nn.Linear(
|
| config.d_model, config.n_kv_heads * config.head_dim, bias=False)
|
| self.wv = nn.Linear(
|
| config.d_model, config.n_kv_heads * config.head_dim, bias=False)
|
| self.wo = nn.Linear(config.n_heads * config.head_dim,
|
| config.d_model, bias=False)
|
|
|
|
|
|
|
| if self.use_qk_norm:
|
| self.q_norm = RMSNorm(config.head_dim)
|
| self.k_norm = RMSNorm(config.head_dim)
|
|
|
|
|
| self.rope = RotaryEmbedding(
|
| head_dim=config.head_dim,
|
| max_seq_len=config.max_seq_len,
|
| theta=config.rope_theta,
|
| )
|
|
|
| def _repeat_kv(self, x: torch.Tensor) -> torch.Tensor:
|
| """
|
| Expand KV heads to match Q head count for SDPA.
|
| (batch, n_kv_heads, seq, head_dim) → (batch, n_heads, seq, head_dim)
|
| """
|
| if self.n_rep == 1:
|
| return x
|
| batch, n_kv, seq, head_dim = x.shape
|
| x = x.unsqueeze(2).expand(batch, n_kv, self.n_rep, seq, head_dim)
|
| return x.reshape(batch, n_kv * self.n_rep, seq, head_dim)
|
|
|
| def forward(
|
| self,
|
| x: torch.Tensor,
|
| attn_mask: torch.Tensor | None = None,
|
| ) -> torch.Tensor:
|
| batch, seq_len, _ = x.shape
|
|
|
|
|
| q = self.wq(x)
|
| k = self.wk(x)
|
| v = self.wv(x)
|
|
|
|
|
| q = q.view(batch, seq_len, self.n_heads,
|
| self.head_dim).transpose(1, 2)
|
| k = k.view(batch, seq_len, self.n_kv_heads,
|
| self.head_dim).transpose(1, 2)
|
| v = v.view(batch, seq_len, self.n_kv_heads,
|
| self.head_dim).transpose(1, 2)
|
|
|
|
|
|
|
| if self.use_qk_norm:
|
| q = self.q_norm(q)
|
| k = self.k_norm(k)
|
|
|
|
|
| q, k = self.rope(q, k)
|
|
|
|
|
| k = self._repeat_kv(k)
|
| v = self._repeat_kv(v)
|
|
|
|
|
|
|
|
|
|
|
| with sdpa_kernel(SDPBackend.EFFICIENT_ATTENTION):
|
| attn_out = F.scaled_dot_product_attention(
|
| q, k, v,
|
| attn_mask=None,
|
| dropout_p=0.0,
|
| is_causal=True,
|
| )
|
|
|
|
|
|
|
| attn_out = attn_out.transpose(1, 2).contiguous().view(
|
| batch, seq_len, self.d_model)
|
| return self.wo(attn_out)
|
|
|
|
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
| from model.config import get_config_tiny
|
|
|
| torch.manual_seed(42)
|
| device = "cuda" if torch.cuda.is_available() else "cpu"
|
| cfg = get_config_tiny()
|
|
|
| print(f"Testing GroupedQueryAttention on {device}...")
|
| print(f" n_heads={cfg.n_heads}, n_kv_heads={cfg.n_kv_heads}, "
|
| f"head_dim={cfg.head_dim}, use_qk_norm={cfg.use_qk_norm}")
|
|
|
| attn = GroupedQueryAttention(cfg).to(device)
|
|
|
|
|
| n_params = sum(p.numel() for p in attn.parameters())
|
| print(f" Attention block params: {n_params:,}")
|
|
|
|
|
| x = torch.randn(2, 64, cfg.d_model, device=device)
|
| with torch.no_grad():
|
| out = attn(x)
|
|
|
| print(f" Input shape: {tuple(x.shape)}")
|
| print(f" Output shape: {tuple(out.shape)}")
|
| assert out.shape == x.shape, "Output shape mismatch!"
|
|
|
|
|
| x.requires_grad_(True)
|
| x_grad = torch.randn(2, 64, cfg.d_model, device=device)
|
| x_grad.requires_grad_(True)
|
| out2 = attn(x_grad)
|
| loss = out2.sum()
|
| loss.backward()
|
| assert x_grad.grad is not None, "No gradient!"
|
| print(f" Gradient norm: {x_grad.grad.norm().item():.4f}")
|
| print(" All attention tests PASSED.")
|
|
|