🧙 Maestro Yoda Cap. 19 · Architetture

Puntata 207

Puntata 207 — Positional encoding

Livello: 🧙 Maestro Yoda · Capitolo 19 · Architetture

L’attention non sa l’ordine. Per il modello, “il cane morde l’uomo” e “l’uomo morde il cane” sarebbero la stessa frase. Il positional encoding evita la cronaca giudiziaria.

due frasi identiche nei token ma con ordine permutato, sopra di esse una pioggia di colori sinusoidali (sin/cos) che marca ogni posizione. Tag: “questa è la differenza tra significato e disordine”

Il problema, in 30 secondi

La self-attention è permutation-invariant: se mescoli l’ordine dei token in input, l’output del blocco è (a meno della permutazione corrispondente) lo stesso. Il modello non sa che token_3 viene dopo token_2.

Soluzione: iniettare informazione di posizione, tipicamente sommandola all’embedding del token prima del primo blocco, oppure modificandola dentro l’attention.

Negli ultimi 8 anni questa apparentemente semplice scelta è stata reinventata 5-6 volte. Vale la pena conoscere le 3 che dominano nel 2026.


1. Positional encoding sinusoidale (paper originale 2017)

L’idea di Vaswani et al.: per ogni posizione pos, costruisci un vettore di dimensione d_model dove le componenti sono sin e cos di varie frequenze:

PE(pos, 2i)   = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

Le componenti spaziano da frequenze altissime (cambia rapidamente) a frequenze bassissime (cambia lentamente). Caratteristiche:

  • Determinista: no parametri appresi. Niente da trainare.
  • Estrapolabile in teoria: posizioni mai viste in training sono ancora calcolabili.
  • Lineare per traslazioni: PE(pos+k) è una rotazione di PE(pos), fatto che si dimostra utile per relazioni relative.

In pratica, vecchio: faceva fatica oltre il context di training, addestrare un modello a context 2k e farlo girare a 8k dava risultati pessimi.


2. Learned positional embeddings (GPT-1/2, BERT)

Più stupido, ma per anni più efficace: una embedding table di dimensione max_length × d_model con righe imparate come parametri.

pos_emb = nn.Embedding(max_seq_len, d_model)
x = tok_emb + pos_emb(positions)
  • Pro: nessuna assunzione, il modello impara la struttura che vuole.
  • Contro: non estrapola affatto. Position 4097 non esiste se hai trainato a 4096. Game over.

Tutti i modelli pre-2022 vivevano con questo vincolo. Estendere il context era un’impresa, non un parametro.


3. ALiBi (Press et al., 2021) — attenzione “naturalmente decadente”

Attention with Linear Biases: niente embedding di posizione. Invece, dentro l’attention, sottrai dai logit una penalità proporzionale alla distanza:

attn_logits = Q K^T - m · |pos_i - pos_j|

m è una pendenza fissa, una diversa per ogni testa (preassegnata, non appresa).

  • Pro: estrapola sorprendentemente bene. Modello trainato a 1k token spesso funziona ragionevolmente a 4k, 8k.
  • Contro: l’attention diventa monotonicamente decadente sulla distanza, perdendo precisione su “lookup chirurgici” lontani.
  • Adottato da BLOOM, MPT, Falcon-180B. Poi caduto in disuso quando RoPE ha vinto.

4. RoPE (Su et al., 2021) — il vincitore

Rotary Position Embedding: invece di sommare un vettore di posizione, ruota le componenti di Q e K di un angolo proporzionale alla posizione.

Intuitivamente: tratti (x_{2i}, x_{2i+1}) come un numero complesso e lo moltiplichi per e^(i · θ_pos), una rotazione nel piano.

Tre proprietà magiche:

  1. L’inner product Q_i · K_j dipende solo da (i - j): posizioni relative gratis, dalla geometria.
  2. Invariante per traslazione assoluta — sposta tutto in avanti di k token, niente cambia.
  3. Fatto dentro l’attention, non sull’embedding iniziale: ogni layer rivede la posizione.

Adottato da: Llama (tutti), Mistral, Gemma, DeepSeek, Qwen, GPT-J, GPT-NeoX. Standard di fatto.

def apply_rope(x, pos, theta_base=10000):
    # x: (..., d) con d pari
    half_d = x.shape[-1] // 2
    freqs = theta_base ** (-torch.arange(half_d) / half_d * 2)
    angles = pos[..., None] * freqs
    cos = angles.cos()
    sin = angles.sin()
    x1, x2 = x[..., :half_d], x[..., half_d:]
    return torch.cat([x1*cos - x2*sin, x1*sin + x2*cos], dim=-1)

