Aller au contenu
9/30Chapitre 9 sur 30

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 tt, 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 t1t-1 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 pas

De 1997 à environ 2017, le résumé était un état récurrent : garder un vecteur h\mathbf{h} et le mettre à jour à chaque token, ht=f(ht1,xt)\mathbf{h}_t = f(\mathbf{h}_{t-1}, \mathbf{x}_t). 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 TT étapes multiplie TT jacobiennes, donc le gradient disparaît ou explose — la maladie que Chapter 5 a mesurée à l’intérieur d’un seul nœud tanh\tanh. 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 : ht\mathbf{h}_t a besoin de ht1\mathbf{h}_{t-1}, 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.

La fonction la plus évidente qui prend un nombre variable de vecteurs et renvoie un vecteur est la moyenne :

ct=1ti=1txi\mathbf{c}_t = \frac{1}{t}\sum_{i=1}^{t} \mathbf{x}_i

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 à 10310^{-3} avec un batch de 64. La perplexité est mesurée sur la partition tenue à l’écart.4

modèleparamètresperplexité de validation
le token courant seulement, aucun contexte263,16859.71
plus la moyenne uniforme de tout ce qui le précède263,168248.07
plus des embeddings de position appris279,552245.93
moyenne uniforme ajoutée au token au lieu de le remplacer263,16860.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 :

order.pyPYTHON
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())
TEXT
2.9802322387695312e-08

Bruit 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 softmax

Faire 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 :

mechanics.pyPYTHON
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 again
TEXT
loop 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.167

Trois composants nommés d’un transformer sont maintenant à l’écran. Le triangle est le mask causal, imposé par l’objectif : si la position tt pouvait voir la position t+1t{+}1, 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 à -\infty les envoie exactement à zéro et normalise ce qui reste, donc masquer et normaliser sont une seule opération. (Utilisez -\infty, pas -1e9 : c’est la valeur que le masking signifie, elle survit à un cast en float16 comme -\infty, 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 ?

Ils ne peuvent pas être de simples paramètres. Une matrice T×TT \times T 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 tt à la position ii 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 ii pour la position tt comme xtxi\mathbf{x}_t \cdot \mathbf{x}_i 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, qt=Wqxt\mathbf{q}_t = W_q\mathbf{x}_t, la query ; et ce qu’elle offre pour être trouvée par, ki=Wkxi\mathbf{k}_i = W_k\mathbf{x}_i, la key. Scorer qtki\mathbf{q}_t \cdot \mathbf{k}_i et la symétrie disparaît, parce que WqWkW_q \neq W_k : un token peut annoncer une chose et en chercher une autre.

Une chose reste incorrecte. La somme pondérée portait sur les xi\mathbf{x}_i 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, vi=Wvxi\mathbf{v}_i = W_v\mathbf{x}_i, la value, et on somme celles-là.

La formule n’est plus que de la tenue de comptes :

Attention(Q,K,V)=softmax ⁣(QKdk+M)V\mathrm{Attention}(Q, K, V) = \mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}} + M\right)V

avec MM le mask causal, zéro sur et sous la diagonale et -\infty au-dessus. En code, cela fait trente lignes, dont vingt sont des shapes :

attention.pyPYTHON
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éfend

Presque toutes les explications de dk\sqrt{d_k} 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 q\mathbf{q} et k\mathbf{k} sont indépendantes, de moyenne nulle et de variance un, chaque produit qjkjq_j k_j a une variance un, et les variances de choses indépendantes s’additionnent :

Var(qk)=j=1dkVar(qjkj)=dk\mathrm{Var}(\mathbf{q}\cdot\mathbf{k}) = \sum_{j=1}^{d_k}\mathrm{Var}(q_j k_j) = d_k

Les scores ont donc un écart-type dk\sqrt{d_k}. Mesuré sur vingt mille paires aléatoires :

TEXT
     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.000

Pourquoi 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 :

dkd_kplus grand poids, non diviséentropietokens effectifsplus grand poids, diviséentropietokens effectifs
40.2052.94419.00.0813.75842.9
160.4381.6925.40.0753.84946.9
640.4890.8742.40.0853.67339.4
2560.99990.00071.00.1433.54734.7
10241.00000.00001.00.1323.64438.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, à dk=256d_k = 256, 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 tanh\tanh. Un softmax engagé sur une entrée n’a presque plus de dérivée : la diagonale de sa jacobienne est wi(1wi)w_i(1-w_i), zéro aux deux extrêmes. Sur deux mille lignes aléatoires :

dkd_kiwi(1wi)\sum_i w_i(1-w_i) non divisédivisélignes saturées (plus grand poids au-dessus de 0.99)
40.84270.95680.2 % → 0.0 %
640.29400.960917.9 % → 0.0 %
2560.14060.960949.1 % → 0.0 %
10240.06810.961170.4 % → 0.0 %

