Attention et bloc transformer, dérivés d’une moyenne
Partez de la moyenne du contexte, mesurez son échec, puis laissez la formule d’attention émerger de la réparation.
Dans cet article
Vous arrivez ici avec un tokenizer de Chapter 7, une table d’embedding de Chapter 8, et l’objectif qui va avec : étant donnés les tokens jusqu’ici, attribuer une probabilité au suivant.
Ce qui manque, c’est le milieu. Pour prédire le token , le modèle a besoin d’un vecteur qui résume tout ce qui le précède, et rien de ce que vous avez construit n’en produit un. L’embedding du token ne suffit pas — ce serait un modèle bigramme, incapable de savoir que la phrase a commencé par une question. Une concaténation de tous les embeddings précédents ne convient pas non plus : leur nombre change à chaque étape, et une matrice de poids fixe ne peut pas recevoir une entrée de longueur variable.
Donc : un vecteur de taille fixe, qui résume un nombre variable de vecteurs. C’est tout le problème, et attention est ce que vous obtenez en le résolvant de la manière la plus paresseuse possible, puis en réparant les deux choses qui cassent.
La réponse qu’avait le domaine, et pourquoi nous ne la construisons pas
Lien vers la section : La réponse qu’avait le domaine, et pourquoi nous ne la construisons pasDe 1997 à environ 2017, le résumé était un état récurrent : garder un vecteur et le mettre à jour à chaque token, . Taille fixe, entrée variable, exactement la bonne forme.
Il a échoué de trois manières, et l’architecture de ce chapitre répond aux trois. Backpropagation à travers étapes multiplie jacobiennes, donc le gradient disparaît ou explose — la maladie que Chapter 5 a mesurée à l’intérieur d’un seul nœud . Le LSTM1 a été conçu précisément contre cela et a fait passer la portée utilisable de dizaines d’étapes à des centaines, sans changer le fait que l’information du token 5 n’atteint le token 500 qu’en survivant à 495 mises à jour séquentielles. Toute la source devait tenir dans un seul vecteur : en traduction sequence-to-sequence2, un encodeur compresse l’entrée dans son état final. Bahdanau, Cho et Bengio ont nommé ce goulot d’étranglement et l’ont corrigé en 2014, trois ans avant le transformer, en permettant au décodeur de prendre une somme pondérée de tous les états de l’encodeur avec des poids qu’il calculait lui-même.3 Tout ce qui suit est cette idée, appliquée par une séquence à elle-même, avec la récurrence supprimée. Et la mise à jour est séquentielle par construction : a besoin de , et un GPU à dix mille cœurs ne peut rien faire de cela. L’architecture qui a gagné n’est pas manifestement plus intelligente ; c’est celle dont l’étape coûteuse est une multiplication matricielle.
L’autre biais inductif classique, la convolution — faire glisser un petit filtre sur toute l’entrée, de sorte qu’une caractéristique détectée n’importe où soit détectée partout — n’est pas construit ici non plus ; il est presque exactement adapté aux images et relève d’un cours de vision. Ni la récurrence ni la convolution ne réapparaissent après cette page, c’est pourquoi aucune des deux n’a droit à un chapitre : Chapter 1 promettait que les omissions seraient déclarées plutôt que silencieuses.
Le résumé le moins cher qui soit
Lien vers la section : Le résumé le moins cher qui soitLa fonction la plus évidente qui prend un nombre variable de vecteurs et renvoie un vecteur est la moyenne :
N’importe quel nombre d’entrées, une taille de sortie fixe, différentiable, gratuite. Une table d’embedding plus cette moyenne plus une couche linéaire vers le vocabulaire forment un modèle de langage complet en quinze lignes. Il est aussi terrible, et la manière dont il est terrible constitue toute la dérivation.
Le corpus ci-dessous est un mégaoctet de Shakespeare, 1 115 394 caractères, passé dans un tokenizer BPE au niveau des octets du type construit au Chapter 7 avec un vocabulaire de 1024 : 459 760 tokens à 2,43 caractères chacun, découpés en 90/10. Chaque modèle a une largeur de 128, voit 128 tokens, et s’entraîne pendant 3000 étapes d’AdamW à avec un batch de 64. La perplexité est mesurée sur la partition tenue à l’écart.4
| modèle | paramètres | perplexité de validation |
|---|---|---|
| le token courant seulement, aucun contexte | 263,168 | 59.71 |
| plus la moyenne uniforme de tout ce qui le précède | 263,168 | 248.07 |
| plus des embeddings de position appris | 279,552 | 245.93 |
| moyenne uniforme ajoutée au token au lieu de le remplacer | 263,168 | 60.45 |
Lisez deux fois la deuxième ligne. Faire la moyenne du contexte n’aide pas un peu ; cela rend le modèle quatre fois pire que s’il ignorait entièrement le contexte. Deux raisons, toutes deux démontrables plutôt qu’empiriques.
La moyenne ne voit pas l’ordre. L’addition est commutative, donc mélanger la fenêtre laisse le résumé inchangé — pas approximativement :
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-08Bruit en virgule flottante sur une somme réordonnée : les deux résumés sont le même vecteur. Un modèle dont la seule vue du contexte est une moyenne ne peut pas distinguer the dog bit the man de the man bit the dog. La troisième ligne prouve que cela ne se corrige pas en ajoutant des positions aux entrées — un embedding de position appris sur chaque token avant la moyenne a gagné 2,14 points sur 188. Les positions entrent dans la somme, et la somme les oublie.
Et la moyenne noie le présent. À la position 100, le token courant représente un centième du résumé. Il existe une correction bon marché que vous possédez déjà : garder le token et lui ajouter le résumé — une connexion résiduelle, venue de Chapter 6, et la quatrième ligne montre ce qu’elle fait. Une fois la dilution réparée, la moyenne uniforme n’apporte strictement rien : 60.45 contre une baseline à 59.71. Chaque token y est, pondéré également, et une pondération égale revient à n’avoir aucune information.
Le problème n’est pas la moyenne. Ce sont les poids.
La moyenne est une multiplication matricielle, et le mask est un softmax
Lien vers la section : La moyenne est une multiplication matricielle, et le mask est un softmaxFaire la moyenne sur un préfixe croissant ressemble à une boucle. C’est une seule multiplication par une matrice triangulaire inférieure dont les lignes somment à un — et c’est aussi, exactement, un 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.167Trois composants nommés d’un transformer sont maintenant à l’écran. Le triangle est le mask causal, imposé par l’objectif : si la position pouvait voir la position , la réponse serait dans l’entrée — la fuite que Chapter 6 vous disait d’auditer, sauf qu’elle est ici dans l’architecture. Le softmax est la manière dont le mask est implémenté : fixer les entrées interdites à les envoie exactement à zéro et normalise ce qui reste, donc masquer et normaliser sont une seule opération. (Utilisez , pas -1e9 : c’est la valeur que le masking signifie, elle survit à un cast en float16 comme , et elle vous épargne de décider si la constante choisie est assez grande pour l’intervalle dans lequel vous vous trouvez — ce qui est la boîte de virgule flottante du Chapter 2 posant une question à laquelle vous n’avez pas à répondre.) Et les scores sont le paramètre libre. La moyenne uniforme est ce que vous obtenez quand chaque score autorisé est le même nombre ; mettez n’importe quels nombres à cet endroit et le softmax les transforme en poids valides.
Le reste de ce chapitre tient en une question : d’où viennent ces nombres ?
Query, key, value
Lien vers la section : Query, key, valueIls ne peuvent pas être de simples paramètres. Une matrice apprise serait identique pour chaque phrase — elle pourrait encoder « regarder quatre tokens en arrière », mais jamais « regarder le nom auquel ce pronom fait référence ». Le poids reliant la position à la position doit dépendre de ce qui se trouve aux deux positions, parce que la pertinence est une relation, pas une propriété : le mot it n’est pas intrinsèquement pertinent, il est pertinent pour quelque chose.
La fonction la moins chère qui prend deux vecteurs et renvoie un nombre est le produit scalaire du Chapter 1. Scorer la position pour la position comme et le mécanisme fonctionne — mal, de deux manières qui forcent tout le reste. Le produit scalaire d’un vecteur avec lui-même est sa norme au carré, donc chaque token prêterait surtout attention à lui-même. Et la relation serait symétrique : si it attend fortement à animal, alors animal attend fortement à it, ce qui est faux pour le langage, où un adjectif a beaucoup plus besoin de son nom que le nom n’a besoin de l’adjectif.
Donnez donc à chaque token deux rôles, comme deux applications linéaires apprises de celui-ci : ce que cette position cherche, , la query ; et ce qu’elle offre pour être trouvée par, , la key. Scorer et la symétrie disparaît, parce que : un token peut annoncer une chose et en chercher une autre.
Une chose reste incorrecte. La somme pondérée portait sur les eux-mêmes, ce qui force la chose copiée à être la chose appariée. L’appariement veut les caractéristiques qui identifient un token ; la copie veut les caractéristiques utiles en aval. On apprend donc une troisième application, , la value, et on somme celles-là.
La formule n’est plus que de la tenue de comptes :
avec le mask causal, zéro sur et sous la diagonale et au-dessus. En code, cela fait trente lignes, dont vingt sont des shapes :
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 Scorer, masquer, normaliser, mélanger. Tout le reste est une projection.
La division par la racine carrée, et ce contre quoi elle défend
Lien vers la section : La division par la racine carrée, et ce contre quoi elle défendPresque toutes les explications de disent « pour empêcher le softmax de saturer », ce qui est vrai et n’explique rien. L’argument tient en deux lignes de variance du Chapter 2. Si les entrées de et sont indépendantes, de moyenne nulle et de variance un, chaque produit a une variance un, et les variances de choses indépendantes s’additionnent :
Les scores ont donc un écart-type . Mesuré sur vingt mille paires aléatoires :
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.000Pourquoi cela compte : le softmax est sensible à l’échelle d’une manière qu’une couche linéaire ne l’est pas. Doubler l’entrée d’une couche linéaire double sa sortie ; multiplier les scores par dix avant un softmax transforme un mélange doux en choix dur. Une ligne de 64 scores, avec et sans la division :
| plus grand poids, non divisé | entropie | tokens effectifs | plus grand poids, divisé | entropie | tokens effectifs | |
|---|---|---|---|---|---|---|
| 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 |
Les « tokens effectifs » sont l’exponentielle de l’entropie : le nombre de positions sur lesquelles la ligne fait réellement la moyenne. Sans division, à , une head fraîchement initialisée attend exactement à un token sur 64, choisi par rien d’autre que le tirage aléatoire.
C’est mauvais en forward et pire en backward, dans une forme que Chapter 5 a déjà mesurée sur un . Un softmax engagé sur une entrée n’a presque plus de dérivée : la diagonale de sa jacobienne est , zéro aux deux extrêmes. Sur deux mille lignes aléatoires :
| non divisé | divisé | lignes saturées (plus grand poids au-dessus de 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 % |
À , sept lignes sur dix sont gelées avant le début de l’entraînement, et une head qui commence gelée ne peut pas apprendre où regarder. Avec la division, la quantité reste plate à 0.96 à toutes les largeurs et rien ne sature.
Maintenant la partie que personne ne publie : cela change-t-il la perplexité finale ? Supprimez la division et entraînez, avec quatre largeurs de head :
| largeur de head | non divisé | divisé par | divisé par |
|---|---|---|---|
| quatre heads, | 37.29 | 38.07 | 37.89 |
| une head, | 48.51 | 46.10 | 45.99 |
| une head, | 65.37 | 47.53 | — |
| une head, | 67.06 | 49.15 | — |
| une head, | 76.69 | 59.17 | — |
Les deux premières lignes viennent du budget de 3000 étapes ci-dessus ; les trois dernières sont une exécution plus courte — 1500 étapes, batch de 32, une head, aucune normalisation avant les projections — avec les deux variantes sous des réglages identiques.
À , la division ne vaut rien et l’exécution sans elle est très légèrement devant. Ce n’est pas une licence pour la supprimer, parce qu’à 256 elle vaut 18 points de perplexité et à 1024 elle en vaut 17. Le mécanisme est visible dans les scores eux-mêmes :
| écart-type du score à l’initialisation | après 1500 étapes, non divisé | après 1500 étapes, divisé | lignes saturées, non divisé | divisé | |
|---|---|---|---|---|---|
| 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 divisée ne récupère pas. Elle s’emballe : l’écart-type de ses scores passe de 21 à l’initialisation à 5147, l’entropie d’attention tombe à zéro, et 99,9 % des lignes placent plus de 0,99 de leur poids sur un seul token. Une fois qu’une head est un sélecteur dur, son gradient est presque nul et rien ne la ramène, donc l’effondrement est stable. La head divisée reste à un écart-type de score de 3.44 après le même entraînement, ce qui est un mélange doux encore modifiable.
Vaswani et al. disent exactement cela et rien de plus — ils soupçonnent que les produits « deviennent grands en magnitude pour de grandes valeurs de » et divisent.5 Le mot grand porte la charge, et les tableaux disent où commence grand : rien à 32, tout à 256.
Plus d’un avis, et les deux tiers dont personne ne parle
Lien vers la section : Plus d’un avis, et les deux tiers dont personne ne parleUne head est une ligne de softmax par position, elle porte donc une seule réponse à « qu’est-ce qui est pertinent ici ». Prédire le mot après the dans the animal that crossed the wet street exige à la fois l’emplacement syntaxique, le sujet et le token précédent, et une seule distribution de probabilité ne peut pas être concentrée en trois endroits. On exécute donc plusieurs heads en parallèle, chacune de largeur , on concatène, et on mélange avec une autre matrice : vous avez partitionné la largeur, pas ajouté à celle-ci.
Attention fait aussi exactement une chose — elle déplace l’information entre positions. Chaque opération dans le code ci-dessus est linéaire le long de l’axe des caractéristiques, et Chapter 5 a prouvé ce qu’est une pile d’applications linéaires. Chaque bloc porte donc aussi un petit MLP appliqué à chaque position indépendamment, qui multiplie la largeur par quatre puis revient, avec un GELU au milieu. La division du travail mérite d’être mémorisée : attention mélange entre les positions, le réseau feed-forward calcule au sein d’une position.
L’échelle complète, chaque ligne ajoutant une pièce à la ligne au-dessus :
| modèle | paramètres | perplexité de validation |
|---|---|---|
| moyenne uniforme, ajoutée | 279,552 | 60.45 |
| une attention head, remplaçant le token | 328,704 | 55.47 |
| une attention head, ajoutée | 328,704 | 46.10 |
| quatre heads au lieu d’une | 345,216 | 43.21 |
| plus le réseau feed-forward | 476,928 | 39.87 |
| plus LayerNorm — le bloc complet | 477,696 | 38.07 |
Les poids appris battent les poids uniformes de 14 points de perplexité, ce qui est tout l’argument de ce chapitre en une ligne. Quatre heads achètent encore 3 points pour 16 512 paramètres supplémentaires. Et la même head vaut 9 points de plus quand elle est ajoutée que quand elle remplace : attention apporte de l’information, elle ne décide pas ce qu’est une position.
Maintenant, où se trouvent réellement les paramètres, ce qui surprend les personnes qui n’ont vu que le diagramme :
| largeur | heads | attention | feed-forward | total par bloc |
|---|---|---|---|---|
| 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 |
Deux tiers de chaque bloc transformer sont le réseau feed-forward, à toutes les échelles, parce que attention a quatre matrices et que le MLP a l’équivalent de huit. Quoi qu’un modèle sache, la plupart des paramètres qui le portent sont dans le MLP par position.
Résiduels et LayerNorm, hérités du Chapter 6
Lien vers la section : Résiduels et LayerNorm, hérités du Chapter 6LayerNorm a été construit et mesuré au Chapter 6, et ce chapitre l’utilise tel qu’il y a été laissé ; les connexions résiduelles y ont été nommées et ablatées, et sont construites ici. Les lignes « ajoutée, pas remplaçante » ci-dessus sont des connexions résiduelles, valant 188 points de perplexité pour la moyenne et 9 pour une head. LayerNorm7 normalise chaque exemple sur ses caractéristiques, et Chapter 6 a donné les raisons pour lesquelles elle, et non BatchNorm, a survécu ici — pas de dépendance au batch, pas de statistiques courantes, identique en entraînement et en inférence, indifférente à la longueur de séquence — chacune devenant une exigence quand vous générez un token à la fois pour un utilisateur, là où aboutit Chapter 13. Elle coûte 768 paramètres et achète 1,8 point de perplexité.
class Block(nn.Module):
def forward(self, x):
x = x + self.att(self.ln1(x))
x = x + self.ff(self.ln2(x))
return xRegardez où se situe la normalisation : à l’entrée de chaque sous-couche, avec le chemin résiduel de l’entrée à la sortie jamais normalisé. C’est le pre-norm. L’article de 2017 fait l’inverse, x = LayerNorm(x + Att(x)) — post-norm, qui place une LayerNorm sur le chemin résiduel lui-même.
Xiong et al. ont expliqué la différence par le gradient à l’initialisation, qui dans un réseau post-norm est mal mis à l’échelle avec la profondeur — la raison pour laquelle le transformer original avait besoin d’un warmup du taux d’apprentissage pour s’entraîner tout court.8 Douze blocs, 1000 étapes, taux d’apprentissage :
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 sans warmup est huit fois pire, et post-norm avec warmup correspond exactement à pre-norm. Le warmup n’est pas une bonne pratique générale ici ; c’est un patch pour un agencement précis de la normalisation, et déplacer la LayerNorm supprime son besoin. C’est pourquoi pratiquement tous les modèles depuis 2019 sont pre-norm, et pourquoi le diagramme de 2017 doit être lu comme de l’histoire plutôt que comme une spécification.
Où est un token ?
Lien vers la section : Où est un token ?Supprimez les embeddings de position et le modèle s’entraîne encore ; il ne peut simplement pas savoir où se trouve quoi que ce soit, et c’est une symétrie plutôt qu’un échec d’entraînement. Rien dans le score d’attention ne mentionne ou eux-mêmes, donc permuter l’entrée permute la sortie : self-attention est équivariante par permutation. C’est l’insensibilité de la moyenne à l’ordre sous un meilleur déguisement — le mask causal restaure un peu d’ordre, puisque chaque position voit un préfixe différent, mais au sein d’un préfixe tous les ordres se valent.
Quatre manières d’injecter la position, entraînées sur des fenêtres de 64 tokens et évaluées à 64, 128 et 256 — au-delà de toute longueur qu’elles ont vue :
| positions | perplexité à 64 | à 128 | à 256 |
|---|---|---|---|
| aucune | 48.79 | 52.63 | 57.52 |
| embeddings absolus appris | 38.63 | 108.47 | 181.94 |
| sinusoïdes fixes | 42.96 | 95.26 | 152.25 |
| RoPE | 44.12 | 50.52 | 84.84 |
| ALiBi | 44.95 | 43.51 | 42.49 |
Les embeddings absolus appris — un vecteur par position, ajouté au token — gagnent à la longueur d’entraînement puis tombent d’une falaise, parce que la position 100 n’a jamais été dans un batch et que son embedding est encore le vecteur aléatoire du départ. Les sinusoïdes, le choix original, sont calculées plutôt qu’apprises, à partir de sinus et cosinus à fréquences espacées géométriquement ; l’article de 2017 espérait que cela extrapolerait, et le tableau dit que non — la fonction est définie à la position 200, mais le modèle n’a jamais appris à la lire là. RoPE9 n’ajoute rien et fait pivoter à la place query et key d’un angle proportionnel à la position, par tranches bidimensionnelles ; comme faire pivoter les deux côtés d’un produit scalaire de manière égale le laisse inchangé, le score finit par ne dépendre que de , donc la position devient relative gratuitement et aucune table ne peut s’épuiser. Il se dégrade, mais il se dégrade. ALiBi10 est le résultat le plus simple et le plus étrange ici : une pénalité linéaire sur le score proportionnelle à la distance, avec une pente différente par head. Sa perplexité s’améliore quand la fenêtre dépasse la longueur d’entraînement, de 44.95 à 42.49, parce que la pénalité est définie à toute distance et que chaque head continue de faire ce pour quoi elle a été entraînée.
La leçon survit au tableau : une architecture qui ne peut pas représenter quelque chose est un problème différent d’une architecture qui n’a jamais appris cette plage, et c’est le second qui mord. C’est aussi la mécanique derrière chaque annonce « nous avons étendu le contexte à 128K » — il s’agit presque toujours de remises à l’échelle d’un encodage rotatif, et c’est pourquoi Chapter 16 dit que la limite de contexte se déplace plutôt qu’elle ne disparaît.
Dropout est hérité de la même manière : il apparaît sur les poids d’attention après le softmax, sur la sortie de chaque sous-couche avant l’addition résiduelle, et sur la somme d’embedding, faisant exactement ce que Chapter 6 décrivait. Dans les grands runs de pré-entraînement, il est souvent mis à zéro, parce qu’un modèle qui voit chaque token une seule fois n’est pas en position de surapprendre.
Ce que cela coûte
Lien vers la section : Ce que cela coûteDeux tenseurs dans la couche ont la shape , où est le nombre de tokens : les scores et les poids après le softmax. Tout le reste — chaque projection, tout le MLP — est linéaire en .
Une couche d’attention, largeur 512, 8 heads, batch de un, float32, sur un GPU d’ordinateur portable. Lisez les deux colonnes en millisecondes uniquement pour leurs ratios : c’est le temps réel sur une carte portable de 8 Go qui throttle de 1 785 MHz à moins de 300 MHz quand elle chauffe, donc une exécution à froid du même code revient sept à dix fois plus vite et une exécution chargée encore plus lentement. Les colonnes en mégaoctets sont des décomptes d’octets de l’allocateur et ne bougent pas.
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.87Les colonnes x4 sont le ratio par rapport à la ligne du dessus, et un doublement de converge vers exactement 4 pour le temps comme pour la mémoire — 3.91 à la dernière étape contre un 4 théorique. La colonne des projections est le contrôle : de 4.0 ms à 1024 tokens à 40.1 ms à 8192, un facteur dix pour un facteur huit. Linéaire, comme annoncé.
Puis la dernière ligne. Une couche d’attention, une séquence, aucun modèle autour, épuise la mémoire sur un GPU de 8 Go à 16 384 tokens — la matrice de scores seule ferait 8 Go, étant 8 heads fois 16 384 fois 16 384 fois 4 octets. Pas le modèle ; un tenseur intermédiaire dans une couche.
C’est le fait physique sous trois chapitres ultérieurs. C’est pourquoi une context window a une limite tout court, que Chapter 16 transforme en prix. C’est pourquoi FlashAttention existe, calculant le même résultat par tuiles sans jamais stocker la matrice — une optimisation mémoire avant d’être une optimisation de vitesse.11 Et c’est l’arithmétique derrière le prix d’un long prompt, que Chapter 24 paie dans une boucle d’agent — une question distincte de l’autre résultat de ce chapitre, à savoir qu’un modèle utilise aussi moins bien un long contexte, ce qu’il mesure et refuse d’imputer à cette formule.
Afficher les détails
Les deux variantes qui réduisent le cache, nommées ici et payées au Chapter 13.
La génération met en cache les keys et values des tokens déjà traités — une key et une value par token, par head et par couche. Multi-query attention12 garde projections de query mais une seule projection de key et de value partagée par toutes les heads, divisant ce cache par . Grouped-query attention13 interpole : les heads sont groupées, chaque groupe partageant une key et une value, donc est l’attention ordinaire et est multi-query. Presque tous les modèles ouverts depuis 2023 l’utilisent avec 4 ou 8 groupes. Aucun des deux n’existe pour la qualité ; les deux existent pour la taille de ce cache, et Chapter 13 fait l’arithmétique qui le transforme en « quel modèle tient dans votre GPU ».
Deux shapes, et la taille de l’une
Lien vers la section : Deux shapes, et la taille de l’uneL’article de 2017 décrit un encoder-decoder : une pile lisant la source avec une attention non masquée, une seconde générant la cible causalement, et un troisième type d’attention au milieu où les queries du décodeur rencontrent les keys de l’encodeur. C’est juste pour la traduction, où l’entrée et la sortie sont deux séquences.
Ce qui a gagné, c’est la moitié decoder-only — une seule pile, causale partout, entrée et sortie dans la même séquence — et la raison n’est pas l’élégance. « Prédire le prochain token » fonctionne sur n’importe quel texte, donc le jeu d’entraînement est Internet plutôt qu’un corpus parallèle, et tout devient cette seule tâche : une traduction est un document contenant source puis cible, une question et sa réponse sont un document, une conversation avec un tool call au milieu est un document. Chapter 11 explique comment le dernier est fabriqué. Les encodeurs n’ont pas disparu — l’un voit toute l’entrée à la fois, ce qui est ce que vous voulez quand le travail consiste à représenter un texte plutôt qu’à le continuer, et c’est pourquoi les retrieval embeddings du Chapter 19 viennent d’encodeurs et non du modèle qui discute.
Une fois le bloc défini, la taille du modèle est de l’arithmétique. Par bloc, avec largeur et expansion par quatre : pour avec des biais sur les quatre, comme GPT-2 les a — le tableau ci-dessus omet le biais de trois d’entre eux, d’où 2 304 de moins par bloc à ; pour le MLP ; pour deux LayerNorm — , plus une table de tokens de et, pour les positions absolues, . Pour la shape de GPT-2 small — , 12 blocs, un vocabulaire de 50 257, un contexte de 1024, la couche de sortie partageant les poids d’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,808Ce qui est la taille publiée de ce modèle. La formule n’est pas une approximation ; c’est le modèle. Notez aussi que près d’un tiers d’un petit modèle est la table d’embedding, ce qui explique pourquoi la taille du vocabulaire est une décision architecturale et non de prétraitement — le compromis mis en place au Chapter 7.
Ce qu’une head regarde réellement
Lien vers la section : Ce qu’une head regarde réellementLa perplexité est un nombre à propos d’un corpus. Ce que fait une head est une autre question, et un modèle entraîné sur un mégaoctet de Shakespeare est le mauvais instrument pour y répondre : la chose honnête à dire de la carte d’attention d’un modèle de 500 000 paramètres est qu’elle est surtout non interprétable. Donc : une langue où la question a une bonne réponse.
L’illustration classique est the animal did not cross the street because it was too tired, où it est l’animal, contre …because it was too wet, où un mot déplace le référent vers la rue. Ce sont des schémas de Winograd14 — des paires de phrases identiques sauf pour un mot, où ce mot décide à quoi un pronom fait référence.
Ils sont aussi solubles par triche, ce que les tutoriels passent sous silence. Si les deux candidats sont un animal et un lieu, tired et wet identifient le référent par catégorie, et un modèle qui sait seulement quels mots sont présents obtient la bonne réponse sans rien savoir de l’ordre. Mesuré sur cette version de la tâche, avec des paires animal/lieu tenues à l’écart :
uniform causal average held-out referent accuracy 100.0 %
one transformer block held-out referent accuracy 91.7 %Le sac de mots bat le transformer. Toute démonstration bâtie sur cette phrase ne prouve rien sur attention.
Fermez donc la brèche : tirez les deux candidats dans un seul pool de seize noms, chacun pouvant apparaître dans l’un ou l’autre emplacement, et divisez les adjectifs par rôle plutôt que par catégorie — quatre faisant de it celui qui traverse (tired, scared, slow, weak), quatre en faisant ce qui est traversé (wet, wide, busy, steep).
the {x} did not cross the {y} because it was too {adj} , so the {ref} waited .Entraînez comme un prédicteur ordinaire du next-token, scorez une position — le mot après so the — et construisez l’ensemble tenu à l’écart à partir de paires de noms dont l’ordre inversé était dans l’entraînement, de sorte que tout ce qui sait quels deux noms sont présents mais pas lequel vient en premier doit répondre à l’envers.
| modèle | paramètres | tenu à l’écart | nomme l’autre nom |
|---|---|---|---|
| token courant seulement | 5,796 | 5.2 % | 5.2 % |
| moyenne causale uniforme | 5,796 | 27.9 % | 50.0 % |
| une head d’attention apprise | 18,084 | 35.4 % | 64.6 % |
| quatre heads | 22,244 | 75.0 % | 15.6 % |
| un bloc transformer | 55,716 | 92.7 % | 4.2 % |
| deux blocs transformer | 105,508 | 100.0 % | 0.0 % |
Le hasard parmi les deux noms présents vaut 50 %. La moyenne uniforme arrive à 27.9 % et répond avec le mauvais nom de la paire exactement la moitié du temps — la signature de quelque chose qui sait quels mots sont là et rien de leur ordre, comme le test de mélange l’avait prédit trois sections plus tôt.
Maintenant la carte : l’attention à la position qui doit nommer le référent, moyennée sur les quatre heads de chaque bloc, pour les deux phrases qui diffèrent d’un seul mot. Une moyenne uniforme placerait 0.067 sur chacun des quinze tokens visibles.
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.25Le bloc 1 est identique dans les deux phrases — 0.70 sur le premier nom, quel que soit l’adjectif. Ce n’est pas un échec mais une preuve : dans la première couche, la query à une position est une fonction du token et de l’indice propres à cette position, et the à la position 14 est le même token dans les deux phrases. Une head de première couche ne peut pas se conditionner sur un mot qu’elle n’a pas encore récupéré. Donc le bloc 1 fait la seule chose utile à sa disposition et tire le premier nom vers l’avant.
Le bloc 2 est là où les phrases se séparent, et la même ligne sur les huit adjectifs montre la règle que le modèle a trouvée :
| adjectif | bloc 2 sur animal | sur street | sur l’adjectif | réponse |
|---|---|---|---|---|
| tired, scared, slow, weak | 0.000 | 0.000 | 1.000 | animal |
| wet, wide, busy, steep | 0.000 | 0.491 | 0.00–0.03 | street |
Pour un adjectif de traversant, le second bloc consacre tout son poids à l’adjectif, parce que la réponse est déjà dans le flux résiduel — le bloc 1 l’y a placée — et il n’a besoin que de confirmation. Pour un adjectif de traversé, il va récupérer l’autre nom. C’est un circuit à deux sauts : une head déplace un candidat vers l’avant, une head d’une couche ultérieure lit un token qui décide s’il faut le garder. La composition entre couches est le mécanisme, et c’est pourquoi un bloc a atteint 92.7 % et deux ont atteint 100 %.
C’est aussi la forme du circuit le mieux documenté dans les vrais modèles. Les induction heads — une previous-token head alimentant une head dans la couche suivante qui complète le motif [A][B] … [A] → [B] — sont ce que le travail d’interprétabilité d’Anthropic identifie derrière une grande partie de l’in-context learning, et elles se forment à un moment identifiable pendant le pré-entraînement. Ce chapitre ne tente pas cette analyse : elle est déléguée, avec les deux articles dans les références, parce que lire des circuits dans un vrai modèle est un domaine de recherche et non une section.
Enfin, l’implémentation. Les trente lignes ci-dessus, avec leurs poids copiés depuis ceux de PyTorch :
ours vs nn.MultiheadAttention max |diff| = 1.7881393432617188e-07
ours vs F.scaled_dot_product_attention max |diff| = 1.7881393432617188e-07sur des sorties dont la magnitude moyenne est 0.159 : la même arithmétique dans un ordre différent, à la précision float32.
Où cela mène ensuite
Lien vers la section : Où cela mène ensuiteVous avez l’architecture dont chaque modèle du reste de ce cours est construit, et elle est plus petite que sa réputation : une moyenne pondérée dont les poids sont appris, un MLP par position portant deux tiers des paramètres, deux normalisations et deux additions, empilées.
Ce que vous n’avez pas, c’est un modèle qui sache quoi que ce soit, et empiler ne corrigera pas cela à lui seul. Deux blocs sur ce corpus atteignent une perplexité d’entraînement de 14.49 et une perplexité de validation de 40.57, contre 18.77 et 38.07 pour un bloc — plus de capacité, mieux sur ce qu’il a vu, pire sur ce qu’il n’a pas vu, ce qui est le tableau du Chapter 6 avec un transformer dedans. La distance entre ce modèle et ceux auxquels les Chapters 14 à 30 parlent n’est pas architecturale. C’est le même bloc, répété plus de fois, sur immensément plus de texte.
Ce qui en fait un problème de comptabilité, et la comptabilité est plus étrange qu’elle n’en a l’air. Combien de texte, et où quelqu’un l’obtient-il ? Combien d’arithmétique, et comment l’estimer avant que l’argent ne soit dépensé ? Avec un budget fixe, vaut-il mieux agrandir le modèle ou lui montrer plus de données — et existe-t-il une bonne réponse, ou seulement une mode ? Chapter 10 répond aux trois par la mesure, et met un prix sur la forme utile la moins chère de la question : que coûte aujourd’hui l’entraînement d’un modèle comme GPT-2 à partir de rien ?
Sources et méthode
Lien vers la section : Sources et méthodeTrois explications de ce matériau sont meilleures que celle-ci dans ce pour quoi elles sont faites, et ce chapitre est écrit pour être lu avec elles. The Illustrated Transformer de Jay Alammar est la meilleure image du flux de données jamais dessinée. The Annotated Transformer de Harvard NLP est l’article de 2017 avec du code exécutable intercalé ligne par ligne. Let’s build GPT: from scratch, in code, spelled out d’Andrej Karpathy construit le même modèle en direct en deux heures, et l’échelle d’ablations ci-dessus est la même colonne vertébrale mesurée sur un autre corpus. Pour la question d’interprétabilité que ce chapitre ne fait qu’effleurer, les sources primaires sont Elhage et al., A Mathematical Framework for Transformer Circuits (2021) et Olsson et al., In-context Learning and Induction Heads (2022), tous deux issus du groupe d’interprétabilité d’Anthropic.
Références
Lien vers la section : Références-
Hochreiter, S. and Schmidhuber, J. Long Short-Term Memory. Neural Computation 9(8), pp. 1735–1780 (1997). ↩
-
Sutskever, I., Vinyals, O. and Le, Q. V. Sequence to Sequence Learning with Neural Networks. arXiv:1409.3215 (2014). L’encoder-decoder dont le vecteur de contexte unique est le goulot d’étranglement. ↩
-
Bahdanau, D., Cho, K. and Bengio, Y. Neural Machine Translation by Jointly Learning to Align and Translate. arXiv:1409.0473 (2014). Attention, trois ans avant le transformer. ↩
-
La perplexité est l’exponentielle de la cross-entropy moyenne par token, depuis Chapter 8. Chaque nombre ici utilise le même tokenizer et la même partition de validation, ce qui est la seule condition sous laquelle deux perplexités peuvent être comparées. ↩
-
Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, Ł. and Polosukhin, I. Attention Is All You Need. arXiv:1706.03762 (2017). La section 3.2.1 est la seule phrase sur que ce chapitre passe une section à mesurer. ↩
-
Shazeer, N., Mirhoseini, A., Maziarz, K., Davis, A., Le, Q., Hinton, G. and Dean, J. Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer. arXiv:1701.06538 (2017). ↩
-
Ba, J. L., Kiros, J. R. and Hinton, G. E. Layer Normalization. arXiv:1607.06450 (2016). Introduite et mesurée au Chapter 6 ; utilisée ici sans changement. ↩
-
Xiong, R., Yang, Y., He, D., Zheng, K., Zheng, S., Xing, C., Zhang, H., Lan, Y., Wang, L. and Liu, T.-Y. On Layer Normalization in the Transformer Architecture. arXiv:2002.04745 (2020). L’analyse du gradient derrière pre-norm, et l’argument selon lequel le warmup est un symptôme. ↩
-
Su, J., Lu, Y., Pan, S., Murtadha, A., Wen, B. and Liu, Y. RoFormer: Enhanced Transformer with Rotary Position Embedding. arXiv:2104.09864 (2021). ↩
-
Press, O., Smith, N. A. and Lewis, M. Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation. arXiv:2108.12409 (2021). Le résultat d’extrapolation reproduit ci-dessus. ↩
-
Dao, T., Fu, D. Y., Ermon, S., Rudra, A. and 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. and Sanghai, S. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). ↩
-
Levesque, H. J., Davis, E. and Morgenstern, L. The Winograd Schema Challenge. KR (2012). La construction derrière la phrase animal / street que tous les tutoriels sur attention utilisent. ↩