RoPE scaling: estendere il context senza retrainare da zero

Qui le cose interessanti del 2023-2026. Llama-2 era a 4k. Per portarlo a 32k senza un altro full training:

Linear / Position Interpolation (Chen et al., Meta 2023)

“Comprimi” linearmente le posizioni dentro l’intervallo trainato: pos → pos / s con s fattore di scala. Veloce, ma perde risoluzione locale.

NTK-aware scaling (community Reddit, poi formalizzato)

Invece di scalare linearmente, modifica theta_base: θ_base × s^(d/(d-2)). Conserva meglio la high-frequency, estende meglio.

YaRN (Peng et al., 2023)

“Yet another RoPE extensioN”: mix più sofisticato che scala frequenze in modo non uniforme + correzione del prompt iniziale. È quello che usa, ad esempio, DeepSeek-V2/V3 per arrivare a 128k.

LongRoPE (Microsoft, 2024)

Cerca automaticamente lo scaling ottimale per ogni dimensione di RoPE via algoritmo evolutivo. Usato in Phi-3 per arrivare a 128k context.

Tutte queste tecniche richiedono di solito un breve fine-tuning di seguito (~1B token sufficienti) per stabilizzare. Nessuna è “gratuita”.


5. NoPE (Kazemnejad et al., 2023) e oltre

In alcuni paper recenti si è dimostrato che modelli decoder-only senza positional encoding comunque imparano la posizione, perché la maschera causale già spezza la simmetria.

Risultato: NoPE può estrapolare meglio dei modelli con RoPE su context molto lunghi, ma in-distribution è di solito peggio. Usato sperimentalmente, non in produzione.


Confronto pratico (2026)

SchemaApprende?Estrapola?Stato 2026
SinusoidalNoMediocreStorico
LearnedNoSolo BERT-era
ALiBiSolo slopesSì (decadente)Marginale
RoPENoSì (con scaling)Standard
RoPE + YaRN/LongRoPENo + scalingSì (fino a 128k–1M)Production
NoPENienteSperimentale

Quanto pesa nella pratica?

Per la maggior parte degli sviluppatori applicativi: niente, se usi transformers o un’API. Il positional encoding è scritto nel modello e amen.

Diventa rilevante in 3 scenari:

  1. Stai estendendo il context oltre quello nativo. Devi conoscere le tecniche di RoPE scaling, e accettare che certe abilità degradino (matematica, retrieval) anche se il modello “non rifiuta” la lunghezza.
  2. Stai trainando un modello custom. RoPE è la scelta default; ALiBi solo per casi specifici (modelli di streaming infinito).
  3. Stai debuggando regression su context lunghi. Spesso il problema è proprio nella scelta del theta_base o del fattore di scaling.

Glossario lampo

  • Position-relative attention — l’attention dipende dalla differenza di posizione, non dalla posizione assoluta.
  • Theta base — la “scala temporale” delle frequenze in RoPE. Default 10000, alzato a 500000 o 1M per context molto lunghi (Llama-3.1, Mistral-Large).
  • Context extension — pratica di portare un modello al di là del context su cui è stato pretrainato.

TEST hands-on

  1. In Python, implementa RoPE in 15 righe (cos/sin + rotazione). Verifica che Q_rot_i · K_rot_j dipenda solo da i - j. Una funzione, due input, due posizioni a coppie diverse ma con stessa differenza → stesso inner product (a meno di noise numerico).
  2. Su HuggingFace, carica meta-llama/Llama-3.2-3B e ispeziona model.config.rope_theta. Default è 500000.
  3. Modifica rope_theta a 100000 e prova un prompt lungo (es. 8k token). Verifica che la qualità cali drasticamente: hai “scollegato” il modello dalla sua tabella di posizioni.
  4. Bonus: leggi il paper di YaRN (arXiv:2309.00071) e capisci la differenza tra “interpolazione lineare” e “blended interpolation by frequency”. È matematica leggera ma profonda.

Take-away

Il positional encoding è il dettaglio che separa un transformer giocattolo da uno utilizzabile su context lunghi. RoPE ha vinto perché non aggiunge parametri, non aggiunge calcolo, e si presta a “estensioni” ex-post. Il resto della letteratura è ingegneria attorno a quella scelta.


➡️ Prossima puntata: Multi-head attention — perché serve avere più “occhi” sulla stessa scena.