À dk=1024d_k = 1024, 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 headnon divisédivisé par dk\sqrt{d_k}divisé par dkd_k
quatre heads, dk=32d_k = 3237.2938.0737.89
une head, dk=128d_k = 12848.5146.1045.99
une head, dk=256d_k = 25665.3747.53
une head, dk=512d_k = 51267.0649.15
une head, dk=1024d_k = 102476.6959.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.

À dk=32d_k = 32, 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 :

dkd_kécart-type du score à l’initialisationaprès 1500 étapes, non diviséaprès 1500 étapes, divisélignes saturées, non divisédivisé
25610.49121.672.1391.9 %0.8 %
51215.13836.852.6698.7 %1.3 %
102421.155147.463.4499.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 dkd_k » 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 parle

Une 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 dmodel/hd_{\text{model}}/h, on concatène, et on mélange avec une autre matrice WoW_o : 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èleparamètresperplexité de validation
moyenne uniforme, ajoutée279,55260.45
une attention head, remplaçant le token328,70455.47
une attention head, ajoutée328,70446.10
quatre heads au lieu d’une345,21643.21
plus le réseau feed-forward476,92839.87
plus LayerNorm — le bloc complet477,69638.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 :

largeurheadsattentionfeed-forwardtotal par bloc
128465,664 (33.2 %)131,712 (66.6 %)197,888
768122,360,064 (33.3 %)4,722,432 (66.6 %)7,085,568
40963267,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 d×dd \times d 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.

LayerNorm 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é.

block.pyPYTHON
class Block(nn.Module):
    def forward(self, x):
        x = x + self.att(self.ln1(x))     
        x = x + self.ff(self.ln2(x))      
        return x

Regardez 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 3×1033 \times 10^{-3} :

TEXT
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.88

Post-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.

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 tt ou ii 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 :

positionsperplexité à 64à 128à 256
aucune48.7952.6357.52
embeddings absolus appris38.63108.47181.94
sinusoïdes fixes42.9695.26152.25
RoPE44.1250.5284.84
ALiBi44.9543.5142.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 tit - i, 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.

Deux tenseurs dans la couche ont la shape n×nn \times n, où nn 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 nn.

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.

TEXT
  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.87

Les colonnes x4 sont le ratio par rapport à la ligne du dessus, et un doublement de nn 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 hh projections de query mais une seule projection de key et de value partagée par toutes les heads, divisant ce cache par hh. Grouped-query attention13 interpole : les heads sont groupées, chaque groupe partageant une key et une value, donc g=hg = h est l’attention ordinaire et g=1g = 1 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 ».

L’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 dd et expansion par quatre : 4d2+4d4d^2 + 4d pour Wq,Wk,Wv,WoW_q, W_k, W_v, W_o 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 à d=768d = 768 ; 8d2+5d8d^2 + 5d pour le MLP ; 4d4d pour deux LayerNorm — 12d2+13d12d^2 + 13d, plus une table de tokens de V×dV \times d et, pour les positions absolues, nctx×dn_{\text{ctx}} \times d. Pour la shape de GPT-2 small — d=768d = 768, 12 blocs, un vocabulaire de 50 257, un contexte de 1024, la couche de sortie partageant les poids d’embedding :

TEXT
  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,808

Ce 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.

La 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 :

TEXT
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).

TEXT
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èleparamètrestenu à l’écartnomme l’autre nom
token courant seulement5,7965.2 %5.2 %
moyenne causale uniforme5,79627.9 %50.0 %
une head d’attention apprise18,08435.4 %64.6 %
quatre heads22,24475.0 %15.6 %
un bloc transformer55,71692.7 %4.2 %
deux blocs transformer105,508100.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.

TEXT
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.25

Le 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 :

adjectifbloc 2 sur animalsur streetsur l’adjectifréponse
tired, scared, slow, weak0.0000.0001.000animal
wet, wide, busy, steep0.0000.4910.00–0.03street

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 :

TEXT
ours vs nn.MultiheadAttention           max |diff| = 1.7881393432617188e-07
ours vs F.scaled_dot_product_attention  max |diff| = 1.7881393432617188e-07

1.8×1071.8 \times 10^{-7} sur des sorties dont la magnitude moyenne est 0.159 : la même arithmétique dans un ordre différent, à la précision float32.

Vous 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 ?


Trois 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.

  1. Hochreiter, S. and Schmidhuber, J. Long Short-Term Memory. Neural Computation 9(8), pp. 1735–1780 (1997).

  2. 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.

  3. 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.

  4. 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.

  5. 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 dk\sqrt{d_k} que ce chapitre passe une section à mesurer.

  6. 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).

  7. 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.

  8. 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.

  9. Su, J., Lu, Y., Pan, S., Murtadha, A., Wen, B. and Liu, Y. RoFormer: Enhanced Transformer with Rotary Position Embedding. arXiv:2104.09864 (2021).

  10. 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.

  11. 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).

  12. Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019).

  13. 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).

  14. 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.

Prêt à laisser LIA choisir à votre place ?

Créez avec tous les modèles d'IA au même endroit — commencez gratuitement dès aujourd'hui.