Attention e transformer block, derivati da una media
Parti dal riassunto più economico di un context — la media — misura quanto fallisce e lascia emergere la formula dell’attention.
In questa pagina
Arrivi qui con un tokenizer dal Capitolo 7, una tabella di embedding dal Capitolo 8 e l’obiettivo che li accompagna: dati i token finora, assegnare una probabilità al prossimo.
Manca il mezzo. Per prevedere il token il modello ha bisogno di un vettore che riassuma tutto ciò che lo precede, e nulla di ciò che hai costruito ne produce uno. L’embedding del token non basta: quello è un modello bigram, e non può sapere che la frase è iniziata con una domanda. Nemmeno una concatenazione di tutti gli embedding precedenti basta: il loro numero cambia a ogni passo, e una matrice di pesi fissa non può accettare un input di lunghezza variabile.
Quindi: un vettore di dimensione fissa che riassume un numero variabile di vettori. Questo è tutto il problema, e attention è ciò che ottieni risolvendolo nel modo più pigro possibile e poi riparando le due cose che si rompono.
La risposta che il campo aveva, e perché non la stiamo costruendo
Link alla sezione: La risposta che il campo aveva, e perché non la stiamo costruendoDal 1997 a circa il 2017 il riassunto era uno stato ricorrente: mantieni un vettore e aggiornalo a ogni token, . Dimensione fissa, input variabile, esattamente la forma giusta.
Falliva in tre modi, e l’architettura di questo capitolo risponde a tutti e tre. Fare backpropagation attraverso passi moltiplica Jacobiane, quindi il gradiente svanisce o esplode — la malattia che il Capitolo 5 ha misurato dentro un singolo nodo . La LSTM1 fu progettata esattamente contro questo problema e spinse l’intervallo utilizzabile da decine di passi a centinaia, senza cambiare il fatto che l’informazione dal token 5 raggiunge il token 500 solo sopravvivendo a 495 aggiornamenti sequenziali. L’intera sorgente doveva stare in un solo vettore: nella traduzione sequence-to-sequence2 un encoder comprime l’input nel suo stato finale. Bahdanau, Cho e Bengio diedero un nome a quel collo di bottiglia e lo risolsero nel 2014, tre anni prima del transformer, lasciando che il decoder prendesse una somma pesata di tutti gli stati dell’encoder con pesi calcolati da sé.3 Tutto ciò che segue è quell’idea, applicata da una sequenza a sé stessa, con la ricorrenza eliminata. E l’aggiornamento è sequenziale per costruzione: ha bisogno di , e una GPU con diecimila core non può farci nulla. L’architettura che ha vinto non è ovviamente più intelligente; è quella il cui passo costoso è una moltiplicazione di matrici.
L’altro bias induttivo classico, la convoluzione — far scorrere un piccolo filtro su tutto l’input, così una feature rilevata ovunque viene rilevata ovunque — non viene costruito nemmeno qui; è quasi esattamente giusto per le immagini ed è delegato a un corso di visione. Né ricorrenza né convoluzione riappaiono dopo questa pagina, motivo per cui nessuna delle due ottiene un capitolo: il Capitolo 1 ha promesso che le omissioni sarebbero state dichiarate, non lasciate in silenzio.
Il riassunto più economico che esista
Link alla sezione: Il riassunto più economico che esistaLa funzione più ovvia che prende un numero variabile di vettori e restituisce un vettore è la media:
Qualsiasi numero di input, dimensione di output fissa, differenziabile, gratis. Tabella di embedding più questa media più un layer lineare verso il vocabolario: un language model completo in quindici righe. È anche terribile, e il modo in cui è terribile è l’intera derivazione.
Il corpus qui sotto è un megabyte di Shakespeare, 1.115.394 caratteri, passato attraverso un tokenizer BPE a livello di byte del tipo costruito nel Capitolo 7 con un vocabolario di 1024: 459.760 token da 2,43 caratteri ciascuno, divisi 90/10. Ogni modello ha larghezza 128, vede 128 token e si allena per 3000 passi di AdamW a con un batch di 64. La perplexity è calcolata sullo split tenuto da parte.4
| modello | parametri | perplexity di validazione |
|---|---|---|
| solo il token corrente, nessun context | 263.168 | 59,71 |
| più la media uniforme di tutto ciò che lo precede | 263.168 | 248,07 |
| più position embedding appresi | 279.552 | 245,93 |
| media uniforme aggiunta al token invece di sostituirlo | 263.168 | 60,45 |
Leggi la seconda riga due volte. Fare la media del context non aiuta un po’; rende il modello quattro volte peggiore che ignorare completamente il context. Due ragioni, entrambe dimostrabili più che empiriche.
La media non vede l’ordine. L’addizione commuta, quindi rimescolare la window lascia il riassunto invariato — non approssimativamente:
A = torch.tril(torch.ones(T, T))
A = A / A.sum(1, keepdim=True) # rows of the averaging matrix
y = x[torch.randperm(T)] # the same tokens, shuffled
print((A[-1] @ x - A[-1] @ y).abs().max().item())2.9802322387695312e-08Rumore floating-point su una somma riordinata: i due riassunti sono lo stesso vettore. Un modello la cui unica vista del context è una media non può distinguere il cane ha morso l’uomo da l’uomo ha morso il cane. La terza riga dimostra che non si risolve aggiungendo posizioni agli input: un position embedding appreso su ogni token prima della media ha guadagnato 2,14 punti su 188. Le posizioni entrano nella somma, e la somma le dimentica.
E la media annega il presente. Alla posizione 100 il token corrente è un centesimo del riassunto. Questo ha una soluzione economica che possiedi già: tieni il token e aggiungi il riassunto — una residual connection, dal Capitolo 6, e la quarta riga mostra cosa fa. Riparata la diluizione, la media uniforme non contribuisce nulla: 60,45 contro una baseline di 59,71. Ogni token è lì dentro, pesato allo stesso modo, e pesare tutto allo stesso modo equivale a non avere informazione.
Il problema non è fare la media. Sono i pesi.
La media è una moltiplicazione di matrici, e la mask è una softmax
Link alla sezione: La media è una moltiplicazione di matrici, e la mask è una softmaxFare la media su un prefisso crescente sembra un loop. È una moltiplicazione per una matrice triangolare inferiore le cui righe sommano a uno — ed è anche, esattamente, una softmax:
loop = torch.stack([x[:t + 1].mean(0) for t in range(T)]) # the obvious version
A = torch.tril(torch.ones(T, T))
A = A / A.sum(1, keepdim=True)
mat = A @ x # the same thing
S = torch.zeros(T, T).masked_fill(torch.tril(torch.ones(T, T)) == 0, float("-inf"))
soft = F.softmax(S, dim=-1) @ x # and the same thing againloop vs matmul max |diff| = 5.960464477539063e-08
loop vs softmax max |diff| = 5.960464477539063e-08
the averaging matrix A (rows sum to 1, upper triangle is zero):
1.000 0.000 0.000 0.000 0.000 0.000
0.500 0.500 0.000 0.000 0.000 0.000
0.333 0.333 0.333 0.000 0.000 0.000
0.250 0.250 0.250 0.250 0.000 0.000
0.200 0.200 0.200 0.200 0.200 0.000
0.167 0.167 0.167 0.167 0.167 0.167Tre componenti nominati di un transformer sono ora sullo schermo. Il triangolo è la causal mask, imposta dall’obiettivo: se la posizione potesse vedere la posizione , la risposta sarebbe nell’input — la fuga che il Capitolo 6 ti ha detto di verificare, solo dentro l’architettura. La softmax è il modo in cui la mask viene implementata: impostare le voci vietate a le manda esattamente a zero e normalizza ciò che resta, quindi masking e normalizzazione sono una sola operazione. (Usa , non -1e9: è il valore che la mask significa, sopravvive a un cast a float16 come , e ti evita di decidere se la costante che hai scelto sia abbastanza grande per l’intervallo in cui ti capita di essere — cioè il riquadro sul floating-point del Capitolo 2 che fa una domanda a cui non devi rispondere.) E gli score sono il parametro libero. La media uniforme è ciò che ottieni quando ogni score consentito è lo stesso numero; mettici qualunque numero e la softmax li trasforma in pesi validi.
Il resto di questo capitolo è una sola domanda: da dove vengono quei numeri?
Query, key, value
Link alla sezione: Query, key, valueNon possono essere parametri semplici. Una matrice appresa sarebbe identica per ogni frase — potrebbe codificare «guarda quattro token indietro», ma mai «guarda il nome a cui si riferisce questo pronome». Il peso che collega la posizione alla posizione deve dipendere da ciò che si trova in entrambe le posizioni, perché la rilevanza è una relazione, non una proprietà: la parola it non è rilevante in sé, è rilevante per qualcosa.
La funzione più economica di due vettori che restituisce un numero è il prodotto scalare del Capitolo 1. Assegna uno score alla posizione per la posizione come e il meccanismo funziona — male, in due modi che forzano tutto il resto. Il prodotto scalare di un vettore con sé stesso è la sua norma al quadrato, quindi ogni token farebbe attention soprattutto a sé stesso. E la relazione sarebbe simmetrica: se it fa attention con forza a animal, allora animal fa attention con forza a it, cosa falsa nel linguaggio, dove un aggettivo ha bisogno del suo nome molto più di quanto il nome abbia bisogno dell’aggettivo.
Quindi dai a ogni token due ruoli, come due mappe lineari apprese di quel token: ciò che questa posizione sta cercando, , la query; e ciò che offre per essere trovata, , la key. Lo score elimina la simmetria, perché : un token può pubblicizzare una cosa e cercarne un’altra.
Una cosa è ancora sbagliata. La somma pesata era sugli stessi, il che costringe ciò che viene copiato a essere ciò che viene abbinato. Il matching vuole le feature che identificano un token; la copia vuole le feature utili a valle. Quindi apprendi una terza mappa, , il value, e somma quelli.
La formula ora è contabilità:
con causal mask, zero sulla diagonale e sotto, e sopra. In codice sono trenta righe, venti delle quali sono forme:
class Head(nn.Module):
"""One head of causal self-attention."""
def __init__(self, d_model, d_head, block):
super().__init__()
self.q = nn.Linear(d_model, d_head, bias=False)
self.k = nn.Linear(d_model, d_head, bias=False)
self.v = nn.Linear(d_model, d_head, bias=False)
self.d_head = d_head
self.register_buffer("mask", torch.tril(torch.ones(block, block)).bool())
def forward(self, x):
T = x.shape[1]
q, k, v = self.q(x), self.k(x), self.v(x)
s = q @ k.transpose(-2, -1) / math.sqrt(self.d_head)
s = s.masked_fill(~self.mask[:T, :T], float("-inf"))
w = F.softmax(s, dim=-1)
return w @ v Score, mask, normalizza, mescola. Tutto il resto è una proiezione.
La divisione per la radice quadrata, e da cosa difende
Link alla sezione: La divisione per la radice quadrata, e da cosa difendeQuasi ogni spiegazione di dice «per evitare che la softmax saturi», il che è vero e non spiega nulla. L’argomento sono due righe di varianza dal Capitolo 2. Se le voci di e sono indipendenti con media zero e varianza uno, ogni prodotto ha varianza uno, e le varianze di cose indipendenti si sommano:
Quindi gli score hanno deviazione standard . Misurata su ventimila coppie casuali:
d Var(q.k) std sqrt(d)
4 3.975 1.994 2.000
16 16.071 4.009 4.000
64 64.249 8.016 8.000
256 253.065 15.908 16.000
1024 1015.562 31.868 32.000Perché conta: la softmax è sensibile alla scala in un modo in cui un layer lineare non lo è. Raddoppiare l’input di un layer lineare raddoppia il suo output; moltiplicare gli score per dieci prima di una softmax trasforma una miscela morbida in una scelta dura. Una riga di 64 score, con e senza divisione:
| peso massimo, non diviso | entropia | token effettivi | peso massimo, diviso | entropia | token effettivi | |
|---|---|---|---|---|---|---|
| 4 | 0,205 | 2,944 | 19,0 | 0,081 | 3,758 | 42,9 |
| 16 | 0,438 | 1,692 | 5,4 | 0,075 | 3,849 | 46,9 |
| 64 | 0,489 | 0,874 | 2,4 | 0,085 | 3,673 | 39,4 |
| 256 | 0,9999 | 0,0007 | 1,0 | 0,143 | 3,547 | 34,7 |
| 1024 | 1,0000 | 0,0000 | 1,0 | 0,132 | 3,644 | 38,3 |
«Token effettivi» è l’esponenziale dell’entropia: su quante posizioni la riga fa davvero la media. Senza divisione, a , una head appena inizializzata fa attention esattamente a un token su 64, scelto da nulla se non dal sorteggio casuale.
È male in forward e peggio in backward, con una forma che il Capitolo 5 ha già misurato su una . Una softmax impegnata su una sola voce ha derivata quasi nulla: la diagonale della sua Jacobiana è , zero a entrambe le estremità. Su duemila righe casuali:
| non diviso | diviso | righe sature (peso massimo sopra 0,99) | |
|---|---|---|---|
| 4 | 0,8427 | 0,9568 | 0,2 % → 0,0 % |
| 64 | 0,2940 | 0,9609 | 17,9 % → 0,0 % |
| 256 | 0,1406 | 0,9609 | 49,1 % → 0,0 % |
| 1024 | 0,0681 | 0,9611 | 70,4 % → 0,0 % |
A , sette righe su dieci sono congelate prima che inizi il training, e una head che parte congelata non può imparare cosa guardare. Dividendo, la quantità resta piatta a 0,96 a ogni larghezza e nulla satura.
Ora la parte che nessuno pubblica: cambia la perplexity finale? Elimina la divisione e allena, a quattro larghezze di head:
| larghezza head | non diviso | diviso per | diviso per |
|---|---|---|---|
| quattro heads, | 37,29 | 38,07 | 37,89 |
| una head, | 48,51 | 46,10 | 45,99 |
| una head, | 65,37 | 47,53 | — |
| una head, | 67,06 | 49,15 | — |
| una head, | 76,69 | 59,17 | — |
Le prime due righe vengono dal budget di 3000 passi sopra; le ultime tre sono una run più breve — 1500 passi, batch di 32, una head, nessuna normalizzazione prima delle proiezioni — con entrambe le varianti nelle stesse identiche impostazioni.
A la divisione non vale nulla e la run senza è leggermente avanti. Non è una licenza a eliminarla, perché a 256 vale 18 punti di perplexity e a 1024 ne vale 17. Il meccanismo è visibile negli score stessi:
| std score all’init | dopo 1500 passi, non diviso | dopo 1500 passi, diviso | righe sature, non diviso | diviso | |
|---|---|---|---|---|---|
| 256 | 10,49 | 121,67 | 2,13 | 91,9 % | 0,8 % |
| 512 | 15,13 | 836,85 | 2,66 | 98,7 % | 1,3 % |
| 1024 | 21,15 | 5147,46 | 3,44 | 99,9 % | 16,5 % |
La head non divisa non recupera. Scappa via: la deviazione standard dei suoi score passa da 21 all’inizializzazione a 5147, l’entropia dell’attention cade a zero e il 99,9 % delle righe mette più di 0,99 del proprio peso su un singolo token. Una volta che una head è un selettore duro, il suo gradiente è quasi zero e nulla la riporta indietro, quindi il collasso è stabile. La head divisa resta a una deviazione standard degli score di 3,44 dopo lo stesso training, cioè una miscela morbida che può ancora essere cambiata.
Vaswani et al. dicono esattamente questo e niente di più: sospettano che i prodotti «crescano in magnitudine per valori grandi di » e dividono.5 La parola grandi porta il peso della frase, e le tabelle dicono dove inizia il grande: nulla a 32, tutto entro 256.
Più di un’opinione, e i due terzi di cui nessuno parla
Link alla sezione: Più di un’opinione, e i due terzi di cui nessuno parlaUna head è una riga di softmax per posizione, quindi contiene una risposta a «cosa è rilevante qui». Prevedere la parola dopo the in the animal that crossed the wet street richiede lo slot sintattico, il soggetto e il token precedente nello stesso momento, e una distribuzione di probabilità non può essere concentrata in tre posti. Quindi esegui più heads in parallelo, ciascuna di larghezza , concatena e mescola con un’altra matrice : hai partizionato la larghezza, non l’hai aumentata.
Attention fa anche esattamente una cosa: sposta informazione tra posizioni. Ogni operazione nel codice sopra è lineare lungo l’asse delle feature, e il Capitolo 5 ha dimostrato cos’è uno stack di mappe lineari. Quindi ogni block contiene anche un piccolo MLP applicato a ogni posizione indipendentemente, che espande la larghezza di quattro volte e torna indietro, con una GELU in mezzo. Vale la pena memorizzare la divisione del lavoro: attention mescola tra posizioni, il feed-forward network calcola dentro una posizione.
La scala completa, ogni riga aggiunge un pezzo alla riga sopra:
| modello | parametri | perplexity di validazione |
|---|---|---|
| media uniforme, aggiunta | 279.552 | 60,45 |
| una attention head, sostituendo il token | 328.704 | 55,47 |
| una attention head, aggiunta | 328.704 | 46,10 |
| quattro heads invece di una | 345.216 | 43,21 |
| più il feed-forward network | 476.928 | 39,87 |
| più LayerNorm — il block completo | 477.696 | 38,07 |
Pesi appresi battono pesi uniformi di 14 punti di perplexity, che è tutto l’argomento di questo capitolo in una riga. Quattro heads comprano altri 3 punti per 16.512 parametri extra. E la stessa head vale 9 punti in più aggiunta che sostituendo: attention porta informazione dentro, non decide cosa sia una posizione.
Ora dove stanno davvero i parametri, cosa che sorprende chi ha visto solo il diagramma:
| larghezza | heads | attention | feed-forward | totale per block |
|---|---|---|---|---|
| 128 | 4 | 65.664 (33,2 %) | 131.712 (66,6 %) | 197.888 |
| 768 | 12 | 2.360.064 (33,3 %) | 4.722.432 (66,6 %) | 7.085.568 |
| 4096 | 32 | 67.112.960 (33,3 %) | 134.238.208 (66,7 %) | 201.367.552 |
Due terzi di ogni transformer block sono il feed-forward network, a ogni scala, perché attention ha quattro matrici e l’MLP ne ha l’equivalente di otto. Qualunque cosa sappia un modello, la maggior parte dei parametri che la contengono si trova nell’MLP per-position.
Residuals e LayerNorm, ereditati dal Capitolo 6
Link alla sezione: Residuals e LayerNorm, ereditati dal Capitolo 6LayerNorm è stata costruita e misurata nel Capitolo 6, e questo capitolo la usa come era stata lasciata; le residual connections sono state nominate e sottoposte ad ablation lì, e vengono costruite qui. Le righe «aggiunto, non sostituito» sopra sono residual connections, valgono 188 punti di perplexity per la media e 9 per una head. LayerNorm7 normalizza ogni esempio sulle sue feature, e il Capitolo 6 ha dato le ragioni per cui è sopravvissuta qui al posto di BatchNorm — nessuna dipendenza dal batch, nessuna statistica running, identica in training e inference, indifferente alla lunghezza della sequenza — ognuna delle quali diventa un requisito quando generi un token alla volta per un utente, dove finisce il Capitolo 13. Costa 768 parametri e compra 1,8 punti di perplexity.
class Block(nn.Module):
def forward(self, x):
x = x + self.att(self.ln1(x))
x = x + self.ff(self.ln2(x))
return xGuarda dove si trova la normalizzazione: sull’input di ogni sub-layer, con il percorso residuale dall’input all’output mai normalizzato. Questo è pre-norm. Il paper del 2017 fa l’opposto, x = LayerNorm(x + Att(x)) — post-norm, che mette una LayerNorm sul percorso residuale stesso.
Xiong et al. hanno spiegato la differenza attraverso il gradiente all’inizializzazione, che in una rete post-norm è scalato male con la profondità — il motivo per cui il transformer originale aveva bisogno di un warmup del learning rate per allenarsi.8 Dodici block, 1000 passi, learning rate :
gradient norm per block at initialisation, before any step
pre-norm block 1 0.0498 ... block 12 0.0657 ratio last/first 1.32
post-norm block 1 0.0977 ... block 12 0.1613 ratio last/first 1.65
pre-norm, no warmup perplexity 37.82
pre-norm, 200-step warmup perplexity 37.62
post-norm, no warmup perplexity 308.05
post-norm, 200-step warmup perplexity 37.88Post-norm senza warmup è otto volte peggiore, e post-norm con warmup eguaglia esattamente pre-norm. Qui il warmup non è una buona pratica generale; è una patch per una disposizione specifica della normalizzazione, e spostare la LayerNorm elimina la necessità di usarlo. Ecco perché praticamente ogni modello dal 2019 è pre-norm, e perché il diagramma del 2017 va letto come storia più che come specifica.
Dov’è un token?
Link alla sezione: Dov’è un token?Elimina i position embedding e il modello si allena comunque; semplicemente non può sapere dove sia qualcosa, e questa è una simmetria più che un fallimento del training. Nulla nello score di attention menziona o stessi, quindi permutare l’input permuta l’output: self-attention è equivariante per permutazione. È la cecità all’ordine della media sotto un travestimento migliore — la causal mask ripristina un po’ di ordine, dato che ogni posizione vede un prefisso diverso, ma dentro un prefisso tutti gli ordinamenti si equivalgono.
Quattro modi di iniettare la posizione, allenati su window da 64 token e valutati a 64, 128 e 256 — oltre qualunque lunghezza abbiano visto:
| posizioni | perplexity a 64 | a 128 | a 256 |
|---|---|---|---|
| nessuna | 48,79 | 52,63 | 57,52 |
| embedding assoluti appresi | 38,63 | 108,47 | 181,94 |
| sinusoidi fisse | 42,96 | 95,26 | 152,25 |
| RoPE | 44,12 | 50,52 | 84,84 |
| ALiBi | 44,95 | 43,51 | 42,49 |
Embedding assoluti appresi — un vettore per posizione, aggiunto al token — vincono alla lunghezza di training e poi precipitano, perché la posizione 100 non è mai stata in un batch e il suo embedding è ancora il vettore casuale con cui ha iniziato. Sinusoidi, la scelta originale, sono calcolate invece che apprese, da seni e coseni a frequenze spaziate geometricamente; il paper del 2017 sperava che questo extrapolasse, e la tabella dice di no — la funzione è definita alla posizione 200, ma il modello non ha mai imparato a leggerla lì. RoPE9 non aggiunge nulla e invece ruota query e key di un angolo proporzionale alla posizione, in sezioni bidimensionali; poiché ruotare allo stesso modo entrambi i lati di un prodotto scalare lo lascia invariato, lo score finisce per dipendere solo da , quindi la posizione diventa relativa gratis e non c’è tabella che possa finire. Degrada, ma degrada. ALiBi10 è il risultato più semplice e più strano qui: una penalità lineare sullo score proporzionale alla distanza, con una pendenza diversa per head. La sua perplexity migliora quando la window cresce oltre la lunghezza di training, da 44,95 a 42,49, perché la penalità è definita a qualsiasi distanza e ogni head continua a fare ciò per cui è stata addestrata.
La lezione sopravvive alla tabella: un’architettura che non può rappresentare qualcosa è un problema diverso da una che non ha mai imparato quell’intervallo, ed è il secondo a mordere. È anche il meccanismo dietro ogni annuncio «abbiamo esteso il context a 128K»: quasi sempre sono riscalature di una codifica rotary, ed è per questo che il Capitolo 16 dice che il limite di context si sposta invece di scomparire.
Dropout è ereditato allo stesso modo: appare sui pesi di attention dopo la softmax, sull’output di ogni sub-layer prima dell’aggiunta residuale e sulla somma degli embedding, facendo esattamente ciò che il Capitolo 6 ha descritto. Nei grandi run di pretraining è spesso impostato a zero, perché un modello che vede ogni token una volta non è nella posizione di fare overfit.
Quanto costa
Link alla sezione: Quanto costaDue tensori nel layer hanno forma , dove è il numero di token: gli score e i pesi dopo la softmax. Tutto il resto — ogni proiezione, l’intero MLP — è lineare in .
Un layer di attention, larghezza 512, 8 heads, batch di uno, float32, su una GPU da laptop. Leggi le due colonne in millisecondi solo per i loro rapporti: sono wall clock su una scheda laptop da 8 GB che fa throttling da 1.785 MHz a meno di 300 MHz quando si scalda, quindi una run a freddo dello stesso codice torna sette-dieci volte più veloce e una run con la macchina occupata ancora più lenta. Le colonne in megabyte sono conteggi di byte dell’allocator e non si muovono.
tokens ms total ms x4 ms projections attn matrix MB peak MB MB x4
128 2.246 - 1.324 0.5 14.6 -
256 2.855 1.27 2.113 2.0 19.2 1.31
512 5.761 2.02 3.105 8.0 34.4 1.79
1024 16.414 2.85 4.008 32.0 89.1 2.59
2048 51.573 3.14 9.989 128.0 296.1 3.32
4096 225.432 4.37 20.176 512.0 1100.1 3.72
8192 832.838 3.69 40.106 2048.0 4300.1 3.91
16384 OUT OF MEMORY 8192.0
fitted exponent (log-log slope, last four rows): time ~ n^1.91 memory ~ n^1.87Le colonne x4 sono il rapporto rispetto alla riga sopra, e un raddoppio di converge esattamente a 4 sia per tempo sia per memoria — 3,91 all’ultimo passo contro un 4 teorico. La colonna delle proiezioni è il controllo: da 4,0 ms a 1024 token a 40,1 ms a 8192, un fattore dieci per un fattore otto. Lineare, come promesso.
Poi l’ultima riga. Un layer di attention, una sequenza, nessun modello intorno, esaurisce la memoria su una GPU da 8 GB a 16.384 token — la matrice degli score da sola sarebbe 8 GB, essendo 8 heads per 16.384 per 16.384 per 4 byte. Non il modello; un tensore intermedio in un layer.
Questo è il fatto fisico sotto tre capitoli successivi. È il motivo per cui una context window ha un limite, che il Capitolo 16 trasforma in un prezzo. È il motivo per cui esiste FlashAttention, che calcola lo stesso risultato in tile senza mai memorizzare la matrice — un’ottimizzazione di memoria prima che di velocità.11 Ed è l’aritmetica dietro il prezzo di un prompt lungo, che il Capitolo 24 paga in un loop di agent — una questione separata dall’altra scoperta di quel capitolo, cioè che un modello usa anche un context lungo peggio, cosa che misura e si rifiuta di imputare a questa formula.
Mostra dettagli
Le due varianti che riducono la cache, nominate qui e pagate nel Capitolo 13.
La generazione mette in cache le key e i value dei token già processati — una key e un value per token, per head per layer. Multi-query attention12 mantiene proiezioni di query ma una sola proiezione di key e value condivisa da tutte le heads, dividendo quella cache per . Grouped-query attention13 interpola: le heads sono raggruppate, ogni gruppo condivide una key e un value, quindi è attention ordinaria e è multi-query. Quasi ogni modello open dal 2023 la usa con 4 o 8 gruppi. Nessuna delle due esiste per qualità; entrambe esistono per la dimensione di quella cache, e il Capitolo 13 fa l’aritmetica che la trasforma in «quale modello entra nella tua GPU».
Due forme, e la dimensione di una
Link alla sezione: Due forme, e la dimensione di unaIl paper del 2017 descrive un encoder-decoder: uno stack legge la sorgente con attention non mascherata, un secondo genera il target causalmente, e un terzo tipo di attention in mezzo fa incontrare le query del decoder con le key dell’encoder. È giusto per la traduzione, dove input e output sono due sequenze.
A vincere è stata la metà decoder-only — uno stack, causale dappertutto, input e output nella stessa sequenza — e il motivo non è l’eleganza. «Prevedi il prossimo token» funziona su qualsiasi testo, quindi il training set è internet invece di un corpus parallelo, e tutto diventa quell’unico compito: una traduzione è un documento che contiene sorgente e poi target, una domanda e la sua risposta sono un documento, una conversazione con una tool call in mezzo è un documento. Il Capitolo 11 parla di come viene fabbricato quest’ultimo. Gli encoder non sono scomparsi — uno vede tutto l’input in una volta, che è ciò che vuoi quando il lavoro è rappresentare un testo invece di continuarlo, ed è per questo che gli embedding di retrieval del Capitolo 19 vengono da encoder e non dal modello che chatta.
Definito il block, la dimensione del modello è aritmetica. Per block, con larghezza e un’espansione per quattro: per con bias su tutti e quattro, come li ha GPT-2 — la tabella sopra lascia il bias fuori da tre di essi, quindi 2.304 in meno per block a ; per l’MLP; per due LayerNorm — , più una tabella di token da e, per le posizioni assolute, . Per la forma di GPT-2 small — , 12 block, un vocabolario di 50.257, un context di 1024, l’output layer che condivide i pesi dell’embedding:
token embeddings 50,257 x 768 = 38,597,376
position embeddings 1,024 x 768 = 786,432
one block 7,087,872
12 blocks 85,054,464
final LayerNorm 2 x 768 = 1,536
total (weights tied) 124,439,808Che è la dimensione pubblicata di quel modello. La formula non è un’approssimazione; è il modello. Nota anche che quasi un terzo di un modello piccolo è la tabella di embedding, motivo per cui la dimensione del vocabolario è una decisione architetturale e non di preprocessing — il trade-off impostato dal Capitolo 7.
Cosa guarda davvero una head
Link alla sezione: Cosa guarda davvero una headLa perplexity è un numero su un corpus. Cosa faccia una head è un’altra domanda, e un modello addestrato su un megabyte di Shakespeare è lo strumento sbagliato: la cosa onesta da dire sulla mappa di attention di un modello da 500.000 parametri è che perlopiù non è interpretabile. Quindi: un linguaggio in cui la domanda ha una risposta giusta.
L’illustrazione classica è the animal did not cross the street because it was too tired, dove it è l’animale, contro …because it was too wet, dove una parola sposta il referente alla strada. Questi sono schemi di Winograd14 — coppie di frasi identiche tranne che per una parola, dove quella parola decide a cosa si riferisce un pronome.
Sono anche risolvibili barando, che è la parte che i tutorial saltano. Se i due candidati sono un animale e un luogo, tired e wet identificano il referente per categoria, e un modello che sa solo quali parole sono presenti risponde bene senza sapere nulla dell’ordine. Misurato su quella versione del compito, con coppie animale/luogo tenute da parte:
uniform causal average held-out referent accuracy 100.0 %
one transformer block held-out referent accuracy 91.7 %Il bag of words batte il transformer. Qualunque dimostrazione costruita su quella frase non prova nulla sull’attention.
Quindi chiudi il buco: estrai entrambi i candidati da un unico pool di sedici nomi, ciascuno dei quali può apparire in ciascuno slot, e dividi gli aggettivi per ruolo invece che per categoria — quattro rendono it ciò che attraversa (tired, scared, slow, weak), quattro lo rendono ciò che viene attraversato (wet, wide, busy, steep).
the {x} did not cross the {y} because it was too {adj} , so the {ref} waited .Allena come un ordinary next-token predictor, assegna uno score a una posizione — la parola dopo so the — e costruisci il set tenuto da parte da coppie di nomi il cui ordine invertito era nel training, così qualunque cosa sappia quali due nomi sono presenti ma non quale sia venuto prima deve rispondere al contrario.
| modello | parametri | tenuto da parte | nomina l’altro nome |
|---|---|---|---|
| solo il token corrente | 5.796 | 5,2 % | 5,2 % |
| media causale uniforme | 5.796 | 27,9 % | 50,0 % |
| una head di attention appresa | 18.084 | 35,4 % | 64,6 % |
| quattro heads | 22.244 | 75,0 % | 15,6 % |
| un transformer block | 55.716 | 92,7 % | 4,2 % |
| due transformer block | 105.508 | 100,0 % | 0,0 % |
La probabilità casuale tra i due nomi presenti è 50 %. La media uniforme arriva al 27,9 % e risponde con il nome sbagliato della coppia esattamente metà delle volte — la firma di qualcosa che sa quali parole ci sono e nulla del loro ordine, come previsto dal test di shuffle tre sezioni fa.
Ora la mappa: l’attention alla posizione che deve nominare il referente, mediata sulle quattro heads di ogni block, per le due frasi che differiscono per una parola. Una media uniforme metterebbe 0,067 su ciascuno dei quindici token visibili.
the animal did not cross the street because it was too tired , so the animal waited .
blk 1 the:0.00 animal:0.70 did:0.00 not:0.00 cross:0.00 the:0.00 street:0.06
because:0.00 it:0.00 was:0.00 too:0.00 tired:0.00 ,:0.05 so:0.00 the:0.19
blk 2 the:0.00 animal:0.00 did:0.00 not:0.00 cross:0.00 the:0.00 street:0.00
because:0.00 it:0.00 was:0.00 too:0.00 tired:1.00 ,:0.00 so:0.00 the:0.00
the animal did not cross the street because it was too wet , so the street waited .
blk 1 the:0.00 animal:0.70 did:0.00 not:0.00 cross:0.00 the:0.00 street:0.06
because:0.00 it:0.00 was:0.00 too:0.00 wet:0.00 ,:0.05 so:0.00 the:0.19
blk 2 the:0.00 animal:0.00 did:0.00 not:0.00 cross:0.03 the:0.00 street:0.49
because:0.00 it:0.00 was:0.00 too:0.20 wet:0.03 ,:0.00 so:0.00 the:0.25Il Block 1 è identico in entrambe le frasi — 0,70 sul primo nome, qualunque sia l’aggettivo. Non è un fallimento ma una prova: nel primo layer la query a una posizione è una funzione del token e dell’indice propri di quella posizione, e the alla posizione 14 è lo stesso token in entrambe le frasi. Una head di primo layer non può condizionarsi su una parola che non ha ancora recuperato. Quindi il block 1 fa l’unica cosa utile a sua disposizione e trascina avanti il primo nome.
Il Block 2 è dove le frasi si separano, e la stessa riga su tutti e otto gli aggettivi mostra la regola trovata dal modello:
| aggettivo | block 2 su animal | su street | sull’aggettivo | risposta |
|---|---|---|---|---|
| tired, scared, slow, weak | 0,000 | 0,000 | 1,000 | animal |
| wet, wide, busy, steep | 0,000 | 0,491 | 0,00–0,03 | street |
Per un aggettivo da soggetto che attraversa, il secondo block mette tutto il proprio peso sull’aggettivo, perché la risposta è già nel residual stream — il block 1 l’ha messa lì — e tutto ciò di cui ha bisogno è conferma. Per un aggettivo da cosa attraversata, va invece a recuperare l’altro nome. Questo è un circuito a due hop: una head sposta avanti un candidato, una head in un layer successivo legge un token che decide se tenerlo. La composizione tra layer è il meccanismo, ed è per questo che un block ha raggiunto il 92,7 % e due hanno raggiunto il 100 %.
È anche la forma del circuito meglio documentato nei modelli reali. Induction heads — una previous-token head che alimenta una head nel layer successivo che completa il pattern [A][B] … [A] → [B] — sono ciò che il lavoro di interpretabilità di Anthropic identifica dietro una larga parte dell’in-context learning, e si formano in un momento identificabile durante il pretraining. Questo capitolo non tenta quell’analisi: è delegata, con entrambi i paper nei riferimenti, perché leggere circuiti da un modello reale è un campo di ricerca e non una sezione.
Infine, l’implementazione. Le trenta righe sopra, con i pesi copiati da quelli di PyTorch:
ours vs nn.MultiheadAttention max |diff| = 1.7881393432617188e-07
ours vs F.scaled_dot_product_attention max |diff| = 1.7881393432617188e-07su output la cui magnitudine media è 0,159: la stessa aritmetica in un ordine diverso, a precisione float32.
Dove si va da qui
Link alla sezione: Dove si va da quiHai l’architettura da cui è costruito ogni modello nel resto di questo corso, ed è più piccola della sua reputazione: una media pesata i cui pesi sono appresi, un MLP per-position che contiene due terzi dei parametri, due normalizzazioni e due addizioni, impilate.
Ciò che non hai è un modello che sappia qualcosa, e impilare non lo risolverà da solo. Due block su questo corpus raggiungono una perplexity di training di 14,49 e una perplexity di validazione di 40,57, contro 18,77 e 38,07 di un block — più capacità, meglio su ciò che ha visto, peggio su ciò che non ha visto, cioè la tabella del Capitolo 6 con dentro un transformer. La distanza tra questo modello e quelli con cui parlano i Capitoli 14-30 non è architetturale. È lo stesso block, ripetuto più volte, su molto più testo.
Il che lo rende un problema di contabilità, e la contabilità è più strana di quanto sembri. Quanto testo, e da dove lo prende qualcuno? Quanta aritmetica, e come la stimi prima che il denaro sia speso? Dato un budget fisso, è meglio rendere il modello più grande o mostrargli più dati — ed esiste una risposta corretta, o solo una moda? Il Capitolo 10 risponde a tutte e tre per misurazione, e mette un prezzo alla forma utile più economica della domanda: quanto costa, oggi, addestrare da zero un modello come GPT-2?
Fonti e metodo
Link alla sezione: Fonti e metodoTre spiegazioni di questo materiale sono migliori di questa in ciò per cui esistono, e questo capitolo è scritto per essere letto accanto a esse. The Illustrated Transformer di Jay Alammar è la migliore immagine del flusso di dati mai disegnata. The Annotated Transformer di Harvard NLP è il paper del 2017 con codice eseguibile intercalato riga per riga. Let’s build GPT: from scratch, in code, spelled out di Andrej Karpathy costruisce lo stesso modello dal vivo in due ore, e la scala di ablation sopra è la stessa spina dorsale misurata su un corpus diverso. Per la domanda di interpretabilità che questo capitolo sfiora soltanto, le fonti primarie sono Elhage et al., A Mathematical Framework for Transformer Circuits (2021) e Olsson et al., In-context Learning and Induction Heads (2022), entrambi dal gruppo di interpretabilità di Anthropic.
Riferimenti
Link alla sezione: Riferimenti-
Hochreiter, S. e Schmidhuber, J. Long Short-Term Memory. Neural Computation 9(8), pp. 1735–1780 (1997). ↩
-
Sutskever, I., Vinyals, O. e Le, Q. V. Sequence to Sequence Learning with Neural Networks. arXiv:1409.3215 (2014). L’encoder-decoder il cui singolo vettore di context è il collo di bottiglia. ↩
-
Bahdanau, D., Cho, K. e Bengio, Y. Neural Machine Translation by Jointly Learning to Align and Translate. arXiv:1409.0473 (2014). Attention, tre anni prima del transformer. ↩
-
La perplexity è l’esponenziale della cross-entropy media per token, dal Capitolo 8. Ogni numero qui usa lo stesso tokenizer e lo stesso split di validazione, che è l’unica condizione in cui due perplexity possono essere confrontate. ↩
-
Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, Ł. e Polosukhin, I. Attention Is All You Need. arXiv:1706.03762 (2017). La sezione 3.2.1 è l’unica frase su che questo capitolo passa una sezione a misurare. ↩
-
Shazeer, N., Mirhoseini, A., Maziarz, K., Davis, A., Le, Q., Hinton, G. e Dean, J. Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer. arXiv:1701.06538 (2017). ↩
-
Ba, J. L., Kiros, J. R. e Hinton, G. E. Layer Normalization. arXiv:1607.06450 (2016). Introdotta e misurata nel Capitolo 6; usata qui senza modifiche. ↩
-
Xiong, R., Yang, Y., He, D., Zheng, K., Zheng, S., Xing, C., Zhang, H., Lan, Y., Wang, L. e Liu, T.-Y. On Layer Normalization in the Transformer Architecture. arXiv:2002.04745 (2020). L’analisi del gradiente dietro pre-norm e l’argomento secondo cui il warmup è un sintomo. ↩
-
Su, J., Lu, Y., Pan, S., Murtadha, A., Wen, B. e Liu, Y. RoFormer: Enhanced Transformer with Rotary Position Embedding. arXiv:2104.09864 (2021). ↩
-
Press, O., Smith, N. A. e Lewis, M. Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation. arXiv:2108.12409 (2021). Il risultato di extrapolazione riprodotto sopra. ↩
-
Dao, T., Fu, D. Y., Ermon, S., Rudra, A. e Ré, C. FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness. arXiv:2205.14135 (2022). ↩
-
Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). ↩
-
Ainslie, J., Lee-Thorp, J., de Jong, M., Zemlyanskiy, Y., Lebrón, F. e Sanghai, S. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). ↩
-
Levesque, H. J., Davis, E. e Morgenstern, L. The Winograd Schema Challenge. KR (2012). La costruzione dietro la frase animal / street che usa ogni tutorial sull’attention. ↩