File size: 6,546 Bytes
a2ec932
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"""
CausalSelfAttention
────────────────────
Modern causal self-attention with:

1. SCALED DOT-PRODUCT ATTENTION (SDPA)
   torch.nn.functional.scaled_dot_product_attention dispatches to Flash
   Attention 2 on CUDA automatically. Flash Attention rewrites the standard
   O(NΒ²) memory algorithm as a tiled, IO-efficient kernel β€” peak memory is
   O(N) instead of O(NΒ²), enabling much longer contexts on the same GPU.

2. ROTARY POSITION EMBEDDINGS (RoPE)
   Applied to Q and K before attention. Encodes relative position as a
   rotation in head-dimension space β€” see positional_encoding.py.

3. GROUPED-QUERY ATTENTION (GQA)
   Standard MHA uses n_heads Q, K, V projections.
   GQA shares fewer KV heads across multiple Q heads:
     - n_kv_heads == n_heads β†’ standard MHA (original Transformer)
     - n_kv_heads <  n_heads β†’ GQA (Llama 3, Mistral)
     - n_kv_heads == 1       β†’ MQA (one shared KV pair)
   GQA reduces KV-cache memory at inference by a factor of (n_heads / n_kv_heads)
   with minimal quality loss.

4. KV-CACHE
   During autoregressive inference, we only ever process one new token but
   recompute all previous K, V from scratch if we run the full sequence each
   step. The KV-cache stores previous K, V tensors and appends the new ones,
   reducing each inference step from O(TΒ²) to O(T) compute.

5. BIAS-FREE PROJECTIONS
   All linear projections use bias=False, matching Llama/Mistral practice.
   Biases provide negligible benefit in large models and waste memory.
"""

import torch
import torch.nn as nn
import torch.nn.functional as F

from model.positional_encoding import apply_rotary_emb


class CausalSelfAttention(nn.Module):
    """
    Multi-head causal self-attention with RoPE, GQA, SDPA, and KV-cache.

    Args:
        d_model:    Model hidden dimension.
        n_heads:    Number of query attention heads.
        n_kv_heads: Number of key/value heads (≀ n_heads). For standard MHA
                    set n_kv_heads == n_heads.
        dropout:    Attention dropout probability (0.0 recommended at scale).
    """

    def __init__(
        self,
        d_model:    int,
        n_heads:    int,
        n_kv_heads: int,
        dropout:    float = 0.0,
    ):
        super().__init__()
        assert d_model % n_heads == 0, (
            f"d_model ({d_model}) must be divisible by n_heads ({n_heads})"
        )
        assert n_heads % n_kv_heads == 0, (
            f"n_heads ({n_heads}) must be divisible by n_kv_heads ({n_kv_heads})"
        )

        self.n_heads    = n_heads
        self.n_kv_heads = n_kv_heads
        self.head_dim   = d_model // n_heads
        self.dropout    = dropout
        self.kv_groups  = n_heads // n_kv_heads   # how many Q heads share each KV head

        # Separate Q/K/V projections β€” sizes differ for GQA
        self.q_proj = nn.Linear(d_model, n_heads    * self.head_dim, bias=False)
        self.k_proj = nn.Linear(d_model, n_kv_heads * self.head_dim, bias=False)
        self.v_proj = nn.Linear(d_model, n_kv_heads * self.head_dim, bias=False)
        self.o_proj = nn.Linear(d_model, d_model,                    bias=False)

    def forward(
        self,
        x:       torch.Tensor,
        cos:     torch.Tensor,
        sin:     torch.Tensor,
        past_kv: tuple[torch.Tensor, torch.Tensor] | None = None,
    ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
        """
        Args:
            x:       Input, shape [B, T, d_model].
            cos:     RoPE cosines, shape [1, 1, T, head_dim].
            sin:     RoPE sines,   shape [1, 1, T, head_dim].
            past_kv: Optional cached (k, v) from previous decode steps.
                     - None  β†’ training / first inference step (is_causal=True)
                     - tuple β†’ incremental decode step (append to cache)

        Returns:
            out:        Attention output, shape [B, T, d_model].
            present_kv: Updated (k, v) cache tuple for use in the next step.
        """
        B, T, _ = x.shape

        # ── Project Q, K, V ──────────────────────────────────────────────────
        q = self.q_proj(x).view(B, T, self.n_heads,    self.head_dim).transpose(1, 2)
        k = self.k_proj(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
        v = self.v_proj(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
        # Shapes after transpose: [B, heads, T, head_dim]

        # ── Apply Rotary Embeddings ───────────────────────────────────────────
        q, k = apply_rotary_emb(q, k, cos, sin)

        # ── KV-Cache (inference only) ─────────────────────────────────────────
        # Append new K/V to the cached history so we never recompute old tokens.
        if past_kv is not None:
            k = torch.cat([past_kv[0], k], dim=2)   # grow along sequence axis
            v = torch.cat([past_kv[1], v], dim=2)

        present_kv = (k, v)   # return updated cache

        # ── GQA: broadcast KV heads to match Q heads ─────────────────────────
        # Each group of (n_heads/n_kv_heads) Q heads attends to the same KV pair.
        if self.kv_groups > 1:
            k = k.repeat_interleave(self.kv_groups, dim=1)   # [B, n_heads, S, head_dim]
            v = v.repeat_interleave(self.kv_groups, dim=1)

        # ── Scaled Dot-Product Attention ──────────────────────────────────────
        # is_causal=True  β†’ auto-builds a causal mask (training / first step)
        # is_causal=False β†’ no mask (KV-cache handles causality by construction)
        # PyTorch dispatches this to Flash Attention 2 on CUDA when available.
        is_causal = past_kv is None
        attn_out = F.scaled_dot_product_attention(
            q, k, v,
            dropout_p = self.dropout if self.training else 0.0,
            is_causal = is_causal,
        )

        # ── Merge heads and project ───────────────────────────────────────────
        out = attn_out.transpose(1, 2).contiguous().view(B, T, -1)
        return self.o_proj(out), present_kv