File size: 2,360 Bytes
37d8122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Rotary Position Embedding (RoPE) — implémenté à la main.

RoPE encode la position en **faisant tourner** par paires les dimensions de Q et K,
au lieu d'**ajouter** un vecteur de position aux embeddings. Propriétés :

* la position devient **relative** dans le produit scalaire Q·K ;
* aucun paramètre appris, aucune contrainte sur la longueur ;
* meilleure extrapolation en longueur qu'un embedding positionnel absolu appris.

Justification complète : docs/architecture.md §2.1.

Convention : les dimensions ``(2i, 2i+1)`` d'un vecteur forment la paire ``i``.
"""

from __future__ import annotations

import torch


def build_rope_cache(
    seq_len: int,
    head_dim: int,
    theta: float = 10000.0,
    device: torch.device | None = None,
    dtype: torch.dtype = torch.float32,
) -> tuple[torch.Tensor, torch.Tensor]:
    """Précalcule ``cos`` et ``sin`` pour les positions ``0..seq_len-1``.

    Retourne deux tenseurs de forme ``(seq_len, head_dim)``, où chaque paire
    ``(2i, 2i+1)`` partage le même angle.
    """
    if head_dim % 2 != 0:
        raise ValueError(f"head_dim doit être pair pour RoPE, reçu {head_dim}")

    half = head_dim // 2
    # Fréquences décroissantes : inv_freq[i] = theta^(-2i/head_dim)
    inv_freq = 1.0 / (theta ** (torch.arange(0, half, device=device, dtype=torch.float32) / half))
    positions = torch.arange(seq_len, device=device, dtype=torch.float32)

    freqs = torch.outer(positions, inv_freq)  # (seq_len, half)
    emb = torch.cat([freqs, freqs], dim=-1)   # (seq_len, head_dim)
    return emb.cos().to(dtype), emb.sin().to(dtype)


def rotate_half(x: torch.Tensor) -> torch.Tensor:
    """Échange et inverse les deux moitiés : ``[a, b] -> [-b, a]`` sur la dernière dim."""
    half = x.shape[-1] // 2
    x1, x2 = x[..., :half], x[..., half:]
    return torch.cat([-x2, x1], dim=-1)


def apply_rope(
    q: torch.Tensor,
    k: torch.Tensor,
    cos: torch.Tensor,
    sin: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
    """Applique RoPE à Q et K.

    ``q``, ``k`` : ``(batch, n_head, seq_len, head_dim)``.
    ``cos``, ``sin`` : ``(seq_len, head_dim)`` (broadcast sur batch et têtes).
    """
    cos = cos[None, None, :, :]
    sin = sin[None, None, :, :]
    q_rot = q * cos + rotate_half(q) * sin
    k_rot = k * cos + rotate_half(k) * sin
    return q_rot, k_rot