Cortex-0 / src /model /attention.py
Frankenstein-Labs's picture
feat(vision): Phase 4 — encodeur image ViT
6857ec0 verified
Raw History Blame Contribute Delete
3.38 kB
"""Multi-Head Self-Attention causale — écrite à la main.
Aucun `torch.nn.MultiheadAttention`, aucun module `transformers` : tout est
explicite (projections, découpage en têtes, masque causal, softmax, RoPE).
Causalité : la position ``t`` ne peut pas regarder ``t+1``. On le garantit par
un masque triangulaire supérieur mis à ``-inf`` avant le softmax, donc les poids
d'attention vers le futur valent **exactement 0**.
"""
from __future__ import annotations
import math
import torch
import torch.nn as nn
from src.model.rope import apply_rope, build_rope_cache
def build_causal_mask(seq_len: int, device: torch.device | None = None) -> torch.Tensor:
"""Masque booléen ``(seq_len, seq_len)`` : True = position autorisée.
``mask[i, j] = j <= i`` (une position voit le passé et elle-même).
"""
idx = torch.arange(seq_len, device=device)
return idx[None, :] <= idx[:, None]
class MultiHeadSelfAttention(nn.Module):
"""Attention multi-têtes causale avec RoPE."""
def __init__(
self,
n_embd: int,
n_head: int,
dropout: float = 0.0,
bias: bool = False,
rope_theta: float = 10000.0,
use_rope: bool = True,
causal: bool = True,
):
super().__init__()
if n_embd % n_head != 0:
raise ValueError(f"n_embd ({n_embd}) doit être divisible par n_head ({n_head})")
self.n_embd = n_embd
self.n_head = n_head
self.head_dim = n_embd // n_head
self.dropout = dropout
self.rope_theta = rope_theta
self.use_rope = use_rope
# causal=True pour le texte (décodage autorégressif) ; causal=False pour
# les encodeurs (image/audio), où chaque token voit toute la séquence.
self.causal = causal
# Projections Q, K, V et sortie. Pas de biais par défaut (style moderne).
self.q_proj = nn.Linear(n_embd, n_embd, bias=bias)
self.k_proj = nn.Linear(n_embd, n_embd, bias=bias)
self.v_proj = nn.Linear(n_embd, n_embd, bias=bias)
self.out_proj = nn.Linear(n_embd, n_embd, bias=bias)
self.attn_dropout = nn.Dropout(dropout)
self.resid_dropout = nn.Dropout(dropout)
def forward(
self,
x: torch.Tensor,
cos: torch.Tensor | None = None,
sin: torch.Tensor | None = None,
) -> torch.Tensor:
B, T, C = x.shape
q = self.q_proj(x).view(B, T, self.n_head, self.head_dim).transpose(1, 2)
k = self.k_proj(x).view(B, T, self.n_head, self.head_dim).transpose(1, 2)
v = self.v_proj(x).view(B, T, self.n_head, self.head_dim).transpose(1, 2)
# -> (B, n_head, T, head_dim)
if self.use_rope:
if cos is None or sin is None:
cos, sin = build_rope_cache(T, self.head_dim, self.rope_theta, x.device, x.dtype)
q, k = apply_rope(q, k, cos, sin)
# Scores d'attention : (B, n_head, T, T)
att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
if self.causal:
mask = build_causal_mask(T, x.device)
att = att.masked_fill(~mask, float("-inf"))
att = torch.softmax(att, dim=-1)
att = self.attn_dropout(att)
y = att @ v # (B, n_head, T, head_dim)
y = y.transpose(1, 2).contiguous().view(B, T, C)
return self.resid_dropout(self.out_proj(y))